Technical deep-dive
Splice is a native macOS desktop app built with a Rust backend and a Svelte 5 frontend, connected through Tauri's IPC bridge. This page covers the full system — from how terminal bytes become pixels to how Claude Code hooks work.
The Stack
Every user action flows down through UI components, reactive stores, and the Tauri IPC bridge before reaching the Rust backend. Real-time data — terminal frames, LSP diagnostics, file changes — flows back up as events.
Developer
Keyboard · Mouse · macOS menu bar · Context menus
App.svelte
Root. Global shortcuts, menu events, toasts, Claude attention notifications, session restore on mount.
PaneGrid + Panes
Renders the binary-tree split layout. Drag-to-resize handles. Each leaf is an editor or terminal pane.
CodeMirror Editor
File editor with syntax highlighting, LSP squiggles, completions, hover tooltips, and find-replace.
Canvas Terminal
Decodes binary frames from Rust and paints them to a canvas. Handles selection, search, URL clicks, keyboard.
Sidebar
File tree with inline rename/create, global text search (ripgrep), workspace switcher.
Overlays
Command palette (⌘K), SSH form, Send-to-Claude modal, toast notifications.
workspaceManager
The god store. Owns every workspace: open files, pane layouts, tabs, terminals, SSH config. All other stores are secondary.
layout
Binary tree of pane nodes. In-place mutation for resize; fresh tree for split/remove to avoid Svelte proxy issues.
ui & settings
UI flags (sidebar, zen mode, zoom) and user preferences (font, shell, theme). Settings debounced-saved to disk on change.
lspClient
Lazy language server startup. Deduplicates concurrent starts with a Promise cache. Tracks failures to avoid retry storms.
attentionStore
Holds Claude hook notifications keyed by terminal ID — permission needed, task finished, session started.
ipc/commands.ts
Every invoke() call, typed. Single source of truth for all command names. Groups: filesystem, terminal, workspace, LSP, SSH, settings.
ipc/events.ts
Typed subscriptions to backend push events. Each returns an unlisten(). Key events: terminal:grid, lsp:diagnostics, fs:change, attention:notify.
AppState
Single Mutex<AppState>. Holds all terminals, workspace state per window, LSP sessions, SSH sessions, and file watchers.
Terminal emulator
Full VT100/ANSI emulator. PTY bytes → VTE parser → cell grid → binary frame serialiser → Tauri event at ~120fps.
Filesystem
Read, write, create, rename, delete, search (ripgrep), file watching. Every path validated against allowed roots.
LSP proxy
One language server process per language. Routes JSON-RPC responses to callers; pushes diagnostics as Tauri events.
Attention server
Local HTTP server on a random port. Receives Claude hook POSTs, validates auth token, emits Tauri event to frontend.
SSH / SFTP
Persistent ControlMaster SSH session per workspace. File ops over SFTP. Terminals open separate SSH connections.
Shell (PTY)
bash, zsh, fish, or ssh — spawned in a pseudo-terminal. Validated against a hardcoded allowlist.
LSP servers
typescript-language-server, rust-analyzer, pyright — installed separately, spoken to over stdin/stdout JSON-RPC.
Filesystem
Local disk for project files. ~/.config/Splice/ for workspace state, settings, and Claude hook credentials.
Claude Code
Runs in a Splice terminal. Splice installs hooks in ~/.claude/settings.json so Claude can POST notifications back.
SSH remote host
Any SSH-accessible machine. Files via SFTP, shell via SSH. Identical experience to local workspaces.
Live Data Flow
Each layer in the stack. Particles animate along the edges showing real-time data flow direction — downward for invoke() requests, upward for events pushed back to the frontend. Hover a node to highlight its connections.
Data Flows
Step-by-step traces of the most important operations in Splice — from the moment you trigger an action to the moment you see the result.
Terminal rendering
Shell bytes → canvas pixels
This loop runs at up to ~120fps. The full path from shell output to screen is under 10ms on modern hardware.
Shell (PTY)
Writes ANSI escape sequences and UTF-8 text to the PTY master. E.g. ESC[32mHello ESC[0m means "green text, then reset".
Rust — VTE parser (term.rs)
VTE calls GridPerformer for each byte sequence. Prints characters, moves cursor, applies colour/style attributes to the cell grid.
Rust — emitter.rs (~120fps)
Rate-limited thread wakes every ~8ms. If the grid changed, serialises it to a compact binary frame and base64-encodes it.
Header (20 bytes): cols, rows, cursor pos/style, mode flags, scrollback length, first visible row Cells (12 bytes each): codepoint u32, fg RGB, bg RGB, style flags, char width
Tauri event: terminal:grid:<id>
Frame emitted to the webview as a base64 string, targeted to the specific terminal ID.
renderer.ts — dirty-rect diff
Decodes base64 → Uint8Array. Compares each cell to the previous frame. Only repaints cells that changed — a huge win when most of the screen is static.
CanvasTerminal.svelte — canvas 2D
Paints changed cells using a colour string cache and ASCII char cache. Handles cursor blink, selection overlay, search highlights, scrollbar.
File save
⌘S → bytes on disk
For SSH workspaces the IPC command changes to sftp_write_file but everything else is identical.
App.svelte — global keydown
⌘S fires the "save" action. Calls workspaceManager.saveActiveFile().
workspace-file-ops.ts
Resolves active workspace → active pane → active file path → OpenFile object. Reads file.content (kept in sync by CodeMirror on every keystroke). Marks file.dirty = false optimistically.
invoke("write_file") or invoke("sftp_write_file")
Local: write_file(path, content). SSH remote: sftp_write_file(workspaceId, path, content).
Rust — fs/write.rs
Validates path is within allowed roots. Writes to disk. The file watcher fires an fs:change event — the frontend ignores it if the content matches what was just saved.
LSP: diagnostics & completions
Error squiggles and autocomplete
Language servers are started lazily — only when you open the first file of a given language.
CodeMirrorEditor.svelte
On file open, calls lspClient.ensureReady(filePath, content, wsRoot).
lspClient — lazy start + dedup
Maps file extension → language ID. Calls lsp_start if not running. A startPromises map ensures concurrent opens for the same language only start the server once.
Rust — lsp/mod.rs
Spawns e.g. typescript-language-server --stdio. Writer task → stdin, reader task parses stdout JSON-RPC:
Response (has id) → wake the invoke() caller Notification (push) → emit as lsp:diagnostics event Server request (both) → answer inline
Tauri event: lsp:diagnostics
Frontend updates diagnosticsStore. CodeMirror's linter extension re-renders squiggles.
lsp-extensions.ts — CodeMirror
Completions via buildLspCompletionSource() calling textDocument/completion. Hover tooltips via buildHoverExtension().
Claude attention hook
Claude → Splice notifications
The key insight: Claude reads SPLICE_TERMINAL_ID from its environment — no PID tree walking needed.
Rust — on app startup
Generates or loads a 128-bit auth token. Binds a random TCP port; writes it to ~/.config/Splice/.attention_port. Installs versioned Python hook scripts into ~/.claude/settings.json.
Rust — PTY spawn
Every terminal gets SPLICE_TERMINAL_ID=<n> injected into its shell environment. This propagates to all child processes, including Claude.
Claude Code — hook fires
The installed Python script reads SPLICE_TERMINAL_ID from its env and the port from the file. POSTs to Splice's local HTTP server:
POST http://127.0.0.1:<port>/attention
Authorization: Bearer <token>
{ "terminal_id": 3, "type": "permission" }
{ "terminal_id": 3, "type": "idle" }
Tauri event: attention:notify
Rust validates the token and emits the payload to the frontend.
attentionStore + terminal tab UI
Stored by terminal ID. Terminal tab gets a badge. Permission requests show inline allow/deny. Idle shows a "done" indicator.
Session restore
Relaunch → exactly where you left off
Same layout, same files, same terminals. Claude sessions resume automatically once the shell is ready.
App.svelte — onMount
Calls workspaceManager.restoreWorkspaceImpl().
invoke("get_workspaces")
Rust reads the per-window JSON: workspaces.json for main, workspaces-{label}.json for secondary windows.
workspace-session.ts — deserialise
Remaps snake_case → camelCase. Reconstructs layout tree, pane configs, open file list. Reads each file from disk, detecting external edits.
invoke("spawn_terminal") per terminal
Respawns each saved terminal. waitForShellReady() polls terminal frames until cursor position is stable for 3 consecutive frames — the shell prompt has appeared. Then sends claude --resume <session_id>.
UI renders
Full layout restored: pinned tabs, split panes, expanded file tree folders, active pane, Claude sessions resuming.
SSH remote workspace
Edit files and run terminals on a remote machine
Identical experience to a local workspace from the user's perspective — same canvas terminal, same editor, same file tree.
SshConnectForm.svelte
User enters host/user/port/key/remotePath. Calls invoke("ssh_connect").
Rust — ssh.rs
Opens a persistent ControlMaster SSH session via the openssh crate. Stored in AppState::ssh_sessions[workspace_id]. All SFTP operations reuse this one TCP connection.
File ops via SFTP
sftp_list_dir runs a remote find command. sftp_read_file and sftp_write_file transfer full file contents over the existing SSH connection.
Terminal via SSH binary
spawn_terminal is called with extra args built from the SSH config. The shell is /usr/bin/ssh (on the allowed list). Same PTY + canvas renderer as local.
"cd '/remote/path' && exec $SHELL -l"