Cli tool #9

Merged
Zacharias-Brohn merged 33 commits from cli-tool into main 2026-02-22 21:43:51 +01:00
8 changed files with 83 additions and 101 deletions
Showing only changes of commit ee62e61b06 - Show all commits
+1 -1
View File
@@ -61,7 +61,7 @@ Singleton {
if (!isPreview) { if (!isPreview) {
root.scheme = scheme.name; root.scheme = scheme.name;
flavour = scheme.flavour; flavour = scheme.flavor;
currentLight = scheme.mode === "light"; currentLight = scheme.mode === "light";
} else { } else {
previewLight = scheme.mode === "light"; previewLight = scheme.mode === "light";
+1 -1
View File
@@ -9,7 +9,7 @@ Item {
implicitWidth: textMetrics.width + contentRow.spacing + 30 implicitWidth: textMetrics.width + contentRow.spacing + 30
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
property color textColor: DynamicColors.palette.m3secondary property color textColor: DynamicColors.palette.m3onSurface
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
Binary file not shown.
+51 -99
View File
@@ -2,6 +2,7 @@ from typing import Annotated, Optional
import typer import typer
import json import json
from zshell.utils.schemepalettes import PRESETS
from pathlib import Path from pathlib import Path
from PIL import Image from PIL import Image
from materialyoucolor.quantize import QuantizeCelebi 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."), "fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."),
# preset inputs (optional - used for preset mode) # preset inputs (optional - used for preset mode)
preset: Optional[str] = typer.Option( preset: Optional[str] = typer.Option(
None, help="Name of a premade scheme (preset)."), None, help="Name of a premade scheme in this format: <preset_name>:<preset_flavor>"),
flavour: str = typer.Option("default", help="Flavor of the preset scheme."),
mode: str = typer.Option( mode: str = typer.Option(
"dark", help="Mode of the preset scheme (dark or light)."), "dark", help="Mode of the preset scheme (dark or light)."),
# output (required) # output (required)
output: Path = typer.Option(..., help="Output JSON path.") 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: match scheme:
case "fruit-salad": case "fruit-salad":
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
@@ -52,47 +60,6 @@ def generate(
case _: case _:
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme 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)): def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
thumbnail_file = Path(thumbnail_path) thumbnail_file = Path(thumbnail_path)
@@ -103,8 +70,8 @@ def generate(
thumbnail_file.parent.mkdir(parents=True, exist_ok=True) thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
image.save(thumbnail_path, "JPEG") image.save(thumbnail_path, "JPEG")
def generate_color_scheme(thumbnail_path, output_path): def seed_from_image(image_path: Path) -> Hct:
image = Image.open(thumbnail_path) image = Image.open(image_path)
pixel_len = image.width * image.height pixel_len = image.width * image.height
image_data = image.getdata() image_data = image.getdata()
@@ -112,11 +79,27 @@ def generate(
pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)] pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)]
result = QuantizeCelebi(pixel_array, 128) 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( scheme = Scheme(
Hct.from_int(score), seed,
True, is_dark,
0.0 0.0
) )
@@ -127,65 +110,34 @@ def generate(
color_int = color_name.get_hct(scheme).to_int() color_int = color_name.get_hct(scheme).to_int()
color_dict[color] = int_to_hex(color_int) color_dict[color] = int_to_hex(color_int)
output_dict = { return color_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)
def int_to_hex(argb_int): def int_to_hex(argb_int):
return "#{:06X}".format(argb_int & 0xFFFFFF) return "#{:06X}".format(argb_int & 0xFFFFFF)
try: try:
if preset: if preset:
# try local presets directory: presets/<preset>/<flavour>.txt or presets/<preset>-<flavour>.txt seed = seed_from_preset(preset)
base1 = Path(__file__).parent.parent / "assets" / "schemes" / \ colors = generate_color_scheme(seed, mode)
preset / flavour / f"{mode}.txt" name, flavor = preset.split(":")
if base1.exists(): else:
preset_path = base1 generate_thumbnail(image_path, str(thumbnail_path))
else: seed = seed_from_image(thumbnail_path)
raise FileNotFoundError( colors = generate_color_scheme(seed, mode)
f"Preset file not found. Looked for: {base1.resolve()}. " name = "dynamic"
"You can also pass --preset-file <path> directly." flavor = "default"
)
mapping = parse_preset_file(preset_path) output_dict = {
generate_color_scheme_from_preset( "name": name,
mapping, output, preset or preset_path.stem, flavour) "flavor": flavor,
typer.echo(f"Wrote preset-based scheme to {output}") "mode": mode,
raise typer.Exit() "variant": scheme,
"colors": colors
}
generate_thumbnail(image_path, str(thumbnail_path)) output.parent.mkdir(parents=True, exist_ok=True)
generate_color_scheme(str(thumbnail_path), output) with open(output, "w") as f:
json.dump(output_dict, f, indent=4)
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}")
# with open(output, "w") as f: # with open(output, "w") as f:
+30
View File
@@ -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,
}