termrender 0.8.0__tar.gz → 1.0.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-1.0.0/.git +1 -0
- {termrender-0.8.0 → termrender-1.0.0}/CHANGELOG.md +90 -29
- termrender-1.0.0/CLAUDE.md +25 -0
- {termrender-0.8.0 → termrender-1.0.0}/PKG-INFO +5 -5
- {termrender-0.8.0 → termrender-1.0.0}/README.md +4 -4
- termrender-1.0.0/src/termrender/CLAUDE.md +15 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/__main__.py +15 -5
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/emit.py +13 -1
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/layout.py +8 -2
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/parser.py +13 -35
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/text.py +1 -1
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/timeline.py +7 -9
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/style.py +9 -2
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_column_alignment.py +8 -8
- termrender-1.0.0/tests/test_linebreak.py +79 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_myst_gaps.py +25 -38
- termrender-0.8.0/CLAUDE.md +0 -64
- termrender-0.8.0/src/termrender/CLAUDE.md +0 -75
- {termrender-0.8.0 → termrender-1.0.0}/.github/workflows/publish.yml +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/.gitignore +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/LICENSE +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/design.json +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/pyproject.toml +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/requirements.json +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/__init__.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/blocks.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/py.typed +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/CLAUDE.md +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/__init__.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/borders.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/charts.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/code.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/columns.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/diff.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/divider.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/mermaid.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/panel.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/quote.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/stat.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/table.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/src/termrender/renderers/tree.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/__init__.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_charts.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_diff.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_inline_badge.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_mermaid_compat.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_stat.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_tasklist.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_timeline.py +0 -0
- {termrender-0.8.0 → termrender-1.0.0}/tests/test_variable_colons.py +0 -0
termrender-1.0.0/.git
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitdir: /Users/silasrhyneer/Code/cli/termrender/.git/worktrees/tr-v100
|
|
@@ -1,12 +1,73 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.0.0 (2026-04-27)
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
|
|
8
|
+
- **claude-md**: Tighten root and src CLAUDE.md
|
|
9
|
+
([`7fe01ec`](https://github.com/crouton-labs/termrender/commit/7fe01ec2da0c7a0c1aa0f4eccf9b958496562be8))
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **mermaid**: Switch to :::mermaid directive, drop backtick fence forms
|
|
14
|
+
([`083f590`](https://github.com/crouton-labs/termrender/commit/083f5900b4b516f9df599dd08129afee34310e3d))
|
|
15
|
+
|
|
16
|
+
BREAKING CHANGE: ```mermaid fenced code blocks are no longer rendered as mermaid diagrams — they now
|
|
17
|
+
render as plain code blocks. Mermaid diagrams must use the new :::mermaid directive. The
|
|
18
|
+
MyST-style ```{name} backtick directive form is also removed; backtick fences now always produce a
|
|
19
|
+
code block, regardless of the language tag. Every directive uses ::: exclusively.
|
|
20
|
+
|
|
21
|
+
### Breaking Changes
|
|
22
|
+
|
|
23
|
+
- **mermaid**: ```mermaid fenced code blocks are no longer rendered as mermaid diagrams — they now
|
|
24
|
+
render as plain code blocks. Mermaid diagrams must use the new :::mermaid directive. The
|
|
25
|
+
MyST-style ```{name} backtick directive form is also removed; backtick fences now always produce a
|
|
26
|
+
code block, regardless of the language tag. Every directive uses ::: exclusively.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## v0.9.1 (2026-04-25)
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
- **timeline**: Wrap event text instead of truncating with ellipsis
|
|
34
|
+
([`5af0e04`](https://github.com/crouton-labs/termrender/commit/5af0e04c3256075a2016e2cf8ae3d44e9e78c8fc))
|
|
35
|
+
|
|
36
|
+
Long event entries previously got clipped with `…` when they exceeded event_w. Now they wrap across
|
|
37
|
+
multiple lines, with continuation lines indented under the bullet and prefixed by the accent bar.
|
|
38
|
+
Layout height sums per-entry wrapped line counts so the block reserves the right space.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## v0.9.0 (2026-04-21)
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
- **wrap**: Honor hard line breaks in wrap_text
|
|
46
|
+
([`a383f4d`](https://github.com/crouton-labs/termrender/commit/a383f4de66b3d8370bda500dd4c3771a591563aa))
|
|
47
|
+
|
|
48
|
+
Markdown hard breaks were parsed as \n spans but wrap_text only split on spaces, leaking raw \n into
|
|
49
|
+
wrapped output. Inside panels and columns this broke border alignment because visual_ljust padded
|
|
50
|
+
the string once, not per visual line.
|
|
51
|
+
|
|
52
|
+
wrap_text now recursively wraps each \n-separated segment; the text-renderer offset heuristic skips
|
|
53
|
+
\n as well as space between lines. Layout height calcs pick up the extra lines automatically.
|
|
54
|
+
|
|
55
|
+
### Features
|
|
56
|
+
|
|
57
|
+
- **spacing**: Add blank lines between hard breaks and top-level blocks
|
|
58
|
+
([`7610189`](https://github.com/crouton-labs/termrender/commit/761018928504cf9626678fe46b5ee66d5e899d5d))
|
|
59
|
+
|
|
60
|
+
Hard line breaks now render a blank line between the two sides (parser emits \n\n so wrap_text
|
|
61
|
+
naturally produces the gap), and DOCUMENT-level siblings are separated by a blank padded line so
|
|
62
|
+
paragraphs, headings, and blocks no longer visually run together.
|
|
63
|
+
|
|
64
|
+
|
|
4
65
|
## v0.8.0 (2026-04-18)
|
|
5
66
|
|
|
6
67
|
### Features
|
|
7
68
|
|
|
8
69
|
- **mermaid**: Preprocess sequence diagrams for mermaid-ascii compatibility
|
|
9
|
-
([`a642576`](https://github.com/
|
|
70
|
+
([`a642576`](https://github.com/crouton-labs/termrender/commit/a642576d41d5dbde372d7de2ab47745296a78e32))
|
|
10
71
|
|
|
11
72
|
mermaid-ascii only parses ->> / -->> arrows, participants, and self-loops; every other common
|
|
12
73
|
sequence-diagram construct made it fail and fall back to raw source. Rewrite Note lines into
|
|
@@ -20,13 +81,13 @@ mermaid-ascii only parses ->> / -->> arrows, participants, and self-loops; every
|
|
|
20
81
|
### Bug Fixes
|
|
21
82
|
|
|
22
83
|
- **code**: Wrap long code lines to fit layout width
|
|
23
|
-
([`31c6e59`](https://github.com/
|
|
84
|
+
([`31c6e59`](https://github.com/crouton-labs/termrender/commit/31c6e595a438c4ced8c61fff679b59d4ae55f938))
|
|
24
85
|
|
|
25
86
|
Code blocks previously used raw line count for height and let render_box grow beyond the layout
|
|
26
87
|
allocation. Now wraps source lines to the available content width in both layout and renderer.
|
|
27
88
|
|
|
28
89
|
- **parser**: Add directive trace and file-absolute line numbers to error messages
|
|
29
|
-
([`0f99ea0`](https://github.com/
|
|
90
|
+
([`0f99ea0`](https://github.com/crouton-labs/termrender/commit/0f99ea0310116f8fa06e933cd26126246d7a3b43))
|
|
30
91
|
|
|
31
92
|
Stray-closer and unclosed-directive errors now print the full open/close trace and, when nested
|
|
32
93
|
directives share a colon count, name the specific cause and suggest the fix. Recursive body
|
|
@@ -39,7 +100,7 @@ Stray-closer and unclosed-directive errors now print the full open/close trace a
|
|
|
39
100
|
### Bug Fixes
|
|
40
101
|
|
|
41
102
|
- **cli**: Default --tmux pane to 1/3 window width
|
|
42
|
-
([`d9c1bcc`](https://github.com/
|
|
103
|
+
([`d9c1bcc`](https://github.com/crouton-labs/termrender/commit/d9c1bccbe95a4e5cf1f975b82cbafde6d9d3807a))
|
|
43
104
|
|
|
44
105
|
Instead of preview-rendering at 80 cols to measure content width, default to (window_width - 2) // 3
|
|
45
106
|
for a consistent 1/3 split.
|
|
@@ -50,7 +111,7 @@ Instead of preview-rendering at 80 cols to measure content width, default to (wi
|
|
|
50
111
|
### Bug Fixes
|
|
51
112
|
|
|
52
113
|
- **cli**: Give --pane error paths actionable recovery guidance
|
|
53
|
-
([`f857c32`](https://github.com/
|
|
114
|
+
([`f857c32`](https://github.com/crouton-labs/termrender/commit/f857c32c89afe32a3a668f03a3d570b0f14dae97))
|
|
54
115
|
|
|
55
116
|
The two --pane error paths now tell the agent how to recover instead of restating the problem.
|
|
56
117
|
"Check that the pane id is valid" is a dead end for an agent — it needs either a command to list
|
|
@@ -62,7 +123,7 @@ The two --pane error paths now tell the agent how to recover instead of restatin
|
|
|
62
123
|
### Features
|
|
63
124
|
|
|
64
125
|
- **cli**: Add --pane for in-place tmux pane updates
|
|
65
|
-
([`4ab1d77`](https://github.com/
|
|
126
|
+
([`4ab1d77`](https://github.com/crouton-labs/termrender/commit/4ab1d77b996aa356926407dcc11c1b408e68e0ee))
|
|
66
127
|
|
|
67
128
|
--tmux now prints the newly-created pane id to stdout (via split-window -P -F) so callers can
|
|
68
129
|
capture it for subsequent updates. --pane <ID> targets an existing pane via tmux respawn-pane -k
|
|
@@ -82,7 +143,7 @@ Also in this commit: - Expand -h epilog to cover the 8 visualization directives
|
|
|
82
143
|
### Bug Fixes
|
|
83
144
|
|
|
84
145
|
- **borders**: Grow render_box to fit overflowing content and titles
|
|
85
|
-
([`dc108c8`](https://github.com/
|
|
146
|
+
([`dc108c8`](https://github.com/crouton-labs/termrender/commit/dc108c8242763828245569f719abce64b26ddf5b))
|
|
86
147
|
|
|
87
148
|
mermaid-ascii's --maxWidth is non-strict, so a child mermaid block can return lines wider than the
|
|
88
149
|
panel's allocated content area. Previously the side walls floated outward to accommodate the
|
|
@@ -99,7 +160,7 @@ render_box now measures the widest content line (and the title) and grows its ef
|
|
|
99
160
|
### Features
|
|
100
161
|
|
|
101
162
|
- Add diff, charts, stat, timeline, tasklist, and inline badges
|
|
102
|
-
([`e14f615`](https://github.com/
|
|
163
|
+
([`e14f615`](https://github.com/crouton-labs/termrender/commit/e14f615ae8d0723405db61c79b0f858d7bf0f863))
|
|
103
164
|
|
|
104
165
|
New block-level directives: - :::diff — colored unified diff with +/- gutters - :::bar — multi-bar
|
|
105
166
|
chart with sub-cell precision via eighth blocks - :::progress — single-line progress bar (auto
|
|
@@ -122,7 +183,7 @@ Cross-cutting changes: - InlineSpan gained fg/bg fields; render_spans and span-s
|
|
|
122
183
|
63 new tests across six test files. All 94 tests pass.
|
|
123
184
|
|
|
124
185
|
- **cli**: Add --watch mode for live re-rendering
|
|
125
|
-
([`4223ad8`](https://github.com/
|
|
186
|
+
([`4223ad8`](https://github.com/crouton-labs/termrender/commit/4223ad86805b0b3ad45450bd7ca4441a668f0e23))
|
|
126
187
|
|
|
127
188
|
Re-renders the file whenever its mtime changes, with terminal-resize detection and inline error
|
|
128
189
|
display so the watcher survives malformed input. Uses the alternate screen buffer so Ctrl+C
|
|
@@ -134,7 +195,7 @@ Composes with --tmux: --tmux --watch points the spawned pane at the real file pa
|
|
|
134
195
|
### Refactoring
|
|
135
196
|
|
|
136
197
|
- **parser**: Require strictly more colons on outer fences
|
|
137
|
-
([`4a501d9`](https://github.com/
|
|
198
|
+
([`4a501d9`](https://github.com/crouton-labs/termrender/commit/4a501d917db191f758874bb6c3d922c879a763be))
|
|
138
199
|
|
|
139
200
|
Drops the depth-counter that allowed `:::outer ... :::inner ... ::: ... :::` nesting with same colon
|
|
140
201
|
counts. Termrender now matches the standard followed by MyST, Pandoc fenced divs,
|
|
@@ -154,7 +215,7 @@ Fixtures in test_column_alignment.py rewritten to ascending colon counts (7/6/5/
|
|
|
154
215
|
### Features
|
|
155
216
|
|
|
156
217
|
- **table**: Render horizontal separator lines between data rows
|
|
157
|
-
([`3e4c74a`](https://github.com/
|
|
218
|
+
([`3e4c74a`](https://github.com/crouton-labs/termrender/commit/3e4c74a10d63470f2eb2ec096bb47cf41f0b7f70))
|
|
158
219
|
|
|
159
220
|
|
|
160
221
|
## v0.4.0 (2026-04-05)
|
|
@@ -162,7 +223,7 @@ Fixtures in test_column_alignment.py rewritten to ascending colon counts (7/6/5/
|
|
|
162
223
|
### Features
|
|
163
224
|
|
|
164
225
|
- **parser**: Variable colon counts, backtick fence directives, and gloam-inspired theming
|
|
165
|
-
([`47fac7f`](https://github.com/
|
|
226
|
+
([`47fac7f`](https://github.com/crouton-labs/termrender/commit/47fac7fcf13d33e5d9986d3f9ca42ddaf5e7207d))
|
|
166
227
|
|
|
167
228
|
Parser changes: - Support 3+ colon openers/closers with stack-based matching - Backtick fence
|
|
168
229
|
directive syntax (```{name}) via mistune AST interception - Option line stripping (:key: value)
|
|
@@ -184,18 +245,18 @@ Theming (gloam-inspired defaults): - Headings: depth-based colored fg + dim tint
|
|
|
184
245
|
### Documentation
|
|
185
246
|
|
|
186
247
|
- Update CLAUDE.md notes for mermaid, tmux, and layout
|
|
187
|
-
([`9e104d5`](https://github.com/
|
|
248
|
+
([`9e104d5`](https://github.com/crouton-labs/termrender/commit/9e104d5ee7bad9a57902e79586c02b0e8d80c589))
|
|
188
249
|
|
|
189
250
|
### Features
|
|
190
251
|
|
|
191
252
|
- **cli**: Auto-size tmux pane to fit rendered content
|
|
192
|
-
([`91f0414`](https://github.com/
|
|
253
|
+
([`91f0414`](https://github.com/crouton-labs/termrender/commit/91f0414d0bf8bfbe4d7167159b928ed9c736db74))
|
|
193
254
|
|
|
194
255
|
- **mermaid**: Pass width and vertical padding to mermaid-ascii
|
|
195
|
-
([`96145c2`](https://github.com/
|
|
256
|
+
([`96145c2`](https://github.com/crouton-labs/termrender/commit/96145c2789a52a4d94e9bc5f4adf7f3a88d8501f))
|
|
196
257
|
|
|
197
258
|
- **table**: Auto-wrap cell content when columns overflow
|
|
198
|
-
([`0fae56f`](https://github.com/
|
|
259
|
+
([`0fae56f`](https://github.com/crouton-labs/termrender/commit/0fae56f8f00260c3263671df9a63a5bea17820bb))
|
|
199
260
|
|
|
200
261
|
When a table exceeds available width, cells now wrap text within their proportionally-shrunk column
|
|
201
262
|
widths instead of overflowing. Layout height calculation updated to account for multi-line cells.
|
|
@@ -206,7 +267,7 @@ When a table exceeds available width, cells now wrap text within their proportio
|
|
|
206
267
|
### Bug Fixes
|
|
207
268
|
|
|
208
269
|
- **mermaid**: Undo double-encoded UTF-8 from mermaid-ascii output
|
|
209
|
-
([`9e0560c`](https://github.com/
|
|
270
|
+
([`9e0560c`](https://github.com/crouton-labs/termrender/commit/9e0560ce46b6dc3f90d2d716a97780713e5e5e53))
|
|
210
271
|
|
|
211
272
|
mermaid-ascii misinterprets UTF-8 bytes as Latin-1 and re-encodes, corrupting multi-byte characters
|
|
212
273
|
(e.g. → renders as â<U+0086><U+0092>). Apply latin-1 round-trip to recover original UTF-8 in both
|
|
@@ -215,7 +276,7 @@ mermaid-ascii misinterprets UTF-8 bytes as Latin-1 and re-encodes, corrupting mu
|
|
|
215
276
|
### Documentation
|
|
216
277
|
|
|
217
278
|
- Add tmux pane lifecycle and --check interaction notes to CLAUDE.md
|
|
218
|
-
([`9400092`](https://github.com/
|
|
279
|
+
([`9400092`](https://github.com/crouton-labs/termrender/commit/9400092e507d470acb97ac5a17b66fcf0e9aa2f6))
|
|
219
280
|
|
|
220
281
|
|
|
221
282
|
## v0.2.0 (2026-04-05)
|
|
@@ -223,27 +284,27 @@ mermaid-ascii misinterprets UTF-8 bytes as Latin-1 and re-encodes, corrupting mu
|
|
|
223
284
|
### Bug Fixes
|
|
224
285
|
|
|
225
286
|
- Handle zero-width and emoji presentation chars in visual width calculation
|
|
226
|
-
([`d0bb8dc`](https://github.com/
|
|
287
|
+
([`d0bb8dc`](https://github.com/crouton-labs/termrender/commit/d0bb8dcfa5ca0d2c16d78a1d7f81825231b9cb59))
|
|
227
288
|
|
|
228
289
|
_char_width now returns 0 for combining marks and format characters (ZWJ, variation selectors).
|
|
229
290
|
visual_len handles VS16 emoji presentation sequences by promoting the preceding character to width
|
|
230
291
|
2. Fixes panel border misalignment when content contains emoji or special Unicode.
|
|
231
292
|
|
|
232
293
|
- **docs**: Update README output examples to match actual rendered output
|
|
233
|
-
([`de6d0cc`](https://github.com/
|
|
294
|
+
([`de6d0cc`](https://github.com/crouton-labs/termrender/commit/de6d0ccfd8a60aca20f2b2659a313f8d8c87d853))
|
|
234
295
|
|
|
235
296
|
### Chores
|
|
236
297
|
|
|
237
298
|
- Add README, design specs, and project CLAUDE.md files
|
|
238
|
-
([`93ac358`](https://github.com/
|
|
299
|
+
([`93ac358`](https://github.com/crouton-labs/termrender/commit/93ac35857981c549797a9359573cacea1478b3ad))
|
|
239
300
|
|
|
240
301
|
- Derive version from git tags via hatch-vcs
|
|
241
|
-
([`33595a0`](https://github.com/
|
|
302
|
+
([`33595a0`](https://github.com/crouton-labs/termrender/commit/33595a0b64363e445b90c9df135a50a4652e2bae))
|
|
242
303
|
|
|
243
304
|
### Continuous Integration
|
|
244
305
|
|
|
245
306
|
- Auto-release and publish via conventional commits
|
|
246
|
-
([`80a456b`](https://github.com/
|
|
307
|
+
([`80a456b`](https://github.com/crouton-labs/termrender/commit/80a456b7301c57f2fd2b0cd30622b78f2d4b931e))
|
|
247
308
|
|
|
248
309
|
Replace manual GitHub release trigger with python-semantic-release. On push to main, conventional
|
|
249
310
|
commits are analyzed to determine version bumps (feat→minor, fix→patch) and publish to PyPI
|
|
@@ -252,12 +313,12 @@ Replace manual GitHub release trigger with python-semantic-release. On push to m
|
|
|
252
313
|
### Documentation
|
|
253
314
|
|
|
254
315
|
- Update README token count and expand CLAUDE.md implementation notes
|
|
255
|
-
([`1f70a53`](https://github.com/
|
|
316
|
+
([`1f70a53`](https://github.com/crouton-labs/termrender/commit/1f70a5352cbced30219012dbede7040c6ac97457))
|
|
256
317
|
|
|
257
318
|
### Features
|
|
258
319
|
|
|
259
320
|
- Add CJK ambiguous-width support, strict directive parsing, and rendering fixes
|
|
260
|
-
([`c000883`](https://github.com/
|
|
321
|
+
([`c000883`](https://github.com/crouton-labs/termrender/commit/c0008835d66b721b0a09c7a34dde11d08b3d3d94))
|
|
261
322
|
|
|
262
323
|
- Add emoji presentation and East Asian ambiguous-width character handling with --cjk flag and
|
|
263
324
|
TERMRENDER_CJK env var - All renderers (borders, divider, quote, tree) now compute box-drawing
|
|
@@ -266,14 +327,14 @@ Replace manual GitHub release trigger with python-semantic-release. On push to m
|
|
|
266
327
|
for inter-column gaps - Support 'author' as alias for 'by' attribute on quote blocks
|
|
267
328
|
|
|
268
329
|
- Add GFM table rendering with box-drawing borders
|
|
269
|
-
([`c3b61cd`](https://github.com/
|
|
330
|
+
([`c3b61cd`](https://github.com/crouton-labs/termrender/commit/c3b61cdd659fdb782089cbca2fd3f74b18486605))
|
|
270
331
|
|
|
271
332
|
Enable mistune table plugin, parse table AST into TABLE blocks, and render with box-drawing
|
|
272
333
|
characters. Supports left/center/right column alignment, bold headers, auto-sized columns, and
|
|
273
334
|
proportional overflow distribution.
|
|
274
335
|
|
|
275
336
|
- **cli**: Add --tmux pane output, --check validation, and structured error handling
|
|
276
|
-
([`36b52ee`](https://github.com/
|
|
337
|
+
([`36b52ee`](https://github.com/crouton-labs/termrender/commit/36b52eed9701cd0acd363db2d0fa3d277244c8b0))
|
|
277
338
|
|
|
278
339
|
- --tmux renders in a new tmux side pane via split-window, piped through less -R - --check validates
|
|
279
340
|
directive syntax without rendering (exit 0/2) - Structured _error() helper with fix/hint guidance
|
|
@@ -283,12 +344,12 @@ Enable mistune table plugin, parse table AST into TABLE blocks, and render with
|
|
|
283
344
|
depth note
|
|
284
345
|
|
|
285
346
|
- **cli**: Improve help output with examples, version flag, and tty detection
|
|
286
|
-
([`cb3e7e2`](https://github.com/
|
|
347
|
+
([`cb3e7e2`](https://github.com/crouton-labs/termrender/commit/cb3e7e2752ae860e5c3cbd4c4f1627e925a9c431))
|
|
287
348
|
|
|
288
349
|
### Testing
|
|
289
350
|
|
|
290
351
|
- Add column alignment and visual width tests
|
|
291
|
-
([`f8b6099`](https://github.com/
|
|
352
|
+
([`f8b6099`](https://github.com/crouton-labs/termrender/commit/f8b60998625977b10dd4697f8e772d80125cb9ce))
|
|
292
353
|
|
|
293
354
|
Covers showpiece rendering, column line width consistency, status marker visual widths (text vs
|
|
294
355
|
emoji presentation), and panel border alignment.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# termrender
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
```bash
|
|
5
|
+
pip install -e .
|
|
6
|
+
pytest tests/
|
|
7
|
+
python -m termrender <file.md>
|
|
8
|
+
python -m build
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No linter or formatter is configured.
|
|
12
|
+
|
|
13
|
+
## Constraints
|
|
14
|
+
- **Layout pass order is load-bearing**: `resolve_width()` top-down must complete before `resolve_height()` bottom-up — height calls `wrap_text(text, width)`, which requires width already set.
|
|
15
|
+
- **`borders.py` `render_box` width**: takes **total** width including borders, not content width. Passing content width silently overflows.
|
|
16
|
+
- **`wrap_text()` CJK bug**: uses `len()` internally, not `visual_len()` — silently overflows for CJK content.
|
|
17
|
+
- **`_ambiguous_width` is global mutable state** with no reset path — `set_ambiguous_width()` or `TERMRENDER_CJK` env var changes persist for the process lifetime.
|
|
18
|
+
- **Version**: derived from git tags via hatch-vcs — no version in `pyproject.toml`. Adding one will conflict.
|
|
19
|
+
- **Commits**: conventional commits. `feat` → minor, `fix`/`perf` → patch. Auto-released via python-semantic-release on main.
|
|
20
|
+
|
|
21
|
+
## Supplementary CLAUDE.md files
|
|
22
|
+
- `src/termrender/CLAUDE.md` — parser, layout, mermaid, nesting, and `--check`/`--tmux` implementation gotchas
|
|
23
|
+
- `src/termrender/renderers/CLAUDE.md` — renderer contracts, `render_box` width semantics, EAW edge cases
|
|
24
|
+
|
|
25
|
+
Read these before modifying layout, parsing, or renderer code.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termrender
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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
|
|
@@ -393,17 +393,17 @@ Horizontal rules with optional centered labels.
|
|
|
393
393
|
|
|
394
394
|
### Mermaid diagrams
|
|
395
395
|
|
|
396
|
-
Renders mermaid flowcharts as ASCII art via [mermaid-ascii](https://github.com/mermaid-js/mermaid-ascii).
|
|
396
|
+
Renders mermaid flowcharts as ASCII art via [mermaid-ascii](https://github.com/mermaid-js/mermaid-ascii). Mermaid uses the `:::mermaid` directive — backtick fences (`` ```mermaid ``) are treated as plain code blocks.
|
|
397
397
|
|
|
398
398
|
**Input:**
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
```markdown
|
|
400
|
+
:::mermaid
|
|
401
401
|
graph LR
|
|
402
402
|
A[Request] --> B{Auth?}
|
|
403
403
|
B -->|Yes| C[Handler]
|
|
404
404
|
B -->|No| D[401]
|
|
405
|
+
:::
|
|
405
406
|
```
|
|
406
|
-
````
|
|
407
407
|
|
|
408
408
|
The diagram gets rendered as ASCII art inline in the terminal output. Requires `mermaid-ascii` to be installed (it's a dependency, so it should be).
|
|
409
409
|
|
|
@@ -365,17 +365,17 @@ Horizontal rules with optional centered labels.
|
|
|
365
365
|
|
|
366
366
|
### Mermaid diagrams
|
|
367
367
|
|
|
368
|
-
Renders mermaid flowcharts as ASCII art via [mermaid-ascii](https://github.com/mermaid-js/mermaid-ascii).
|
|
368
|
+
Renders mermaid flowcharts as ASCII art via [mermaid-ascii](https://github.com/mermaid-js/mermaid-ascii). Mermaid uses the `:::mermaid` directive — backtick fences (`` ```mermaid ``) are treated as plain code blocks.
|
|
369
369
|
|
|
370
370
|
**Input:**
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
```markdown
|
|
372
|
+
:::mermaid
|
|
373
373
|
graph LR
|
|
374
374
|
A[Request] --> B{Auth?}
|
|
375
375
|
B -->|Yes| C[Handler]
|
|
376
376
|
B -->|No| D[401]
|
|
377
|
+
:::
|
|
377
378
|
```
|
|
378
|
-
````
|
|
379
379
|
|
|
380
380
|
The diagram gets rendered as ASCII art inline in the terminal output. Requires `mermaid-ascii` to be installed (it's a dependency, so it should be).
|
|
381
381
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
- `layout.py` imports `fix_mermaid_encoding` and `preprocess_mermaid_for_ascii` from `renderers/mermaid.py` — the only reverse dependency from layout into renderers. Reorganizing `renderers/` must preserve these imports. The two mermaid subprocess call sites differ: layout uses `check=True` (non-zero exit raises → caught → raw-source fallback); the renderer omits `check` (non-zero exit silently reads `stdout`, which may be empty or partial).
|
|
2
|
+
|
|
3
|
+
- `--check` calls `parse()` and exits — it never runs `layout.py`. Layout-time failures (mermaid subprocess missing, column percent overflow, `resolve_width`/`resolve_height` exceptions) pass `--check` cleanly but crash at render time.
|
|
4
|
+
|
|
5
|
+
- Column widths: explicit percent/absolute allocations are subtracted first; remaining space is split among auto-width columns with `max(remaining, 0)`. Two columns each claiming 80% of a 100px terminal leaves auto-width columns with `width = 1` — no error, no proportional scaling.
|
|
6
|
+
|
|
7
|
+
- **QUOTE** height gets `+1` only when `author` or `by` attr is set. Using any other key (`attribution`, `source`) silently omits the extra line — the renderer's attribution line is clipped.
|
|
8
|
+
|
|
9
|
+
- **LIST_ITEM** layout wraps at `max(width - 2, 1)`, hardcoding a 2-column indent. If the renderer changes the indent width, layout height and actual render height diverge silently.
|
|
10
|
+
|
|
11
|
+
- `text.py:32–33`: after each wrapped line, the offset skips one character if the next plain-text character is a space (the space `wrap_text` consumed). If wrapping preserves trailing spaces, this skip shifts subsequent span styling by one character.
|
|
12
|
+
|
|
13
|
+
- `--tmux` exits `EXIT_OK` after the tmux pane command succeeds — the `--check` branch comes later and is never reached. `--check` is silently dropped when combined with `--tmux`.
|
|
14
|
+
|
|
15
|
+
- `_EMOJI_WIDE_RANGES` in `style.py`: `_char_width()` exits early when `cp < lo`, assuming ranges are in ascending codepoint order. Adding a range out of order silently misclassifies all codepoints whose `lo` is higher than the inserted range's `lo`.
|
|
@@ -53,7 +53,8 @@ directives (close each with a matching colon count):
|
|
|
53
53
|
:::tasklist Checkbox list — [x] checked, [ ] unchecked, [!] in-progress
|
|
54
54
|
Plain lists with at least one marker auto-promote; use the directive
|
|
55
55
|
to force unchecked styling on items without explicit markers.
|
|
56
|
-
|
|
56
|
+
:::mermaid Mermaid diagram (via mermaid-ascii)
|
|
57
|
+
body: standard mermaid source (graph TD, flowchart, sequenceDiagram, ...)
|
|
57
58
|
|
|
58
59
|
Inline:
|
|
59
60
|
:badge[text]{color=c} Inline pill badge
|
|
@@ -335,18 +336,27 @@ def main() -> None:
|
|
|
335
336
|
if args.width:
|
|
336
337
|
pane_width = args.width
|
|
337
338
|
else:
|
|
338
|
-
# Default
|
|
339
|
+
# Default sizing aims for a readable side pane (~80 cols) and
|
|
340
|
+
# only falls back to 1/3-of-window when the window is too narrow
|
|
341
|
+
# to give that much without crushing the source pane.
|
|
339
342
|
try:
|
|
340
343
|
result = subprocess.run(
|
|
341
344
|
["tmux", "display-message", "-p", "#{window_width}"],
|
|
342
345
|
capture_output=True, text=True, check=True,
|
|
343
346
|
)
|
|
344
347
|
window_width = int(result.stdout.strip())
|
|
345
|
-
|
|
348
|
+
# Prefer 80 cols if the source pane keeps at least 60.
|
|
349
|
+
# Otherwise split evenly. Floor of 40 keeps text legible.
|
|
350
|
+
if window_width >= 140:
|
|
351
|
+
pane_width = 80
|
|
352
|
+
elif window_width >= 100:
|
|
353
|
+
pane_width = window_width // 2
|
|
354
|
+
else:
|
|
355
|
+
pane_width = max(window_width - 2 - 50, 40)
|
|
346
356
|
except Exception:
|
|
347
|
-
pane_width =
|
|
357
|
+
pane_width = 80
|
|
348
358
|
|
|
349
|
-
pane_width = max(pane_width,
|
|
359
|
+
pane_width = max(pane_width, 40) # absolute minimum for readability
|
|
350
360
|
|
|
351
361
|
# Watch mode points the new pane at the user's real file so edits
|
|
352
362
|
# propagate; non-watch mode snapshots source into a tempfile.
|
|
@@ -7,12 +7,24 @@ from termrender.renderers import (
|
|
|
7
7
|
panel, columns, tree, code, text, divider, quote, mermaid, table,
|
|
8
8
|
diff, charts, stat, timeline,
|
|
9
9
|
)
|
|
10
|
+
from termrender.style import visual_ljust
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def emit_block(block: Block, color: bool) -> list[str]:
|
|
13
14
|
"""Render a single block and its children, returning output lines."""
|
|
14
15
|
match block.type:
|
|
15
|
-
case BlockType.DOCUMENT
|
|
16
|
+
case BlockType.DOCUMENT:
|
|
17
|
+
# Insert a blank padded line between top-level siblings so
|
|
18
|
+
# paragraphs, headings, and blocks don't visually run together.
|
|
19
|
+
lines: list[str] = []
|
|
20
|
+
sep = visual_ljust("", block.width or 0)
|
|
21
|
+
for i, child in enumerate(block.children):
|
|
22
|
+
if i > 0:
|
|
23
|
+
lines.append(sep)
|
|
24
|
+
lines.extend(emit_block(child, color))
|
|
25
|
+
return lines
|
|
26
|
+
|
|
27
|
+
case BlockType.COL:
|
|
16
28
|
lines: list[str] = []
|
|
17
29
|
for child in block.children:
|
|
18
30
|
lines.extend(emit_block(child, color))
|
|
@@ -192,9 +192,15 @@ def resolve_height(block: Block) -> None:
|
|
|
192
192
|
elif bt == BlockType.TIMELINE:
|
|
193
193
|
entries = block.attrs.get("entries", [])
|
|
194
194
|
title_h = 1 if block.attrs.get("title") else 0
|
|
195
|
-
# Each entry takes 1 line + 1 connector line between entries (none after last)
|
|
196
195
|
if entries:
|
|
197
|
-
|
|
196
|
+
date_w = max(visual_len(e["date"]) for e in entries)
|
|
197
|
+
event_w = max((block.width or 60) - date_w - 4, 5)
|
|
198
|
+
total = 0
|
|
199
|
+
for entry in entries:
|
|
200
|
+
wrapped = wrap_text(entry["event"], event_w) or [""]
|
|
201
|
+
total += len(wrapped)
|
|
202
|
+
total += len(entries) - 1 # connector between entries
|
|
203
|
+
block.height = title_h + total
|
|
198
204
|
else:
|
|
199
205
|
block.height = max(title_h, 1)
|
|
200
206
|
|
|
@@ -69,15 +69,13 @@ _DIRECTIVE_TO_BLOCK: dict[str, BlockType] = {
|
|
|
69
69
|
"gauge": BlockType.GAUGE,
|
|
70
70
|
"stat": BlockType.STAT,
|
|
71
71
|
"timeline": BlockType.TIMELINE,
|
|
72
|
+
"mermaid": BlockType.MERMAID,
|
|
72
73
|
"tasklist": BlockType.LIST, # alias: forces tasklist styling on the inner list
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
_SELF_CLOSING_DIRECTIVES = frozenset({"divider", "progress", "gauge"})
|
|
76
77
|
|
|
77
|
-
#
|
|
78
|
-
_BACKTICK_DIRECTIVE_RE = re.compile(r"^\{(\w[\w-]*)\}(.*)")
|
|
79
|
-
|
|
80
|
-
# MyST option line: :key: value — intentionally requires a value after the key
|
|
78
|
+
# Option line: :key: value — intentionally requires a value after the key
|
|
81
79
|
# (the \s+(.+) part). Flag-style options like :nosandbox: (no value) won't match
|
|
82
80
|
# and will be treated as body content.
|
|
83
81
|
_OPTION_LINE_RE = re.compile(r"^:(\w[\w-]*):\s+(.+)$")
|
|
@@ -134,7 +132,10 @@ def _convert_inline(nodes: list[dict]) -> list[InlineSpan]:
|
|
|
134
132
|
elif ntype == "softbreak":
|
|
135
133
|
spans.append(InlineSpan(text=" "))
|
|
136
134
|
elif ntype == "linebreak":
|
|
137
|
-
|
|
135
|
+
# Two \n so wrap_text emits a blank line between the two sides
|
|
136
|
+
# of the hard break, giving more vertical breathing room than a
|
|
137
|
+
# soft wrap. See tests/test_linebreak.py.
|
|
138
|
+
spans.append(InlineSpan(text="\n\n"))
|
|
138
139
|
else:
|
|
139
140
|
# Fallback: try raw text
|
|
140
141
|
if "raw" in node:
|
|
@@ -245,7 +246,7 @@ def _expand_inline_roles(spans: list[InlineSpan]) -> list[InlineSpan]:
|
|
|
245
246
|
|
|
246
247
|
|
|
247
248
|
def _strip_options(body: str) -> tuple[dict[str, str], str]:
|
|
248
|
-
"""Strip
|
|
249
|
+
"""Strip option lines from the start of a directive body.
|
|
249
250
|
|
|
250
251
|
Option lines have the form `:key: value` and appear at the start of the body.
|
|
251
252
|
Blank lines between option lines are allowed. Scanning stops at the first
|
|
@@ -302,33 +303,10 @@ def _convert_ast(nodes: list[dict], _depth: int = 0) -> list[Block]:
|
|
|
302
303
|
elif ntype == "block_code":
|
|
303
304
|
raw = node.get("raw", "")
|
|
304
305
|
info = node.get("attrs", {}).get("info", "")
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
arg_text = m_directive.group(2).strip()
|
|
310
|
-
if dir_name == "mermaid":
|
|
311
|
-
options, body = _strip_options(raw)
|
|
312
|
-
attrs = dict(options)
|
|
313
|
-
if arg_text:
|
|
314
|
-
attrs["argument"] = arg_text
|
|
315
|
-
attrs["source"] = body
|
|
316
|
-
blocks.append(Block(type=BlockType.MERMAID, attrs=attrs))
|
|
317
|
-
else:
|
|
318
|
-
attrs: dict[str, Any] = {}
|
|
319
|
-
if arg_text:
|
|
320
|
-
attrs["argument"] = arg_text
|
|
321
|
-
blocks.append(_directive_to_block(dir_name, attrs, raw, _depth=_depth))
|
|
322
|
-
elif info == "mermaid":
|
|
323
|
-
blocks.append(Block(
|
|
324
|
-
type=BlockType.MERMAID,
|
|
325
|
-
attrs={"source": raw},
|
|
326
|
-
))
|
|
327
|
-
else:
|
|
328
|
-
blocks.append(Block(
|
|
329
|
-
type=BlockType.CODE,
|
|
330
|
-
attrs={"lang": info, "source": raw},
|
|
331
|
-
))
|
|
306
|
+
blocks.append(Block(
|
|
307
|
+
type=BlockType.CODE,
|
|
308
|
+
attrs={"lang": info, "source": raw},
|
|
309
|
+
))
|
|
332
310
|
|
|
333
311
|
elif ntype == "list":
|
|
334
312
|
ordered = node.get("attrs", {}).get("ordered", False)
|
|
@@ -610,8 +588,8 @@ def _directive_to_block(name: str, attrs: dict[str, Any], body: str, _depth: int
|
|
|
610
588
|
|
|
611
589
|
block_type = _DIRECTIVE_TO_BLOCK.get(name, BlockType.PANEL)
|
|
612
590
|
|
|
613
|
-
# Tree, Code, Diff: store raw body, don't parse as markdown
|
|
614
|
-
if block_type in (BlockType.TREE, BlockType.CODE, BlockType.DIFF):
|
|
591
|
+
# Tree, Code, Diff, Mermaid: store raw body, don't parse as markdown
|
|
592
|
+
if block_type in (BlockType.TREE, BlockType.CODE, BlockType.DIFF, BlockType.MERMAID):
|
|
615
593
|
attrs["source"] = body
|
|
616
594
|
return Block(type=block_type, attrs=attrs)
|
|
617
595
|
|
|
@@ -29,7 +29,7 @@ def _render_wrapped_spans(
|
|
|
29
29
|
prefix = first_prefix if i == 0 else cont_prefix
|
|
30
30
|
lines.append(visual_ljust(prefix + styled, total_width))
|
|
31
31
|
char_offset += line_len
|
|
32
|
-
if char_offset < len(plain) and plain[char_offset]
|
|
32
|
+
if char_offset < len(plain) and plain[char_offset] in (" ", "\n"):
|
|
33
33
|
char_offset += 1
|
|
34
34
|
|
|
35
35
|
return lines
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from termrender.blocks import Block
|
|
6
|
-
from termrender.style import style, visual_len, visual_ljust
|
|
6
|
+
from termrender.style import style, visual_len, visual_ljust, wrap_text
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def render(block: Block, color: bool, render_child=None) -> list[str]:
|
|
@@ -28,18 +28,16 @@ def render(block: Block, color: bool, render_child=None) -> list[str]:
|
|
|
28
28
|
bar = style("│", color=accent, dim=True, enabled=color)
|
|
29
29
|
|
|
30
30
|
event_w = max(w - date_w - 4, 5) # date + space + bullet + space + event
|
|
31
|
+
cont_indent = " " * (date_w + 1)
|
|
31
32
|
|
|
32
33
|
for i, entry in enumerate(entries):
|
|
33
34
|
date_text = entry["date"].rjust(date_w)
|
|
34
35
|
date_styled = style(date_text, dim=True, enabled=color)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
lines.append(visual_ljust(line, w))
|
|
36
|
+
wrapped = wrap_text(entry["event"], event_w) or [""]
|
|
37
|
+
lines.append(visual_ljust(f"{date_styled} {bullet} {wrapped[0]}", w))
|
|
38
|
+
for cont in wrapped[1:]:
|
|
39
|
+
lines.append(visual_ljust(f"{cont_indent}{bar} {cont}", w))
|
|
40
40
|
if i < len(entries) - 1:
|
|
41
|
-
|
|
42
|
-
connector = f"{connector_indent}{bar}"
|
|
43
|
-
lines.append(visual_ljust(connector, w))
|
|
41
|
+
lines.append(visual_ljust(f"{cont_indent}{bar}", w))
|
|
44
42
|
|
|
45
43
|
return lines
|
|
@@ -219,10 +219,17 @@ def visual_center(s: str, width: int, fillchar: str = ' ') -> str:
|
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
def wrap_text(text: str, width: int) -> list[str]:
|
|
222
|
-
if not text
|
|
222
|
+
if not text:
|
|
223
|
+
return ['']
|
|
224
|
+
if "\n" in text:
|
|
225
|
+
result: list[str] = []
|
|
226
|
+
for seg in text.split("\n"):
|
|
227
|
+
result.extend(wrap_text(seg, width))
|
|
228
|
+
return result
|
|
229
|
+
if text.isspace():
|
|
223
230
|
return ['']
|
|
224
231
|
if width <= 0:
|
|
225
|
-
return [text]
|
|
232
|
+
return [text]
|
|
226
233
|
words = text.split(' ')
|
|
227
234
|
lines: list[str] = []
|
|
228
235
|
current = ''
|
|
@@ -85,24 +85,24 @@ c/
|
|
|
85
85
|
# the column allocation. The inner panel's side walls and corner glyphs must
|
|
86
86
|
# stay aligned even when content forces the panel to grow past its allotment.
|
|
87
87
|
NESTED_PANEL_OVERFLOW_INPUT = """\
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
:::::::panel{title="Outer" color="cyan"}
|
|
89
|
+
::::::columns
|
|
90
|
+
:::::col{width="58%"}
|
|
91
|
+
::::panel{title="Request Flow" color="blue"}
|
|
92
|
+
:::mermaid
|
|
93
93
|
graph TD
|
|
94
94
|
A[Edge gateway<br/>accepts request] --> B{Token valid?}
|
|
95
95
|
B -->|yes| C[Route via LB<br/>to backend pool]
|
|
96
96
|
B -->|no| D[Reject 401,<br/>write audit log]
|
|
97
97
|
C --> E[Service responds,<br/>metrics emitted]
|
|
98
|
-
```
|
|
99
98
|
:::
|
|
100
99
|
::::
|
|
101
|
-
|
|
100
|
+
:::::
|
|
101
|
+
:::::col{width="42%"}
|
|
102
102
|
right
|
|
103
|
-
::::
|
|
104
103
|
:::::
|
|
105
104
|
::::::
|
|
105
|
+
:::::::
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
108
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from termrender import render
|
|
4
|
+
from termrender.style import visual_len, wrap_text
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestWrapTextLinebreak(unittest.TestCase):
|
|
8
|
+
def test_single_newline_splits(self):
|
|
9
|
+
self.assertEqual(wrap_text("a\nb", 10), ["a", "b"])
|
|
10
|
+
|
|
11
|
+
def test_newline_with_wrap(self):
|
|
12
|
+
# Each segment wraps independently.
|
|
13
|
+
self.assertEqual(wrap_text("aa bb\ncc dd", 3), ["aa", "bb", "cc", "dd"])
|
|
14
|
+
|
|
15
|
+
def test_leading_newline(self):
|
|
16
|
+
self.assertEqual(wrap_text("\nfoo", 10), ["", "foo"])
|
|
17
|
+
|
|
18
|
+
def test_trailing_newline(self):
|
|
19
|
+
self.assertEqual(wrap_text("foo\n", 10), ["foo", ""])
|
|
20
|
+
|
|
21
|
+
def test_consecutive_newlines(self):
|
|
22
|
+
self.assertEqual(wrap_text("a\n\nb", 10), ["a", "", "b"])
|
|
23
|
+
|
|
24
|
+
def test_plain_wrap_unchanged(self):
|
|
25
|
+
self.assertEqual(wrap_text("hello world foo bar", 10),
|
|
26
|
+
["hello", "world foo", "bar"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestLinebreakRendering(unittest.TestCase):
|
|
30
|
+
def test_hard_break_in_paragraph_pads_both_lines(self):
|
|
31
|
+
# Two trailing spaces = markdown hard line break.
|
|
32
|
+
# Hard breaks now emit a blank line between the two sides for extra
|
|
33
|
+
# vertical breathing room, so expect 3 padded lines.
|
|
34
|
+
output = render("line one \nline two", width=40, color=False)
|
|
35
|
+
lines = output.split("\n")
|
|
36
|
+
if lines and lines[-1] == "":
|
|
37
|
+
lines = lines[:-1]
|
|
38
|
+
self.assertEqual(len(lines), 3)
|
|
39
|
+
self.assertEqual(lines[1].strip(), "")
|
|
40
|
+
for line in lines:
|
|
41
|
+
self.assertEqual(visual_len(line), 40)
|
|
42
|
+
|
|
43
|
+
def test_hard_break_in_panel_aligns_borders(self):
|
|
44
|
+
src = ':::panel{title="t"}\nline one \nline two\n:::'
|
|
45
|
+
output = render(src, width=30, color=False)
|
|
46
|
+
lines = [ln for ln in output.split("\n") if ln]
|
|
47
|
+
# Panel: top border, line 1, blank, line 2, bottom border = 5 lines.
|
|
48
|
+
self.assertEqual(len(lines), 5)
|
|
49
|
+
widths = {visual_len(ln) for ln in lines}
|
|
50
|
+
self.assertEqual(widths, {30})
|
|
51
|
+
# Interior lines begin and end with the side border glyph.
|
|
52
|
+
for interior in lines[1:-1]:
|
|
53
|
+
self.assertTrue(interior.startswith("│"))
|
|
54
|
+
self.assertTrue(interior.rstrip().endswith("│"))
|
|
55
|
+
|
|
56
|
+
def test_hard_break_in_columns_preserves_row_width(self):
|
|
57
|
+
src = (
|
|
58
|
+
"::::columns\n:::col\nleft one \nleft two\n:::\n"
|
|
59
|
+
":::col\nright\n:::\n::::"
|
|
60
|
+
)
|
|
61
|
+
output = render(src, width=40, color=False)
|
|
62
|
+
lines = [ln for ln in output.split("\n") if ln]
|
|
63
|
+
# Left column: "left one", blank, "left two" = 3 rows.
|
|
64
|
+
self.assertEqual(len(lines), 3)
|
|
65
|
+
for line in lines:
|
|
66
|
+
self.assertEqual(visual_len(line), 40)
|
|
67
|
+
|
|
68
|
+
def test_hard_break_preserves_inline_style(self):
|
|
69
|
+
# Bold span followed by hard break and plain text — bold must not
|
|
70
|
+
# bleed onto line two (or the blank between).
|
|
71
|
+
output = render("**bold** \nplain", width=30, color=True)
|
|
72
|
+
lines = output.rstrip("\n").split("\n")
|
|
73
|
+
self.assertEqual(len(lines), 3)
|
|
74
|
+
self.assertIn("\x1b[1m", lines[0])
|
|
75
|
+
self.assertNotIn("\x1b[1m", lines[2])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
unittest.main()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for directive option lines and the colon-fence mermaid directive."""
|
|
2
2
|
|
|
3
3
|
import unittest
|
|
4
4
|
|
|
@@ -6,56 +6,43 @@ from termrender.blocks import BlockType
|
|
|
6
6
|
from termrender.parser import parse, _strip_options
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
10
|
-
"""
|
|
9
|
+
class TestMermaidDirective(unittest.TestCase):
|
|
10
|
+
""":::mermaid is the only fence form for mermaid diagrams."""
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
"""
|
|
14
|
-
doc = parse("
|
|
12
|
+
def test_basic_mermaid_directive(self):
|
|
13
|
+
""":::mermaid\ngraph LR\nA-->B\n::: → BlockType.MERMAID"""
|
|
14
|
+
doc = parse(":::mermaid\ngraph LR\nA-->B\n:::")
|
|
15
15
|
self.assertEqual(len(doc.children), 1)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"""```{panel}\n:title: Hello\ncontent\n``` → panel with title attr"""
|
|
20
|
-
doc = parse("```{panel}\n:title: Hello\ncontent\n```")
|
|
21
|
-
panel = doc.children[0]
|
|
22
|
-
self.assertEqual(panel.type, BlockType.PANEL)
|
|
23
|
-
self.assertEqual(panel.attrs["title"], "Hello")
|
|
16
|
+
block = doc.children[0]
|
|
17
|
+
self.assertEqual(block.type, BlockType.MERMAID)
|
|
18
|
+
self.assertIn("graph LR", block.attrs["source"])
|
|
24
19
|
|
|
25
|
-
def
|
|
26
|
-
"""
|
|
27
|
-
doc = parse("
|
|
28
|
-
self.assertEqual(len(doc.children), 1)
|
|
20
|
+
def test_mermaid_with_option_lines(self):
|
|
21
|
+
"""Option lines at the top of a mermaid body set attrs, not source."""
|
|
22
|
+
doc = parse(":::mermaid\n:title: Flow\ngraph LR\nA-->B\n:::")
|
|
29
23
|
block = doc.children[0]
|
|
30
24
|
self.assertEqual(block.type, BlockType.MERMAID)
|
|
25
|
+
self.assertEqual(block.attrs.get("title"), "Flow")
|
|
31
26
|
self.assertIn("graph LR", block.attrs["source"])
|
|
27
|
+
self.assertNotIn(":title:", block.attrs["source"])
|
|
32
28
|
|
|
33
|
-
def
|
|
34
|
-
"""```mermaid
|
|
29
|
+
def test_backtick_mermaid_is_plain_code_block(self):
|
|
30
|
+
"""```mermaid is no longer a mermaid block — it renders as a code block."""
|
|
35
31
|
doc = parse("```mermaid\ngraph LR\nA-->B\n```")
|
|
36
|
-
self.assertEqual(doc.children[0].type, BlockType.MERMAID)
|
|
37
|
-
|
|
38
|
-
def test_empty_body_backtick_directive(self):
|
|
39
|
-
"""```{panel}\n``` → panel with no children"""
|
|
40
|
-
doc = parse("```{panel}\n```")
|
|
41
|
-
panel = doc.children[0]
|
|
42
|
-
self.assertEqual(panel.type, BlockType.PANEL)
|
|
43
|
-
|
|
44
|
-
def test_backtick_fence_with_argument(self):
|
|
45
|
-
"""```{code-block} python\nprint("hi")\n``` → attrs["argument"] = "python" """
|
|
46
|
-
doc = parse('```{code-block} python\nprint("hi")\n```')
|
|
47
32
|
block = doc.children[0]
|
|
48
|
-
self.assertEqual(block.
|
|
33
|
+
self.assertEqual(block.type, BlockType.CODE)
|
|
34
|
+
self.assertEqual(block.attrs.get("lang"), "mermaid")
|
|
49
35
|
|
|
50
|
-
def
|
|
51
|
-
"""
|
|
52
|
-
doc = parse("
|
|
53
|
-
|
|
54
|
-
self.assertEqual(
|
|
36
|
+
def test_backtick_directive_is_plain_code_block(self):
|
|
37
|
+
"""```{panel} is no longer a directive — it renders as a code block."""
|
|
38
|
+
doc = parse("```{panel}\ncontent\n```")
|
|
39
|
+
block = doc.children[0]
|
|
40
|
+
self.assertEqual(block.type, BlockType.CODE)
|
|
41
|
+
self.assertEqual(block.attrs.get("lang"), "{panel}")
|
|
55
42
|
|
|
56
43
|
|
|
57
44
|
class TestDirectiveOptionLines(unittest.TestCase):
|
|
58
|
-
"""
|
|
45
|
+
"""Directive option lines (`:key: value` at top of body)."""
|
|
59
46
|
|
|
60
47
|
def test_colon_directive_with_options(self):
|
|
61
48
|
""":::panel\n:title: Hi\n:color: blue\ncontent\n::: → attrs have title and color"""
|
termrender-0.8.0/CLAUDE.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## What this is
|
|
6
|
-
|
|
7
|
-
termrender renders directive-flavored markdown to ANSI terminal output. LLM agents describe layout with `:::directives` (panels, columns, trees, callouts, etc.) and termrender produces styled terminal output. Public API: `from termrender import render`.
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# Install in dev mode
|
|
13
|
-
pip install -e .
|
|
14
|
-
|
|
15
|
-
# Run tests
|
|
16
|
-
pytest tests/
|
|
17
|
-
pytest tests/test_column_alignment.py::TestColumnAlignment::test_showpiece_renders_without_error
|
|
18
|
-
|
|
19
|
-
# Run the CLI
|
|
20
|
-
python -m termrender <file.md>
|
|
21
|
-
echo ':::panel{title="Hi"}\nHello\n:::' | python -m termrender
|
|
22
|
-
|
|
23
|
-
# Build
|
|
24
|
-
python -m build
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
No linter or formatter is configured.
|
|
28
|
-
|
|
29
|
-
## Architecture
|
|
30
|
-
|
|
31
|
-
Three-stage pipeline: **parse → layout → emit**.
|
|
32
|
-
|
|
33
|
-
1. **Parse** (`parser.py`) — Two-pass: regex extracts `:::directives` first, then mistune v3 processes markdown segments. Produces a tree of `Block` dataclasses (`blocks.py`).
|
|
34
|
-
2. **Layout** (`layout.py`) — Two-pass, order is load-bearing: `resolve_width()` top-down, then `resolve_height()` bottom-up. Width must resolve first because height calls `wrap_text(text, width)`.
|
|
35
|
-
3. **Emit** (`emit.py`) — Walks the block tree and dispatches to renderer functions in `renderers/`.
|
|
36
|
-
|
|
37
|
-
Entry points:
|
|
38
|
-
- **Library**: `__init__.py:render()` — parse → layout → emit
|
|
39
|
-
- **CLI**: `__main__.py:main()` — argparse with `--width`, `--no-color`, `--check`, `--cjk`, `--tmux`. Exit codes: 0=ok, 1=input, 2=syntax, 3=terminal.
|
|
40
|
-
|
|
41
|
-
### Renderers (`src/termrender/renderers/`)
|
|
42
|
-
|
|
43
|
-
Two signatures (not type-enforced):
|
|
44
|
-
- **Leaf**: `render(block, color) -> list[str]` — divider, tree
|
|
45
|
-
- **Container**: `render(block, color, render_child) -> list[str]` — panel, quote, code, columns, callout, table, text, mermaid
|
|
46
|
-
|
|
47
|
-
`borders.py` is a shared utility, not a renderer. Its `render_box(content_lines, width, ...)` takes **total** width (including borders), not content width.
|
|
48
|
-
|
|
49
|
-
### Style (`style.py`)
|
|
50
|
-
|
|
51
|
-
`visual_len()` measures display width accounting for ANSI escapes, emoji, CJK, and combining marks. `wrap_text()` uses `len()` internally (known bug: CJK overflow). `_ambiguous_width` is global mutable state with no reset path — set via `set_ambiguous_width()` or `TERMRENDER_CJK` env var.
|
|
52
|
-
|
|
53
|
-
## Conventions
|
|
54
|
-
|
|
55
|
-
- **Commits**: conventional commits (`feat:`, `fix:`, `chore:`, etc.). `feat` → minor, `fix`/`perf` → patch. Auto-released via python-semantic-release on main.
|
|
56
|
-
- **Version**: derived from git tags via hatch-vcs (no version in pyproject.toml).
|
|
57
|
-
- **Python**: 3.10+.
|
|
58
|
-
|
|
59
|
-
## Supplementary CLAUDE.md files
|
|
60
|
-
|
|
61
|
-
- `src/termrender/CLAUDE.md` — parser, layout, mermaid, nesting, and `--check`/`--tmux` implementation gotchas
|
|
62
|
-
- `src/termrender/renderers/CLAUDE.md` — renderer contracts, `render_box` width semantics, EAW edge cases
|
|
63
|
-
|
|
64
|
-
Read these before modifying layout, parsing, or renderer code.
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# termrender/src/termrender
|
|
2
|
-
|
|
3
|
-
## Layout: two-pass order is mandatory
|
|
4
|
-
|
|
5
|
-
`layout.py` runs `resolve_width()` then `resolve_height()` — this order is load-bearing. Height resolution calls `wrap_text(text, width)`, so every block must have `.width` set first. Reversing the order causes `block.width = None`, which silently falls back to `width = 1` (layout.py:77), drastically underestimating all heights.
|
|
6
|
-
|
|
7
|
-
## Mermaid: subprocess runs in layout, not in the renderer
|
|
8
|
-
|
|
9
|
-
`layout.py:119–134` runs `mermaid-ascii` and caches the result in `block.attrs["_rendered"]`. The `mermaid` renderer (renderers/mermaid.py) reads this cache key; if it's absent, it runs the subprocess again. A failed layout subprocess (tool missing, timeout) silently stores the raw source diagram in `_rendered`, so the renderer falls back to printing source — no error is raised. Both sites have a 30s timeout.
|
|
10
|
-
|
|
11
|
-
`layout.py` imports `fix_mermaid_encoding` and `preprocess_mermaid_for_ascii` from `renderers/mermaid.py` — the only reverse dependency from layout into renderers. Reorganizing `renderers/` must account for these imports. The two subprocess call sites differ: layout uses `check=True` (non-zero exit raises `CalledProcessError` → caught → raw source fallback); the renderer omits `check` (non-zero exit silently reads `stdout`, which may be empty or partial). See `renderers/CLAUDE.md` for encoding-fix details.
|
|
12
|
-
|
|
13
|
-
`preprocess_mermaid_for_ascii` rewrites sequence diagrams into the subset `mermaid-ascii` parses (it only supports `->>` / `-->>` arrows, `participant`, and self-loops). `Note over|left of|right of X[,Y]: msg` becomes a self-loop `X->>X: 📝 msg`; `->`, `-x`, `--x`, `-)`, `--)`, and bare `-->` are mapped to `->>`/`-->>`; block keywords (`loop`/`alt`/`opt`/`par`/`critical`/`break`/`rect`/`activate`/`deactivate`/`autonumber`/`else`/`and`/`end`) are dropped so the inner arrow lines still render; `<br/>` is flattened to ` / `. Non-sequence diagrams (`flowchart`, `graph`, etc.) pass through unchanged. Semantics are lossy by design — `-x` (fail arrow) renders as a plain arrow, and block scoping is lost — but the flow diagram renders instead of silently degrading to raw source.
|
|
14
|
-
|
|
15
|
-
## Directive nesting: outer must have more colons than inner
|
|
16
|
-
|
|
17
|
-
The parser handles two directive syntaxes through different passes: colon directives (`:::name`) are extracted in pass 1 via regex in `_split_directives`, while backtick fence directives (`` ```{name} ``) are resolved in pass 2 via mistune's AST walk in `_convert_ast`. Both paths funnel through `_directive_to_block` for block construction.
|
|
18
|
-
|
|
19
|
-
For colon directives, closers are paired strictly by colon count. A closer whose colon count differs from the open directive's colon count is treated as body content; the recursive `parse()` call inside `_directive_to_block` re-parses it as an inner directive. This matches the standard rule used by MyST, Pandoc fenced divs, markdown-it-container, and CommonMark fenced code blocks: outer fences must use strictly more colons than the inner fences they wrap, making opener/closer pairing unambiguous.
|
|
20
|
-
|
|
21
|
-
Max recursion depth is 50; exceeding it raises `ValueError`, not `DirectiveError`. Both the render path and `--check` path in `__main__.py` catch `ValueError` and map it to exit code 2 (`EXIT_SYNTAX`).
|
|
22
|
-
|
|
23
|
-
## `--check` validates parse only, not layout
|
|
24
|
-
|
|
25
|
-
`__main__.py` `--check` calls `parse()` directly and exits — it never runs `layout.py`. Layout-time failures (mermaid subprocess missing, column percent overflow, `resolve_width`/`resolve_height` exceptions) pass `--check` cleanly but crash at render time. Use `--check` to catch directive syntax errors, not to guarantee a successful render.
|
|
26
|
-
|
|
27
|
-
## `_ambiguous_width` is global mutable state; `TERMRENDER_CJK` makes it permanent
|
|
28
|
-
|
|
29
|
-
`style.set_ambiguous_width(n)` (style.py:21–23) changes East Asian ambiguous-width measurement for the entire process with no reset function. The CLI `--cjk` flag sets `os.environ["TERMRENDER_CJK"]` rather than calling the function directly, so it persists for the entire process. `__init__.py:30–31` calls `set_ambiguous_width(2)` on every `render()` call when the env var is set — but since there's no reset, a single call in any render context permanently widens ambiguous-width for all subsequent renders in the same process. Affects `visual_len()` and therefore all wrapping and column math.
|
|
30
|
-
|
|
31
|
-
## Column width: explicit widths exceeding available space truncate auto-columns to 1
|
|
32
|
-
|
|
33
|
-
`layout.py:30–63`: explicit column widths (percent or absolute) are allocated first; remaining space is split among auto-width columns with `max(remaining, 0)`. Two columns each claiming 80% of a 100px terminal leaves auto-width columns with width 1 — no error, no proportional scaling.
|
|
34
|
-
|
|
35
|
-
## Height calculations with hidden assumptions
|
|
36
|
-
|
|
37
|
-
- **QUOTE** (layout.py:138): height gets `+1` only when the `author` or `by` attr is set. Using any other key (`attribution`, `source`) silently omits the extra line — the renderer's attribution line is clipped.
|
|
38
|
-
- **TABLE** (layout.py:117): `height = len(rows) + 4` where `rows` includes the header row. The code comment mis-describes this as 5 structural parts; `rows[0]` is the header. Adding a footer or subtitle row needs `+5`, not `+4+1`.
|
|
39
|
-
- **LIST_ITEM** (layout.py:107): text wraps at `max(width - 2, 1)`, hardcoding a 2-column indent. If the renderer changes the indent width, layout height and actual render height diverge silently.
|
|
40
|
-
|
|
41
|
-
## Character offset tracking across wrapped lines
|
|
42
|
-
|
|
43
|
-
`text.py:32–33`: after rendering each wrapped line, the offset advances by the line's length and then skips one character if the next character in the original plain text is a space (the space `wrap_text` consumed). If wrapping preserves trailing spaces, this skip is wrong and subsequent span styling shifts by one character.
|
|
44
|
-
|
|
45
|
-
## `wrap_text` measures in characters, not visual columns
|
|
46
|
-
|
|
47
|
-
`style.py:195–241`: all line-length comparisons inside `wrap_text` use `len()` (character count), not `visual_len()`. A 2-column-wide CJK character is counted as 1 column-unit, so wrapped lines silently overflow their allocated width by one cell per wide character. This affects every block type that calls `wrap_text`.
|
|
48
|
-
|
|
49
|
-
## Unknown directive names silently become PANEL
|
|
50
|
-
|
|
51
|
-
`parser.py:339`: `_DIRECTIVE_TO_BLOCK.get(name, BlockType.PANEL)` — any unrecognized `:::name` becomes a bordered PANEL block with no error or warning. Typos in directive names (e.g. `:::callOut`) produce visible output that looks correct but lacks the expected behavior (callout type, icon, color).
|
|
52
|
-
|
|
53
|
-
## `--tmux`: exit code reflects pane creation, auto-sizing runs a full render, tempfiles leak
|
|
54
|
-
|
|
55
|
-
`__main__.py:357`: after `tmux split-window` succeeds the parent exits `EXIT_OK` immediately — the pane renders asynchronously. Render errors surface only inside the pane. `--check` is silently dropped with `--tmux` (exit at line 357 precedes the `--check` branch at line 368), even though `--tmux` now runs its own `parse()` call (lines 253–266) for fail-fast syntax validation before spawning the pane. The `--check` "ok" message and exit-code contract are not honoured.
|
|
56
|
-
|
|
57
|
-
Tempfile cleanup is embedded as `... | less -R; rm -f <tmpfile>` (line 339); `less` killed abnormally (SIGKILL, closed session) leaks `/tmp/termrender-*.md`. `--tmux --watch` skips tempfile creation entirely — the pane is pointed at the real file path, so no leak.
|
|
58
|
-
|
|
59
|
-
When `--width` is omitted, `--tmux` calls `render(source, width=80, color=False)` (lines 283–290) to measure content width — mermaid subprocesses fire and `TERMRENDER_CJK` mutations apply here. Any exception silently falls back to `pane_width=80`. The pane is capped to `tmux #{pane_width} - 10`; if the tmux query fails, no cap is applied. Minimum enforced pane width is 20.
|
|
60
|
-
|
|
61
|
-
## `--watch`: re-renders on resize as well as file change; errors are inline, not fatal
|
|
62
|
-
|
|
63
|
-
`_watch_loop` (lines 99–169) polls `os.path.getmtime` every 0.2 s and also re-renders when `shutil.get_terminal_size()` changes — so a terminal resize triggers a re-render even with no file edit. `width=None` is passed to `render()` each cycle, so auto-detection always uses current pane width.
|
|
64
|
-
|
|
65
|
-
The loop catches bare `Exception` (line 142) to keep the watcher alive across render errors; errors appear as a one-line message in the pane, not as an exit. `DirectiveError`, `TerminalError`, and `ValueError` (nesting depth) are each caught separately with distinct prefixes for that same reason — the watcher never exits on render failure, only on `KeyboardInterrupt`.
|
|
66
|
-
|
|
67
|
-
The alternate screen buffer (`\033[?1049h` / `\033[?1049l`) is entered on start and restored in a `finally` block, so Ctrl+C cleanly returns to the prior terminal state. `--watch` requires a FILE argument; stdin cannot be watched (polled by path, not fd).
|
|
68
|
-
|
|
69
|
-
## `TERMRENDER_COLOR=1` forces color when stdout is not a tty
|
|
70
|
-
|
|
71
|
-
`use_color` (lines 361–363 and 388–390) is `True` when stdout is a tty OR `TERMRENDER_COLOR == "1"`. The tmux pane command is prefixed with `TERMRENDER_COLOR=1` (lines 334, 337) so color survives the `| less -R` pipe. Setting this env var in scripts achieves the same effect — it overrides the tty check entirely.
|
|
72
|
-
|
|
73
|
-
## `_EMOJI_WIDE_RANGES` must stay sorted by codepoint
|
|
74
|
-
|
|
75
|
-
`_char_width()` (style.py:144) exits the range scan early on `cp < lo`, assuming all ranges are in ascending codepoint order. Adding a new range out of order causes the early exit to skip ranges with higher `lo` values, silently misclassifying those codepoints as 1-wide.
|
|
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
|