From c8d6711466601a454caac972f9ad09cf1ea052ec Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 1 Dec 2025 06:45:17 -0400 Subject: [PATCH] safe --- resources/styles/_apps-window.scss | 30 +++++-- resources/styles/_runner.scss | 39 ++++---- src/app.ts | 2 +- src/cli/index.ts | 6 +- src/env.d.ts | 6 +- src/modules/arg-handler.ts | 6 +- src/modules/nightlight.ts | 16 +++- src/modules/wallpaper.ts | 140 ++++++++++++++++++++++------- src/runner/Runner.tsx | 39 +++++++- src/widget/Notification.tsx | 10 ++- src/window/apps-window/index.tsx | 17 ++-- src/window/bar/index.tsx | 8 +- src/window/bar/widgets/Apps.tsx | 4 +- src/window/bar/widgets/Clock.tsx | 4 +- src/window/bar/widgets/Media.tsx | 4 +- src/window/bar/widgets/Status.tsx | 4 +- src/windows.ts | 63 ++++++++++--- 17 files changed, 298 insertions(+), 100 deletions(-) diff --git a/resources/styles/_apps-window.scss b/resources/styles/_apps-window.scss index dc147bb..1af4d26 100644 --- a/resources/styles/_apps-window.scss +++ b/resources/styles/_apps-window.scss @@ -7,6 +7,9 @@ padding: 28px; background: colors.$bg-translucent; border-radius: $radius $radius 0 0; + max-width: 1600px; + margin-left: auto; + margin-right: auto; & entry { background: transparent; @@ -22,29 +25,46 @@ } & flowbox { - padding: 16px 24px; + padding: 16px 36px; & > flowboxchild { & > button { padding: 10px; border-radius: 24px; + background: linear-gradient( + 135deg, + color.change($color: colors.$bg-primary, $alpha: 0.9), + color.change($color: colors.$bg-secondary, $alpha: 0.7) + ); + border: 1px solid transparent; & image { -gtk-icon-size: 64px; } - & label { + & label.app-name { margin-top: 24px; + font-size: 16px; text-shadow: 1px 1px 1px rgba(colors.$bg-primary, .2); font-weight: 500; } } &:focus > button, - &:selected > button, - & > button:hover { - background-color: rgba($color: colors.$bg-secondary, $alpha: .5); + &:selected > button { + background: linear-gradient( + 135deg, + color.change($color: colors.$bg-secondary, $alpha: 0.95), + color.change($color: colors.$bg-tertiary, $alpha: 0.8) + ); + border-color: colors.$bg-tertiary; + box-shadow: 0 0 0 1px colors.$bg-tertiary; + + & label.app-name { + font-weight: 700; + } } + } } } diff --git a/resources/styles/_runner.scss b/resources/styles/_runner.scss index ada8559..33c0196 100644 --- a/resources/styles/_runner.scss +++ b/resources/styles/_runner.scss @@ -1,3 +1,4 @@ +@use "sass:color"; @use "./colors"; .runner .popup-window-container { @@ -29,16 +30,18 @@ } } - & scrolledwindow { - margin: 6px; - } - & list { + padding: 0 12px; & .result { padding: 10px; - background: colors.$bg-primary; - margin: 2px 0; - border-radius: 14px; + background: linear-gradient( + 135deg, + color.change($color: colors.$bg-primary, $alpha: 0.9), + color.change($color: colors.$bg-secondary, $alpha: 0.7) + ); + margin: 6px 0; + border-radius: 18px; + border: 1px solid transparent; & image { -gtk-icon-size: 28px; @@ -47,7 +50,7 @@ & .title { font-weight: 500; - font-size: 16px; + font-size: 17px; } & .description { @@ -57,17 +60,19 @@ } & > *:selected .result, - & > *:active .result, - & > *:hover .result { - background: colors.$bg-secondary; - } + & > *:active .result { + background: linear-gradient( + 135deg, + color.change($color: colors.$bg-secondary, $alpha: 0.95), + color.change($color: colors.$bg-tertiary, $alpha: 0.8) + ); - & > *:first-child { - margin-top: 12px; - } + border-color: colors.$bg-tertiary; + box-shadow: 0 0 0 1px colors.$bg-tertiary; - &:last-child { - margin-bottom: 0; + & .title { + font-weight: 700; + } } } } diff --git a/src/app.ts b/src/app.ts index c14185b..8e82e8a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,7 +67,7 @@ export class Shell extends Adw.Application { super({ applicationId: "io.github.retrozinndev.colorshell", flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE, - version: COLORSHELL_VERSION ?? "0.0.0-unknown", + version: (typeof COLORSHELL_VERSION !== "undefined" ? COLORSHELL_VERSION : "0.0.0-unknown"), }); setConsoleLogDomain("Colorshell"); diff --git a/src/cli/index.ts b/src/cli/index.ts index 5748bb5..13c2821 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -44,8 +44,8 @@ export namespace Cli { name: "version", alias: "v", help: "print the current colorshell version", - onCalled: () => `colorshell by retrozinndev, version ${COLORSHELL_VERSION - }${DEVEL ? "(devel)" : ""}` + onCalled: () => `colorshell by retrozinndev, version ${(typeof COLORSHELL_VERSION !== "undefined" ? COLORSHELL_VERSION : "unknown") + }${(typeof DEVEL !== "undefined" && DEVEL) ? "(devel)" : ""}` } ] }, @@ -110,7 +110,7 @@ export namespace Cli { initialized = true; rootScope = scope; - DEVEL && modules.push(devel); + (typeof DEVEL !== "undefined" && DEVEL) && modules.push(devel); scope.run(() => { if(communicationMethod instanceof Gio.SocketService) { diff --git a/src/env.d.ts b/src/env.d.ts index 9a476ac..4b977e2 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,7 +1,7 @@ declare const SRC: string -declare const DEVEL: boolean; -declare const GRESOURCES_FILE: string; -declare const COLORSHELL_VERSION: string; +declare const DEVEL: boolean | undefined; +declare const GRESOURCES_FILE: string | undefined; +declare const COLORSHELL_VERSION: string | undefined; declare module "inline:*" { const content: string diff --git a/src/modules/arg-handler.ts b/src/modules/arg-handler.ts index 7ba59db..1e1aefb 100644 --- a/src/modules/arg-handler.ts +++ b/src/modules/arg-handler.ts @@ -37,7 +37,7 @@ Audio Controls: Media Controls: media: manage colorshell's active player, see "media help". -${DEVEL ? ` +${(typeof DEVEL !== "undefined" && DEVEL) ? ` Development Tools: dev: tools to help debugging colorshell ` : ""} @@ -60,8 +60,8 @@ export function handleArguments(cmd: RemoteCaller, args: Array): number case "version": case "v": - cmd.print_literal(`colorshell by retrozinndev, version ${COLORSHELL_VERSION - }${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`); + cmd.print_literal(`colorshell by retrozinndev, version ${(typeof COLORSHELL_VERSION !== "undefined" ? COLORSHELL_VERSION : "unknown") + }${(typeof DEVEL !== "undefined" && DEVEL) ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`); return 0; case "dev": diff --git a/src/modules/nightlight.ts b/src/modules/nightlight.ts index 0fe41ce..77dfaf9 100644 --- a/src/modules/nightlight.ts +++ b/src/modules/nightlight.ts @@ -120,7 +120,13 @@ export class NightLight extends GObject.Object { } public applyIdentity(): void { - this.dispatch("identity"); + try { + this.dispatch("identity"); + } catch (e) { + // hyprsunset not available, skip + console.warn("Night Light: hyprsunset not available, cannot apply identity"); + return; + } if(!this.#identity) { this.#identity = true; @@ -133,7 +139,13 @@ export class NightLight extends GObject.Object { private dispatch(call: "identity"): string; private dispatch(call: "temperature"|"gamma"|"identity", val?: number): string { - return exec(`hyprctl hyprsunset ${call}${val != null ? ` ${val}` : ""}`); + try { + return exec(`hyprctl hyprsunset ${call}${val != null ? ` ${val}` : ""}`); + } catch (e) { + // hyprsunset not available, return empty string + console.warn(`Night Light: hyprsunset not available, skipping ${call} command`); + return ""; + } } private async dispatchAsync(call: "temperature", val: number): Promise; diff --git a/src/modules/wallpaper.ts b/src/modules/wallpaper.ts index b785d64..8eb570a 100644 --- a/src/modules/wallpaper.ts +++ b/src/modules/wallpaper.ts @@ -4,7 +4,7 @@ import GObject, { register, getter, gtype, property, setter } from "ags/gobject" import Gio from "gi://Gio?version=2.0"; import GLib from "gi://GLib?version=2.0"; -import { createSubscription, encoder } from "./utils"; +import { createSubscription } from "./utils"; import { Notifications } from "./notifications"; import { generalConfig } from "../config"; import { createRoot, getScope, Scope } from "ags"; @@ -161,37 +161,45 @@ class Wallpaper extends GObject.Object { return this.instance; } - private writeChanges(): void { - this.#hyprpaperFile.replace_async(null, false, - Gio.FileCreateFlags.REPLACE_DESTINATION, - GLib.PRIORITY_DEFAULT, null, (_, result) => { - const res = this.#hyprpaperFile.replace_finish(result); - if(!res) { - console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`); + private writeChanges(): Promise { + return new Promise((resolve, reject) => { + try { + const content = `# This file was automatically generated by colorshell + +preload = ${this.#wallpaper} +splash = ${this.#splash} +wallpaper = , ${this.positioning === "cover" ? "" : `${this.positioning}:`}${this.#wallpaper} +`; + + // Use synchronous file writing for reliability + const filePath = this.#hyprpaperFile.get_path(); + if(!filePath) { + reject(new Error("Could not get hyprpaper file path")); return; } - // success - res.write_bytes_async(encoder.encode(`\ - # This file was automatically generated by colorshell + // Ensure directory exists + const parentDir = this.#hyprpaperFile.get_parent(); + if(parentDir && !parentDir.query_exists(null)) { + parentDir.make_directory_with_parents(null); + } - preload = ${this.#wallpaper} - splash = ${this.#splash} - wallpaper = , ${this.positioning === "cover" ? "" : `${this.positioning}:`}${ - this.#wallpaper}`.split('\n').map(str => str.trimStart()).join('\n')), - GLib.PRIORITY_DEFAULT, null, (_, asyncRes) => { - if(_!.write_finish(asyncRes)) res.flush(null); - res.close(null); - } - ); - - return; + // Write file synchronously using GLib + const success = GLib.file_set_contents(filePath, content); + if(success) { + resolve(); + } else { + reject(new Error("Failed to write hyprpaper config file")); + } + } catch (e: any) { + reject(new Error(`Failed to write config file: ${e.message}`)); } - ); + }); } public getData(): WalData { - const content = readFile(`${GLib.getenv("XDG_CACHE_HOME")}/wal/colors.json`); + const cacheHome = GLib.getenv("XDG_CACHE_HOME") || `${GLib.get_home_dir()}/.cache`; + const content = readFile(`${cacheHome}/wal/colors.json`); return JSON.parse(content) as WalData; } @@ -210,10 +218,41 @@ class Wallpaper extends GObject.Object { } public reloadColors(): void { - execAsync(`wal -t --cols16 ${this.colorMode} -i "${this.#wallpaper}"`).then(() => { + const cacheHome = GLib.getenv("XDG_CACHE_HOME") || `${GLib.get_home_dir()}/.cache`; + const colorsKittyPath = `${cacheHome}/wal/colors-kitty.conf`; + const kittyConfigPath = `${GLib.get_user_config_dir()}/kitty/kitty.conf`; + + const runWal = (extraArgs: string = "") => + execAsync(`wal -t --cols16 ${this.colorMode} ${extraArgs} -i "${this.#wallpaper}"`); + + // First try default backend; if it fails (e.g. some images on aarch64), + // fall back to a more forgiving backend like "colorz". + runWal().catch((e: Error) => { + console.error(`Wallpaper: Couldn't update shell colors with default backend. Stderr: ${e.message}`); + console.log("Wallpaper: Falling back to pywal backend 'colorz'"); + return runWal("--backend colorz"); + }).then(() => { console.log("Wallpaper: reloaded shell colors"); + + // First, try to set colors on all existing kitty instances + execAsync(`kitty @ set-colors --all ${colorsKittyPath}`).then(() => { + console.log("Wallpaper: reloaded colors in existing kitty instances"); + }).catch((e: Error) => { + // It's okay if this fails (e.g., no kitty instances running) + console.log(`Wallpaper: Couldn't reload kitty colors in existing instances: ${e.message}`); + }); + + // Then, update the configured colors for future kitty instances + // This is critical - it tells kitty to use these colors for new windows + execAsync(`kitty @ set-colors --configured ${colorsKittyPath}`).then(() => { + console.log("Wallpaper: configured colors for future kitty instances"); + }).catch((e: Error) => { + // If no kitty instances are running, we can't set configured colors + // In this case, new instances should still pick up colors from the include directive + console.log(`Wallpaper: Couldn't set configured colors (new instances will use include directive): ${e.message}`); + }); }).catch((e: Error) => { - console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${e.message}`); + console.error(`Wallpaper: Couldn't update shell colors even with fallback backend. Stderr: ${e.message}`); }); } @@ -221,10 +260,39 @@ class Wallpaper extends GObject.Object { if(this.wallpaper.trim() === "") return; - await execAsync(`hyprctl hyprpaper reload \", ${ - this.positioning === "cover" ? "" : `${this.positioning}:` - }${this.wallpaper}\"`); - write && this.writeChanges(); + const wallpaperPath = this.#wallpaper.trim(); + + try { + // Write config file first if needed + if(write) { + await this.writeChanges(); + } + + // Unload all current wallpapers + await execAsync(`hyprctl hyprpaper unload all`).catch(() => { + // Ignore errors - this is usually fine + }); + + // Preload the new wallpaper + await execAsync(`hyprctl hyprpaper preload "${wallpaperPath}"`); + + // Set wallpaper on all monitors + await execAsync(`hyprctl hyprpaper wallpaper ", ${wallpaperPath}"`); + + // Note: We don't need to reload or restart hyprpaper here + // The preload and wallpaper commands should apply the change immediately + // The config file is written for persistence across hyprpaper restarts + + } catch (e: any) { + console.error(`Wallpaper: Error reloading wallpaper: ${e.message}`); + console.error(`Wallpaper: Stack trace: ${e.stack}`); + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Failed to set wallpaper", + body: `Error: ${e.message}` + }); + throw e; + } } public setWallpaper(path: string|Gio.File, write: boolean = true): void { @@ -244,10 +312,16 @@ class Wallpaper extends GObject.Object { public async pickWallpaper(): Promise { return (await execAsync(`zenity --file-selection`).then(wall => { - if(!wall.trim()) return undefined; + const trimmedWall = wall.trim(); + if(!trimmedWall) return undefined; - this.setWallpaper(wall); - return wall; + // Ensure path is absolute + const absolutePath = GLib.path_is_absolute(trimmedWall) + ? trimmedWall + : GLib.build_filenamev([GLib.get_current_dir(), trimmedWall]); + + this.setWallpaper(absolutePath); + return absolutePath; }).catch((e: Error) => { console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${e.message}`); return undefined; diff --git a/src/runner/Runner.tsx b/src/runner/Runner.tsx index 8fe0f35..5ccb108 100644 --- a/src/runner/Runner.tsx +++ b/src/runner/Runner.tsx @@ -276,11 +276,14 @@ export function openRunner(props: RunnerProps, placeholders?: Array): As props.height ??= 420; let clickTimeout: GLib.Source|undefined; + let lastMouseX: number|null = null; + let lastMouseY: number|null = null; + let lastKeyboardNavTime: number = 0; if(!instance) instance = Windows.getDefault().createWindowForFocusedMonitor((mon, root) => - { @@ -302,12 +305,14 @@ export function openRunner(props: RunnerProps, placeholders?: Array): As case Gdk.KEY_Up: selectPreviousItem(listbox); gtkEntry?.grab_focus(); + lastKeyboardNavTime = Date.now(); return; case Gdk.KEY_Right: case Gdk.KEY_Down: selectNextItem(listbox); gtkEntry?.grab_focus(); + lastKeyboardNavTime = Date.now(); return; } @@ -374,6 +379,36 @@ export function openRunner(props: RunnerProps, placeholders?: Array): As child.closeOnClick && Runner.close(); } + }} $={(self) => { + // Hover-based selection: only triggers when the mouse actually moves + const motion = Gtk.EventControllerMotion.new(); + self.add_controller(motion); + + motion.connect("motion", (_controller, x, y) => { + const now = Date.now(); + + // While user is actively navigating with keyboard, + // don't let hover steal selection + if(lastKeyboardNavTime && now - lastKeyboardNavTime < 200) + return; + + // First motion: just record pointer position, don't change selection + if(lastMouseX === null && lastMouseY === null) { + lastMouseX = x; + lastMouseY = y; + return; + } + + // Ignore synthetic events that don't actually move the pointer + if(x === lastMouseX && y === lastMouseY) + return; + + lastMouseX = x; + lastMouseY = y; + + const row = self.get_row_at_y(y); + row && self.select_row(row as Gtk.ListBoxRow); + }); }} /> diff --git a/src/widget/Notification.tsx b/src/widget/Notification.tsx index 644469c..52ec743 100644 --- a/src/widget/Notification.tsx +++ b/src/widget/Notification.tsx @@ -13,9 +13,15 @@ import { generalConfig } from "../config"; function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) { - const img = notif.image || notif.appIcon; + // AstalNotifd.Notification uses snake_case properties (app_icon, image), + // while our HistoryNotification uses camelCase (appIcon, image). + const anyNotif = notif as any; + const img: string | undefined = + anyNotif.image || + anyNotif.app_icon || + anyNotif.appIcon; - if(!img || !img.includes('/')) + if (typeof img !== "string" || !img.includes("/")) return undefined; return pathToURI(img); diff --git a/src/window/apps-window/index.tsx b/src/window/apps-window/index.tsx index 54a3d6b..62cd28a 100644 --- a/src/window/apps-window/index.tsx +++ b/src/window/apps-window/index.tsx @@ -24,9 +24,10 @@ export const AppsWindow = (mon: number) => { const [results, setResults] = createState(getApps() as Array); return { const entry = getPopupWindowContainer(self).get_first_child()! .get_first_child()!.get_first_child()! as Gtk.SearchEntry; @@ -36,8 +37,8 @@ export const AppsWindow = (mon: number) => { entry.grab_focus(); }}> - - { + + { setResults(getAstalApps().fuzzy_query(self.text.trim())); }} onStopSearch={(self) => (self.get_root() as Astal.Window)?.close()} /> @@ -46,9 +47,11 @@ export const AppsWindow = (mon: number) => { hscrollbarPolicy={Gtk.PolicyType.NEVER} overlayScrolling propagateNaturalHeight={false} hexpand vexpand> - - + + child.get_child()!.activate() // pass activation to button }> diff --git a/src/window/bar/index.tsx b/src/window/bar/index.tsx index b103004..0e97f37 100644 --- a/src/window/bar/index.tsx +++ b/src/window/bar/index.tsx @@ -21,7 +21,7 @@ export const Bar = (mon: number) => { halign={Gtk.Align.START} spacing={widgetSpacing} $type="start"> - + @@ -29,14 +29,14 @@ export const Bar = (mon: number) => { spacing={widgetSpacing} halign={Gtk.Align.CENTER} $type="center"> - - + + - + diff --git a/src/window/bar/widgets/Apps.tsx b/src/window/bar/widgets/Apps.tsx index fcf8cea..858fb3f 100644 --- a/src/window/bar/widgets/Apps.tsx +++ b/src/window/bar/widgets/Apps.tsx @@ -4,10 +4,10 @@ import { createBinding } from "ags"; import { tr } from "../../../i18n/intl"; -export const Apps = () => +export const Apps = ({ monitor }: { monitor: number }) => `apps ${Object.hasOwn(openWindows, "apps-window") ? "open" : ""}` )} iconName={"applications-other-symbolic"} halign={Gtk.Align.CENTER} hexpand tooltipText={tr("apps")} onClicked={() => - Windows.getDefault().open("apps-window")} + Windows.getDefault().open("apps-window", false, monitor)} />; diff --git a/src/window/bar/widgets/Clock.tsx b/src/window/bar/widgets/Clock.tsx index 583621b..25ea854 100644 --- a/src/window/bar/widgets/Clock.tsx +++ b/src/window/bar/widgets/Clock.tsx @@ -5,10 +5,10 @@ import { time } from "../../../modules/utils"; import { generalConfig } from "../../../config"; -export const Clock = () => +export const Clock = ({ monitor }: { monitor: number }) => `clock ${wins.includes("center-window") ? "open" : ""}`)} - onClicked={() => Windows.getDefault().toggle("center-window")} + onClicked={() => Windows.getDefault().toggle("center-window", monitor)} label={time((dt) => dt.format( generalConfig.getProperty("clock.date_format", "string")) ?? "An error occurred" diff --git a/src/window/bar/widgets/Media.tsx b/src/window/bar/widgets/Media.tsx index f65c3cc..4c7142c 100644 --- a/src/window/bar/widgets/Media.tsx +++ b/src/window/bar/widgets/Media.tsx @@ -11,7 +11,7 @@ import AstalMpris from "gi://AstalMpris"; import Pango from "gi://Pango?version=1.0"; -export const Media = () => +export const Media = ({ monitor }: { monitor: number }) => p.available)}> { self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL) @@ -42,7 +42,7 @@ export const Media = () => return true; }} /> - Windows.getDefault().toggle("center-window")} /> + Windows.getDefault().toggle("center-window", monitor)} /> { const revealer = self.get_widget()!.get_last_child() as Gtk.Revealer; revealer.set_reveal_child(true); diff --git a/src/window/bar/widgets/Status.tsx b/src/window/bar/widgets/Status.tsx index ceaab04..11b8dac 100644 --- a/src/window/bar/widgets/Status.tsx +++ b/src/window/bar/widgets/Status.tsx @@ -14,12 +14,12 @@ import AstalNetwork from "gi://AstalNetwork"; import AstalWp from "gi://AstalWp"; -export const Status = () => +export const Status = ({ monitor }: { monitor: number }) => openWins.includes("control-center") ? "open status" : "status" - )} onClicked={() => Windows.getDefault().toggle("control-center")}> + )} onClicked={() => Windows.getDefault().toggle("control-center", monitor)}> diff --git a/src/windows.ts b/src/windows.ts index 7e45d05..74c805b 100644 --- a/src/windows.ts +++ b/src/windows.ts @@ -17,8 +17,10 @@ import AstalHyprland from "gi://AstalHyprland"; export type WindowInstance = { instance?: Astal.Window, connections: Array }; export type WindowData = { create: () => (Astal.Window | Array); + createForMonitor?: (mon: number) => Astal.Window; instance?: WindowInstance | Array; status?: "open" | "closed"; + preferredMonitor?: number | null; }; @@ -41,12 +43,12 @@ export class Windows extends GObject.Object { #scope!: ReturnType; #windows: Record = { "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) } + "osd": { create: this.createWindowForFocusedMonitor(OSD), createForMonitor: this.createWindowForMonitor(OSD) }, + "control-center": { create: this.createWindowForFocusedMonitor(ControlCenter), createForMonitor: this.createWindowForMonitor(ControlCenter) }, + "center-window": { create: this.createWindowForFocusedMonitor(CenterWindow), createForMonitor: this.createWindowForMonitor(CenterWindow) }, + "logout-menu": { create: this.createWindowForFocusedMonitor(LogoutMenu), createForMonitor: this.createWindowForMonitor(LogoutMenu) }, + "floating-notifications": { create: this.createWindowForFocusedMonitor(FloatingNotifications), createForMonitor: this.createWindowForMonitor(FloatingNotifications) }, + "apps-window": { create: this.createWindowForFocusedMonitor(AppsWindow), createForMonitor: this.createWindowForMonitor(AppsWindow) } }; @signal(String) windowOpen(_name: string) {} @@ -244,6 +246,29 @@ export class Windows extends GObject.Object { } } + /** + * Creates a window instance for a specific monitor + * @param create generates the window. use provided monitor number in the returned window + * @returns a function that when called with a monitor ID, returns a Astal.Window instance + */ + public createWindowForMonitor(create: (mon: number, scope: ReturnType) => GObject.Object|Astal.Window): ((mon: number) => Astal.Window) { + return (mon: number) => { + return createRoot((dispose) => { + const scope = getScope(); + const instance = create(mon, 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): void { this.#windows[name] = { create }; } @@ -264,7 +289,7 @@ export class Windows extends GObject.Object { return this.openWindows.includes(name); } - public open(name: string, ignoreOpenStatus: boolean = false): void { + public open(name: string, ignoreOpenStatus: boolean = false, monitor?: number | null): void { if(this.isOpen(name) && !ignoreOpenStatus) return; const window = this.#windows[name]; @@ -273,8 +298,22 @@ export class Windows extends GObject.Object { return; } + // Store preferred monitor if provided + if(monitor !== undefined) { + window.preferredMonitor = monitor; + } + this.#windows[name].status = "open"; - const windowInstance = window.create(); + + // Use createForMonitor if monitor is specified and available, otherwise use default create + let windowInstance: Astal.Window | Array; + if(monitor !== null && monitor !== undefined && window.createForMonitor) { + windowInstance = window.createForMonitor(monitor); + } else if(window.preferredMonitor !== null && window.preferredMonitor !== undefined && window.createForMonitor) { + windowInstance = window.createForMonitor(window.preferredMonitor); + } else { + windowInstance = window.create(); + } if(Array.isArray(windowInstance)) { window.instance = windowInstance.map(wi => { @@ -309,8 +348,12 @@ export class Windows extends GObject.Object { this.notify("open-windows"); } - public toggle(name: string): void { - this.isOpen(name) ? this.close(name) : this.open(name); + public toggle(name: string, monitor?: number | null): void { + if(this.isOpen(name)) { + this.close(name); + } else { + this.open(name, false, monitor); + } } public closeAll(): void {