Skip to content

Repository files navigation

termdown

CI Platforms Swift License: MIT

A minimal terminal Markdown browser written in pure Swift. It lists every Markdown file under the current directory, lets you pick one with the arrow keys, renders it natively in the terminal as styled ANSI text — including Mermaid diagrams drawn as ASCII/Unicode art — and lets you edit blocks inline (press e) and save back to disk, all without leaving the terminal.

demo.mp4

Install

Homebrew (macOS and Linux)

brew install dsaad68/termdown/termdown

Upgrade later with brew upgrade termdown.

From source

Requires a Swift 6.2+ toolchain (see Requirements).

swift build -c release
cp .build/release/termdown /usr/local/bin/termdown   # or: just install

just install builds the release binary and symlinks it into ~/.local/bin (no sudo required). To run straight from a clone without installing:

swift run termdown            # scan the current directory
swift run termdown ~/notes    # scan a specific directory

Features

  • Recursively finds .md / .markdown / .mdx … files in the current folder (skips .git, node_modules, .build, etc.). With stdout or stdin redirected there is no keyboard UI to run, so termdown notes/ | grep … prints the list of files it found instead.
  • Fuzzy file finder with real-time filtering and match highlighting.
  • Folder browsing (d): swap the flat list of every file for the folders one level at a time — Enter steps into the selected folder and shows its folders, Backspace (or /h) comes back out, and each row carries how many Markdown files are beneath it. d again leaves the browser with the file list narrowed to the folder you were standing in, so browsing is also how you scope the list; Esc widens it back to the whole project. Folders that lead to no Markdown file are never shown, and / searches every folder as before. The header keeps naming the folder termdown was opened on, and a breadcrumb row opens the list from the moment you press d❯ .. › docs › api, on its own surface directly above the ../ row (which the narrowed file list carries too, so a folder's files are never a dead end). Set file-list-view: folders to open on the browser instead of the file list.
  • Settings view (,, from the file list or the viewer): every scalar config key on one screen — theme, width, colors, mouse, mermaid, the picker's starting list — with Space cycling a value, Enter toggling or opening a list to pick from, and a number typed for width. Each change is written to ~/.config/termdown/config.yaml as you make it, replacing that key's line and leaving your comments alone, so there is no save step and nothing to lose by pressing Esc. Rows that are only read at startup are marked , and a key your project's .termdown.yaml also sets is marked local, since that file wins.
  • Full terminal rendering powered by Apple's swift-markdown parser:
    • Headings with colored underlines
    • Bold, italic, strikethrough, inline code — bold carries a high-contrast color from the theme as well as the weight, so it stays visible under tmux and with fonts that ship no bold face
    • Bullet / ordered / nested lists and - [ ] task lists
    • Fenced code blocks with syntax highlighting across ~35 languages (via Chroma), drawn as a framed card and mapped onto the matte palette
    • Mermaid diagrams: ```mermaid blocks render as ASCII/Unicode art (flowcharts and sequence diagrams) via a native Swift port of mermaid-ascii — no external tools. All node shapes are accepted ([], (), {}, ([]), [[]], [()], (()), {{}}, >]) and drawn as rectangles; labels may be quoted and may span lines with \n or <br>. Diagrams are laid out to fit the text column — labels wrap, spacing tightens, and a left-to-right flowchart that still will not fit is stacked top-down — so the same block renders differently in a narrow terminal than a wide one. Falls back to a highlighted code block for unsupported diagram types, and for a diagram no layout can fit: an edge label is drawn inline along a one-row arrow and cannot wrap, so it sets a hard floor
    • Line breaks follow CommonMark: consecutive lines are one paragraph and re-flow together, so three commands written on three lines render as one wrapped run. End a line with two spaces or a backslash for a hard break — or use a list or a blank line — to keep them apart
    • GFM tables drawn with box-drawing borders and column alignment
    • Block quotes (including nested)
    • Callouts: a blockquote opening with > [!TAG] renders as a colored callout with a ● TAG header. GitHub's five (NOTE, TIP, IMPORTANT, WARNING, CAUTION) plus INFO, TODO, ABSTRACT, SUCCESS, EXAMPLE, QUESTION, DECISION, FAILURE, DANGER and BUG have colors of their own; tags are matched case-insensitively, so [!note] and [!NOTE] are the same tag. Text after the tag becomes the title in place of the tag name (> [!TIP] Try this instead), and can carry inline markup. Any other tag still renders as a callout — in the plain quote color, titled with its own name — so a vault full of house-style tags reads sensibly instead of leaking [!MYTAG] into the prose. Callouts hold whole blocks: lists, code and multiple paragraphs
    • YAML frontmatter displayed as a metadata panel
    • Thematic breaks (horizontal rules)
    • Links as clickable OSC 8 hyperlinks
    • Footnotes rendered in a dedicated section
    • Math ($…$ / $$…$$) converted to Unicode: Greek, super/subscripts, \frac, \sqrt, accents and common operators
  • Tabs: open multiple documents side by side. T opens the file finder in a new tab, Shift-Enter (or O) opens the focused link in a new tab. Switch with 19 / } / { and close with x. Once a second tab is open the footer shows a tab strip (active tab highlighted).
  • Navigation:
    • t opens a Contents / Open Tabs overlay: the document outline plus the list of open tabs; press t again to switch between the two panes
    • Toggleable outline sidebar (press s) that highlights the section you're currently reading as you scroll
    • Collapsible sections: z folds/unfolds the current section (or the one selected in the sidebar); Z collapses to a top-level outline
    • Heading-to-heading navigation (]/[)
    • In-document incremental search (/ to search live, n/N for next/previous)
  • Copy to clipboard (works over SSH via OSC 52): y yanks the code block nearest the cursor, Y yanks the focused link's URL
  • Doc browser / link navigation:
    • Tab/Shift-Tab cycle between links in the document
    • Enter/o opens the focused link: external URLs launch in your browser, while relative links to other Markdown files open in-app
    • Wikilinks: [[Page]], [[Page|alias]] and [[Page#Heading]] resolve to a discovered Markdown file (matched by name) and open in-app; [[Page#Heading]] jumps to that section after loading
    • Backspace walks back through your navigation history
  • Project-wide search (\): a live "grep" across every discovered Markdown file, showing file:line matches with a preview; Enter jumps straight into that file at the match
  • Runtime layout controls:
    • w toggles line wrapping (chop long lines; / scroll horizontally)
    • +/- adjust the text column width for comfortable reading
    • F toggles follow mode (tail -f-style auto-scroll to the bottom on reload)
  • Cursor mode, selection & inline editing: press v to toggle a line cursor (off by default — j/k scroll as usual until then). With it shown, j/k move the highlighted line (its source line shows as L42), and Shift+↑/↓ (or Shift+J/Shift+K) select multiple linesy copies the selection as raw markdown, Y as rendered text. You can also drag to select text character by character across lines, copied to the clipboard on release (on by default; --no-mouse-select turns it off). Press e to edit the block under the cursor (paragraph, heading, list item, table row, …) as its raw markdown in place while the rest stays rendered. Enter commits the edit to the buffer and marks the document unsaved (●); Ctrl-S writes it to disk, and quitting with unsaved changes prompts to Save / Discard / Cancel.
  • Task lists: in cursor mode, Space ticks the - [ ] under the cursor and writes - [x] back to the source — indentation, bullet style (-/*/+, 1.) and everything after the checkbox are preserved byte for byte. It rides the same path as an inline edit: the toggle marks the document unsaved (●) and Ctrl-S commits it. A nested item toggles itself rather than its parent, and a task that wraps over several rows toggles from any of them. Outside cursor mode — and on any line that isn't a task — Space still pages down.
  • Live reload: automatically reloads when file changes
  • Color themes (29): dark, light, mono; popular ports (catppuccin, rose-pine, nord, tokyo-night, gruvbox, dracula, solarized-dark, solarized-light, everforest, kanagawa, one-dark, monokai, ayu-mirage, night-owl); and custom true-color pastels across matte, cold and warm families (matte-rose, matte-slate, matte-moss, frost, mint, dusk, glacier, blossom, sand, coral, ember, terracotta). Press p in the viewer for a live-preview theme selector that saves your pick. 24-bit color is used automatically when the terminal supports it (COLORTERM)
  • Configurable: supports .termdown.yaml (project root or home dir) for default settings
  • Tests: comprehensive test coverage for core functionality

Requirements

  • macOS 13+ (Swift 6.2+ / Xcode 16+), or

  • Linux with a Swift 6.2+ toolchain

    (the Swift floor is set by the Chroma syntax-highlighting dependency; the package manifest itself still declares swift-tools-version:5.9.)

Usage

The examples/ folder is a guided tour — open it with termdown examples and start at index.md. Each page says which keys it is there to exercise: link navigation, task checkboxes (tasks.md), Mermaid diagrams, wikilinks, and a stress test for long and deeply nested content.

Keybindings

Context Key Action
File list Type text fuzzy filter files
File list / or k/j move selection
File list g / G jump to first / last
File list Enter open the selected file
File list d switch between files and folders
Folder browser Enter step into the folder (its files, if it has none inside)
Folder browser Backspace / / h up one level
Folder browser d back to the files, narrowed to this folder
File list Esc widen back to the whole project
File list , settings (edit the config file)
File list \ project-wide search (live grep)
File list q / Esc quit
Viewer (pager) / or k/j scroll one line
Viewer (pager) Space/PgDn, b/PgUp scroll one page
Viewer (pager) d / u scroll half a page
Viewer (pager) / or h/l scroll horizontally (no-wrap)
Viewer (pager) g/Home, G/End top / bottom
Viewer (pager) :+number+Enter jump to line N
Viewer (pager) Ctrl-L force redraw
Viewer (pager) / incremental search (live)
Viewer (pager) n / N next / previous search match
Viewer (pager) \ project-wide search (live grep)
Viewer (pager) Tab / Shift-Tab cycle to next / previous link
Viewer (pager) Enter / o open the focused link in place
Viewer (pager) O / Shift-Enter open the focused link in a new tab
Viewer (pager) T open a document in a new tab
Viewer (pager) 19 jump to tab N
Viewer (pager) } / { next / previous tab
Viewer (pager) x close current tab
Viewer (pager) y copy code block nearest cursor
Viewer (pager) Y copy focused / nearest link URL
Viewer (pager) Backspace navigate back (history)
Viewer (pager) t Contents / Open Tabs overlay (t switches panes)
Viewer (pager) s toggle outline sidebar
Viewer (pager) s (sidebar open) focus sidebar (↑↓ move, Enter jump, z fold, Esc unfocus, q close)
Viewer (pager) z fold / unfold current section
Viewer (pager) Z fold all / unfold all sections
Viewer (pager) ] / [ next / previous heading
Viewer (pager) v show/hide the line cursor (cursor mode)
Viewer (pager) Shift-↑/, J/K select lines (cursor mode); y copies as markdown, Y as rendered text
Viewer (pager) drag select text character by character; copied on release, y/Y re-copy, any key clears (--no-mouse-select to disable)
Viewer (pager) double / triple click select the word / the whole line
Viewer (pager) Space (cursor mode) toggle the - [ ] task checkbox under the cursor (else page down)
Viewer (pager) e edit the block under the cursor (raw markdown); Enter commits to buffer, Esc cancels
Viewer (pager) Ctrl-S save unsaved edits to the file
Viewer (pager) w toggle line wrap
Viewer (pager) + / - widen / narrow text column
Viewer (pager) F toggle follow mode (tail)
Viewer (pager) B toggle heading banners (h1–h4 as filled color blocks)
Viewer (pager) , settings (edit the config file)
Viewer (pager) p theme selector (live preview, Enter saves to config)
Viewer (pager) q / Esc close sidebar, else extra tab, else back to the file list
Viewer (pager) ? show help

Command-line options

termdown                      # file picker over the current directory
termdown DIR                  # file picker over DIR
termdown FILE.md              # open FILE.md in the viewer
                              #   (renders to stdout with `bare-render: true`)
termdown -o FILE.md           # always open in the viewer
termdown -r FILE.md           # always render to stdout
termdown render FILE.md       # older spelling of -r
termdown -                    # read from stdin

Options:
  -o, --open PATH   Open PATH in the viewer whatever `bare-render` says
                    (a directory opens the file picker)
  -r, --render PATH Render PATH to stdout and exit, whatever `bare-render` says
  --width N         Set terminal width (default: auto-detect)
  --theme NAME      Set color theme. Base: dark, light, mono. Ports:
                    catppuccin, rose-pine, nord, tokyo-night, gruvbox,
                    dracula, solarized-dark, solarized-light, everforest,
                    kanagawa, one-dark, monokai, ayu-mirage, night-owl.
                    Pastels: matte-rose, matte-slate, matte-moss, frost,
                    mint, dusk, glacier, blossom, sand, coral, ember,
                    terracotta
  --no-color        Disable ANSI colors
  --mouse           Enable mouse scroll (on by default)
  --no-mouse        Disable mouse scroll
  --mouse-select    Enable drag-to-select text, copied on release (on by default)
  --no-mouse-select Disable drag-to-select
  --version, -V     Show version information
  --help, -h        Show help message

Configuration

On first run termdown automatically creates a global config file at ~/.config/termdown/config.yaml with commented defaults:

# termdown configuration
theme: dark       # see the full theme list below
# width: 80      # uncomment to fix column width
no-color: false
mouse: true         # false to hand the mouse back to the terminal
mouse-select: true  # false to keep the terminal's own click-drag selection
file-list-view: files   # or `folders` to open on the folder browser
# ignore-patterns: [vendor, "*.snap", archive]   # extra paths to skip

You can edit these from inside termdown with , (the settings view) instead of opening the file: it writes the same keys to the same file, one line at a time.

The location follows XDG_CONFIG_HOME when that variable is set ($XDG_CONFIG_HOME/termdown/config.yaml); otherwise it is the path above.

termdown also writes a config-version: line here. It uses that to add keys your file has never seen — once, so a setting introduced in a later release does not stay invisible just because your config predates it. It never edits a value your file already states, even one that matches an old default: nothing in the file distinguishes a setting you chose from one you left alone, so the only safe assumption is that you meant it. Changed defaults therefore apply to fresh installs; to pick one up on an existing config, delete the key or set it yourself.

Config keys

Key Type Values Effect
theme string see Themes below Content color palette (unknown values fall back to dark)
width int e.g. 80 Fixes the text column width; omit for auto-detect
no-color bool true/false Disables all ANSI color
mouse bool true/false Mouse scroll in the finder and pager (default true)
wide-emoji string cluster/scalar How emoji are measured (default cluster: a ZWJ, skin-tone or variation-selector sequence is one two-column glyph). Use scalar only if your terminal draws the components separately
mouse-select bool true/false Drag to select text in the pager, copied on release (default true; replaces the terminal's own click-drag selection)
config-version int written by termdown Tracks which shipped defaults this file has seen, so a default change reaches existing configs once. Leave it alone
ignore-patterns list [a, b, c] Extra path patterns to skip during file discovery (beyond the built-in .git/node_modules/.build skips)
mermaid bool true/false Render ```mermaid blocks as diagrams (default true; falls back to a code block on parse failure)
mermaid-charset string unicode/ascii Box-drawing character set for diagrams (default unicode)
file-list-view string files/folders Which list the file picker opens on: files (default, every Markdown file in the project) or folders (the folder browser). d switches between them while running either way
bare-render bool true/false What a bare file path does: false (default) opens termdown notes.md in the viewer, true renders it to stdout and exits. -o/-r override it either way; a bare directory opens the picker regardless

Themes: dark, light, mono; ports: catppuccin, rose-pine, nord, tokyo-night, gruvbox, dracula, solarized-dark, solarized-light, everforest, kanagawa, one-dark, monokai, ayu-mirage, night-owl; custom pastels: matte (matte-rose, matte-slate, matte-moss), cold (frost, mint, dusk, glacier), warm (blossom, sand, coral, ember, terracotta). Press p in the viewer to preview/switch live.

A theme colors the document only — the TUI chrome (status bar, sidebar, menus) keeps its own fixed palette. The light themes (light, solarized-light) therefore still sit inside dark chrome.

Custom viewer keys. Bind a key to a viewer action with key-<action>: <char> (the action's default key keeps working; overrides add a key). This applies to the pager only, not text-entry contexts (search / the fuzzy file list).

key-scroll-down: e     # 'e' now scrolls down too (j still works)
key-theme: _           # open the theme selector with '_'

Rebindable actions: scroll-down, scroll-up, page-down, page-up, half-down, half-up, top, bottom, search, next-match, prev-match, project-search, open-link, new-tab, theme, sidebar, wrap, follow, banner, fold, fold-all, next-heading, prev-heading, edit, cursor, toggle-task, contents, help, quit.

Note that toggle-task shares its default key with page-down (Space does both, picking by context), so rebinding it gives the new key both meanings too.

What a bare path does. termdown notes.md opens the file in the viewer, and termdown ~/notes opens the file picker over that folder. bare-render flips what a bare file means:

bare-render: true

With it on, termdown notes.md prints the rendered document to stdout and exits instead. Everything else is unchanged: a bare directory still opens the picker, a typo still fails with no such file or directory, and termdown with no argument still opens the picker over the current directory.

Two flags override the setting in either direction, so neither behaviour is reachable only by editing a config file:

bare-render: false (default) bare-render: true
termdown FILE.md opens the viewer renders to stdout
termdown -o FILE.md opens the viewer opens the viewer
termdown -r FILE.md renders to stdout renders to stdout
termdown DIR file picker file picker

termdown render FILE.md is an older spelling of -r and still works.

A file opened directly gets the folder features of the directory it sits in — project search, new tabs and wikilinks all resolve against its neighbours — but q leaves termdown rather than dropping you into a picker you did not ask for. Note that the folder scan skips hidden files, node_modules and .build, so a file opened from one of those will not find itself in its own project search.

Rendered output keeps ANSI color when stdout is a terminal; pipe or redirect it and you get escape codes in the text, so add --no-color when capturing to a file.

Booleans accept true/yes/on/1 as true. The config reader is flat: key: value lines only, so ignore-patterns must be inline ([...] or a comma-separated list), not a multi-line - item block. Aliases: no-color = nocolor = no_color; ignore-patterns = ignorepatterns = ignore_patterns.

To override settings for a specific project, create a .termdown.yaml in that directory. It is merged on top of the global config: only the keys you set take effect, everything else falls back to the global config:

# .termdown.yaml (project root)
mouse: true
width: 100

Priority order (highest to lowest):

  1. CLI flags (--mouse, --no-color, etc.)
  2. Project-local .termdown.yaml in the current directory
  3. Global ~/.config/termdown/config.yaml

Render a single file to stdout

Useful for piping or quick previews:

termdown -r path/to/file.md
termdown -r README.md | less -R
termdown -r README.md --no-color > README.txt   # plain text, no escape codes

Notes

  • Math ($…$ inline / $$…$$ display) is converted to Unicode: Greek letters, super-/subscripts, \frac, \sqrt, accents and the common operators/relations. Commands without a Unicode form (and spacing macros markdown strips, like \,) degrade gracefully rather than disappearing.
  • Tabs: T opens the file finder and loads the chosen document in a new tab; Shift-Enter (or O) opens the focused link in a new tab. 19 jump to a tab, } / { cycle, x closes the current one, and q peels back a layer at a time (sidebar → extra tab → file list). The tab strip is shown only when 2+ tabs are open. Each tab keeps its own scroll position, folds, sidebar state and search. Note: most terminals (Apple Terminal, default iTerm2) send the same bytes for Enter and Shift-Enter, so Shift-Enter only opens a new tab on terminals that report it distinctly (kitty keyboard protocol / xterm modifyOtherKeys); use O as the universal equivalent.
  • Copy to clipboard: y copies the code block nearest the cursor; Y copies the focused (or nearest visible) link URL. Uses OSC 52 so it works over SSH, with a pbcopy fallback on macOS.
  • Folding: z collapses/expands the section the cursor is in (or the selected heading in the outline sidebar); Z collapses the whole document to a top-level outline, or expands it again.
  • Clickable links use the OSC 8 escape; terminals that don't support it simply show the underlined link text.
  • Link navigation: Tab cycles links; Enter/o opens the focused one. Relative links to other Markdown files are followed in-app (use Backspace to go back); everything else is handed to the system (open) to launch in your browser. Link cycling requires colors (it relies on OSC 8 markers, so it is disabled under --no-color).
  • Outline sidebar (s) and wrap/width/follow controls (w, +/-, F) only affect the interactive viewer, not render output.
  • Mouse is on by default. Turn it off with --no-mouse on the CLI or mouse: false in your config. It works in both the file list and the viewer (pager), and inside the theme picker, outline sidebar, project search and inline editor: the wheel scrolls or moves the selection, and a click picks a row (clicking the highlighted row activates it), positions the editor caret, or follows the link under the cursor (pager) or selects a file, and clicking the highlighted file opens it (list). Mouse reporting uses SGR 1006 mode; terminals that don't support it will just ignore the escape sequences.
  • Drag-to-select is also on by default (--no-mouse-select or mouse-select: false to turn it off). Drag in the viewer to select text character by character — across lines, starting and ending mid-word — and it's copied to the clipboard on release; y/Y re-copy it and any key clears it. A click that doesn't move still follows a link. It stays a separate key from mouse because it additionally reports pointer motion, which replaces your terminal's own click-drag selection while termdown is running — hold Shift, or Option on macOS, to fall back to it. Copying goes through OSC 52, so it works over SSH.
  • Live reload monitors the file modification time and reloads when changed.

Development

See CONTRIBUTING.md for the full workflow. In short:

just test         # swift test
just check        # format-check + lint (strict) + test; run before a PR
just format       # apply SwiftFormat;  just lint runs SwiftLint --strict
just integration  # run the built binary end to end (Tests/Integration/cli.sh)
just linux-integration  # the same checks on Linux, in a container

The project is structured as a Swift Package with a library target (termdownCore), an executable target (termdown), and two test targets: termdownCoreTests (the library) and termdownTests (the executable's UI logic, via @testable import). Source files are kept small and single-purpose (≤300 lines); larger types are split across Type+Concern.swift extensions. Tests/Integration/cli.sh adds a third kind of test: the built binary, run the way a shell runs it. Linting/formatting is configured in .swiftlint.yml and .swiftformat, and CI runs every one of these checks on macOS and Linux.

Snapshot tests

SnapshotTests renders the fixtures under Tests/Fixtures/*.md to ANSI and compares them against committed .ansi golden files, so any change in rendered output is caught. After an intentional rendering change, regenerate the goldens:

TD_UPDATE_SNAPSHOTS=1 swift test

Review the resulting diff before committing.

Credits

  • Mermaid rendering is inspired by and ported from mermaid-ascii by Alexander Grooff (MIT) — the terminal-diagram approach and rendering are its work, reimplemented natively in Swift. The ported engine lives in Sources/MermaidRenderer (see its NOTICE); the diagram fidelity fixtures under Tests/MermaidRendererTests/testdata are copied verbatim from that project.

About

A minimal terminal Markdown browser written in pure Swift, with ASCII/Unicode Mermaid diagrams and inline block editing.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages