Skip to content

Scrollback dump — every shell pane across every tab, to disk

tmnl has two scrollback-export verbs.

The first is scrollback.save — the focused pane only. You’re working in one tab, something just scrolled off the top, you want the buffer on disk. One file, one pane, done.

The second — the subject of this page — is scrollback.dump_all_panes. It walks every tab + every shell pane in the focused window and writes one file per pane into a fresh timestamped directory, plus an INDEX.txt mapping each tab/pane to its filename. The use case is different: “I’m about to quit and want everything captured,” or “I’m filing a bug that spans tabs,” or “this multi-hour build session has scrollback in five panes and I want all of it.”

Different intent, different file shape. The two commands are deliberately separate primitives — saving 30 files when you wanted 1 is noise; saving 1 file when you wanted 30 is data loss.

From the command palette (Cmd+Shift+P or F1), search for “Dump all shell scrollback”. Press Enter. The command id is scrollback.dump_all_panes; there’s no default chord. Bind one in ~/.config/tmnl/config.toml if you reach for it often:

[[keybind]]
key = "cmd+shift+s"
command = "scrollback.dump_all_panes"

The command is gated on no_modal_open — it stays inert while the command palette, Settings overlay, or any other modal is in the foreground, so the chord can’t fire while you’re searching for it.

On macOS, the dump dir is created under ~/Desktop and revealed in Finder when the write completes:

~/Desktop/tmnl-scrollback-dump-1718295496/
├── INDEX.txt
├── tab00-pane00-zsh.txt
├── tab00-pane01-claude-code.txt
├── tab01-pane00-pnpm-build.txt
├── tab02-pane00-TE-1234.txt
└── tab03-pane00-mixr.txt

The trailing number on the dir is the Unix timestamp at dump time, so two dumps in the same session don’t collide. If ~/Desktop isn’t a directory (headless macOS, or another OS), the dir falls back to $HOME; if that’s also unavailable, the cwd.

tab<NN>-pane<NN>-<sluggified-tab-label>.txt
  • tab<NN> and pane<NN> are zero-padded two-digit indices into the window’s tab list and the tab’s pane vec, in render order. They mean you can sort the directory listing and read top-to-bottom matching the window layout.
  • The label slug is the tab’s chip label, with anything outside [A-Za-z0-9_-] collapsed to -, capped at 40 chars. Emoji, Unicode, spaces, slashes — all get normalised so the filename is portable and readable across ls, Finder, and the usual shell tooling.
  • An empty tab label falls back to tab-<idx>.

The index file at the top of the dir has one line per dumped pane plus a summary footer:

tmnl scrollback dump @ 1718295496
tab 0 pane 0 (zsh): 32 rows + 410 scrollback → tab00-pane00-zsh.txt
tab 0 pane 1 (claude code): 32 rows + 198 scrollback → tab00-pane01-claude-code.txt
tab 1 pane 0 (pnpm build): 32 rows + 5021 scrollback → tab01-pane00-pnpm-build.txt
tab 2 pane 0 (TE-1234): 48 rows + 12 scrollback → tab02-pane00-TE-1234.txt
tab 3 pane 0 (mixr): 32 rows + 0 scrollback → tab03-pane00-mixr.txt
wrote 5/5 shell panes total.

Each line carries:

  • The tab index and pane index — match these against the tab strip / pane layout to identify a row.
  • The live tab label (un-slugified — the readable form).
  • <rows> rows + <scrollback_len> scrollbackrows is the visible grid height at dump time; scrollback_len is how many additional rows sit above the visible region in the vt100 scrollback buffer.
  • The on-disk filename.

The summary footer reports wrote <N>/<M> shell panes total, where M is the count of shell panes the dump pass found and N is the count that successfully serialised. A mismatch is rare but indicates an IO failure on one or more panes — the per-line error makes it explicit:

tab 1 pane 0: ERROR No space left on device (os error 28)

Each pane file is the pane’s full text content — scrollback above the visible region, plus the visible grid — with one row per line and trailing whitespace trimmed. No ANSI escape codes. No frame markers. No header. It’s the same shape scrollback.save produces, just one per pane.

The content is captured from the vt100 parser via all_rows_text(), which walks the scrollback ring buffer + the live screen in render order. The vt100 grid is cols × rows of styled cells; the dump flattens style information away — colours, attrs, cursor position are not preserved. The use case is “I want the text” not “I want a session replay.”

The dump pass walks app.win.tabs (the focused window only) and visits every pane. It includes:

  • Shell panes with an attached session (one with a live pty). Each becomes a file.

It skips:

  • Native panes (mnml, mixr, custom tmnl-protocol clients). They don’t have a vt100 grid — their content is structured Frames of cells, not a parseable text buffer. The dump pass walks past them silently.
  • Browser panes. Same shape — no vt100 grid.
  • Shell panes whose session is detached (an adopted pty that hasn’t connected yet, or a session marked released after a pty-fd handoff — see Native tabs).

A window with three native tabs and one shell tab will produce a dir with one .txt file in it. The INDEX.txt summary footer counts only shell panes, so the <N>/<M> ratio always refers to dump-eligible panes.

