Skip to main content

termhelp

An interactive Markdown help browser for Python CLI applications.

Write your help docs as Markdown, ship them inside your package, then add an -i / --interactive flag to your help command. Your users get a keyboard-driven, scrollable, searchable, cross-referenced help system instead of a wall of --help text that scrolls off-screen.

No Rich, no Textual — just markdown-it-py and prompt_toolkit.

What it looks like

A single full-screen page with a title bar, the rendered topic, and a status line. Cross-topic and anchor links are highlighted; pressing ↑ / ↓ walks between them, Enter follows. Press / to search, p for a fuzzy topic picker, ? for the in-app keymap.

 termhelp · Configuration  [3/4]  L1-22/45
 ─────────────────────────────────────────────────
  # Configuration

  Back to the index — see also Getting started.

  myapp is configured via a TOML file. Every key
  is optional; defaults are shown below.

  ## Top-level keys

  ┌─────────┬───────┬───────────┬───────────────┐
  │ Key     │ Type  │ Default   │ Description   │
  ├─────────┼───────┼───────────┼───────────────┤
  │ profile │ table │ {}        │ Named conn... │
  │ log     │ table │ see below │ Logging conf. │
  └─────────┴───────┴───────────┴───────────────┘

 ─────────────────────────────────────────────────
  ↑/↓ link · ←/→ back/follow · / search · p pick

The model is Lynx + :help: arrows walk between hyperlinks, ← goes back in history, ← / → thread through your topics like web pages, / searches inside the current page, and p jumps to any topic by name.

Quick start

Clone the repo and try the bundled demo docs:

git clone https://github.com/.../termhelp
cd termhelp
uv sync
uv run python -m termhelp tests/fixtures/docs

Move around with the arrow keys; press ? for the full keymap.

Add it to your CLI

The whole public API is one function:

from termhelp import browse

browse("path/to/docs")               # open the first topic (index.md if present)
browse("path/to/docs", entry="config")  # open a specific topic

A typical wiring with argparse (the same pattern works with Click and Typer):

# myapp/help.py
from importlib.resources import files
from termhelp import browse

def help_interactive() -> None:
    docs_dir = files("myapp") / "docs"
    browse(docs_dir, entry="index")
# myapp/cli.py
parser.add_argument("-i", "--interactive", action="store_true")
# ...
if args.interactive:
    from myapp.help import help_interactive
    help_interactive()

You can also invoke termhelp directly during authoring:

python -m termhelp ./docs              # browse a docs dir
python -m termhelp ./docs -e config    # open straight on the 'config' topic

Writing your help docs

Drop a directory of .md files anywhere in your package. Each file is one topic; the filename (without .md) is the topic slug; the first # heading becomes the topic title shown in the browser.

myapp/docs/
├── index.md
├── getting-started.md
├── config.md
└── commands.md

index.md, if present, is always listed first. Other topics are sorted alphabetically.

Cross-references

Standard Markdown links power navigation between topics:

