Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The terminal

Each pane is a real PTY running a real program — a login shell, or the Claude CLI — rendered on a canvas. It is a terminal, not a transcript viewer: full escape-sequence handling, scrollback, selection, colours.

Selection and clipboard

Drag with the mouse to select; a double-click selects the word or filename under the pointer; Shift+click extends the current selection. The chords that reach the clipboard:

macOSWindows / Linux
Copy selectionCmd+CCtrl+Shift+C
PasteCmd+VCtrl+V or Ctrl+Shift+V

Copy/paste is the one binding that is deliberately irregular per platform: on Windows and Linux Ctrl+C must stay the interrupt. Ctrl+C sends SIGINT on every platform.

Mouse gestures

The two classic terminal conventions are available, both off by default — so nothing reaches the clipboard unless you asked for it:

"terminal": { "copy_on_select": true, "paste_on_right_click": true }
  • copy_on_select — releasing a drag, or double-clicking a word, copies the selection outright. With it off the selection still highlights and waits for the copy chord, which asks the terminal for its current selection — even one scrolled out of view — so the text you copy is the text you just selected, never what you copied last. With nothing selected the chord does nothing — in particular in a pane whose program owns the mouse (below), where the program’s own copy of a drag stays on the clipboard untouched.
  • paste_on_right_click — a right-click pastes into the pane under the pointer, which need not be the focused one, and is bracketed when that pane asked for bracketed paste. The click also focuses that pane, so the keys you type next go where you just pasted. A confirmation prompt refuses it, exactly as it refuses the paste chord — the pointer is not a way past a prompt.

When the program reads the mouse

A full-screen program that turns mouse reporting on — Claude Code’s /diff and /resume, vim, lazygit, fzf, less, tmux with mouse on — owns the mouse while it runs. Clicks, drags and (when it asked for them) bare moves go to the program, so clicking an entry in /resume selects it and clicking in vim places the cursor; the terminal draws no selection of its own over it, and a right-click reaches the program instead of pasting. The wheel already worked this way.

Hold Shift to take the mouse back: Shift+drag selects text from a mouse-mode program exactly as a plain drag does at a shell, and the copy gestures above apply to it. The link modifier does the same for a Ctrl/Cmd+click on a path or URL, which opens it rather than reaching the program. Neither modifier is reported to the program, so it never sees the click it lost.

See settings.json.

Scrollback

The wheel scrolls back through history. Cmd/Ctrl+Up jumps to the top of the buffer, Cmd/Ctrl+Down back to the bottom.

Zoom

Cmd/Ctrl + + / - / 0 steps the font size up, down, and back to the configured base. Zoom is a runtime state — it does not rewrite terminal.font_size in your settings.

Three chords are bound to zoom-in, not one: mod+=, mod+plus and mod+shift+plus. = is the unshifted face of the + key on QWERTY and the unshifted key on AZERTY, so between them the same gesture works across layouts.

URLs in terminal output (http, https, file, ftp) are detected per row. Hold Cmd/Ctrl: the link under the pointer underlines and the cursor becomes a hand; Cmd/Ctrl+click opens it in your OS default handler. Trailing prose punctuation and unbalanced brackets are trimmed from the match.

Hidden hyperlinks work the same way. A program can print a label over a URL it never shows — the OSC 8 escape, which gh, ls --hyperlink, cargo and Claude Code’s rendered markdown all emit — so a table cell reading #76 may point at the full issue URL. termherd carries that target with the grid: hover the label and it underlines, click it and the hidden URL opens. Where a label sits over a hyperlink and looks like a URL itself, the hyperlink wins — it is what the program meant, the text is only its face.

Clickable file paths

The same gesture opens file paths — the payoff being that cargo test prints crates/pty/src/grid.rs:184, and you click it.

Detection is syntactic, then checked against the disk. A bare word with no separator and no extension is never a candidate (otherwise every word under the pointer would cost a stat). A :line[:column] suffix is split off the target before resolution. URL cells are masked out so the two detectors never light the same columns.

Resolution walks candidate roots innermost first, and that order is the disambiguation rule — cargo, git and pytest print relative to different roots:

  1. the session’s live working directory (which follows your cds),
  2. the repository containing it,
  3. the directory the session was launched from.

A path underlines only once it has resolved. An underline that turns out to point at nothing is worse than an underline one frame late.

What it opens the file with

By default, the OS default handler — and that has two consequences worth knowing, both of which the open setting removes.

A line number cannot be honoured. Detection splits :184 off the target and carries it all the way through, but “open this file” is all the OS handoff can express.

Executable-by-association files are neither underlined nor opened. Handing a path to the OS means “do what the association says”, and for a program that means run it — an ls of an untrusted clone is enough to put payload.app on screen. The refusal happens where the resolved path arrives, so hover and click can never disagree: what will not open does not underline either, and you see the refusal before you click. It is a mitigation, not a guarantee: on macOS and Linux the set of extensions that execute is small and closed, but on Windows the association table maps .js, .py and every installed language onto an interpreter, and excluding those would refuse exactly the source files the feature exists to open.

Naming an editor removes both. Set open.command with {path}, {line} and {col} templates — see settings.json — and the click lands on the right line. An explicit editor consults no association either, so the refusal above lifts and those files open like any other. Give a GUI editor: the child’s standard streams are closed, so a terminal editor would start invisible and unkillable. The command runs as argv, never through a shell, and it is split on whitespace before {path} is filled in — so a filename containing spaces or & can never become a second argument.

One limit remains, known and accepted: a path that wraps across two lines is not detected, the same limit URLs have.

Colours and font

The terminal grid keeps its own colours, independent of the app chrome theme. Start from a built-in scheme (solarized-dark, solarized-light, gruvbox-dark, gruvbox-light) and override any slot — foreground, background, cursor, and the 16 ANSI colours. A malformed colour degrades alone, with a logged warning, instead of failing the file. See settings.json.

What the shell announces

TermHerd tracks the session’s working directory from the shell’s own OSC 7 announcements, so a pane’s cwd follows your cds rather than reporting the directory it was launched in forever. That live cwd is what Cmd/Ctrl+T opens a new shell in, what path resolution tries first, and what snapshot reports over MCP.