claim CLI tabs by the command's pid so tab order follows argv order

Command-line tabs fork in a tight loop and connect concurrently, so the
first-free-slot claim left the tab order equal to connection order, an
unpredictable permutation of the command-line order.  Match the new
toplevel's client pid to the slot the compositor forked it from (the pid
survives the sh -c exec), keeping tab order == command-line order even
when clients finish loading out of sequence.  Wrapped commands whose
exec pid differs from the fork pid fall back to first-free-slot as
before.  The last tab to map still becomes the active tab.
This commit is contained in:
Andrew Hamilton 2026-08-18 09:11:07 +10:00
parent 6fa1c0dfb1
commit 0e57ced16c

View file

@ -2160,6 +2160,11 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
(void)listener;
struct wlr_xdg_toplevel *toplevel = data;
pid_t cpid = -1;
if (toplevel->base->client)
wl_client_get_credentials(toplevel->base->client->client,
&cpid, NULL, NULL);
struct tabbed_app *app = NULL;
/* Prefer a slot prepared by add_tab() (Ctrl+Shift+T): its position was
chosen to sit right next to the tab the user launched it from, so an
@ -2167,8 +2172,19 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
for (int i = 0; i < server.num_apps && !app; i++)
if (!server.apps[i]->toplevel && server.apps[i]->pending_claim)
app = server.apps[i];
/* Otherwise claim the first free slot, as before (CLI startup tabs are
claimed in connection order). */
/* A command-line startup tab belongs to the slot whose launch pid matches
the connecting client, so the tab order mirrors the command-line order
even though the clients start concurrently and connect out of sequence.
(add_tab()'s tracked pid can also match its own slot, so the
pending_claim pass above always wins for those.) */
if (!app && cpid > 0)
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel && server.apps[i]->pid == cpid) {
app = server.apps[i];
break;
}
/* Otherwise claim the first free slot: a client opened by a running app,
or a wrapped command whose exec pid differs from the fork pid. */
if (!app)
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) {
@ -2218,20 +2234,16 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
wlr_scene_node_set_enabled(&app->tree->node, false);
/* [claim] ties the app slot to the *connected client's* PID, so tests
can kill the tab that is actually active. App slots are claimed in
connection order (first free slot), which is NOT the launch order
the [launch] lines use when two clients start concurrently. */
pid_t cpid = -1;
if (toplevel->base->client)
wl_client_get_credentials(toplevel->base->client->client,
&cpid, NULL, NULL);
/* The client that actually occupies this slot may have been launched by a
*different* app entry (slots are claimed in connection order, which is
not the launch order used for app->pid). Record its process group so a
close signals the right one -- but only when that group is one WE own.
An externally started client (e.g. from the compositor's own terminal)
shares our process group; adopting it would make close/shutdown group-
signal the launcher's whole session. Such clients are killed by pid. */
can kill the tab that is actually active. The slot was chosen above by
pid match (CLI tabs keep command-line order), by add_tab()'s pending
claim, or by the first-free-slot fallback. */
/* The client that actually occupies this slot may be the CLI command that
forked it, an add_tab() tab, or a fallback-claimed extra. Record its
process group so a close signals the right one -- but only when that
group is one WE own. An externally started client (e.g. from the
compositor's own terminal) shares our process group; adopting it would
make close/shutdown group-signal the launcher's whole session. Such
clients are killed by pid. */
app->client_pid = cpid;
pid_t cpg = cpid > 0 ? getpgid(cpid) : -1;
app->pgid = (cpg > 0 && cpg != getpgid(0)) ? cpg : -1;