"""Paint pass — write IR nodes into the CharGrid as characters. Depth-first traversal: each node writes its content at its assigned rect position. Containers recurse into children after drawing their own structure (borders, etc.). """ from __future__ import annotations import textwrap from uframe.chars import ( 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, ) def _align_text(text: str, width: int, align: Align) -> str: """Align text within a field of the given width.""" if len(text) >= width: return text[:width] if align == Align.CENTER: return text.center(width) elif align == Align.RIGHT: return text.rjust(width) return text.ljust(width) def _style_from_node(node: IRNode) -> CellStyle: """Create a CellStyle from a node's style attributes.""" return CellStyle( fg=node.style.fg, bg=node.style.bg, bold=node.style.bold, italic=node.style.italic, underline=node.style.underline, ) def paint(node: IRNode, grid: CharGrid) -> None: """Recursively paint an IR node and its children into the grid.""" x, y, w = node.rect.x, node.rect.y, node.rect.w # Ensure grid is tall enough grid.grow_height(y + node.rect.h) if isinstance(node, Page): for child in node.children: paint(child, grid) elif isinstance(node, Box): # Draw the border title_style = CellStyle(bold=True, fg=node.style.fg) grid.draw_border(x, y, w, node.rect.h, weight=node.weight, title=node.title, title_style=title_style) # Paint children inside the border for child in node.children: paint(child, grid) elif isinstance(node, Row): for child in node.children: paint(child, grid) elif isinstance(node, Col): for child in node.children: paint(child, grid) elif isinstance(node, Spacer): pass # Just empty space elif isinstance(node, Pad): for child in node.children: paint(child, grid) elif isinstance(node, Heading): style = CellStyle(bold=True) if node.level == HeadingLevel.H1: style.fg = "0f0" # green elif node.level == HeadingLevel.H2: style.fg = "0cf" # cyan elif node.level == HeadingLevel.H3: style.fg = "88f" # light blue # Underline-style heading grid.put_text(x, y, node.text[:w], style=style) elif isinstance(node, Text): style = _style_from_node(node) if node.spans and any( s.bold or s.italic or s.underline or s.fg or s.bg for s in node.spans ): # Render with inline spans col = x row = y for span in node.spans: span_style = CellStyle( fg=span.fg or style.fg, bg=span.bg or style.bg, bold=span.bold or style.bold, italic=span.italic or style.italic, underline=span.underline or style.underline, ) for ch in span.text: if col >= x + w: col = x row += 1 if grid.in_bounds(col, row): grid.put(col, row, ch, style=span_style) col += 1 else: # Simple text with word wrapping wrapped = textwrap.wrap(node.content, width=w) if node.content else [""] for i, line in enumerate(wrapped): if y + i < grid.height: text = _align_text(line, w, node.style.align) grid.put_text(x, y + i, text, style=style) elif isinstance(node, Label): style = _style_from_node(node) key_style = CellStyle(bold=True, fg=style.fg, bg=style.bg) # Key: value layout with padding key_text = f"{node.key}:" pad = max(1, 16 - len(key_text)) grid.put_text(x, y, key_text, style=key_style) grid.put_text(x + len(key_text) + pad, y, node.value, style=style) elif isinstance(node, Divider): ds = node.divider_style char = DIVIDER_CHARS.get(ds.name.lower(), "─") style = CellStyle(fg="555") 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. 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) elif isinstance(node, ListItem): style = _style_from_node(node) # Parent determines bullet style — use a simple bullet for now bullet = "• " grid.put_text(x - 2, y, bullet, style=CellStyle(fg="888")) # Wrap content wrapped = textwrap.wrap(node.content, width=w) if node.content else [""] for i, line in enumerate(wrapped): if y + i < grid.height: grid.put_text(x, y + i, line, style=style) elif isinstance(node, Gauge): style = _style_from_node(node) # Label label_text = f"{node.label} " grid.put_text(x, y, label_text, style=CellStyle(bold=True)) bar_x = x + len(label_text) bar_w = min(node.bar_width, w - len(label_text) - 6) if bar_w > 0: pct = min(node.value / node.max_val, 1.0) if node.max_val > 0 else 0 filled = int(bar_w * pct) # Determine color based on thresholds fg = "0f0" # green if node.crit is not None and node.value >= node.crit: fg = "f00" # red elif node.warn is not None and node.value >= node.warn: fg = "ff0" # yellow for i in range(bar_w): if i < filled: grid.put(bar_x + i, y, GAUGE_FILLED, style=CellStyle(fg=fg)) else: grid.put(bar_x + i, y, GAUGE_EMPTY, style=CellStyle(fg="555")) # Percentage pct_text = f" {int(pct * 100)}%" grid.put_text(bar_x + bar_w, y, pct_text, style=CellStyle(fg=fg)) elif isinstance(node, Sparkline): style = _style_from_node(node) label_text = f"{node.label} " grid.put_text(x, y, label_text, style=CellStyle(bold=True)) spark_x = x + len(label_text) chars = sparkline_chars(node.values, node.spark_width) spark_style = CellStyle(fg="0cf") 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") grid.put(x, y, char, style=CellStyle(fg=color)) grid.put_text(x + 2, y, node.label) else: # Generic: paint children for child in node.children: paint(child, grid)