mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
659 lines
32 KiB
QML
659 lines
32 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Services.System
|
|
import qs.Services.Theming
|
|
import qs.Services.UI
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string dynamicConfigPath: Settings.cacheDir + "matugen.dynamic.toml"
|
|
|
|
readonly property var schemeNameMap: ({
|
|
"Noctalia (default)": "Noctalia-default",
|
|
"Noctalia (legacy)": "Noctalia-legacy",
|
|
"Tokyo Night": "Tokyo-Night",
|
|
"Rose Pine": "Rosepine"
|
|
})
|
|
|
|
readonly property var terminalPaths: ({
|
|
"foot": "~/.config/foot/themes/noctalia",
|
|
"ghostty": "~/.config/ghostty/themes/noctalia",
|
|
"kitty": "~/.config/kitty/themes/noctalia.conf",
|
|
"alacritty": "~/.config/alacritty/themes/noctalia.toml",
|
|
"wezterm": "~/.config/wezterm/colors/Noctalia.toml"
|
|
})
|
|
|
|
function escapeTomlString(value) {
|
|
if (!value)
|
|
return "";
|
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
}
|
|
|
|
/**
|
|
* Process wallpaper colors using matugen CLI
|
|
* Dual-path architecture (wallpaper uses matugen CLI)
|
|
*/
|
|
function processWallpaperColors(wallpaperPath, mode) {
|
|
const content = buildMatugenConfig();
|
|
if (!content)
|
|
return;
|
|
const wp = wallpaperPath.replace(/'/g, "'\\''");
|
|
const script = buildMatugenScript(content, wp, mode);
|
|
|
|
generateProcess.generator = "matugen";
|
|
generateProcess.command = ["sh", "-lc", script];
|
|
generateProcess.running = true;
|
|
}
|
|
|
|
// Queue for processing templates one by one
|
|
property var templateQueue: []
|
|
property var currentTemplateContext: null
|
|
|
|
/**
|
|
* Process predefined color scheme using sed scripts
|
|
* Dual-path architecture (predefined uses sed scripts)
|
|
* Templates are processed one by one for better error reporting
|
|
*/
|
|
function processPredefinedScheme(schemeData, mode) {
|
|
handleTerminalThemes(mode);
|
|
|
|
const colors = schemeData[mode];
|
|
const homeDir = Quickshell.env("HOME");
|
|
|
|
// Build queue of templates to process
|
|
templateQueue = buildTemplateQueue(colors, mode, schemeData, homeDir);
|
|
|
|
// Add user templates if enabled
|
|
const userScript = buildUserTemplateCommandForPredefined(schemeData, mode);
|
|
if (userScript) {
|
|
templateQueue.push({
|
|
id: "user-templates",
|
|
script: userScript
|
|
});
|
|
}
|
|
|
|
// Start processing
|
|
processNextTemplate();
|
|
}
|
|
|
|
function buildTemplateQueue(colors, mode, schemeData, homeDir) {
|
|
const queue = [];
|
|
|
|
TemplateRegistry.applications.forEach(app => {
|
|
if (app.id === "discord") {
|
|
if (Settings.data.templates.discord) {
|
|
const items = buildDiscordTemplateItems(app, colors, homeDir);
|
|
items.forEach(item => queue.push(item));
|
|
}
|
|
} else if (app.id === "code") {
|
|
if (Settings.data.templates.code) {
|
|
const items = buildCodeTemplateItems(app, colors, homeDir);
|
|
items.forEach(item => queue.push(item));
|
|
}
|
|
} else {
|
|
if (Settings.data.templates[app.id]) {
|
|
const items = buildAppTemplateItems(app, colors, mode, homeDir, schemeData);
|
|
items.forEach(item => queue.push(item));
|
|
}
|
|
}
|
|
});
|
|
return queue;
|
|
}
|
|
|
|
function processNextTemplate() {
|
|
if (templateQueue.length === 0) {
|
|
currentTemplateContext = null;
|
|
return;
|
|
}
|
|
|
|
const item = templateQueue.shift();
|
|
currentTemplateContext = item;
|
|
|
|
templateProcess.command = ["sh", "-lc", item.script];
|
|
templateProcess.running = true;
|
|
}
|
|
|
|
// ================================================================================
|
|
// WALLPAPER-BASED GENERATION (matugen CLI)
|
|
// ================================================================================
|
|
function buildMatugenConfig() {
|
|
var lines = [];
|
|
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
|
|
|
|
if (Settings.data.colorSchemes.useWallpaperColors) {
|
|
addWallpaperTemplates(lines, mode);
|
|
}
|
|
|
|
addApplicationTemplates(lines, mode);
|
|
|
|
if (lines.length > 0) {
|
|
return ["[config]"].concat(lines).join("\n") + "\n";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function addWallpaperTemplates(lines, mode) {
|
|
const homeDir = Quickshell.env("HOME");
|
|
// Noctalia colors JSON
|
|
lines.push("[templates.noctalia]");
|
|
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/noctalia.json"');
|
|
lines.push('output_path = "' + Settings.configDir + 'colors.json"');
|
|
|
|
// Terminal templates
|
|
TemplateRegistry.terminals.forEach(terminal => {
|
|
if (Settings.data.templates[terminal.id]) {
|
|
lines.push(`\n[templates.${terminal.id}]`);
|
|
lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${terminal.matugenPath}"`);
|
|
const outputPath = terminal.outputPath.replace("~", homeDir);
|
|
lines.push(`output_path = "${outputPath}"`);
|
|
const postHook = terminal.postHook || `${TemplateRegistry.colorsApplyScript} ${terminal.id}`;
|
|
const postHookEsc = escapeTomlString(postHook);
|
|
lines.push(`post_hook = "${postHookEsc}"`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function addApplicationTemplates(lines, mode) {
|
|
const homeDir = Quickshell.env("HOME");
|
|
TemplateRegistry.applications.forEach(app => {
|
|
if (app.id === "discord") {
|
|
// Handle Discord clients specially
|
|
if (Settings.data.templates.discord) {
|
|
app.clients.forEach(client => {
|
|
// Check if this specific client is detected
|
|
if (isDiscordClientEnabled(client.name)) {
|
|
lines.push(`\n[templates.discord_${client.name}]`);
|
|
lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`);
|
|
const outputPath = client.path.replace("~", homeDir) + "/themes/noctalia.theme.css";
|
|
lines.push(`output_path = "${outputPath}"`);
|
|
}
|
|
});
|
|
}
|
|
} else if (app.id === "code") {
|
|
// Handle Code clients specially
|
|
if (Settings.data.templates.code) {
|
|
app.clients.forEach(client => {
|
|
// Check if this specific client is detected
|
|
if (isCodeClientEnabled(client.name)) {
|
|
lines.push(`\n[templates.code_${client.name}]`);
|
|
lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`);
|
|
const expandedPath = client.path.replace("~", homeDir);
|
|
lines.push(`output_path = "${expandedPath}"`);
|
|
}
|
|
});
|
|
}
|
|
} else if (app.id === "emacs" && app.checkDoomFirst) {
|
|
if (Settings.data.templates.emacs) {
|
|
const doomPathTemplate = app.outputs[0].path; // ~/.config/doom/themes/noctalia-theme.el
|
|
const standardPathTemplate = app.outputs[1].path; // ~/.emacs.d/themes/noctalia-theme.el
|
|
const doomPath = doomPathTemplate.replace("~", homeDir);
|
|
const standardPath = standardPathTemplate.replace("~", homeDir);
|
|
const doomConfigDir = `${homeDir}/.config/doom`;
|
|
const doomDir = doomPath.substring(0, doomPath.lastIndexOf('/'));
|
|
|
|
lines.push(`\n[templates.emacs]`);
|
|
lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`);
|
|
lines.push(`output_path = "${standardPath}"`);
|
|
// Move to doom if doom exists, then remove empty .emacs.d/themes and .emacs.d directories
|
|
// Check directories are empty before removing
|
|
const postHook = `sh -c 'if [ -d "${doomConfigDir}" ] && [ -f "${standardPath}" ]; then mkdir -p "${doomDir}" && mv "${standardPath}" "${doomPath}" && rmdir "${homeDir}/.emacs.d/themes" 2>/dev/null && rmdir "${homeDir}/.emacs.d" 2>/dev/null || true; fi'`;
|
|
const postHookEsc = escapeTomlString(postHook);
|
|
lines.push(`post_hook = "${postHookEsc}"`);
|
|
}
|
|
} else {
|
|
// Handle regular apps
|
|
if (Settings.data.templates[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/MatugenTemplates/${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;
|
|
}
|
|
|
|
function buildMatugenScript(content, wallpaper, mode) {
|
|
const delimiter = "MATUGEN_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9);
|
|
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 = `cat > '${pathEsc}' << '${delimiter}'\n${content}\n${delimiter}\n`;
|
|
script += `NOCTALIA_WP_PATH=$(cat << '${wpDelimiter}'\n${wallpaper}\n${wpDelimiter}\n)\n`;
|
|
script += 'matugen ';
|
|
if (ProgramCheckerService.matugenVersion >= "3.1.0") {
|
|
// Matugen 3.1.0+ supports --continue-on-error to process all templates even if some fail
|
|
script += '--continue-on-error ';
|
|
}
|
|
script += `image "$NOCTALIA_WP_PATH" --config '${pathEsc}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}`;
|
|
script += buildUserTemplateCommand("$NOCTALIA_WP_PATH", mode);
|
|
|
|
return script + "\n";
|
|
}
|
|
|
|
// ================================================================================
|
|
// PREDEFINED SCHEME GENERATION (queue-based, template by template)
|
|
// ================================================================================
|
|
function buildDiscordTemplateItems(discordApp, colors, homeDir) {
|
|
const items = [];
|
|
const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false);
|
|
|
|
discordApp.clients.forEach(client => {
|
|
if (!isDiscordClientEnabled(client.name))
|
|
return;
|
|
|
|
const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${discordApp.input}`;
|
|
const outputPath = `${client.path}/themes/noctalia.theme.css`.replace("~", homeDir);
|
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
const baseConfigDir = outputDir.replace("/themes", "");
|
|
|
|
let script = "";
|
|
script += `if [ -d "${baseConfigDir}" ]; then\n`;
|
|
script += ` mkdir -p ${outputDir}\n`;
|
|
script += ` cp '${templatePath}' '${outputPath}'\n`;
|
|
script += ` ${replaceColorsInFile(outputPath, palette)}`;
|
|
script += `fi\n`;
|
|
|
|
items.push({
|
|
id: `discord-${client.name}`,
|
|
outputPath: outputPath,
|
|
script: script
|
|
});
|
|
});
|
|
|
|
return items;
|
|
}
|
|
|
|
function buildCodeTemplateItems(codeApp, colors, homeDir) {
|
|
const items = [];
|
|
const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false);
|
|
|
|
codeApp.clients.forEach(client => {
|
|
if (!isCodeClientEnabled(client.name))
|
|
return;
|
|
|
|
const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${codeApp.input}`;
|
|
const outputPath = client.path.replace("~", homeDir);
|
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
|
|
let baseConfigDir = "";
|
|
if (client.name === "code") {
|
|
baseConfigDir = homeDir + "/.vscode";
|
|
} else if (client.name === "codium") {
|
|
baseConfigDir = homeDir + "/.vscode-oss";
|
|
}
|
|
|
|
let script = "";
|
|
script += `if [ -d "${baseConfigDir}" ]; then\n`;
|
|
script += ` mkdir -p ${outputDir}\n`;
|
|
script += ` cp '${templatePath}' '${outputPath}'\n`;
|
|
script += ` ${replaceColorsInFile(outputPath, palette)}`;
|
|
script += `fi\n`;
|
|
|
|
items.push({
|
|
id: `code-${client.name}`,
|
|
outputPath: outputPath,
|
|
script: script
|
|
});
|
|
});
|
|
|
|
return items;
|
|
}
|
|
|
|
function buildAppTemplateItems(app, colors, mode, homeDir, schemeData) {
|
|
const items = [];
|
|
const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, app.strict || false);
|
|
|
|
const hasDualModePatterns = app.dualMode || false;
|
|
let darkPalette, lightPalette;
|
|
if (hasDualModePatterns && schemeData) {
|
|
darkPalette = ColorPaletteGenerator.generatePalette(schemeData.dark, true, app.strict || false);
|
|
lightPalette = ColorPaletteGenerator.generatePalette(schemeData.light, false, app.strict || false);
|
|
}
|
|
|
|
if (app.id === "emacs" && app.checkDoomFirst) {
|
|
const doomPath = app.outputs[0].path.replace("~", homeDir);
|
|
const doomDir = doomPath.substring(0, doomPath.lastIndexOf('/'));
|
|
const doomConfigDir = doomDir.substring(0, doomDir.lastIndexOf('/'));
|
|
const standardPath = app.outputs[1].path.replace("~", homeDir);
|
|
const standardDir = standardPath.substring(0, standardPath.lastIndexOf('/'));
|
|
const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}`;
|
|
|
|
let script = "";
|
|
script += `if [ -d "${doomConfigDir}" ]; then\n`;
|
|
script += ` mkdir -p ${doomDir}\n`;
|
|
script += ` cp '${templatePath}' '${doomPath}'\n`;
|
|
script += replaceColorsInFile(doomPath, palette);
|
|
script += `else\n`;
|
|
script += ` mkdir -p ${standardDir}\n`;
|
|
script += ` cp '${templatePath}' '${standardPath}'\n`;
|
|
script += replaceColorsInFile(standardPath, palette);
|
|
script += `fi\n`;
|
|
|
|
items.push({
|
|
id: app.id,
|
|
outputPath: doomPath,
|
|
script: script
|
|
});
|
|
} else {
|
|
app.outputs.forEach((output, idx) => {
|
|
const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}`;
|
|
const outputPath = output.path.replace("~", homeDir);
|
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
|
|
let script = "";
|
|
script += `mkdir -p ${outputDir}\n`;
|
|
const templateFile = output.input ? `${Quickshell.shellDir}/Assets/MatugenTemplates/${output.input}` : templatePath;
|
|
script += `cp '${templateFile}' '${outputPath}'\n`;
|
|
script += replaceColorsInFile(outputPath, palette);
|
|
if (hasDualModePatterns && darkPalette && lightPalette) {
|
|
script += replaceColorsInFileWithMode(outputPath, darkPalette, lightPalette);
|
|
}
|
|
|
|
// Add postProcess only on last output
|
|
if (app.postProcess && idx === app.outputs.length - 1) {
|
|
script += app.postProcess(mode);
|
|
}
|
|
|
|
items.push({
|
|
id: app.outputs.length > 1 ? `${app.id}-${idx}` : app.id,
|
|
outputPath: outputPath,
|
|
script: script
|
|
});
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function replaceColorsInFile(filePath, colors) {
|
|
let expressions = [];
|
|
|
|
Object.keys(colors).forEach(colorKey => {
|
|
const hexValue = colors[colorKey].default.hex;
|
|
const hexStrippedValue = colors[colorKey].default.hex_stripped;
|
|
|
|
const escapedHex = hexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const escapedHexStripped = hexStrippedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
// Batch all replacements into a single sed command to avoid ARG_MAX limits
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.default\\.hex_stripped}}/${escapedHexStripped}/g'`);
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.default\\.hex}}/${escapedHex}/g'`);
|
|
});
|
|
return `sed -i ${expressions.join(' ')} '${filePath}'\n`;
|
|
}
|
|
|
|
function replaceColorsInFileWithMode(filePath, darkColors, lightColors) {
|
|
let expressions = [];
|
|
|
|
// Replace dark mode patterns
|
|
Object.keys(darkColors).forEach(colorKey => {
|
|
const hexValue = darkColors[colorKey].default.hex;
|
|
const hexStrippedValue = darkColors[colorKey].default.hex_stripped;
|
|
const escapedHex = hexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const escapedHexStripped = hexStrippedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.hex_stripped}}/${escapedHexStripped}/g'`);
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.hex}}/${escapedHex}/g'`);
|
|
});
|
|
|
|
// Replace light mode patterns
|
|
Object.keys(lightColors).forEach(colorKey => {
|
|
const hexValue = lightColors[colorKey].default.hex;
|
|
const hexStrippedValue = lightColors[colorKey].default.hex_stripped;
|
|
const escapedHex = hexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const escapedHexStripped = hexStrippedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.hex_stripped}}/${escapedHexStripped}/g'`);
|
|
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.hex}}/${escapedHex}/g'`);
|
|
});
|
|
|
|
// Batch all replacements into a single sed command to avoid ARG_MAX limits
|
|
return `sed -i ${expressions.join(' ')} '${filePath}'\n`;
|
|
}
|
|
|
|
// ================================================================================
|
|
// TERMINAL THEMES (predefined schemes use pre-rendered files)
|
|
// ================================================================================
|
|
function escapeShellPath(path) {
|
|
// Escape single quotes by ending the quoted string, adding an escaped quote, and starting a new quoted string
|
|
return "'" + path.replace(/'/g, "'\\''") + "'";
|
|
}
|
|
|
|
function handleTerminalThemes(mode) {
|
|
const commands = [];
|
|
const homeDir = Quickshell.env("HOME");
|
|
|
|
Object.keys(terminalPaths).forEach(terminal => {
|
|
if (Settings.data.templates[terminal]) {
|
|
const outputPath = terminalPaths[terminal].replace("~", homeDir);
|
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
const templatePaths = getTerminalColorsTemplate(terminal, mode);
|
|
|
|
commands.push(`mkdir -p ${escapeShellPath(outputDir)}`);
|
|
// Try hyphen first (most common), then space (for schemes like "Rosey AMOLED")
|
|
const hyphenPath = escapeShellPath(templatePaths.hyphen);
|
|
const spacePath = escapeShellPath(templatePaths.space);
|
|
commands.push(`if [ -f ${hyphenPath} ]; then cp -f ${hyphenPath} ${escapeShellPath(outputPath)}; elif [ -f ${spacePath} ]; then cp -f ${spacePath} ${escapeShellPath(outputPath)}; else echo "ERROR: Template file not found for ${terminal} (tried both hyphen and space patterns)"; fi`);
|
|
commands.push(`${TemplateRegistry.colorsApplyScript} ${terminal}`);
|
|
}
|
|
});
|
|
|
|
if (commands.length > 0) {
|
|
copyProcess.command = ["sh", "-lc", commands.join('; ')];
|
|
copyProcess.running = true;
|
|
}
|
|
}
|
|
|
|
function getTerminalColorsTemplate(terminal, mode) {
|
|
let colorScheme = Settings.data.colorSchemes.predefinedScheme;
|
|
colorScheme = schemeNameMap[colorScheme] || colorScheme;
|
|
|
|
let extension = "";
|
|
if (terminal === 'kitty') {
|
|
extension = ".conf";
|
|
} else if (terminal === 'wezterm') {
|
|
extension = ".toml";
|
|
}
|
|
|
|
// Support both naming conventions: "SchemeName-dark" (hyphen) and "SchemeName dark" (space)
|
|
const fileNameHyphen = `${colorScheme}-${mode}${extension}`;
|
|
const fileNameSpace = `${colorScheme} ${mode}${extension}`;
|
|
const relativePathHyphen = `terminal/${terminal}/${fileNameHyphen}`;
|
|
const relativePathSpace = `terminal/${terminal}/${fileNameSpace}`;
|
|
|
|
// Try to find the scheme in the loaded schemes list to determine which directory it's in
|
|
for (let i = 0; i < ColorSchemeService.schemes.length; i++) {
|
|
const schemeJsonPath = ColorSchemeService.schemes[i];
|
|
// Check if this is the scheme we're looking for
|
|
if (schemeJsonPath.indexOf(`/${colorScheme}/`) !== -1 || schemeJsonPath.indexOf(`/${colorScheme}.json`) !== -1) {
|
|
// Extract the scheme directory from the JSON path
|
|
// JSON path is like: /path/to/scheme/SchemeName/SchemeName.json
|
|
// We need: /path/to/scheme/SchemeName/terminal/...
|
|
const schemeDir = schemeJsonPath.substring(0, schemeJsonPath.lastIndexOf('/'));
|
|
return {
|
|
hyphen: `${schemeDir}/${relativePathHyphen}`,
|
|
space: `${schemeDir}/${relativePathSpace}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// Fallback: try downloaded first, then preinstalled
|
|
const downloadedPathHyphen = `${ColorSchemeService.downloadedSchemesDirectory}/${colorScheme}/${relativePathHyphen}`;
|
|
const downloadedPathSpace = `${ColorSchemeService.downloadedSchemesDirectory}/${colorScheme}/${relativePathSpace}`;
|
|
const preinstalledPathHyphen = `${ColorSchemeService.schemesDirectory}/${colorScheme}/${relativePathHyphen}`;
|
|
const preinstalledPathSpace = `${ColorSchemeService.schemesDirectory}/${colorScheme}/${relativePathSpace}`;
|
|
|
|
return {
|
|
hyphen: preinstalledPathHyphen,
|
|
space: preinstalledPathSpace
|
|
};
|
|
}
|
|
|
|
// ================================================================================
|
|
// USER TEMPLATES, advanced usage
|
|
// ================================================================================
|
|
function buildUserTemplateCommand(input, mode) {
|
|
if (!Settings.data.templates.enableUserTemplates)
|
|
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, "'\\''")}'`;
|
|
script += ` matugen image ${inputQuoted} --config '${userConfigPath}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}\n`;
|
|
script += "fi";
|
|
|
|
return script;
|
|
}
|
|
|
|
function buildUserTemplateCommandForPredefined(schemeData, mode) {
|
|
if (!Settings.data.templates.enableUserTemplates)
|
|
return "";
|
|
|
|
const userConfigPath = getUserConfigPath();
|
|
const colors = schemeData[mode];
|
|
const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false);
|
|
|
|
const tempJsonPath = Settings.cacheDir + "predefined-colors.json";
|
|
const tempJsonPathEsc = tempJsonPath.replace(/'/g, "'\\''");
|
|
|
|
let script = "\n# Execute user templates with predefined scheme colors\n";
|
|
script += `if [ -f '${userConfigPath}' ]; then\n`;
|
|
script += ` cat > '${tempJsonPathEsc}' << 'EOF'\n`;
|
|
script += JSON.stringify({
|
|
"colors": palette
|
|
}, null, 2) + "\n";
|
|
script += "EOF\n";
|
|
script += ` matugen json '${tempJsonPathEsc}' --config '${userConfigPath}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}\n`;
|
|
script += "fi";
|
|
|
|
return script;
|
|
}
|
|
|
|
function getUserConfigPath() {
|
|
return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''");
|
|
}
|
|
|
|
// ================================================================================
|
|
// PROCESSES
|
|
// ================================================================================
|
|
Process {
|
|
id: generateProcess
|
|
workingDirectory: Quickshell.shellDir
|
|
running: false
|
|
|
|
// Error reporting helpers
|
|
property string generator: ""
|
|
|
|
function buildErrorMessage() {
|
|
const description = (stderr.text && stderr.text.trim() !== "") ? stderr.text.trim() : ((stdout.text && stdout.text.trim() !== "") ? stdout.text.trim() : I18n.tr("toast.theming-processor-failed.desc-generic"));
|
|
const title = I18n.tr(`toast.theming-processor-failed.title-${generator}`);
|
|
return description;
|
|
}
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode !== 0) {
|
|
const description = generateProcess.buildErrorMessage();
|
|
Logger.e("TemplateProcessor", `Process failed (generator: ${generator}) with exit code`, exitCode, description);
|
|
Logger.d("TemplateProcessor", "Failed command:", command.join(" ").substring(0, 500));
|
|
}
|
|
}
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
if (this.text)
|
|
Logger.d("TemplateProcessor", "stdout:", this.text);
|
|
}
|
|
}
|
|
|
|
stderr: StdioCollector {
|
|
onStreamFinished: {}
|
|
}
|
|
}
|
|
|
|
// ------------
|
|
// Process for queue-based template processing (predefined schemes)
|
|
Process {
|
|
id: templateProcess
|
|
workingDirectory: Quickshell.shellDir
|
|
running: false
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode !== 0) {
|
|
const ctx = currentTemplateContext;
|
|
const errText = stderr.text ? stderr.text.trim() : "";
|
|
const outText = stdout.text ? stdout.text.trim() : "";
|
|
const description = errText || outText || "Unknown error";
|
|
|
|
Logger.e("TemplateProcessor", `Template "${ctx?.id}" failed (exit code ${exitCode}): ${description}`);
|
|
if (ctx?.outputPath) {
|
|
Logger.e("TemplateProcessor", ` Output path: ${ctx.outputPath}`);
|
|
}
|
|
Logger.d("TemplateProcessor", ` Script: ${ctx?.script?.substring(0, 300)}`);
|
|
}
|
|
|
|
// Continue with next template regardless of success/failure
|
|
processNextTemplate();
|
|
}
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {}
|
|
}
|
|
|
|
stderr: StdioCollector {
|
|
onStreamFinished: {}
|
|
}
|
|
}
|
|
|
|
// ------------
|
|
Process {
|
|
id: copyProcess
|
|
workingDirectory: Quickshell.shellDir
|
|
running: false
|
|
stderr: StdioCollector {
|
|
onStreamFinished: {
|
|
if (this.text) {
|
|
Logger.e("TemplateProcessor", "copyProcess stderr:", this.text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|