<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/include/app_internal.h, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/include/app_internal.h?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/include/app_internal.h?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-10-09T10:31:53Z</updated>
<entry>
<title>tab bar: a sidebar that fits the terminal, and gives up the edge last</title>
<updated>2026-10-09T10:31:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:24:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=3a0825c6890eff566307c0eaad809cdbcf854efd'/>
<id>urn:sha1:3a0825c6890eff566307c0eaad809cdbcf854efd</id>
<content type='text'>
`tab_bar_width` was a reservation: the configured columns came off the
screen whatever the screen was, and the only fallback was all the way back
to the top row. On a narrow terminal that is chrome displacing the thing it
is chrome for, and the jump to the top moves every target you were aiming
at.

It is a ceiling now. app_tab_bar_cols takes the app, because the answer
depends on the terminal and not only on the config: held to the columns
left after a pane's collapse width, and to a third of the screen, and
narrowed rather than dropped. TAB_BAR_FIT (14) is the narrowest width this
code will invent on its own -- a config that asks for less is taken at its
word -- and 0 means there is no room for a sidebar at all, which is the one
case that still falls back on top.

Height counts too, which it did not before: 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. tab_bar_min_rows
is that floor. Not when the top strip could not seat them either -- moving
them would gain nothing, so the side you configured stands.

All of it is re-derived per frame, like the layout, so a resize moves the
bar and nothing remembers that it did.

test_tab_bar also stops depending on where the suite is run from. An
unnamed tab borrows the basename of its focused pane's cwd, so eight checks
read `tests` under `make test` (which cds there) and `slosh` from the
repository root, where they failed. They are about padding, alignment and
the index, not about that derivation, so they name the tab first -- by id,
since `set-name` takes 0 for the focused *pane* only. 147 checks, green
from either directory.
</content>
</entry>
<entry>
<title>panes: the focus a closed tab left behind</title>
<updated>2026-10-09T10:23:50Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:23:50Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=2cb6dce416c814298f24087fdaafb0c119a584b6'/>
<id>urn:sha1:2cb6dce416c814298f24087fdaafb0c119a584b6</id>
<content type='text'>
A tab's focus outlived the tree it pointed into. close_leaf's last-pane
branch freed the leaf and removed the tab but never cleared `t-&gt;focus`,
and tab_remove only shifts the slots *after* the one it removes -- so with
the last tab gone, `a-&gt;tabs[0]` kept the stale tab_t while `ntabs` was 0
and `cur` was 0. `cur(a)` is `&amp;a-&gt;tabs[a-&gt;cur]` with no bounds check, and
`a-&gt;quit` is a request rather than a barrier: the loop keeps serving
commands, and the next one to ask for the focused pane was handed freed
memory.

From outside it looked like a random crash, because what the freed chunk
held depended on what had been allocated since. Two from one afternoon: a
pane_t recycled into a `panes` JSON reply, whose `term` pointer was then
the ASCII of `k":true,` and faulted as a general protection fault; and the
same node read back zeroed, which left pane_clear_bell testing a flag at
NULL+0x5d. Typing `exit` in the last pane is the whole recipe.

  close_leaf        drops the focus with the tree
  tab_remove        blanks the slot the shift vacated
  app_compose       tests -&gt;pane as well as -&gt;focus, the way the search
                    beside it already did
  app_apply_layout  clears restore_focus up front -- it is a node pointer,
                    and an apply that fails before consuming it leaves it
                    naming a tree the next apply frees

Two more the same reading turned up, both in close_leaf: an emptied split
set the focus to the node just freed, and a leaf whose parent disagreed
with that parent's child list underflowed `p-&gt;nkids - at - 1` into a
memmove of nearly SIZE_MAX.

FOCUS_CHECK -- behind -DSLOSH_FOCUS_CHECK, compiled out otherwise -- is
the invariant all of this turns on: every tab's focus is a leaf of that
tab's own tree. It is what found this one.

