feat: termynal output mode for the directive - #35
Conversation
Render a Typer/Click app's --help as an animated, colored termynal block instead of Markdown tables, via the :termynal: (and :width:) directive options or the global plugin config. The app is introspected in-process: each command's --help is captured with rich's terminal output forced, hidden commands skipped, ANSI converted to inline HTML with ansi2html, and wrapped in termynal's data-ty markup. No subprocess, deterministic, and it runs under both mkdocs and zensical since it is a markdown extension. termynal and ansi2html are an optional extra (mkdocs-typer2[termynal]); using :termynal: without it raises a clear install hint. The markup is emitted directly rather than importing termynal's renderer, and a contract test guards it against drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the ANSI color palette and dark/light variant for termynal mode, as per-block directive options (:scheme:, :dark_bg:) and global plugin config, mirroring :width:. scheme accepts ansi2html's eight palettes (ansi2html, dracula, mint-terminal, osx, osx-basic, osx-solid-colors, solarized, xterm) and falls back to xterm on an invalid value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drift-guard test called escape() as a method on the Termynal instance, which raises AttributeError on termynal>=0.12 where escape is a module-level function. All 7 parametrized cases failed, so the guard never actually ran. Import and use the module-level escape instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
termynal styles [data-termynal] with padding but no margin, and exposes no wrapper class or inter-block spacing hook. Normal termynal usage doesn't notice because each terminal is its own Markdown block; we emit several <div data-termynal> glued together in one HTML-stash blob, so the root command and per-subcommand blocks stacked flush. Add margin-top only to blocks after the first, so stacked blocks are separated without imposing a margin on the boundary between the first/last block and surrounding page content (that stays the theme's concern). A lone block gets no inline style at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_resolve_app in termynal_render duplicated the import + getattr-fallback + _resolve_click_command dance already in build_tree_from_click_app. Extract it as pretty.resolve_click_command and call it from both, so the two output modes can't drift in how they locate the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The top-level width/scheme/dark_bg plugin options were generic and only meaningful in termynal mode. Rename to termynal_width/termynal_scheme/ termynal_dark_bg so the global config reads unambiguously and the names won't collide with future options. Block-level :width:/:scheme:/:dark_bg: stay as-is since they're already scoped under the directive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tracked CLI (Termynal) page alongside the other mode pages, wire it into the nav, and enable the termynal MkDocs plugin so its CSS/JS animate the generated blocks. Builds clean under mkdocs --strict. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the width/scheme/dark_bg kwargs (previously re-listed across TyperExtension and TyperProcessor and threaded individually into the renderer) into a single TermynalOptions dataclass, defined in termynal_render.py alongside the option domains. Option values are now Literal-typed (AnsiScheme, ButtonStyle) with their tuples derived via get_args, and the data-ty-* attribute names live in module-level globals as the single source of truth. The directive parser is DRY'd behind _directive_value / _as_bool / _as_int helpers. No behavior change for the existing width/scheme/dark_bg options. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose buttons (macos|windows), prompt, and the type/line/start delay timings as both directive (:buttons:, :prompt:, :type_delay:, ...) and plugin (termynal_buttons, ...) options, mapped to the matching data-ty-* attributes. buttons falls back to macos on invalid input; timings emit attributes only when set, otherwise termynal's own defaults apply. Drift guards now assert our button attributes appear in termynal's CSS and our timing attributes in termynal.js, so a future termynal rename fails loudly against the BUTTONS / TIMING_ATTRS source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render only the root command's --help by default instead of always stacking root plus its direct subcommands. Add a `subcommands` recursion-depth option, available per block (`:subcommands:`) and globally (`termynal_subcommands`): 0 renders the root only (default), N renders N levels, and -1 renders the full tree. Hidden commands are skipped at every level, matching --help. Also harden and DRY the option handling: floor `width` at 1 in normalization so a non-positive value can't reach rich.Console, and source every per-option default from the TermynalOptions dataclass so the MkDocs config_scheme and the Markdown-extension (Zensical) entry points can no longer drift apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Select a subcommand to render instead of the root via a space-separated path: `:command: export` renders `<cli> export --help`, and `:command: subapp sub-command` selects a nested command. `:subcommands:` recursion applies relative to the selected command, so you can document one command per block with your own surrounding headings and prose. Explicit selection renders the command even if it is hidden; an unknown path raises a clear ValueError. Block-level only (selecting a fixed subcommand globally makes no sense). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a three-level synthetic Typer fixture (root -> middle -> inner -> leaf-cmd) and assert both mechanisms reach the third level: a `:command: middle inner leaf-cmd` path selects the sub-subcommand, and `:subcommands: -1` recurses down to it while `:subcommands: 1` stops short. The repo's own CLI is only two levels deep, so this was previously unverified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi there! Thanks for opening a pull-request! I like the idea. Is your PR ready for review / testing? Happy to prioritize it and get it released if so. |
|
Hi great to hear! Not yet. Ill mark it as ready as soon as i hardened it. |
…t tests _colored_help swaps typer.rich_utils._get_rich_console to capture colored --help; typer exposes no public hook for this. Make the coupling explicit and resilient: reference the symbol via TYPER_RICH_CONSOLE_HOOK, and if it ever disappears, render through a stdout-redirected fallback so help comes out monochrome instead of crashing the build or leaking to the console. Add test_typer_rich_console_hook_present (contract drift guard; needs only typer, so it runs without the termynal extra) and a fallback-branch test asserting rendering still emits a block without crash or stdout leak. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… helpers Route the :prompt: directive through _directive_line so a prompt containing spaces (e.g. `my $`) is kept whole instead of truncated at the first token by the \S+ capture; the other scalar options stay single-token. Add a directive test for the multi-word prompt and unit tests for the _as_bool/_as_int invalid-value fallback branches (markdown.py now fully covered). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The _as_bool/_as_int coverage tests landed in test_plugin.py (a general plugin test file); relocate them to a termynal-scoped test_termynal_directive.py so the termynal feature keeps its own test footprint. test_plugin.py is restored to its prior state; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@0x054 Its ready |
|
@FBumann Thanks again! Great work here, really appreciate it. Just a couple requests:
|
|
@0x054 Sure. And ill check zensical |
|
Zensical needs the termynal css/js injected manually. Luckily, termynal exposes a public api for this! |
…vascript Zensical does not run termynal's MkDocs plugin, so its CSS/JS must be added explicitly or the blocks render as unstyled text. Document a CDN one-liner (pinned to the installed termynal version so the assets match the emitted markup) plus a self-hosted fallback, and warn against inlining the CSS/JS into page content (Zensical folds raw <style> text into the page title/heading). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@0x054 I documented how to get it working for zensical. As zensical does not yet have a way of injecting js/css, this needs some work from the user (minimal) |
|
@0x054 Opened the issue |
|
@FBumann Awesome! I think just take care of the Build & Test failures and I'll approve! |
Collapse a call that fit on one line; fixes the ruff-format pre-commit hook failing in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@0x054 Done. Was just a ruff format. And a weird codecov timeout, which should be unrelated to my changes... |
## Summary Prepare **0.4.0** for release: - Bump `version` in `pyproject.toml` to `0.4.0` - Document termynal output mode and related docs merged since `0.3.1` in `CHANGELOG.md` ## Changes in 0.4.0 - **Added:** `termynal` output mode for animated CLI `--help` rendering ([#35](#35)) - **Added:** Termynal directive options (`:command:`, `subcommands`, styling/timing globals) - **Added:** Termynal docs page and Zensical asset setup notes ## After merge 1. Create GitHub Release `v0.4.0` (publish) to trigger PyPI upload via `publish.yml` 2. Docs site updates automatically on merge to `main` ## Test plan - [x] Changelog and version bump only - [ ] CI passes on this PR Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
|
@FBumann I've released your feature. You can install v0.4.0! |

