pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons Singleton { id: root Component.onCompleted: { if (Settings.data.templates.enableUserTheming) writeUserTemplatesToml(); } readonly property string templateApplyScript: Quickshell.shellDir + '/Scripts/bash/template-apply.sh' readonly property string gtkRefreshScript: Quickshell.shellDir + '/Scripts/python/src/theming/gtk-refresh.py' readonly property string vscodeHelperScript: Quickshell.shellDir + '/Scripts/python/src/theming/vscode-helper.py' // Dynamically resolved VSCode extension theme paths (all matching noctalia extensions) property var resolvedCodePaths: [] property var resolvedCodiumPaths: [] // Terminal configurations (for wallpaper-based templates) // Each terminal must define a postHook that sets up config includes and triggers reload readonly property var terminals: [ { "id": "foot", "name": "Foot", "templatePath": "terminal/foot", "predefinedTemplatePath": "terminal/foot-predefined", "outputPath": "~/.config/foot/themes/noctalia", "postHook": `${templateApplyScript} foot` }, { "id": "ghostty", "name": "Ghostty", "templatePath": "terminal/ghostty", "predefinedTemplatePath": "terminal/ghostty-predefined", "outputPath": "~/.config/ghostty/themes/noctalia", "postHook": `${templateApplyScript} ghostty` }, { "id": "kitty", "name": "Kitty", "templatePath": "terminal/kitty.conf", "predefinedTemplatePath": "terminal/kitty-predefined.conf", "outputPath": "~/.config/kitty/themes/noctalia.conf", "postHook": `${templateApplyScript} kitty` }, { "id": "alacritty", "name": "Alacritty", "templatePath": "terminal/alacritty.toml", "predefinedTemplatePath": "terminal/alacritty-predefined.toml", "outputPath": "~/.config/alacritty/themes/noctalia.toml", "postHook": `${templateApplyScript} alacritty` }, { "id": "wezterm", "name": "Wezterm", "templatePath": "terminal/wezterm.toml", "predefinedTemplatePath": "terminal/wezterm-predefined.toml", "outputPath": "~/.config/wezterm/colors/Noctalia.toml", "postHook": `${templateApplyScript} wezterm` } ] // Application configurations - consolidated from Theming + AppThemeService readonly property var applications: [ { "id": "gtk", "name": "GTK", "category": "system", "input": "gtk4.css", "outputs": [ { "path": "~/.config/gtk-3.0/noctalia.css", "input": "gtk3.css" }, { "path": "~/.config/gtk-4.0/noctalia.css", "input": "gtk4.css" } ], "postProcess": mode => `python3 ${gtkRefreshScript} ${mode}` }, { "id": "qt", "name": "Qt", "category": "system", "input": "qtct.conf", "outputs": [ { "path": "~/.config/qt5ct/colors/noctalia.conf" }, { "path": "~/.config/qt6ct/colors/noctalia.conf" } ] }, { "id": "kcolorscheme", "name": "KColorScheme", "category": "system", "input": "kcolorscheme.colors", "outputs": [ { "path": "~/.local/share/color-schemes/noctalia.colors" } ], "postProcess": () => "if command -v plasma-apply-colorscheme >/dev/null 2>&1; then plasma-apply-colorscheme BreezeDark; sleep 0.5; plasma-apply-colorscheme noctalia; fi" }, { "id": "fuzzel", "name": "Fuzzel", "category": "launcher", "input": "fuzzel.conf", "outputs": [ { "path": "~/.config/fuzzel/themes/noctalia" } ], "postProcess": () => `${templateApplyScript} fuzzel` }, { "id": "vicinae", "name": "Vicinae", "category": "launcher", "input": "vicinae.toml", "outputs": [ { "path": "~/.local/share/vicinae/themes/noctalia.toml" } ], "postProcess": () => `cp --update=none ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${templateApplyScript} vicinae` }, { "id": "walker", "name": "Walker", "category": "launcher", "input": "walker.css", "outputs": [ { "path": "~/.config/walker/themes/noctalia/style.css" } ], "postProcess": () => `${templateApplyScript} walker`, "strict": true // Use strict mode for palette generation (preserves custom surface/outline values) }, { "id": "pywalfox", "name": "Pywalfox", "category": "browser", "input": "pywalfox.json", "outputs": [ { "path": "~/.cache/wal/colors.json" } ], "postProcess": mode => `${templateApplyScript} pywalfox ${mode}` } // CONSOLIDATED DISCORD CLIENTS , { "id": "discord", "name": "Discord", "category": "misc", "input": ["discord-midnight.css", "discord-material.css"], "clients": [ { "name": "vesktop", "path": "~/.config/vesktop" }, { "name": "webcord", "path": "~/.config/webcord" }, { "name": "armcord", "path": "~/.config/armcord" }, { "name": "equibop", "path": "~/.config/equibop" }, { "name": "equicord", "path": "~/.config/Equicord" }, { "name": "lightcord", "path": "~/.config/lightcord" }, { "name": "dorion", "path": "~/.config/dorion" }, { "name": "vencord", "path": "~/.config/Vencord" }, { "name": "vencord-flatpak", "path": "~/.var/app/com.discordapp.Discord/config/Vencord" }, { "name": "betterdiscord", "path": "~/.config/BetterDiscord" } ] }, { "id": "code", "name": "VSCode", "category": "editor", "input": "code.json", "clients": [ { "name": "code", "path": "~/.vscode/extensions/noctalia.noctaliatheme-0.0.5/themes/NoctaliaTheme-color-theme.json" }, { "name": "codium", "path": "~/.vscode-oss/extensions/noctalia.noctaliatheme-0.0.5-universal/themes/NoctaliaTheme-color-theme.json" } ] }, { "id": "zed", "name": "Zed", "category": "editor", "input": "zed.json", "outputs": [ { "path": "~/.config/zed/themes/noctalia.json" } ], "dualMode": true // Template contains both dark and light theme patterns }, { "id": "helix", "name": "Helix", "category": "editor", "input": "helix.toml", "outputs": [ { "path": "~/.config/helix/themes/noctalia.toml" } ] }, { "id": "spicetify", "name": "Spicetify", "category": "audio", "input": "spicetify.ini", "outputs": [ { "path": "~/.config/spicetify/Themes/Comfy/color.ini" } ], "postProcess": () => `spicetify -q apply --no-restart` }, { "id": "telegram", "name": "Telegram", "category": "misc", "input": "telegram.tdesktop-theme", "outputs": [ { "path": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme" } ] }, { "id": "zenBrowser", "name": "Zen Browser", "category": "browser", "input": "zen-browser/zen-userChrome.css", "outputs": [ { "path": "~/.cache/noctalia/zen-browser/zen-userChrome.css" }, { "path": "~/.cache/noctalia/zen-browser/zen-userContent.css", "input": "zen-browser/zen-userContent.css" } ], "postProcess": () => "sh -c 'CSS_CHROME=\"$HOME/.cache/noctalia/zen-browser/zen-userChrome.css\"; CSS_CONTENT=\"$HOME/.cache/noctalia/zen-browser/zen-userContent.css\"; LINE_CHROME=\"@import \\\"$CSS_CHROME\\\";\"; LINE_CONTENT=\"@import \\\"$CSS_CONTENT\\\";\"; find \"$HOME/.config/zen\" \"$HOME/.zen\" -mindepth 2 -maxdepth 2 -type d -name chrome -print0 2>/dev/null | while IFS= read -r -d \"\" dir; do USER_CHROME=\"$dir/userChrome.css\"; USER_CONTENT=\"$dir/userContent.css\"; mkdir -p \"$dir\"; touch \"$USER_CHROME\" \"$USER_CONTENT\"; sed -i \"/zen-browser\\/zen-userChrome\\.css/d\" \"$USER_CHROME\"; sed -i \"/zen-browser\\/zen-userContent\\.css/d\" \"$USER_CONTENT\"; if ! grep -Fq \"$LINE_CHROME\" \"$USER_CHROME\"; then printf \"%s\\n\" \"$LINE_CHROME\" >> \"$USER_CHROME\"; fi; if ! grep -Fq \"$LINE_CONTENT\" \"$USER_CONTENT\"; then printf \"%s\\n\" \"$LINE_CONTENT\" >> \"$USER_CONTENT\"; fi; done'" }, { "id": "cava", "name": "Cava", "category": "audio", "input": "cava.ini", "outputs": [ { "path": "~/.config/cava/themes/noctalia" } ], "postProcess": () => `${templateApplyScript} cava` }, { "id": "yazi", "name": "Yazi", "category": "misc", "input": "yazi.toml", "outputs": [ { "path": "~/.config/yazi/flavors/noctalia.yazi/flavor.toml" } ], "postProcess": () => `${templateApplyScript} yazi` }, { "id": "emacs", "name": "Emacs", "category": "editor", "input": "emacs.el", "postProcess": () => `emacsclient -e "(load-theme 'noctalia t)"` }, { "id": "labwc", "name": "Labwc", "category": "compositor", "input": "labwc.conf", "outputs": [ { "path": "~/.config/labwc/themerc-override" } ], "postProcess": () => `${templateApplyScript} labwc` }, { "id": "niri", "name": "Niri", "category": "compositor", "input": "niri.kdl", "outputs": [ { "path": "~/.config/niri/noctalia.kdl" } ], "postProcess": () => `${templateApplyScript} niri` }, { "id": "sway", "name": "Sway", "category": "compositor", "input": "sway", "outputs": [ { "path": "~/.config/sway/noctalia" } ], "postProcess": () => `${templateApplyScript} sway` }, { "id": "scroll", "name": "Scroll", "category": "compositor", "input": "scroll", "outputs": [ { "path": "~/.config/scroll/noctalia" } ], "postProcess": () => `${templateApplyScript} scroll` }, { "id": "hyprland", "name": "Hyprland", "category": "compositor", "input": "hyprland.conf", "outputs": [ { "path": "~/.config/hypr/noctalia/noctalia-colors.conf" } ], "postProcess": () => `${templateApplyScript} hyprland` }, { "id": "hyprtoolkit", "name": "Hyprtoolkit", "category": "system", "input": "hyprtoolkit.conf", "outputs": [ { "path": "~/.config/hypr/hyprtoolkit.conf" } ] }, { "id": "mango", "name": "Mango", "category": "compositor", "input": "mango.conf", "outputs": [ { "path": "~/.config/mango/noctalia.conf" } ], "postProcess": () => `${templateApplyScript} mango` }, { "id": "btop", "name": "btop", "category": "misc", "input": "btop.theme", "outputs": [ { "path": "~/.config/btop/themes/noctalia.theme" } ], "postProcess": () => `${templateApplyScript} btop` }, { "id": "zathura", "name": "Zathura", "category": "misc", "input": "zathurarc", "outputs": [ { "path": "~/.config/zathura/noctaliarc" } ], "postProcess": () => `${templateApplyScript} zathura` }, { "id": "steam", "name": "Steam", "category": "misc", "input": "steam.css", "outputs": [ { "path": "~/.steam/steam/steamui/skins/Material-Theme/css/main/colors/matugen.css" } ] } ] // Extract Discord clients for ProgramCheckerService compatibility readonly property var discordClients: { var clients = []; var discordApp = applications.find(app => app.id === "discord"); if (discordApp && discordApp.clients) { discordApp.clients.forEach(client => { clients.push({ "name": client.name, "configPath": client.path, "themePath": `${client.path}/themes/noctalia.theme.css` }); }); } return clients; } // Get resolved theme paths for a code client (returns array of all matching paths) function resolvedCodeClientPaths(clientName) { if (clientName === "code") return resolvedCodePaths; if (clientName === "codium") return resolvedCodiumPaths; return []; } // Extract Code clients for ProgramCheckerService compatibility readonly property var codeClients: { var clients = []; var codeApp = applications.find(app => app.id === "code"); if (codeApp && codeApp.clients) { codeApp.clients.forEach(client => { // Extract base config directory from theme path var themePath = client.path; var baseConfigDir = ""; if (client.name === "code") { // For VSCode: ~/.vscode/extensions/... -> ~/.vscode baseConfigDir = "~/.vscode"; } else if (client.name === "codium") { // For VSCodium: ~/.vscode-oss/extensions/... -> ~/.vscode-oss baseConfigDir = "~/.vscode-oss"; } clients.push({ "name": client.name, "configPath": baseConfigDir, "themePath": "" // resolved dynamically via resolvedCodeClientPaths() }); }); } return clients; } // Resolve VSCode extension paths dynamically Process { id: codeResolverProcess command: ["python3", vscodeHelperScript, "~/.vscode/extensions"] running: true property var paths: [] stdout: SplitParser { onRead: data => { var line = data.trim(); if (line) codeResolverProcess.paths.push(line); } } onExited: { root.resolvedCodePaths = paths; } } Process { id: codiumResolverProcess command: ["python3", vscodeHelperScript, "~/.vscode-oss/extensions"] running: true property var paths: [] stdout: SplitParser { onRead: data => { var line = data.trim(); if (line) codiumResolverProcess.paths.push(line); } } onExited: { root.resolvedCodiumPaths = paths; } } // Build user templates TOML content function buildUserTemplatesToml() { var lines = []; lines.push("[config]"); lines.push(""); lines.push("[templates]"); lines.push(""); lines.push("# User-defined templates"); lines.push("# Add your custom templates below"); lines.push("# Example:"); lines.push("# [templates.myapp]"); lines.push("# input_path = \"~/.config/noctalia/templates/myapp.css\""); lines.push("# output_path = \"~/.config/myapp/theme.css\""); lines.push("# post_hook = \"myapp --reload-theme\""); lines.push(""); lines.push("# Remove this section and add your own templates"); lines.push("#[templates.placeholder]"); lines.push("#input_path = \"" + Quickshell.shellDir + "/Assets/Templates/noctalia.json\""); lines.push("#output_path = \"" + Settings.cacheDir + "placeholder.json\""); lines.push(""); return lines.join("\n") + "\n"; } // Write user templates TOML file (moved from Theming) function writeUserTemplatesToml() { var userConfigPath = Settings.configDir + "user-templates.toml"; // Check if file already exists fileCheckProcess.command = ["test", "-s", userConfigPath]; fileCheckProcess.running = true; } function doWriteUserTemplatesToml() { var userConfigPath = Settings.configDir + "user-templates.toml"; var configContent = buildUserTemplatesToml(); var userConfigPathEsc = userConfigPath.replace(/'/g, "'\\''"); var configDirEsc = Settings.configDir.replace(/'/g, "'\\''"); // Combine mkdir and write in a single script to avoid race condition var script = `mkdir -p '${configDirEsc}' && cat > '${userConfigPathEsc}' << 'EOF'\n`; script += configContent; script += "EOF\n"; fileWriteProcess.command = ["sh", "-c", script]; fileWriteProcess.running = true; } // Extract Emacs clients for ProgramCheckerService compatibility readonly property var emacsClients: [ { "name": "doom", "path": "~/.config/doom" }, { "name": "modern", "path": "~/.config/emacs" }, { "name": "traditional", "path": "~/.emacs.d" } ] // Process for checking if user templates file exists and is non-empty Process { id: fileCheckProcess running: false onExited: function (exitCode) { if (exitCode === 0) { // File exists and is non-empty, skip creation Logger.d("TemplateRegistry", "User templates config already exists, skipping creation"); } else { // File doesn't exist or is empty, create it doWriteUserTemplatesToml(); } } } // Process for writing user templates file with error reporting Process { id: fileWriteProcess running: false onExited: function (exitCode) { if (exitCode === 0) { Logger.d("TemplateRegistry", "User templates config written to:", Settings.configDir + "user-templates.toml"); } else { Logger.e("TemplateRegistry", "Failed to write user templates config (exit code:", exitCode + ")"); } } } }