Committed with --no-verify: the files are deliberately partly staged, and
the staged blobs were checked against clang-format directly.
</content>
</entry>
<entry>
<title>layouts, moves: build panes without pulling the view to them</title>
<updated>2026-09-29T12:26:28Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-29T12:26:28Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ca1b5bdf5bf7b5ff01ab515846802b3487599d3a'/>
<id>urn:sha1:ca1b5bdf5bf7b5ff01ab515846802b3487599d3a</id>
<content type='text'>
Applying a layout landed on the first tab it made, and moving a pane into a
tab took focus there. For a program building a pane in the background that
is two failures at once: it yanks the view off the human mid-sentence, and
it leaves the *next* caller to read focus as "where I am" and put its work
in a stranger's tab.

`apply-layout focus:false` builds the tabs and keeps looking at whatever was
on screen; a layout naming an `active` tab still wins, because that is a
statement about where to be. `move-pane` gains `beside` (which pane in the
destination to split, rather than whatever it has focused) and `focus:false`
-- the two things a caller placing a pane next to *itself* needs.

That uncovered a sizing bug that was unreachable before: a pane's size comes
from a layout pass, and a tab nobody has looked at never had one, so a pane
built in the background stayed 1x1 and its program drew into a single cell.
The jump is what used to size it. layout_tab() gives a background tab a real
pass, borrowing the tab cursor for its length -- nothing composes, reads
input or draws a frame in between.
</content>
</entry>
<entry>
<title>theme: the busy spinner is its own colour (tab_status_spinner)</title>
<updated>2026-09-22T13:38:08Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:38:08Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1cbfb4474ca31c61254be8d5f3ec046967bd1ff3'/>
<id>urn:sha1:1cbfb4474ca31c61254be8d5f3ec046967bd1ff3</id>
<content type='text'>
It was drawn in tab_status_busy, the colour of the words it sits beside, which
made one decision out of two. The words have to stay readable at a glance and
a mark does not: a theme may well want the glyph loud and the text calm -- or
the text in the accent and the mark a quiet dot -- and with one entry neither
is available without moving both.

Unset it follows tab_status_busy, derived after it and from whatever that
resolved to, named or derived, so nothing changes for anyone who does not ask
and no theme in contrib needs a line. `--dump-theme` writes both, like every
other colour the compositor draws.
</content>
</entry>
<entry>
<title>osc 5577: `busy`, and a sidebar that shows it (busy_mark)</title>
<updated>2026-09-22T13:12:02Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:12:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=720aa48e249c0c962e8f73372ceded6092414a6e'/>
<id>urn:sha1:720aa48e249c0c962e8f73372ceded6092414a6e</id>
<content type='text'>
A status is a line of text, and a line of text cannot say whether it is still
true. "building 3/7" reads exactly the same while it builds and ten minutes
after it stopped, which is the one thing somebody reading it from another tab
wants to know -- and the only workaround available to a program was to write
the answer into the words and keep rewriting them.

So `busy`:

    printf '\033]5577;1;status;make test\033\\'
    printf '\033]5577;1;busy;1\033\\'
    ...
    printf '\033]5577;1;busy;0\033\\'

A flag of its own rather than a field of `status`, because a status is the
*whole* payload after the verb -- `status;a;b;c` has always been the text
"a;b;c" -- so anything added to that line would change what every existing
sender means, including the five pi extensions D1 promises byte compatibility
to. Orthogonal, so a program can leave one line up and toggle the other;
anything but 0/false/no/off is on; `clear` takes both; and a pane whose program
has gone is never busy, whatever it last claimed, because a spinner turning
under a dead tab is a lie you keep looking at.

In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived
like the other status colours, so no theme needs a line -- and takes a spinner
in the column `tab_bar_padding` was already holding blank, so nothing reflows
when a program starts or stops working. `busy_mark` is the frames, separated by
spaces: one frame is a static mark and costs nothing, several make it turn, and
only then does the session keep a frame clock -- via the same `animating` flag
an animated shader uses, so the cost lands only while such a row is actually
composed, and `""` drops the mark entirely and leaves the colour to say it.
The frame is a pure function of the clock, like every other pulse here, so two
clients attached to one session show the same one.

