import { execAsync } from "ags/process"; import { timeout } from "ags/time"; import { monitorFile, readFile } from "ags/file"; import GObject, { register, getter } from "ags/gobject"; import AstalIO from "gi://AstalIO"; import Gio from "gi://Gio?version=2.0"; import GLib from "gi://GLib?version=2.0"; import { decoder, encoder } from "./utils"; 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; }; }; @register({ GTypeName: "Wallpaper" }) class Wallpaper extends GObject.Object { private static instance: Wallpaper; #wallpaper: (string|undefined); #splash: boolean = true; #monitor: Gio.FileMonitor; #hyprpaperFile: Gio.File; #wallpapersPath: string; #ignoreWatch: boolean = false; @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) public get wallpaper() { return this.#wallpaper ?? ""; } public set wallpaper(newValue: string) { this.setWallpaper(newValue); } public get wallpapersPath() { return this.#wallpapersPath; } 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(); }); let tmeout: (AstalIO.Time|undefined) = undefined; this.#monitor = monitorFile(this.#hyprpaperFile.get_path()!, (_, event) => { if(event !== Gio.FileMonitorEvent.CHANGED && event !== Gio.FileMonitorEvent.CREATED && event !== Gio.FileMonitorEvent.MOVED_IN) return; if(tmeout) return; else tmeout = timeout(1500, () => tmeout = undefined); if(this.#ignoreWatch) { this.#ignoreWatch = false; return; } const [ loaded, text ] = this.#hyprpaperFile.load_contents(null); if(!loaded) console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!"); const content = decoder.decode(text); if(content) { let setWall: boolean = true; for(const line of content.split('\n')) { if(line.trim().startsWith('#')) continue; const lineSplit = line.split('='); const key = lineSplit[0].trim(), value = lineSplit.filter((_, i) => i !== 0).join('=').trim(); switch(key) { case "splash": this.splash = (/(yes|true|on|enable|enabled|1).*/.test(value)) ? true : false; break; case "wallpaper": if(this.#wallpaper !== value && setWall) { this.setWallpaper(value, false); setWall = false; // wallpaper already set } break; } } } }); } vfunc_dispose(): void { this.#monitor.cancel(); } public static getDefault(): Wallpaper { if(!this.instance) this.instance = new Wallpaper(); return this.instance; } private writeChanges(): void { this.#ignoreWatch = true; // tell monitor to ignore file replace this.#hyprpaperFile.replace_async(null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, GLib.PRIORITY_DEFAULT, null, (_, result) => { const res = this.#hyprpaperFile.replace_finish(result); if(res) { // success this.#ignoreWatch = true; // tell monitor to ignore this change res.write_bytes_async(encoder.encode(`# This file was automatically generated by color-shell preload = ${this.#wallpaper} splash = ${this.#splash} wallpaper = , ${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; } console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`); } ); } public getData(): WalData { const content = readFile(`${GLib.get_user_cache_dir()}/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: (string|undefined) = stdout.split('=')[1]?.trim(); if(!loaded) console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`); return loaded; }).catch((err: Gio.IOErrorEnum) => { console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`); return undefined; }); } public reloadColors(): void { execAsync(`wal -t --cols16 darken -i "${this.#wallpaper}"`).then(() => { console.log("Wallpaper: reloaded shell colors"); }).catch(r => { console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`); }); } public setWallpaper(path: string|Gio.File, write: boolean = true): void { execAsync("hyprctl hyprpaper unload all").then(() => execAsync(`hyprctl hyprpaper preload ${path}`).then(() => execAsync(`hyprctl hyprpaper wallpaper ${path}`).then(() => { this.#wallpaper = (typeof path === "string") ? path : path.get_path()!; this.reloadColors(); write && this.writeChanges(); }).catch(r => { console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`); }) ).catch(r => { console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`); }) ).catch(r => { console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`); }); } public async pickWallpaper(): Promise { return (await execAsync(`zenity --file-selection`).then(wall => { if(!wall.trim()) return undefined; this.setWallpaper(wall); return wall; }).catch(r => { console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`); return undefined; })); } }