Files
nix-flake/home/ags-config/windows.ts.bak
Zephrynis b2ae32a078 feat: make AGS colorshell configuration fully declarative
- Add complete colorshell v2.0.3 configuration to home/ags-config/
- Disable runner plugin and NightLight tile (incompatible with NixOS)
- Customize SCSS with full opacity (no transparency)
- Add dark pale blue color scheme in home/pywal-colors/
- Configure Papirus-Dark icon theme via home-manager
- Make ~/.config/ags/ immutable and managed by Nix store
- Auto-deploy pywal colors to ~/.cache/wal/colors.json

All AGS configuration is now reproducible and version controlled.
2025-11-04 21:36:38 +00:00

328 lines
11 KiB
TypeScript

import { Astal } from "ags/gtk4";
import { Shell } from "./app";
import GObject, { getter, register, signal } from "ags/gobject";
import { variableToBoolean } from "./modules/utils";
import { createRoot, getScope, onCleanup } from "ags";
import { Bar } from "./window/bar";
import { OSD } from "./window/osd";
import { ControlCenter } from "./window/control-center";
import { FloatingNotifications } from "./window/floating-notifications";
import { CenterWindow } from "./window/center-window";
import { LogoutMenu } from "./window/logout-menu";
// Removed AppsWindow (runner) - using Vicinae instead
// import { AppsWindow } from "./window/apps-window";
import AstalHyprland from "gi://AstalHyprland";
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({ GTypeName: "Windows" })
export class Windows extends GObject.Object {
private static instance: (Windows | null);
declare $signals: GObject.Object.SignalSignatures & {
"window-open": (name: string) => void;
"window-closed": (name: string) => void;
};
#scope!: ReturnType<typeof getScope>;
#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), }
// Removed apps-window (runner) - using Vicinae instead
// "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();
createRoot((dispose) => {
this.#scope = getScope();
Shell.getDefault().scope.onMount(dispose);
// 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())
];
onCleanup(() => {
hyprConnections.forEach(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: ReturnType<typeof getScope>) => 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 => {
return createRoot(() => {
const scope = getScope();
const instance = create(mon.id, scope) as Astal.Window;
const connection: number = instance.connect("close-request", () =>
scope.dispose());
this.#scope.onMount(scope.dispose);
scope.onCleanup(() =>
GObject.signal_handler_is_connected(instance, connection) &&
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: ReturnType<typeof getScope>) => 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 () => {
return createRoot((dispose) => {
const scope = getScope();
const instance = create(focusedMonitor, scope) as Astal.Window;
const connection = instance.connect("close-request", () => dispose());
this.#scope.onMount(dispose)
scope.onCleanup(() =>
GObject.signal_handler_is_connected(instance, connection) &&
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));
}
}