`panes` now reports `status` and `busy`, which is the scriptable half of the
same fact: what a pane says about itself, without reading its screen.
</content>
</entry>
<entry>
<title>themes: a picker that wears them as you browse (C-a t)</title>
<updated>2026-09-22T10:50:12Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T10:50:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=6283e484613116d1c38cab9a43e3330e09042193'/>
<id>urn:sha1:6283e484613116d1c38cab9a43e3330e09042193</id>
<content type='text'>
`theme_name` is a line in a file, and a name in a list tells you nothing about
a colour scheme. So the picker previews by *applying*: moving the selection
puts that theme on, which is the only honest preview of a palette meant to
dress a whole session -- a swatch in a list would be a small lie about it, and
the session is already on screen behind the box.

It opens on the theme being worn rather than at the top, so the first arrow
press moves off it instead of silently changing the session, and the row your
config names says so, because "which one is this" and "which one comes back on
restart" are two different facts the moment you switch.

Escape puts back what was worn, since a picker that changed your session by
being looked at is one you stop opening. Enter keeps it for the session and
says as much. Only C-s writes `theme_name` into your config, which is the one
place in the UI where a keystroke edits it, so it is a deliberate chord and
the footer says so: "C-s keeps it, esc undoes".

Reuses the picker the finder, the palette and the projects list already wear
-- one machine with a subject, so the query, the selection, the mouse, the
frame and the scroll behaviour are the ones already tested. The list is
scanned on the keystroke, like the projects are: the moment it opens is the
moment it has to be right, and a theme written since the session started is
exactly the one being looked for. With nothing to list it toasts the directory
it looked in rather than opening an empty box.

`t` was free: tabs cycle on tab, and new-tab has had `c` since the first day.
</content>
</entry>
<entry>
<title>tabs: a plate under the ones you are not in (tab_idle_bg)</title>
<updated>2026-09-22T09:59:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T09:59:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=cb0d993fa284005d5679b628bb08d51f12812b8c'/>
<id>urn:sha1:cb0d993fa284005d5679b628bb08d51f12812b8c</id>
<content type='text'>
The active tab has been filled with the accent since the strip existed; every
other tab was words on the background. That leaves the strip with one shape on
it and nothing to compare it against -- where a tab ends, how far the row you
are about to click reaches, and in the sidebar which rows are tabs rather than
statuses, all had to be read off the text.

tab_idle_bg fills them. Barely off the background and in the label's own grey,
because this is not a second highlight: it gives a tab edges, and it gives the
accent something to be brighter than, which on a strip of short labels is most
of how the eye finds the tab it is in. Hover keeps it for the same reason the
active tab keeps its fill -- losing the plate under the pointer would read as
the tab going away -- so hovering still lands on the text, and the bell mark
sits on the plate rather than punching a hole in it.

Derived a third of the way from default_bg to tab_idle when nobody names it,
the same bargain tab_status_fg and tab_status_stripe already make: both ends
are colours every theme in contrib defines, so each gets a plate in its own
family -- paper lightens, phosphor greens, mono stays grey -- without a line
added to any of them. Naming it wins, and naming your own background is how
you ask for the bare tabs back.

The `+ new tab` button and the gaps between tabs stay unpainted: the button is
the one row in the list that does not mean "go here", and tab_gap is air
between plates, which is what makes them read as separate tabs at all.
</content>
</entry>
<entry>
<title>sidebar: a status can have more than one row (tab_bar_status_lines)</title>
<updated>2026-09-20T17:55:03Z</updated>
<author>
<name>Peter Geissler</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-20T17:55:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=65a694b194bd68f8c6ba4b961e2cc906b8738a6c'/>
<id>urn:sha1:65a694b194bd68f8c6ba4b961e2cc906b8738a6c</id>
<content type='text'>
Sixteen cells is not enough for a sentence, and a sentence is what a pane
has to say: "add a summary…" is most of a status and none of its meaning.
tab_bar_status_lines 3 word-wraps instead of cutting, and the same column
carries forty-eight cells rather than sixteen.

