Skip to content

feat: termynal output mode for the directive - #1

Closed
FBumann wants to merge 11 commits into
mainfrom
feat/termynal-output
Closed

feat: termynal output mode for the directive#1
FBumann wants to merge 11 commits into
mainfrom
feat/termynal-output

Conversation

@FBumann

@FBumann FBumann commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a termynal output mode that renders a CLI's --help as an animated, colored termynal terminal instead of Markdown tables. The app is introspected in-process (no subprocess): each command's --help is rendered through rich so Typer apps come out colored, plain Click apps render monochrome, hidden commands are skipped, and the ANSI is converted to inline HTML via ansi2html and wrapped in termynal's data-ty markup. The root command renders first, followed by one block per non-hidden direct subcommand.

Enabled globally (termynal: true) or per block (:termynal: true). Lives behind an optional [termynal] extra (ansi2html, termynal); using it without the extra raises a clear install hint, and the rest of the plugin keeps no termynal dependency.

Options

Each is available per-block (:option:) and globally (termynal_-prefixed); block-level wins.

Option Purpose
width capture width in columns (default 80)
scheme ansi2html palette; invalid → xterm
dark_bg light/dark background variant
buttons window chrome macos|windows; invalid → macos
prompt prompt symbol (default $)
type_delay / line_delay / start_delay termynal animation timings (ms); unset → termynal defaults

Options are bundled in a TermynalOptions dataclass with Literal-typed domains (AnsiScheme, ButtonStyle); the data-ty-* attribute names and option domains live as module-level constants in termynal_render.py as the single source of truth.

Design notes

  • No raw config passthrough. termynal's global plugin config and JS animation defaults do not reach raw-HTML [data-termynal] blocks (they only drive its fenced-code preprocessor, which we bypass). Anything tunable is emitted as an explicit data-ty-* attribute by us.
  • We emit termynal's markup directly rather than calling Termynal.convert, because convert escapes its output — which would destroy the color spans — and does prompt-parsing over the whole blob. Per-line control is required.
  • Stacked-block spacing is owned here (termynal styles [data-termynal] with padding but no margin); a margin-top is applied only between stacked blocks, so a lone block imposes no margin on surrounding page content.

Tests

  • Colored Typer render, monochrome plain-Click fallback, subcommand recursion, end-to-end htmlStash injection through the Markdown pipeline.
  • Each new option (buttons/prompt/timings) verified both directly and threaded through the directive, plus invalid-value fallbacks.
  • Drift guards against termynal: emitted markup tokens checked against termynal's own output, button attributes against its CSS, and timing attributes against its JS — so a future termynal rename fails loudly.

Docs: a tracked CLI (Termynal) page is added to the site nav, and the README documents the mode and all options.

Breaking change

None for end users yet (feature is new). Note for the changelog: the termynal plugin options are namespaced termynal_width / termynal_scheme / termynal_dark_bg / etc.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added termynal output mode rendering CLI help as animated, interactive terminal UI with configurable ANSI color schemes, timing controls, and button styles.
  • Documentation

    • Extended README and added dedicated guide documenting termynal configuration options and usage examples.
  • Tests

    • Added comprehensive termynal rendering and contract verification tests.
  • Chores

    • Added optional dependencies supporting termynal functionality.

FBumann and others added 10 commits June 15, 2026 20:37
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>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FBumann, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 55 minutes and 21 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46f87ba7-3e84-4fb9-bb8b-df3a86d0c643

📥 Commits

Reviewing files that changed from the base of the PR and between 0703552 and 0b92ff0.

📒 Files selected for processing (1)
  • CHANGELOG.md
📝 Walkthrough

Walkthrough

Adds a termynal output mode to mkdocs-typer2 that renders Typer/Click --help output as animated colored terminal HTML using the Termynal library. A new termynal_render.py module handles ANSI capture and HTML generation; the Markdown extension and MkDocs plugin are extended to thread all new options; contract and render tests guard markup correctness; and documentation is updated throughout.

Changes

Termynal Output Mode

