Skip to content

How the Terminal Works

github-actions[bot] edited this page Oct 18, 2026 · 2 revisions

The integrated terminal runs real shells in pseudo terminals (PTYs: the kernel device that makes a program think it talks to a terminal window) and draws them with xterm.js. The user side is in Terminal. Split terminals, find, file links, dropped files and the optional addons are in How terminal features work.

Why we need it

People run git commands, dev servers and tests next to their code, and users expect a terminal in the window. Prompts, colors, vim and Ctrl+C need a real PTY, not a simple output view.

How it works

Backend: one PTY and three threads per terminal

src-tauri/src/terminal.rs starts the shell with portable-pty. Each terminal gets three threads:

  • writer: writes keystrokes from an unbounded queue in order, so a full PTY buffer never blocks the caller;
  • reader: reads output in 16 KB chunks;
  • waiter: waits for the shell, then up to 500 ms for the last output (a nohup x & job can keep the PTY open), then reports the exit once.

TerminalRegistry (in AppState) maps terminal ids to PtyProcess values; a terminal leaves it when its shell exits.

sequenceDiagram
  participant View as TerminalView.svelte
  participant Cmd as commands/terminal.rs
  participant Reg as TerminalRegistry
  participant Shell as shell in a PTY
  View->>View: new Channel, onmessage writes to xterm
  View->>Cmd: terminal_spawn(shellId, cwd, cols, rows, channel)
  Cmd->>Reg: spawn without holding the lock
  Reg->>Shell: start, threads up
  Cmd-->>View: TerminalInfo (terminalId, pid, shell, cwd)
  View->>Cmd: terminal_write(terminalId, keys), sync
  Shell-->>View: raw bytes on the Channel
  Shell-->>Reg: exit, removed from the registry
  Reg-->>View: event "terminal-exited" {terminalId, exitCode}
Loading

The commands are terminal_shells, terminal_spawn, terminal_write, terminal_resize, terminal_close and terminal_close_all, plus terminal_unstash and terminal_reattach for Clear Cache and terminal_memory for the memory details (arguments in Commands and Events). Small and big output chunks take different routes, so the last chunk can arrive after terminal-exited; the store waits 250 ms before it closes a terminal that exited with code 0. exitCode is null when the shell was killed.

Shells. shell_profiles reads /etc/shells, keeps executable files, removes duplicates by real path and puts the login shell ($SHELL, else the passwd entry, for apps opened from Finder) first. zsh, bash and fish start with -l, so PATH matches Terminal.app. Windows shells are in Platforms and Signing.

Environment. The app's environment plus TERM=xterm-256color, COLORTERM=truecolor, TERM_PROGRAM=Nibtree, TERM_PROGRAM_VERSION (the app version), and LANG=en_US.UTF-8 when LANG is empty (apps opened from Finder have none).

Killing. SIGHUP to the shell's process group and the foreground job's group, then SIGKILL after 2 s. At app exit, shutdown hangs up every shell, waits up to 300 ms and kills the rest.

Frontend: a store, a host and moving frames

terminalStore.svelte.ts keeps the TerminalEntry list (backend id, name, shell, folder, exit state and location: "panel", "editor" or "run") and the panel state, never xterm objects.

TerminalHost.svelte mounts every terminal's TerminalView once, for its whole life. Its TerminalFrame moves the element with appendChild into the place that shows it, so moving a terminal never restarts the shell or loses the scrollback. In the panel that place is one pane per terminal of the shown split group (paneSlots); terminals of other groups wait, hidden, in the panel's view area.

flowchart LR
  Store["terminalStore<br/>entries with location"] --> Host["TerminalHost<br/>one TerminalFrame each"]
  Host --> Frame["TerminalFrame<br/>moves its element"]
  Frame -->|"panel"| Panel["TerminalPanel pane<br/>of the shown group"]
  Frame -->|"editor"| Tab["TerminalSlot in the<br/>terminal: tab"]
  Frame -->|"run"| Run["RunView slot"]
  Frame -->|"no slot on screen"| Parked["parked in the host"]
Loading

TerminalView loads xterm with import() (its own chunk, see xterm.ts), shows "Loading terminal..." then "Starting zsh...", sets the Channel's onmessage before calling terminalSpawn, and buffers typing until the shell is ready. A ResizeObserver refits the grid and calls terminalResize only when the size in cells changes.

