331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
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<WallpaperPositioning>(String))
|
|
positioning: WallpaperPositioning = "cover";
|
|
|
|
@property(gtype<WalMode>(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<void> {
|
|
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<string|undefined> {
|
|
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<void> {
|
|
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<string|undefined> {
|
|
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;
|
|
}));
|
|
}
|
|
}
|