add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
|
||||
// When the wallpaper changes, regenerate theme if necessary
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
|
||||
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
|
||||
effectiveMonitor = Screen.name;
|
||||
}
|
||||
|
||||
if (screenName !== effectiveMonitor)
|
||||
return;
|
||||
|
||||
if (Settings.data.colorSchemes.useWallpaperColors) {
|
||||
generateFromWallpaper();
|
||||
} else {
|
||||
// Re-run predefined scheme templates so {{image}} reflects the new wallpaper path
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.colorSchemes
|
||||
function onDarkModeChanged() {
|
||||
Logger.d("AppThemeService", "Detected dark mode change");
|
||||
generate();
|
||||
}
|
||||
function onMonitorForColorsChanged() {
|
||||
if (Settings.data.colorSchemes.useWallpaperColors) {
|
||||
Logger.d("AppThemeService", "Monitor for colors changed to:", Settings.data.colorSchemes.monitorForColors);
|
||||
generateFromWallpaper();
|
||||
}
|
||||
}
|
||||
function onGenerationMethodChanged() {
|
||||
Logger.d("AppThemeService", "Generation method changed to:", Settings.data.colorSchemes.generationMethod);
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
// PUBLIC FUNCTIONS
|
||||
function init() {
|
||||
Logger.i("AppThemeService", "Service started");
|
||||
}
|
||||
|
||||
function generate() {
|
||||
if (Settings.data.colorSchemes.useWallpaperColors) {
|
||||
generateFromWallpaper();
|
||||
} else {
|
||||
// applyScheme will trigger template generation via schemeReader.onLoaded
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
|
||||
function generateFromWallpaper() {
|
||||
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
|
||||
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
|
||||
effectiveMonitor = Screen.name;
|
||||
}
|
||||
|
||||
const wp = WallpaperService.getWallpaper(effectiveMonitor);
|
||||
if (!wp) {
|
||||
Logger.e("AppThemeService", "No wallpaper found for monitor:", effectiveMonitor);
|
||||
return;
|
||||
}
|
||||
const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
|
||||
TemplateProcessor.processWallpaperColors(wp, mode);
|
||||
}
|
||||
|
||||
function generateFromPredefinedScheme(schemeData) {
|
||||
Logger.i("AppThemeService", "Generating templates from predefined color scheme");
|
||||
const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
|
||||
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
|
||||
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
|
||||
effectiveMonitor = Screen.name;
|
||||
}
|
||||
const wallpaperPath = WallpaperService.getWallpaper(effectiveMonitor) || "";
|
||||
TemplateProcessor.processPredefinedScheme(schemeData, mode, wallpaperPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
pragma Singleton
|
||||
import Qt.labs.folderlistmodel
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var schemes: []
|
||||
property bool scanning: false
|
||||
property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme"
|
||||
property string downloadedSchemesDirectory: Settings.configDir + "colorschemes"
|
||||
property string colorsJsonFilePath: Settings.configDir + "colors.json"
|
||||
readonly property string gtkRefreshScript: Quickshell.shellDir + "/Scripts/python/src/theming/gtk-refresh.py"
|
||||
|
||||
// prefer-light/prefer-dark only; GTK template post_hook still runs full gtk-refresh.
|
||||
function pushSystemColorScheme() {
|
||||
if (!Settings.data.colorSchemes.syncGsettings)
|
||||
return;
|
||||
const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
|
||||
Quickshell.execDetached(["python3", gtkRefreshScript, "--appearance-only", mode]);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.colorSchemes
|
||||
function onDarkModeChanged() {
|
||||
Logger.d("ColorScheme", "Detected dark mode change");
|
||||
if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) {
|
||||
// Re-apply current scheme to pick the right variant
|
||||
applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
root.pushSystemColorScheme();
|
||||
// Toast: dark/light mode switched
|
||||
const enabled = !!Settings.data.colorSchemes.darkMode;
|
||||
const label = enabled ? I18n.tr("tooltips.switch-to-dark-mode") : I18n.tr("tooltips.switch-to-light-mode");
|
||||
const description = I18n.tr("common.enabled");
|
||||
ToastService.showNotice(label, description, "dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function init() {
|
||||
// does nothing but ensure the singleton is created
|
||||
// do not remove
|
||||
Logger.i("ColorScheme", "Service started");
|
||||
loadColorSchemes();
|
||||
}
|
||||
|
||||
function loadColorSchemes() {
|
||||
Logger.d("ColorScheme", "Load colorScheme");
|
||||
scanning = true;
|
||||
schemes = [];
|
||||
// Use find command to locate all scheme.json files in both directories
|
||||
// First ensure the downloaded schemes directory exists
|
||||
Quickshell.execDetached(["mkdir", "-p", downloadedSchemesDirectory]);
|
||||
// Find in both preinstalled and downloaded directories
|
||||
findProcess.command = ["find", "-L", schemesDirectory, downloadedSchemesDirectory, "-mindepth", "2", "-name", "*.json", "-type", "f"];
|
||||
findProcess.running = true;
|
||||
}
|
||||
|
||||
function getBasename(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
var chunks = path.split("/");
|
||||
// Get the filename without extension
|
||||
var filename = chunks[chunks.length - 1];
|
||||
var schemeName = filename.replace(".json", "");
|
||||
// Convert back to display names for special cases
|
||||
if (schemeName === "Noctalia-default") {
|
||||
return "Noctalia (default)";
|
||||
} else if (schemeName === "Noctalia-legacy") {
|
||||
return "Noctalia (legacy)";
|
||||
} else if (schemeName === "Tokyo-Night") {
|
||||
return "Tokyo Night";
|
||||
} else if (schemeName === "Rosepine") {
|
||||
return "Rose Pine";
|
||||
}
|
||||
return schemeName;
|
||||
}
|
||||
|
||||
function resolveSchemePath(nameOrPath) {
|
||||
if (!nameOrPath)
|
||||
return "";
|
||||
if (nameOrPath.indexOf("/") !== -1) {
|
||||
return nameOrPath;
|
||||
}
|
||||
// Handle special cases for Noctalia schemes
|
||||
var schemeName = nameOrPath.replace(".json", "");
|
||||
if (schemeName === "Noctalia (default)") {
|
||||
schemeName = "Noctalia-default";
|
||||
} else if (schemeName === "Noctalia (legacy)") {
|
||||
schemeName = "Noctalia-legacy";
|
||||
} else if (schemeName === "Tokyo Night") {
|
||||
schemeName = "Tokyo-Night";
|
||||
} else if (schemeName === "Rose Pine") {
|
||||
schemeName = "Rosepine";
|
||||
}
|
||||
// Check preinstalled directory first, then downloaded directory
|
||||
var preinstalledPath = schemesDirectory + "/" + schemeName + "/" + schemeName + ".json";
|
||||
var downloadedPath = downloadedSchemesDirectory + "/" + schemeName + "/" + schemeName + ".json";
|
||||
// Try to find the scheme in the loaded schemes list to determine which directory it's in
|
||||
for (var i = 0; i < schemes.length; i++) {
|
||||
if (schemes[i].indexOf("/" + schemeName + "/") !== -1 || schemes[i].indexOf("/" + schemeName + ".json") !== -1) {
|
||||
return schemes[i];
|
||||
}
|
||||
}
|
||||
// Fallback: prefer preinstalled, then downloaded
|
||||
return preinstalledPath;
|
||||
}
|
||||
|
||||
function applyScheme(nameOrPath) {
|
||||
// Force reload by bouncing the path
|
||||
var filePath = resolveSchemePath(nameOrPath);
|
||||
schemeReader.path = "";
|
||||
schemeReader.path = filePath;
|
||||
}
|
||||
|
||||
function setPredefinedScheme(schemeName) {
|
||||
Logger.i("ColorScheme", "Attempting to set predefined scheme to:", schemeName);
|
||||
|
||||
var resolvedPath = resolveSchemePath(schemeName);
|
||||
var basename = getBasename(schemeName);
|
||||
|
||||
// Check if the scheme actually exists in the loaded schemes list
|
||||
var schemeExists = false;
|
||||
for (var i = 0; i < schemes.length; i++) {
|
||||
if (getBasename(schemes[i]) === basename) {
|
||||
schemeExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (schemeExists) {
|
||||
Settings.data.colorSchemes.predefinedScheme = basename;
|
||||
applyScheme(schemeName);
|
||||
ToastService.showNotice(I18n.tr("panels.color-scheme.title"), basename, "settings-color-scheme");
|
||||
} else {
|
||||
Logger.e("ColorScheme", "Scheme not found:", schemeName);
|
||||
ToastService.showError(I18n.tr("panels.color-scheme.title"), `'${basename}' ` + I18n.tr("common.not-found"));
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: findProcess
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
var output = stdout.text.trim();
|
||||
var files = output.split('\n').filter(function (line) {
|
||||
return line.length > 0;
|
||||
});
|
||||
files.sort(function (a, b) {
|
||||
var nameA = getBasename(a).toLowerCase();
|
||||
var nameB = getBasename(b).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
schemes = files;
|
||||
scanning = false;
|
||||
Logger.d("ColorScheme", "Listed", schemes.length, "schemes");
|
||||
// Normalize stored scheme to basename and re-apply if necessary
|
||||
var stored = Settings.data.colorSchemes.predefinedScheme;
|
||||
if (stored) {
|
||||
var basename = getBasename(stored);
|
||||
if (basename !== stored) {
|
||||
Settings.data.colorSchemes.predefinedScheme = basename;
|
||||
}
|
||||
if (!Settings.data.colorSchemes.useWallpaperColors) {
|
||||
applyScheme(basename);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.e("ColorScheme", "Failed to find color scheme files");
|
||||
schemes = [];
|
||||
scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Internal loader to read a scheme file
|
||||
FileView {
|
||||
id: schemeReader
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text());
|
||||
var variant = data;
|
||||
// If scheme provides dark/light variants, pick based on settings
|
||||
if (data && (data.dark || data.light)) {
|
||||
if (Settings.data.colorSchemes.darkMode) {
|
||||
variant = data.dark || data.light;
|
||||
} else {
|
||||
variant = data.light || data.dark;
|
||||
}
|
||||
}
|
||||
writeColorsToDisk(variant);
|
||||
Logger.i("ColorScheme", "Applying color scheme:", getBasename(path));
|
||||
|
||||
// Generate templates for predefined color schemes
|
||||
if (hasEnabledTemplates() || Settings.data.templates.enableUserTheming) {
|
||||
AppThemeService.generateFromPredefinedScheme(data);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("ColorScheme", "Failed to parse scheme JSON:", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any templates are enabled
|
||||
function hasEnabledTemplates() {
|
||||
const activeTemplates = Settings.data.templates.activeTemplates;
|
||||
if (!activeTemplates || activeTemplates.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < activeTemplates.length; i++) {
|
||||
if (activeTemplates[i].enabled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Writer to colors.json using a JsonAdapter for safety
|
||||
FileView {
|
||||
id: colorsWriter
|
||||
path: colorsJsonFilePath
|
||||
printErrors: false
|
||||
onSaved:
|
||||
|
||||
// Logger.i("ColorScheme", "Colors saved")
|
||||
{}
|
||||
JsonAdapter {
|
||||
id: out
|
||||
property color mPrimary: "#000000"
|
||||
property color mOnPrimary: "#000000"
|
||||
property color mSecondary: "#000000"
|
||||
property color mOnSecondary: "#000000"
|
||||
property color mTertiary: "#000000"
|
||||
property color mOnTertiary: "#000000"
|
||||
property color mError: "#000000"
|
||||
property color mOnError: "#000000"
|
||||
property color mSurface: "#000000"
|
||||
property color mOnSurface: "#000000"
|
||||
property color mSurfaceVariant: "#000000"
|
||||
property color mOnSurfaceVariant: "#000000"
|
||||
property color mOutline: "#000000"
|
||||
property color mShadow: "#000000"
|
||||
property color mHover: "#000000"
|
||||
property color mOnHover: "#000000"
|
||||
}
|
||||
}
|
||||
|
||||
function writeColorsToDisk(obj) {
|
||||
function pick(o, a, b, fallback) {
|
||||
return (o && (o[a] || o[b])) || fallback;
|
||||
}
|
||||
out.mPrimary = pick(obj, "mPrimary", "primary", out.mPrimary);
|
||||
out.mOnPrimary = pick(obj, "mOnPrimary", "onPrimary", out.mOnPrimary);
|
||||
out.mSecondary = pick(obj, "mSecondary", "secondary", out.mSecondary);
|
||||
out.mOnSecondary = pick(obj, "mOnSecondary", "onSecondary", out.mOnSecondary);
|
||||
out.mTertiary = pick(obj, "mTertiary", "tertiary", out.mTertiary);
|
||||
out.mOnTertiary = pick(obj, "mOnTertiary", "onTertiary", out.mOnTertiary);
|
||||
out.mError = pick(obj, "mError", "error", out.mError);
|
||||
out.mOnError = pick(obj, "mOnError", "onError", out.mOnError);
|
||||
out.mSurface = pick(obj, "mSurface", "surface", out.mSurface);
|
||||
out.mOnSurface = pick(obj, "mOnSurface", "onSurface", out.mOnSurface);
|
||||
out.mSurfaceVariant = pick(obj, "mSurfaceVariant", "surfaceVariant", out.mSurfaceVariant);
|
||||
out.mOnSurfaceVariant = pick(obj, "mOnSurfaceVariant", "onSurfaceVariant", out.mOnSurfaceVariant);
|
||||
out.mOutline = pick(obj, "mOutline", "outline", out.mOutline);
|
||||
out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow);
|
||||
out.mHover = pick(obj, "mHover", "hover", out.mHover);
|
||||
out.mOnHover = pick(obj, "mOnHover", "onHover", out.mOnHover);
|
||||
|
||||
// Force a rewrite by updating the path
|
||||
colorsWriter.path = "";
|
||||
colorsWriter.path = colorsJsonFilePath;
|
||||
colorsWriter.writeAdapter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Signal emitted when color generation completes successfully (for wallpaper-based theming)
|
||||
signal colorsGenerated
|
||||
|
||||
readonly property string dynamicConfigPath: Settings.cacheDir + "theming.dynamic.toml"
|
||||
readonly property string templateProcessorScript: Quickshell.shellDir + "/Scripts/python/src/theming/template-processor.py"
|
||||
|
||||
// Debounce state for wallpaper processing
|
||||
property var pendingWallpaperRequest: null
|
||||
property var pendingPredefinedRequest: null
|
||||
|
||||
readonly property var schemeTypes: [
|
||||
{
|
||||
"key": "tonal-spot",
|
||||
"name": "M3-Tonal Spot" // Do not translate
|
||||
},
|
||||
{
|
||||
"key": "content",
|
||||
"name": "M3-Content" // Do not translate
|
||||
},
|
||||
{
|
||||
"key": "fruit-salad",
|
||||
"name": "M3-Fruit Salad" // Do not translate
|
||||
},
|
||||
{
|
||||
"key": "rainbow",
|
||||
"name": "M3-Rainbow" // Do not translate
|
||||
},
|
||||
{
|
||||
"key": "monochrome",
|
||||
"name": "M3-Monochrome" // Do not translate
|
||||
},
|
||||
{
|
||||
"key": "vibrant",
|
||||
"name": I18n.tr("common.vibrant")
|
||||
},
|
||||
{
|
||||
"key": "faithful",
|
||||
"name": I18n.tr("common.faithful")
|
||||
},
|
||||
{
|
||||
"key": "dysfunctional",
|
||||
"name": I18n.tr("common.dysfunctional")
|
||||
},
|
||||
{
|
||||
"key": "muted",
|
||||
"name": I18n.tr("common.color-muted")
|
||||
},
|
||||
]
|
||||
|
||||
// Check if a template is enabled in the activeTemplates array
|
||||
function isTemplateEnabled(templateId) {
|
||||
const activeTemplates = Settings.data.templates.activeTemplates;
|
||||
if (!activeTemplates)
|
||||
return false;
|
||||
for (let i = 0; i < activeTemplates.length; i++) {
|
||||
if (activeTemplates[i].id === templateId && activeTemplates[i].enabled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function escapeTomlString(value) {
|
||||
if (!value)
|
||||
return "";
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process wallpaper colors using internal themer
|
||||
* Dual-path architecture (wallpaper generation)
|
||||
* Uses debouncing to prevent spawning multiple processes when spamming wallpaper changes
|
||||
*/
|
||||
function processWallpaperColors(wallpaperPath, mode) {
|
||||
Logger.d("TemplateProcessor", `processWallpaperColors called: path=${wallpaperPath}, mode=${mode}`);
|
||||
pendingWallpaperRequest = {
|
||||
wallpaperPath: wallpaperPath,
|
||||
mode: mode
|
||||
};
|
||||
pendingPredefinedRequest = null;
|
||||
debounceTimer.restart();
|
||||
}
|
||||
|
||||
function executeWallpaperColors(wallpaperPath, mode) {
|
||||
Logger.d("TemplateProcessor", `executeWallpaperColors: path=${wallpaperPath}, mode=${mode}`);
|
||||
const content = buildThemeConfig();
|
||||
if (!content && !Settings.data.templates.enableUserTheming) {
|
||||
Logger.d("TemplateProcessor", "executeWallpaperColors: no config content and no user theming, aborting");
|
||||
return;
|
||||
}
|
||||
const script = buildGenerationScript(content, wallpaperPath, mode);
|
||||
|
||||
generateProcess.command = ["sh", "-c", script];
|
||||
generateProcess.running = true;
|
||||
}
|
||||
|
||||
readonly property string schemeJsonPath: Settings.cacheDir + "predefined-scheme.json"
|
||||
readonly property string predefinedConfigPath: Settings.cacheDir + "theming.predefined.toml"
|
||||
|
||||
/**
|
||||
* Process predefined color scheme using Python template processor
|
||||
* Uses --scheme flag to expand 14-color scheme to full 48-color palette
|
||||
* Uses debouncing to prevent spawning multiple processes when spamming scheme changes
|
||||
*/
|
||||
function processPredefinedScheme(schemeData, mode, wallpaperPath) {
|
||||
pendingPredefinedRequest = {
|
||||
schemeData: schemeData,
|
||||
mode: mode,
|
||||
wallpaperPath: wallpaperPath || ""
|
||||
};
|
||||
pendingWallpaperRequest = null;
|
||||
debounceTimer.restart();
|
||||
}
|
||||
|
||||
function executePredefinedScheme(schemeData, mode, wallpaperPath) {
|
||||
// 1. Build TOML config for application templates (including terminals)
|
||||
const tomlContent = buildPredefinedTemplateConfig(mode);
|
||||
if (!tomlContent && !Settings.data.templates.enableUserTheming) {
|
||||
Logger.d("TemplateProcessor", "No application templates enabled for predefined scheme");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Build script to write files and run Python
|
||||
const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''");
|
||||
|
||||
let script = "";
|
||||
|
||||
// Write scheme JSON (needed by both built-in and user templates)
|
||||
const schemeDelimiter = "SCHEME_JSON_EOF_" + Math.random().toString(36).substr(2, 9);
|
||||
script += `cat > '${schemeJsonPathEsc}' << '${schemeDelimiter}'\n`;
|
||||
script += JSON.stringify(schemeData, null, 2) + "\n";
|
||||
script += `${schemeDelimiter}\n`;
|
||||
|
||||
// Run built-in template processor only if there are templates configured
|
||||
if (tomlContent) {
|
||||
const configPathEsc = predefinedConfigPath.replace(/'/g, "'\\''");
|
||||
const tomlDelimiter = "TOML_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Write TOML config
|
||||
script += `cat > '${configPathEsc}' << '${tomlDelimiter}'\n`;
|
||||
script += tomlContent + "\n";
|
||||
script += `${tomlDelimiter}\n`;
|
||||
|
||||
// Run Python template processor with --scheme flag
|
||||
// Don't pass --mode so templates get both dark and light colors (e.g., zed.json needs both)
|
||||
// Pass --default-mode so "default" in templates resolves to the current theme mode
|
||||
// Pass wallpaper as positional arg so image_path is available in templates (no extraction occurs when --scheme is used)
|
||||
const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : "";
|
||||
script += `python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${configPathEsc}' --default-mode ${mode}\n`;
|
||||
}
|
||||
|
||||
// Add user templates if enabled
|
||||
script += buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath);
|
||||
|
||||
generateProcess.command = ["sh", "-c", script];
|
||||
generateProcess.running = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TOML config for predefined scheme templates (excludes terminal themes)
|
||||
*/
|
||||
function buildPredefinedTemplateConfig(mode) {
|
||||
var lines = [];
|
||||
const homeDir = Quickshell.env("HOME");
|
||||
|
||||
// Add terminal templates
|
||||
TemplateRegistry.terminals.forEach(terminal => {
|
||||
if (isTemplateEnabled(terminal.id)) {
|
||||
lines.push(`\n[templates.${terminal.id}]`);
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${terminal.predefinedTemplatePath}"`);
|
||||
const outputPath = terminal.outputPath.replace("~", homeDir);
|
||||
lines.push(`output_path = "${outputPath}"`);
|
||||
const postHookEsc = escapeTomlString(terminal.postHook);
|
||||
lines.push(`post_hook = "${postHookEsc}"`);
|
||||
}
|
||||
});
|
||||
|
||||
addApplicationTheming(lines, mode);
|
||||
|
||||
if (lines.length > 0) {
|
||||
return ["[config]"].concat(lines).join("\n") + "\n";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// WALLPAPER-BASED GENERATION
|
||||
// ================================================================================
|
||||
function buildThemeConfig() {
|
||||
var lines = [];
|
||||
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
|
||||
|
||||
if (Settings.data.colorSchemes.useWallpaperColors) {
|
||||
addWallpaperTheming(lines, mode);
|
||||
}
|
||||
|
||||
addApplicationTheming(lines, mode);
|
||||
|
||||
if (lines.length > 0) {
|
||||
return ["[config]"].concat(lines).join("\n") + "\n";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function addWallpaperTheming(lines, mode) {
|
||||
const homeDir = Quickshell.env("HOME");
|
||||
// Noctalia colors JSON
|
||||
lines.push("[templates.noctalia]");
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Templates/noctalia.json"');
|
||||
lines.push('output_path = "' + Settings.configDir + 'colors.json"');
|
||||
|
||||
// Terminal templates
|
||||
TemplateRegistry.terminals.forEach(terminal => {
|
||||
if (isTemplateEnabled(terminal.id)) {
|
||||
lines.push(`\n[templates.${terminal.id}]`);
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${terminal.templatePath}"`);
|
||||
const outputPath = terminal.outputPath.replace("~", homeDir);
|
||||
lines.push(`output_path = "${outputPath}"`);
|
||||
const postHookEsc = escapeTomlString(terminal.postHook);
|
||||
lines.push(`post_hook = "${postHookEsc}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addApplicationTheming(lines, mode) {
|
||||
const homeDir = Quickshell.env("HOME");
|
||||
TemplateRegistry.applications.forEach(app => {
|
||||
if (app.id === "discord") {
|
||||
// Handle Discord clients specially - multiple CSS themes
|
||||
if (isTemplateEnabled("discord")) {
|
||||
const inputs = Array.isArray(app.input) ? app.input : [app.input];
|
||||
inputs.forEach((inputFile, idx) => {
|
||||
// Derive theme suffix from input filename: discord-midnight.css → midnight
|
||||
const themeSuffix = inputFile.replace(/^discord-/, "").replace(/\.css$/, "");
|
||||
app.clients.forEach(client => {
|
||||
if (isDiscordClientEnabled(client.name)) {
|
||||
lines.push(`\n[templates.discord_${themeSuffix}_${client.name}]`);
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${inputFile}"`);
|
||||
// First input uses legacy name for backward compatibility
|
||||
const outputFile = idx === 0 ? "noctalia.theme.css" : `noctalia-${themeSuffix}.theme.css`;
|
||||
const outputPath = client.path.replace("~", homeDir) + `/themes/${outputFile}`;
|
||||
lines.push(`output_path = "${outputPath}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (app.id === "code") {
|
||||
// Handle Code clients specially
|
||||
if (isTemplateEnabled("code")) {
|
||||
app.clients.forEach(client => {
|
||||
// Check if this specific client is detected
|
||||
var resolvedPaths = TemplateRegistry.resolvedCodeClientPaths(client.name);
|
||||
if (isCodeClientEnabled(client.name) && resolvedPaths.length > 0) {
|
||||
resolvedPaths.forEach((resolvedPath, pathIndex) => {
|
||||
var suffix = resolvedPaths.length > 1 ? `_${pathIndex}` : "";
|
||||
lines.push(`\n[templates.code_${client.name}${suffix}]`);
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${app.input}"`);
|
||||
lines.push(`output_path = "${resolvedPath}"`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (app.id === "emacs") {
|
||||
if (isTemplateEnabled("emacs")) {
|
||||
ProgramCheckerService.availableEmacsClients.forEach(client => {
|
||||
lines.push(`\n[templates.emacs_${client.name}]`);
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${app.input}"`);
|
||||
const expandedPath = client.path.replace("~", homeDir) + "/themes/noctalia-theme.el";
|
||||
lines.push(`output_path = "${expandedPath}"`);
|
||||
if (app.postProcess) {
|
||||
const postHook = escapeTomlString(app.postProcess(mode));
|
||||
lines.push(`post_hook = "${postHook}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Handle regular apps
|
||||
if (isTemplateEnabled(app.id)) {
|
||||
app.outputs.forEach((output, idx) => {
|
||||
lines.push(`\n[templates.${app.id}_${idx}]`);
|
||||
const inputFile = output.input || app.input;
|
||||
lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${inputFile}"`);
|
||||
const outputPath = output.path.replace("~", homeDir);
|
||||
lines.push(`output_path = "${outputPath}"`);
|
||||
if (app.postProcess) {
|
||||
const postHook = escapeTomlString(app.postProcess(mode));
|
||||
lines.push(`post_hook = "${postHook}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isDiscordClientEnabled(clientName) {
|
||||
// Check ProgramCheckerService to see if client is detected
|
||||
for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
|
||||
if (ProgramCheckerService.availableDiscordClients[i].name === clientName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCodeClientEnabled(clientName) {
|
||||
// Check ProgramCheckerService to see if client is detected
|
||||
for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) {
|
||||
if (ProgramCheckerService.availableCodeClients[i].name === clientName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get scheme type, defaulting to tonal-spot if not a recognized value
|
||||
function getSchemeType() {
|
||||
const method = Settings.data.colorSchemes.generationMethod;
|
||||
const validKeys = root.schemeTypes.map(scheme => scheme.key);
|
||||
return validKeys.includes(method) ? method : "tonal-spot";
|
||||
}
|
||||
|
||||
function buildGenerationScript(content, wallpaper, mode) {
|
||||
const pathEsc = dynamicConfigPath.replace(/'/g, "'\\''");
|
||||
const wpDelimiter = "WALLPAPER_PATH_EOF_" + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Use heredoc for wallpaper path to avoid all escaping issues
|
||||
let script = `NOCTALIA_WP_PATH=$(cat << '${wpDelimiter}'\n${wallpaper}\n${wpDelimiter}\n)\n`;
|
||||
|
||||
// Run built-in template processor only if there are templates configured
|
||||
if (content) {
|
||||
const delimiter = "THEME_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9);
|
||||
script += `cat > '${pathEsc}' << '${delimiter}'\n${content}\n${delimiter}\n`;
|
||||
|
||||
// Use template-processor.py (Python implementation)
|
||||
// Don't pass --mode so templates get both dark and light colors (e.g., zed.json needs both)
|
||||
// Pass --default-mode so "default" in templates resolves to the current theme mode
|
||||
const schemeType = getSchemeType();
|
||||
script += `python3 "${templateProcessorScript}" "$NOCTALIA_WP_PATH" --scheme-type ${schemeType} --config '${pathEsc}' --default-mode ${mode}\n`;
|
||||
}
|
||||
|
||||
script += buildUserTemplateCommand("$NOCTALIA_WP_PATH", mode);
|
||||
|
||||
return script + "\n";
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// USER TEMPLATES, advanced usage
|
||||
// ================================================================================
|
||||
function buildUserTemplateCommand(input, mode) {
|
||||
if (!Settings.data.templates.enableUserTheming)
|
||||
return "";
|
||||
|
||||
const userConfigPath = getUserConfigPath();
|
||||
let script = "\n# Execute user config if it exists\n";
|
||||
script += `if [ -f '${userConfigPath}' ]; then\n`;
|
||||
// If input is a shell variable (starts with $), use double quotes to allow expansion
|
||||
// Otherwise, use single quotes for safety with file paths
|
||||
const inputQuoted = input.startsWith("$") ? `"${input}"` : `'${input.replace(/'/g, "'\\''")}'`;
|
||||
|
||||
const schemeType = getSchemeType();
|
||||
// Don't pass --mode so user templates get both dark and light colors
|
||||
// Pass --default-mode so "default" in templates resolves to the current theme mode
|
||||
script += ` python3 "${templateProcessorScript}" ${inputQuoted} --scheme-type ${schemeType} --config '${userConfigPath}' --default-mode ${mode}\n`;
|
||||
script += "fi";
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
function buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath) {
|
||||
if (!Settings.data.templates.enableUserTheming)
|
||||
return "";
|
||||
|
||||
const userConfigPath = getUserConfigPath();
|
||||
|
||||
// Reuse the scheme JSON already written by processPredefinedScheme()
|
||||
const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''");
|
||||
const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : "";
|
||||
|
||||
let script = "\n# Execute user templates with predefined scheme colors\n";
|
||||
script += `if [ -f '${userConfigPath}' ]; then\n`;
|
||||
// Use --scheme flag with the already-written scheme JSON
|
||||
// Don't pass --mode so user templates get both dark and light colors
|
||||
// Pass --default-mode so "default" in templates resolves to the current theme mode
|
||||
// Pass wallpaper as positional arg so image_path is available in templates
|
||||
script += ` python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${userConfigPath}' --default-mode ${mode}\n`;
|
||||
script += "fi";
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
function getUserConfigPath() {
|
||||
return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''");
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// DEBOUNCE TIMER
|
||||
// ================================================================================
|
||||
function executePendingRequest() {
|
||||
Logger.d("TemplateProcessor", `executePendingRequest: hasWallpaper=${!!pendingWallpaperRequest}, hasPredefined=${!!pendingPredefinedRequest}`);
|
||||
if (pendingWallpaperRequest) {
|
||||
const req = pendingWallpaperRequest;
|
||||
pendingWallpaperRequest = null;
|
||||
executeWallpaperColors(req.wallpaperPath, req.mode);
|
||||
} else if (pendingPredefinedRequest) {
|
||||
const req = pendingPredefinedRequest;
|
||||
pendingPredefinedRequest = null;
|
||||
executePredefinedScheme(req.schemeData, req.mode, req.wallpaperPath);
|
||||
} else {
|
||||
Logger.d("TemplateProcessor", "executePendingRequest: no pending request");
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Logger.d("TemplateProcessor", `debounceTimer fired: processRunning=${generateProcess.running}`);
|
||||
// Kill any running process before starting new one
|
||||
if (generateProcess.running) {
|
||||
Logger.d("TemplateProcessor", "debounceTimer: stopping running process");
|
||||
generateProcess.running = false;
|
||||
// executePendingRequest will be called from onExited
|
||||
} else {
|
||||
executePendingRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// PROCESSES
|
||||
// ================================================================================
|
||||
Process {
|
||||
id: generateProcess
|
||||
workingDirectory: Quickshell.shellDir
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
// Execute any pending request (handles both kill case and debounce timer interval case)
|
||||
if (pendingWallpaperRequest || pendingPredefinedRequest) {
|
||||
Logger.d("TemplateProcessor", "generateProcess onExited: has pending request, executing");
|
||||
executePendingRequest();
|
||||
} else if (exitCode === 0) {
|
||||
// No pending request and successful completion - emit signal
|
||||
root.colorsGenerated();
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const text = this.text.trim();
|
||||
if (text && text.includes("Template error:")) {
|
||||
const errorLines = text.split("\n").filter(l => l.includes("Template error:"));
|
||||
const errors = errorLines.slice(0, 3).join("\n") + (errorLines.length > 3 ? `\n... (+${errorLines.length - 3} more)` : "");
|
||||
Logger.w("TemplateProcessor", errors);
|
||||
ToastService.showWarning(I18n.tr("toast.theming-processor-failed.title"), errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user