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. |
Links and history
| Key | Action |
|---|---|
→ / Enter |
Follow the selected link. |
← |
Back in history. |
Esc |
Clear search highlights / deselect the current link. |
Search
| 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, |
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*.mdfiles, parses each withmarkdown-it-py, extracts the title from the first# heading.renderer.py— walks the Markdown token stream and produces aDocumentcontaining:lines— pre-wrapped styled lines ready forprompt_toolkitlinks— target, kind (internal / anchor / external), and the line indices the link spansanchors— heading slug → line index, used for in-page jumps
browser.py— aprompt_toolkitApplicationwith 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 singleBrowserobject. Four UI modes share the same three windows: browse (the rendered page), search (the/prompt), picker (theptopic 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 transitionstests/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)
| File | Size | Uploaded | |
|---|---|---|---|
| termhelp-0.1.0.tar.gz | 15.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|