workmap 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ """How setup looks.
2
+
3
+ Setup is the first thing anybody sees, and it is the only screen that has to
4
+ sell the tool before the tool has done anything. So it uses the same vocabulary
5
+ as the desk, rounded panels, the hairline rail, one restrained palette, and
6
+ not a second style invented for the occasion.
7
+
8
+ Presentation only. Every decision setup makes lives in `workmap/setup.py`;
9
+ nothing here knows what a root or an agent is, which is why it can be rendered
10
+ into a string and asserted on.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import itertools
15
+ import os
16
+ import sys
17
+ import threading
18
+ import time
19
+
20
+ from .text import (AMBER, BOLD, DIM, RESET, cell, fg, strip_ansi)
21
+ from .widgets import RAIL_ON, box_bot, box_row, box_top, frame_width
22
+
23
+ # Progress glyphs. Filled / current / not yet, the same "state at a glance"
24
+ # idea as the project rail, so the two screens read as one product.
25
+ DONE, CURRENT, TODO = "●", "◆", "○"
26
+ CHECK, CROSS = "✓", "·"
27
+
28
+ ACCENT = 111 # the blue the panels lean on
29
+ MUTED = 244
30
+
31
+ SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
32
+ FRAME_S = 0.08
33
+ REVEAL_S = 0.035 # per-row stagger; small enough to feel like one motion
34
+
35
+
36
+ def colour() -> bool:
37
+ """Style output? Not when piped, and not when asked not to.
38
+
39
+ NO_COLOR is honoured because a setup screen is exactly the kind of thing
40
+ people capture into an issue or a terminal recording.
41
+ """
42
+ if os.environ.get("NO_COLOR"):
43
+ return False
44
+ return sys.stdout.isatty()
45
+
46
+
47
+ def animate() -> bool:
48
+ """Motion is for people watching. Never for pipes, CI, or a recording."""
49
+ return colour() and not os.environ.get("WORKMAP_NO_ANIM")
50
+
51
+
52
+ def _style(s: str, *codes: str) -> str:
53
+ return "".join(codes) + s + RESET if colour() else s
54
+
55
+
56
+ def plain(line: str) -> str:
57
+ """Drop styling when nobody can see it.
58
+
59
+ The panel helpers in widgets.py always colour their borders, they were
60
+ written for a TUI, which is only ever on a terminal. Setup is not: people
61
+ pipe it, capture it into issues, and run it in CI, and escape codes in a
62
+ pasted transcript are noise.
63
+ """
64
+ return line if colour() else strip_ansi(line)
65
+
66
+
67
+ def say(line: str = "") -> None:
68
+ """One line, styled only if it will be seen."""
69
+ print(plain(line))
70
+
71
+
72
+ def width() -> int:
73
+ try:
74
+ cols = os.get_terminal_size().columns
75
+ except OSError:
76
+ cols = 80
77
+ # No cap of its own: `frame_width` already stops at FRAME_MAX. This used
78
+ # to read `min(cols, 100)`, which says a setup panel can be a hundred
79
+ # columns wide. It cannot, at any width, and a second ceiling written in
80
+ # a different number is a ceiling somebody will one day raise.
81
+ return frame_width(max(46, cols))
82
+
83
+
84
+ def rail(step: int, total: int) -> str:
85
+ """● ● ◆ ○, where you are, without spending a line saying so."""
86
+ marks = []
87
+ for i in range(1, total + 1):
88
+ if i < step:
89
+ marks.append(_style(DONE, fg(ACCENT)))
90
+ elif i == step:
91
+ marks.append(_style(CURRENT, fg(ACCENT), BOLD))
92
+ else:
93
+ marks.append(_style(TODO, DIM))
94
+ return " ".join(marks)
95
+
96
+
97
+ def panel(title: str, body: list[str], *, step: int, total: int) -> list[str]:
98
+ """One step, framed. `body` lines may carry ANSI; they are padded safely."""
99
+ inner = width() - 4
100
+ head = _style("workmap setup", BOLD)
101
+ lines = [box_top(inner, title=head, tail=rail(step, total)), box_row(inner)]
102
+ lines.append(box_row(inner, " " + _style(title, BOLD)))
103
+ lines.append(box_row(inner))
104
+ for line in body:
105
+ lines.append(box_row(inner, " " + line if line else ""))
106
+ lines.append(box_row(inner))
107
+ lines.append(box_bot(inner))
108
+ return lines
109
+
110
+
111
+ def render(lines: list[str], *, stagger: bool = True) -> None:
112
+ """Print a panel, one row at a time when someone is watching."""
113
+ delay = REVEAL_S if (stagger and animate()) else 0
114
+ for line in lines:
115
+ sys.stdout.write(plain(line) + "\n")
116
+ if delay:
117
+ sys.stdout.flush()
118
+ time.sleep(delay)
119
+ if not delay:
120
+ sys.stdout.flush()
121
+
122
+
123
+ def found(label: str, detail: str = "", *, ok: bool = True) -> str:
124
+ """A checklist row: mark, name, and a dim detail column."""
125
+ mark = _style(CHECK, fg(ACCENT)) if ok else _style(CROSS, DIM)
126
+ name = cell(label, 12)
127
+ name = _style(name, BOLD) if ok else _style(name, DIM)
128
+ return f"{mark} {name}{_style(detail, DIM)}"
129
+
130
+
131
+ def listing(text: str, *, ok: bool = False) -> str:
132
+ """A checklist row whose label is prose, not a name to line up."""
133
+ mark = _style(CHECK, fg(ACCENT)) if ok else _style(CROSS, DIM)
134
+ return f"{mark} {_style(text, DIM)}"
135
+
136
+
137
+ def path_label(path) -> str:
138
+ """`~/dev/Company` rather than the whole thing.
139
+
140
+ Panels are a fixed measure and paths are the longest thing in them, so
141
+ the part that identifies the directory is the part worth keeping.
142
+ """
143
+ text = str(path)
144
+ home = str(os.path.expanduser("~"))
145
+ if text == home:
146
+ return "~"
147
+ if text.startswith(home + os.sep):
148
+ return "~" + text[len(home):]
149
+ return text
150
+
151
+
152
+ def command(cmd: str) -> str:
153
+ """Something you can literally type, on its own line.
154
+
155
+ Paired with command_note() underneath rather than a description beside
156
+ it: `work acme-api` plus a sentence does not fit across a panel, and a
157
+ command that wraps is a command you cannot copy.
158
+ """
159
+ return f"{_style(RAIL_ON, fg(ACCENT))} {_style(cmd, BOLD)}"
160
+
161
+
162
+ def command_note(what: str) -> str:
163
+ """What the command above does."""
164
+ return f" {_style(what, DIM)}"
165
+
166
+
167
+ def note(text: str) -> str:
168
+ return _style(text, DIM)
169
+
170
+
171
+ def warn(text: str) -> str:
172
+ return _style(text, AMBER)
173
+
174
+
175
+ def bullet(text: str) -> str:
176
+ return f"{_style(RAIL_ON, fg(ACCENT))} {text}"
177
+
178
+
179
+ def spin(label: str, work):
180
+ """Run `work()` behind a spinner. The spinner tracks real work only.
181
+
182
+ If the work finishes instantly the spinner is never drawn, because a
183
+ loader for something that did not take time is a small lie.
184
+ """
185
+ if not animate():
186
+ result = work()
187
+ say(" " + listing(label, ok=True))
188
+ return result
189
+
190
+ box: dict = {}
191
+
192
+ def run():
193
+ try:
194
+ box["value"] = work()
195
+ except BaseException as exc: # surfaced by the caller
196
+ box["error"] = exc
197
+
198
+ thread = threading.Thread(target=run, daemon=True)
199
+ thread.start()
200
+ for frame in itertools.cycle(SPINNER):
201
+ thread.join(timeout=FRAME_S)
202
+ if not thread.is_alive():
203
+ break
204
+ sys.stdout.write(f"\r {_style(frame, fg(ACCENT))} {note(label)}")
205
+ sys.stdout.flush()
206
+ sys.stdout.write("\r\033[2K")
207
+ if "error" in box:
208
+ raise box["error"]
209
+ return box.get("value")
210
+
211
+
212
+ def ask(question: str, *, enter_does: str, default: str = "") -> str:
213
+ """A question, and what the Enter key will do about it.
214
+
215
+ `enter_does` is not optional on purpose. The first version of this screen
216
+ rendered "change them? colon-separated paths [keep]" and the first person
217
+ to use it did not know that pressing Enter would move on, a prompt that
218
+ does not name its own default is a prompt you have to guess at.
219
+ """
220
+ arrow = _style("›", fg(ACCENT))
221
+ hint = _style(f"(Enter = {enter_does})", DIM)
222
+ try:
223
+ answer = input(f" {arrow} {question} {hint} ").strip()
224
+ except (EOFError, KeyboardInterrupt):
225
+ print()
226
+ return default
227
+ return answer or default
228
+
229
+
230
+ def title_card(steps: list[str]) -> list[str]:
231
+ """What is about to happen, and how to get through it.
232
+
233
+ Listing the steps up front costs four lines and buys the thing every
234
+ setup screen is missing: knowing how long this is and what it wants.
235
+ """
236
+ inner = width() - 4
237
+ out = [
238
+ box_top(inner, title=_style("workmap", BOLD)),
239
+ box_row(inner),
240
+ box_row(inner, " " + _style("Let's get you set up.", BOLD)),
241
+ box_row(inner),
242
+ ]
243
+ for i, step in enumerate(steps, 1):
244
+ out.append(box_row(inner, f" {_style(str(i), fg(ACCENT), BOLD)} {step}"))
245
+ out.append(box_row(inner))
246
+ out.append(box_row(inner, " " + _style(
247
+ "Each question suggests an answer in grey.", DIM)))
248
+ out.append(box_row(inner, " " + _style(
249
+ "Press Enter to take it, or type your own.", DIM)))
250
+ out.append(box_row(inner))
251
+ out.append(box_bot(inner))
252
+ return out
253
+
254
+
255
+ def done_card(lines: list[str], title: str = "you're all set") -> list[str]:
256
+ """The last screen. `title` because a run where a step was skipped is not
257
+ one where you are all set, and saying so anyway is the kind of small lie
258
+ that makes the rest of the screen worth less."""
259
+ inner = width() - 4
260
+ head = (fg(ACCENT) + BOLD + title + RESET if colour() else title)
261
+ out = [box_top(inner, title=head), box_row(inner)]
262
+ for line in lines:
263
+ out.append(box_row(inner, " " + line if line else ""))
264
+ out.append(box_row(inner))
265
+ out.append(box_bot(inner))
266
+ return out
workmap/tui/text.py ADDED
@@ -0,0 +1,156 @@
1
+ """ANSI styling and the display-width model.
2
+
3
+ Terminal titles carry CJK and emoji, so every pad/truncate measures display
4
+ columns rather than len(). Pure string maths, no state, no I/O."""
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ import unicodedata
9
+
10
+
11
+ RESET = "\033[0m"
12
+
13
+ DIM = "\033[2m"
14
+
15
+ BOLD = "\033[1m"
16
+
17
+ REV = "\033[7m"
18
+
19
+ HIDE = "\033[?25l"
20
+
21
+ SHOW = "\033[?25h"
22
+
23
+ ALT_ON = "\033[?1049h"
24
+
25
+ ALT_OFF = "\033[?1049l"
26
+
27
+ HOME = "\033[H"
28
+
29
+ CLEAR_LINE = "\033[2K"
30
+
31
+ # Restrained palette: borders recede, chips pop, amber = needs attention.
32
+ BORDER = "\033[38;5;240m"
33
+
34
+ CHIP_BG = "\033[48;5;238m"
35
+
36
+ CHIP_TX = "\033[38;5;231m"
37
+
38
+ AMBER = "\033[38;5;208m" # real warnings only: swap pressure, confirmations
39
+
40
+ TAG = "\033[38;5;137m" # muted marker for the inline "orphaned" tag
41
+
42
+ GOOD = "\033[38;5;114m"
43
+
44
+
45
+ def fg(n: int) -> str:
46
+ return f"\033[38;5;{n}m"
47
+
48
+ # ---------------------------------------------------------------- width model
49
+ # Every pad/truncate goes through display columns, not len(). Terminal titles
50
+ # carry CJK and emoji; len() would silently skew whole columns.
51
+ _ANSI_RE = re.compile(r"\033\[[0-9;?]*[ -/]*[@-~]")
52
+
53
+
54
+ def _char_w(ch: str) -> int:
55
+ if unicodedata.combining(ch):
56
+ return 0
57
+ o = ord(ch)
58
+ if o < 32 or o == 0x7F:
59
+ return 0
60
+ if unicodedata.east_asian_width(ch) in ("W", "F"):
61
+ return 2
62
+ if 0x1F300 <= o <= 0x1FAFF or 0x1F004 <= o <= 0x1F0CF:
63
+ return 2
64
+ return 1
65
+
66
+
67
+ def disp_width(s: str) -> int:
68
+ """Display columns of PLAIN text."""
69
+ return sum(_char_w(c) for c in s)
70
+
71
+
72
+ def strip_ansi(s: str) -> str:
73
+ return _ANSI_RE.sub("", s)
74
+
75
+
76
+ def visible_len(s: str) -> int:
77
+ """Display columns of possibly-styled text."""
78
+ return disp_width(strip_ansi(s))
79
+
80
+
81
+ def cell(text: str, width: int, *, align: str = "left") -> str:
82
+ """Exact-width PLAIN cell. Truncate/pad before any ANSI is applied.
83
+
84
+ Control characters are dropped rather than measured. They are zero columns
85
+ wide, so the arithmetic here was always right, but a cell is composed into
86
+ a screen the caller has already laid out and an escape character carries no
87
+ width while still moving the cursor. Project names come from directory
88
+ names and session labels from window titles, so this is the last point
89
+ where either can still be text rather than instructions.
90
+
91
+ A combining mark at the front is dropped for the same reason. It has
92
+ nothing inside the cell to attach to, so the terminal draws it over
93
+ whatever occupies the column to the left: the rail glyph, or the last
94
+ character of the cell before. One cell putting a mark in another one is
95
+ the same class of problem as an escape moving the cursor.
96
+ """
97
+ text = "".join(" " if ch in "\t\n\r" else ch
98
+ for ch in text if (ch >= " " and ch != "\x7f") or ch in "\t\n\r")
99
+ lead = 0
100
+ while lead < len(text) and unicodedata.combining(text[lead]):
101
+ lead += 1
102
+ text = text[lead:]
103
+ if width <= 0:
104
+ return ""
105
+ w = disp_width(text)
106
+ if w > width:
107
+ out = ""
108
+ used = 0
109
+ for ch in text:
110
+ cw = _char_w(ch)
111
+ if used + cw > width - 1:
112
+ break
113
+ out += ch
114
+ used += cw
115
+ return out + "…" + " " * (width - 1 - used)
116
+ return (" " * (width - w)) + text if align == "right" else text + (" " * (width - w))
117
+
118
+
119
+ def pad_ansi(s: str, width: int) -> str:
120
+ """Pad/truncate styled text to exact display columns (ANSI-safe)."""
121
+ w = visible_len(s)
122
+ if w <= width:
123
+ return s + " " * (width - w)
124
+ out: list[str] = []
125
+ used = 0
126
+ i = 0
127
+ while i < len(s):
128
+ if s[i] == "\033":
129
+ m = _ANSI_RE.match(s, i)
130
+ if m:
131
+ out.append(m.group())
132
+ i = m.end()
133
+ continue
134
+ cw = _char_w(s[i])
135
+ if used + cw > max(0, width - 1):
136
+ break
137
+ out.append(s[i])
138
+ used += cw
139
+ i += 1
140
+ return "".join(out) + "…" + RESET + " " * max(0, width - used - 1)
141
+
142
+
143
+ def wrap_plain(text: str, width: int) -> list[str]:
144
+ words = text.split()
145
+ lines: list[str] = []
146
+ cur = ""
147
+ for w in words:
148
+ trial = f"{cur} {w}".strip()
149
+ if disp_width(trial) <= width or not cur:
150
+ cur = trial
151
+ else:
152
+ lines.append(cur)
153
+ cur = w
154
+ if cur:
155
+ lines.append(cur)
156
+ return lines or [""]
workmap/tui/widgets.py ADDED
@@ -0,0 +1,189 @@
1
+ """The visual vocabulary: rounded panels, key chips, and table rows.
2
+
3
+ Every function returns a string of an exact display width, so the caller can
4
+ compose without re-measuring."""
5
+ from __future__ import annotations
6
+
7
+ from .text import (AMBER, BOLD, CHIP_BG, CHIP_TX, DIM, RESET, REV, BORDER,
8
+ TAG, cell, disp_width, pad_ansi,
9
+ strip_ansi, visible_len)
10
+
11
+
12
+ # ------------------------------------------------------------------- geometry
13
+ # One measure for the whole UI: header, list and command panels share edges.
14
+ FRAME_MIN = 44
15
+
16
+ FRAME_MAX = 66
17
+
18
+ RAIL_W = 1
19
+
20
+ PROJ_W = 14
21
+
22
+ THEME_W = 14
23
+
24
+ RAM_W = 7
25
+
26
+ META_GAP = 2 # THEME → RAM
27
+
28
+ TABLE_MIN = RAIL_W + PROJ_W + 1 + THEME_W + META_GAP + RAM_W # 39
29
+
30
+ CHIP_W = 5 # widest chip is " Esc "
31
+
32
+ RAIL_OFF = "▏" # hairline rail, distinct from the panel's own │ border
33
+
34
+ RAIL_ON = "▊" # selected project's rail lights up
35
+
36
+
37
+ def frame_width(cols: int) -> int:
38
+ """Total width of the boxed UI, borders included."""
39
+ return max(FRAME_MIN, min(cols - 2, FRAME_MAX))
40
+
41
+
42
+ def boxed(cols: int) -> bool:
43
+ return cols >= FRAME_MIN + 2
44
+
45
+
46
+ def inner_width(cols: int) -> int:
47
+ """Content measure. Same column engine boxed or not, only borders differ."""
48
+ return frame_width(cols) - 4 if boxed(cols) else max(TABLE_MIN, cols - 1)
49
+
50
+ # ---------------------------------------------------------------------- boxes
51
+ TL, TR, BL, BR, HBAR, VBAR = "╭", "╮", "╰", "╯", "─", "│"
52
+
53
+
54
+ def _edge(inner: int, left: str, right: str, title: str = "", tail: str = "") -> str:
55
+ """Rounded rule with optional inline title (left) and tail (right)."""
56
+ span = inner + 2
57
+ t = f" {title} " if title else ""
58
+ e = f" {tail} " if tail else ""
59
+ fill = span - 2 - visible_len(t) - visible_len(e)
60
+ if fill < 1 and e:
61
+ e = ""
62
+ fill = span - 2 - visible_len(t)
63
+ if fill < 1 and t:
64
+ t = cell(strip_ansi(title), max(1, span - 4))
65
+ t = f" {t} "
66
+ fill = max(0, span - 2 - visible_len(t))
67
+ bar = BORDER + HBAR + RESET
68
+ return (
69
+ f"{BORDER}{left}{RESET}{bar}{t}{BORDER}{HBAR * fill}{RESET}"
70
+ f"{e}{bar}{BORDER}{right}{RESET}"
71
+ )
72
+
73
+
74
+ def box_top(inner: int, title: str = "", tail: str = "") -> str:
75
+ return _edge(inner, TL, TR, title, tail)
76
+
77
+
78
+ def box_bot(inner: int, title: str = "", tail: str = "") -> str:
79
+ return _edge(inner, BL, BR, title, tail)
80
+
81
+
82
+ def box_row(inner: int, content: str = "") -> str:
83
+ v = f"{BORDER}{VBAR}{RESET}"
84
+ return f"{v} {pad_ansi(content, inner)} {v}"
85
+
86
+ # ------------------------------------------------------------- command panels
87
+
88
+
89
+ def chip(key: str, width: int = CHIP_W) -> str:
90
+ """Key glyph on a solid chip so the eye lands on keys before labels."""
91
+ body = f" {key} "
92
+ pad = max(0, width - disp_width(body))
93
+ return f"{CHIP_BG}{CHIP_TX}{BOLD}{body}{RESET}" + " " * pad
94
+
95
+
96
+ def cmd_cell(key: str, label: str, width: int) -> str:
97
+ # Labels may carry ANSI (dim counts), measure and pad in display columns.
98
+ return chip(key) + pad_ansi(label, max(0, width - CHIP_W))
99
+
100
+
101
+ def longest_label(items: list[tuple], short: bool = False) -> int:
102
+ idx = 2 if short else 1
103
+ return max(visible_len(str(i[idx])) for i in items) if items else 0
104
+
105
+
106
+ def panel(width: int, title: str, items: list[tuple], *, columns: int = 1,
107
+ short: bool = False, min_rows: int = 0) -> list[str]:
108
+ """A titled command panel exactly `width` display columns wide.
109
+
110
+ min_rows pads with empty rows so side-by-side panels close on the same
111
+ line, uneven bottom edges read as a layout bug, not as design.
112
+ """
113
+ inner = width - 4
114
+ idx = 2 if short else 1
115
+ lines = [box_top(inner, title=title)]
116
+ per = (inner - 2 * (columns - 1)) // columns
117
+ for i in range(0, len(items), columns):
118
+ chunk = items[i : i + columns]
119
+ cells = [cmd_cell(it[0], str(it[idx]), per) for it in chunk]
120
+ while len(cells) < columns:
121
+ cells.append(" " * per)
122
+ lines.append(box_row(inner, " ".join(cells)))
123
+ while len(lines) - 1 < min_rows:
124
+ lines.append(box_row(inner, ""))
125
+ lines.append(box_bot(inner))
126
+ return lines
127
+
128
+
129
+ def flat_panel(width: int, title: str, items: list[tuple], *,
130
+ short: bool = False) -> list[str]:
131
+ """Borderless panel for terminals too narrow to spend 2 columns on chrome."""
132
+ idx = 2 if short else 1
133
+ return [f" {title}"] + [
134
+ " " + cmd_cell(it[0], str(it[idx]), width - 1) for it in items
135
+ ]
136
+
137
+
138
+ def fit_strip(bits: list[tuple[str, str]], width: int) -> str:
139
+ """Chip strip that sheds commands rather than getting cut mid-label.
140
+
141
+ Sheds from the back but keeps the last pair pinned, the tail is the way
142
+ out (`q quit`), and a strip that can't tell you how to leave is a trap.
143
+ """
144
+ items = list(bits)
145
+
146
+ def render(seq):
147
+ return " ".join(f"{chip(k, disp_width(k) + 2)} {lab}" for k, lab in seq)
148
+
149
+ while len(items) > 1:
150
+ if visible_len(render(items)) <= width:
151
+ return render(items)
152
+ items.pop(-2)
153
+ return render(items) if items and visible_len(render(items)) <= width else ""
154
+
155
+
156
+ def side_by_side(left: list[str], lw: int, right: list[str], rw: int) -> list[str]:
157
+ """Two panels on one row; short one is padded so both bottoms line up."""
158
+ n = max(len(left), len(right))
159
+ out = []
160
+ for i in range(n):
161
+ a = left[i] if i < len(left) else " " * lw
162
+ b = right[i] if i < len(right) else " " * rw
163
+ out.append(f"{pad_ansi(a, lw)} {pad_ansi(b, rw)}")
164
+ return out
165
+
166
+ # ------------------------------------------------------------------ list rows
167
+
168
+
169
+ def project_row(name: str, theme: str, ram: str, inner: int, *,
170
+ rail: str, selected: bool) -> str:
171
+ """rail | PROJECT | THEME … RAM, identity clustered left, size anchored right."""
172
+ gutter = max(META_GAP, inner - TABLE_MIN + META_GAP)
173
+ p = cell(name, PROJ_W)
174
+ t = cell(theme, THEME_W)
175
+ r = cell(ram, RAM_W, align="right")
176
+ p_out = f"{REV}{p}{RESET}" if selected else p
177
+ return f"{rail}{p_out} {DIM}{t}{RESET}{' ' * gutter}{BOLD}{r}{RESET}"
178
+
179
+
180
+ def session_row(rail: str, label: str, extra: str, ram: str, inner: int) -> str:
181
+ """Session lines get the full measure, long agent titles stay readable."""
182
+ label_w = inner - RAIL_W - META_GAP - RAM_W
183
+ tail = f" · {extra}" if extra else ""
184
+ # Truncate the label, never the "· orphaned" tag it's qualified by.
185
+ keep = max(1, min(disp_width(label), label_w - disp_width(tail)))
186
+ accent = TAG if extra == "orphaned" else DIM
187
+ body = f"{DIM}{cell(label, keep)}{RESET}{accent}{cell(tail, label_w - keep)}{RESET}"
188
+ r = cell(ram, RAM_W, align="right")
189
+ return f"{rail}{body}{' ' * META_GAP}{DIM}{r}{RESET}"