Launch CLI tabs in parallel; slot-claim by process group

- All command-line tabs fork at startup; the last one is active and shown
  black until its first frame maps, matching the existing README contract
  that the last command's window appears immediately.
- Claim incoming toplevels by process group, not exact pid, so wrapped
  commands (sh -c "foot -e htop") keep their own slot even though the
  sh wrapper forks the real client.
- Closed tabs are fully removed: remove_app() compacts the array, frees the
  slot, clears parent refs, and prefers the left neighbor as the successor
  (right on explicit close, mirroring the old behavior on destroy).
- Black (pending) tabs are live slots: cycle/reorder walk them, activation
  logs '[diag] active slot=N (black, awaiting map)', and Alt+Delete closes
  one outright.  A SIGCHLD-latched sweep drops black tabs whose whole
  process group exits before ever connecting (e.g. /bin/false).
- The map-reveal rule keys on the scene node still being disabled, since a
  slot's toplevel is set at claim time (before its map event).
- Update the standalone/headless and nested tests to wait for a mapped
  frame (the startup tab is active before it maps, so the old 'no tab was
  ever activated' wait no longer fires), and add two tests covering
  Alt+Delete on a black active tab and the never-connecting-command sweep.
This commit is contained in:
Andrew Hamilton 2026-08-18 22:41:27 +10:00
parent 5d840a09dd
commit 7f756ba8ee
3 changed files with 485 additions and 219 deletions

View file

@ -94,20 +94,30 @@ Notes:
### Startup order
The command-line commands are launched one at a time — each starts only after
the previous one's client has connected to the socket — so the tabs appear in
exactly the order you wrote them on the command line, no matter which program
takes longest to open its window.
The command-line commands are all launched at the same time, in the order
you wrote them. Each command runs in its own process group, and when its
client connects it claims its own slot by that group — so every tab keeps
its command-line position even with wrapped commands like `sh -c "foot -e
htop"`, no matter which program takes longest to open its window.
The window stays black while they load (tabs map hidden). When the **last**
command's tab is ready it becomes the active tab and the window reveals it;
nothing steals focus before that, so you don't flicker through every
intermediate tab. This "last command wins" rule also means the tab you see on
startup is always the one from the last argument.
Until a client connects, its tab is black and listed in the overlay under
its **command line** instead of a window title. From the very first frame
the **last** command's tab is the active one, so you wait only on that
single tab: when it maps, the window reveals it and nothing steals focus
before that, so you don't flicker through every intermediate tab. This
"last command wins" rule also means the tab you see on startup is always
the one from the last argument.
If a command never connects — e.g. `sleep 999` — the rest of the command line
never starts and the window stays black; the compositor itself keeps running
normally (`Alt+Shift+T` still works) until you close it.
If a command never connects — e.g. `sleep 999` — the rest of the command
line still starts (they all launched together) and the window shows the
last tab as soon as it is ready. A command whose whole process group exits
without ever connecting (e.g. `tabbed-wlroots /bin/false`) drops its tab
entirely instead of leaving a permanent black hole.
Closing a tab takes it out of the list for good: its slot is removed and
the remaining tabs reflow into place — there are no holes for later clients
to fall into, so externally started windows are always appended, not
absorbed into a dead tab's position.
Later tab openings follow the same no-steal rule: a brand-new tab never yanks
the active surface on its own. `Alt+Shift+T` still focuses its fresh