| notes | ||
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| Makefile | ||
| PLAN.md | ||
| README.md | ||
pando — an i3-style tree-tiling terminal multiplexer
A terminal multiplexer whose core is i3's container tree, not a layout
engine. Every workspace is a tree: internal nodes are containers with a
layout (splith, splitv, tabbed, stacked) and an ordered list of
children; leaves are terminal panes. Splits nest arbitrarily, windows
break in and out of containers when moved, and every container remembers
its focus stack — exactly the i3 model.
Build & run
make install # to ~/.local/bin (PREFIX=/usr/local for system-wide)
pando # attach to session "main", starting it if needed
After upgrading, running servers keep executing the old binary until you
quit or pando kill them; a mismatched client fails with a clear protocol
version error rather than misbehaving.
Sessions (detach / attach)
pando is client–server: the session (shells, layout, scrollback) lives in a background server and survives your terminal closing.
pando # attach to "main", autostarting the server
pando -s work # ... a named session
pando attach -s work # attach only if it exists
Alt+Shift+d detaches; closing the last shell (or Alt+Shift+e) ends the
session. Kill the terminal, SSH in later, pando attach — everything is
still there. Sockets live in $XDG_RUNTIME_DIR/pando/. One client per
session: attaching again replaces the old client. --in-process runs the
old single-process mode (no detach).
Scripting (IPC)
Every session socket also takes commands and publishes events — the same command language as keybindings:
pando ls # list sessions (attached/detached)
pando msg -s work split v # run any command in a session
pando msg -s work workspace 3
pando dump -s work | jq . # the container tree as JSON
pando sub -s work # stream events: window-new/close, focus,
# workspace, attach, detach
pando new -s scratch # create + attach (error if it exists)
pando kill -s scratch # end a session
pando save -s work > work.json # save the layout (splits, layouts, sizes)
pando restore work.json -s work # fresh session rebuilt from it (new shells)
sub + msg is the extension story: a shell script that reacts to events
and issues commands can do most of what a plugin system would.
Keys (Alt is $mod, Ctrl+b is the leader)
The philosophy: i3 owns space, tmux owns sessions. Spatial operations
(focus, move, split, layouts, workspaces) are direct Alt chords — no
prefix, i3-fast. Session lifecycle and rare/dangerous operations live
behind a tmux-style leader (Ctrl+b, shown as [^B] in the status bar
while pending): leader d detach · leader [ copy mode · leader q end
session. Press the leader twice to send it through to the shell; Esc
cancels. Configurable (leader = "alt+space" or "none", plus a
[leader] binding table).
| Key | Action |
|---|---|
Alt+Enter |
new terminal (sibling after focused, like i3) |
Alt+h/j/k/l |
focus left/down/up/right |
Alt+Shift+h/j/k/l |
move window (swap, enter neighbor container, or break out at edges) |
Alt+b / Alt+v |
split horizontal / vertical |
Alt+w / Alt+s |
layout tabbed / stacking |
Alt+e |
layout toggle split (H↔V) |
Alt+a / Alt+Shift+a |
focus parent / focus child — operate on whole containers |
Alt+f |
fullscreen toggle (window or focused container) |
Alt+Ctrl+h/j/k/l |
resize (shrink width / grow height / shrink height / grow width) |
Alt+1..9 |
switch workspace |
Alt+Shift+1..9 |
move window to workspace |
Alt+Shift+q |
kill focused window or container |
Alt+Shift+d / Ctrl+b d |
detach (session keeps running) |
Alt+c |
copy mode (scrollback + selection) |
Alt+Shift+e |
quit |
Copy mode (10 000 lines of scrollback per pane, vi keys like tmux's
copy-mode-vi): h/j/k/l/arrows move, Ctrl+u/Ctrl+d half-page,
PageUp/PageDown page, g/G top/bottom, 0/$ line start/end,
v start selection, y/Enter yank (y with no selection takes the cursor line), q/Esc exit. Yanks go to the
system clipboard via OSC 52 through your outer terminal — works over SSH.
Alt bindings still work while in copy mode, so you can switch panes
mid-scroll. Pasting into pando honours bracketed paste per pane.
Mouse: click focuses panes, title bars, tabs and stack headers; click a
workspace chip in the status bar to switch; drag the │ separator (or a
title bar in a vertical split) to resize; wheel up over a shell scrolls
back (enters copy mode), wheel down past the bottom leaves it. Drag over
a shell pane to select text — releasing copies it to the system clipboard
(OSC 52), and a click repositions the copy-mode cursor. Apps that request
mouse reporting (vim, htop) get pane-local SGR/X10 events passed through
instead; use copy mode (Alt+c) to select from those.
The focused window's decoration shows a colored arrow at its right end —
i3's split indicator: → means the next window opens to the right
(splith, or a new tab), ↓ below (splitv, or a new stack entry). It
flips immediately on Alt+b/Alt+v, so a pending split is visible
before anything opens.
With a container focused (Alt+a), split/layout/move/kill/fullscreen act on
the whole container; typing still goes to the active window inside it, and
the container highlights as one unit. Directional focus wraps at workspace
edges, like i3's focus_wrapping yes.
Anything else is forwarded to the focused shell, including unbound
Alt+<key> combinations (sent as ESC-prefixed Meta for emacs et al.).
Non-US keyboard layouts: Alt+Shift+digit arrives as a layout-dependent
symbol in legacy terminal encodings. Set PANDO_SHIFTED_DIGITS to your
layout's shifted digit row, e.g. for Norwegian:
set -x PANDO_SHIFTED_DIGITS '!"#¤%&/()'
Configuration
~/.config/pando/config.toml (or $XDG_CONFIG_HOME/pando/config.toml),
all optional:
scrollback = 5000 # lines per pane (default 10000)
clipboard = "wl-copy" # external command fed the yank on stdin;
# default "osc52" (through the outer
# terminal — VTE terminals drop it)
shifted_digits = "!\"#¤%&/()" # what Shift+1..9 type on your layout
workspaces = ["dev", "web", "ops"]
status_position = "bottom" # status bar location; default "top"
border = "frame" # window decoration: "title" (default) =
# 1-row title bars; "none" = bare │/─ lanes,
# split indicator in the status bar;
# "frame" = shared box-drawing frames,
# titles in the top edge, focused frame
# highlighted, and the edge where the next
# window opens in the indicator color (i3)
set_title = true # report the focused window's title to the
# outer terminal (OSC 2; default false)
[colors] # any of: focused_fg/bg, inactive_fg/bg,
focused_bg = "#285577" # unfocused_fg/bg, separator, bar_fg/bg,
bar_active_bg = "#285577" # bar_active_fg/bg, indicator —
indicator = "#2e9ef4" # "#rrggbb" or "default"
leader = "ctrl+b" # or "alt+space", or "none" to disable
[leader] # keys after the leader (bare keys work
"s" = "workspace 9" # here, even "[")
[keys] # add or override bindings; commands are
"alt+n" = "new" # the same language the future IPC speaks:
"alt+t" = "layout tabbed" # new/kill/quit/fullscreen/copy-mode,
"alt+shift+x" = "kill" # focus <dir|parent|child>, move <dir>,
# split <h|v>, layout <tabbed|stacking|
# splith|splitv|toggle>, workspace <1-9>,
# move workspace <1-9>,
# resize <grow|shrink> <width|height>
A config error aborts startup with the offending line; pando never starts half-configured.
macOS note: in your terminal emulator, enable "Use Option as Meta/Alt" (Terminal.app: Profiles → Keyboard; iTerm2: Profiles → Keys → Left Option = Esc+; kitty:
macos_option_as_alt yes; ghostty:macos-option-as-alt = true), otherwise Option types special characters instead of reaching pando.
Architecture
src/tree.rs— the i3 container tree: arena of nodes by id, split containers with per-container focus indices, and the i3 operations (split, directional focus,movewith break-out and workspace re-orientation, tree flattening on close). Pure data structure, fully unit-tested, no terminal dependencies.src/pane.rs— one PTY + child shell +vt100screen per leaf; a reader thread pumps bytes to the main loop, which owns the parser.src/render.rs— recursive geometry from the tree (title bars for windows in plain splits, tab rows for tabbed, title stacks for stacked), composited into a cell buffer and diffed against the previous frame.src/input.rs— Alt-modifier keybindings → actions; everything else is re-encoded into the bytes a terminal would send and forwarded to the focused PTY.src/main.rs— event loop: one channel fed by the input thread and all PTY reader threads; coalesces bursts into single frames, keeps PTY sizes in sync with the computed content rects.
Not yet implemented (roadmap)
- Layout save/restore; hooks running commands on events.