termrender 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {termrender-0.6.0 → termrender-0.7.0}/CHANGELOG.md +37 -0
- {termrender-0.6.0 → termrender-0.7.0}/PKG-INFO +1 -1
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/__main__.py +121 -36
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/borders.py +19 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/text.py +2 -2
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_column_alignment.py +76 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_tasklist.py +3 -3
- {termrender-0.6.0 → termrender-0.7.0}/.github/workflows/publish.yml +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/.gitignore +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/CLAUDE.md +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/LICENSE +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/README.md +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/design.json +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/pyproject.toml +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/requirements.json +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/CLAUDE.md +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/__init__.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/blocks.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/emit.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/layout.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/parser.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/py.typed +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/CLAUDE.md +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/__init__.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/charts.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/code.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/columns.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/diff.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/divider.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/mermaid.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/panel.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/quote.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/stat.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/table.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/timeline.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/renderers/tree.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/src/termrender/style.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/__init__.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_charts.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_diff.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_inline_badge.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_myst_gaps.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_stat.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_timeline.py +0 -0
- {termrender-0.6.0 → termrender-0.7.0}/tests/test_variable_colons.py +0 -0
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.7.0 (2026-04-08)
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
- **cli**: Add --pane for in-place tmux pane updates
|
|
9
|
+
([`4ab1d77`](https://github.com/CaptainCrouton89/termrender/commit/4ab1d77b996aa356926407dcc11c1b408e68e0ee))
|
|
10
|
+
|
|
11
|
+
--tmux now prints the newly-created pane id to stdout (via split-window -P -F) so callers can
|
|
12
|
+
capture it for subsequent updates. --pane <ID> targets an existing pane via tmux respawn-pane -k
|
|
13
|
+
instead of spawning a new one — the existing process is killed and replaced with the new render.
|
|
14
|
+
This lets agents synchronously re-render a doc on every edit without spawning fresh panes or
|
|
15
|
+
relying on --watch polling.
|
|
16
|
+
|
|
17
|
+
Also in this commit: - Expand -h epilog to cover the 8 visualization directives (stat, bar,
|
|
18
|
+
progress, gauge, diff, timeline, tasklist, inline badge) and rewrite the nesting note to describe
|
|
19
|
+
the strict colon-count rule. The previous epilog only documented the base directives and said
|
|
20
|
+
"every opener needs a matching :::", which contradicts the actual parser behavior. - Render
|
|
21
|
+
tasklist checkboxes as filled/empty dots (● / ○ / ◐) instead of boxed glyphs (☑ / ☐ / ◐).
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## v0.6.1 (2026-04-08)
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
- **borders**: Grow render_box to fit overflowing content and titles
|
|
29
|
+
([`dc108c8`](https://github.com/CaptainCrouton89/termrender/commit/dc108c8242763828245569f719abce64b26ddf5b))
|
|
30
|
+
|
|
31
|
+
mermaid-ascii's --maxWidth is non-strict, so a child mermaid block can return lines wider than the
|
|
32
|
+
panel's allocated content area. Previously the side walls floated outward to accommodate the
|
|
33
|
+
content while the top/bottom borders stayed at the requested width, leaving corner glyphs one
|
|
34
|
+
column inside the side walls and producing a visibly jagged box.
|
|
35
|
+
|
|
36
|
+
render_box now measures the widest content line (and the title) and grows its effective width up
|
|
37
|
+
front so all four borders land at the same column. Trade-off: the box may overflow its parent
|
|
38
|
+
allocation, but the box itself is internally consistent.
|
|
39
|
+
|
|
40
|
+
|
|
4
41
|
## v0.6.0 (2026-04-07)
|
|
5
42
|
|
|
6
43
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termrender
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Rich terminal rendering of directive-flavored markdown
|
|
5
5
|
Project-URL: Homepage, https://github.com/CaptainCrouton89/termrender
|
|
6
6
|
Project-URL: Repository, https://github.com/CaptainCrouton89/termrender
|
|
@@ -19,7 +19,7 @@ except Exception:
|
|
|
19
19
|
__version__ = "dev"
|
|
20
20
|
|
|
21
21
|
_EPILOG = """\
|
|
22
|
-
directives (close each with
|
|
22
|
+
directives (close each with a matching colon count):
|
|
23
23
|
:::panel{title="T" color="c"} Bordered box
|
|
24
24
|
attrs: title (string), color (red|green|yellow|blue|magenta|cyan|white|gray)
|
|
25
25
|
:::columns Side-by-side column layout container
|
|
@@ -33,19 +33,48 @@ directives (close each with :::):
|
|
|
33
33
|
attrs: author or by (string)
|
|
34
34
|
:::code{lang="python"} Code block with syntax highlighting
|
|
35
35
|
attrs: lang (any Pygments lexer name)
|
|
36
|
-
:::divider{label="L"} Horizontal rule (self-closing
|
|
36
|
+
:::divider{label="L"} Horizontal rule (top-level self-closing)
|
|
37
37
|
attrs: label (centered text)
|
|
38
|
+
:::stat{label="L" value="V" delta="D"}
|
|
39
|
+
KPI tile — label + big value + trend arrow
|
|
40
|
+
attrs: label, value, delta (e.g. "-12%"), trend=up|down|flat
|
|
41
|
+
:::bar{title="T" color="c"} Multi-bar horizontal chart
|
|
42
|
+
body: one "label: value" per line
|
|
43
|
+
:::progress{value=70 max=100 label="L"}
|
|
44
|
+
Single-line progress bar (top-level self-closing)
|
|
45
|
+
attrs: value, max, label, color (auto by ratio if unset)
|
|
46
|
+
:::gauge{value=88 max=100 label="L" unit="%"}
|
|
47
|
+
3-line meter — label, bar, readout (top-level self-closing)
|
|
48
|
+
attrs: value, max, label, unit, color (auto by load if unset)
|
|
49
|
+
:::diff{title="T"} Colored unified diff (+green / -red / @magenta)
|
|
50
|
+
attrs: title (defaults to "diff")
|
|
51
|
+
:::timeline{title="T" color="c"} Vertical event timeline
|
|
52
|
+
body: one "- date: event" per line (| also works as separator)
|
|
53
|
+
:::tasklist Checkbox list — [x] checked, [ ] unchecked, [!] in-progress
|
|
54
|
+
Plain lists with at least one marker auto-promote; use the directive
|
|
55
|
+
to force unchecked styling on items without explicit markers.
|
|
38
56
|
```mermaid ... ``` Mermaid diagram (via mermaid-ascii)
|
|
39
57
|
|
|
58
|
+
Inline:
|
|
59
|
+
:badge[text]{color=c} Inline pill badge
|
|
60
|
+
colors: red|green|yellow|blue|magenta|cyan|gray (default blue)
|
|
61
|
+
|
|
40
62
|
nesting:
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
Outer fences must use STRICTLY MORE colons than the inner fences they
|
|
64
|
+
wrap. Closers are paired by colon count; a wrong-count closer is silently
|
|
65
|
+
re-parsed as body content. Use --check to validate.
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
:::
|
|
46
|
-
|
|
67
|
+
::::columns ← 4 colons (outer)
|
|
68
|
+
:::col{width="50%"} ← 3 colons (inner)
|
|
69
|
+
Left content.
|
|
47
70
|
:::
|
|
71
|
+
:::col{width="50%"}
|
|
72
|
+
Right content.
|
|
48
73
|
:::
|
|
74
|
+
::::
|
|
75
|
+
|
|
76
|
+
divider, progress, and gauge are self-closing ONLY at the top level —
|
|
77
|
+
nested inside another directive they need an explicit closer.
|
|
49
78
|
|
|
50
79
|
markup:
|
|
51
80
|
# heading **bold** *italic* `code`
|
|
@@ -76,6 +105,12 @@ examples:
|
|
|
76
105
|
termrender --tmux doc.md Render in a new tmux side pane
|
|
77
106
|
termrender --watch doc.md Live-render in current terminal
|
|
78
107
|
termrender --tmux --watch doc.md Live-render in a new tmux side pane
|
|
108
|
+
|
|
109
|
+
# Synchronous pane updates: spawn once, then re-render in place.
|
|
110
|
+
# --tmux prints the new pane id; pass it back via --pane on subsequent calls.
|
|
111
|
+
PANE=$(termrender --tmux doc.md)
|
|
112
|
+
termrender --pane "$PANE" doc.md # update the same pane after edits
|
|
113
|
+
|
|
79
114
|
termrender <<'EOF'
|
|
80
115
|
:::panel{title="Status" color="green"}
|
|
81
116
|
- All systems operational
|
|
@@ -209,7 +244,13 @@ def main() -> None:
|
|
|
209
244
|
parser.add_argument(
|
|
210
245
|
"--tmux",
|
|
211
246
|
action="store_true",
|
|
212
|
-
help="open rendered output in a new tmux side pane (requires tmux)",
|
|
247
|
+
help="open rendered output in a new tmux side pane (requires tmux). Prints the new pane id to stdout",
|
|
248
|
+
)
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
"--pane",
|
|
251
|
+
metavar="ID",
|
|
252
|
+
default=None,
|
|
253
|
+
help="tmux pane id to update in place (e.g. %%23) instead of spawning a new pane. Implies --tmux",
|
|
213
254
|
)
|
|
214
255
|
parser.add_argument(
|
|
215
256
|
"--watch",
|
|
@@ -223,6 +264,10 @@ def main() -> None:
|
|
|
223
264
|
)
|
|
224
265
|
args = parser.parse_args()
|
|
225
266
|
|
|
267
|
+
# --pane implies --tmux (it's only meaningful in a tmux session)
|
|
268
|
+
if args.pane:
|
|
269
|
+
args.tmux = True
|
|
270
|
+
|
|
226
271
|
# --watch needs a real file path to poll; stdin can't be watched.
|
|
227
272
|
if args.watch and args.file is None:
|
|
228
273
|
_error(
|
|
@@ -274,32 +319,51 @@ def main() -> None:
|
|
|
274
319
|
fix="run inside tmux or omit --tmux")
|
|
275
320
|
|
|
276
321
|
# Determine desired pane width
|
|
277
|
-
if args.
|
|
278
|
-
|
|
322
|
+
if args.pane:
|
|
323
|
+
# Updating an existing pane: use its current width unless overridden.
|
|
324
|
+
# No measurement / capping pass — the pane is already sized.
|
|
325
|
+
if args.width:
|
|
326
|
+
pane_width = args.width
|
|
327
|
+
else:
|
|
328
|
+
try:
|
|
329
|
+
result = subprocess.run(
|
|
330
|
+
["tmux", "display-message", "-p", "-t", args.pane, "#{pane_width}"],
|
|
331
|
+
capture_output=True, text=True, check=True,
|
|
332
|
+
)
|
|
333
|
+
pane_width = int(result.stdout.strip())
|
|
334
|
+
except (subprocess.CalledProcessError, ValueError, FileNotFoundError):
|
|
335
|
+
_error(
|
|
336
|
+
f"could not query tmux pane {args.pane}",
|
|
337
|
+
fix="check that the pane id is valid (e.g. %23)",
|
|
338
|
+
)
|
|
339
|
+
pane_width = max(pane_width, 20)
|
|
279
340
|
else:
|
|
280
|
-
|
|
281
|
-
|
|
341
|
+
if args.width:
|
|
342
|
+
pane_width = args.width
|
|
343
|
+
else:
|
|
344
|
+
# Preview render to measure content width
|
|
345
|
+
from termrender.style import visual_len
|
|
346
|
+
try:
|
|
347
|
+
preview = render(source, width=80, color=False)
|
|
348
|
+
max_w = max(
|
|
349
|
+
(visual_len(line) for line in preview.split('\n') if line),
|
|
350
|
+
default=40,
|
|
351
|
+
)
|
|
352
|
+
pane_width = max(max_w, 40)
|
|
353
|
+
except Exception:
|
|
354
|
+
pane_width = 80
|
|
355
|
+
|
|
356
|
+
# Cap to available tmux space (leave room for the source pane)
|
|
282
357
|
try:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
default=40,
|
|
358
|
+
result = subprocess.run(
|
|
359
|
+
["tmux", "display-message", "-p", "#{pane_width}"],
|
|
360
|
+
capture_output=True, text=True, check=True,
|
|
287
361
|
)
|
|
288
|
-
|
|
362
|
+
available = int(result.stdout.strip())
|
|
363
|
+
pane_width = min(pane_width, available - 10)
|
|
289
364
|
except Exception:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# Cap to available tmux space (leave room for the source pane)
|
|
293
|
-
try:
|
|
294
|
-
result = subprocess.run(
|
|
295
|
-
["tmux", "display-message", "-p", "#{pane_width}"],
|
|
296
|
-
capture_output=True, text=True, check=True,
|
|
297
|
-
)
|
|
298
|
-
available = int(result.stdout.strip())
|
|
299
|
-
pane_width = min(pane_width, available - 10)
|
|
300
|
-
except Exception:
|
|
301
|
-
pass
|
|
302
|
-
pane_width = max(pane_width, 20) # absolute minimum
|
|
365
|
+
pass
|
|
366
|
+
pane_width = max(pane_width, 20) # absolute minimum
|
|
303
367
|
|
|
304
368
|
# Watch mode points the new pane at the user's real file so edits
|
|
305
369
|
# propagate; non-watch mode snapshots source into a tempfile.
|
|
@@ -340,10 +404,23 @@ def main() -> None:
|
|
|
340
404
|
)
|
|
341
405
|
|
|
342
406
|
try:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
407
|
+
if args.pane:
|
|
408
|
+
# respawn-pane -k kills the existing process in the target
|
|
409
|
+
# pane and runs the new command. The pane id stays the same.
|
|
410
|
+
subprocess.run(
|
|
411
|
+
["tmux", "respawn-pane", "-k", "-t", args.pane, pane_cmd],
|
|
412
|
+
check=True,
|
|
413
|
+
)
|
|
414
|
+
pane_id = args.pane
|
|
415
|
+
else:
|
|
416
|
+
# -P -F prints the new pane's id to stdout so the caller
|
|
417
|
+
# can capture it for subsequent --pane updates.
|
|
418
|
+
result = subprocess.run(
|
|
419
|
+
["tmux", "split-window", "-h", "-f", "-l", str(pane_width),
|
|
420
|
+
"-P", "-F", "#{pane_id}", pane_cmd],
|
|
421
|
+
check=True, capture_output=True, text=True,
|
|
422
|
+
)
|
|
423
|
+
pane_id = result.stdout.strip()
|
|
347
424
|
except FileNotFoundError:
|
|
348
425
|
if tmpfile:
|
|
349
426
|
os.unlink(tmpfile)
|
|
@@ -351,9 +428,17 @@ def main() -> None:
|
|
|
351
428
|
except subprocess.CalledProcessError:
|
|
352
429
|
if tmpfile:
|
|
353
430
|
os.unlink(tmpfile)
|
|
354
|
-
|
|
355
|
-
|
|
431
|
+
if args.pane:
|
|
432
|
+
_error(
|
|
433
|
+
f"failed to update tmux pane {args.pane}",
|
|
434
|
+
hint="check that the pane id is still valid",
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
_error("failed to create tmux pane",
|
|
438
|
+
hint="check that tmux is running and has space for a new pane")
|
|
356
439
|
|
|
440
|
+
# Echo the pane id so callers can chain --pane updates
|
|
441
|
+
print(pane_id)
|
|
357
442
|
sys.exit(EXIT_OK)
|
|
358
443
|
|
|
359
444
|
# --watch: live-render in the current terminal
|
|
@@ -30,6 +30,25 @@ def render_box(
|
|
|
30
30
|
dash_v = visual_len("─")
|
|
31
31
|
corner_v = visual_len("┌") # same as ┐, └, ┘
|
|
32
32
|
|
|
33
|
+
# Grow the box if any content line (or the title) won't fit at the
|
|
34
|
+
# requested width. mermaid-ascii's --maxWidth is non-strict, so a child
|
|
35
|
+
# mermaid block can return lines wider than its allocated content area.
|
|
36
|
+
# Truncating would corrupt the diagram; growing keeps the box's top,
|
|
37
|
+
# bottom, and side borders aligned at the same column even if it
|
|
38
|
+
# overflows the parent's allocation.
|
|
39
|
+
content_max = visual_len("")
|
|
40
|
+
for cl in content_lines:
|
|
41
|
+
cl_w = visual_len(cl)
|
|
42
|
+
if cl_w > content_max:
|
|
43
|
+
content_max = cl_w
|
|
44
|
+
required_for_content = content_max + 2 + 2 * border_v # pads + walls
|
|
45
|
+
required_for_title = 0
|
|
46
|
+
if title:
|
|
47
|
+
title_part_w = dash_v + 2 + visual_len(title) # "─ TITLE "
|
|
48
|
+
# Reserve at least one trailing dash so the corner has chrome.
|
|
49
|
+
required_for_title = title_part_w + dash_v + 2 * border_v
|
|
50
|
+
width = max(width, required_for_content, required_for_title)
|
|
51
|
+
|
|
33
52
|
# inner_w = space between the two vertical borders
|
|
34
53
|
inner_w = width - 2 * border_v
|
|
35
54
|
# content_w = space for actual content (inner minus 1-space padding each side)
|
|
@@ -104,10 +104,10 @@ def _render_heading(block: Block, color: bool) -> list[str]:
|
|
|
104
104
|
def _task_prefix(item: Block, color: bool) -> str:
|
|
105
105
|
"""Build a checkbox prefix for a list item with `checked`/`pending` attrs."""
|
|
106
106
|
if item.attrs.get("checked"):
|
|
107
|
-
return style("
|
|
107
|
+
return style("● ", color="green", enabled=color)
|
|
108
108
|
if item.attrs.get("pending"):
|
|
109
109
|
return style("◐ ", color="yellow", enabled=color)
|
|
110
|
-
return style("
|
|
110
|
+
return style("○ ", dim=True, enabled=color)
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def _render_list(block: Block, color: bool) -> list[str]:
|
|
@@ -81,6 +81,30 @@ c/
|
|
|
81
81
|
::::
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
|
+
# A panel nested inside :::col whose content (a wide mermaid diagram) exceeds
|
|
85
|
+
# the column allocation. The inner panel's side walls and corner glyphs must
|
|
86
|
+
# stay aligned even when content forces the panel to grow past its allotment.
|
|
87
|
+
NESTED_PANEL_OVERFLOW_INPUT = """\
|
|
88
|
+
::::::panel{title="Outer" color="cyan"}
|
|
89
|
+
:::::columns
|
|
90
|
+
::::col{width="58%"}
|
|
91
|
+
:::panel{title="Request Flow" color="blue"}
|
|
92
|
+
```mermaid
|
|
93
|
+
graph TD
|
|
94
|
+
A[Edge gateway<br/>accepts request] --> B{Token valid?}
|
|
95
|
+
B -->|yes| C[Route via LB<br/>to backend pool]
|
|
96
|
+
B -->|no| D[Reject 401,<br/>write audit log]
|
|
97
|
+
C --> E[Service responds,<br/>metrics emitted]
|
|
98
|
+
```
|
|
99
|
+
:::
|
|
100
|
+
::::
|
|
101
|
+
::::col{width="42%"}
|
|
102
|
+
right
|
|
103
|
+
::::
|
|
104
|
+
:::::
|
|
105
|
+
::::::
|
|
106
|
+
"""
|
|
107
|
+
|
|
84
108
|
|
|
85
109
|
class TestColumnAlignment(unittest.TestCase):
|
|
86
110
|
|
|
@@ -132,6 +156,58 @@ class TestColumnAlignment(unittest.TestCase):
|
|
|
132
156
|
f"Line {i} has visual width {w}, expected 40: {line!r}",
|
|
133
157
|
)
|
|
134
158
|
|
|
159
|
+
def test_nested_panel_corners_align_with_side_walls(self):
|
|
160
|
+
# When a panel inside a column receives content wider than the column
|
|
161
|
+
# allocation, both the side walls AND the top/bottom border glyphs
|
|
162
|
+
# must extend to the same width. Otherwise the corner glyphs (┐ ┘)
|
|
163
|
+
# land one column inside the side walls (│), producing a jagged box.
|
|
164
|
+
from termrender.style import _char_width
|
|
165
|
+
|
|
166
|
+
output = render(NESTED_PANEL_OVERFLOW_INPUT, width=80, color=False)
|
|
167
|
+
lines = output.split("\n")
|
|
168
|
+
if lines and lines[-1] == "":
|
|
169
|
+
lines = lines[:-1]
|
|
170
|
+
|
|
171
|
+
def visual_positions(line, glyphs):
|
|
172
|
+
pos = 0
|
|
173
|
+
found = []
|
|
174
|
+
for ch in line:
|
|
175
|
+
if ch == "\033":
|
|
176
|
+
continue
|
|
177
|
+
if ch in glyphs:
|
|
178
|
+
found.append(pos)
|
|
179
|
+
pos += _char_width(ch)
|
|
180
|
+
return found
|
|
181
|
+
|
|
182
|
+
# The outer panel's left wall is always at pos 0 and the right wall
|
|
183
|
+
# at the rightmost glyph. The inner panel's right border is the
|
|
184
|
+
# next-most-right border glyph in any row that contains both the
|
|
185
|
+
# outer panel's walls and inner-panel content. Across all such rows
|
|
186
|
+
# the inner panel's right border position must be constant.
|
|
187
|
+
inner_right_positions = []
|
|
188
|
+
for line in lines:
|
|
189
|
+
glyphs = sorted(set(visual_positions(line, "┌┐└┘│")))
|
|
190
|
+
# Skip rows that don't span the inner panel (e.g. outer-only).
|
|
191
|
+
if len(glyphs) < 3:
|
|
192
|
+
continue
|
|
193
|
+
outer_right = glyphs[-1]
|
|
194
|
+
# Inner panel's right border is whichever glyph sits just inside
|
|
195
|
+
# the outer right wall — i.e. the next-rightmost.
|
|
196
|
+
inner_right = glyphs[-2]
|
|
197
|
+
# Ignore the inner panel's LEFT wall (which on bare side-wall
|
|
198
|
+
# rows might appear as glyphs[1]) — we only care about the
|
|
199
|
+
# right-side glyph adjacent to the outer right wall.
|
|
200
|
+
if inner_right == glyphs[0]:
|
|
201
|
+
continue
|
|
202
|
+
inner_right_positions.append(inner_right)
|
|
203
|
+
|
|
204
|
+
unique = set(inner_right_positions)
|
|
205
|
+
self.assertEqual(
|
|
206
|
+
len(unique),
|
|
207
|
+
1,
|
|
208
|
+
f"Inner panel right border drifts across rows: {sorted(unique)}",
|
|
209
|
+
)
|
|
210
|
+
|
|
135
211
|
|
|
136
212
|
if __name__ == "__main__":
|
|
137
213
|
unittest.main()
|
|
@@ -27,8 +27,8 @@ class TestTasklist(unittest.TestCase):
|
|
|
27
27
|
def test_renders_checkboxes(self):
|
|
28
28
|
src = "- [x] done\n- [ ] todo\n- [!] in progress\n"
|
|
29
29
|
output = render(src, width=40, color=False)
|
|
30
|
-
self.assertIn("
|
|
31
|
-
self.assertIn("
|
|
30
|
+
self.assertIn("●", output) # checked
|
|
31
|
+
self.assertIn("○", output) # unchecked
|
|
32
32
|
self.assertIn("◐", output) # pending
|
|
33
33
|
|
|
34
34
|
def test_tasklist_directive_alias(self):
|
|
@@ -45,7 +45,7 @@ class TestTasklist(unittest.TestCase):
|
|
|
45
45
|
src = ":::tasklist\n- foo\n- bar\n:::"
|
|
46
46
|
output = render(src, width=40, color=False)
|
|
47
47
|
# Both items should render as unchecked
|
|
48
|
-
self.assertEqual(output.count("
|
|
48
|
+
self.assertEqual(output.count("○"), 2)
|
|
49
49
|
|
|
50
50
|
def test_visual_widths_match(self):
|
|
51
51
|
src = "- [x] done\n- [ ] todo\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|