The dump is focused-window-only. A multi-window session with three windows running dumps the focused window’s panes; background windows need a separate dump pass after you `Cmd+“ cycle to them. (Adding a “dump all windows” verb is a possible follow-up; today’s primitive is narrower so the resulting directory doesn’t explode.)

Three flows it fits.

Quit-time capture. You’ve been working all day across six tabs. Build logs in tab 1, a Claude Code session in tab 2, a few shells in the rest. Before you Cmd+Q, you want the lot. One palette command, the dir opens in Finder, you have a complete snapshot.

Bug filing across panes. A reproduction involves what pnpm build said in one tab and what psql said in another, plus the shell error that triggered them both. Dump-all, attach the timestamped dir to the issue (tar czf dump.tgz ~/Desktop/tmnl-scrollback-dump-* is the usual follow-up), include INDEX.txt as the orientation.

Long-running session triage. A multi-hour build that scrolled off the visible region in three panes, plus a shell that’s been running your test loop, plus mnml in a native tab. Dump-all writes the shell buffers and skips the native; you read INDEX.txt to see which pane had the most scrollback (<scrollback_len> in the index line is the answer), open that file first, search.

The whole verb is ~100 lines in src/command.rs under the scrollback.dump_all_panes builtin. The text extraction goes through ShellSession::all_rows_text() in src/shell/scroll.rs — the same method scrollback.save calls, so the per-pane file format matches.

The slug helper at the top of command.rs is shared with the single-pane scrollback.save path (and the selection-export path) so filenames stay consistent across all three verbs.

Failures are best-effort:

  • The dir mkdir failure logs + bails (log::warn!("scrollback dump mkdir …")).
  • A single per-pane write failure logs + records the error inline in INDEX.txt, then the pass continues to the next pane. You get a partial dump rather than nothing.
  • The open <dir> Finder-reveal on macOS is silent on failure — the files are still on disk; the log line records the path either way.

In-flight crash-recovery dump (automatic, every 30 seconds)

Section titled “In-flight crash-recovery dump (automatic, every 30 seconds)”

The two verbs above are explicit — you ask, tmnl writes. There’s a third path that runs by itself, with no command: an in-flight dump that survives a crash.

When cfg.scrollback_persist_on_quit is on (the same knob that controls the clean-exit save) tmnl walks every Shell pane in the focused window every ~30 seconds and writes its current scrollback to a per-tab file:

~/.cache/tmnl/scrollback/
├── tab-0.txt
├── tab-1.txt
└── tab-2.txt

One file per tab, indexed by tab position. Overwritten on every dump — this is “the last 30 seconds is recoverable” insurance, not a backup. A clean quit still writes scrollback-last.txt via the historical path, so you have two artifacts: the focused tab’s end-of-session capture and a per-tab snapshot from up to 30s before the crash.

The dump runs in App::tick, throttled by an Instant comparison — cheap when not due, silent on FS errors so a broken cache dir can’t gum up the loop. There’s no menu item or command for it; the cfg knob alone gates it on/off.

When to look at the in-flight files:

  • A panic, SIGKILL, or sudden window close lost the session. Check ~/.cache/tmnl/scrollback/tab-<N>.txt to recover what was on screen.
  • You want to grep across tabs without invoking the explicit dump_all_panes command — the files are already there.

When NOT to rely on them:

  • They’re focused-window-only — background windows aren’t dumped. Multi-window sessions need an explicit dump if you want the others.
  • The 30-second cadence means you can lose up to ~30s of recent output between dumps.
  • Native panes (mnml, mixr) are skipped — same reason dump_all_panes skips them.

Disabling it: flip scrollback_persist_on_quit = false in ~/.config/tmnl/config.toml. That’s the same switch that disables the clean-exit save — one knob, both behaviors.

When tmnl boots and finds saved tabs via the existing session restore (~/.local/state/tmnl/session.json), it now also checks ~/.cache/tmnl/scrollback/tab-<idx>.txt for each Shell tab. If a file exists, the contents are fed back into the new pty’s vt100 parser before any fresh output appears — so you open tmnl after a crash and see your old terminal output above the new prompt.

The pty itself is fresh: the running process is gone (a crash killed it), and only the visible history is reproduced. Hitting ↵ on the restored prompt runs in the new session, not the dead one — the scrollback is read-only history.

Two practical notes:

  • The replay normalizes \n to \r\n so the parser advances the cursor properly between rows. A trailing newline is appended so the new prompt lands cleanly below the replay.
  • The dump file is left in place for the next dump tick to overwrite. Reopening tmnl within the same dump cycle replays the same snapshot.
  • Tabs, splits, and panes — the tab + pane model the dump walks. Useful for matching tab<NN>-pane<NN> indices back to a window layout.
  • Multi-window — dump-all is focused-window-only; a multi-window session needs a pass per window.
  • Native tabs — why native panes are silently skipped (they don’t have a vt100 grid to flatten).
  • Updates — the other “background utility” page.
  • FEATURES.md — the shipped-feature inventory.