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; (void)listener;
struct wlr_xdg_toplevel *toplevel = data; 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; struct tabbed_app *app = NULL;
/* Prefer a slot prepared by add_tab() (Ctrl+Shift+T): its position was /* 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 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++) for (int i = 0; i < server.num_apps && !app; i++)
if (!server.apps[i]->toplevel && server.apps[i]->pending_claim) if (!server.apps[i]->toplevel && server.apps[i]->pending_claim)
app = server.apps[i]; app = server.apps[i];
/* Otherwise claim the first free slot, as before (CLI startup tabs are /* A command-line startup tab belongs to the slot whose launch pid matches
claimed in connection order). */ 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) if (!app)
for (int i = 0; i < server.num_apps; i++) for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) { 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); wlr_scene_node_set_enabled(&app->tree->node, false);
/* [claim] ties the app slot to the *connected client's* PID, so tests /* [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 can kill the tab that is actually active. The slot was chosen above by
connection order (first free slot), which is NOT the launch order pid match (CLI tabs keep command-line order), by add_tab()'s pending
the [launch] lines use when two clients start concurrently. */ claim, or by the first-free-slot fallback. */
pid_t cpid = -1; /* The client that actually occupies this slot may be the CLI command that
if (toplevel->base->client) forked it, an add_tab() tab, or a fallback-claimed extra. Record its
wl_client_get_credentials(toplevel->base->client->client, process group so a close signals the right one -- but only when that
&cpid, NULL, NULL); group is one WE own. An externally started client (e.g. from the
/* The client that actually occupies this slot may have been launched by a compositor's own terminal) shares our process group; adopting it would
*different* app entry (slots are claimed in connection order, which is make close/shutdown group-signal the launcher's whole session. Such
not the launch order used for app->pid). Record its process group so a clients are killed by pid. */
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; app->client_pid = cpid;
pid_t cpg = cpid > 0 ? getpgid(cpid) : -1; pid_t cpg = cpid > 0 ? getpgid(cpid) : -1;
app->pgid = (cpg > 0 && cpg != getpgid(0)) ? cpg : -1; app->pgid = (cpg > 0 && cpg != getpgid(0)) ? cpg : -1;