Wrapped, the rows under a tab stop being a list of panes and become a
paragraph each, so the shape no longer says where one pane's status ends and
the next begins -- both are italic text down one column. Alternate panes are
drawn on alternate bands to say it instead: tab_status_stripe, derived from
the theme like tab_status_fg, so a theme that has never heard of any of this
still gets a band in its own family. Nothing is painted at one line, where
there is no ambiguity to resolve and a band would take a translucent
terminal's background away for nothing.

1 is the default and is the old behaviour exactly, down to cutting at the
column rather than the last word before it: with no next row to move the word
to, breaking early would spend cells on nothing. The reason to prefer a word
boundary only exists once there is somewhere for the word to go.

Rows count against tab_bar_status as before, so wrapping trades panes for
words within the same allowance, and the allowance itself still gives way to
the tabs. A status that will not fit whole is dropped whole and reported with
the ellipsis, rather than shown as a fragment that reads like the whole
thing. Every row of a status is the same `find:` door.

tabstatus's buffer goes to 256 to match pane_status's own slot: at one line
the drawing cut it anyway, but wrapped it is the whole string that has to
survive the trip.
</content>
</entry>
<entry>
<title>sidebar: statuses wear italic instead of an indent, and can wear colour</title>
<updated>2026-09-10T14:32:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-10T14:32:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=a2d08f4876226446c0052f033e8afea2f2ffbbf1'/>
<id>urn:sha1:a2d08f4876226446c0052f033e8afea2f2ffbbf1</id>
<content type='text'>
Three columns of indent is a fifth of a sixteen-column sidebar, spent
saying 'this belongs to the row above'. The slant says it in none: a
status now starts in the same column its tab's name starts in, and is
set in italic. That also survives the monochrome themes in contrib,
where a colour-only distinction would have had nothing to spend.

tab_status_fg / tab_status_bg are there for themes that do have colour
to spend. The fg follows tab_count when a theme does not name it -- the
same declared/derived split apply_scrolled makes -- so the six themes
in contrib get coherent status rows without a line added to any of
them. The bg is unset by default, because a band is opt-in and 'none'
is what a translucent terminal wants; the row is padded so a band that
*is* set covers the row rather than the words on it.

Two things fell out. The padding loop existed three times and is now
pad_cells. And cb_color wrote #000000 for an unset colour, so
 quietly pinned attach_bg -- 'gray
ink on the terminal's own background', says its comment -- to a black
band. Unset colours are now written commented out, the same trick the
states block uses for what it wants re-derived rather than pinned.
</content>
</entry>
<entry>
<title>tabs: the strip can stand on its side (tab_bar_side left/right)</title>
<updated>2026-09-10T13:40:09Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-10T13:40:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=9c5b3319ac4d9a63404fe0cf0d55320754c982ba'/>
<id>urn:sha1:9c5b3319ac4d9a63404fe0cf0d55320754c982ba</id>
<content type='text'>
A sidebar lists one tab per row down the chosen edge, tab_bar_width
columns wide; the panes trade the strip's row for the sidebar's columns
in tab_area(), the one place that arithmetic lives. The per-tab cell
drawing is extracted from draw_tab_strip (label, rename editor, bell,
hover, drop states, hit) and shared, so every mouse verb -- click,
double-click rename, drag to a tab, drop on + -- works in both shapes
because the hits move with the cells.

The strip's indicators (pane count, prefix badge) take the sidebar's
bottom rows, reserved first under the same budgeting rule the strip
applies from the right. The status line below keeps the full width. A
terminal too narrow to give up the columns falls back to top, re-derived
per frame, so a resize moves the bar without anyone storing state.
</content>
</entry>
</feed>
