Panes and tabs
A tab is a tree of splits. Panes keep running in every tab; only the tab you are looking at is drawn.
Arranging
- Split
keyboard:
C-a Entermouse: hover near existing pane border in the middle to reveal clickable split actions. - A new pane starts in the directory the pane it came out of is in, not where the session was started.
- Resize with
C-aH J K L: the boundary moves the way you press. Sizes are weights, so a resized layout survives the window changing size. Of course you can also drag in the gutters of the panes to freely resize them! (or the pane's border incompactmode) - Equalize panes using
C-a 0(equalize), it will attempt to equalize the space between all panes. - Rotate your current layout with
C-a Space(rotate_layout); it turns the whole tab a quarter turn clockwise. - You can (temporarily) Zoom in on panes with
C-a z(zoom); it fills the tab with one pane and triggering it again unzooms. - Minimizing panes can be done with
C-a m(minimize); it minimizes the active pane into a strip along the bottom, still running. Each put-away pane sits there as a miniature pane frame, and clicking it anywhere brings the pane back. BEL notifications are drawn in the minitature pane frame as they arrive.
The mouse
- Click the middle of a pane's border to split toward it
- The top row is the drag handle, and its handle splits upward. Clicking a pane's header to focus it never splits it. Its handle goes as near the middle of the row as the row allows: centred when the row is empty, beside the title when the title holds the middle (the title keeps its cells for the double-click rename). Arming an edge leaves the name, the buttons and a dead pane's epitaph legible: the heavy rule goes round them.
- Drag the gap between panes to move that boundary. Where two gaps cross,
drag the crossing to move both at once. With
gap 0andcompact truethere is no gap to grab, so grabbing the border itself and pulling does the same thing. - Drag a pane by its title onto another to swap them, or to place it: the pane under the pointer subdivides into a centre and four edge bands, and where you let go is what the drop means.
- ...or onto a tab in the strip to move it into that tab, or onto the
+for a tab of its own. - Double-click a name to rename it in place: a pane's title or a tab in the
strip. Enter keeps it, Escape abandons it,
C-uclears the field (a rename seeds with the current label, since it is usually an edit), empty gives it back to the program. The same two verbs are in the palette asrename-paneandrename-tab, for hands that are on the keyboard. A name is not a purpose: the purpose is a separate label. - Drag a tab along the strip to reorder it. The strip rearranges as you go rather than dropping it at the end.
- Click a tab to switch,
+to open one, a pane to focus it, and the marks in a frame's corner to minimise (▬), zoom (□) or close (✕) it.
The tab bar
The strip of tabs sits along the top by default. It does not have to:
tab_bar_side "left" // or "right"; "top" is the default
tab_bar_width 18 // columns the sidebar takes, at most
tab_bar_index "right" // the tab number at the far end; "prefix" is 1:name
tab_bar_padding 0 1 0 2 // air inside it, in cells: top right bottom left
tab_bar_pad 0 // ...or just the top of that, the older name
tab_gap 0 // ...and between one tab and the next
tab_bar_chrome true // frame it like a pane
tab_bar_status_lines 1 // rows one status may wrap onto
newtab_button true // the `+` at the end of the list
newtab_pad 1 // ...and the air before it
A sidebar lists one tab per row down the chosen edge, and the panes trade
the strip's row for its columns. Everything the strip does travels with it:
click to switch, double-click to rename in place, drag a pane onto a tab or
onto the + — which, given a row of its own and the columns to spare, spells
itself out as + new tab. That button sits newtab_pad rows below the last
tab (columns after it, in the top strip): it is the one row in the list that
does something other than "go here", and flush against the tabs it is what you
hit when you meant the last one. The air is paint, not layout — the panes keep
their rectangle whatever it is, and it is given up before the button is when
the strip runs out of room. Set newtab_button false to drop the button
altogether; new-tab is still a chord and a palette entry, and a
pane can no longer be dropped onto it. The pane count and the prefix badge move
to the sidebar's bottom rows; the status line along the bottom
keeps the full width.
The bar fits itself to the terminal, and gives up the edge only when it has
to. tab_bar_width is a ceiling rather than a reservation: the sidebar is
held to a third of the screen, so 18 columns is a margin beside a 90-column
terminal and narrows to 16 beside a 50-column one, with the panes keeping the
rest and never dropping below the width they collapse at. Only when even a
legible sidebar (14 columns, or whatever narrower width you asked for) will not
fit does the strip go back to the top row.
Height counts too: a sidebar spends a
row per tab, so a wide but short terminal runs out of list before it runs out
of screen — and a tab with no row is not clipped, it is unreachable. When the
rows for the labels and the + are not there, the strip goes back on top,
where the same tabs lie along one row. Not when that row could not hold them
either: with more tabs than the top strip can seat, moving them gains nothing
and the side you configured stands. All of it is re-derived per frame from the
terminal's size, so a resize moves the bar and nothing remembers it did.
The sidebar is framed like a pane, so it belongs to the chrome rather than
floating beside it. Under compact the frame's inner line
is the tab's outer ring: the two meet in real junctions (┬, ┴) and read
as one figure, the same way a divider running into the ring does. Set
tab_bar_chrome false to give those two columns back to the labels.
Under each tab, the sidebar says what the panes inside are up to. A pane
that announced a status over OSC 5577
or OSC 7501
puts that line under its tab — visible from every other tab, which is the
point — and a pane that died shows how instead (exited: status 3). At most
tab_bar_status rows per tab (3 by default, 0 turns it off); more than that
and the last row is an ellipsis rather than a status pretending the list is
complete. A short terminal cuts the list the same way, and says so the same
way: the statuses give way before the tabs do, because the list is how you
navigate and a status is only an annotation on it — a talkative pane must not
be able to cost you a tab you can click.
A pane can say its status is still happening, and the sidebar says so back:
busy over OSC 5577 turns that row tab_status_busy and puts a spinner
(busy_mark, in tab_status_spinner — its own theme entry, because the words
have to stay readable and a mark does not, and unset it follows the words) in
the first column of the indent — so make test running and
make test finished, which are the same words, stop looking the same from
another tab. Nothing moves when it flips, since the mark takes a column the
padding was already holding blank, and the rest of that padding is the space
after it. One frame
in busy_mark is a static mark and costs nothing, several make it turn, and the
session only keeps a frame clock while such a row is actually on screen. The
flag dies with the program: a status left behind by something that exited
describes the past. A program reporting over OSC 7501 gets the same spinner for
working and for blocked — a question waiting on you is still happening — and
none for done or error, which are not.
tab_bar_padding is the air inside the strip, between its frame and the
list: 1, 2 or 4 values in CSS order, the shape padding takes for
a pane. In cells rather than padding's rows-times-gap_aspect, because the
two sides do different jobs here — the columns indent the labels and their
statuses, the rows are air above the first tab and below the last thing in the
strip — and scaling the columns would spend four of a sixteen-column sidebar on
2. The default is 0 1 0 2: one column on the right, where the tab number stops,
and two on the left, because that indent is also the gutter a busy pane's
spinner sits in — the mark takes its first column and what is left over is the
air after the mark, so a single column would stand the spinner against the
first letter of the status. tab_bar_padding 0 1 is that squashed look for
anyone who wants the column back, and tab_bar_padding 0 puts the names
against the frame, which the hard-coded leading space they used to carry never
allowed. It is air inside the plate — a tab's
fill still spans the whole row, because the row is what you click.
tab_bar_pad is the older name for the top of it alone, still honoured, and it
wins where both are written.
tab_gap puts air between one tab and the next, which is what stops a
list of tabs that each carry a paragraph of status from reading as one column
of text. It is the space between tabs only: above the first is tab_bar_pad
and before the + is newtab_pad, so none of the three is paid twice. Like
them it is paint rather than layout, and like them it is given up a row at a
time when the sidebar runs short — a gap must never be what costs you the tab
it was separating.
A status can be given more than one row. tab_bar_status_lines 3 word-wraps
it instead of cutting it, which is the difference between add a summary… and
the sentence — three rows of a sixteen-column sidebar is forty-eight cells. The
rows under a tab then read as a paragraph per pane rather than a line per pane,
so alternate panes sit on alternate bands (tab_status_stripe, derived from
the theme unless you name it): with the shape no longer saying where one pane's
status ends, the band does. At one line there is nothing to disambiguate and no
band is drawn, which leaves a translucent terminal's own background alone. Every
row of a status is the same door — clicking any of them jumps to that pane — and
they all count against tab_bar_status. Clicking a status row jumps to the
pane that said it, across tabs. A status is set in italic rather than indented under its tab — the slant
says "this belongs to the row above" without spending columns a narrow sidebar
has none of, and says it on a monochrome theme too. The colour is part way
from tab_count to tab_hover — the live thing in the sidebar should not read
in the flat grey of the chrome around it, and should not shout over the tab it
belongs to either. Unset, tab_status_fg is mixed from those two, so a theme
gets a status colour in its own family without naming one (a monochrome theme
mixes two greys and stays grey). Name it, or tab_status_bg for a band, and
yours wins; the band is unset by default, which leaves a translucent
terminal's own background alone. The top strip has one row and no room for any of this, which is half of
why the sidebar exists.
A sidebar's tab numbers sit at the far end of the row, api on the left
and 3 hard against the right edge, rather than as the 3:api the top strip
writes. tab_bar_index "prefix" puts them back:
│ tests 1 │ │ 1:tests │
│ verylongproj 2 │ vs │ 2:verylongproje│
│ api 3 │ │ 3:api │
Every name starts in the same column and the numbers line up in one of their
own, which is worth a column of a sidebar and is not available to the top strip
at all — a tab there is exactly as wide as its label, so it has no far end
to align against, and the setting is ignored. A row too narrow to spare the
columns goes back to the prefix, because the name is the part you came for, and
a name long enough to reach the number always stops one cell short of it:
verylongproje2 is a tab called verylongproje2 as far as any reader can tell.
Every tab is a plate, not a word on the background. The one you are in is
filled with the accent (tab_active_bg); the rest sit on tab_idle_bg, 43% of
the way from the theme's background to tab_idle, the grey it writes their
labels in — a fraction chosen as a contrast ratio, about 1.9:1 against the
background whether the theme is dark or light. That label grey is brighter than
the one on idle frames and titles, because a tab you are not in is a place to
go and has to read from across the screen, where a dimmed border only has to be
findable. Two things come of the quiet fill: a tab has edges, so where one ends, how
far the row you are about to click reaches, and — in the sidebar especially —
which rows are tabs rather than statuses are all visible; and the active fill
has something to be brighter than, which on a strip of short labels is most
of how the eye finds it. Unset, it is mixed from two colours every theme
defines, so each gets a plate in its own family — paper lightens, phosphor
greens. Name it and yours wins: set it to your own default_bg for the bare
strip, or somewhere further along for a louder one.
An unnamed tab borrows the directory its focused pane is in — the
kernel's answer, so a cd moves the label with you, and home shows as ~.
A real name or a purpose wins the moment one exists;
when a tab's panes sit in different directories, the focused one names the
tab. This holds wherever the bar sits, top row included.
Floating a pane
C-a f lifts a pane out of the layout and draws it on top of the tiled ones
(below the modals) where it can be moved and shaped freely. It pops to
the centre of the tab. Pressing f again puts it back in the seat it kept,
with the layout exactly as it was.
C-a F opens a new floating shell, the throwaway terminal, over
whatever you are doing, in the focused pane's directory; exit (or ^D) closes
it like any shell, and un-floating lands it beside the pane it was opened over.
Links
OSC 8 hyperlinks pass through. A program that emits real hyperlinks
(ls --hyperlink, gcc's diagnostics, delta) keeps them: the compositor
carries the link beside each cell and re-emits it to your terminal, which
offers it with its own gesture.
Plain-text URLs are your terminal's own matcher, running over what slosh paints, with one catch worth knowing. While any program owns the mouse (a multiplexer does), Ghostty only offers links when shift is added to the usual gesture: shift is its mouse-capture escape, and it is stripped before the link's own modifier is checked. So the gesture inside slosh (or tmux, or zellij) is shift+cmd+hover to highlight and shift+cmd+click to open (shift+ctrl on Linux). A URL that wraps onto a second row cannot be matched this way through any multiplexer, because the screen is repainted row by row and the terminal sees two hard lines. That is exactly what OSC 8 links, which pass through whole, are for.
Finding a pane
Tabs stop being navigation somewhere around six projects. C-a s opens a picker
over the whole session: every tab, including panes a small window has collapsed
out of sight. Type to narrow it by pane title, tab name or
purpose; arrows, C-n/C-p or tab to move, C-u to clear
what you typed, enter to go.
A dot marks where you already are.
The finder searches what this session has; C-a w lists what is on disk,
every project under your roots, open or not (workspaces). A
project you have not opened yet has no pane for the finder to match.
Panes that were given a command
Split off a shell, do something, type exit: it closes, like a terminal should.
But a pane that was told to run something, from a layout or the
control API, keeps what it printed when that something exits,
with two lines saying what ran and how it ended, and two buttons:
[ran: npm run dev]
[process exited: status 3]
╰ exited: status 3 ───────────────────────────────[re-run]─[close]─╯
[re-run] (or C-a r) runs the same command again in the same pane, keeping
the previous run above it in the scrollback: a pane you re-run becomes a log of
what ran, and a command that failed while you were looking elsewhere still has
its error message when you get back.
Which panes stay is one setting, keep_dead: commands (the default), all,
or none.
Responsive Layout for small terminals
When the panes no longer fit, the tab becomes a list of one-line headers with the focused pane open below them, and returns to exactly the layout you had when there is room again.
min_pane is where that starts happening; min_split is the smaller pane a
split is willing to create. A split is refused when either floor would be
broken, including the axis it does not divide, since splitting cannot improve
that one: a pane already too short for two rows has no room for two columns
either, whatever its width says.
Text, images, bells
- Select text to copy it. Release the mouse and it is on your clipboard, sent to your terminal over ssh as well as locally. Middle click pastes.
- Double-click a word to select it, and it is copied the same way a drag is:
the highlight is the copy. What counts as one word is
word_separators, which lists what a word stops at rather than what it is made of, sosrc/app.c:1234,https://a.b/c?d=1,snake_caseand a run of CJK each come out whole, while a quote or a bracket ends one. A click on a blank cell or on a separator selects nothing and leaves your clipboard as it was. - Images work. This is still in it's early stages but Kitty graphics pass through to your terminal, including the half of the protocol other multiplexers drop, where a program uploads an image once and places it every frame. Your terminal's cell size is carried through to each pane, so a program can size an image without saying how many cells to cover. PNG uploads are decoded here, because the terminal core ships no image decoder. (Sixel is not supported.)
- A pane that beeps says so: in its title, in its tab, and until you look at
it.
bell_indicator falsemakes a bell silent and invisible, which is a real choice. A chrome shader can make it louder.
