feat: themes
This commit is contained in:
@@ -11,8 +11,7 @@ import re
|
||||
import textwrap
|
||||
|
||||
from uframe.chars import (
|
||||
BOX_CHARS, DIVIDER_CHARS, GAUGE_FILLED, GAUGE_EMPTY,
|
||||
STATUS_CHARS, STATUS_COLORS, sparkline_chars,
|
||||
BOX_CHARS, DIVIDER_CHARS, sparkline_chars,
|
||||
)
|
||||
from uframe.grid import CharGrid, CellStyle
|
||||
from uframe.ir import (
|
||||
@@ -22,6 +21,7 @@ from uframe.ir import (
|
||||
Form, Field, Password, Radio, Checkbox, FormButton,
|
||||
HeadingLevel, DividerStyle, ListStyle, Align, BorderWeight,
|
||||
)
|
||||
from uframe.themes import ThemeDef, THEME_DEFAULT
|
||||
|
||||
|
||||
def _align_text(text: str, width: int, align: Align) -> str:
|
||||
@@ -46,8 +46,9 @@ def _style_from_node(node: IRNode) -> CellStyle:
|
||||
)
|
||||
|
||||
|
||||
def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
def paint(node: IRNode, grid: CharGrid, theme: ThemeDef | None = None) -> None:
|
||||
"""Recursively paint an IR node and its children into the grid."""
|
||||
th = theme or THEME_DEFAULT
|
||||
x, y, w = node.rect.x, node.rect.y, node.rect.w
|
||||
|
||||
# Ensure grid is tall enough
|
||||
@@ -55,46 +56,48 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
|
||||
if isinstance(node, Page):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Box):
|
||||
# Draw the border
|
||||
title_style = CellStyle(bold=True, fg=node.style.fg)
|
||||
# Draw the border with themed characters
|
||||
title_style = CellStyle(bold=True, fg=node.style.fg or th.palette.accent)
|
||||
grid.draw_border(x, y, w, node.rect.h,
|
||||
weight=node.weight,
|
||||
title=node.title,
|
||||
title_style=title_style)
|
||||
title_style=title_style,
|
||||
border_chars=th.border_dict(node.weight.name.lower()),
|
||||
title_caps=(th.title_caps.left, th.title_caps.right))
|
||||
# Propagate box alignment/color to children that don't have their own
|
||||
for child in node.children:
|
||||
if node.style.align != Align.LEFT and child.style.align == Align.LEFT:
|
||||
child.style.align = node.style.align
|
||||
if node.style.fg and not child.style.fg:
|
||||
child.style.fg = node.style.fg
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Row):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Col):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Spacer):
|
||||
pass # Just empty space
|
||||
|
||||
elif isinstance(node, Pad):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Heading):
|
||||
style = CellStyle(bold=True)
|
||||
if node.level == HeadingLevel.H1:
|
||||
style.fg = "0f0" # green
|
||||
style.fg = th.palette.accent
|
||||
elif node.level == HeadingLevel.H2:
|
||||
style.fg = "0cf" # cyan
|
||||
style.fg = th.palette.accent2
|
||||
elif node.level == HeadingLevel.H3:
|
||||
style.fg = "88f" # light blue
|
||||
style.fg = th.palette.accent3
|
||||
|
||||
# Underline-style heading
|
||||
grid.put_text(x, y, node.text[:w], style=style)
|
||||
@@ -143,26 +146,24 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
|
||||
elif isinstance(node, Divider):
|
||||
ds = node.divider_style
|
||||
char = DIVIDER_CHARS.get(ds.name.lower(), "─")
|
||||
style = CellStyle(fg="555")
|
||||
char = getattr(th.dividers, ds.name.lower(), th.dividers.light)
|
||||
style = CellStyle(fg=th.palette.muted)
|
||||
for col in range(x, x + w):
|
||||
grid.put(col, y, char, style=style)
|
||||
|
||||
elif isinstance(node, Link):
|
||||
style = CellStyle(fg="0cf", underline=True)
|
||||
# In ASCII mode, display as [text]. In Micron, the emitter wraps with link syntax.
|
||||
# Write just the display text — the link metadata goes on cells for Micron emission.
|
||||
style = CellStyle(fg=th.palette.info, underline=True)
|
||||
grid.put_text(x, y, node.display[:w], style=style, link=node.dest)
|
||||
|
||||
elif isinstance(node, ListNode):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, ListItem):
|
||||
style = _style_from_node(node)
|
||||
# Bullet to the left of the content (safe: put_text clips to bounds)
|
||||
bullet_x = max(0, x - 2)
|
||||
grid.put_text(bullet_x, y, "• ", style=CellStyle(fg="888"))
|
||||
grid.put_text(bullet_x, y, f"{th.ornaments.bullet} ", style=CellStyle(fg=th.palette.label))
|
||||
# Wrap content
|
||||
wrapped = textwrap.wrap(node.content, width=w) if node.content else [""]
|
||||
for i, line in enumerate(wrapped):
|
||||
@@ -183,17 +184,17 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
filled = int(bar_w * pct)
|
||||
|
||||
# Determine color based on thresholds
|
||||
fg = "0f0" # green
|
||||
fg = th.palette.success
|
||||
if node.crit is not None and node.value >= node.crit:
|
||||
fg = "f00" # red
|
||||
fg = th.palette.danger
|
||||
elif node.warn is not None and node.value >= node.warn:
|
||||
fg = "ff0" # yellow
|
||||
fg = th.palette.warning
|
||||
|
||||
for i in range(bar_w):
|
||||
if i < filled:
|
||||
grid.put(bar_x + i, y, GAUGE_FILLED, style=CellStyle(fg=fg))
|
||||
grid.put(bar_x + i, y, th.gauge.filled, style=CellStyle(fg=fg))
|
||||
else:
|
||||
grid.put(bar_x + i, y, GAUGE_EMPTY, style=CellStyle(fg="555"))
|
||||
grid.put(bar_x + i, y, th.gauge.empty, style=CellStyle(fg=th.palette.muted))
|
||||
|
||||
# Percentage
|
||||
pct_text = f" {int(pct * 100)}%"
|
||||
@@ -206,13 +207,16 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
|
||||
spark_x = x + len(label_text)
|
||||
chars = sparkline_chars(node.values, node.spark_width)
|
||||
spark_style = CellStyle(fg="0cf")
|
||||
spark_style = CellStyle(fg=th.palette.info)
|
||||
for i, ch in enumerate(chars):
|
||||
grid.put(spark_x + i, y, ch, style=spark_style)
|
||||
|
||||
elif isinstance(node, Status):
|
||||
char = STATUS_CHARS.get(node.state, "◌")
|
||||
color = STATUS_COLORS.get(node.state, "888")
|
||||
char = getattr(th.indicators, node.state, th.indicators.unknown)
|
||||
color_map = {"online": th.palette.success, "offline": th.palette.danger,
|
||||
"degraded": th.palette.warning, "unknown": th.palette.label,
|
||||
"alert": th.palette.danger}
|
||||
color = color_map.get(node.state, th.palette.label)
|
||||
grid.put(x, y, char, style=CellStyle(fg=color))
|
||||
grid.put_text(x + 2, y, node.label)
|
||||
|
||||
@@ -221,62 +225,65 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
||||
|
||||
elif isinstance(node, Form):
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
elif isinstance(node, Field):
|
||||
label_style = CellStyle(fg="888")
|
||||
field_style = CellStyle(fg="0cf")
|
||||
label_style = CellStyle(fg=th.palette.label)
|
||||
field_style = CellStyle(fg=th.palette.form)
|
||||
label_text = f"{node.field_name}: "
|
||||
grid.put_text(x, y, label_text, style=label_style)
|
||||
# Draw [ placeholder_______ ]
|
||||
fl = th.form.field_l
|
||||
fr = th.form.field_r
|
||||
fx = x + len(label_text)
|
||||
fw = min(node.field_width, w - len(label_text) - 2)
|
||||
grid.put(fx, y, "[", style=field_style)
|
||||
fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
|
||||
grid.put_text(fx, y, fl, style=field_style)
|
||||
placeholder = node.placeholder or node.field_name
|
||||
inner = f" {placeholder}".ljust(fw - 1)[:fw - 1]
|
||||
grid.put_text(fx + 1, y, inner, style=CellStyle(fg="555"))
|
||||
grid.put(fx + fw, y, "]", style=field_style)
|
||||
inner = placeholder.ljust(fw)[:fw]
|
||||
grid.put_text(fx + len(fl), y, inner, style=CellStyle(fg=th.palette.muted))
|
||||
grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)
|
||||
|
||||
elif isinstance(node, Password):
|
||||
label_style = CellStyle(fg="888")
|
||||
field_style = CellStyle(fg="0cf")
|
||||
label_style = CellStyle(fg=th.palette.label)
|
||||
field_style = CellStyle(fg=th.palette.form)
|
||||
label_text = f"{node.field_name}: "
|
||||
grid.put_text(x, y, label_text, style=label_style)
|
||||
fl = th.form.field_l
|
||||
fr = th.form.field_r
|
||||
fx = x + len(label_text)
|
||||
fw = min(node.field_width, w - len(label_text) - 2)
|
||||
grid.put(fx, y, "[", style=field_style)
|
||||
inner = " " + "•" * (fw - 2)
|
||||
grid.put_text(fx + 1, y, inner[:fw - 1], style=CellStyle(fg="555"))
|
||||
grid.put(fx + fw, y, "]", style=field_style)
|
||||
fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
|
||||
grid.put_text(fx, y, fl, style=field_style)
|
||||
inner = "•" * fw
|
||||
grid.put_text(fx + len(fl), y, inner[:fw], style=CellStyle(fg=th.palette.muted))
|
||||
grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)
|
||||
|
||||
elif isinstance(node, Radio):
|
||||
label_style = CellStyle(fg="888")
|
||||
label_style = CellStyle(fg=th.palette.label)
|
||||
label_text = f"{node.group}: "
|
||||
grid.put_text(x, y, label_text, style=label_style)
|
||||
rx = x + len(label_text)
|
||||
for i, opt in enumerate(node.options):
|
||||
dot = "(•)" if i == 0 else "( )"
|
||||
opt_style = CellStyle(fg="0cf" if i == 0 else "888")
|
||||
dot = th.form.radio_on if i == 0 else th.form.radio_off
|
||||
opt_style = CellStyle(fg=th.palette.form if i == 0 else th.palette.label)
|
||||
grid.put_text(rx, y, dot, style=opt_style)
|
||||
rx += 4
|
||||
rx += len(dot) + 1
|
||||
grid.put_text(rx, y, opt, style=CellStyle())
|
||||
rx += len(opt) + 2
|
||||
|
||||
elif isinstance(node, Checkbox):
|
||||
check_style = CellStyle(fg="0cf")
|
||||
box_char = "[✓]" if node.checked else "[ ]"
|
||||
check_style = CellStyle(fg=th.palette.form)
|
||||
box_char = th.form.check_on if node.checked else th.form.check_off
|
||||
grid.put_text(x, y, box_char, style=check_style)
|
||||
grid.put_text(x + 4, y, node.checkbox_label)
|
||||
grid.put_text(x + len(box_char) + 1, y, node.checkbox_label)
|
||||
|
||||
elif isinstance(node, FormButton):
|
||||
btn_style = CellStyle(bold=True, fg="0f0")
|
||||
btn_style = CellStyle(bold=True, fg=th.palette.button)
|
||||
btn_text = f"[ {node.button_label} ]"
|
||||
grid.put_text(x, y, btn_text, style=btn_style, link=node.dest)
|
||||
|
||||
else:
|
||||
# Generic: paint children
|
||||
for child in node.children:
|
||||
paint(child, grid)
|
||||
paint(child, grid, th)
|
||||
|
||||
|
||||
def _paint_table(node: Table, grid: CharGrid, x: int, y: int, w: int) -> None:
|
||||
|
||||
Reference in New Issue
Block a user