From b0956e24c5c28a20bc998ae1c3aefac218ad00ca Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Tue, 11 Nov 2025 16:37:02 -0300 Subject: [PATCH] :sparkles: feat(wallpaper, config): add config properties to configure wallpaper position and color generation modes --- src/config.ts | 45 ++++++--- src/modules/wallpaper.ts | 196 +++++++++++++++++++++------------------ 2 files changed, 141 insertions(+), 100 deletions(-) diff --git a/src/config.ts b/src/config.ts index 86535cf..9214363 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,47 +1,70 @@ import { Config } from "./modules/config"; +import { WallpaperPositioning, WalMode } from "./modules/wallpaper"; import GLib from "gi://GLib?version=2.0"; const generalConfigDefaults = { notifications: { + /** low-priority notification timeout + * @default 4000 */ timeout_low: 4000, + /** regular notification timeout + * @default 6000 */ timeout_normal: 6000, + /** critical/very important notification timeout + * @default 0 */ timeout_critical: 0, /** notification popup horizontal position. can be "left" or "right" - * @default "right" */ + * @default "right" */ position_h: "right", /** vertical notification popup position. can be "top" or "bottom" - * @default "top" */ - position_v: "top" + * @default "top" */ + position_v: "top", + /** dismisses notification popup when unhovered after hovering + * @default false */ + dismiss_after_unhover: false }, night_light: { - /** whether to save night light values to disk */ + /** whether to save night light/gamma filter values to disk when clicking + * on power/session actions(suspend, log out, power off, reboot) + * @default true */ save_on_shutdown: true }, + wallpaper: { + /** wallpaper positioning mode (hyprpaper) */ + positioning: "cover" satisfies WallpaperPositioning, + /** color generation mode. + * darken: picks darker colors; lighten: picks brighter colors */ + color_mode: "darken" satisfies WalMode + }, + workspaces: { /** breaks `enable_helper`, makes all workspaces show their respective ID - * by default */ + * by default */ always_show_id: false, /** this is the function that shows the Workspace's IDs - * around the current workspace if one breaks the crescent order. - * It basically helps keyboard navigation between workspaces. - * --- - * Example: 1(empty, current, shows ID), 2(empty, does not appear(makes - * the previous not to be in a crescent order)), 3(not empty, shows ID) */ + * around the current workspace if one breaks the crescent order. + * It basically helps keyboard navigation between workspaces. + * --- + * Example: 1(empty, current, shows ID), 2(empty, does not appear(makes + * the previous not to be in a crescent order)), 3(not empty, shows ID) */ enable_helper: true, /** hide workspace indicator if there's only one active workspace */ hide_if_single: false }, clock: { - /** use the same format as gnu's `date` command */ + /** use the same format as gnu's `date` command + * @default "%A %d, %H:%M" // -> "tuesday, 11, 15:44" */ date_format: "%A %d, %H:%M" }, misc: { + /** plays a system-bell sound effect using canberra-gtk-play on volume change + * @default true */ play_bell_on_volume_change: true } }; diff --git a/src/modules/wallpaper.ts b/src/modules/wallpaper.ts index 8960d3b..7954f2d 100644 --- a/src/modules/wallpaper.ts +++ b/src/modules/wallpaper.ts @@ -1,12 +1,12 @@ import { execAsync } from "ags/process"; -import { timeout } from "ags/time"; -import { monitorFile, readFile } from "ags/file"; -import GObject, { register, getter } from "ags/gobject"; +import { readFile } from "ags/file"; +import GObject, { register, getter, gtype, property, setter } 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"; +import { createSubscription, encoder } from "./utils"; +import { Notifications } from "./notifications"; +import { generalConfig } from "../config"; export { Wallpaper }; @@ -40,15 +40,19 @@ type WalData = { }; }; + +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); #splash: boolean = true; - #monitor: Gio.FileMonitor; #hyprpaperFile: Gio.File; #wallpapersPath: string; - #ignoreWatch: boolean = false; @getter(Boolean) public get splash() { return this.#splash; } @@ -57,14 +61,21 @@ class Wallpaper extends GObject.Object { this.notify("splash"); } - /** current wallpaper's complete path - * can be an empty string if undefined */ + /** 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); } + get wallpaper() { return this.#wallpaper ?? ""; } + + @setter(String) + set wallpaper(newValue: string) { this.setWallpaper(newValue); } public get wallpapersPath() { return this.#wallpapersPath; } + @property(gtype(String)) + positioning: WallpaperMode = "cover"; + + @property(gtype(String)) + colorMode: WalMode = "darken"; + constructor() { super(); @@ -78,58 +89,52 @@ class Wallpaper extends GObject.Object { if(wall?.trim()) this.#wallpaper = wall.trim(); }); - let tmeout: (AstalIO.Time|undefined) = undefined; + createSubscription( + generalConfig.bindProperty("wallpaper.color_mode", "string"), + () => { + const mode = generalConfig.getProperty("wallpaper.color_mode", "string"); + 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.#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; + this.colorMode = mode as WalMode; + this.reloadColors(); } + ); - const [ loaded, text ] = this.#hyprpaperFile.load_contents(null); - if(!loaded) - console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!"); + createSubscription( + generalConfig.bindProperty("wallpaper.positioning", "string"), + () => { + const positioning = generalConfig + .getProperty("wallpaper.positioning", "string") as WallpaperPositioning; - const content = decoder.decode(text); + if(!positioning || (positioning !== "contain" && + positioning !== "cover" && + positioning !== "tile")) { - 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; - } + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Couldn't update wallpaper position", + body: "Invalid position value. Possible values are: \"cover\", \"contain\" or \"tile\"" + }); + return; } - } - }); - } - vfunc_dispose(): void { - this.#monitor.cancel(); + 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}` + }) + ); + } + ); } public static getDefault(): Wallpaper { @@ -140,29 +145,28 @@ class Wallpaper extends GObject.Object { } 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); - } - ); - + if(!res) { + console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`); return; } - console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`); + // success + 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; } ); } @@ -173,42 +177,56 @@ class Wallpaper extends GObject.Object { } 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(); + return await execAsync("hyprctl hyprpaper listactive").then(stdout => { + const lineSplit = stdout.split('\n'); + stdout = lineSplit[lineSplit.length - 1]; + + 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((err: Gio.IOErrorEnum) => { - console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`); + }).catch((err: Error) => { + console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message}`); return undefined; }); } public reloadColors(): void { - execAsync(`wal -t --cols16 darken -i "${this.#wallpaper}"`).then(() => { + execAsync(`wal -t --cols16 ${this.colorMode} -i "${this.#wallpaper}"`).then(() => { console.log("Wallpaper: reloaded shell colors"); - }).catch(r => { - console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`); + }).catch((e: Error) => { + console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${e.message}`); }); } + public async reloadWallpaper(write: boolean = true): Promise { + if(this.wallpaper.trim() === "") + return; + + await execAsync(`hyprctl hyprpaper wallpaper \", ${this.positioning}:${this.wallpaper}\"`); + this.reloadColors(); + write && this.writeChanges(); + } + public setWallpaper(path: string|Gio.File, write: boolean = true): void { + path = typeof path === "string" ? path : path.peek_path()!; + 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()!; + execAsync(`hyprctl hyprpaper wallpaper \", ${this.positioning}:${path}\"`).then(() => { + this.#wallpaper = path; this.reloadColors(); write && this.writeChanges(); - }).catch(r => { - console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`); + }).catch((e: Error) => { + console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${e.message}`); }) - ).catch(r => { - console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`); + ).catch((e: Error) => { + console.error(`Wallpaper: Couldn't preload image. Stderr: ${e.message}`); }) - ).catch(r => { - console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`); + ).catch((e: Error) => { + console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${e.message}`); }); } @@ -218,8 +236,8 @@ class Wallpaper extends GObject.Object { this.setWallpaper(wall); return wall; - }).catch(r => { - console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`); + }).catch((e: Error) => { + console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${e.message}`); return undefined; })); }