Anvil
Anvil is a solo-built native desktop terminal workspace. The idea is simple: one window, many projects, every project a canvas of tiled xterm.js terminals you can drag around, pin, and recall. The execution is the interesting bit. It ships as a single ~25 MB binary (Wails v2) that bundles a Svelte 5 (runes) frontend with a Go backend bound through Wails' IPC, talks to native ConPTY on Windows and PTY on Unix via go-pty, and persists user state to ~/.config/anvil/ with atomic writes.
The frontend leans hard on Svelte 5 runes, no Redux, no stores library, no UI framework. State lives in five `.svelte.ts` modules (projectsState, tabsState, canvasState, paletteState, keybindings store) and components subscribe via $derived. xterm.js handles the terminal raster. A minimap component over the canvas gives a bird's-eye view of every tile. Tabs are per-canvas, so each project carries its own working set. The command palette is a fuzzy-filtered union of action sources (project actions, tab actions, terminal actions, settings) and respects the same keybinding registry the UI shortcuts use. Defaults live in code, user overrides persist as diffs in settings.json so changing a default in a future release never requires migration. Drag-and-drop file targets are wired through Wails' CSSDropProperty so a folder dropped on the window becomes a project add.
The backend is one bound struct (`ipc.API`) that the frontend talks to via generated wailsjs bindings. PTY lifecycle is owned by `pty.Manager`: one Session per shell, each with its own read goroutine pumping a 32 KB buffer. The non-trivial piece is the output batcher. Emitting `EventsEmit` on every PTY read freezes the webview under load (`cat largefile`, `npm install`, anything chatty). The batcher coalesces bytes per session and flushes every 16 ms or 64 KB, whichever comes first. Keystrokes go back the other way base64-encoded over the same event bus because Wails marshals event payloads as JSON and binary keys (arrows, Alt-sequences) would otherwise mangle. Resize is debounced ≥50 ms because ConPTY drops bytes immediately after a SIGWINCH equivalent.
The project + settings store is a tiny on-disk JSON registry behind a `sync.RWMutex`, with atomic write-then-rename so a crash mid-write can't leave an empty file. Reorder is intentionally forgiving: a stale frontend reorder that omits ids keeps those projects at the end of the slice instead of dropping them. Settings carry an editor command, default shell, active project pin, and keybinding overrides. Shells are auto-detected by walking $PATH for pwsh, cmd, bash, zsh, fish, and nu, all of which show up automatically when installed.
What makes this portfolio-worthy isn't 'I built a terminal'. It's the wiring. Wails IPC, ConPTY quirks, base64 keystroke framing, the batcher choice, Svelte 5 runes used as actual app state instead of decoration, and the discipline of keeping the binary single-artifact and the persistence atomic.
One window. One binary. Three runtimes humming in lockstep.
Wails wraps a Svelte 5 (runes) frontend and a Go PTY backend into a single ~25 MB binary. The non-trivial piece is the seam between them: a coalesced event-bus that keeps the webview alive under high-throughput shell output.
The bits that earned the diagram
Wails marshals every EventsEmit through the webview's JSON channel. Naively
forwarding raw PTY bytes one read at a time freezes the renderer the moment a shell gets
chatty. cat largefile, npm install, a verbose test run, all of
them enough to wedge the UI.
Anvil's fix is a per-session outputBatcher that appends to a buffer, flushes on
either a 16 ms timer or a 64 KB threshold, and emits a
single base64-framed event per flush. Same bytes, ~60× fewer event boundaries, no jank.
Wails serializes IPC payloads as JSON. JSON has opinions about bytes that arrow keys, Alt
combinations, and bracketed-paste markers do not share. Frontend wraps every keystroke in base64 on the way down and the backend decodes before writing to the PTY. The
API accepts both std and url-safe encodings so the frontend can
pick whichever is convenient.
Resize is debounced ≥50 ms because ConPTY occasionally drops bytes immediately after a geometry change.
No store library, no Redux, no Pinia. Five .svelte.ts modules (projectsState,
tabsState, canvasState, paletteState, keybindings) own the entire client. Components
subscribe via $derived; the command palette and the keyboard shortcuts share
the same registry by reference, not by event.
The projects + settings store at ~/.config/anvil/ writes to a .tmp sibling and renames into place, so a crash mid-write cannot leave an
empty file behind. Reorder accepts a stale id list from the frontend without dropping
projects: anything the client forgot keeps its original position and is appended at the
end. Keybindings persist as diffs against defaults so shipping a new
default tomorrow never requires a migration script.
Frontend dist/ is embedded into the Go binary via //go:embed. PTY
abstraction comes from go-pty: ConPTY on Windows, native PTY on Unix, the
same Session API on both. Shells are auto-detected by walking $PATH so pwsh,
cmd, bash, zsh, fish, and nu light up the moment they're installed.