scheme: add --json flag to list-presets, --accent flag, drop :accent from preset spec
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 44s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m48s

- list-presets --json outputs structured JSON with variants,
  modes, accents, and default_accent for accent-aware schemes
- --accent flag replaces :accent shorthand in preset string
- Validate --accent against variant's available accents
- resolve_preset returns tuple[str, str] (scheme + variant only)
- Update tests for new signature
This commit is contained in:
2026-05-23 17:42:58 +02:00
parent 2934d863ca
commit 21ed178bbc
3 changed files with 51 additions and 26 deletions
+42 -16
View File
@@ -21,18 +21,36 @@ app = typer.Typer()
@app.command() @app.command()
def list_presets(): def list_presets(
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
schemes = list_schemes() schemes = list_schemes()
for sid, meta in sorted(schemes.items()): if json_format:
var_list = [] out = {}
for v in meta.variants: for sid, meta in sorted(schemes.items()):
parts = [f"{v.id} ({', '.join(sorted(v.modes))})"] variants = {}
if v.accents: for v in meta.variants:
parts.append(f"accents: {', '.join(v.accents)}") entry = {"modes": sorted(v.modes)}
var_list.append(" | ".join(parts)) if v.accents:
print(f"{meta.name} ({sid})") entry["accents"] = sorted(v.accents)
print(f" Variants: {', '.join(var_list)}") entry["default_accent"] = sorted(v.accents)[0]
print() variants[v.id] = entry
out[meta.name] = {
"id": sid,
"variants": variants,
}
print(json.dumps({"presets": out}, indent=2))
else:
for sid, meta in sorted(schemes.items()):
var_list = []
for v in meta.variants:
parts = [f"{v.id} ({', '.join(sorted(v.modes))})"]
if v.accents:
parts.append(f"accents: {', '.join(v.accents)}")
var_list.append(" | ".join(parts))
print(f"{meta.name} ({sid})")
print(f" Variants: {', '.join(var_list)}")
print()
@app.command() @app.command()
@@ -41,10 +59,9 @@ def generate(
scheme: Optional[str] = typer.Option( scheme: Optional[str] = typer.Option(
None, help="Color scheme algorithm to use for image mode. Ignored in preset mode." None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."
), ),
preset: Optional[str] = typer.Option( preset: Optional[str] = typer.Option(None, help="Name of a premade scheme in this format: <scheme>:<variant>"),
None, help="Name of a premade scheme in this format: <scheme>:<variant>[:<accent>]"
),
mode: Optional[str] = typer.Option(None, help="Mode of the preset scheme (dark or light)."), mode: Optional[str] = typer.Option(None, help="Mode of the preset scheme (dark or light)."),
accent: Optional[str] = typer.Option(None, help="Accent for schemes that support it (e.g. mauve)."),
): ):
HOME = str(os.getenv("HOME")) HOME = str(os.getenv("HOME"))
@@ -473,8 +490,17 @@ def generate(
scheme_class = get_scheme_class(scheme) scheme_class = get_scheme_class(scheme)
if preset: if preset:
p_scheme, p_variant, p_accent = resolve_preset(preset) p_scheme, p_variant = resolve_preset(preset)
palette_obj = get_palette(p_scheme, p_variant, mode or config_mode, accent=p_accent) schemes = list_schemes()
if accent and p_scheme in schemes:
meta = schemes[p_scheme]
var_accents = next((v.accents for v in meta.variants if v.id == p_variant), ())
if accent not in var_accents:
available = ", ".join(var_accents) if var_accents else "none"
raise typer.BadParameter(
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
)
palette_obj = get_palette(p_scheme, p_variant, mode or config_mode, accent=accent)
colors = palette_obj.colors colors = palette_obj.colors
effective_mode = palette_obj.mode effective_mode = palette_obj.mode
name = palette_obj.scheme name = palette_obj.scheme
+4 -6
View File
@@ -132,12 +132,10 @@ def list_schemes() -> dict[str, SchemeMeta]:
return dict(SCHEMES) return dict(SCHEMES)
def resolve_preset(spec: str) -> tuple[str, str, str | None]: def resolve_preset(spec: str) -> tuple[str, str]:
parts = spec.split(":") parts = spec.split(":")
if len(parts) == 3:
return parts[0], parts[1], parts[2]
if len(parts) == 2: if len(parts) == 2:
return parts[0], parts[1], None return parts[0], parts[1]
if len(parts) == 1: if len(parts) == 1:
return parts[0], "default", None return parts[0], "default"
raise ValueError(f"Invalid preset spec '{spec}'. Use <scheme>:<variant>[:<accent>]") raise ValueError(f"Invalid preset spec '{spec}'. Use <scheme>:<variant>")
+5 -4
View File
@@ -154,13 +154,14 @@ class TestListSchemes:
class TestResolvePreset: class TestResolvePreset:
def test_two_parts(self): def test_two_parts(self):
assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium", None) assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium")
def test_three_parts(self): def test_three_parts(self):
assert sp.resolve_preset("catppuccin:mocha:mauve") == ("catppuccin", "mocha", "mauve") with pytest.raises(ValueError, match="Invalid preset spec"):
sp.resolve_preset("catppuccin:mocha:mauve")
def test_one_part(self): def test_one_part(self):
assert sp.resolve_preset("default") == ("default", "default", None) assert sp.resolve_preset("default") == ("default", "default")
def test_edge_spaces(self): def test_edge_spaces(self):
assert sp.resolve_preset(" catppuccin : mocha : mauve ") == (" catppuccin ", " mocha ", " mauve ") assert sp.resolve_preset(" catppuccin : mocha ") == (" catppuccin ", " mocha ")