feat(wallpaper, config): add config properties to configure wallpaper position and color generation modes

This commit is contained in:
retrozinndev
2025-11-11 16:37:02 -03:00
parent 63d6ba2e30
commit b0956e24c5
2 changed files with 141 additions and 100 deletions
+34 -11
View File
@@ -1,47 +1,70 @@
import { Config } from "./modules/config"; import { Config } from "./modules/config";
import { WallpaperPositioning, WalMode } from "./modules/wallpaper";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
const generalConfigDefaults = { const generalConfigDefaults = {
notifications: { notifications: {
/** low-priority notification timeout
* @default 4000 */
timeout_low: 4000, timeout_low: 4000,
/** regular notification timeout
* @default 6000 */
timeout_normal: 6000, timeout_normal: 6000,
/** critical/very important notification timeout
* @default 0 */
timeout_critical: 0, timeout_critical: 0,
/** notification popup horizontal position. can be "left" or "right" /** notification popup horizontal position. can be "left" or "right"
* @default "right" */ * @default "right" */
position_h: "right", position_h: "right",
/** vertical notification popup position. can be "top" or "bottom" /** vertical notification popup position. can be "top" or "bottom"
* @default "top" */ * @default "top" */
position_v: "top" position_v: "top",
/** dismisses notification popup when unhovered after hovering
* @default false */
dismiss_after_unhover: false
}, },
night_light: { 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 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: { workspaces: {
/** breaks `enable_helper`, makes all workspaces show their respective ID /** breaks `enable_helper`, makes all workspaces show their respective ID
* by default */ * by default */
always_show_id: false, always_show_id: false,
/** this is the function that shows the Workspace's IDs /** this is the function that shows the Workspace's IDs
* around the current workspace if one breaks the crescent order. * around the current workspace if one breaks the crescent order.
* It basically helps keyboard navigation between workspaces. * It basically helps keyboard navigation between workspaces.
* --- * ---
* Example: 1(empty, current, shows ID), 2(empty, does not appear(makes * 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) */ * the previous not to be in a crescent order)), 3(not empty, shows ID) */
enable_helper: true, enable_helper: true,
/** hide workspace indicator if there's only one active workspace */ /** hide workspace indicator if there's only one active workspace */
hide_if_single: false hide_if_single: false
}, },
clock: { 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" date_format: "%A %d, %H:%M"
}, },
misc: { misc: {
/** plays a system-bell sound effect using canberra-gtk-play on volume change
* @default true */
play_bell_on_volume_change: true play_bell_on_volume_change: true
} }
}; };
+107 -89
View File
@@ -1,12 +1,12 @@
import { execAsync } from "ags/process"; import { execAsync } from "ags/process";
import { timeout } from "ags/time"; import { readFile } from "ags/file";
import { monitorFile, readFile } from "ags/file"; import GObject, { register, getter, gtype, property, setter } from "ags/gobject";
import GObject, { register, getter } from "ags/gobject";
import AstalIO from "gi://AstalIO";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
import GLib from "gi://GLib?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 }; 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" }) @register({ GTypeName: "Wallpaper" })
class Wallpaper extends GObject.Object { class Wallpaper extends GObject.Object {
private static instance: Wallpaper; private static instance: Wallpaper;
#wallpaper: (string|undefined); #wallpaper: (string|undefined);
#splash: boolean = true; #splash: boolean = true;
#monitor: Gio.FileMonitor;
#hyprpaperFile: Gio.File; #hyprpaperFile: Gio.File;
#wallpapersPath: string; #wallpapersPath: string;
#ignoreWatch: boolean = false;
@getter(Boolean) @getter(Boolean)
public get splash() { return this.#splash; } public get splash() { return this.#splash; }
@@ -57,14 +61,21 @@ class Wallpaper extends GObject.Object {
this.notify("splash"); this.notify("splash");
} }
/** current wallpaper's complete path /** current wallpaper's complete path. can be an empty string if undefined */
* can be an empty string if undefined */
@getter(String) @getter(String)
public get wallpaper() { return this.#wallpaper ?? ""; } get wallpaper() { return this.#wallpaper ?? ""; }
public set wallpaper(newValue: string) { this.setWallpaper(newValue); }
@setter(String)
set wallpaper(newValue: string) { this.setWallpaper(newValue); }
public get wallpapersPath() { return this.#wallpapersPath; } public get wallpapersPath() { return this.#wallpapersPath; }
@property(gtype<WallpaperMode>(String))
positioning: WallpaperMode = "cover";
@property(gtype<WalMode>(String))
colorMode: WalMode = "darken";
constructor() { constructor() {
super(); super();
@@ -78,58 +89,52 @@ class Wallpaper extends GObject.Object {
if(wall?.trim()) this.#wallpaper = wall.trim(); 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) => { this.colorMode = mode as WalMode;
if(event !== Gio.FileMonitorEvent.CHANGED && event !== Gio.FileMonitorEvent.CREATED && this.reloadColors();
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); createSubscription(
if(!loaded) generalConfig.bindProperty("wallpaper.positioning", "string"),
console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!"); () => {
const positioning = generalConfig
.getProperty("wallpaper.positioning", "string") as WallpaperPositioning;
const content = decoder.decode(text); if(!positioning || (positioning !== "contain" &&
positioning !== "cover" &&
positioning !== "tile")) {
if(content) { Notifications.getDefault().sendNotification({
let setWall: boolean = true; appName: "colorshell",
summary: "Couldn't update wallpaper position",
for(const line of content.split('\n')) { body: "Invalid position value. Possible values are: \"cover\", \"contain\" or \"tile\""
if(line.trim().startsWith('#')) });
continue; return;
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.positioning = positioning;
this.#monitor.cancel(); 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 { public static getDefault(): Wallpaper {
@@ -140,29 +145,28 @@ class Wallpaper extends GObject.Object {
} }
private writeChanges(): void { private writeChanges(): void {
this.#ignoreWatch = true; // tell monitor to ignore file replace
this.#hyprpaperFile.replace_async(null, false, this.#hyprpaperFile.replace_async(null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION, Gio.FileCreateFlags.REPLACE_DESTINATION,
GLib.PRIORITY_DEFAULT, null, (_, result) => { GLib.PRIORITY_DEFAULT, null, (_, result) => {
const res = this.#hyprpaperFile.replace_finish(result); const res = this.#hyprpaperFile.replace_finish(result);
if(res) { if(!res) {
// success console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`);
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; 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<string|undefined> { public async getWallpaper(): Promise<string|undefined> {
return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => { return await execAsync("hyprctl hyprpaper listactive").then(stdout => {
const loaded: (string|undefined) = stdout.split('=')[1]?.trim(); const lineSplit = stdout.split('\n');
stdout = lineSplit[lineSplit.length - 1];
const loaded = stdout.split('=')[1]?.trim();
if(!loaded) if(!loaded)
console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`); console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`);
return loaded; return loaded;
}).catch((err: Gio.IOErrorEnum) => { }).catch((err: Error) => {
console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`); console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message}`);
return undefined; return undefined;
}); });
} }
public reloadColors(): void { 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"); console.log("Wallpaper: reloaded shell colors");
}).catch(r => { }).catch((e: Error) => {
console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`); console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${e.message}`);
}); });
} }
public async reloadWallpaper(write: boolean = true): Promise<void> {
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 { 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 unload all").then(() =>
execAsync(`hyprctl hyprpaper preload ${path}`).then(() => execAsync(`hyprctl hyprpaper preload ${path}`).then(() =>
execAsync(`hyprctl hyprpaper wallpaper ${path}`).then(() => { execAsync(`hyprctl hyprpaper wallpaper \", ${this.positioning}:${path}\"`).then(() => {
this.#wallpaper = (typeof path === "string") ? path : path.get_path()!; this.#wallpaper = path;
this.reloadColors(); this.reloadColors();
write && this.writeChanges(); write && this.writeChanges();
}).catch(r => { }).catch((e: Error) => {
console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`); console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${e.message}`);
}) })
).catch(r => { ).catch((e: Error) => {
console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`); console.error(`Wallpaper: Couldn't preload image. Stderr: ${e.message}`);
}) })
).catch(r => { ).catch((e: Error) => {
console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`); console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${e.message}`);
}); });
} }
@@ -218,8 +236,8 @@ class Wallpaper extends GObject.Object {
this.setWallpaper(wall); this.setWallpaper(wall);
return wall; return wall;
}).catch(r => { }).catch((e: Error) => {
console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`); console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${e.message}`);
return undefined; return undefined;
})); }));
} }