diff --git a/Config/DynamicColors.qml b/Config/DynamicColors.qml index 03d3bc0..284a2db 100644 --- a/Config/DynamicColors.qml +++ b/Config/DynamicColors.qml @@ -61,7 +61,7 @@ Singleton { if (!isPreview) { root.scheme = scheme.name; - flavour = scheme.flavour; + flavour = scheme.flavor; currentLight = scheme.mode === "light"; } else { previewLight = scheme.mode === "light"; diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index c7bd498..6fce70a 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -9,7 +9,7 @@ Item { implicitWidth: textMetrics.width + contentRow.spacing + 30 anchors.top: parent.top anchors.bottom: parent.bottom - property color textColor: DynamicColors.palette.m3secondary + property color textColor: DynamicColors.palette.m3onSurface Rectangle { anchors.left: parent.left diff --git a/cli/dist/zshell-0.1.0-py3-none-any.whl b/cli/dist/zshell-0.1.0-py3-none-any.whl index f7c525e..e410d02 100644 Binary files a/cli/dist/zshell-0.1.0-py3-none-any.whl and b/cli/dist/zshell-0.1.0-py3-none-any.whl differ diff --git a/cli/src/zshell/subcommands/__pycache__/scheme.cpython-313.pyc b/cli/src/zshell/subcommands/__pycache__/scheme.cpython-313.pyc index 5e2d3e2..ce31c6b 100644 Binary files a/cli/src/zshell/subcommands/__pycache__/scheme.cpython-313.pyc and b/cli/src/zshell/subcommands/__pycache__/scheme.cpython-313.pyc differ diff --git a/cli/src/zshell/subcommands/__pycache__/shell.cpython-313.pyc b/cli/src/zshell/subcommands/__pycache__/shell.cpython-313.pyc index b579631..af12c92 100644 Binary files a/cli/src/zshell/subcommands/__pycache__/shell.cpython-313.pyc and b/cli/src/zshell/subcommands/__pycache__/shell.cpython-313.pyc differ diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index d30273a..aa9a98c 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -2,6 +2,7 @@ from typing import Annotated, Optional import typer import json +from zshell.utils.schemepalettes import PRESETS from pathlib import Path from PIL import Image from materialyoucolor.quantize import QuantizeCelebi @@ -23,13 +24,20 @@ def generate( "fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."), # preset inputs (optional - used for preset mode) preset: Optional[str] = typer.Option( - None, help="Name of a premade scheme (preset)."), - flavour: str = typer.Option("default", help="Flavor of the preset scheme."), + None, help="Name of a premade scheme in this format: :"), mode: str = typer.Option( "dark", help="Mode of the preset scheme (dark or light)."), # output (required) output: Path = typer.Option(..., help="Output JSON path.") ): + if preset is None and image_path is None: + raise typer.BadParameter( + "Either --image-path or --preset must be provided.") + + if preset is not None and image_path is not None: + raise typer.BadParameter( + "Use either --image-path or --preset, not both.") + match scheme: case "fruit-salad": from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme @@ -52,47 +60,6 @@ def generate( case _: from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme - def normalize_hex(s: str) -> str: - # Accepts "6a73ac", "#6a73ac", "B5CCBA" and returns "#B5CCBA" - s = s.strip() - if s.startswith("#"): - s = s[1:] - # If the value contains alpha (8 chars), drop alpha and keep RGB (last 6). - if len(s) == 8: - s = s[2:] # assume AARRGGBB -> drop AA - if len(s) != 6: - raise ValueError( - f"Invalid hex color '{s}' (expected 6 hex digits, optionally prefixed with '#').") - return f"#{s.upper()}" - - def parse_preset_file(file_path: Path) -> dict: - """ - Parse a preset scheme file with lines like: - primary_paletteKeyColor 6a73ac - background 131317 - onBackground e4e1e7 - Returns a dict mapping key -> "#RRGGBB" - """ - mapping = {} - with file_path.open("r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - parts = line.split() - if len(parts) < 2: - # ignore malformed lines - continue - key = parts[0].strip() - value = parts[1].strip() - try: - mapping[key] = normalize_hex(value) - except ValueError: - # skip invalid hex but log to stdout - print( - f"Warning: skipping invalid color value for '{key}': {value}") - return mapping - def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)): thumbnail_file = Path(thumbnail_path) @@ -103,8 +70,8 @@ def generate( thumbnail_file.parent.mkdir(parents=True, exist_ok=True) image.save(thumbnail_path, "JPEG") - def generate_color_scheme(thumbnail_path, output_path): - image = Image.open(thumbnail_path) + def seed_from_image(image_path: Path) -> Hct: + image = Image.open(image_path) pixel_len = image.width * image.height image_data = image.getdata() @@ -112,11 +79,27 @@ def generate( pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)] result = QuantizeCelebi(pixel_array, 128) - score = Score.score(result)[0] + return Hct.from_int(Score.score(result)[0]) + + def seed_from_preset(name: str) -> Hct: + try: + return PRESETS[name].primary + except KeyError: + raise typer.BadParameter( + f"Preset '{name}' not found. Available presets: {', '.join(PRESETS.keys())}") + + def generate_color_scheme(seed: Hct, mode: str) -> dict[str, str]: + + is_dark = mode.lower() == "dark" + + if is_dark: + seed = Hct.from_hct(seed.hue, seed.chroma, min(seed.tone, 20)) + else: + seed = Hct.from_hct(seed.hue, seed.chroma, max(seed.tone, 70)) scheme = Scheme( - Hct.from_int(score), - True, + seed, + is_dark, 0.0 ) @@ -127,65 +110,34 @@ def generate( color_int = color_name.get_hct(scheme).to_int() color_dict[color] = int_to_hex(color_int) - output_dict = { - "name": "dynamic", - "flavour": "default", - "mode": "dark", - "variant": "tonalspot", - "colors": color_dict - } - - output_file = Path(output_path) - output_file.parent.mkdir(parents=True, exist_ok=True) - - with open(output_file, "w") as f: - json.dump(output_dict, f, indent=4) - - def generate_color_scheme_from_preset(preset_mapping: dict, output_path: Path, preset_name: str, flavour: str): - """ - Build JSON output using keys from the preset file. - Any keys in preset_mapping are included verbatim in the "colors" object. - """ - color_dict = {} - for k, v in preset_mapping.items(): - color_dict[k] = v # v already normalized to "#RRGGBB" - - output_dict = { - "name": preset_name, - "flavour": flavour, - "mode": mode, # could be made configurable / auto-detected - "variant": scheme, - "colors": color_dict - } - - output_path.parent.mkdir(parents=True, exist_ok=True) - with output_path.open("w", encoding="utf-8") as f: - json.dump(output_dict, f, indent=4) + return color_dict def int_to_hex(argb_int): return "#{:06X}".format(argb_int & 0xFFFFFF) try: if preset: - # try local presets directory: presets//.txt or presets/-.txt - base1 = Path(__file__).parent.parent / "assets" / "schemes" / \ - preset / flavour / f"{mode}.txt" - if base1.exists(): - preset_path = base1 - else: - raise FileNotFoundError( - f"Preset file not found. Looked for: {base1.resolve()}. " - "You can also pass --preset-file directly." - ) + seed = seed_from_preset(preset) + colors = generate_color_scheme(seed, mode) + name, flavor = preset.split(":") + else: + generate_thumbnail(image_path, str(thumbnail_path)) + seed = seed_from_image(thumbnail_path) + colors = generate_color_scheme(seed, mode) + name = "dynamic" + flavor = "default" - mapping = parse_preset_file(preset_path) - generate_color_scheme_from_preset( - mapping, output, preset or preset_path.stem, flavour) - typer.echo(f"Wrote preset-based scheme to {output}") - raise typer.Exit() + output_dict = { + "name": name, + "flavor": flavor, + "mode": mode, + "variant": scheme, + "colors": colors + } - generate_thumbnail(image_path, str(thumbnail_path)) - generate_color_scheme(str(thumbnail_path), output) + output.parent.mkdir(parents=True, exist_ok=True) + with open(output, "w") as f: + json.dump(output_dict, f, indent=4) except Exception as e: print(f"Error: {e}") # with open(output, "w") as f: diff --git a/cli/src/zshell/utils/__pycache__/schemepalettes.cpython-313.pyc b/cli/src/zshell/utils/__pycache__/schemepalettes.cpython-313.pyc new file mode 100644 index 0000000..4f3c04b Binary files /dev/null and b/cli/src/zshell/utils/__pycache__/schemepalettes.cpython-313.pyc differ diff --git a/cli/src/zshell/utils/schemepalettes.py b/cli/src/zshell/utils/schemepalettes.py new file mode 100644 index 0000000..00227be --- /dev/null +++ b/cli/src/zshell/utils/schemepalettes.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from materialyoucolor.hct.hct import Hct +from typing import Mapping + + +@dataclass(frozen=True) +class SeedPalette: + primary: Hct + secondary: Hct + tertiary: Hct + neutral: Hct + neutral_variant: Hct + error: Hct | None = None + + +def hex_to_hct(hex_: str) -> Hct: + return Hct.from_int(int(f"0xFF{hex_}", 16)) + + +CATPPUCCIN_MACCHIATO = SeedPalette( + primary=hex_to_hct("C6A0F6"), + secondary=hex_to_hct("7DC4E4"), + tertiary=hex_to_hct("F5BDE6"), + neutral=hex_to_hct("24273A"), + neutral_variant=hex_to_hct("363A4F"), +) + +PRESETS: Mapping[str, SeedPalette] = { + "catppuccin:macchiato": CATPPUCCIN_MACCHIATO, +}