A small tmux-like multiplexer: windows, splittable panes, a detachable daemon, mouse support, and a lock mode for when a nested program wants the prefix key.
devbox run start # or: devbox run build && ./tiletile attaches to the running session, starting the daemon if there isn't one.
| Command | What it does |
|---|---|
tile / tile attach |
attach, spawning the daemon if needed |
tile kill-server |
stop the daemon and every shell in it |
tile ls |
list running sessions |
tile help |
the full command list, no daemon required |
Every command takes -t name to act on a session other than the default —
tile -t work attaches to (or starts) a session named "work", independent of
any other session's windows and panes.
There is also a scripting interface for driving a session from outside it — enumerating panes, reading what they printed, and typing into them — which is how a script or an AI agent works with tile.
The prefix is Ctrl+B. Press it, then:
| Key | Action |
|---|---|
] [ / 0–9 |
next / previous window / select by number |
a |
new pane, auto-splitting whichever axis has more room |
| ` | /-` |
z |
zoom the active pane to fill the window, or restore it |
f |
toggle a floating terminal in the center of the window |
o / arrows |
cycle panes / move to the pane in that direction |
| arrows, at a dead end in a stack | switch which stacked layer shows |
Ctrl+arrow / Alt+arrow |
resize the active pane by 1 / by 5 |
w c / & |
new window / kill the active window |
w r |
rename the active window's tab |
p x |
kill the active pane |
p r |
rename the active pane |
p p |
open the floating pane picker |
p s |
stack a pane behind the active one |
s n |
create a new session and switch to it |
s x |
delete this session and every shell in it, after confirming |
s r |
rename this session |
s o |
close every other running session |
s p |
open a picker to switch to another running session |
S |
save the current layout to a named preset |
L |
load a saved preset |
T |
open the colorscheme picker |
R |
reload config.yaml (keymap, theme, margin) |
d |
detach, leaving everything running |
q |
quit and stop the daemon, after confirming |
Ctrl+B |
send a literal Ctrl+B to the shell |
Holding the prefix pops up a which-key-style tooltip in the bottom-right
corner listing every binding, read live from config.yaml.
F12 toggles lock mode at any time, with or without the prefix. While locked every key — the prefix included — goes straight through to the program in the pane, so a nested tile, tmux, or anything else bound to Ctrl+B keeps working. F12 is the only key tile never forwards.
w and p are sub-layers: press either and the which-key tooltip switches
to a second list — w opens c new, & kill, r rename for windows; p
opens r rename, x kill, p pane picker for panes — instead of running
on its own, the same way the prefix works, one level deeper; the top-level
tooltip shows a single windows…/panes… row for each rather than listing
their actions individually. Every key above (except arrows, digits and the
Ctrl+B passthrough, which are structural) is remappable — see
Configuration.
Every pane is drawn in its own titled border, the title taken from the
shell's own title (or whatever program is running). The window tab shown
at the top follows that same title, unless the window itself was renamed
with w r — see Renaming windows. The active
pane's border is highlighted in the theme's accent color; every other
pane's is dimmed. The first command you run in a pane renames its border
to that command line — truncated with … if it's too long — overriding
whatever the shell called it, and later commands leave that name alone.
p r renames a pane's border directly instead; see
Renaming panes.
A split (| / -) divides the screen; both panes stay visible side by
side. A stack (p s) does not divide anything — the new pane shares the
exact same rect as the one it was stacked behind, like a new layer in an
image editor. Only one layer runs full-speed rendering at a time, but every
other layer still leaves a one-row title bar on screen, stacked above or
below the active layer in the order it was added — zellij's compact stacked
look. Click a layer's title bar, or press an arrow key that has nowhere
spatial to go (a stack has no side-by-side neighbours), to bring it to the
front. Closing a layer (p x) falls back to the one below it. The status bar
shows layer 2/3 whenever the active pane is part of a stack, so it's clear
how many layers there are and which one you're looking at.
z grows the active pane to fill the entire window, hiding every other
pane without closing or resizing them. You can still switch focus while
zoomed — o, the arrows, clicking a tab — and whichever pane becomes
active fills the screen in its place. Pressing z again restores the
original layout, focused on whichever pane was active when you unzoomed.
The status bar shows zoom while it's on.
f toggles a floating terminal: a bordered pane covering three quarters of
the window, centered over the tiled layout instead of taking a slice out of
it. While it's up it holds focus — every unprefixed key goes to it, the
tiled borders all dim, and the status bar shows float. Pressing f again
hides it and hands focus back to whichever pane had it; the floating shell
keeps running, so the next f brings the same session back, scrollback and
all. p x kills a floating layer, and killing the last one closes the
float for good.
A float is one rect, so |, -, a and z are refused while it has
focus — there is nothing to subdivide and nothing to grow into. s is the
exception: stacking shares a rect instead of carving it up, so it works,
and a floating stack behaves exactly like a tiled one — collapsed title
bars for the background layers, layer 2/3 in the status bar, arrows or
o to flip between them, a click on a title bar to bring one forward.
Clicks outside the float are swallowed rather than passed through to the panes it covers, so a stray click can't quietly move focus out from under it. It's per-window: each window has its own, and picking a tiled pane in the pane picker leaves that window's float.
A pane's border normally follows its shell's own title, or the first
command you run in it (see above). p r opens a rename prompt over the
layout, seeded with the active pane's current border name; type a new one
and Enter confirms it, Esc cancels. Once renamed, the border keeps that
name regardless of what runs in the pane, until you rename it again with a
blank name, which reverts to auto-tracking the next command. The rename is
scoped to the pane's border only — the window tab keeps following the
pane's shell title independently, so renaming a pane never touches its tab.
A window's tab normally follows its active pane's border title (see
above). w r opens a rename prompt over the layout, seeded with the
window's current tab name; type a new one and Enter confirms it, Esc
cancels. Once renamed, the tab keeps that name regardless of which pane is
active or what runs in it, until you rename it again with a blank name,
which reverts to following the active pane's title.
Each session — the unit tile ls lists and -t name targets — is a wholly
separate daemon with its own windows and panes; s n/s x/s r/s
o/s p reach five things worth doing to one without dropping back to a
shell prompt.
s n creates a new session under an automatically chosen name
(session-1, session-2, …, the first one nothing else is using) and
switches to it, the same detach-and-reattach s p uses — just aimed at
a name nothing is listening on yet, so the CLI starts a fresh daemon for
it instead of reattaching to an existing one. Rename it afterward with
s r if the auto-picked name isn't the one you want.
s r opens a rename prompt over the layout the same way p r/w r
do, seeded with the session's current name (default if it was never
given one); type a new one and Enter confirms it, Esc cancels. Unlike a
pane or window rename, a blank commit is a no-op rather than a revert —
a session has no auto-tracked name to fall back to. Renaming takes effect
immediately: every future tile -t <name> finds the session under its new
name, tile ls included.
s x deletes the current session: same as q, it stops the daemon and
every shell in it, so it asks first the same way — y goes through with
it, any other key backs out.
s o closes every other running session — every daemon tile ls
would list besides this one — without asking: it can never take down the
session you're looking at, so there's nothing here for a confirmation to
guard.
s p opens a picker listing every running session, the same way p p
lists panes: ↑/↓ (or j/k) move the highlight, which starts on the
current session's own entry (marked (current)). Enter switches to the
highlighted session — detaching from this one and reattaching to the
other, exactly like running tile -t <name> from a shell, just without
leaving the terminal you're already in — or does nothing if it's the one
you're already attached to. Esc/q cancels.
p p opens a floating pane picker over the current layout: every window
and its split/stack tree on the left, a live preview of the highlighted
pane's content on the right. ↑/↓ (or j/k) move the highlight —
skipping over window headers and the branch rows a nested split or stack
draws for itself — and the preview column updates to match. Enter
switches to the highlighted pane, jumping windows if it's in a different
one; Esc/q cancels back to whatever was focused before you opened it.
S saves the current arrangement of windows, panes and their layout as a
named preset: a prompt opens over the layout the same way p r/w r's
rename prompt does — type a name and Enter confirms it, Esc cancels
without saving anything. Saving under a name that's already used overwrites
that preset instead of creating a duplicate. A preset remembers each
window's split/stack tree and relative pane sizes, plus any manual window
or pane names set with w r / p r; it does not remember what was
running in a pane, so loading one always starts fresh shells.
L opens a picker listing every saved preset — ↑/↓ (or j/k) move
the highlight, Enter restores the highlighted one, Esc/q cancels.
Restoring a preset adds its windows after whatever is already open rather
than replacing it, so loading one never throws away work in progress. x
deletes the highlighted preset from disk instead — with a confirmation
nowhere in sight, so double-check the highlight before pressing it; deleting
the last one closes the picker rather than leaving it open on an empty list.
Presets are stored in presets.yaml alongside config.yaml, in the same
$XDG_CONFIG_HOME/tile (or ~/.config/tile) directory.
q is the only key that ends more than it starts: it stops the daemon, and
with it every shell in every window, whether or not anything is attached.
So it asks first, in a box over the layout — y goes through with it,
any other key backs out, so a stray keystroke can neither end the
session nor leave you stuck in front of the dialog. d (detach) is the one
you want if you'd rather leave everything running.
tile kill-server does the same thing from outside, without asking: the
dialog guards the keystroke you can hit by accident, and spelling out
kill-server at a shell already says it. It reports "no server running"
rather than pretending it did something.
T opens a floating colorscheme picker over the current layout. ↑/↓
(or j/k) move the highlight and preview it live — the tab bar, status
bar and tooltip repaint immediately so you can see it before committing.
Enter keeps the highlighted scheme and remembers it in config.yaml
(same file as the keymap); Esc/q cancels back to whatever was active
before you opened the picker.
Only the four Catppuccin flavors — Latte, Frappé, Macchiato, Mocha — ship today. Pane content is untouched either way; the picker only skins tile's own chrome.
Theme, keymap and the pane margin are all read from
$XDG_CONFIG_HOME/tile/config.yaml, or ~/.config/tile/config.yaml if
XDG_CONFIG_HOME isn't set (same path on macOS and Linux). The daemon
writes out the full default file the first time it starts, so every setting
is listed explicitly and ready to edit:
theme: Catppuccin Macchiato
margin: 1 # blank gutter cells between panes; 0 makes them touch
keymap:
prefix: ctrl+b
lock: f12
next_window: "]"
prev_window: "["
cycle_pane: o
new_pane: a
split_horiz: "|"
split_vert: "-"
zoom: z
float: f
preset: S
load_preset: L
delete_preset: x
theme: T
reload: R
detach: d
quit: q
windows:
key: w
new: c
kill: "&"
rename: r
panes:
key: p
kill: x
rename: r
picker: p
stack: s
sessions:
key: s
new: n
delete: x
rename: r
close_others: o
picker: ptheme is one of the four Catppuccin flavor names
(Latte, Frappé, Macchiato, Mocha). Under keymap, prefix and lock accept
an optional ctrl+/alt+/shift+ modifier before the key; every other
top-level binding is one character. windows, panes and sessions are
nested sub-layers: key is the leader that opens each, and its other
fields are the one-character actions reached after pressing it (new,
kill, rename for windows; kill, rename, picker, stack for
panes; new, delete, rename, close_others, picker for sessions,
each one acting on the whole daemon behind the attached session rather
than a window or pane in it) — leave any of them blank to drop that action
from the layer. Press reload (R by default) after editing to apply
changes live, no restart needed.
Every command below acts on a running session over the same socket the TUI uses, and answers on stdout. None of them attach, so they can run while you are using the session and will not detach you — that is what makes them safe to hand to a script or an AI agent.
Panes are addressed as %<id>, windows as @<id>, both printed by
tile list. Ids come from one counter, so a pane and a window never share a
number, and unlike a position in the tab bar an id does not shift when
something before it closes.
tile list [--json] every window and pane
tile capture %p [--lines N] a pane's text, no escape codes
tile send-keys %p [--key SPEC] [--enter] [text...]
tile split %p [-h|-v] split a pane, prints the new pane's id
tile stack %p layer a pane behind it, prints its id
tile new-window prints the new window's id
tile kill-pane %p
tile kill-window @w
tile focus %p
tile resize %p <left|right|up|down> <cells>
tile even %p|@w equal shares: the pane's branch, or a window
tile rename %p|@w [name] blank name reverts to the shell's title
Anything that fails exits non-zero with the reason on stderr.
$ tile list
* @2 1: zsh
%1 ├─ go test ./...
%3 └─ tail -f server.log <- focused--json gives the same tree with geometry and focus, for a program to read:
$ tile list --json
[
{
"id": 2,
"index": 1,
"name": "zsh",
"active": true,
"root": {
"dir": "horiz",
"w": 80, "h": 22,
"children": [
{ "id": 1, "title": "zsh", "name": "go test ./...", "w": 40, "h": 22 },
{ "id": 3, "title": "zsh", "w": 40, "h": 22, "focused": true }
]
}
}
]A node with children is a split or a stack and has a dir; a node with an
id is a pane. A window also carries float when it has a floating
terminal, and zoomed when one of its panes is zoomed.
send-keys types into the pane you name without going through the prefix
key, so the session's mode — prefix pending, locked, a picker open — never
changes what a script sends. Text is passed as arguments; --enter appends
a newline, and --key sends a single named or modified key.
tile send-keys %1 --enter 'go test ./...'
tile send-keys %1 --key ctrl+c # interrupt it
tile send-keys %1 --key escape
tile send-keys %1 -- '-n leading dashes' # -- ends flag parsingTyping a command this way also names the pane after it, exactly as if you had typed it yourself.
capture returns the pane's text with the styling stripped and the blank
rows below the cursor dropped, so --lines 20 means the last twenty lines
that have something on them. --lines 0 returns the whole scrollback.
$ tile capture %1 --lines 3
ok tile/internal/server 0.51s
❯A terminal pane is a screen, not a stream of command results: nothing marks
where one command's output ends. So there is no "run and wait" — send the
keys, then poll capture until what comes back looks finished.
tile send-keys %1 --enter 'go test ./...'
sleep 2
tile capture %1 --lines 30Splitting, stacking and killing act at the pane you name and leave focus
where pressing the equivalent key would — split focuses the pane it just
made, kill-pane focuses the survivor. tile focus moves it back.
new=$(tile split %1 -v) # prints e.g. %7
tile send-keys "$new" --enter 'tail -f server.log'
tile rename "$new" logs
tile focus %1 # hand focus backSplitting does not divide evenly. A split halves the share of the pane it
splits, so splitting the same pane twice leaves 50/25/25 rather than thirds.
even is the way back: given a pane it evens that pane's branch, given a
window it evens every branch in it.
$ tile split %1 -h; tile split %3 -h # 50/25/25
$ tile even %1 # 34/33/33resize moves one border, in cells, and leaves focus alone. It works in
weights underneath, so a step is approximate at small sizes — for equal
shares reach for even rather than resizing your way there.
tile resize %5 down 2 # grow %5 downward, taking from the pane belowSizes are relative, so a layout built at one terminal size holds its proportions at any other.
Click a tab at the top to switch windows, click a pane to focus it, and drag a gutter between panes to resize. When the program inside a pane turns on mouse reporting (vim, htop, …), its clicks are forwarded to it instead. While a floating terminal is up it takes the mouse: clicks outside it are ignored rather than reaching the panes underneath.
0:bash │ 1:vim │ 2:htop ← tab bar (click to switch)
┌─ window 1 ─────────────────┐
│ pane │ pane │
│ ├──── │
│ │ pane │
└────────────────────────────┘
NORMAL layer 2/3 window 2/3 ← status bar
Panes live in a tree of splits with relative weights, so resizing the terminal keeps the proportions. The daemon owns the tree, the PTYs and the terminal emulators; the client just draws frames and forwards input, which is why detaching leaves everything running.
devbox run test # layout, resize and hit-testing maths
devbox run build