Skip to content

Repository files navigation

hexe

A session-based terminal workspace where the frontend is disposable and your shells are not.

Crash the terminal frontend, restart it, reattach, and your terminals keep running exactly where you left them.


How it works

hexe splits into four layers:

  • hexe terminal — the terminal UI frontend (aliases: hexe mux, hexe multiplexer).
  • shared frontend runtime — attach lifecycle, transport, and the frontend-side session projection.
  • hexe session / hexe ses — the session authority that owns canonical session state.
  • hexe pod — one per pane. Owns the PTY, holds the shell, buffers output even while detached.

See architecture for the full picture.


Palette namespaces

A program can claim its own 256-colour table — one of 32 numbered slots — for the output it writes. Recolour that table and only its cells change — the rest of the pane stays exactly as it was, on screen and in scrollback, with no redraw from the application.

hexe palette set --ns 4 33=#ff00aa bg=#1a1020
hexe palette get                                    # what is actually set

An application drives it directly, with nothing to negotiate first — claim a slot, print, release it:

printf '\033]1330;set;4;33=#ff00aa\033\\'
printf '\033]1330;use;4\033\\'
printf 'this line resolves colour 33 through slot 4\n'
printf '\033]1330;end\033\\'

Slot 0 is what unclaimed output resolves against, slot 1 is hexe's own chrome — borders, status bar, float titles — and slots 2–31 are yours.

hexe holds the colours and resolves the indexes; it never decides which cells belong to which slot. Every cell records the slot that was current when it was written — the number itself, so there is no mapping to lose — and two slots are correct on screen at once, with a repaint reaching scrollback. Setting slot 0 recolours the ordinary palette; setting slot 1 recolours hexe's own furniture. Anything hexe does not recognise — an unknown name, a program that claims nothing, another terminal entirely — falls back to your own palette, so a default install looks exactly as it did before.

See the palette protocol for the sequences to emit.


Anything can drive it

Everything hexe knows about its panes, floats, tabs and session has one definition — the live Lua API — and a socket hands that same API to any program that can open one. There is no second list of fields to drift out of step with the first, and nothing has to scrape formatted output for facts hexe holds exactly.

hexe api panes                      # every pane, as JSON
hexe api count '"panes"'
hexe api act '{"type":"split.v"}'
hexe api send '"904dd85…"' '"make test\n"'

hexe api is a thin client; the socket is the interface. Frames are a 4-byte big-endian length and a JSON body, so a gateway, a phone client behind one, or a shell script are all the same amount of work. Instead of polling, a client can subscribe and be told.

Three doors, each with its own authority, because a grant a caller can decline is not a grant:

socket who holds it may do
api@<session>.sock you everything
plug@<session>.<name>.sock one plugin only what it declared
pane@<uuid>.sock whatever runs in that pane read and type into that pane

A plugin is a package that declares what it needs — stream, typing, keyboard, popup — and is handed a socket carrying exactly that; a verb it did not ask for is refused by name. A pane's socket is exported to its shell as $HEXE_PANE_API_SOCKET, so a program in a pane finally has something it can safely be given: a session-wide call is refused, and a selector naming another pane resolves to nothing rather than to the caller's own.

For another Lua — a shell, an editor, a sibling tool — hexe lua-api prints a plain-Lua client, and the client verb returns the same source over the socket for a host that cannot shell out:

local mux = hexe.connect()            -- or connect_pane(), inside a pane
for _, pane in ipairs(mux.panes()) do print(pane.name, pane.cwd) end

See the control socket, access and plugins.


Docs

One document per feature in docs/, each opening with a recording of it running: how it works, how it differs from tmux, what it cannot do, and where it lives in the tree. Every claim in them was checked against the source or a running build, and the recordings are made by hexe itself — see recording.

Four processes frontend, runtime, SES, pod — and why only one is disposable
Sessions detach, reattach, replay, adoption, and what is written down
Pods one daemon per pane: the PTY, the backlog ring, exactly-once input
Instances two whole stacks on one machine that cannot see each other
Panes and tabs the split tree, geometric focus, zoom, broadcast, select-and-swap
Floats overlay panes with lifetimes: per-directory, sticky, exclusive, sandboxed
Keybindings no prefix: chords, conditions, and what happens to the key afterwards
Reading what happened scrollback search, copy-mode, OSC 133 prompt marks
Overlays and popups notifications, questions, pickers, keycast, pane labels
Painting the bar, titles, sprites and popups are drawn by an external painter
Shell integration what a shell reports to the mux, and how the prompt is drawn
Palette protocol a program claims its own 256-colour table for the output it writes
Configuration one Lua file, a schema that refuses typos, reload without losing panes
Project sessions .hexe.lua, freezing a session, and the trust ledger
Isolation namespaces and cgroups per pane — and what it needs from the kernel
The command line addressing sessions, panes and pods from a script
The control socket the live API over a socket: what any program can ask and do
Access what a helper may do — stream, typing, keyboard, popup — declared and enforced
Plugins install, declare, approve, remove — a package, not a command string
Streaming a pane a pane's bytes, who is watching, and how to cut them off
Dictation speech to text as a tool hexe drives, and the sign that a mic is open
Names how panes and sessions get names, and what a name is allowed to be
Decorations borders, titles and what a pane is allowed to draw around itself
Recording hexe writes asciicasts of itself; every film in the docs was made that way

Quick start

Build. Needs Zig 0.15.2. A static musl binary:

scripts/vendor-ghostty.sh                                  # once: fetch + patch ghostty-vt
scripts/vendor-yazap.sh                                    # once: fetch + patch yazap
zig build -Doptimize=ReleaseFast -Dstrip=true -Dtarget=x86_64-linux-musl

That is the whole build, and it is what CI runs — CI has no oslo, so the pins live in the scripts rather than in a recipe. With oslo the recipes in .make.lua wrap them. There is no Makefile; the targets come from oslo:

oslo make vendor    # fetch the pinned dependencies and apply hexe's patches
oslo make build     # the build above, then size and which hexe is on $PATH
oslo make install   # …and copy it everywhere hexe already is
oslo make test      # the Zig unit tests
oslo make configs   # install config/ into ~/.config/hexe, then check it loads
oslo make smoke     # the live end-to-end suite
oslo make           # every target, with a line each

Build with oslo make build rather than a bare zig build: the default is a Debug binary, which is slow enough to look like a bug.

Run:

hexe terminal new             # a new session, named after a pokemon
hexe terminal new --name work # or named by you
hexe                          # bare: attach to a session rooted here, or load ./.hexe.lua

Detach and come back. Detach is a keybinding, so it is whatever your config says — there is no built-in chord:

hexe.key({ hexe.key.ctrl, hexe.key.alt, hexe.key.d }, hexe.action.detach())
hexe session list                    # what is running, attached or not
hexe terminal attach work            # by name, or by uuid prefix

Config lives at ~/.config/hexe/init.lua and is Lua. See configuration, and keybindings for the binding language.


History

Started as bash and Python hacks wrapped around tmux. Absolutely cursed code. Shell scripts spawning tmux sessions, Python daemons talking to tmux through send-keys, config files that were basically more shell scripts. It was wild. But it worked, and it was the workflow I wanted.

Rewrote it properly in Rust on top of tmux-rs, got far, learned a lot about terminal internals. But that crate is mostly unsafe and you're still building on top of tmux's architecture rather than escaping it.

Then Ghostty came out. Saw what Mitchell was doing with Zig and decided to start from scratch. Zero regrets. Zig is a joy, Ghostty's VT implementation is solid, and the architecture finally matches what I actually wanted to build.


Credits

About

terminal multiplexer based on libghostty

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages