Cross-platform terminal emulator built with Tauri 2, React 19 + TypeScript, xterm.js, and a Rust PTY backend (portable-pty).
A per-project tab & split workspace with session restore, themes, a command palette, global search, broadcast input, and a status bar with live git info.
See CHANGELOG.md for the full release history.
- Projects — add folders as named, colored projects with drag-and-drop reorder. Each project owns its own tab group.
- Tabs per project —
Cmd/Ctrl+Topens a new tab in the active project; switching projects restores the last tab you were on. - Recursive splits — horizontal / vertical splits inside a tab via
Cmd/Ctrl+D/Cmd/Ctrl+Shift+Dor the panel context menu. Each split = own PTY. - Session restore — tabs, splits (with sizes) and cwd per panel persist across restarts.
- OSC 7 auto-setup (opt-in) — tracks
cdin zsh / bash / fish so session restore returns you to the right directory. - Themes — 8 presets (Default, Dracula, Nord, Solarized Dark, Tokyo Night, GitHub Dark, iTerm2 Dark Background, Gruvbox Dark). The app chrome (sidebar, tab bar, status bar) auto-aligns to the theme's background.
- Terminal search (
Cmd/Ctrl+F) — inline search bar above the focused terminal, including scrollback. - Global search (
Cmd/Ctrl+Shift+F) — fuzzy-searches the buffer of every mounted terminal; result click jumps to the tab and scrolls to the match. - Clickable URLs — Cmd/Ctrl-click opens in the system browser.
- Clipboard polish — copy-on-select to system clipboard, instant paste (via Tauri clipboard plugin, no permission prompt).
- Status bar — shell · cwd (abbreviated with
~) · live git status (branch, staged / modified / untracked / conflicts, ahead / behind upstream) · app version. - Activity badge — tabs with background output show a colored dot until opened.
- Command palette (
Cmd/Ctrl+Shift+P) — fuzzy-searchable: switch projects, new / close / rename tab, split, change theme, toggle sidebar, open settings, about, global search. - Broadcast input — type once, every panel in the tab receives it (per-tab toggle).
- Hide / show sidebar (
Cmd/Ctrl+B) — reclaim space when you need it. - Settings dialog (
Cmd/Ctrl+,) — font family + size (terminal), UI font size, theme, cwd tracking toggle. - About dialog — logo, version, credits.
- Error boundary — a React crash shows a reload screen instead of a blank window.
- Node.js 20.19+ or 22+ (project ships
.nvmrcpinning 22 —nvm usepicks it up) - Rust stable toolchain — install via rustup
- git on
PATH— only needed for status bar git indicators; terminal itself works without - Platform build prerequisites per Tauri 2 docs
- macOS: Xcode Command Line Tools (
xcode-select --install) - Linux: GTK/WebKit dev packages plus
xdg-desktop-portalfor native file dialogs - Windows: WebView2 + MSVC build tools
- macOS: Xcode Command Line Tools (
nvm use # picks up .nvmrc
npm install
npm run tauri devFirst launch compiles the Rust backend — expect a few minutes. Subsequent runs are incremental.
Production build: npm run tauri build.
npm run releaseWraps tauri build and prints where the installable bundle landed. On macOS produces .app and .dmg; on Linux produces .AppImage, .deb, and (if rpmbuild is present) .rpm.
For multi-arch on macOS:
npm run release -- --target aarch64-apple-darwin # Apple Silicon
npm run release -- --target x86_64-apple-darwin # IntelRun the Linux build inside a Docker container — no Linux VM required:
npm run release:linuxFirst invocation builds the image shellboard-linux-build:latest from scripts/docker/Dockerfile.linux (Ubuntu 24.04 + Rust stable + Node 22 + webkit2gtk dev libs). That takes ~3–5 minutes. Subsequent runs reuse the image.
Cargo and npm caches are persisted across runs in a Docker volume (shellboard-build-cache), so incremental rebuilds are fast. Linux artifacts land in a separate src-tauri/target-linux/ tree so they don't collide with local macOS builds.
What it builds: .deb and .rpm by default. .AppImage is intentionally skipped because linuxdeploy (which Tauri uses to package AppImages) fails under Rosetta emulation on Apple Silicon in ways that are impractical to debug — it aborts with std::logic_error: subprocess failed (exit code 2) and Tauri swallows the subprocess stderr. The GitHub Actions release workflow produces .AppImage correctly on its native Linux runner.
To still attempt AppImage locally (e.g. if you've moved to a native Linux host):
SHELLBOARD_BUNDLES=deb,rpm,appimage npm run release:linuxRequirements: Docker (Docker Desktop on macOS works). On Apple Silicon the container runs under linux/amd64 via Rosetta — slower than native but fully functional for .deb / .rpm packaging.
Docker memory: the image runs with CARGO_BUILD_JOBS=2 by default so the build fits inside ~4 GB of container RAM. If rustc dies with signal: 9, SIGKILL on heavy crates (gtk, webkit2gtk-sys), Docker ran out of memory — either:
- raise Docker Desktop's memory in Settings → Resources (6–8 GB recommended), then:
CARGO_BUILD_JOBS=4 npm run release:linux
- or keep the defaults and retry; incremental rebuilds only recompile what changed, so OOM-prone crates usually don't have to be recompiled next run.
Push a v* tag and the release workflow builds bundles for macOS (Apple Silicon + Intel) and Linux, then creates a draft GitHub Release with those bundles attached.
# Bump version in package.json, tauri.conf.json, and Cargo.toml
git commit -am "release: v0.1.0"
git tag v0.1.0
git push origin main --tagsWait for the workflow to finish (~10 minutes), go to Releases on GitHub, review the draft, write release notes, and click Publish.
Without signing, macOS Gatekeeper refuses to open the app on first launch. Users can either right-click → Open, or strip the quarantine:
xattr -cr /Applications/Shellboard.appIf you have an Apple Developer ID, set these repository secrets to sign + notarize automatically in CI: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_ID.
If you replace logo.png in the repo root:
npm run tauri icon logo.png…requires a square source. For the macOS-style squircle wrap we ship a one-off script:
node scripts/make-macos-icon.mjs # writes logo-macos.png
npm run tauri icon logo-macos.png
cp logo-macos.png public/logo.png # About dialog uses thisA cargo:rerun-if-changed=icons directive in src-tauri/build.rs ensures the bundled binary picks up fresh icons on next tauri dev.
Cmd on macOS, Ctrl elsewhere.
| Shortcut | Action |
|---|---|
Cmd/Ctrl+T |
New tab in active project |
Cmd/Ctrl+W |
Close active tab |
Cmd/Ctrl+Shift+W |
Close active split panel |
Cmd/Ctrl+D |
Split vertical (new panel right) |
Cmd/Ctrl+Shift+D |
Split horizontal (new panel down) |
Cmd/Ctrl+Tab / Shift+Tab |
Next / previous tab in project |
Cmd+Shift+] / Cmd+Shift+[ |
Next / previous tab (macOS alias) |
Cmd/Ctrl+1..9 |
Jump to tab N (within project) |
Cmd/Ctrl+Alt+Arrows |
Move focus between split panels |
Cmd/Ctrl+B |
Hide / show sidebar |
Cmd/Ctrl+F |
Find in focused terminal |
Cmd/Ctrl+Shift+F |
Global search across terminals |
Cmd/Ctrl+, |
Open settings |
Cmd/Ctrl+Shift+P |
Open command palette |
In terminal:
| Shortcut | Action |
|---|---|
| Drag / double-click text | Select (auto-copies to clipboard) |
Cmd+C (macOS) |
Copy selection (manual) |
Cmd+V (macOS) |
Paste |
Ctrl+Shift+C (Linux/Win) |
Copy |
Ctrl+Shift+V (Linux/Win) |
Paste |
Cmd/Ctrl+click on URL |
Open link in browser |
Context menus:
- Right-click tab split panel → Split Left / Right / Up / Down
- Right-click project row → Rename / Change color / Open in file explorer / Remove
- Double-click tab title → Inline rename
Bottom row of the app, always visible. When a terminal is active:
zsh · ~/Projects/shellboard · ⎇ main +3 ●2 ?1 ↑4 v0.1.0
Git indicators (each hidden when 0, coloured for quick scanning):
| Symbol | Meaning | Color |
|---|---|---|
⎇ |
Branch name | blue |
+N |
Staged changes | green |
●N |
Modified (unstaged) | orange |
?N |
Untracked files | grey |
⚠N |
Merge conflicts | red |
↑N |
Commits ahead upstream | green |
↓N |
Commits behind upstream | red |
Hover any segment for a detailed tooltip. Git status polls every 5 seconds so commits / stages reflect without manual refresh, plus immediately on cwd change.
The app version (from tauri.conf.json) appears on the right, auto-pulled via @tauri-apps/api/app.
Single zustand store (src/store/appStore.ts). Per-terminal xterm instances live in <Terminal> components which stay mounted for the lifetime of their PTY — switching tabs toggles visibility: hidden, so xterm scrollback, focus and PTY stream survive. Layout changes (tabs, splits) are compositor-only: no flicker.
<App>
├── <Sidebar> project list, resizable, DnD reorder
│ └── <ProjectList>
│ └── <ProjectRow> inline rename / color picker / context menu
├── <main>
│ ├── <TabBar> per-project tabs, activity badge, broadcast icon
│ ├── <TerminalHost> all tabs mounted; only active is visible
│ │ └── <MosaicTab> react-mosaic-component split layout
│ │ └── <Terminal> xterm + search, web-links, OSC 7, broadcast
│ └── <StatusBar> shell · cwd · git status · version
├── <SettingsDialog> font + theme + cwd tracking
├── <CommandPalette> cmdk fuzzy searchable actions
├── <GlobalSearch> cross-terminal buffer search
├── <AboutDialog> logo, version
└── <AddProjectFlow> native folder picker → modal
PTY lifecycle is owned by the store, not by components:
addTab/splitPanel→ Rustspawn_pty→ new sessioncloseTab/closeActivePanel/handleTerminalExit→ Rustkill_pty<Terminal>attaches xterm, registers it interminalRegistry(for global search), wires events/invokes.
PtyManager—Mutex<HashMap<String, PtySession>>as Tauri managed state.PtySession— master PTY (forresize), writer (Box<dyn Write + Send>), child (Box<dyn Child + Send + Sync>).spawn_ptyopens a PTY vianative_pty_system(), spawns the default shell with optional OSC 7 rc injection, returns a UUID session id.- Reader loop runs on
tokio::task::spawn_blocking; emitspty://{id}/dataevents with UTF-8 chunks. On EOF it emitspty://{id}/exitso the frontend tears down the panel. - Git helpers (
git_branch,git_status) shell out to the systemgitbinary and parse--porcelain=v2output.
- Unix:
$SHELL(fallback/bin/sh). cwd defaults to project path or$HOMEwhen unknown. - Windows:
pwsh.exe→powershell.exe→cmd.exe(first onPATH).native_pty_system()picks ConPTY automatically.
When Track current directory is enabled in Settings, Shellboard writes a temporary shell init file in the OS temp dir and wires it into the spawned shell:
- zsh — custom
ZDOTDIRwith a.zshrcthat sources the user's, then hookschpwd_functions. - bash —
--rcfilepointing at a generated rc that sources~/.bashrc, then prepends an OSC 7 emitter intoPROMPT_COMMAND. - fish —
--init-commandwith a--on-variable PWDfunction.
Only affects newly-spawned terminals after the toggle is on. Other shells (dash, tcsh, etc.) fall through without injection.
| Command | Args | Returns |
|---|---|---|
spawn_pty |
cols: u16, rows: u16, cwd?: String, autoCwdTracking?: bool |
String (session id) |
write_to_pty |
id: String, data: String |
() |
resize_pty |
id: String, cols: u16, rows: u16 |
() |
kill_pty |
id: String |
() |
home_dir |
— | String |
git_branch |
path: String |
Option<String> |
git_status |
path: String |
Option<GitStatus> (see below) |
GitStatus shape:
{
branch: string | null;
ahead: number; behind: number;
staged: number; modified: number; untracked: number; conflicts: number;
}Events:
pty://{id}/data—{ data: string }(UTF-8 lossy-decoded chunks)pty://{id}/exit— empty payload when the shell ends
Two JSON files in the app config dir (macOS: ~/Library/Application Support/cz.petrhlozek.shellboard/).
shellboard.json — user-level config:
{
"projects": [{ "id": "...", "name": "...", "path": "...", "color": "#...", "createdAt": 0 }],
"sidebarWidth": 240,
"sidebarVisible": true,
"settings": {
"terminalFontFamily": "...",
"terminalFontSize": 13,
"uiFontSize": 12,
"terminalTheme": "default",
"autoCwdTracking": false
}
}session.json — per-run volatile state (saved debounced 500 ms after any change):
{
"version": 1,
"activeProjectId": "...",
"lastActiveTabByProject": { "projectId": "tabId" },
"tabsByProject": {
"projectId": [
{
"id": "tab-uuid",
"title": "project 1",
"customTitle": false,
"layout": {
"type": "split",
"direction": "row",
"splitPercentage": 50,
"first": { "type": "leaf", "cwd": "/path" },
"second": { "type": "leaf", "cwd": "/other" }
}
}
]
}
}shellboard/
├── .nvmrc
├── .github/workflows/build.yml # matrix build macOS / Linux / Windows
├── scripts/
│ └── make-macos-icon.mjs # wrap any logo in a macOS squircle
├── logo.png # source artwork
├── public/logo.png # served by Vite for the About dialog
├── src/ # React frontend
│ ├── App.tsx # layout, shortcuts, startup hydrate, theme vars
│ ├── main.tsx # ErrorBoundary
│ ├── store/appStore.ts # zustand: tabs, projects, settings, session
│ ├── utils/
│ │ ├── mosaic.ts # tree helpers for splits
│ │ ├── persistence.ts # typed wrappers over plugin-store
│ │ ├── sessionSerialize.ts # mosaic ↔ PersistedLayout
│ │ ├── themes.ts # theme presets (8)
│ │ └── terminalRegistry.ts # module-level xterm registry (for global search)
│ └── components/
│ ├── Terminal.tsx # xterm + search/web-links/OSC 7/broadcast/clipboard
│ ├── MosaicTab.tsx # split layout per tab
│ ├── TerminalHost.tsx # hosts all tabs, visibility-toggled
│ ├── TabBar.tsx # tabs, inline rename, activity/broadcast icons
│ ├── Sidebar.tsx # resizable project panel
│ ├── ProjectList.tsx # DnD, context menu, inline rename
│ ├── ProjectRow.tsx
│ ├── AddProjectFlow.tsx # folder picker + metadata modal
│ ├── ColorPicker.tsx # preset palette + custom hex
│ ├── SettingsDialog.tsx # font + theme + cwd tracking
│ ├── CommandPalette.tsx # cmdk + fuzzy search
│ ├── GlobalSearch.tsx # cross-terminal search modal
│ ├── StatusBar.tsx # shell · cwd · git · version
│ ├── AboutDialog.tsx # logo + version
│ ├── Modal.tsx # generic backdrop
│ ├── ContextMenu.tsx # floating menu (tabs, panels, projects)
│ └── ErrorBoundary.tsx
└── src-tauri/
├── Cargo.toml
├── build.rs # cargo:rerun-if-changed=icons + tauri_build::build()
├── tauri.conf.json
├── capabilities/default.json # core/event/store/dialog/opener/clipboard-manager
├── icons/ # generated per-platform bundle icons
└── src/
├── main.rs
├── lib.rs # plugins + manage(PtyManager) + commands
└── pty.rs # PtyManager + PTY commands + OSC 7 wiring + git helpers
- Shell output is decoded with
String::from_utf8_lossy; non-UTF-8 byte sequences become replacement chars. A future phase may switch to a binary-safe transport. - On Windows the OSC 7 auto-setup is a no-op; ConPTY exposes cwd through a different channel that isn't wired up yet.
- A restored tab lands with focus on the first panel regardless of which panel was focused at save time.
- Git status polls every 5 s; a commit made between polls appears with a slight delay.
- No in-app updater yet; install a new release manually.
