termrender 0.6.1__tar.gz → 0.7.1__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.
Files changed (45) hide show
  1. {termrender-0.6.1 → termrender-0.7.1}/CHANGELOG.md +32 -0
  2. {termrender-0.6.1 → termrender-0.7.1}/PKG-INFO +1 -1
  3. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/__main__.py +124 -36
  4. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/text.py +2 -2
  5. {termrender-0.6.1 → termrender-0.7.1}/tests/test_tasklist.py +3 -3
  6. {termrender-0.6.1 → termrender-0.7.1}/.github/workflows/publish.yml +0 -0
  7. {termrender-0.6.1 → termrender-0.7.1}/.gitignore +0 -0
  8. {termrender-0.6.1 → termrender-0.7.1}/CLAUDE.md +0 -0
  9. {termrender-0.6.1 → termrender-0.7.1}/LICENSE +0 -0
  10. {termrender-0.6.1 → termrender-0.7.1}/README.md +0 -0
  11. {termrender-0.6.1 → termrender-0.7.1}/design.json +0 -0
  12. {termrender-0.6.1 → termrender-0.7.1}/pyproject.toml +0 -0
  13. {termrender-0.6.1 → termrender-0.7.1}/requirements.json +0 -0
  14. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/CLAUDE.md +0 -0
  15. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/__init__.py +0 -0
  16. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/blocks.py +0 -0
  17. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/emit.py +0 -0
  18. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/layout.py +0 -0
  19. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/parser.py +0 -0
  20. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/py.typed +0 -0
  21. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/CLAUDE.md +0 -0
  22. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/__init__.py +0 -0
  23. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/borders.py +0 -0
  24. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/charts.py +0 -0
  25. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/code.py +0 -0
  26. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/columns.py +0 -0
  27. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/diff.py +0 -0
  28. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/divider.py +0 -0
  29. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/mermaid.py +0 -0
  30. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/panel.py +0 -0
  31. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/quote.py +0 -0
  32. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/stat.py +0 -0
  33. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/table.py +0 -0
  34. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/timeline.py +0 -0
  35. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/renderers/tree.py +0 -0
  36. {termrender-0.6.1 → termrender-0.7.1}/src/termrender/style.py +0 -0
  37. {termrender-0.6.1 → termrender-0.7.1}/tests/__init__.py +0 -0
  38. {termrender-0.6.1 → termrender-0.7.1}/tests/test_charts.py +0 -0
  39. {termrender-0.6.1 → termrender-0.7.1}/tests/test_column_alignment.py +0 -0
  40. {termrender-0.6.1 → termrender-0.7.1}/tests/test_diff.py +0 -0
  41. {termrender-0.6.1 → termrender-0.7.1}/tests/test_inline_badge.py +0 -0
  42. {termrender-0.6.1 → termrender-0.7.1}/tests/test_myst_gaps.py +0 -0
  43. {termrender-0.6.1 → termrender-0.7.1}/tests/test_stat.py +0 -0
  44. {termrender-0.6.1 → termrender-0.7.1}/tests/test_timeline.py +0 -0
  45. {termrender-0.6.1 → termrender-0.7.1}/tests/test_variable_colons.py +0 -0
