| Mode | Name | Size | |
|---|---|---|---|
| -rw-r--r-- | .gitignore | 123 | logstatsplainblame |
| -rw-r--r-- | DESIGN.md | 19218 | logstatsplainblame |
| -rw-r--r-- | Makefile | 2411 | logstatsplainblame |
| -rw-r--r-- | README.md | 7491 | logstatsplainblame |
| -rw-r--r-- | SEED.md | 1095 | logstatsplainblame |
| d--------- | config | 84 | logstatsplain |
| d--------- | src | 1022 | logstatsplain |
| d--------- | tests | 839 | logstatsplain |
| d--------- | vendor | 93 | logstatsplain |
sl0ppty
An opinionated terminal multiplexer in C, built on libghostty-vt.
It exists because driving LLM agent sessions wants a multiplexer whose chrome is programmable — a pane can draw its own status line and clickable buttons — and bending an existing one into that shape cost more than writing this did.
1 2:build +tab 5 panes
pi · ready────────────────────────────────────────────────────
pi · ready────────────────────────────────────────────────────
╭──────────────────────────── pi ──────────────────────────+─╮
│ │
│ │
╰ alt+N · ready · #7 ────────────────────[continue]─[explain]─╯
The bottom row is the pane's own: it printed it with an escape sequence, and a
click on [continue] is delivered to it on stdin.
Build
Needs zig 0.16 (for libghostty-vt, and as the C compiler) and nothing else — no cmake, no autotools, no libraries.
make vendor # build the vendored libghostty-vt (once, ~2 min)
make # 0.15s
make test # 250 checks, ~30s
make test-live # the ones that need a real tty
Use
sl0ppty # attach to session "main", creating it if needed
sl0ppty -s work # a named session
sl0ppty ls # live sessions
sl0ppty cmd '{"cmd":"panes"}'
| key | |
|---|---|
C-a \ / C-a - |
split into columns / rows |
C-a h j k l or arrows |
move focus |
C-a c / C-a n C-a p / C-a 1..9 |
new tab / cycle / select |
C-a f |
find a pane by title, purpose or tab |
C-a H J K L or shift+arrows |
move the boundary (the pane grows or shrinks accordingly) |
C-a PgUp C-a PgDn C-a Home C-a End |
scrollback (the wheel does it too) |
C-a x / C-a d / C-a q |
close pane / detach / quit |
C-a C-a |
send a literal C-a |
The mouse works throughout. Click a pane's border to split toward it — the
side you click is the side the new pane appears on, and hovering a border arms
it and shows a dashed line where the split would land. Click a pane to focus
it, a tab to switch, +tab to open one, a collapsed header to expand it, and a
pane's own buttons to talk to the program inside it. Drag a pane's title bar
onto another pane to swap them, or drag the gap between two panes to move the
boundary. Focus follows the pointer (focus_follows_mouse, on by default,
and it knows when not to).
Select to copy. Drag over text and release: it is on your clipboard, sent
to your terminal as OSC 52 so it works over ssh. Middle click pastes it back,
the way a primary selection behaves everywhere else. A program in a pane can
copy the same way (OSC 52) and can raise a notification (OSC 9), which appears
as a toast in the corner — as do {"cmd":"notify","text":"..."} and things
like "copied 13 chars".
Configuration is config/config.kdl, which documents
every setting by being the defaults. Copy it to ~/.config/sl0ppty/config.kdl.
Panes can draw their own chrome (OSC 5577)
printf '\033]5577;1;status;building 3/7\033\\'
printf '\033]5577;1;buttons;approve:Approve;cancel:Cancel\033\\'
# a click arrives on stdin as: \033]5577;1;click;approve\033\\
printf '\033]5577;1;clear\033\\'
Byte-compatible with the sl0ppi zellij fork, so
extensions written against that work here unmodified. A pane can also propose
its own purpose (\033]5577;1;purpose;agent:main\033\\) — but a purpose
declared by a layout outranks it and cannot be overridden, so a pane cannot
relabel itself into something tooling trusts.
Images
Kitty graphics pass through. A program in a pane transmits an image and it
appears, because libghostty-vt tracks the images and placements and sl0ppty
re-emits them to your terminal with ids remapped so two panes using image 7
stay two images. Placements follow the layout, are cropped at the pane's edges
by moving the source rectangle (asking for fewer cells would scale instead),
and are deleted when their pane closes or scrolls away.
This is the thing tmux and zellij drop, and the reason tools fall back to sixel under them. Sixel itself is not supported: libghostty-vt has no sixel decoder, so those sequences are swallowed.
Sessions as files
layout {
tab name="api" purpose="project:api.a1b2c3" cwd="~/dev/api" {
pane purpose="agent:main" command="pi"
pane split="rows" {
pane purpose="service:web" command="npm run dev" suspended=true
pane purpose="shell:scratch"
}
}
}
sl0ppty --layout session.kdl # build it
sl0ppty cmd '{"cmd":"apply-layout","path":"..."}' # add it to a live session
suspended panes exist, are laid out, and have run nothing — they start on
their first keystroke, because twelve projects should not be twelve running
dev servers. Purposes a layout declares are locked, so identity comes from the
file rather than from whatever the program inside prints. Full example with
commentary: config/layout.example.kdl.
Control API
One JSON object per line, on the session's unix socket. It is the same vocabulary the test harness drives, so nothing tested can drift from what the API does.
$ sl0ppty -s work cmd '{"cmd":"new-tab","name":"api","purpose":"project:api.a1b2"}'
{"ok":true,"id":2}
$ sl0ppty -s work cmd '{"cmd":"panes"}'
{"ok":true,"panes":[{"id":1,"x":2,"y":2,...,"purpose":"agent:main"}]}
panes tabs snapshot send raw resize split focus close new-tab close-tab
select-tab set-name set-purpose apply-layout reload alive quit. Panes are addressed by id, so a
background tab is scriptable, and a detached session answers exactly as a live
one does.
How it is built
See DESIGN.md for the reasoning and the decisions. The two that shape everything:
One geometry. Every painted interactive element records its (rect,
action) as it is painted, and a click is a lookup in that list. Drawing and
hit-testing cannot disagree, because there is only one of them.
Headless from day one. sl0ppty --script runs the whole compositor with no
tty: commands in, composited screen out as JSON — text, style runs, cursor,
hit list. Tests are "drive these events, assert this screen", and they take
milliseconds.
src/
input.c outer terminal bytes -> semantic events (kitty, mouse, paste)
pane.c a pty + a libghostty-vt terminal + its OSC 5577 chrome
app.c the layout tree, tabs, focus, chrome, what a key does
screen.c the compositor: cell buffer, diff, minimal ANSI out
server.c the session that outlives its client
client.c a terminal in raw mode and a socket
cmd.c one command vocabulary, shared by the driver and the socket
kdl.c the config parser
~5500 lines, no dependencies beyond libc and libghostty-vt, static binary.
