mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
template-processor: removed the old 'sed' implementation and moved it to python
This commit is contained in:
@@ -17,6 +17,7 @@ from .image import read_image, ImageReadError
|
||||
from .palette import extract_palette
|
||||
from .theme import generate_theme
|
||||
from .renderer import TemplateRenderer
|
||||
from .scheme import expand_predefined_scheme
|
||||
|
||||
__all__ = [
|
||||
# Color
|
||||
@@ -44,4 +45,6 @@ __all__ = [
|
||||
"generate_theme",
|
||||
# Renderer
|
||||
"TemplateRenderer",
|
||||
# Scheme
|
||||
"expand_predefined_scheme",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
Predefined scheme expansion - Convert 14-color schemes to full palette.
|
||||
|
||||
This module expands predefined color schemes (like Tokyo-Night) from their
|
||||
14 core colors to the full 48-color palette used by templates.
|
||||
|
||||
Input format (14 colors):
|
||||
mPrimary, mOnPrimary, mSecondary, mOnSecondary, mTertiary, mOnTertiary,
|
||||
mError, mOnError, mSurface, mOnSurface, mSurfaceVariant, mOnSurfaceVariant,
|
||||
mOutline, mHover
|
||||
|
||||
Output: Full 48-color palette matching generate_theme() output.
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from .color import Color, adjust_surface
|
||||
from .contrast import ensure_contrast
|
||||
|
||||
ThemeMode = Literal["dark", "light"]
|
||||
|
||||
|
||||
def _hex_to_color(hex_str: str) -> Color:
|
||||
"""Convert hex string to Color object."""
|
||||
hex_str = hex_str.lstrip("#")
|
||||
r = int(hex_str[0:2], 16)
|
||||
g = int(hex_str[2:4], 16)
|
||||
b = int(hex_str[4:6], 16)
|
||||
return Color(r, g, b)
|
||||
|
||||
|
||||
def _make_container_dark(base: Color) -> Color:
|
||||
"""Generate container color for dark mode."""
|
||||
h, s, l = base.to_hsl()
|
||||
return Color.from_hsl(h, min(s + 0.15, 1.0), max(l - 0.35, 0.15))
|
||||
|
||||
|
||||
def _make_container_light(base: Color) -> Color:
|
||||
"""Generate container color for light mode."""
|
||||
h, s, l = base.to_hsl()
|
||||
return Color.from_hsl(h, max(s - 0.20, 0.30), min(l + 0.35, 0.85))
|
||||
|
||||
|
||||
def _make_fixed_dark(base: Color) -> tuple[Color, Color]:
|
||||
"""Generate fixed and fixed_dim colors for dark mode."""
|
||||
h, s, _ = base.to_hsl()
|
||||
fixed = Color.from_hsl(h, max(s, 0.70), 0.85)
|
||||
fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.75)
|
||||
return fixed, fixed_dim
|
||||
|
||||
|
||||
def _make_fixed_light(base: Color) -> tuple[Color, Color]:
|
||||
"""Generate fixed and fixed_dim colors for light mode."""
|
||||
h, s, _ = base.to_hsl()
|
||||
fixed = Color.from_hsl(h, max(s, 0.70), 0.40)
|
||||
fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.30)
|
||||
return fixed, fixed_dim
|
||||
|
||||
|
||||
def expand_predefined_scheme(scheme_data: dict[str, str], mode: ThemeMode) -> dict[str, str]:
|
||||
"""
|
||||
Expand 14-color predefined scheme to full 48-color palette.
|
||||
|
||||
Args:
|
||||
scheme_data: Dictionary with keys like mPrimary, mSecondary, etc.
|
||||
mode: "dark" or "light"
|
||||
|
||||
Returns:
|
||||
Dictionary with all 48 color names mapped to hex values.
|
||||
"""
|
||||
is_dark = mode == "dark"
|
||||
|
||||
# Parse input colors
|
||||
primary = _hex_to_color(scheme_data["mPrimary"])
|
||||
on_primary = _hex_to_color(scheme_data["mOnPrimary"])
|
||||
secondary = _hex_to_color(scheme_data["mSecondary"])
|
||||
on_secondary = _hex_to_color(scheme_data["mOnSecondary"])
|
||||
tertiary = _hex_to_color(scheme_data["mTertiary"])
|
||||
on_tertiary = _hex_to_color(scheme_data["mOnTertiary"])
|
||||
error = _hex_to_color(scheme_data["mError"])
|
||||
on_error = _hex_to_color(scheme_data["mOnError"])
|
||||
surface = _hex_to_color(scheme_data["mSurface"])
|
||||
on_surface = _hex_to_color(scheme_data["mOnSurface"])
|
||||
surface_variant = _hex_to_color(scheme_data["mSurfaceVariant"])
|
||||
on_surface_variant = _hex_to_color(scheme_data["mOnSurfaceVariant"])
|
||||
outline = _hex_to_color(scheme_data["mOutline"])
|
||||
|
||||
# Generate container colors
|
||||
if is_dark:
|
||||
primary_container = _make_container_dark(primary)
|
||||
secondary_container = _make_container_dark(secondary)
|
||||
tertiary_container = _make_container_dark(tertiary)
|
||||
error_container = _make_container_dark(error)
|
||||
else:
|
||||
primary_container = _make_container_light(primary)
|
||||
secondary_container = _make_container_light(secondary)
|
||||
tertiary_container = _make_container_light(tertiary)
|
||||
error_container = _make_container_light(error)
|
||||
|
||||
# Generate "on container" colors with proper contrast
|
||||
primary_h, primary_s, _ = primary.to_hsl()
|
||||
secondary_h, secondary_s, _ = secondary.to_hsl()
|
||||
tertiary_h, tertiary_s, _ = tertiary.to_hsl()
|
||||
error_h, error_s, _ = error.to_hsl()
|
||||
|
||||
if is_dark:
|
||||
# Light text on dark containers
|
||||
on_primary_container = ensure_contrast(
|
||||
Color.from_hsl(primary_h, primary_s, 0.90), primary_container, 4.5
|
||||
)
|
||||
on_secondary_container = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, secondary_s, 0.90), secondary_container, 4.5
|
||||
)
|
||||
on_tertiary_container = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, tertiary_s, 0.90), tertiary_container, 4.5
|
||||
)
|
||||
on_error_container = ensure_contrast(
|
||||
Color.from_hsl(error_h, error_s, 0.90), error_container, 4.5
|
||||
)
|
||||
else:
|
||||
# Dark text on light containers
|
||||
on_primary_container = ensure_contrast(
|
||||
Color.from_hsl(primary_h, primary_s, 0.15), primary_container, 4.5
|
||||
)
|
||||
on_secondary_container = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, secondary_s, 0.15), secondary_container, 4.5
|
||||
)
|
||||
on_tertiary_container = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, tertiary_s, 0.15), tertiary_container, 4.5
|
||||
)
|
||||
on_error_container = ensure_contrast(
|
||||
Color.from_hsl(error_h, error_s, 0.15), error_container, 4.5
|
||||
)
|
||||
|
||||
# Generate fixed colors
|
||||
if is_dark:
|
||||
primary_fixed, primary_fixed_dim = _make_fixed_dark(primary)
|
||||
secondary_fixed, secondary_fixed_dim = _make_fixed_dark(secondary)
|
||||
tertiary_fixed, tertiary_fixed_dim = _make_fixed_dark(tertiary)
|
||||
else:
|
||||
primary_fixed, primary_fixed_dim = _make_fixed_light(primary)
|
||||
secondary_fixed, secondary_fixed_dim = _make_fixed_light(secondary)
|
||||
tertiary_fixed, tertiary_fixed_dim = _make_fixed_light(tertiary)
|
||||
|
||||
# Generate "on fixed" colors
|
||||
if is_dark:
|
||||
on_primary_fixed = ensure_contrast(
|
||||
Color.from_hsl(primary_h, 0.15, 0.15), primary_fixed, 4.5
|
||||
)
|
||||
on_primary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(primary_h, 0.15, 0.20), primary_fixed_dim, 4.5
|
||||
)
|
||||
on_secondary_fixed = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, 0.15, 0.15), secondary_fixed, 4.5
|
||||
)
|
||||
on_secondary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, 0.15, 0.20), secondary_fixed_dim, 4.5
|
||||
)
|
||||
on_tertiary_fixed = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, 0.15, 0.15), tertiary_fixed, 4.5
|
||||
)
|
||||
on_tertiary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, 0.15, 0.20), tertiary_fixed_dim, 4.5
|
||||
)
|
||||
else:
|
||||
on_primary_fixed = ensure_contrast(
|
||||
Color.from_hsl(primary_h, 0.15, 0.90), primary_fixed, 4.5
|
||||
)
|
||||
on_primary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(primary_h, 0.15, 0.85), primary_fixed_dim, 4.5
|
||||
)
|
||||
on_secondary_fixed = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, 0.15, 0.90), secondary_fixed, 4.5
|
||||
)
|
||||
on_secondary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(secondary_h, 0.15, 0.85), secondary_fixed_dim, 4.5
|
||||
)
|
||||
on_tertiary_fixed = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, 0.15, 0.90), tertiary_fixed, 4.5
|
||||
)
|
||||
on_tertiary_fixed_variant = ensure_contrast(
|
||||
Color.from_hsl(tertiary_h, 0.15, 0.85), tertiary_fixed_dim, 4.5
|
||||
)
|
||||
|
||||
# Generate surface containers from the surface color
|
||||
surface_h, surface_s, _ = surface.to_hsl()
|
||||
base_surface = Color.from_hsl(surface_h, surface_s, 0.5)
|
||||
|
||||
if is_dark:
|
||||
surface_container_lowest = adjust_surface(base_surface, 0.85, 0.06)
|
||||
surface_container_low = adjust_surface(base_surface, 0.85, 0.10)
|
||||
surface_container = adjust_surface(base_surface, 0.70, 0.20)
|
||||
surface_container_high = adjust_surface(base_surface, 0.75, 0.18)
|
||||
surface_container_highest = adjust_surface(base_surface, 0.70, 0.22)
|
||||
surface_dim = adjust_surface(base_surface, 0.85, 0.08)
|
||||
surface_bright = adjust_surface(base_surface, 0.75, 0.24)
|
||||
else:
|
||||
surface_container_lowest = adjust_surface(base_surface, 0.85, 0.96)
|
||||
surface_container_low = adjust_surface(base_surface, 0.85, 0.92)
|
||||
surface_container = adjust_surface(base_surface, 0.80, 0.86)
|
||||
surface_container_high = adjust_surface(base_surface, 0.75, 0.84)
|
||||
surface_container_highest = adjust_surface(base_surface, 0.70, 0.80)
|
||||
surface_dim = adjust_surface(base_surface, 0.85, 0.82)
|
||||
surface_bright = adjust_surface(base_surface, 0.90, 0.95)
|
||||
|
||||
# Generate outline variant
|
||||
outline_h, outline_s, outline_l = outline.to_hsl()
|
||||
if is_dark:
|
||||
outline_variant = Color.from_hsl(outline_h, outline_s, max(outline_l - 0.15, 0.1))
|
||||
else:
|
||||
outline_variant = Color.from_hsl(outline_h, outline_s, min(outline_l + 0.15, 0.9))
|
||||
|
||||
# Shadow and scrim
|
||||
shadow = surface # Use surface color for shadow in dark mode
|
||||
scrim = Color(0, 0, 0)
|
||||
|
||||
# Inverse colors
|
||||
if is_dark:
|
||||
inverse_surface = Color.from_hsl(surface_h, 0.08, 0.90)
|
||||
inverse_on_surface = Color.from_hsl(surface_h, 0.05, 0.15)
|
||||
inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.40)
|
||||
else:
|
||||
inverse_surface = Color.from_hsl(surface_h, 0.08, 0.15)
|
||||
inverse_on_surface = Color.from_hsl(surface_h, 0.05, 0.90)
|
||||
inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.70)
|
||||
|
||||
# Background is same as surface in MD3
|
||||
background = surface
|
||||
on_background = on_surface
|
||||
|
||||
return {
|
||||
# Primary
|
||||
"primary": primary.to_hex(),
|
||||
"on_primary": on_primary.to_hex(),
|
||||
"primary_container": primary_container.to_hex(),
|
||||
"on_primary_container": on_primary_container.to_hex(),
|
||||
"primary_fixed": primary_fixed.to_hex(),
|
||||
"primary_fixed_dim": primary_fixed_dim.to_hex(),
|
||||
"on_primary_fixed": on_primary_fixed.to_hex(),
|
||||
"on_primary_fixed_variant": on_primary_fixed_variant.to_hex(),
|
||||
# Secondary
|
||||
"secondary": secondary.to_hex(),
|
||||
"on_secondary": on_secondary.to_hex(),
|
||||
"secondary_container": secondary_container.to_hex(),
|
||||
"on_secondary_container": on_secondary_container.to_hex(),
|
||||
"secondary_fixed": secondary_fixed.to_hex(),
|
||||
"secondary_fixed_dim": secondary_fixed_dim.to_hex(),
|
||||
"on_secondary_fixed": on_secondary_fixed.to_hex(),
|
||||
"on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(),
|
||||
# Tertiary
|
||||
"tertiary": tertiary.to_hex(),
|
||||
"on_tertiary": on_tertiary.to_hex(),
|
||||
"tertiary_container": tertiary_container.to_hex(),
|
||||
"on_tertiary_container": on_tertiary_container.to_hex(),
|
||||
"tertiary_fixed": tertiary_fixed.to_hex(),
|
||||
"tertiary_fixed_dim": tertiary_fixed_dim.to_hex(),
|
||||
"on_tertiary_fixed": on_tertiary_fixed.to_hex(),
|
||||
"on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(),
|
||||
# Error
|
||||
"error": error.to_hex(),
|
||||
"on_error": on_error.to_hex(),
|
||||
"error_container": error_container.to_hex(),
|
||||
"on_error_container": on_error_container.to_hex(),
|
||||
# Surface
|
||||
"surface": surface.to_hex(),
|
||||
"on_surface": on_surface.to_hex(),
|
||||
"surface_variant": surface_variant.to_hex(),
|
||||
"on_surface_variant": on_surface_variant.to_hex(),
|
||||
"surface_dim": surface_dim.to_hex(),
|
||||
"surface_bright": surface_bright.to_hex(),
|
||||
# Surface containers
|
||||
"surface_container_lowest": surface_container_lowest.to_hex(),
|
||||
"surface_container_low": surface_container_low.to_hex(),
|
||||
"surface_container": surface_container.to_hex(),
|
||||
"surface_container_high": surface_container_high.to_hex(),
|
||||
"surface_container_highest": surface_container_highest.to_hex(),
|
||||
# Outline and other
|
||||
"outline": outline.to_hex(),
|
||||
"outline_variant": outline_variant.to_hex(),
|
||||
"shadow": shadow.to_hex(),
|
||||
"scrim": scrim.to_hex(),
|
||||
# Inverse
|
||||
"inverse_surface": inverse_surface.to_hex(),
|
||||
"inverse_on_surface": inverse_on_surface.to_hex(),
|
||||
"inverse_primary": inverse_primary.to_hex(),
|
||||
# Background
|
||||
"background": background.to_hex(),
|
||||
"on_background": on_background.to_hex(),
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Import from lib package
|
||||
from lib import read_image, ImageReadError, extract_palette, generate_theme, TemplateRenderer
|
||||
from lib import read_image, ImageReadError, extract_palette, generate_theme, TemplateRenderer, expand_predefined_scheme
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -62,7 +62,8 @@ Examples:
|
||||
parser.add_argument(
|
||||
'image',
|
||||
type=Path,
|
||||
help='Path to wallpaper image (PNG/JPG) or JSON color palette'
|
||||
nargs='?',
|
||||
help='Path to wallpaper image (PNG/JPG) or JSON color palette (not required if --scheme is used)'
|
||||
)
|
||||
|
||||
# Theme style (mutually exclusive)
|
||||
@@ -121,6 +122,12 @@ Examples:
|
||||
help='Theme mode: dark or light'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--scheme',
|
||||
type=Path,
|
||||
help='Path to predefined scheme JSON file (bypasses image extraction)'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -128,95 +135,121 @@ def main() -> int:
|
||||
"""Main entry point."""
|
||||
args = parse_args()
|
||||
|
||||
# Validate image path
|
||||
if not args.image.exists():
|
||||
print(f"Error: Image not found: {args.image}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Initialize result dictionary
|
||||
result: dict[str, dict[str, str]] = {}
|
||||
|
||||
# Check if input is a JSON palette (Predefined Scheme bypass)
|
||||
if args.image.suffix.lower() == '.json':
|
||||
try:
|
||||
with open(args.image, 'r') as f:
|
||||
input_data = json.load(f)
|
||||
|
||||
# Expect {"colors": ...} or direct dict
|
||||
colors_data = input_data.get("colors", input_data)
|
||||
|
||||
# Flatten QML-style object structure if needed
|
||||
# structure: key -> { default: { hex: "#..." } } or key -> "#..."
|
||||
flat_colors = {}
|
||||
for k, v in colors_data.items():
|
||||
if isinstance(v, dict) and 'default' in v and 'hex' in v['default']:
|
||||
flat_colors[k] = v['default']['hex']
|
||||
elif isinstance(v, str):
|
||||
flat_colors[k] = v
|
||||
else:
|
||||
# Best effort fallback
|
||||
flat_colors[k] = str(v)
|
||||
|
||||
# Assign to both/all modes since predefined scheme usually provides the correct palette for the requested mode
|
||||
result["dark"] = flat_colors
|
||||
result["light"] = flat_colors
|
||||
|
||||
# Skip extraction logic
|
||||
palette = None
|
||||
except Exception as e:
|
||||
print(f"Error reading JSON palette: {e}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
# Standard Image Extraction
|
||||
# Validate image path is a file
|
||||
if not args.image.is_file():
|
||||
print(f"Error: Not a file: {args.image}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Read image
|
||||
try:
|
||||
pixels = read_image(args.image)
|
||||
except ImageReadError as e:
|
||||
print(f"Error reading image: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Unexpected error reading image: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Extract palette
|
||||
k = 5
|
||||
palette = extract_palette(pixels, k=k)
|
||||
|
||||
if not palette:
|
||||
print("Error: Could not extract colors from image", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Determine which themes to generate
|
||||
use_material = args.material
|
||||
|
||||
# Handle --mode compatibility
|
||||
arg_dark = args.dark
|
||||
arg_light = args.light
|
||||
arg_both = args.both
|
||||
|
||||
# Determine mode from arguments
|
||||
if args.mode == 'dark':
|
||||
arg_dark = True
|
||||
arg_light = False
|
||||
arg_both = False
|
||||
modes = ["dark"]
|
||||
elif args.mode == 'light':
|
||||
arg_dark = False
|
||||
arg_light = True
|
||||
arg_both = False
|
||||
modes = ["light"]
|
||||
elif args.dark:
|
||||
modes = ["dark"]
|
||||
elif args.light:
|
||||
modes = ["light"]
|
||||
else:
|
||||
modes = ["dark", "light"]
|
||||
|
||||
if palette:
|
||||
if arg_dark:
|
||||
result["dark"] = generate_theme(palette, "dark", use_material)
|
||||
elif arg_light:
|
||||
result["light"] = generate_theme(palette, "light", use_material)
|
||||
# Path 1: Predefined scheme (--scheme flag)
|
||||
if args.scheme:
|
||||
if not args.scheme.exists():
|
||||
print(f"Error: Scheme file not found: {args.scheme}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
with open(args.scheme, 'r') as f:
|
||||
scheme_data = json.load(f)
|
||||
|
||||
# Scheme format: {"dark": {"mPrimary": "#...", ...}, "light": {...}}
|
||||
# or single mode: {"mPrimary": "#...", ...}
|
||||
for mode in modes:
|
||||
if mode in scheme_data:
|
||||
# Multi-mode format
|
||||
result[mode] = expand_predefined_scheme(scheme_data[mode], mode)
|
||||
elif "mPrimary" in scheme_data:
|
||||
# Single-mode format - use same colors for requested mode
|
||||
result[mode] = expand_predefined_scheme(scheme_data, mode)
|
||||
else:
|
||||
print(f"Error: Invalid scheme format - missing '{mode}' or 'mPrimary'", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing scheme JSON: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except KeyError as e:
|
||||
print(f"Error: Missing required color in scheme: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error processing scheme: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Path 2: Image-based extraction (default)
|
||||
else:
|
||||
# Validate image argument is provided
|
||||
if args.image is None:
|
||||
print("Error: Image path is required (unless --scheme is used)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Validate image path
|
||||
if not args.image.exists():
|
||||
print(f"Error: Image not found: {args.image}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Check if input is a JSON palette (legacy Predefined Scheme bypass)
|
||||
if args.image.suffix.lower() == '.json':
|
||||
try:
|
||||
with open(args.image, 'r') as f:
|
||||
input_data = json.load(f)
|
||||
|
||||
# Expect {"colors": ...} or direct dict
|
||||
colors_data = input_data.get("colors", input_data)
|
||||
|
||||
# Flatten QML-style object structure if needed
|
||||
# structure: key -> { default: { hex: "#..." } } or key -> "#..."
|
||||
flat_colors = {}
|
||||
for k, v in colors_data.items():
|
||||
if isinstance(v, dict) and 'default' in v and 'hex' in v['default']:
|
||||
flat_colors[k] = v['default']['hex']
|
||||
elif isinstance(v, str):
|
||||
flat_colors[k] = v
|
||||
else:
|
||||
# Best effort fallback
|
||||
flat_colors[k] = str(v)
|
||||
|
||||
# Assign to requested modes
|
||||
for mode in modes:
|
||||
result[mode] = flat_colors
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading JSON palette: {e}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
# Generate both (default)
|
||||
result["dark"] = generate_theme(palette, "dark", use_material)
|
||||
result["light"] = generate_theme(palette, "light", use_material)
|
||||
# Standard Image Extraction
|
||||
if not args.image.is_file():
|
||||
print(f"Error: Not a file: {args.image}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
pixels = read_image(args.image)
|
||||
except ImageReadError as e:
|
||||
print(f"Error reading image: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Unexpected error reading image: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Extract palette
|
||||
k = 5
|
||||
palette = extract_palette(pixels, k=k)
|
||||
|
||||
if not palette:
|
||||
print("Error: Could not extract colors from image", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Generate theme for each mode
|
||||
use_material = args.material
|
||||
for mode in modes:
|
||||
result[mode] = generate_theme(palette, mode, use_material)
|
||||
|
||||
# Output JSON
|
||||
json_output = json.dumps(result, indent=2)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../../Helpers/ColorsConvert.js" as ColorsConvert
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* Generate Material Design 3 color palette from base colors
|
||||
* @param colors - Object with mPrimary, mSecondary, mTertiary, mError, mSurface, etc.
|
||||
* @param isDarkMode - Boolean indicating dark or light mode
|
||||
* @param isStrict - Boolean; if true, use mSurfaceVariant/mOnSurfaceVariant/mOutline directly
|
||||
* @returns Object with all MD3 color roles
|
||||
*/
|
||||
function generatePalette(colors, isDarkMode, isStrict) {
|
||||
const c = hex => {
|
||||
const hsl = ColorsConvert.hexToHSL(hex);
|
||||
return {
|
||||
"default": {
|
||||
"hex": hex,
|
||||
"hex_stripped": hex.replace(/^#/, ""),
|
||||
"hue": hsl.h,
|
||||
"saturation": hsl.s,
|
||||
"lightness": hsl.l
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Generate container colors
|
||||
const primaryContainer = ColorsConvert.generateContainerColor(colors.mPrimary, isDarkMode);
|
||||
const secondaryContainer = ColorsConvert.generateContainerColor(colors.mSecondary, isDarkMode);
|
||||
const tertiaryContainer = ColorsConvert.generateContainerColor(colors.mTertiary, isDarkMode);
|
||||
|
||||
// Generate "on" colors
|
||||
const onPrimary = ColorsConvert.generateOnColor(colors.mPrimary, isDarkMode);
|
||||
const onSecondary = ColorsConvert.generateOnColor(colors.mSecondary, isDarkMode);
|
||||
const onTertiary = ColorsConvert.generateOnColor(colors.mTertiary, isDarkMode);
|
||||
|
||||
const onPrimaryContainer = ColorsConvert.generateOnColor(primaryContainer, isDarkMode);
|
||||
const onSecondaryContainer = ColorsConvert.generateOnColor(secondaryContainer, isDarkMode);
|
||||
const onTertiaryContainer = ColorsConvert.generateOnColor(tertiaryContainer, isDarkMode);
|
||||
|
||||
// Generate error colors (standard red-based)
|
||||
const errorContainer = ColorsConvert.generateContainerColor(colors.mError, isDarkMode);
|
||||
const onError = ColorsConvert.generateOnColor(colors.mError, isDarkMode);
|
||||
const onErrorContainer = ColorsConvert.generateOnColor(errorContainer, isDarkMode);
|
||||
|
||||
// Surface is same as background in Material Design 3
|
||||
const surface = colors.mSurface;
|
||||
const onSurface = isStrict ? colors.mOnSurface : ColorsConvert.generateOnColor(colors.mSurface, isDarkMode);
|
||||
|
||||
// Generate surface variant (slightly different tone)
|
||||
const surfaceVariant = isStrict ? colors.mSurfaceVariant : ColorsConvert.adjustLightness(colors.mSurface, isDarkMode ? 5 : -3);
|
||||
const onSurfaceVariant = isStrict ? colors.mOnSurfaceVariant : ColorsConvert.generateOnColor(surfaceVariant, isDarkMode);
|
||||
|
||||
// Generate surface containers (progressive elevation)
|
||||
const surfaceContainerLowest = ColorsConvert.generateSurfaceVariant(surface, 0, isDarkMode);
|
||||
const surfaceContainerLow = ColorsConvert.generateSurfaceVariant(surface, 1, isDarkMode);
|
||||
const surfaceContainer = ColorsConvert.generateSurfaceVariant(surface, 2, isDarkMode);
|
||||
const surfaceContainerHigh = ColorsConvert.generateSurfaceVariant(surface, 3, isDarkMode);
|
||||
const surfaceContainerHighest = ColorsConvert.generateSurfaceVariant(surface, 4, isDarkMode);
|
||||
|
||||
// Generate outline colors (for borders/dividers)
|
||||
const outline = isStrict ? colors.mOutline : ColorsConvert.adjustLightnessAndSaturation(colors.mOnSurface, isDarkMode ? -30 : 30, isDarkMode ? -30 : 30);
|
||||
const outlineVariant = ColorsConvert.adjustLightness(outline, isDarkMode ? -20 : 20);
|
||||
|
||||
// Generate surface_dim (darker/dimmer surface variant)
|
||||
const surfaceDim = ColorsConvert.generateSurfaceVariant(surface, -1, isDarkMode);
|
||||
|
||||
// Generate "fixed" colors (high-chroma accents that are consistent across modes)
|
||||
// Fixed colors are lighter in dark mode, darker in light mode - opposite of containers
|
||||
const primaryFixed = isDarkMode ? ColorsConvert.adjustLightness(colors.mPrimary, 30) : ColorsConvert.adjustLightness(colors.mPrimary, -10);
|
||||
const primaryFixedDim = ColorsConvert.adjustLightness(primaryFixed, isDarkMode ? -15 : 10);
|
||||
const onPrimaryFixedVariant = ColorsConvert.generateOnColor(primaryFixedDim, isDarkMode);
|
||||
|
||||
const secondaryFixed = isDarkMode ? ColorsConvert.adjustLightness(colors.mSecondary, 30) : ColorsConvert.adjustLightness(colors.mSecondary, -10);
|
||||
|
||||
const tertiaryFixed = isDarkMode ? ColorsConvert.adjustLightness(colors.mTertiary, 30) : ColorsConvert.adjustLightness(colors.mTertiary, -10);
|
||||
const tertiaryFixedDim = ColorsConvert.adjustLightness(tertiaryFixed, isDarkMode ? -15 : 10);
|
||||
const onTertiaryFixed = ColorsConvert.generateOnColor(tertiaryFixed, isDarkMode);
|
||||
|
||||
// Shadow is always pitch black
|
||||
const shadow = "#000000";
|
||||
|
||||
return {
|
||||
"primary": c(colors.mPrimary),
|
||||
"on_primary": c(onPrimary),
|
||||
"primary_container": c(primaryContainer),
|
||||
"on_primary_container": c(onPrimaryContainer),
|
||||
"secondary": c(colors.mSecondary),
|
||||
"on_secondary": c(onSecondary),
|
||||
"secondary_container": c(secondaryContainer),
|
||||
"on_secondary_container": c(onSecondaryContainer),
|
||||
"tertiary": c(colors.mTertiary),
|
||||
"on_tertiary": c(onTertiary),
|
||||
"tertiary_container": c(tertiaryContainer),
|
||||
"on_tertiary_container": c(onTertiaryContainer),
|
||||
"error": c(colors.mError),
|
||||
"on_error": c(onError),
|
||||
"error_container": c(errorContainer),
|
||||
"on_error_container": c(onErrorContainer),
|
||||
"background": c(surface),
|
||||
"on_background": c(onSurface),
|
||||
"surface": c(surface),
|
||||
"on_surface": c(onSurface),
|
||||
"surface_variant": c(surfaceVariant),
|
||||
"on_surface_variant": c(onSurfaceVariant),
|
||||
"surface_container_lowest": c(surfaceContainerLowest),
|
||||
"surface_container_low": c(surfaceContainerLow),
|
||||
"surface_container": c(surfaceContainer),
|
||||
"surface_container_high": c(surfaceContainerHigh),
|
||||
"surface_container_highest": c(surfaceContainerHighest),
|
||||
"outline": c(outline),
|
||||
"outline_variant": c(outlineVariant),
|
||||
"shadow": c(shadow),
|
||||
"surface_dim": c(surfaceDim),
|
||||
"primary_fixed": c(primaryFixed),
|
||||
"primary_fixed_dim": c(primaryFixedDim),
|
||||
"on_primary_fixed_variant": c(onPrimaryFixedVariant),
|
||||
"secondary_fixed": c(secondaryFixed),
|
||||
"tertiary_fixed": c(tertiaryFixed),
|
||||
"tertiary_fixed_dim": c(tertiaryFixedDim),
|
||||
"on_tertiary_fixed": c(onTertiaryFixed)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
@@ -66,76 +65,70 @@ Singleton {
|
||||
generateProcess.running = true;
|
||||
}
|
||||
|
||||
// Queue for processing templates one by one
|
||||
property var templateQueue: []
|
||||
property var currentTemplateContext: null
|
||||
readonly property string schemeJsonPath: Settings.cacheDir + "predefined-scheme.json"
|
||||
readonly property string predefinedConfigPath: Settings.cacheDir + "theming.predefined.toml"
|
||||
|
||||
/**
|
||||
* Process predefined color scheme using sed scripts
|
||||
* Dual-path architecture (predefined uses sed scripts)
|
||||
* Templates are processed one by one for better error reporting
|
||||
* Process predefined color scheme using Python template processor
|
||||
* Uses --scheme flag to expand 14-color scheme to full 48-color palette
|
||||
*/
|
||||
function processPredefinedScheme(schemeData, mode) {
|
||||
// 1. Handle terminal themes (pre-rendered file copy)
|
||||
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 (isTemplateEnabled("discord")) {
|
||||
const items = buildDiscordTemplateItems(app, colors, homeDir);
|
||||
items.forEach(item => queue.push(item));
|
||||
}
|
||||
} else if (app.id === "code") {
|
||||
if (isTemplateEnabled("code")) {
|
||||
const items = buildCodeTemplateItems(app, colors, homeDir);
|
||||
items.forEach(item => queue.push(item));
|
||||
}
|
||||
} else {
|
||||
if (isTemplateEnabled(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;
|
||||
// 2. Build TOML config for application templates
|
||||
const tomlContent = buildPredefinedTemplateConfig(mode);
|
||||
if (!tomlContent) {
|
||||
Logger.d("TemplateProcessor", "No application templates enabled for predefined scheme");
|
||||
return;
|
||||
}
|
||||
|
||||
const item = templateQueue.shift();
|
||||
currentTemplateContext = item;
|
||||
// 3. Build script to write files and run Python
|
||||
const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''");
|
||||
const configPathEsc = predefinedConfigPath.replace(/'/g, "'\\''");
|
||||
|
||||
templateProcess.command = ["sh", "-lc", item.script];
|
||||
templateProcess.running = true;
|
||||
// Use heredoc delimiters for safe JSON/TOML content
|
||||
const schemeDelimiter = "SCHEME_JSON_EOF_" + Math.random().toString(36).substr(2, 9);
|
||||
const tomlDelimiter = "TOML_CONFIG_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`;
|
||||
|
||||
// Write TOML config
|
||||
script += `cat > '${configPathEsc}' << '${tomlDelimiter}'\n`;
|
||||
script += tomlContent + "\n";
|
||||
script += `${tomlDelimiter}\n`;
|
||||
|
||||
// Run Python template processor with --scheme flag
|
||||
script += `python3 "${templateProcessorScript}" --scheme '${schemeJsonPathEsc}' --config '${configPathEsc}' --mode ${mode}\n`;
|
||||
|
||||
// Add user templates if enabled
|
||||
script += buildUserTemplateCommandForPredefined(schemeData, mode);
|
||||
|
||||
generateProcess.generator = "predefined";
|
||||
generateProcess.command = ["sh", "-lc", script];
|
||||
generateProcess.running = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TOML config for predefined scheme templates (excludes terminal themes)
|
||||
*/
|
||||
function buildPredefinedTemplateConfig(mode) {
|
||||
var lines = [];
|
||||
addApplicationTheming(lines, mode);
|
||||
|
||||
if (lines.length > 0) {
|
||||
return ["[config]"].concat(lines).join("\n") + "\n";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// WALLPAPER-BASED GENERATION (internal python)
|
||||
// WALLPAPER-BASED GENERATION
|
||||
// ================================================================================
|
||||
function buildThemeConfig() {
|
||||
var lines = [];
|
||||
@@ -279,197 +272,6 @@ Singleton {
|
||||
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/Templates/${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/Templates/${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/Templates/${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/Templates/${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/Templates/${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 colorData = colors[colorKey].default;
|
||||
const hexValue = colorData.hex;
|
||||
const hexStrippedValue = colorData.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'`);
|
||||
|
||||
// HSL components
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.default\\.hue}}/${colorData.hue}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.default\\.saturation}}/${colorData.saturation}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.default\\.lightness}}/${colorData.lightness}/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 colorData = darkColors[colorKey].default;
|
||||
const escapedHex = colorData.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const escapedHexStripped = colorData.hex_stripped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.hex_stripped}}/${escapedHexStripped}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.hex}}/${escapedHex}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.hue}}/${colorData.hue}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.saturation}}/${colorData.saturation}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.dark\\.lightness}}/${colorData.lightness}/g'`);
|
||||
});
|
||||
|
||||
// Replace light mode patterns
|
||||
Object.keys(lightColors).forEach(colorKey => {
|
||||
const colorData = lightColors[colorKey].default;
|
||||
const escapedHex = colorData.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const escapedHexStripped = colorData.hex_stripped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.hex_stripped}}/${escapedHexStripped}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.hex}}/${escapedHex}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.hue}}/${colorData.hue}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.saturation}}/${colorData.saturation}/g'`);
|
||||
expressions.push(`-e 's/{{colors\\.${colorKey}\\.light\\.lightness}}/${colorData.lightness}/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)
|
||||
// ================================================================================
|
||||
@@ -574,24 +376,14 @@ Singleton {
|
||||
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, "'\\''");
|
||||
// Reuse the scheme JSON already written by processPredefinedScheme()
|
||||
const schemeJsonPathEsc = schemeJsonPath.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 += "EOF\n";
|
||||
|
||||
// Call template-processor.py with JSON file as first arg (it will detect extension)
|
||||
script += ` python3 "${templateProcessorScript}" '${tempJsonPathEsc}' --config '${userConfigPath}' --mode ${mode}\n`;
|
||||
// Use --scheme flag with the already-written scheme JSON
|
||||
script += ` python3 "${templateProcessorScript}" --scheme '${schemeJsonPathEsc}' --config '${userConfigPath}' --mode ${mode}\n`;
|
||||
script += "fi";
|
||||
|
||||
return script;
|
||||
@@ -643,49 +435,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------
|
||||
// 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: {
|
||||
if (this.text && this.text.trim() !== "") {
|
||||
Logger.d("TemplateProcessor", "templateProcess stdout:", this.text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (this.text && this.text.trim() !== "") {
|
||||
// Log template errors/warnings from Python script
|
||||
Logger.e("TemplateProcessor", this.text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------
|
||||
Process {
|
||||
id: copyProcess
|
||||
|
||||
Reference in New Issue
Block a user