Working title
Find a file
2026-06-15 10:12:51 +02:00
notes Frames mode: i3's borders as shared box-drawing frames (border = "frame") 2026-06-12 18:04:11 +02:00
scripts Frames mode: i3's borders as shared box-drawing frames (border = "frame") 2026-06-12 18:04:11 +02:00
src Fix window movement behavior 2026-06-15 10:12:51 +02:00
.gitignore Untrack machine-local Claude settings 2026-06-12 12:34:20 +02:00
Cargo.lock M3 phase 2: client-server split — sessions survive terminal close 2026-06-12 15:09:12 +02:00
Cargo.toml M3 phase 2: client-server split — sessions survive terminal close 2026-06-12 15:09:12 +02:00
CLAUDE.md Start a development diary in notes/diary.md 2026-06-12 15:16:41 +02:00
LICENSE-APACHE License under MIT OR Apache-2.0 2026-06-12 13:38:38 +02:00
LICENSE-MIT License under MIT OR Apache-2.0 2026-06-12 13:38:38 +02:00
Makefile Add make install / uninstall (PREFIX, default ~/.local) 2026-06-12 15:47:20 +02:00
PLAN.md M3: layout save/restore — pando save / pando restore 2026-06-12 16:12:11 +02:00
README.md Frames mode: i3's borders as shared box-drawing frames (border = "frame") 2026-06-12 18:04:11 +02:00

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 clientserver: 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, move with 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 + vt100 screen 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.