Layer / File(s) Summary
Optional dependencies
pyproject.toml
Adds termynal optional-dependency group (ansi2html>=1.8, termynal>=0.12,<1) and the same packages to the dev group for CI.
Core renderer and command resolution
src/mkdocs_typer2/termynal_render.py, src/mkdocs_typer2/pretty.py
New termynal_render.py defines TermynalOptions, constants, ANSI-to-HTML conversion, rich console patching for colored help capture, per-block HTML generation, and recursive render_termynal_html(). pretty.py extracts resolve_click_command as a shared helper consumed by both the tree builder and the new renderer.
Markdown extension and plugin wiring
src/mkdocs_typer2/markdown.py, src/mkdocs_typer2/plugin.py
TyperExtension and TyperProcessor accept all termynal options; _resolve_termynal_options merges global defaults with per-directive overrides; run() gains an early branch that stashes termynal HTML via md.htmlStash. MkdocsTyper.config_scheme exposes nine new termynal_* fields forwarded into makeExtension from on_config.
Render and contract tests
tests/test_termynal_render.py, tests/test_termynal_contract.py
test_termynal_render.py covers colored/plain output, recursion, all option variations, directive threading, and end-to-end Markdown pipeline. test_termynal_contract.py compares emitted tokens against Termynal's reference renderer and validates BUTTONS/TIMING_ATTRS against Termynal's default CSS/JS.
Documentation and site config
README.md, docs/cli-termynal.md, mkdocs.yaml
README gains a Termynal Output Mode section with directive parameters, setup requirements, and global/per-block config examples. docs/cli-termynal.md adds a live directive demo page. mkdocs.yaml adds the nav entry and enables the termynal plugin.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop hop, the terminal glows,
With colors and animations in rows!
ANSI to HTML, a magical trick,
Termynal renders help output quick.
The rabbit has typed a most colorful scene—
The shiniest --help you've ever seen! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding termynal output mode to the mkdocs-typer2 directive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/termynal-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mkdocs_typer2/pretty.py`:
- Around line 46-49: The code in the section starting with the first getattr
call for `name` currently falls back to module-level `app` even when an explicit
`name` was provided by the user. This masks configuration errors. Modify the
logic to only use the module-level `app` fallback when `name` is empty or None;
if `name` was explicitly provided but the attribute cannot be resolved, raise an
error instead of silently falling back.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53d9c9f7-b688-4faa-8420-7be3a2f2a790

📥 Commits

Reviewing files that changed from the base of the PR and between f78d81e and 0703552.

📒 Files selected for processing (10)
  • README.md
  • docs/cli-termynal.md
  • mkdocs.yaml
  • pyproject.toml
  • src/mkdocs_typer2/markdown.py
  • src/mkdocs_typer2/plugin.py
  • src/mkdocs_typer2/pretty.py
  • src/mkdocs_typer2/termynal_render.py
  • tests/test_termynal_contract.py
  • tests/test_termynal_render.py

Comment on lines +46 to 49
app = getattr(module_ref, name, None) if name else None
if app is None:
app = getattr(module_ref, "app", None)
if app is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when explicit name cannot be resolved.

Line 46–49 currently falls back to module-level app even when name was explicitly provided. That masks typos/misconfiguration and can render docs for the wrong command. Only use fallback when name is empty; otherwise raise.

Suggested fix
 def resolve_click_command(module: str, name: str) -> click.core.Command:
@@
-    app = getattr(module_ref, name, None) if name else None
-    if app is None:
-        app = getattr(module_ref, "app", None)
+    if name:
+        app = getattr(module_ref, name, None)
+        if app is None:
+            raise ValueError(
+                f"Unable to resolve Typer app '{name}' from module '{module}'."
+            )
+    else:
+        app = getattr(module_ref, "app", None)
     if app is None:
         raise ValueError(f"Unable to resolve Typer app from module '{module}'.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app = getattr(module_ref, name, None) if name else None
if app is None:
app = getattr(module_ref, "app", None)
if app is None:
if name:
app = getattr(module_ref, name, None)
if app is None:
raise ValueError(
f"Unable to resolve Typer app '{name}' from module '{module}'."
)
else:
app = getattr(module_ref, "app", None)
if app is None:
raise ValueError(f"Unable to resolve Typer app from module '{module}'.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mkdocs_typer2/pretty.py` around lines 46 - 49, The code in the section
starting with the first getattr call for `name` currently falls back to
module-level `app` even when an explicit `name` was provided by the user. This
masks configuration errors. Modify the logic to only use the module-level `app`
fallback when `name` is empty or None; if `name` was explicitly provided but the
attribute cannot be resolved, raise an error instead of silently falling back.

@FBumann

FBumann commented Jun 15, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the upstream PR syn54x#35 (same branch). Closing this fork-internal one.

@FBumann FBumann closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant