- Go 98.2%
- Go Template 1.2%
- Makefile 0.6%
|
|
||
|---|---|---|
| internal | Add quick chips | |
| .gitignore | polishing two pane view | |
| DESIGN.md | sprksh: docs + Phase 0/1/2 (instrumented PTY shell, bubbles, UI) | |
| go.mod | coloring, bat | |
| go.sum | polishing two pane view | |
| LICENSE.md | BSD 2 | |
| main.go | polishing two pane view | |
| Makefile | feat(ask): [?] ask an LLM about a bubble | |
| PLAN.md | feat(bubbles): sticky [e] edits, user notes, and [p] pasteboard path | |
| preliminary-plan.md | sprksh: docs + Phase 0/1/2 (instrumented PTY shell, bubbles, UI) | |
| README.md | feat(ui): drag-to-select and copy on-screen text in the shell pane | |
| STATUS.md | feat(ui): drag-to-select and copy on-screen text in the shell pane | |
sprksh — Sparkle Shell
A mouse- and keyboard-driven terminal wrapper that slices your shell session into structured, revisitable bubbles — one card per command — while still letting full-screen apps take over the terminal when they need to.
sprksh wraps a normal, persistent zsh session and makes it more legible.
Every batch command (ls, git status, make, ...) becomes a bubble: a card
showing the directory, the command, its output, and its exit code — that you can
scroll back to, re-run, cd into, or open in an editor/pager long after it
scrolled off. Full-screen apps (nvim, htop, less) are not emulated; they
transparently own the real terminal until they exit.
It's built in Go on Bubble Tea, and the author runs it as a daily driver.
Status: feature-complete for single-session daily use and currently in a QC
interval. See STATUS.md for the detailed checklist and
PLAN.md for the roadmap; DESIGN.md records the
original design rationale.
What it looks like
A side-by-side split: a live shell pane on the left, the bubbles pane on the right.
┌ shell ─────────────────────────┐┌ Bubbles [3-4 of 4] ───────────────┐
│ ~/dev/sprksh $ go build ./... ││┌ dir: ~/dev/sprksh ───────── ✓ 0 ┐ │
│ ~/dev/sprksh $ ▮ │││ $ go build ./... │ │
│ │││ (no output) │ │
│ ││└─────────────────────────────────┘ │
│ ││┌ dir: ~/dev/sprksh ───────── ✗ 1 ┐ │
│ │││ $ go test ./... │ │
│ │││ --- FAIL: TestFoo ... │ │
│ │││ [go] cd [R] rerun [x] pop ... │ │
│ ││└─────────────────────────────────┘ │
└─────────────────────────────────┘└─────────────────────────────────────┘
sprksh · focus:shell · ~/dev/sprksh · ^o switch · ^l resize · ^q quit · ...
The focused card shows its action row: [go] cd [R] rerun [x] pop [e] edit [b] bat [c] copy [p] path.
Quick start
Requirements:
- Go 1.26+ and zsh (the instrumented shell; bash is a later add).
- Optional per-action tools:
batfor[b](quick syntax-highlighted peek),$EDITOR(defaults tonvim) for[e], and a clipboard tool for[p](pbcopyon macOS;wl-copy/xclip/xselon Linux).
Build and run:
# GOPATH must point somewhere writable; this repo uses a git-ignored .gopath/.
export GOPATH=$PWD/.gopath
go run . # run in place
# — or —
go build -o sprksh . # produce a binary
./sprksh
To install it on your PATH (run outside any restricted sandbox):
GOBIN=$HOME/.local/bin GOPATH=$PWD/.gopath go install .
# ensure ~/.local/bin is on PATH
Keybindings & mouse
Global
| Key | Action |
|---|---|
ctrl+o |
Toggle focus between the shell and bubbles panes |
ctrl+l |
Enter split-resize mode (see below); shadows the shell's clear |
ctrl+q |
Quit sprksh (the wrapped session ends) |
When the shell pane is focused, all other keys go straight to the PTY, so your shell, ZLE, and key bindings behave normally.
Split-resize mode (after ctrl+l, a toast shows the keys for ~5s):
| Key | Action |
|---|---|
[ |
Shrink the shell pane |
] |
Grow the shell pane |
= |
Reset to a 50/50 split |
| any | Exit the mode |
Bubbles pane (when focused):
| Key | Action |
|---|---|
↑/k, ↓/j |
Move the selection |
g |
cd the shell into the bubble's directory |
R |
Re-run the bubble's command in the shell |
x |
Pop (remove) the bubble |
e |
Edit the persisted bubble in $EDITOR — trim output / add a note; saved edits stick |
b |
Page the bubble's full record through bat (read-only) |
c |
Copy the bubble's command into the shell prompt (edit + run yourself) |
p |
Flush to disk and copy the bubble's .bubble file path to the clipboard |
/ |
Start editing the filter footer (see below) |
Filter footer (always visible at the bottom of the bubbles pane):
| Key | Action |
|---|---|
/ |
Start editing (begins on the path: field) |
Tab |
Toggle between the path: and cmd: fields |
| type | Filter live — case-insensitive substring on cwd / command |
ctrl+u |
Clear the active field |
Esc/Enter |
Stop editing (the filters stay applied) |
Both fields combine with AND; an empty field matches everything.
Mouse
- Click either pane to focus it; click a bubble to select it.
- Click the
[go]/[R]/[x]/[e]/[b]/[c]/[p]labels on the focused card to trigger those actions directly. - Drag inside the shell pane to select a rectangular block of on-screen text; the highlight is clamped to the pane (blocked at the divider and borders), and releasing copies it to the clipboard — trailing whitespace trimmed, with a "selection copied" toast. This is how you grab visible output despite the live pane having no scrollback.
- Drag the vertical divider to resize the split live.
- Wheel over the bubbles pane to move the selection.
How it works (as built)
Persistent, instrumented shell
Each session is one long-lived zsh -i under a PTY — not a fresh
zsh -c "..." per command — so cd, export, aliases, functions, and activated
virtualenvs persist exactly as in a normal terminal.
To slice that continuous byte stream into per-command blocks without disturbing
the shell's statefulness, sprksh injects shell integration via a generated
ZDOTDIR: a temp .zshrc that first sources your real config, then appends
precmd/preexec hooks emitting the documented OSC 133 semantic-prompt
markers (A prompt-start, B input-start, C output-start, D;<exit>
finished) plus OSC 7 (cwd) and OSC 633;E (the exact command line). These
are the same markers iTerm2, WezTerm, and VS Code use. Every C ... D region
becomes a Block{cwd, cmd, output, exit} → a bubble.
Limitation: prompt frameworks that fully rewrite
PS1each render (e.g. Powerlevel10k) may drop the appendedBmarker;A/C/Dstill fire from the hooks, so blocks and exit codes stay correct.
Two classes of command
- Batch / streaming output is captured into a bubble. Rendering needs only light SGR (color/attribute) handling — never cursor addressing.
- Full-screen / interactive apps are detected by their alt-screen escape
(
ESC[?1049h) and escalated: sprksh suspends its UI, drops the real terminal into raw mode, and relays bytes both ways (forwardingSIGWINCH) until the app leaves the alt screen — then restores its UI. The app runs at native fidelity; sprksh is momentarily just a pipe. This works even for apps launched overssh, because the escape is in-band.
The live shell pane
The left pane is driven by a real VT emulator (vt10x) fed the OSC-stripped stream and kept the same size as the PTY. (This is for rendering the shell pane correctly — ZLE redraws, tab-completion menus — not for emulating full-screen apps, which still escalate.) The pane renders in color by walking the emulator's cells and emitting SGR spans, and shows a focus-aware cursor: a solid reverse-video block when the shell is focused, an underline when it isn't.
Bubbles that persist
Completed bubbles are written to an on-disk, content-addressed store at
$XDG_DATA_HOME/sprksh/bubbles (default ~/.local/share/sprksh/bubbles), one
plain-text file per bubble:
cwd: /abs/path
cmd: go build ./...
exit: 0
notes: {
optional free-form note, may span lines
}
<full, SGR-stripped output>
- The filename is a hash of the content address (
cwd + cmd + exit, plus the note when one is present — output is excluded), so re-running a command updates the one file for it with the most recent output and jumps its card to the most-recent position, rather than forking a new card. The pane stays 1:1 with the store. Adding a note is the deliberate exception: because a non-empty note folds into the address, it forks a new, uniquely-named record — the way you preserve a significant bubble long-term. Note-less bubbles keep their historic hash, so upgrading triggers no mass rename. - Notes are a free-form, multi-line annotation rendered on the card.
[e]opens the bubble with an emptynotes: { ... }scaffold; type inside the braces (or write a single barenotes: your textline — both parse). Thenotes:block is only written when non-empty, so note-less files stay byte-identical to before. - Files are plain text — auditable, grep-friendly, and reusable outside sprksh (a future "scriptlet" direction).
- Writes are deferred to quit. The pane makes no disk I/O per command;
instead it flushes the whole set once on quit (
ctrl+qor the shell exiting), writing the current bubbles and reconciling the store (popped bubbles' files are removed). This keeps mid-session disk churn at zero. The tradeoff: a hard kill (SIGKILL / closing the terminal) loses this session's bubble changes — accepted, since reducing churn is the point. - On launch the pane rehydrates from the store (deduping by content address, most-recent wins, so a pre-change store migrates cleanly on the next quit).
[e]opens the persisted bubble (in store format) in$EDITOR; on editor exit the file is re-parsed and the in-memory bubble replaced, so trimming output or editing a note sticks.[b]opens the same record read-only throughbat. Both are self-injected commands whose own blocks are dropped ([b]suppressed,[e]consumed by the read-back), and[go]cds are suppressed too — so peeking, editing, or navigating never creates a spurious bubble.[c]only types a command into the prompt, so what you run becomes a normal bubble.[p]flushes the pane to disk and copies the selected bubble's.bubblefile path to the system clipboard (best-effort; a transient toast confirms it). Run something, hit[p], and hand the path to a tool — or to Claude — to read the full record, output and note, straight off disk.
Rehydrated bubbles render monochrome (the store keeps plain text); the current session's live bubbles keep color until the next launch.
Layout
The as-built layout is a single shell pane beside the bubbles pane, with a
draggable / key-adjustable split ratio. The bubbles pane carries a two-line
filter footer (path / command, live substring). Multiple shell panes and
stack/T arrangements are deferred (see STATUS.md). Live-pane scrollback is
intentionally not pursued — the bubbles already hold each command's complete
output, so [e]/[b] cover "see what scrolled past."
Roadmap
The next substantial idea is remote / ssh-aware bubbles — parked as a study
cycle in Phase 6 of STATUS.md pending the QC interval. The insight:
because the marker protocol is in-band and transport-agnostic, a remote shell
that emits the same OSC 133 markers would produce bubbles over ssh with no
ssh-specific code; the real work is giving bubbles a host/session identity so
[go]/[R] route correctly.
Development notes
- Module:
github.com/nporcino/sprksh. Key deps:bubbletea,lipgloss,creack/pty,hinshun/vt10x,mattn/go-runewidth. - Build/test inside the safety-cage sandbox with a writable
GOPATH:GOPATH=$PWD/.gopath go build ./... && go vet ./.... - Most unit tests run in the sandbox. Tests that allocate a real PTY (e.g.
TestInstrumentedShellPersistsCwd) and all interactive keyboard/mouse checks must run in a real terminal outside the sandbox — see the "How to verify" section ofSTATUS.md.
Layout of the code
| Path | Responsibility |
|---|---|
main.go |
Entry point: start the shell, wire up the Bubble Tea program |
internal/shell/ |
Instrumented PTY zsh: ZDOTDIR generation, OSC scanner, block builder, alt-screen escalation |
internal/ansi/ |
Light SGR sanitize/strip (color only, no emulation) |
internal/bubble/ |
Bubble card view + the bubbles pane (selection, actions, hit-testing) |
internal/store/ |
Content-addressed on-disk bubble store (Format/Parse) |
internal/clip/ |
Best-effort system clipboard copy (the [p] action) |
internal/passthrough/ |
Raw terminal relay for full-screen escalation |
internal/ui/ |
Top-level model: layout, focus routing, live pane render, status bar |
License
Copyright 2026, Nick Porcino BSD 2-Clause