Persistent terminal workspaces, built on libghostty-vt.
Working name. Early, but usable: phase 1 works end to end.
Terminal emulators lose your work. Close the window and every process dies with
it. tmux solves that, but it also brings a hierarchy — workspaces, then windows,
then panes — that you have to navigate even when you never split a pane.
keep takes the other half of the trade: real persistence with a shape you
can hold in your head. A workspace is a project; a workspace has tabs. Both live
in the daemon, so closing the window loses neither.
A terminal that owns your processes cannot be the same thing as the window you
close. So keep splits them:
┌─ daemon ──────────────────────────────────┐
│ owns the PTYs, outlives every client │
│ libghostty-vt keeps each workspace's grid │
└───────────────────────────────────────────┘
▲ unix socket
┌───────────┴───────────┐
│ client (attach/detach)│
└───────────────────────┘
Because the daemon holds the screen state, a client that reconnects can be
repainted exactly as the workspace left it — libghostty-vt can emit the whole
screen back as VT sequences, colors and hyperlinks included.
The core is portable and knows nothing about UI. Each platform gets a real native client instead of a lowest-common-denominator one.
Phase 1 — daemon + terminal client. Working. The client runs inside an existing terminal and paints by writing VT sequences, so there is no renderer and no font handling to build.
-
keep-vt— safe bindings for libghostty-vt -
keep-proto— framed wire protocol -
keepd— PTY ownership, screen state, workspace registry, unix socket -
keep— attach, detach, repaint, input forwarding, flat picker - workspaces hold tabs; tabs persist with the workspace
Phase 2 — native macOS app. Working. One line of chrome: traffic lights,
a sidebar toggle and the tab row all share the titlebar (an approach adapted
from Ghostty's titlebar tabs). The workspace sidebar runs the full height of
the window Finder-style and the tab row starts at its edge — collapse it and
the tabs slide over to the toggle. Each tab is a real libghostty surface
(Metal rendering, your own Ghostty fonts and theme) running the keep client.
The tab row is drawn by the app rather than by AppKit. Native tabs are windows
sharing a tabbingIdentifier, which made every workspace switch a matter of
creating and closing windows — the source of every flicker, focus loss and
fight with a tiling window manager the app ever had. Switching is now a
visibility flip between mounted views, and ⌘1–⌘9, drag-to-reorder and the
close buttons are ours.
Windows are yours to ask for. ⌘⇧N opens another, ⌥⌘W closes one, a tab dragged clear of the row opens in one of its own, and nothing else in the app makes or unmakes a window. Each carries its own list of workspaces — a new one starts empty, and ⌘P reaches every workspace there is and brings it into that window. Two windows can show the same tab; it is the same shell with two viewers, the way two clients attach to one tmux session, and the daemon fits the tab to the smaller of them. Closing a window kills nothing; closing the last one quits, and everything keeps running in the daemon.
Splits are a tree (⌘D right, ⌘⇧D down) and the daemon hands a closed pane's children to its parent, so closing one pane never takes the arrangement apart. There is a picker (⌘P) over every workspace and tab, search (⌘F, ⌘⇧F), and keyboard motion between panes, workspaces and picker rows.
A picker row is also something you can act on. ⌘K, the chip in the corner and the right button all open one panel — go there, open a tab in that workspace, reveal it in Finder, copy its path, close it — because a right-click does not deserve a different menu from the one the keyboard gets.
⌘⇧P is the same overlay asking what you want done: the commands worth
typing three letters to reach, grouped by what they are for, and two lists
they open into. One is every Ghostty theme installed on the machine, each row
wearing a scrap of itself and previewed as a screen in it; the other is the
monospaced faces, each name set in its own face. What is chosen goes into a
config file Keep writes and loads after yours — yours is read, never edited —
and ghostty_app_update_config applies it live, so the terminal and the
chrome around it change colour together without a relaunch.
Tabs are labelled with the title the program inside sets (OSC 0/2), which shells and editors do on their own, so a tab says what it is without anyone naming it. A workspace also reports whether it is running something, taken from the terminal's foreground process group rather than from shell integration, so a build in a workspace nobody is watching still says so.
The same process group answers two more questions the picker needs, because a
title is not always a name: run the same tool in six tabs and six rows say the
same sentence. So the daemon also reports each tab's working directory — asked
of the kernel, so it needs no shell setup and follows a cd rather than
waiting for the next prompt — and when a byte last went either way through it.
The picker shows the directory relative to the workspace's own, which is short
enough to sit in a column: 777leads/api rather than ~/www/777leads/api.
One wrinkle worth knowing about: the libghostty build we link against accepts
the per-surface command, env_vars and initial_input fields and then
ignores them. Only working_directory survives, so the app fixes the client
app-wide and hands each surface its target through a file in a private
working directory, which the client reads and deletes.
A second, about the platform rather than about libghostty: Liquid Glass
refracts what the window server has behind the window. A terminal that asks
for background-opacity less than one makes the window see-through, and glass
laid over it comes back with the desktop in it — wallpaper and all — whatever
the app paints inside the window, and whether or not the window is held opaque.
So the overlays that float over the terminal are built out of a material, a
tint and a drawn rim instead of being handed to the system.
Another, this one by design: a tab shown in two windows answers a program's questions twice. When something asks the terminal where the cursor is or what it is, the daemon passes the question to every client and each replies on the one PTY. Mirroring a tab costs you that.
Not done yet: scrollback (the repaint covers the visible screen only), and workspaces do not survive a reboot.
Requires a Rust toolchain. On macOS, Zig is not needed — upstream ships a prebuilt universal xcframework:
./vendor/fetch.sh # downloads libghostty-vt (and builds the Linux slices if zig is present)
cargo build --releaseThe daemon and the client run on Linux — that half of Keep is the product there, the way tmux is. Upstream ships no prebuilt Linux library, so the build needs Zig (which also serves as the cross-linker, meaning the whole thing builds from a Mac with no container):
brew install zig cargo-zigbuild # or your distro's zig ≥ 0.16
tools/build-linux.sh # → dist/linux-{x86_64,aarch64}/keep, keepdOn a Linux machine, ./vendor/fetch.sh && cargo build --release does the
same natively. The macOS app does not port; on Linux you attach from any
terminal, which is what the client is for.
Then put target/release/keep and target/release/keepd on your PATH.
The client starts the daemon on demand; you never run keepd yourself.
keep # pick a workspace, or type a name to start one
keep myproject # attach to myproject, creating it if needed
keep ls # list workspaces
keep kill name # end oneInside a workspace, ctrl-\ detaches and leaves everything running.
libghostty-vt has no stable ABI yet and its only release tag is tip. The
vendored copy is pinned on purpose, and keep-vt asserts the C struct sizes it
was built against — if those tests fail after re-vendoring, read the headers
before changing the numbers.
MIT