You write... What happens when followed
[Config](config.md) or [Config](config) Switch to the config topic.
[Top](#top-level-keys) Scroll to that heading in the current topic.
[Env overrides](config.md#environment-overrides) Switch topic and scroll to the anchor.
[Project](https://example.com) Show the URL in the status bar (so the user can copy it). termhelp does not open URLs on the user's behalf.

Anchor slugs are generated by lowercasing the heading and replacing whitespace with -, matching the convention used by most Markdown renderers and static-site generators.

Key bindings

The full reference is always one keystroke away (press ? in the app).

Navigation

Key Action
↑ / ↓ Move between links on the page. Past the first or last link, scrolls one line. On link-free pages, scrolls directly.
j / k Scroll one line down / up.
Space / f / PageDown Page down.
b / PageUp Page up.
g / G Jump to top / bottom of the page.
h Go to the home (first) topic. If already there, scroll to top.
Key Action
→ / Enter Follow the selected link.
← Back in history.
Esc Clear search highlights / deselect the current link.
Key Action
/ Open the search prompt (Enter confirms, Esc cancels).
n / N Next / previous match.

Topic picker

Key Action
p Open the topic picker (a filterable list of every topic).
type Filter by slug or title (case-insensitive).
↑ / ↓ Move selection.
Enter Open the selected topic.
Esc Close the picker.

Help and quit

Key Action
? Toggle the in-app help overlay.
q Quit the app (in browse view) or close the help overlay (in help view).
Ctrl-C Quit, anywhere.

Supported Markdown

Construct Status
Headings #–###### Yes; anchor slugs auto-generated
Paragraphs (width-aware word wrap) Yes
Bullet and ordered lists Yes (basic nesting)
Fenced code blocks (```) Yes; no syntax highlighting
Inline code, bold, italic, strike Yes
Links (internal, anchor, external) Yes
Tables (GFM pipe syntax) Yes; box-drawn borders, bold headers
Block quotes (>) Yes; rendered with a │ margin
Horizontal rules (---) Yes
Images Ignored
HTML passthrough Ignored

Search is case-insensitive substring matching on the rendered plain text.

Status and stability

This is the first public release (0.1.0). The public API — browse(docs_dir, entry=None) — is intentionally minimal and not expected to change. Cosmetic details (key bindings, status-bar wording, color choices) may still evolve before 1.0. See CHANGES.md for the release log.

Limitations and non-goals

  • No mouse support — keyboard-first by design.
  • No syntax highlighting in code blocks (would pull in pygments; out of scope).
  • No HTML rendering or image display.
  • Search is substring, not regex, and not incremental.
  • Anchor-slug generation is heuristic; collisions are theoretically possible in long docs.
  • termhelp shows external URLs in the status bar but does not launch them in a web browser.

How it works (for the curious)

Three modules, roughly 700 lines total:

  • loader.py — scans a directory of *.md files, parses each with markdown-it-py, extracts the title from the first # heading.
  • renderer.py — walks the Markdown token stream and produces a Document containing:
    • lines — pre-wrapped styled lines ready for prompt_toolkit
    • links — target, kind (internal / anchor / external), and the line indices the link spans
    • anchors — heading slug → line index, used for in-page jumps
  • browser.py — a prompt_toolkit Application with a title bar, content pane, and status/search bar. State (current topic, scroll, selected link, history stack, search query, picker filter, mode) lives on a single Browser object. Four UI modes share the same three windows: browse (the rendered page), search (the / prompt), picker (the p topic list), help (the ? overlay).

The renderer re-runs whenever the terminal width changes, so wrapping always matches the current window. Search highlights and selected-link styling are layered on top of the rendered segments at draw time.

Development

uv sync
make test     # pytest
make lint     # ruff + ty + pyrefly + mypy
make format   # ruff format & fix

Test layout:

  • tests/a_unit/ — unit tests for loader, renderer, and browser state transitions
  • tests/fixtures/docs/ — the sample help directory used by tests and the manual demo

Documentation site

The longer-form documentation is in docs/, built with Zensical. Configuration lives in zensical.toml at the repo root. To preview locally:

uv tool install zensical          # one-off
zensical serve                    # http://127.0.0.1:8000

License

MIT (placeholder — see LICENSE once added).

Metadata

Release files for termhelp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for termhelp 0.1.0
File Size Uploaded
termhelp-0.1.0.tar.gz 15.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for termhelp 0.1.0
File Interpreter ABI Platform
termhelp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.7 kB

Release files / termhelp-0.1.0.tar.gz

Download URL termhelp-0.1.0.tar.gz
Size 15.7 kB
Tags Source
SHA-256 checksum
How to use checksums
27759d1d5e23bdb50e9138f77c7da87cb58a71eccdef80647d51be9f9e2e68fc
BLAKE2b-256 checksum
How to use checksums
e9993f25717195249ec29789fa9a8bb259a094c6f4ef8641ec5b70c7c49e1321
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / termhelp-0.1.0-py3-none-any.whl

Download URL termhelp-0.1.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d330f5b729f60b10c656b74a9c729e1dbd6c8f8b85c72215de088e48541b061
BLAKE2b-256 checksum
How to use checksums
1409ac484aae6e06c0f76b27444574b9a09905c5a3a43b1d399f9b61a0c218b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page