Repository navigation
How the Terminal Works
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.
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.
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}
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.
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"]
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.
| 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 |
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]".
-
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.tsandsrc/lib/views/workspaceShortcuts.test.ts. The feature tests are listed in How terminal features work.
Typing, colors and Cmd+V need a manual check.
- 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.pngandterminal-split.png(Docs and Screenshots).
The terminal bugs and their fixes are in Terminal Bugs We Fixed.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode