import app from "ags/gtk4/app"; import { Astal, Gtk } from "ags/gtk4"; import { readFile } from "ags/file"; import { execAsync } from "ags/process"; import css from "./switcher.css"; import GLib from "gi://GLib"; type SwitcherItem = { type: "theme" | "wallpaper"; path: string; name: string; icon?: string; wallpaper: string; active?: boolean; accent?: string; accent2?: string; muted?: string; }; const HYPR_DIR = GLib.getenv("HYPR_DIR") || "/home/pascal/.config/hypr"; const SCRIPT_DIR = `${HYPR_DIR}/Scripts`; const THEME_DIR = GLib.getenv("HYPR_SWITCHER_THEME_DIR") || `${HYPR_DIR}/Themes`; const WALLPAPER_DIR = GLib.getenv("HYPR_SWITCHER_WALLPAPER_DIR") || GLib.getenv("WALLPAPER_DIR") || `${GLib.get_home_dir()}/Bilder/Wallpaper`; const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`; const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"]; type UiTheme = { accent: string; accent2: string; background: string; backgroundSoft: string; foreground: string; muted: string; panelHex: string; }; function notify(message: string) { execAsync(["notify-send", "AGS Switcher", message]).catch(console.error); } function fileExists(path: string) { return GLib.file_test(path, GLib.FileTest.EXISTS); } function readText(path: string) { try { return readFile(path); } catch { return ""; } } function listFiles(dir: string, predicate: (path: string, name: string) => boolean) { try { const directory = GLib.Dir.open(dir, 0); const files: string[] = []; let name = directory.read_name(); while (name !== null) { const path = `${dir}/${name}`; if (predicate(path, name)) { files.push(path); } name = directory.read_name(); } return files.sort((a, b) => a.localeCompare(b)); } catch { return []; } } function shellValue(contents: string, key: string) { const regex = new RegExp(`^${key}=(["']?)(.*?)\\1$`, "m"); return contents.match(regex)?.[2] || ""; } function activeTheme(): UiTheme { const activeWallpaper = currentWallpaper(); const fallback = { accent: "#00ff9c", accent2: "#00cc88", background: "rgba(20, 20, 30, 0.95)", backgroundSoft: "rgba(40, 40, 55, 0.8)", foreground: "#cdd6f4", muted: "#cccccc", panelHex: "#282837", }; const themeFile = listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme")) .find(path => shellValue(readText(path), "WALLPAPER") === activeWallpaper); if (!themeFile) { return fallback; } const contents = readText(themeFile); return { accent: shellValue(contents, "ACCENT") || fallback.accent, accent2: shellValue(contents, "ACCENT_2") || fallback.accent2, background: shellValue(contents, "BACKGROUND") || fallback.background, backgroundSoft: shellValue(contents, "BACKGROUND_SOFT") || fallback.backgroundSoft, foreground: shellValue(contents, "FOREGROUND") || fallback.foreground, muted: shellValue(contents, "MUTED") || fallback.muted, panelHex: shellValue(contents, "PANEL_HEX") || fallback.panelHex, }; } function themeCss(theme: UiTheme) { return [ `@define-color ags_accent ${theme.accent};`, `@define-color ags_accent_2 ${theme.accent2};`, `@define-color ags_bg ${theme.background};`, `@define-color ags_bg_soft ${theme.backgroundSoft};`, `@define-color ags_fg ${theme.foreground};`, `@define-color ags_muted ${theme.muted};`, `@define-color ags_panel ${theme.panelHex};`, css, ].join("\n"); } function basename(path: string) { return GLib.path_get_basename(path); } function currentWallpaper() { return readText(CURRENT_WALLPAPER).trim(); } function loadThemes(): SwitcherItem[] { return listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme")) .map(path => { const contents = readText(path); return { type: "theme", path, name: shellValue(contents, "NAME") || basename(path).replace(/\.theme$/, ""), icon: shellValue(contents, "ICON") || "󰌪", wallpaper: shellValue(contents, "WALLPAPER"), accent: shellValue(contents, "ACCENT") || "#f38ba8", accent2: shellValue(contents, "ACCENT_2") || "#cba6f7", muted: shellValue(contents, "MUTED") || "#cdd6f4", }; }); } function loadWallpapers(): SwitcherItem[] { const active = currentWallpaper(); return listFiles(WALLPAPER_DIR, (_path, name) => IMAGE_EXTENSIONS.some(ext => name.toLowerCase().endsWith(ext)), ).map(path => ({ type: "wallpaper", path, name: basename(path), wallpaper: path, active: path === active, })); } function applyItem(item: SwitcherItem) { const command = item.type === "theme" ? [`${SCRIPT_DIR}/theme-menu.sh`, "--apply", item.path] : [`${SCRIPT_DIR}/wallpaper-menu.sh`, "--apply", item.path]; execAsync(command) .then(() => app.quit()) .catch(error => { console.error(error); notify(`${item.name} konnte nicht angewendet werden.`); }); } function previewWallpaper(item: SwitcherItem) { execAsync([`${SCRIPT_DIR}/wallpaper-menu.sh`, "--preview", item.path]).catch(error => { console.error(error); notify(`${item.name} konnte nicht geoeffnet werden.`); }); } function Preview({ item }: { item: SwitcherItem }) { const hasImage = item.wallpaper && fileExists(item.wallpaper); return ( {hasImage ? : ); } function Swatches({ item }: { item: SwitcherItem }) { return ( {[item.accent, item.accent2, item.muted].map(color => ( ))} ); } function ItemButton({ item }: { item: SwitcherItem }) { const content = ( {item.type === "theme" ? : } ); const applyButton = ( ); if (item.type === "theme") { return applyButton; } return ( {applyButton} ); } function SwitcherWindow(mode: string) { const isTheme = mode === "theme" || mode === "themes"; const items = isTheme ? loadThemes() : loadWallpapers(); const title = isTheme ? "Theme wechseln" : "Wallpaper wechseln"; const empty = isTheme ? `Keine Themes in ${THEME_DIR}` : `Keine Bilder in ${WALLPAPER_DIR}`; return ( {items.length > 0 ? ( {items.map(item => )} ) : ( )} ); } app.start({ css: themeCss(activeTheme()), instanceName: "hypr-switcher", main(mode = "wallpaper") { SwitcherWindow(mode); }, });