✨ chore: restructure the project, make it not use the astal application stuff
now it's more organized and I have more control over the shell behaviour
This commit is contained in:
+321
@@ -0,0 +1,321 @@
|
||||
import { Astal } from "ags/gtk4";
|
||||
import { Bar } from "./window/Bar";
|
||||
import { variableToBoolean } from "./scripts/utils";
|
||||
import { OSD } from "./window/OSD";
|
||||
import { ControlCenter } from "./window/ControlCenter";
|
||||
import { FloatingNotifications } from "./window/FloatingNotifications";
|
||||
import { CenterWindow } from "./window/CenterWindow";
|
||||
import { LogoutMenu } from "./window/LogoutMenu";
|
||||
import { AppsWindow } from "./window/AppsWindow";
|
||||
import { Scope } from "/usr/share/ags/js/gnim/src/jsx/scope";
|
||||
import { Shell } from "./app";
|
||||
import GObject, { getter, register, signal } from "ags/gobject";
|
||||
|
||||
import AstalHyprland from "gi://AstalHyprland";
|
||||
|
||||
|
||||
export { Windows };
|
||||
|
||||
export type WindowInstance = { instance?: Astal.Window, connections: Array<number> };
|
||||
export type WindowData = {
|
||||
create: () => (Astal.Window | Array<Astal.Window>);
|
||||
instance?: WindowInstance | Array<WindowInstance>;
|
||||
status?: "open" | "closed";
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Windowing System
|
||||
* Possible actions: getting window states, close, open, toggle windows and
|
||||
* registering windows.
|
||||
* Also contains util functions to create dynamic windows, opening the window only on focused
|
||||
* monitor, or all available monitors!
|
||||
*/
|
||||
@register()
|
||||
class Windows extends GObject.Object {
|
||||
private static instance: (Windows | null);
|
||||
|
||||
#windows: Record<string, WindowData> = {
|
||||
"bar": { create: this.createWindowForMonitors(Bar) },
|
||||
"osd": { create: this.createWindowForFocusedMonitor(OSD), },
|
||||
"control-center": { create: this.createWindowForFocusedMonitor(ControlCenter), },
|
||||
"center-window": { create: this.createWindowForFocusedMonitor(CenterWindow), },
|
||||
"logout-menu": { create: this.createWindowForFocusedMonitor(LogoutMenu), },
|
||||
"floating-notifications": { create: this.createWindowForFocusedMonitor(FloatingNotifications), },
|
||||
"apps-window": { create: this.createWindowForFocusedMonitor(AppsWindow) }
|
||||
};
|
||||
|
||||
@signal(String) windowOpen(_name: string) {}
|
||||
@signal(String) windowClosed(_name: string) {}
|
||||
|
||||
@getter(Object)
|
||||
get windows(): object { return this.#windows; }
|
||||
|
||||
@getter(Array)
|
||||
get openWindows(): Array<string> {
|
||||
return Object.keys(this.#windows).filter((key) =>
|
||||
this.#windows[key].status === "open");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Listen to monitor events
|
||||
const hyprConnections = [
|
||||
AstalHyprland.get_default().connect("monitor-added", () =>
|
||||
this.reopen()),
|
||||
AstalHyprland.get_default().connect("monitor-removed", () =>
|
||||
AstalHyprland.get_default().get_monitors().length > 0 &&
|
||||
this.reopen())
|
||||
];
|
||||
|
||||
Shell.getDefault().scope.run(() => {
|
||||
// open windows with the "open" status on startup
|
||||
Object.keys(this.#windows).filter((key) =>
|
||||
this.#windows[key].status === "open"
|
||||
).forEach(name => {
|
||||
this.open(name, true);
|
||||
console.log(`Windows: opening window \`${name}\` on startup`);
|
||||
});
|
||||
});
|
||||
|
||||
Shell.getDefault().scope.onCleanup(() => {
|
||||
hyprConnections.forEach(id =>
|
||||
GObject.signal_handler_is_connected(AstalHyprland.get_default(), id) &&
|
||||
AstalHyprland.get_default().disconnect(id)
|
||||
);
|
||||
|
||||
this.openWindows.forEach(name => this.disconnectWindow(name));
|
||||
});
|
||||
}
|
||||
|
||||
private disconnectWindow(name: string) {
|
||||
if(!variableToBoolean(this.#windows[name]?.instance) || !this.#windows[name]) {
|
||||
console.error(`Windows: couldn't disconnect window's connections: either the window \`${name
|
||||
}\` doesn't exist in the windows list or it has no valid instance to disconnect signals from(not open)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const window = this.#windows[name].instance!;
|
||||
|
||||
if(Array.isArray(window)) {
|
||||
window.forEach(win => {
|
||||
this._disconnectAllFromInstance(win.instance!, win.connections!)
|
||||
win.connections = [];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._disconnectAllFromInstance(window.instance!, window.connections!);
|
||||
window.connections = [];
|
||||
}
|
||||
|
||||
private _disconnectAllFromInstance(instance: GObject.Object, connections: Array<number>): void {
|
||||
connections.forEach(id =>
|
||||
GObject.signal_handler_is_connected(instance, id) &&
|
||||
instance.disconnect(id));
|
||||
}
|
||||
|
||||
private hasConnections(name: string): boolean {
|
||||
if(!this.openWindows.includes(name))
|
||||
return false;
|
||||
|
||||
const window = this.#windows[name].instance;
|
||||
if(!window) return false;
|
||||
|
||||
if(Array.isArray(window)) {
|
||||
for(const win of window) {
|
||||
if(win.connections?.length > 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.connections?.length > 0;
|
||||
}
|
||||
|
||||
private connectWindow(name: string) {
|
||||
if(this.hasConnections(name)) {
|
||||
console.log(`Windows: skipped connecting window: \`${name}\`. Already connected`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.openWindows.includes(name)) {
|
||||
console.log(`Windows: \`${name}\` is not open, will not connect`);
|
||||
return;
|
||||
}
|
||||
|
||||
const window = this.#windows[name as keyof typeof this.windows];
|
||||
if(!window || !window.instance) {
|
||||
console.error(`Windows: Either \`${name}\` does not exist in the window list or it doesn't have a valid instance. Please add the window before trying to manage it here`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(Array.isArray(window.instance)) {
|
||||
window.instance.forEach(inst => inst.connections = [
|
||||
inst.instance!.connect("close-request", () => {
|
||||
this.disconnectWindow(name);
|
||||
delete window.instance;
|
||||
window.status = "closed";
|
||||
this.notify("open-windows");
|
||||
})
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.instance.connections = [
|
||||
window.instance.instance!.connect("close-request", () => {
|
||||
this.disconnectWindow(name);
|
||||
delete window.instance;
|
||||
window.status = "closed";
|
||||
this.notify("open-windows");
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
public static getDefault(): Windows {
|
||||
if(!this.instance)
|
||||
this.instance = new Windows();
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window instance for every monitor connected
|
||||
* @param create generates the window. use provided monitor number in the returned window
|
||||
* @returns a function that when called, returns Array<Astal.Window>
|
||||
* @throws Error if there are no monitors connected
|
||||
*/
|
||||
public createWindowForMonitors(create: (mon: number, scope: Scope) => GObject.Object|Astal.Window): (() => Array<Astal.Window>) {
|
||||
const monitors = AstalHyprland.get_default().get_monitors();
|
||||
|
||||
if(monitors.length < 1)
|
||||
throw new Error("Couldn't create window for monitors", {
|
||||
cause: "No monitors connected on Hyprland"
|
||||
});
|
||||
|
||||
// create a scope for every window generator function and dispose on ::close-request
|
||||
return () => monitors.map(mon => {
|
||||
const scope = new Scope(null);
|
||||
return scope.run(() => {
|
||||
const instance = create(mon.id, scope) as Astal.Window;
|
||||
const connection: number = instance.connect("close-request", () =>
|
||||
scope.dispose());
|
||||
|
||||
scope.onCleanup(() => instance.disconnect(connection));
|
||||
|
||||
return instance;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window instance for focused monitor only
|
||||
* @param create generates the window. use provided monitor number in the returned window
|
||||
* @returns a function that when called, returns a Astal.Window instance
|
||||
* @throws Error if no focused monitor is found
|
||||
*/
|
||||
public createWindowForFocusedMonitor(create: (mon: number, scope: Scope) => GObject.Object|Astal.Window): (() => Astal.Window) {
|
||||
const focusedMonitor = this.getFocusedMonitorId();
|
||||
|
||||
if(focusedMonitor == null)
|
||||
throw new Error("Couldn't create window for focused monitor", {
|
||||
cause: `No focused monitor found (${typeof focusedMonitor})`
|
||||
});
|
||||
|
||||
return () => {
|
||||
const scope = new Scope(null);
|
||||
return scope.run(() => {
|
||||
const instance = create(focusedMonitor, scope) as Astal.Window;
|
||||
const connection: number = instance.connect("close-request", () =>
|
||||
scope.dispose());
|
||||
|
||||
scope.onCleanup(() => instance.disconnect(connection));
|
||||
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addWindow(name: string, create: () => Astal.Window|Array<Astal.Window>): void {
|
||||
this.#windows[name] = { create };
|
||||
}
|
||||
|
||||
public hasWindow(name: string): boolean {
|
||||
return Boolean(this.windows?.[name as keyof typeof this.windows]);
|
||||
}
|
||||
|
||||
public getWindows(): Array<(() => (Astal.Window | Array<Astal.Window>))> {
|
||||
return Object.values(this.windows);
|
||||
}
|
||||
|
||||
public getFocusedMonitorId(): (number|null) {
|
||||
return AstalHyprland.get_default().get_monitors().filter(mon => mon.focused)?.[0]?.id ?? null;
|
||||
}
|
||||
|
||||
public isOpen(name: string): boolean {
|
||||
return this.openWindows.includes(name);
|
||||
}
|
||||
|
||||
public open(name: string, ignoreOpenStatus: boolean = false): void {
|
||||
if(this.isOpen(name) && !ignoreOpenStatus) return;
|
||||
|
||||
const window = this.#windows[name];
|
||||
if(!window) {
|
||||
console.error(`Windows: cannot open a window (\`${name}\`) that is not registered/doesn't exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#windows[name].status = "open";
|
||||
const windowInstance = window.create();
|
||||
|
||||
if(Array.isArray(windowInstance)) {
|
||||
window.instance = windowInstance.map(wi => {
|
||||
wi.show();
|
||||
return { instance: wi, connections: [] };
|
||||
});
|
||||
} else {
|
||||
window.instance = { instance: windowInstance, connections: [] };
|
||||
windowInstance.show();
|
||||
}
|
||||
|
||||
this.connectWindow(name);
|
||||
|
||||
this.emit("window-open", name);
|
||||
this.notify("open-windows");
|
||||
}
|
||||
|
||||
public close(name: string): void {
|
||||
if(!this.isOpen(name)) return;
|
||||
|
||||
this.disconnectWindow(name);
|
||||
const window = this.#windows[name];
|
||||
|
||||
if(Array.isArray(window.instance))
|
||||
window.instance.map(inst => inst.instance!.close());
|
||||
else
|
||||
window.instance!.instance!.close();
|
||||
|
||||
this.#windows[name].status = "closed";
|
||||
|
||||
this.emit("window-closed", name);
|
||||
this.notify("open-windows");
|
||||
}
|
||||
|
||||
public toggle(name: string): void {
|
||||
this.isOpen(name) ? this.close(name) : this.open(name);
|
||||
}
|
||||
|
||||
public closeAll(): void {
|
||||
this.openWindows.forEach(name => this.close(name));
|
||||
}
|
||||
|
||||
public reopen(): void {
|
||||
const openWins = [ ...this.openWindows ];
|
||||
this.closeAll();
|
||||
openWins.forEach(name => this.open(name));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user