Splice Splice

Technical deep-dive

How Splice works

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.

Rust + Tauri v2 Svelte 5 runes TypeScript + Vite CodeMirror 6 Tailwind CSS

The Stack

Five layers, one window

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.

User

Developer

Keyboard · Mouse · macOS menu bar · Context menus

renders via Svelte 5 components
Components

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.

reads & mutates via Svelte 5 $state runes — any component reading a property auto-rerenders on change
Stores

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.

invoke() for requests (Promise) · events for real-time pushes (terminal frames at ~120fps, diagnostics, file changes)
IPC Bridge

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.

Tauri IPC — JSON for commands · base64-encoded binary for terminal frames
Rust Backend

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.

syscalls · PTY · TCP/IP · disk I/O · JSON-RPC
External

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

How data moves through Splice

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

How key operations work end-to-end

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.

1

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".

2

Rust — VTE parser (term.rs)

VTE calls GridPerformer for each byte sequence. Prints characters, moves cursor, applies colour/style attributes to the cell grid.

3

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
4

Tauri event: terminal:grid:<id>

Frame emitted to the webview as a base64 string, targeted to the specific terminal ID.

5

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.

6

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.

1

App.svelte — global keydown

⌘S fires the "save" action. Calls workspaceManager.saveActiveFile().

2

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.

3

invoke("write_file") or invoke("sftp_write_file")

Local: write_file(path, content). SSH remote: sftp_write_file(workspaceId, path, content).

4

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.

1

CodeMirrorEditor.svelte

On file open, calls lspClient.ensureReady(filePath, content, wsRoot).

2

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.

3

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
4

Tauri event: lsp:diagnostics

Frontend updates diagnosticsStore. CodeMirror's linter extension re-renders squiggles.

5

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.

1

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.

2

Rust — PTY spawn

Every terminal gets SPLICE_TERMINAL_ID=<n> injected into its shell environment. This propagates to all child processes, including Claude.

3

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" }
4

Tauri event: attention:notify

Rust validates the token and emits the payload to the frontend.

5

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.

1

App.svelte — onMount

Calls workspaceManager.restoreWorkspaceImpl().

2

invoke("get_workspaces")

Rust reads the per-window JSON: workspaces.json for main, workspaces-{label}.json for secondary windows.

3

workspace-session.ts — deserialise

Remaps snake_case → camelCase. Reconstructs layout tree, pane configs, open file list. Reads each file from disk, detecting external edits.

4

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>.

5

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.

1

SshConnectForm.svelte

User enters host/user/port/key/remotePath. Calls invoke("ssh_connect").

2

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.

3

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.

4

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"