import { execAsync } from "ags/process"; import { readFile } from "ags/file"; 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 } from "./utils"; import { Notifications } from "./notifications"; import { generalConfig } from "../config"; import { createRoot, getScope, Scope } from "ags"; export { Wallpaper }; type WalData = { checksum: string; wallpaper: string; alpha: number; special: { background: string; foreground: string; cursor: string; }; colors: { color0: string; color1: string; color2: string; color3: string; color4: string; color5: string; color6: string; color7: string; color8: string; color9: string; color10: string; color11: string; color12: string; color13: string; color14: string; color15: string; }; }; export type WalMode = "darken"|"lighten"; /** wallpaper tiling mode */ export type WallpaperPositioning = "contain"|"tile"|"cover"; @register({ GTypeName: "Wallpaper" }) class Wallpaper extends GObject.Object { private static instance: Wallpaper; #wallpaper: (string|undefined); #scope!: Scope; #splash: boolean = true; #hyprpaperFile: Gio.File; #wallpapersPath: string; @getter(Boolean) public get splash() { return this.#splash; } public set splash(showSplash: boolean) { this.#splash = showSplash; this.notify("splash"); } /** current wallpaper's complete path. can be an empty string if undefined */ @getter(String) get wallpaper() { return this.#wallpaper ?? ""; } @setter(String) set wallpaper(newValue: string) { this.setWallpaper(newValue); } get wallpapersPath() { return this.#wallpapersPath; } @property(gtype(String)) positioning: WallpaperPositioning = "cover"; @property(gtype(String)) colorMode: WalMode = "darken"; constructor() { super(); this.#wallpapersPath = GLib.getenv("WALLPAPERS") ?? `${GLib.get_home_dir()}/wallpapers`; this.#hyprpaperFile = Gio.File.new_for_path(`${ GLib.get_user_config_dir()}/hypr/hyprpaper.conf`); this.getWallpaper().then((wall) => { if(wall?.trim()) this.#wallpaper = wall.trim(); }); createRoot(() => { this.#scope = getScope(); createSubscription( generalConfig.bindProperty("wallpaper.color_mode", "string"), () => { const mode = generalConfig.getProperty("wallpaper.color_mode", "string"); if(this.colorMode === mode) return; if(!mode || (mode !== "darken" && mode !== "lighten")) { Notifications.getDefault().sendNotification({ appName: "colorshell", summary: "Couldn't update color mode", body: "Invalid mode. Possible values are: \"darken\" or \"lighten\"" }); return; }; this.colorMode = mode as WalMode; this.reloadColors(); } ); createSubscription( generalConfig.bindProperty("wallpaper.positioning", "string"), () => { const positioning = generalConfig .getProperty("wallpaper.positioning", "string") as WallpaperPositioning; if(this.positioning === positioning) return; if(!positioning || (positioning !== "contain" && positioning !== "cover" && positioning !== "tile")) { Notifications.getDefault().sendNotification({ appName: "colorshell", summary: "Couldn't update wallpaper position", body: "Invalid position value. Possible values are: \"cover\"(default), \"contain\" or \"tile\"" }); return; } this.positioning = positioning; this.reloadWallpaper().catch((e: Error) => Notifications.getDefault().sendNotification({ appName: "colorshell", summary: "Couldn't update wallpaper position", body: `An error occurred while updating wallpaper's position: ${e.message}` }) ); } ); }); } vfunc_dispose(): void { this.#scope?.dispose(); } public static getDefault(): Wallpaper { if(!this.instance) this.instance = new Wallpaper(); return this.instance; } 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; } // Ensure directory exists const parentDir = this.#hyprpaperFile.get_parent(); if(parentDir && !parentDir.query_exists(null)) { parentDir.make_directory_with_parents(null); } // 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 cacheHome = GLib.getenv("XDG_CACHE_HOME") || `${GLib.get_home_dir()}/.cache`; const content = readFile(`${cacheHome}/wal/colors.json`); return JSON.parse(content) as WalData; } public async getWallpaper(): Promise { return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => { const loaded = stdout.split('=')[1]?.trim(); if(!loaded) console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`); return loaded; }).catch((e: Error) => { console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${e.message}`); return undefined; }); } public reloadColors(): void { 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 even with fallback backend. Stderr: ${e.message}`); }); } public async reloadWallpaper(write: boolean = true): Promise { if(this.wallpaper.trim() === "") return; 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 { path = typeof path === "string" ? path : path.peek_path()!; if(!GLib.file_test(path, GLib.FileTest.EXISTS)) { console.error("Wallpaper: file does not exist, skipped"); return; } this.#wallpaper = path; this.reloadWallpaper(write).catch((e: Error) => { console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${e.message}`); }); this.reloadColors(); } public async pickWallpaper(): Promise { return (await execAsync(`zenity --file-selection`).then(wall => { const trimmedWall = wall.trim(); if(!trimmedWall) return undefined; // 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; })); } }