Editor tabs. A terminal tab uses the pseudo path terminal:<key> (terminalTabs.ts), like commit tabs (How commit tabs work). isPseudoTab in stores/pseudoTabs.ts keeps it out of file lookups and the Back and Forward history. repoStore.onTabsClosed tells the store when tabs close, so closing a terminal tab kills its shell. panelAfterLeave picks the terminal the panel shows next and hides the panel with its last one.

Keys. terminalKeyAction in keys.ts sends each key to the shell, the app, or the view's own copy, paste, clear and select all. Ctrl+ and Ctrl+Shift+ always reach the app (event.code === "Backquote"). App keys return false without preventDefault, so workspaceShortcuts.ts runs them.

The list. With several terminals, TerminalList.svelte shows a list beside the view (the second pane of the UI library's SplitPane), as wide as terminalListWidth in state.json (default 180 px, at least 120). maxListWidth in terminals.ts caps it so the terminal keeps MIN_TERMINAL_VIEW_WIDTH (240 px) in a narrow panel; the saved width stays as dragged.

Settings. terminalDisplayOptions in options.ts maps preferences to xterm options and applyChangedOptions writes only the changed ones, because each write redraws. buildTerminalFontFamily in fonts.ts adds the Nerd Font fallbacks, and theme.ts reads the --term-* tokens again on a theme change. Every key is in How Settings Work.

Where the code lives

File What it does
src-tauri/src/terminal.rs Shells, spawn_pty, PtyProcess, TerminalRegistry
src-tauri/src/commands/terminal.rs The terminal_* commands and the exit event
src/lib/terminal/terminalStore.svelte.ts Entries, panel state, moves, exits
src/lib/terminal/TerminalView.svelte The view on the UI library's TerminalScreen, and the effects that follow settings and visibility
src/lib/terminal/terminalController.svelte.ts, terminalInstance.ts Opening xterm, fitting, Retry, the bell, drops, the menu
src/lib/terminal/terminalShell.svelte.ts Spawning, reconnecting, output with acks, input, resize, the startup status
src/lib/terminal/TerminalHost.svelte, TerminalFrame.svelte, TerminalSlot.svelte, TerminalPanel.svelte, TerminalList.svelte Placing the views, the panel and its list
src/lib/terminal/terminals.ts, terminalTabs.ts, keys.ts, options.ts, fonts.ts, theme.ts Pure helpers

Design decisions

Kill Terminal does not ask. CLAUDE.md asks us to confirm destructive actions, but the app cannot tell whether anything runs in a shell, so the question would come every time and teach people to click through it. The user chose this behavior on 2026-10-01. Closing a Run tab with a running script does ask, because there we know a process runs.

Hiding keeps shells running, so a dev server survives a hidden panel.

terminal_write and terminal_resize are sync. Async commands may run out of order and scramble keystrokes. Both only queue bytes or resize, so they are cheap.

Raw bytes, not strings. xterm joins a UTF-8 character split between two chunks; decoding each chunk ourselves would break it.

A clean exit closes, an error stays, with "[Process exited with code N]".

Tests

  • src-tauri/src/terminal.rs: shell detection, start folder, environment, real processes (output, exit code, input, resize), killing, registry cleanup and shutdown.
  • src/lib/terminal/terminals.test.ts, terminalTabs.test.ts, keys.test.ts, options.test.ts, fonts.test.ts, theme.test.ts, src/lib/views/files/reveal.test.ts and src/lib/views/workspaceShortcuts.test.ts. The feature tests are listed in How terminal features work.

Typing, colors and Cmd+V need a manual check.

Keeping this page in sync

  • Update this page and Commands and Events when a command, the event, the store or the placing of views changes.
  • Update Terminal and Terminal Features for visible changes and retake terminal-panel.png, terminal-shell-menu.png, terminal-list.png, terminal-editor-tab.png, terminal-settings.png, terminal-find.png and terminal-split.png (Docs and Screenshots).

Bugs we fixed

The terminal bugs and their fixes are in Terminal Bugs We Fixed.

Home

Using Nibtree

Developers

How features work

Clone this wiki locally