feat: themes

This commit is contained in:
2026-04-01 10:45:02 +02:00
parent 0316e50233
commit 01a3e0095c
10 changed files with 413 additions and 66 deletions

241
backend/uframe/themes.py Normal file
View File

@@ -0,0 +1,241 @@
"""µFrame Theme System — decorative styles for rich terminal UIs.
A theme maps abstract UI elements to concrete character sets and color
palettes. The same .uf source renders with different visual character
when a different theme is applied.
"""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class BorderChars:
tl: str = ""; t: str = ""; tr: str = ""
l: str = ""; r: str = ""
bl: str = ""; b: str = ""; br: str = ""
@dataclass
class Indicators:
online: str = ""
offline: str = ""
degraded: str = ""
unknown: str = ""
alert: str = ""
@dataclass
class GaugeChars:
filled: str = ""
empty: str = ""
@dataclass
class FormChars:
field_l: str = "[ "
field_r: str = " ]"
radio_on: str = "(•)"
radio_off: str = "( )"
check_on: str = "[✓]"
check_off: str = "[ ]"
@dataclass
class Ornaments:
bullet: str = ""
header: str = ""
separator: str = ""
footer: str = ""
@dataclass
class TitleCaps:
left: str = ""
right: str = ""
@dataclass
class Palette:
accent: str = "0f0" # headings, primary highlights
accent2: str = "0cf" # secondary (H2, links)
accent3: str = "88f" # tertiary (H3)
muted: str = "555" # dividers, empty gauge
border: str = "" # border color (empty = no color)
success: str = "0f0" # online, gauge ok
warning: str = "ff0" # degraded, gauge warn
danger: str = "f00" # offline, gauge crit
info: str = "0cf" # links, sparklines
form: str = "0cf" # form element accents
label: str = "888" # labels, field names
button: str = "0f0" # form buttons
@dataclass
class DividerChars:
light: str = ""
heavy: str = ""
double: str = ""
dash: str = ""
dot: str = ""
@dataclass
class ThemeDef:
name: str = "default"
description: str = "Clean engineering — standard box-drawing"
borders_light: BorderChars = field(default_factory=BorderChars)
borders_heavy: BorderChars = field(default_factory=lambda: BorderChars(
tl="", t="", tr="", l="", r="", bl="", b="", br=""))
borders_double: BorderChars = field(default_factory=lambda: BorderChars(
tl="", t="", tr="", l="", r="", bl="", b="", br=""))
borders_rounded: BorderChars = field(default_factory=lambda: BorderChars(
tl="", t="", tr="", l="", r="", bl="", b="", br=""))
dividers: DividerChars = field(default_factory=DividerChars)
title_caps: TitleCaps = field(default_factory=TitleCaps)
indicators: Indicators = field(default_factory=Indicators)
gauge: GaugeChars = field(default_factory=GaugeChars)
form: FormChars = field(default_factory=FormChars)
ornaments: Ornaments = field(default_factory=Ornaments)
palette: Palette = field(default_factory=Palette)
def border_chars(self, weight_name: str) -> BorderChars:
return {
"light": self.borders_light,
"heavy": self.borders_heavy,
"double": self.borders_double,
"rounded": self.borders_rounded,
}.get(weight_name, self.borders_light)
def border_dict(self, weight_name: str) -> dict[str, str]:
"""Return BOX_CHARS-compatible dict for a border weight."""
bc = self.border_chars(weight_name)
return {
"tl": bc.tl, "tr": bc.tr, "bl": bc.bl, "br": bc.br,
"h": bc.t, "v": bc.l,
"t_down": bc.t, "t_up": bc.b, "t_right": bc.l, "t_left": bc.r,
"cross": bc.t,
}
# ---------------------------------------------------------------------------
# Built-in themes
# ---------------------------------------------------------------------------
THEME_DEFAULT = ThemeDef()
THEME_NOUVEAU = ThemeDef(
name="nouveau",
description="Art Nouveau — organic flowing ornament",
borders_heavy=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_light=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_double=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_rounded=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
dividers=DividerChars(light="", heavy="", double="", dash="", dot=""),
title_caps=TitleCaps(left="✾─── ", right=" ───✾"),
indicators=Indicators(online="", offline="", degraded="", unknown="", alert=""),
gauge=GaugeChars(filled="", empty=""),
form=FormChars(field_l=" ", field_r=" ", radio_on="", radio_off="",
check_on="", check_off=""),
ornaments=Ornaments(bullet="", header="─✾──────✾─", separator="☙━━━━━━━━━━━━━❧"),
palette=Palette(accent="da5", accent2="8b5", accent3="886", muted="886",
border="a85", success="6b4", warning="da5", danger="a33",
info="68a", form="da5", label="886", button="6b4"),
)
THEME_GOTHIC = ThemeDef(
name="gothic",
description="Gothic — heavy blackletter, monumental",
borders_heavy=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_light=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_double=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_rounded=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
dividers=DividerChars(light="", heavy="", double="", dash="", dot=""),
title_caps=TitleCaps(left="═══╡ ", right=" ╞═══"),
indicators=Indicators(online="", offline="", degraded="", unknown="", alert=""),
gauge=GaugeChars(filled="", empty=""),
form=FormChars(field_l="", field_r="", radio_on="", radio_off="",
check_on="", check_off=""),
ornaments=Ornaments(bullet="", header="═══╡══════╞═══"),
palette=Palette(accent="cc8", accent2="a66", accent3="888", muted="666",
border="888", success="8a8", warning="cc8", danger="a44",
info="8ac", form="cc8", label="888", button="cc8"),
)
THEME_BAMBOO = ThemeDef(
name="bamboo",
description="Bamboo — East Asian minimalism, light brush strokes",
borders_heavy=BorderChars(tl="", t=" ", tr="", l=" ", r=" ", bl=" ", b=" ", br=" "),
borders_light=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_double=BorderChars(tl="", t=" ", tr="", l=" ", r=" ", bl=" ", b=" ", br=" "),
borders_rounded=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
dividers=DividerChars(light="", heavy="", double="", dash="", dot=""),
title_caps=TitleCaps(left="┄┄┄ ", right=" ┄┄┄"),
indicators=Indicators(online="", offline="", degraded="", unknown="", alert=""),
gauge=GaugeChars(filled="", empty=" "),
form=FormChars(field_l="", field_r="", radio_on="", radio_off="",
check_on="", check_off=""),
ornaments=Ornaments(bullet=""),
palette=Palette(accent="bca", accent2="ab9", accent3="998", muted="998",
border="776", success="8b8", warning="cc9", danger="b77",
info="9ab", form="bca", label="998", button="8b8"),
)
THEME_CIRCUIT = ThemeDef(
name="circuit",
description="Circuit — digital, technical, neon",
borders_heavy=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_light=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_double=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_rounded=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
dividers=DividerChars(light="", heavy="", double="", dash="", dot=""),
title_caps=TitleCaps(left="══[ ", right=" ]═══"),
indicators=Indicators(online="", offline="", degraded="", unknown="", alert=""),
gauge=GaugeChars(filled="", empty=""),
form=FormChars(field_l=">_ [ ", field_r=" ]", radio_on="[▰]", radio_off="[▱]",
check_on="[▰]", check_off="[▱]"),
ornaments=Ornaments(bullet=""),
palette=Palette(accent="0ff", accent2="f0f", accent3="0af", muted="555",
border="0aa", success="0f0", warning="ff0", danger="f00",
info="0ff", form="0ff", label="0aa", button="0f0"),
)
THEME_BRUTALIST = ThemeDef(
name="brutalist",
description="Brutalist — raw blocks, monochrome, anti-decorative",
borders_heavy=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_light=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_double=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
borders_rounded=BorderChars(tl="", t="", tr="", l="", r="", bl="", b="", br=""),
dividers=DividerChars(light="", heavy="", double="", dash="", dot=""),
title_caps=TitleCaps(left="", right=""),
indicators=Indicators(online="", offline="", degraded="", unknown="", alert="!"),
gauge=GaugeChars(filled="", empty=" "),
form=FormChars(field_l="[", field_r="]", radio_on="", radio_off="",
check_on="", check_off=""),
ornaments=Ornaments(bullet=""),
palette=Palette(accent="fff", accent2="fff", accent3="ccc", muted="888",
border="fff", success="fff", warning="fff", danger="fff",
info="fff", form="fff", label="aaa", button="fff"),
)
# ---------------------------------------------------------------------------
# Theme registry
# ---------------------------------------------------------------------------
BUILTIN_THEMES: dict[str, ThemeDef] = {
"default": THEME_DEFAULT,
"nouveau": THEME_NOUVEAU,
"gothic": THEME_GOTHIC,
"bamboo": THEME_BAMBOO,
"circuit": THEME_CIRCUIT,
"brutalist": THEME_BRUTALIST,
}
def get_theme(name: str) -> ThemeDef:
"""Get a built-in theme by name. Returns default if not found."""
return BUILTIN_THEMES.get(name.lower(), THEME_DEFAULT)