diff options
| author | user <user@clank> | 2026-08-14 21:32:40 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 21:32:40 +0200 |
| commit | c5f4bb75403df3ca7e0bf6c30981c0cb4e54921e (patch) | |
| tree | 44bddd3b1ba1ea0f4782b50d9c095c6b813f7b7b /README.md | |
| parent | sl0ppty: let a child's exit status survive long enough to be read (diff) | |
| download | slosh-c5f4bb75403df3ca7e0bf6c30981c0cb4e54921e.tar.gz slosh-c5f4bb75403df3ca7e0bf6c30981c0cb4e54921e.tar.xz slosh-c5f4bb75403df3ca7e0bf6c30981c0cb4e54921e.zip | |
sl0ppty: a pane outlives the program that was in it
A pane whose program exited was reaped before the next paint. The pane went,
and with it everything the program had printed -- including, always, the one
line that would have explained why it stopped. Close the last one and the
session went too, so a mistyped command in a fresh session closed the
terminal you were typing in.
Now the pane stays. It keeps its contents, writes one line into its own
backlog saying how it ended -- `[process exited: status 3]`, `signal 9` --
says the same thing in its frame and in the status line, and offers the two
verbs that still mean anything:
╰ exited: status 3 ──────────────────────────────[re-run]─[close]─╯
`[re-run]` runs the same argv in the same cwd in the *same pane*: same id,
same node in the tree, same terminal. Nothing that referred to that pane has
to be told anything, and the run that ended stays above the new one in the
scrollback, which is what makes comparing two runs possible at all. C-a r
and {"cmd":"rerun"} do the same thing; a live pane is refused rather than
restarted, because "run it again" is not a request to kill something.
Those buttons are the OSC 5577 row, not a second mechanism. A dead pane's
own buttons are inert -- a click would write a report into a closed pty --
so the row is given over rather than added to. `close` is listed last, so it
is drawn rightmost and is the one that survives a narrow frame: being unable
to dismiss a dead pane is a trap, being unable to re-run one is an
inconvenience.
`dead` is a shader state now, which D13 said it could never be: it named a
frame that could not be drawn, and now there is one. Dead panes drain to
grey by default, so a wall of them reads at a glance. A collapsed header
says "exited" in words as well, because the shader pass does not reach one
and a flattened tab draws nothing else of a pane.
Two things about this are load-bearing rather than incidental:
- the pty fd is closed the moment EOF is seen. A pane that outlives its
program stays in the poll set, and an fd at EOF is readable forever: the
session would spin a core behind a pane that looks idle.
- the exit status is collected with a bounded wait, never a blocking one.
EOF means the program already closed its fds, so it is on its way out and
a few hundred microseconds wins that race. A blocking waitpid would hang
the whole session on one program that closes its terminal and keeps
running; losing the race costs the words "status 0" and nothing else.
`keep_dead false` restores the old behaviour for a one-shot layout that
wants it. `panes` reports alive/exit_code/exit_signal, so a script can find
the pane whose command failed and rerun it.
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 38 |
1 files changed, 30 insertions, 8 deletions
@@ -49,7 +49,8 @@ sl0ppty cmd '{"cmd":"panes"}' | `C-a f` | find a pane by title, purpose or tab | | `C-a H J K L` or shift+arrows | move the boundary (the pane grows or shrinks accordingly) | | `C-a PgUp` `C-a PgDn` `C-a Home` `C-a End` | scrollback (the wheel does it too) | -| `C-a x` / `C-a d` / `C-a q` | close pane / detach / quit | +| `C-a x` / `C-a r` | close pane / re-run a dead pane's command | +| `C-a d` / `C-a q` | detach / quit | | `C-a C-a` | send a literal `C-a` | The mouse works throughout. **Click a pane's border to split toward it** — the @@ -91,6 +92,24 @@ behind take the space back. Click its name to bring it back. Once a tab is too small and has flattened into a list, being minimised means nothing: every pane is a row down there already. +**A pane outlives its program.** When what was running in a pane exits, the +pane stays where it is with everything it printed still on screen, a line in +its own backlog saying how it ended — `[process exited: status 3]`, +`[process exited: signal 9]` — and two buttons in its bottom border: + +``` +╰ exited: status 3 ───────────────────────────────[re-run]─[close]─╯ +``` + +**`[re-run]` runs the same command again in the same pane** — same id, same +place in the layout, same scrollback, so the run that ended stays above the +new one and you can compare them. `[close]` is what takes the pane away, and +with the last pane, the session. So a mistyped command no longer closes your +terminal, and a command that failed while you were looking elsewhere still has +its error message when you get back. Dead panes drain to grey, so a wall of +them reads at a glance. `keep_dead false` restores the old behaviour, where a +pane vanishes the moment its program exits. + **Select to copy.** Drag over text and release: it is on your clipboard, sent to your terminal as OSC 52 so it works over ssh. Middle click pastes it back, the way a primary selection behaves everywhere else. A program in a pane can @@ -106,14 +125,16 @@ false` turns it off; `bell_mark` chooses the character. **Shaders** are colour passes over a pane's *contents* — never the frame, the title or the tab strip, so chrome stays legible over a pane that has been dimmed underneath it. A `states { }` block says what a pane looks like while -it is in a state — being dragged, somewhere a drag could land, suspended, -scrolled back, or simply not focused. A pane is usually in several at once, so -exactly one wins, the most deliberate rather than the most ambient: +it is in a state — being dragged, somewhere a drag could land, dead, +suspended, scrolled back, or simply not focused. A pane is usually in several +at once, so exactly one wins, the most deliberate rather than the most +ambient: ```kdl states { dragging { } // untouched, in your hand drop_target { grayscale amount=200; dim amount=140 } + dead { grayscale amount=200; dim amount=90 } // its program is over suspended { grayscale amount=180; dim amount=90 } scrolled { tint amount=30 color="#ffcc88" } // you are in the past unfocused { dim amount=90 } @@ -148,8 +169,8 @@ a gap can be dragged. `hints false` turns it off. A **strip along the top** carries the tabs, the pane count and the prefix indicator; a **line along the bottom** says what you are looking at — session, tab, the focused pane's purpose or title, which of the tab's panes it is -(`pane 2/5`), and whether it is scrolled back, on an alternate screen, or not -started yet. Either can be turned off +(`pane 2/5`), and whether it is scrolled back, on an alternate screen, not +started yet, or over (`exited: status 3`). Either can be turned off (`status_bar`, `status_line`), and the panes get the row back. Every colour the compositor draws has its own name in `theme` — the split @@ -234,10 +255,11 @@ $ sl0ppty -s work cmd '{"cmd":"panes"}' {"ok":true,"panes":[{"id":1,"x":2,"y":2,...,"purpose":"agent:main"}]} ``` -`panes tabs snapshot send raw resize split focus close new-tab close-tab +`panes tabs snapshot send raw resize split focus close rerun new-tab close-tab select-tab set-name set-purpose apply-layout reload alive quit`. Panes are addressed by id, so a background tab is scriptable, and a detached session answers exactly as a live -one does. +one does. `panes` reports `alive`, `exit_code` and `exit_signal`, so a script +can find the pane whose command failed and `rerun` it. ## How it is built |
