Add your description here
Project description
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).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file termhelp-0.1.0.tar.gz.
File metadata
- Download URL: termhelp-0.1.0.tar.gz
- Upload date:
- Size: 15.7 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27759d1d5e23bdb50e9138f77c7da87cb58a71eccdef80647d51be9f9e2e68fc
|
|
| MD5 |
694437a62901b549006c6aac0e084d81
|
|
| BLAKE2b-256 |
e9993f25717195249ec29789fa9a8bb259a094c6f4ef8641ec5b70c7c49e1321
|
File details
Details for the file termhelp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: termhelp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d330f5b729f60b10c656b74a9c729e1dbd6c8f8b85c72215de088e48541b061
|
|
| MD5 |
19ff086375835bad5f95eff68f97c46d
|
|
| BLAKE2b-256 |
1409ac484aae6e06c0f76b27444574b9a09905c5a3a43b1d399f9b61a0c218b7
|