@@ -1,6 +1,38 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.7.1 (2026-04-08)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **cli**: Give --pane error paths actionable recovery guidance
9
+ ([`f857c32`](https://github.com/CaptainCrouton89/termrender/commit/f857c32c89afe32a3a668f03a3d570b0f14dae97))
10
+
11
+ The two --pane error paths now tell the agent how to recover instead of restating the problem.
12
+ "Check that the pane id is valid" is a dead end for an agent — it needs either a command to list
13
+ valid pane ids (tmux list-panes) or a fallback (spawn a fresh pane via --tmux).
14
+
15
+
16
+ ## v0.7.0 (2026-04-08)
17
+
18
+ ### Features
19
+
20
+ - **cli**: Add --pane for in-place tmux pane updates
21
+ ([`4ab1d77`](https://github.com/CaptainCrouton89/termrender/commit/4ab1d77b996aa356926407dcc11c1b408e68e0ee))
22
+
23
+ --tmux now prints the newly-created pane id to stdout (via split-window -P -F) so callers can
24
+ capture it for subsequent updates. --pane <ID> targets an existing pane via tmux respawn-pane -k
25
+ instead of spawning a new one — the existing process is killed and replaced with the new render.
26
+ This lets agents synchronously re-render a doc on every edit without spawning fresh panes or
27
+ relying on --watch polling.
28
+
29
+ Also in this commit: - Expand -h epilog to cover the 8 visualization directives (stat, bar,
30
+ progress, gauge, diff, timeline, tasklist, inline badge) and rewrite the nesting note to describe
31
+ the strict colon-count rule. The previous epilog only documented the base directives and said
32
+ "every opener needs a matching :::", which contradicts the actual parser behavior. - Render
33
+ tasklist checkboxes as filled/empty dots (● / ○ / ◐) instead of boxed glyphs (☑ / ☐ / ◐).
34
+
35
+
4
36
  ## v0.6.1 (2026-04-08)
5
37
 
6
38
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: termrender
3
- Version: 0.6.1
3
+ Version: 0.7.1
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, no :::)
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
- Directives nest arbitrarily. Every opener needs a matching :::
42
- except :::divider which is self-closing.
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
- :::panel{title="Outer"}
45
- :::callout{type="info"}
46
- Nested content here.
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,53 @@ def main() -> None:
274
319
  fix="run inside tmux or omit --tmux")
275
320
 
276
321
  # Determine desired pane width
277
- if args.width:
278
- pane_width = args.width
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="the pane may have been closed. "
338
+ "List active panes with: tmux list-panes -F '#{pane_id}' "
339
+ "Or spawn a fresh one with: termrender --tmux <file>",
340
+ )
341
+ pane_width = max(pane_width, 20)
279
342
  else:
280
- # Preview render to measure content width
281
- from termrender.style import visual_len
343
+ if args.width:
344
+ pane_width = args.width
345
+ else:
346
+ # Preview render to measure content width
347
+ from termrender.style import visual_len
348
+ try:
349
+ preview = render(source, width=80, color=False)
350
+ max_w = max(
351
+ (visual_len(line) for line in preview.split('\n') if line),
352
+ default=40,
353
+ )
354
+ pane_width = max(max_w, 40)
355
+ except Exception:
356
+ pane_width = 80
357
+
358
+ # Cap to available tmux space (leave room for the source pane)
282
359
  try:
283
- preview = render(source, width=80, color=False)
284
- max_w = max(
285
- (visual_len(line) for line in preview.split('\n') if line),
286
- default=40,
360
+ result = subprocess.run(
361
+ ["tmux", "display-message", "-p", "#{pane_width}"],
362
+ capture_output=True, text=True, check=True,
287
363
  )
288
- pane_width = max(max_w, 40)
364
+ available = int(result.stdout.strip())
365
+ pane_width = min(pane_width, available - 10)
289
366
  except Exception:
290
- pane_width = 80
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
367
+ pass
368
+ pane_width = max(pane_width, 20) # absolute minimum
303
369
 
304
370
  # Watch mode points the new pane at the user's real file so edits
305
371
  # propagate; non-watch mode snapshots source into a tempfile.
@@ -340,10 +406,23 @@ def main() -> None:
340
406
  )
341
407
 
342
408
  try:
343
- subprocess.run(
344
- ["tmux", "split-window", "-h", "-f", "-l", str(pane_width), pane_cmd],
345
- check=True,
346
- )
409
+ if args.pane:
410
+ # respawn-pane -k kills the existing process in the target
411
+ # pane and runs the new command. The pane id stays the same.
412
+ subprocess.run(
413
+ ["tmux", "respawn-pane", "-k", "-t", args.pane, pane_cmd],
414
+ check=True,
415
+ )
416
+ pane_id = args.pane
417
+ else:
418
+ # -P -F prints the new pane's id to stdout so the caller
419
+ # can capture it for subsequent --pane updates.
420
+ result = subprocess.run(
421
+ ["tmux", "split-window", "-h", "-f", "-l", str(pane_width),
422
+ "-P", "-F", "#{pane_id}", pane_cmd],
423
+ check=True, capture_output=True, text=True,
424
+ )
425
+ pane_id = result.stdout.strip()
347
426
  except FileNotFoundError:
348
427
  if tmpfile:
349
428
  os.unlink(tmpfile)
@@ -351,9 +430,18 @@ def main() -> None:
351
430
  except subprocess.CalledProcessError:
352
431
  if tmpfile:
353
432
  os.unlink(tmpfile)
354
- _error("failed to create tmux pane",
355
- hint="check that tmux is running and has space for a new pane")
433
+ if args.pane:
434
+ _error(
435
+ f"failed to update tmux pane {args.pane}",
436
+ fix="the pane may have been closed. Spawn a fresh one with: "
437
+ "termrender --tmux <file>",
438
+ )
439
+ else:
440
+ _error("failed to create tmux pane",
441
+ hint="check that tmux is running and has space for a new pane")
356
442
 
443
+ # Echo the pane id so callers can chain --pane updates
444
+ print(pane_id)
357
445
  sys.exit(EXIT_OK)
358
446
 
359
447
  # --watch: live-render in the current terminal
@@ -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(" ", color="green", enabled=color)
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(" ", dim=True, enabled=color)
110
+ return style(" ", dim=True, enabled=color)
111
111
 
112
112
 
113
113
  def _render_list(block: Block, color: bool) -> list[str]:
@@ -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("", output) # checked
31
- self.assertIn("", output) # unchecked
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(""), 2)
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