Merge branch 'colorgen': move predefined colorschemes to separate templates

This commit is contained in:
Lemmy
2026-03-05 10:01:43 -05:00
10 changed files with 252 additions and 230 deletions
@@ -0,0 +1,33 @@
# Colors (Noctalia)
[colors.bright]
black = '{{colors.terminal_bright_black.default.hex}}'
blue = '{{colors.terminal_bright_blue.default.hex}}'
cyan = '{{colors.terminal_bright_cyan.default.hex}}'
green = '{{colors.terminal_bright_green.default.hex}}'
magenta = '{{colors.terminal_bright_magenta.default.hex}}'
red = '{{colors.terminal_bright_red.default.hex}}'
white = '{{colors.terminal_bright_white.default.hex}}'
yellow = '{{colors.terminal_bright_yellow.default.hex}}'
[colors.cursor]
cursor = '{{colors.terminal_cursor.default.hex}}'
text = '{{colors.terminal_cursor_text.default.hex}}'
[colors.normal]
black = '{{colors.terminal_normal_black.default.hex}}'
blue = '{{colors.terminal_normal_blue.default.hex}}'
cyan = '{{colors.terminal_normal_cyan.default.hex}}'
green = '{{colors.terminal_normal_green.default.hex}}'
magenta = '{{colors.terminal_normal_magenta.default.hex}}'
red = '{{colors.terminal_normal_red.default.hex}}'
white = '{{colors.terminal_normal_white.default.hex}}'
yellow = '{{colors.terminal_normal_yellow.default.hex}}'
[colors.primary]
background = '{{colors.terminal_background.default.hex}}'
foreground = '{{colors.terminal_foreground.default.hex}}'
[colors.selection]
background = '{{colors.terminal_selection_bg.default.hex}}'
text = '{{colors.terminal_selection_fg.default.hex}}'
+22
View File
@@ -0,0 +1,22 @@
[colors]
foreground={{colors.terminal_foreground.default.hex_stripped}}
background={{colors.terminal_background.default.hex_stripped}}
regular0={{colors.terminal_normal_black.default.hex_stripped}}
regular1={{colors.terminal_normal_red.default.hex_stripped}}
regular2={{colors.terminal_normal_green.default.hex_stripped}}
regular3={{colors.terminal_normal_yellow.default.hex_stripped}}
regular4={{colors.terminal_normal_blue.default.hex_stripped}}
regular5={{colors.terminal_normal_magenta.default.hex_stripped}}
regular6={{colors.terminal_normal_cyan.default.hex_stripped}}
regular7={{colors.terminal_normal_white.default.hex_stripped}}
bright0={{colors.terminal_bright_black.default.hex_stripped}}
bright1={{colors.terminal_bright_red.default.hex_stripped}}
bright2={{colors.terminal_bright_green.default.hex_stripped}}
bright3={{colors.terminal_bright_yellow.default.hex_stripped}}
bright4={{colors.terminal_bright_blue.default.hex_stripped}}
bright5={{colors.terminal_bright_magenta.default.hex_stripped}}
bright6={{colors.terminal_bright_cyan.default.hex_stripped}}
bright7={{colors.terminal_bright_white.default.hex_stripped}}
selection-foreground={{colors.terminal_selection_fg.default.hex_stripped}}
selection-background={{colors.terminal_selection_bg.default.hex_stripped}}
cursor={{colors.terminal_cursor_text.default.hex_stripped}} {{colors.terminal_cursor.default.hex_stripped}}
@@ -0,0 +1,22 @@
palette = 0={{colors.terminal_normal_black.default.hex}}
palette = 1={{colors.terminal_normal_red.default.hex}}
palette = 2={{colors.terminal_normal_green.default.hex}}
palette = 3={{colors.terminal_normal_yellow.default.hex}}
palette = 4={{colors.terminal_normal_blue.default.hex}}
palette = 5={{colors.terminal_normal_magenta.default.hex}}
palette = 6={{colors.terminal_normal_cyan.default.hex}}
palette = 7={{colors.terminal_normal_white.default.hex}}
palette = 8={{colors.terminal_bright_black.default.hex}}
palette = 9={{colors.terminal_bright_red.default.hex}}
palette = 10={{colors.terminal_bright_green.default.hex}}
palette = 11={{colors.terminal_bright_yellow.default.hex}}
palette = 12={{colors.terminal_bright_blue.default.hex}}
palette = 13={{colors.terminal_bright_magenta.default.hex}}
palette = 14={{colors.terminal_bright_cyan.default.hex}}
palette = 15={{colors.terminal_bright_white.default.hex}}
background = {{colors.terminal_background.default.hex}}
foreground = {{colors.terminal_foreground.default.hex}}
cursor-color = {{colors.terminal_cursor.default.hex}}
cursor-text = {{colors.terminal_cursor_text.default.hex}}
selection-background = {{colors.terminal_selection_bg.default.hex}}
selection-foreground = {{colors.terminal_selection_fg.default.hex}}
@@ -0,0 +1,24 @@
color0 {{colors.terminal_normal_black.default.hex}}
color1 {{colors.terminal_normal_red.default.hex}}
color2 {{colors.terminal_normal_green.default.hex}}
color3 {{colors.terminal_normal_yellow.default.hex}}
color4 {{colors.terminal_normal_blue.default.hex}}
color5 {{colors.terminal_normal_magenta.default.hex}}
color6 {{colors.terminal_normal_cyan.default.hex}}
color7 {{colors.terminal_normal_white.default.hex}}
color8 {{colors.terminal_bright_black.default.hex}}
color9 {{colors.terminal_bright_red.default.hex}}
color10 {{colors.terminal_bright_green.default.hex}}
color11 {{colors.terminal_bright_yellow.default.hex}}
color12 {{colors.terminal_bright_blue.default.hex}}
color13 {{colors.terminal_bright_magenta.default.hex}}
color14 {{colors.terminal_bright_cyan.default.hex}}
color15 {{colors.terminal_bright_white.default.hex}}
background {{colors.terminal_background.default.hex}}
selection_foreground {{colors.terminal_cursor_text.default.hex}}
cursor {{colors.terminal_cursor.default.hex}}
cursor_text_color {{colors.terminal_cursor_text.default.hex}}
foreground {{colors.terminal_foreground.default.hex}}
selection_background {{colors.terminal_foreground.default.hex}}
active_border_color {{colors.primary.default.hex}}
inactive_border_color {{colors.secondary.default.hex}}
@@ -0,0 +1,84 @@
[colors]
ansi = [
"{{colors.terminal_normal_black.default.hex}}",
"{{colors.terminal_normal_red.default.hex}}",
"{{colors.terminal_normal_green.default.hex}}",
"{{colors.terminal_normal_yellow.default.hex}}",
"{{colors.terminal_normal_blue.default.hex}}",
"{{colors.terminal_normal_magenta.default.hex}}",
"{{colors.terminal_normal_cyan.default.hex}}",
"{{colors.terminal_normal_white.default.hex}}",
]
background = "{{colors.terminal_background.default.hex}}"
brights = [
"{{colors.terminal_bright_black.default.hex}}",
"{{colors.terminal_bright_red.default.hex}}",
"{{colors.terminal_bright_green.default.hex}}",
"{{colors.terminal_bright_yellow.default.hex}}",
"{{colors.terminal_bright_blue.default.hex}}",
"{{colors.terminal_bright_magenta.default.hex}}",
"{{colors.terminal_bright_cyan.default.hex}}",
"{{colors.terminal_bright_white.default.hex}}",
]
compose_cursor = "{{colors.terminal_cursor.default.hex}}"
cursor_bg = "{{colors.terminal_cursor.default.hex}}"
cursor_border = "{{colors.terminal_cursor.default.hex}}"
cursor_fg = "{{colors.terminal_cursor_text.default.hex}}"
foreground = "{{colors.terminal_foreground.default.hex}}"
scrollbar_thumb = "{{colors.terminal_selection_bg.default.hex}}"
selection_bg = "{{colors.terminal_selection_bg.default.hex}}"
selection_fg = "{{colors.terminal_selection_fg.default.hex}}"
split = "{{colors.terminal_bright_black.default.hex}}"
visual_bell = "{{colors.terminal_normal_black.default.hex}}"
[colors.indexed]
16 = "{{colors.secondary.default.hex}}"
17 = "{{colors.terminal_cursor.default.hex}}"
[colors.tab_bar]
background = "{{colors.terminal_background.default.hex | darken 0.1}}"
inactive_tab_edge = "{{colors.terminal_selection_bg.default.hex}}"
[colors.tab_bar.active_tab]
bg_color = "{{colors.primary.default.hex}}"
fg_color = "{{colors.on_primary.default.hex}}"
intensity = "Normal"
italic = false
strikethrough = false
underline = "None"
[colors.tab_bar.inactive_tab]
bg_color = "{{colors.terminal_background.default.hex | darken 0.05}}"
fg_color = "{{colors.terminal_foreground.default.hex}}"
intensity = "Normal"
italic = false
strikethrough = false
underline = "None"
[colors.tab_bar.inactive_tab_hover]
bg_color = "{{colors.terminal_background.default.hex}}"
fg_color = "{{colors.terminal_foreground.default.hex}}"
intensity = "Normal"
italic = false
strikethrough = false
underline = "None"
[colors.tab_bar.new_tab]
bg_color = "{{colors.terminal_selection_bg.default.hex}}"
fg_color = "{{colors.terminal_foreground.default.hex}}"
intensity = "Normal"
italic = false
strikethrough = false
underline = "None"
[colors.tab_bar.new_tab_hover]
bg_color = "{{colors.terminal_bright_black.default.hex}}"
fg_color = "{{colors.terminal_foreground.default.hex}}"
intensity = "Normal"
italic = false
strikethrough = false
underline = "None"
[metadata]
author = "Noctalia"
name = "Noctalia"
+2 -5
View File
@@ -18,8 +18,7 @@ from .palette import extract_palette
from .quantizer import extract_source_color, source_color_to_rgb
from .theme import generate_theme
from .renderer import TemplateRenderer
from .scheme import expand_predefined_scheme
from .terminal import TerminalColors, TerminalGenerator
from .scheme import expand_predefined_scheme, inject_terminal_colors
__all__ = [
# Color
@@ -55,7 +54,5 @@ __all__ = [
"TemplateRenderer",
# Scheme
"expand_predefined_scheme",
# Terminal
"TerminalColors",
"TerminalGenerator",
"inject_terminal_colors",
]
+41
View File
@@ -308,3 +308,44 @@ def expand_predefined_scheme(scheme_data: dict[str, str], mode: ThemeMode) -> di
"background": background.to_hex(),
"on_background": on_background.to_hex(),
}
def inject_terminal_colors(result: dict[str, str], scheme_mode_data: dict) -> dict[str, str]:
"""Flatten scheme's terminal section into template-ready color keys.
Adds keys like terminal_foreground, terminal_normal_black, terminal_bright_red, etc.
so predefined terminal templates can reference them as
{{colors.terminal_foreground.default.hex_stripped}}.
Args:
result: Expanded color palette dict to augment.
scheme_mode_data: Raw scheme JSON mode data (e.g., scheme_data["dark"]).
Returns:
The same result dict with terminal_ keys added.
"""
terminal = scheme_mode_data.get("terminal")
if not terminal:
return result
# Map of JSON keys to flattened key names
direct_keys = {
"foreground": "terminal_foreground",
"background": "terminal_background",
"cursor": "terminal_cursor",
"cursorText": "terminal_cursor_text",
"selectionFg": "terminal_selection_fg",
"selectionBg": "terminal_selection_bg",
}
for json_key, flat_key in direct_keys.items():
if json_key in terminal:
result[flat_key] = terminal[json_key]
# ANSI normal/bright color groups
for group in ("normal", "bright"):
if group in terminal:
for name, hex_val in terminal[group].items():
result[f"terminal_{group}_{name}"] = hex_val
return result
@@ -54,8 +54,8 @@ from lib import (
read_image, ImageReadError, extract_palette, generate_theme,
TemplateRenderer, expand_predefined_scheme,
extract_source_color, source_color_to_rgb, Color,
TerminalColors, TerminalGenerator
)
from lib.scheme import inject_terminal_colors
def parse_args() -> argparse.Namespace:
@@ -144,12 +144,6 @@ Examples:
help='Theme mode to use for "default" in templates (default: dark)'
)
parser.add_argument(
'--terminal-output',
type=str,
help='JSON mapping of terminal IDs to output paths: {"foot": "/path/to/output", ...}'
)
return parser.parse_args()
@@ -188,9 +182,11 @@ def main() -> int:
if mode in scheme_data:
# Multi-mode format
result[mode] = expand_predefined_scheme(scheme_data[mode], mode)
inject_terminal_colors(result[mode], scheme_data[mode])
elif "mPrimary" in scheme_data:
# Single-mode format - use same colors for requested mode
result[mode] = expand_predefined_scheme(scheme_data, mode)
inject_terminal_colors(result[mode], scheme_data)
else:
print(f"Error: Invalid scheme format - missing '{mode}' or 'mPrimary'", file=sys.stderr)
return 1
@@ -344,52 +340,6 @@ def main() -> int:
else:
renderer.process_config_file(args.config)
# Process terminal output if specified
if args.terminal_output and args.scheme:
try:
terminal_outputs = json.loads(args.terminal_output)
except json.JSONDecodeError as e:
print(f"Error parsing --terminal-output JSON: {e}", file=sys.stderr)
return 1
# Load scheme to check for terminal section
with open(args.scheme, 'r') as f:
scheme_data = json.load(f)
# Determine which mode to use for terminal colors
mode = args.default_mode
# Check if scheme has terminal colors
mode_data = scheme_data.get(mode, scheme_data)
if "terminal" not in mode_data:
print(f"Warning: Scheme has no 'terminal' section for mode '{mode}'", file=sys.stderr)
return 0
try:
# Extract scheme UI colors for derivation (mPrimary, mOnPrimary, mSecondary)
scheme_colors = {
"mPrimary": mode_data.get("mPrimary"),
"mOnPrimary": mode_data.get("mOnPrimary"),
"mSecondary": mode_data.get("mSecondary"),
}
terminal_colors = TerminalColors.from_dict(mode_data["terminal"], scheme_colors)
generator = TerminalGenerator(terminal_colors)
for terminal_id, output_path in terminal_outputs.items():
try:
content = generator.generate(terminal_id)
output_file = Path(output_path).expanduser()
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(content)
except ValueError as e:
print(f"Error generating {terminal_id}: {e}", file=sys.stderr)
except IOError as e:
print(f"Error writing {output_path}: {e}", file=sys.stderr)
except KeyError as e:
print(f"Error: Missing required terminal color: {e}", file=sys.stderr)
return 1
return 0
+16 -172
View File
@@ -129,10 +129,7 @@ Singleton {
}
function executePredefinedScheme(schemeData, mode, wallpaperPath) {
// 1. Handle terminal themes (runtime generation or pre-rendered file copy)
handleTerminalThemes(schemeData, mode);
// 2. Build TOML config for application templates
// 1. Build TOML config for application templates (including terminals)
const tomlContent = buildPredefinedTemplateConfig(mode);
if (!tomlContent) {
Logger.d("TemplateProcessor", "No application templates enabled for predefined scheme");
@@ -178,6 +175,20 @@ Singleton {
*/
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) {
@@ -338,162 +349,7 @@ Singleton {
return script + "\n";
}
// ================================================================================
// PREDEFINED COLOR SCHEMES
// TERMINAL THEMES (dual-path: runtime generation or legacy pre-rendered file copy)
// ================================================================================
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(schemeData, mode) {
const homeDir = Quickshell.env("HOME");
// Check if scheme has terminal section (new format)
const modeData = schemeData[mode] || schemeData;
const hasTerminalSection = modeData && modeData.terminal;
if (hasTerminalSection) {
// New path: runtime generation from JSON terminal colors
handleTerminalThemesGenerate(schemeData, mode, homeDir);
} else {
// Old path: copy pre-rendered files (backward compatibility for DLC schemes)
handleTerminalThemesCopy(mode, homeDir);
}
}
/**
* New path: Generate terminal themes at runtime from scheme's terminal section
*/
function handleTerminalThemesGenerate(schemeData, mode, homeDir) {
// Build terminal output mapping for enabled terminals
const terminalOutputs = {};
TemplateRegistry.terminals.forEach(terminal => {
if (isTemplateEnabled(terminal.id)) {
const outputPath = terminal.outputPath.replace("~", homeDir);
terminalOutputs[terminal.id] = outputPath;
}
});
if (Object.keys(terminalOutputs).length === 0) {
Logger.d("TemplateProcessor", "No terminal templates enabled for generation");
return;
}
// Write scheme JSON to temp file and call Python with --terminal-output
const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''");
const schemeDelimiter = "SCHEME_JSON_EOF_" + Math.random().toString(36).substr(2, 9);
let script = "";
// Write scheme JSON
script += `cat > '${schemeJsonPathEsc}' << '${schemeDelimiter}'\n`;
script += JSON.stringify(schemeData, null, 2) + "\n";
script += `${schemeDelimiter}\n`;
// Create output directories
Object.values(terminalOutputs).forEach(path => {
const dir = path.substring(0, path.lastIndexOf('/'));
script += `mkdir -p ${escapeShellPath(dir)}; `;
});
// Run Python with terminal generation
const terminalOutputsJson = JSON.stringify(terminalOutputs).replace(/'/g, "'\\''");
script += `python3 "${templateProcessorScript}" --scheme '${schemeJsonPathEsc}' --default-mode ${mode} --terminal-output '${terminalOutputsJson}'; `;
// Run post-hooks for enabled terminals
TemplateRegistry.terminals.forEach(terminal => {
if (isTemplateEnabled(terminal.id)) {
script += `${terminal.postHook}; `;
}
});
copyProcess.command = ["sh", "-c", script];
copyProcess.running = true;
}
/**
* Old path: Copy pre-rendered terminal files (backward compatibility)
* Should be removed in late february 2026
*/
function handleTerminalThemesCopy(mode, homeDir) {
const commands = [];
TemplateRegistry.terminals.forEach(terminal => {
if (isTemplateEnabled(terminal.id)) {
const outputPath = terminal.outputPath.replace("~", homeDir);
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
const templatePaths = getTerminalColorsTemplate(terminal.id, 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.id} (tried both hyphen and space patterns)"; fi`);
// Always use the apply script to set the theme and attempt hot reloading
commands.push(terminal.postHook);
}
});
if (commands.length > 0) {
copyProcess.command = ["sh", "-c", commands.join('; ')];
copyProcess.running = true;
}
}
function getTerminalColorsTemplate(terminal, mode) {
const schemeNameMap = ({
"Noctalia (default)": "Noctalia-default",
"Noctalia (legacy)": "Noctalia-legacy",
"Tokyo Night": "Tokyo-Night",
"Rose Pine": "Rosepine"
});
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
@@ -611,17 +467,5 @@ Singleton {
}
}
// ------------
Process {
id: copyProcess
workingDirectory: Quickshell.shellDir
running: false
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
Logger.e("TemplateProcessor", "copyProcess stderr:", this.text);
}
}
}
}
}
+5
View File
@@ -23,6 +23,7 @@ Singleton {
"id": "foot",
"name": "Foot",
"templatePath": "terminal/foot",
"predefinedTemplatePath": "terminal/foot-predefined",
"outputPath": "~/.config/foot/themes/noctalia",
"postHook": `${templateApplyScript} foot`
},
@@ -30,6 +31,7 @@ Singleton {
"id": "ghostty",
"name": "Ghostty",
"templatePath": "terminal/ghostty",
"predefinedTemplatePath": "terminal/ghostty-predefined",
"outputPath": "~/.config/ghostty/themes/noctalia",
"postHook": `${templateApplyScript} ghostty`
},
@@ -37,6 +39,7 @@ Singleton {
"id": "kitty",
"name": "Kitty",
"templatePath": "terminal/kitty.conf",
"predefinedTemplatePath": "terminal/kitty-predefined.conf",
"outputPath": "~/.config/kitty/themes/noctalia.conf",
"postHook": `${templateApplyScript} kitty`
},
@@ -44,6 +47,7 @@ Singleton {
"id": "alacritty",
"name": "Alacritty",
"templatePath": "terminal/alacritty.toml",
"predefinedTemplatePath": "terminal/alacritty-predefined.toml",
"outputPath": "~/.config/alacritty/themes/noctalia.toml",
"postHook": `${templateApplyScript} alacritty`
},
@@ -51,6 +55,7 @@ Singleton {
"id": "wezterm",
"name": "Wezterm",
"templatePath": "terminal/wezterm.toml",
"predefinedTemplatePath": "terminal/wezterm-predefined.toml",
"outputPath": "~/.config/wezterm/colors/Noctalia.toml",
"postHook": `${templateApplyScript} wezterm`
}