"""Fixed-column layout. Every emitted line goes through a Row. The Row knows the terminal width and refuses to produce a line that overflows it, because the failure mode of hand-padding with f-strings is that columns drift silently and a footer wraps onto a second line, and neither shows up until you render the thing. """ from __future__ import annotations from dataclasses import dataclass from rich.cells import cell_len from rich.text import Text class Overflow(Exception): """A line exceeded the grid. Always a bug in the caller, never the user's.""" @dataclass(frozen=True) class Grid: """A column grid. All renderers share one so screens line up with each other.""" width: int = 100 indent: int = 2 right_margin: int = 2 @property def right(self) -> int: """Column at which right-aligned content ends.""" return self.width - self.right_margin def row(self, indent: int | None = None) -> Row: return Row(self, self.indent if indent is None else indent) def blank(self) -> Text: return Text() def clip(text: str, limit: int) -> str: """Truncate to a cell budget, counting display width rather than codepoints.""" if limit <= 0: return "" if cell_len(text) <= limit: return text if limit == 1: return "…" out: list[str] = [] used = 0 for char in text: width = cell_len(char) if used + width > limit - 1: break out.append(char) used += width return "".join(out) + "…" class Row: """Builds one line on the grid. Chainable. Row(grid).span("linux", t.text).to(36).span(old, t.old).to(56).span(new, t.new) The primitives cannot emit a line wider than the grid. Content that does not fit is clipped and counted in `clipped`: handing the terminal a line it will wrap corrupts every row below it, but silently losing text is also a bug, so the counter lets tests tell "rendered" from "rendered by luck". """ __slots__ = ("_text", "clipped", "grid") def __init__(self, grid: Grid, indent: int) -> None: self.grid = grid self._text = Text(" " * max(0, min(indent, grid.width))) self.clipped = 0 @property def at(self) -> int: """Current column, i.e. the display width of what we have so far. Measured in terminal cells rather than codepoints: a double-width glyph would otherwise satisfy the overflow check while still corrupting every column to its right. """ return self._text.cell_len def _room(self, upto: int) -> int: return max(0, min(upto, self.grid.width) - self.at) def span(self, text: str, style: str | None = None) -> Row: if not text: return self room = self._room(self.grid.width) if cell_len(text) > room: text = clip(text, room) self.clipped += 1 if text: self._text.append(text, style=style or "") return self def to(self, col: int, minimum: int = 1) -> Row: """Pad with spaces until the cursor sits at absolute column `col`.""" want = max(minimum, col - self.at) room = self._room(self.grid.width) if want > room: want = room self.clipped += 1 self._text.append(" " * want) return self def right(self, text: str, style: str | None = None, col: int | None = None) -> Row: """Right-align `text` so it ends at `col` (default: the right margin).""" end = min(self.grid.right if col is None else col, self.grid.width) room = self._room(end) if room <= 0 or not text: if text: self.clipped += 1 return self # keep at least one cell of separation wherever there is room for it, # otherwise the value collides with whatever precedes it if cell_len(text) > room - 1: text = clip(text, room - 1 if room > 1 else room) self.clipped += 1 if not text: return self self._text.append(" " * (room - cell_len(text))) self._text.append(text, style=style or "") return self def fill(self, char: str = "─", style: str | None = None) -> Row: """Fill the remaining grid width with a repeating character.""" room = self._room(self.grid.width) if room > 0: self._text.append(char * room, style=style or "") return self def done(self) -> Text: if self.at > self.grid.width: raise Overflow( f"line is {self.at} cells, grid is {self.grid.width}: {self._text.plain!r}" ) return self._text def check(lines: list[Text], width: int) -> None: """Validate a whole screen, by display width. Call this in tests on every screen.""" for i, line in enumerate(lines): if cell_len(line.plain) > width: raise Overflow( f"line {i} is {cell_len(line.plain)} cells, grid is {width}: {line.plain!r}" )