Feature request: #36
Hi,
I'm trying to document CLI Typer apps. I really want to document them like they'd be shown in a terminal, so I thought I could combine your repo with https://github.com/termynal/termynal.py to achieve nice, colored documentation. I hope you like it and approve it.
Note
This description was drafted with AI assistance (Claude); the code is accumulated fork work. Happy to trim or adjust anything.
What
Adds a
termynaloutput mode to the::: mkdocs-typer2directive. Instead of Markdown tables, it renders a CLI's--helpas an animated, colored terminal via termynal:::: mkdocs-typer2 :module: my_module.cli :name: mycli :termynal: trueBy default only the root command's
--helpis rendered. Use:command:to render a specific subcommand instead, and:subcommands:to stack a depth of subcommand blocks below the selected command. Enable the mode per block (:termynal: true) or globally (termynal: true).Options
Each render option is available per block (
:option:) and globally (termynal_-prefixed, e.g.termynal_width); the block-level value wins.:command:is block-level only.commandexport, orsubapp sub-commandfor nested).:subcommands:recursion applies relative to it. Block-level onlysubcommands0selected command only (default),Nlevels deep,-1the full tree. Hidden commands skipped at every levelwidthschemextermdark_bgbuttonsmacos|windows; invalid →macosprompt$)type_delay/line_delay/start_delayRender options are bundled in a
TermynalOptionsdataclass withLiteral-typed domains; the option domains anddata-ty-*attribute names live as module-level constants intermynal_render.pyas the single source of truth. The per-option defaults are sourced from that dataclass by both entry points (the MkDocsconfig_schemeand the Markdown-extension constructor used by Zensical), so the two can't drift apart.Per-command blocks
Because
:command:selects exactly one command, you can document one command per block with your own headings and prose around each — instead of one giant stacked terminal:## Export ::: mkdocs-typer2 :module: my_module.cli :name: mycli :termynal: true :command: exportrenders exactly
mycli export --help.How it works
nativeClick resolution; no subprocess. Each command's--helpis captured by forcing rich's terminal output, so Typer apps come out colored (plain Click apps render monochrome). Deterministic and cross-platform. Because the renderer only ever produces help text, the prompt line is always<cmd> --helpso it matches the output exactly. Hidden commands are skipped during recursion, matching--help; selecting a hidden command explicitly via:command:still renders it.--help), matching a bare<cmd> --help.:subcommands: 1adds one block per non-hidden direct subcommand, higher values go deeper (depth-first, parent-then-descendants), and-1walks the whole tree.ansi2html, wrapped in termynal'sdata-tymarkup and injected through the markdownhtmlStash. Because it's a markdown extension, it works under both MkDocs and Zensical.termynal+ansi2htmlare an optional extra,mkdocs-typer2[termynal]. Using the mode without it raises a clear install hint, and the imports are deferred to when the mode is actually used, so non-termynal users pull nothing extra.convert()escapes output, which would strip the color). Drift guards intests/test_termynal_contract.pyassert the markup tokens against termynal's own output, the button attributes against its CSS, and the timing attributes against its JS — so a future termynal rename fails loudly.[data-termynal]with padding but no margin); amargin-topis applied only between stacked blocks, so a lone block imposes no margin on surrounding page content.Docs & scope
CLI (Termynal)page is added to the docs site nav, and thetermynalMkDocs plugin is enabled so its CSS/JS animate the blocks (builds clean undermkdocs build --strict). The README documents the mode and every option.legacy/native/prettybehavior is unchanged when:termynal:is off.Tests
tests/test_termynal_render.py(colored Typer, monochrome Click fallback, root-only default,:command:selection incl. nested and three-level sub-subcommand paths / unknown-path error / recursion relative to the selection, subcommand recursion at depth 1 and unlimited-1verified to reach a third level, each option + invalid-value fallbacks, directive/plugin threading, end-to-endhtmlStashinjection) andtests/test_termynal_contract.py(markup / CSS / JS drift guards). Both skip cleanly when thetermynalextra isn't installed.A CHANGELOG entry is added under
[Unreleased].