sloshUsing it slosh.foo

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#

The mouse#

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
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. A terminal too narrow to give up the columns falls back to the top row until it grows.

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 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.

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.

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#