feat: templates

This commit is contained in:
2026-04-01 07:50:43 +02:00
parent b40c6436cd
commit df11705875
10 changed files with 589 additions and 19 deletions

View File

@@ -10,15 +10,15 @@ from __future__ import annotations
import textwrap
from uframe.chars import (
DIVIDER_CHARS, GAUGE_FILLED, GAUGE_EMPTY,
BOX_CHARS, DIVIDER_CHARS, GAUGE_FILLED, GAUGE_EMPTY,
STATUS_CHARS, STATUS_COLORS, sparkline_chars,
)
from uframe.grid import CharGrid, CellStyle
from uframe.ir import (
IRNode, Page, Box, Row, Col, Spacer, Pad,
Heading, Text, Label, Divider, Link, ListNode, ListItem,
Gauge, Sparkline, Status,
HeadingLevel, DividerStyle, ListStyle, Align,
Gauge, Sparkline, Status, Table,
HeadingLevel, DividerStyle, ListStyle, Align, BorderWeight,
)
@@ -210,7 +210,133 @@ def paint(node: IRNode, grid: CharGrid) -> None:
grid.put(x, y, char, style=CellStyle(fg=color))
grid.put_text(x + 2, y, node.label)
elif isinstance(node, Table):
_paint_table(node, grid, x, y, w)
else:
# Generic: paint children
for child in node.children:
paint(child, grid)
def _paint_table(node: Table, grid: CharGrid, x: int, y: int, w: int) -> None:
"""Paint a box-drawn table with header and data rows."""
if not node.columns:
return
ch = BOX_CHARS[BorderWeight.LIGHT]
border_style = CellStyle()
header_style = CellStyle(bold=True)
num_cols = len(node.columns)
# Calculate column widths
# If columns have explicit widths, use them. Otherwise distribute evenly.
col_widths: list[int] = []
total_explicit = 0
auto_count = 0
for _, cw in node.columns:
if cw > 0:
col_widths.append(cw)
total_explicit += cw
else:
col_widths.append(0)
auto_count += 1
# Available inner width = total - borders (num_cols + 1 border chars)
inner_w = w - (num_cols + 1)
if auto_count > 0:
auto_each = max(1, (inner_w - total_explicit) // auto_count)
for i in range(len(col_widths)):
if col_widths[i] == 0:
col_widths[i] = auto_each
# Compute column x positions (after each left border)
col_x: list[int] = []
cx = x + 1 # after left border
for cw in col_widths:
col_x.append(cx)
cx += cw + 1 # +1 for separator
table_w = cx - x # total table width including right border
row_y = y
# ── Top border ──
grid.put(x, row_y, ch["tl"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
grid.put(x + table_w - 1, row_y, ch["tr"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
for ci, cw in enumerate(col_widths):
for j in range(cw):
grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
# Column separator on top border
if ci < num_cols - 1:
grid.put(col_x[ci] + cw, row_y, ch["t_down"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
row_y += 1
# ── Header row ──
grid.put(x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
for ci, (col_name, _) in enumerate(node.columns):
text = col_name[:col_widths[ci]].ljust(col_widths[ci])
grid.put_text(col_x[ci], row_y, text, style=header_style)
sep_x = col_x[ci] + col_widths[ci]
grid.put(sep_x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
row_y += 1
# ── Header separator ──
grid.put(x, row_y, ch["t_right"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
grid.put(x + table_w - 1, row_y, ch["t_left"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
for ci, cw in enumerate(col_widths):
for j in range(cw):
grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
if ci < num_cols - 1:
grid.put(col_x[ci] + cw, row_y, ch["cross"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
row_y += 1
# ── Data rows ──
cell_style = CellStyle()
for row_data in node.rows:
grid.put(x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
for ci in range(num_cols):
cell_text = row_data[ci] if ci < len(row_data) else ""
text = cell_text[:col_widths[ci]].ljust(col_widths[ci])
# Check for @color{hex}{text} modifiers in cell content
if "@" in cell_text:
spans = []
import re as _re
pattern = _re.compile(r"@color\{([0-9a-fA-F]{3})\}\{([^}]*)\}")
pos = 0
styled_parts: list[tuple[str, CellStyle]] = []
for m in pattern.finditer(cell_text):
if m.start() > pos:
styled_parts.append((cell_text[pos:m.start()], cell_style))
styled_parts.append((m.group(2), CellStyle(fg=m.group(1))))
pos = m.end()
if pos < len(cell_text):
styled_parts.append((cell_text[pos:], cell_style))
col_pos = col_x[ci]
for part_text, part_style in styled_parts:
for pch in part_text:
if col_pos < col_x[ci] + col_widths[ci]:
grid.put(col_pos, row_y, pch, style=part_style)
col_pos += 1
# Pad remaining
while col_pos < col_x[ci] + col_widths[ci]:
grid.put(col_pos, row_y, " ")
col_pos += 1
else:
grid.put_text(col_x[ci], row_y, text, style=cell_style)
sep_x = col_x[ci] + col_widths[ci]
grid.put(sep_x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
row_y += 1
# ── Bottom border ──
grid.put(x, row_y, ch["bl"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
grid.put(x + table_w - 1, row_y, ch["br"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
for ci, cw in enumerate(col_widths):
for j in range(cw):
grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
if ci < num_cols - 1:
grid.put(col_x[ci] + cw, row_y, ch["t_up"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)