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:
parent
5d840a09dd
commit
7f756ba8ee
3 changed files with 485 additions and 219 deletions
34
README.md
34
README.md
|
|
@ -94,20 +94,30 @@ Notes:
|
||||||
|
|
||||||
### Startup order
|
### Startup order
|
||||||
|
|
||||||
The command-line commands are launched one at a time — each starts only after
|
The command-line commands are all launched at the same time, in the order
|
||||||
the previous one's client has connected to the socket — so the tabs appear in
|
you wrote them. Each command runs in its own process group, and when its
|
||||||
exactly the order you wrote them on the command line, no matter which program
|
client connects it claims its own slot by that group — so every tab keeps
|
||||||
takes longest to open its window.
|
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**
|
Until a client connects, its tab is black and listed in the overlay under
|
||||||
command's tab is ready it becomes the active tab and the window reveals it;
|
its **command line** instead of a window title. From the very first frame
|
||||||
nothing steals focus before that, so you don't flicker through every
|
the **last** command's tab is the active one, so you wait only on that
|
||||||
intermediate tab. This "last command wins" rule also means the tab you see on
|
single tab: when it maps, the window reveals it and nothing steals focus
|
||||||
startup is always the one from the last argument.
|
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
|
If a command never connects — e.g. `sleep 999` — the rest of the command
|
||||||
never starts and the window stays black; the compositor itself keeps running
|
line still starts (they all launched together) and the window shows the
|
||||||
normally (`Alt+Shift+T` still works) until you close it.
|
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
|
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
|
the active surface on its own. `Alt+Shift+T` still focuses its fresh
|
||||||
|
|
|
||||||
568
tabbed-wlroots.c
568
tabbed-wlroots.c
|
|
@ -100,10 +100,11 @@ struct tabbed_app {
|
||||||
char *cmd;
|
char *cmd;
|
||||||
pid_t pid;
|
pid_t pid;
|
||||||
/* Process group the *connected* client lives in. Command-line commands
|
/* Process group the *connected* client lives in. Command-line commands
|
||||||
are forked one at a time (see maybe_launch_next), so a CLI tab's slot
|
are forked all at once at startup (see launch_all_cli), so a CLI tab's
|
||||||
is claimed by its own client in command-line order; sub-windows and
|
slot is claimed by whichever command's client connects -- matched by
|
||||||
external clients claim later. A close must signal the group recorded
|
process group at claim time, not by order; sub-windows and external
|
||||||
at claim time rather than the launcher's pid. */
|
clients claim later. A close must signal the group recorded at claim
|
||||||
|
time rather than the launcher's pid. */
|
||||||
pid_t pgid;
|
pid_t pgid;
|
||||||
/* PID of the *connected* client (from claim-time credentials). Used as
|
/* PID of the *connected* client (from claim-time credentials). Used as
|
||||||
the kill target when the client belongs to a process group we do not
|
the kill target when the client belongs to a process group we do not
|
||||||
|
|
@ -129,24 +130,24 @@ struct tabbed_app {
|
||||||
struct wlr_texture *icon_wlr;
|
struct wlr_texture *icon_wlr;
|
||||||
bool icon_resolved; /* true once a load attempt has been made */
|
bool icon_resolved; /* true once a load attempt has been made */
|
||||||
|
|
||||||
/* Slot created by add_tab() (Alt+Shift+T) awaiting its toplevel. Lets
|
/* Slot awaiting its toplevel. True from creation until a client claims
|
||||||
handle_new_toplevel claim exactly this slot instead of an earlier
|
the slot in handle_new_toplevel (matched by launch process group), so
|
||||||
stale free one (a closed tab's hole), so a new terminal keeps its
|
a CLI tab or a fresh Alt+Shift+T terminal is live-but-black while its
|
||||||
"right of the current tab" position. */
|
command starts: it shows in the overlay with its command line as the
|
||||||
|
title, and cycling can land on it (backdrop + title). Cleared on
|
||||||
|
claim, and again when a black tab is closed. A slot without a
|
||||||
|
toplevel and without pending_claim would only exist transiently, so
|
||||||
|
closed tabs leave no holes: remove_app() drops them outright. */
|
||||||
bool pending_claim;
|
bool pending_claim;
|
||||||
|
|
||||||
/* Where a tab's slot came from. ORIGIN_CLI slots hold the command-line
|
/* Where a tab's slot came from. ORIGIN_CLI slots hold the command-line
|
||||||
commands and are forked one at a time (see maybe_launch_next), so the
|
commands and are forked all at once at startup (see launch_all_cli),
|
||||||
tab order mirrors the command line. ORIGIN_ADD_TAB is an Alt+Shift+T
|
so the tab order mirrors the command line. ORIGIN_ADD_TAB is an
|
||||||
terminal. ORIGIN_SUB is a second toplevel from an already-connected
|
Alt+Shift+T terminal. ORIGIN_SUB is a second toplevel from an
|
||||||
client and is glued right of its parent tab. ORIGIN_EXTERNAL is a
|
already-connected client and is glued right of its parent tab.
|
||||||
client we did not launch (e.g. started from our own shell). */
|
ORIGIN_EXTERNAL is a client we did not launch (e.g. started from our
|
||||||
|
own shell). */
|
||||||
enum { ORIGIN_CLI, ORIGIN_ADD_TAB, ORIGIN_SUB, ORIGIN_EXTERNAL } origin;
|
enum { ORIGIN_CLI, ORIGIN_ADD_TAB, ORIGIN_SUB, ORIGIN_EXTERNAL } origin;
|
||||||
/* Stable launch order for ORIGIN_CLI slots. Sub-tab insertion shifts
|
|
||||||
array indices, so the launcher keys on this, not on the array index. */
|
|
||||||
int cli_seq;
|
|
||||||
/* True once this ORIGIN_CLI command has been forked. */
|
|
||||||
bool launched;
|
|
||||||
/* For ORIGIN_SUB: the tab owning the connected client this window
|
/* For ORIGIN_SUB: the tab owning the connected client this window
|
||||||
belongs to (the "parent" tab its slot is glued right of). */
|
belongs to (the "parent" tab its slot is glued right of). */
|
||||||
struct tabbed_app *parent_app;
|
struct tabbed_app *parent_app;
|
||||||
|
|
@ -188,12 +189,12 @@ struct tabbed_server {
|
||||||
struct wl_listener output_destroy;
|
struct wl_listener output_destroy;
|
||||||
struct wl_event_source *ctl_source;
|
struct wl_event_source *ctl_source;
|
||||||
|
|
||||||
struct wlr_xdg_toplevel *active;
|
/* The active tab, by slot. Set to the last command-line command's slot
|
||||||
/* The last command-line command's slot. While the window is still black
|
at startup, so that tab is active -- black, with its command line as
|
||||||
it stays inactive; the moment its toplevel maps it is "crowned" active
|
the overlay title -- from the very first frame and the user only waits
|
||||||
(unless the user already focused something) -- the only map that ever
|
for it. A slot that is active while still pending (no toplevel yet)
|
||||||
auto-focuses on its own. */
|
shows the black backdrop and clear seat focus until it maps. */
|
||||||
struct tabbed_app *crown_app;
|
struct tabbed_app *active_app;
|
||||||
struct tabbed_app **apps;
|
struct tabbed_app **apps;
|
||||||
int num_apps;
|
int num_apps;
|
||||||
int apps_cap;
|
int apps_cap;
|
||||||
|
|
@ -400,15 +401,33 @@ static volatile sig_atomic_t dump_scene_req, dump_host_req;
|
||||||
static int dump_seq;
|
static int dump_seq;
|
||||||
static bool client_dumped = false;
|
static bool client_dumped = false;
|
||||||
|
|
||||||
|
/* Set by sigchld_handler whenever a child exits; the main loop clears it and
|
||||||
|
runs sweep_dead_startup_groups() to drop black tabs whose command exited
|
||||||
|
without ever connecting. A plain flag (set + cleared in the loop) keeps
|
||||||
|
the reaping work out of the signal handler. */
|
||||||
|
static volatile sig_atomic_t sigchld_seen = 0;
|
||||||
|
|
||||||
|
/* A tab is "live" when it can own the show: it has a mapped toplevel, or
|
||||||
|
it is pending (black) -- an unclaimed CLI tab at startup or a fresh
|
||||||
|
Alt+Shift+T terminal. Slots are otherwise removed outright, so a live
|
||||||
|
check is all that's needed to know whether a slot should be shown,
|
||||||
|
cycled to, or handed the active crown. */
|
||||||
|
static bool app_live(struct tabbed_app *app);
|
||||||
|
/* The currently focused toplevel (NULL while the active slot is black). */
|
||||||
|
static struct wlr_xdg_toplevel *active_toplevel(void);
|
||||||
|
static void set_active_app(struct tabbed_app *app);
|
||||||
static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
|
static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
|
||||||
static void render_and_present(void);
|
static void render_and_present(void);
|
||||||
static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
|
static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
|
||||||
static int app_index(struct tabbed_app *app);
|
static int app_index(struct tabbed_app *app);
|
||||||
|
static struct tabbed_app *pick_successor(struct tabbed_app *app,
|
||||||
|
int prefer_right);
|
||||||
|
static void remove_app(int idx);
|
||||||
static int add_tab(const char *cmd);
|
static int add_tab(const char *cmd);
|
||||||
static int cmd_in_path(const char *cmd);
|
static int cmd_in_path(const char *cmd);
|
||||||
static void insert_app_after(struct tabbed_app *app, struct tabbed_app *anchor);
|
static void insert_app_after(struct tabbed_app *app, struct tabbed_app *anchor);
|
||||||
static void launch_client(struct tabbed_app *app);
|
static void launch_client(struct tabbed_app *app);
|
||||||
static void maybe_launch_next(void);
|
static void launch_all_cli(void);
|
||||||
|
|
||||||
/* Logging levels: quiet by default so a plain launch writes nothing to the
|
/* Logging levels: quiet by default so a plain launch writes nothing to the
|
||||||
launching terminal except fatal errors. TABBED_LOG=1 shows status lines,
|
launching terminal except fatal errors. TABBED_LOG=1 shows status lines,
|
||||||
|
|
@ -554,6 +573,14 @@ static int apps_ensure_cap(int n) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool app_live(struct tabbed_app *app) {
|
||||||
|
return app && (app->toplevel || app->pending_claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct wlr_xdg_toplevel *active_toplevel(void) {
|
||||||
|
return server.active_app ? server.active_app->toplevel : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Full shutdown of the nested session: take down the clients first, then
|
/* Full shutdown of the nested session: take down the clients first, then
|
||||||
free our own allocations and exit. Guarded so the various close paths
|
free our own allocations and exit. Guarded so the various close paths
|
||||||
(host window close, last tab closed) only run once. */
|
(host window close, last tab closed) only run once. */
|
||||||
|
|
@ -776,9 +803,10 @@ static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
|
||||||
While the host Alt modifier is held, a passive list of tab titles is
|
While the host Alt modifier is held, a passive list of tab titles is
|
||||||
drawn into the top-left of the host window with the active tab
|
drawn into the top-left of the host window with the active tab
|
||||||
highlighted. Up/Down already cycle tabs while Alt is held, so the
|
highlighted. Up/Down already cycle tabs while Alt is held, so the
|
||||||
highlight follows server.active automatically. Titles are read from the
|
highlight follows server.active_app automatically. Titles are read from
|
||||||
wlr_xdg_toplevel at draw time (terminals retitle themselves), so no
|
the wlr_xdg_toplevel at draw time (terminals retitle themselves); a
|
||||||
per-client listeners are needed. */
|
still-black tab shows its command line instead. No per-client listeners
|
||||||
|
are needed. */
|
||||||
|
|
||||||
static FT_Library overlay_ft_lib;
|
static FT_Library overlay_ft_lib;
|
||||||
static FT_Face overlay_ft_face;
|
static FT_Face overlay_ft_face;
|
||||||
|
|
@ -1402,6 +1430,22 @@ static void fit_text(const char *src, int max_px, char *out, int out_sz)
|
||||||
out[n] = '\0';
|
out[n] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Row title for an overlay slot: the mapped toplevel's title, or the
|
||||||
|
command line for a still-black (pending) tab. */
|
||||||
|
static const char *app_row_title(struct tabbed_app *app) {
|
||||||
|
if (app->toplevel && app->toplevel->title && app->toplevel->title[0])
|
||||||
|
return app->toplevel->title;
|
||||||
|
if (app && app->cmd && app->cmd[0])
|
||||||
|
return app->cmd;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A row in the overlay (both the host-GL and the standalone wlr paths):
|
||||||
|
skip slots that are dead or mid-close; black pending tabs are rows too. */
|
||||||
|
static bool app_overlay_row(struct tabbed_app *app) {
|
||||||
|
return app_live(app) && !app->closing;
|
||||||
|
}
|
||||||
|
|
||||||
static void draw_tab_overlay(void) {
|
static void draw_tab_overlay(void) {
|
||||||
if (!host.overlay_visible || server.num_apps < 1) return;
|
if (!host.overlay_visible || server.num_apps < 1) return;
|
||||||
|
|
||||||
|
|
@ -1412,12 +1456,12 @@ static void draw_tab_overlay(void) {
|
||||||
int max_row_w = 0, n_rows = 0;
|
int max_row_w = 0, n_rows = 0;
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *app = server.apps[i];
|
struct tabbed_app *app = server.apps[i];
|
||||||
if (!app->toplevel) continue;
|
if (!app_overlay_row(app)) continue;
|
||||||
if (!app->icon_resolved)
|
if (!app->icon_resolved)
|
||||||
overlay_icon_load(app);
|
overlay_icon_load(app);
|
||||||
const char *title = app->toplevel->title;
|
const char *title = app_row_title(app);
|
||||||
char fallback[16];
|
char fallback[16];
|
||||||
if (!title || !title[0]) {
|
if (!title) {
|
||||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||||
title = fallback;
|
title = fallback;
|
||||||
}
|
}
|
||||||
|
|
@ -1445,15 +1489,15 @@ static void draw_tab_overlay(void) {
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *app = server.apps[i];
|
struct tabbed_app *app = server.apps[i];
|
||||||
if (!app->toplevel) continue;
|
if (!app_overlay_row(app)) continue;
|
||||||
const char *title = app->toplevel->title;
|
const char *title = app_row_title(app);
|
||||||
char fallback[16];
|
char fallback[16];
|
||||||
if (!title || !title[0]) {
|
if (!title) {
|
||||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||||
title = fallback;
|
title = fallback;
|
||||||
}
|
}
|
||||||
int ry = box_y + boxpad + row * row_h;
|
int ry = box_y + boxpad + row * row_h;
|
||||||
bool active = app->toplevel == server.active;
|
bool active = app == server.active_app;
|
||||||
char row_text[512];
|
char row_text[512];
|
||||||
int budget = max_row_w - icon_sz - icon_gap;
|
int budget = max_row_w - icon_sz - icon_gap;
|
||||||
if (budget < 0) budget = 0;
|
if (budget < 0) budget = 0;
|
||||||
|
|
@ -1477,7 +1521,7 @@ static void draw_tab_overlay(void) {
|
||||||
ix, iy, icon_sz, icon_sz,
|
ix, iy, icon_sz, icon_sz,
|
||||||
host.width, host.height, tint);
|
host.width, host.height, tint);
|
||||||
} else {
|
} else {
|
||||||
/* Generic placeholder: a bordered square for apps with no icon. */
|
/* Generic placeholder for no icon or a still-black tab. */
|
||||||
draw_rect(ix, iy, icon_sz, icon_sz, 0.45f, 0.45f, 0.48f, 0.85f);
|
draw_rect(ix, iy, icon_sz, icon_sz, 0.45f, 0.45f, 0.48f, 0.85f);
|
||||||
draw_rect(ix + 3, iy + 3, icon_sz - 6, icon_sz - 6,
|
draw_rect(ix + 3, iy + 3, icon_sz - 6, icon_sz - 6,
|
||||||
0.07f, 0.07f, 0.09f, 1.0f);
|
0.07f, 0.07f, 0.09f, 1.0f);
|
||||||
|
|
@ -1565,12 +1609,12 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
|
||||||
int max_row_w = 0, n_rows = 0;
|
int max_row_w = 0, n_rows = 0;
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *app = server.apps[i];
|
struct tabbed_app *app = server.apps[i];
|
||||||
if (!app->toplevel) continue;
|
if (!app_overlay_row(app)) continue;
|
||||||
if (!app->icon_resolved)
|
if (!app->icon_resolved)
|
||||||
overlay_icon_load(app);
|
overlay_icon_load(app);
|
||||||
const char *title = app->toplevel->title;
|
const char *title = app_row_title(app);
|
||||||
char fallback[16];
|
char fallback[16];
|
||||||
if (!title || !title[0]) {
|
if (!title) {
|
||||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||||
title = fallback;
|
title = fallback;
|
||||||
}
|
}
|
||||||
|
|
@ -1598,15 +1642,15 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *app = server.apps[i];
|
struct tabbed_app *app = server.apps[i];
|
||||||
if (!app->toplevel) continue;
|
if (!app_overlay_row(app)) continue;
|
||||||
const char *title = app->toplevel->title;
|
const char *title = app_row_title(app);
|
||||||
char fallback[16];
|
char fallback[16];
|
||||||
if (!title || !title[0]) {
|
if (!title) {
|
||||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||||
title = fallback;
|
title = fallback;
|
||||||
}
|
}
|
||||||
int ry = box_y + boxpad + row * row_h;
|
int ry = box_y + boxpad + row * row_h;
|
||||||
bool active = app->toplevel == server.active;
|
bool active = app == server.active_app;
|
||||||
char row_text[512];
|
char row_text[512];
|
||||||
int budget = max_row_w - icon_sz - icon_gap;
|
int budget = max_row_w - icon_sz - icon_gap;
|
||||||
if (budget < 0) budget = 0;
|
if (budget < 0) budget = 0;
|
||||||
|
|
@ -1870,10 +1914,67 @@ server.output = output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nearest live slot, scanning right first (prefer_right) or left first.
|
||||||
|
The scan stops at the array ends -- it never wraps -- so the reflow after
|
||||||
|
a slot is removed matches the old first/last-tab behavior. NULL when
|
||||||
|
`app` is the only live slot. */
|
||||||
|
static struct tabbed_app *pick_successor(struct tabbed_app *app,
|
||||||
|
int prefer_right) {
|
||||||
|
if (!app) return NULL;
|
||||||
|
int died = app_index(app);
|
||||||
|
if (prefer_right) {
|
||||||
|
for (int i = died + 1; i < server.num_apps; i++)
|
||||||
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
||||||
|
return server.apps[i];
|
||||||
|
for (int i = died - 1; i >= 0; i--)
|
||||||
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
||||||
|
return server.apps[i];
|
||||||
|
} else {
|
||||||
|
for (int i = died - 1; i >= 0; i--)
|
||||||
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
||||||
|
return server.apps[i];
|
||||||
|
for (int i = died + 1; i < server.num_apps; i++)
|
||||||
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
||||||
|
return server.apps[i];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop slot `idx` outright: free the tab's allocations, sever any other
|
||||||
|
slot's parent_app reference to it, and compact server.apps[] so a closed
|
||||||
|
tab leaves no hole that a later client could stumble into. The caller
|
||||||
|
must already have handed the active crown elsewhere; remove_app only
|
||||||
|
defends active_app against a slot that was somehow still on it. */
|
||||||
|
static void remove_app(int idx) {
|
||||||
|
if (idx < 0 || idx >= server.num_apps) return;
|
||||||
|
struct tabbed_app *dead = server.apps[idx];
|
||||||
|
if (server.active_app == dead)
|
||||||
|
server.active_app = NULL;
|
||||||
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
|
if (server.apps[i]->parent_app == dead)
|
||||||
|
server.apps[i]->parent_app = NULL;
|
||||||
|
if (dead->close_timer) {
|
||||||
|
wl_event_source_remove(dead->close_timer);
|
||||||
|
dead->close_timer = NULL;
|
||||||
|
}
|
||||||
|
if (host.egl_ok && dead->icon_tex) {
|
||||||
|
glDeleteTextures(1, &dead->icon_tex);
|
||||||
|
dead->icon_tex = 0;
|
||||||
|
}
|
||||||
|
if (dead->icon_wlr) {
|
||||||
|
wlr_texture_destroy(dead->icon_wlr);
|
||||||
|
dead->icon_wlr = NULL;
|
||||||
|
}
|
||||||
|
free(dead->cmd);
|
||||||
|
free(dead);
|
||||||
|
memmove(&server.apps[idx], &server.apps[idx + 1],
|
||||||
|
(size_t)(server.num_apps - idx - 1) * sizeof(*server.apps));
|
||||||
|
server.num_apps--;
|
||||||
|
}
|
||||||
|
|
||||||
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
||||||
(void)data;
|
(void)data;
|
||||||
struct tabbed_app *app = wl_container_of(listener, app, destroy);
|
struct tabbed_app *app = wl_container_of(listener, app, destroy);
|
||||||
struct wlr_xdg_toplevel *toplevel = app->toplevel;
|
|
||||||
app->toplevel = NULL;
|
app->toplevel = NULL;
|
||||||
app->tree = NULL;
|
app->tree = NULL;
|
||||||
|
|
||||||
|
|
@ -1909,42 +2010,35 @@ static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
||||||
app->close_timer = NULL;
|
app->close_timer = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.active != NULL && server.active == toplevel) {
|
if (server.active_app == app) {
|
||||||
int died = -1;
|
server.active_app = NULL;
|
||||||
for (int i = 0; i < server.num_apps; i++)
|
|
||||||
if (server.apps[i] == app) { died = i; break; }
|
|
||||||
server.active = NULL;
|
|
||||||
if (died >= 0) {
|
|
||||||
/* Prefer the neighbor to the left (index died-1); if this is the
|
/* Prefer the neighbor to the left (index died-1); if this is the
|
||||||
first tab, fall through to the rightmost-side neighbor so the
|
first tab, fall through to the right-side neighbor so the tab
|
||||||
tab that was second becomes the new leftmost. */
|
that was second becomes the new leftmost. */
|
||||||
for (int i = died - 1; i >= 0; i--)
|
struct tabbed_app *succ = pick_successor(app, -1);
|
||||||
if (server.apps[i]->toplevel) {
|
if (succ) {
|
||||||
set_active_surface(server.apps[i]->toplevel);
|
set_active_app(succ);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
for (int i = died + 1; i < server.num_apps; i++)
|
|
||||||
if (server.apps[i]->toplevel) {
|
|
||||||
set_active_surface(server.apps[i]->toplevel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* last tab closed: close the host window (nested only) and exit */
|
/* last tab closed: close the host window (nested only) and exit */
|
||||||
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
|
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
|
||||||
if (host.surface) wl_surface_destroy(host.surface);
|
if (host.surface) wl_surface_destroy(host.surface);
|
||||||
shutdown_cleanup();
|
shutdown_cleanup();
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* The slot itself is removed outright: closed tabs leave no hole. */
|
||||||
|
remove_app(app_index(app));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Grace period after the Alt+Delete SIGTERM before SIGKILL escalation. */
|
/* Grace period after the Alt+Delete SIGTERM before SIGKILL escalation. */
|
||||||
#define TABBED_CLOSE_ESCALATE_MS 150
|
#define TABBED_CLOSE_ESCALATE_MS 150
|
||||||
|
|
||||||
/* Escalate an Alt+Delete close to SIGKILL if the client ignored the SIGTERM
|
/* Escalate an Alt+Delete close to SIGKILL if the client ignored the SIGTERM
|
||||||
and its toplevel is still up. The callback lives on the app slot, which is
|
and its toplevel is still up. Live slots are removed at toplevel destroy
|
||||||
never freed at runtime (only in shutdown_cleanup), and handle_toplevel_destroy
|
(which also cancels this timer), so for a mapped tab the callback never
|
||||||
removes the source once the toplevel dies, so a stale callback can never fire
|
fires against a reaped or reused pid. For a black tab there is no
|
||||||
against a reaped or reused pid. */
|
toplevel whose destroy would reap the slot, so its removal happens here,
|
||||||
|
right after the SIGKILL. */
|
||||||
static int close_tab_escalate(void *data) {
|
static int close_tab_escalate(void *data) {
|
||||||
struct tabbed_app *app = data;
|
struct tabbed_app *app = data;
|
||||||
if (app->closing && app_killable(app))
|
if (app->closing && app_killable(app))
|
||||||
|
|
@ -1953,43 +2047,47 @@ static int close_tab_escalate(void *data) {
|
||||||
wl_event_source_remove(app->close_timer);
|
wl_event_source_remove(app->close_timer);
|
||||||
app->close_timer = NULL;
|
app->close_timer = NULL;
|
||||||
}
|
}
|
||||||
|
if (app->closing && !app->toplevel)
|
||||||
|
remove_app(app_index(app));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alt+Delete close of the active tab. The tab vanishes immediately: the
|
/* Alt+Delete close of the active tab. The tab vanishes immediately: the
|
||||||
active surface moves to the right neighbor (or the left neighbor if the
|
active crown moves to the right neighbor (or the left neighbor if the
|
||||||
closed tab was the rightmost) in this same call, and the dying client's
|
closed tab was the rightmost) in this same call. The client's whole
|
||||||
scene node is disabled. The client's whole process group gets SIGTERM, then
|
process group gets SIGTERM, then SIGKILL after a short grace if it ignores
|
||||||
SIGKILL after a short grace if it ignores the TERM (see close_tab_escalate).
|
the TERM (see close_tab_escalate). A closing black tab (still pending)
|
||||||
When the last tab is closed the session tears down via handle_toplevel_destroy
|
drops out of the overlay and cycle right away and is removed by the
|
||||||
once the client's socket drops. */
|
escalation; a closing last tab tears the session down here, since nothing
|
||||||
|
else will. */
|
||||||
static void close_active_tab(void) {
|
static void close_active_tab(void) {
|
||||||
struct tabbed_app *app = app_for_toplevel(server.active);
|
struct tabbed_app *app = server.active_app;
|
||||||
if (!app || !app_killable(app)) return;
|
if (!app || !app_killable(app)) return;
|
||||||
pid_t g = app_signal_group(app);
|
pid_t g = app_signal_group(app);
|
||||||
|
pid_t shown = (g > 0 && g != getpgid(0)) ? g : app->client_pid;
|
||||||
|
|
||||||
app->closing = true;
|
app->closing = true;
|
||||||
|
|
||||||
int died = app_index(app);
|
int died = app_index(app);
|
||||||
struct wlr_xdg_toplevel *next = NULL;
|
|
||||||
/* Prefer the neighbor to the right so the tabs shift left; a rightmost
|
|
||||||
tab falls back to its left neighbor. */
|
|
||||||
for (int i = died + 1; i < server.num_apps; i++)
|
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
|
||||||
next = server.apps[i]->toplevel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!next)
|
|
||||||
for (int i = died - 1; i >= 0; i--)
|
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
|
||||||
next = server.apps[i]->toplevel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (next)
|
|
||||||
set_active_surface(next);
|
|
||||||
/* With no successor this was the last tab; handle_toplevel_destroy exits. */
|
|
||||||
|
|
||||||
pid_t shown = (g > 0 && g != getpgid(0)) ? g : app->client_pid;
|
/* A black tab never fires a toplevel destroy, so drop it from the
|
||||||
|
overlay/cycle immediately; only the escalation timer and the
|
||||||
|
success-removal paths touch it afterwards. */
|
||||||
|
if (!app->toplevel)
|
||||||
|
app->pending_claim = false;
|
||||||
|
|
||||||
|
struct tabbed_app *succ = pick_successor(app, +1);
|
||||||
|
if (succ) {
|
||||||
|
set_active_app(succ);
|
||||||
|
} else {
|
||||||
|
/* Last live tab: tear the session down now. */
|
||||||
|
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: closing tab %d (pid %d)\n",
|
||||||
|
died, (int)shown);
|
||||||
|
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
|
||||||
|
if (host.surface) wl_surface_destroy(host.surface);
|
||||||
|
shutdown_cleanup();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
kill_app(app, SIGTERM);
|
kill_app(app, SIGTERM);
|
||||||
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: closing tab %d (pid %d)\n",
|
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: closing tab %d (pid %d)\n",
|
||||||
died, (int)shown);
|
died, (int)shown);
|
||||||
|
|
@ -2109,7 +2207,7 @@ static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
|
||||||
is_dma,
|
is_dma,
|
||||||
is_dma ? (dma.n_planes ? "" : " planes=0") : "",
|
is_dma ? (dma.n_planes ? "" : " planes=0") : "",
|
||||||
node_on,
|
node_on,
|
||||||
server.active == toplevel);
|
server.active_app == app);
|
||||||
if (is_dma) {
|
if (is_dma) {
|
||||||
tabbed_log(TABBED_LOG_DEBUG, "[diag] dmabuf planes=%d "
|
tabbed_log(TABBED_LOG_DEBUG, "[diag] dmabuf planes=%d "
|
||||||
"format=0x%x mod=0x%llx\n",
|
"format=0x%x mod=0x%llx\n",
|
||||||
|
|
@ -2237,19 +2335,28 @@ static void handle_toplevel_map(struct wl_listener *listener, void *data) {
|
||||||
|
|
||||||
/* A new tab never steals focus on its own. The exceptions: a
|
/* A new tab never steals focus on its own. The exceptions: a
|
||||||
Alt+Shift+T terminal (add_tab), a sub-window whose parent tab is the
|
Alt+Shift+T terminal (add_tab), a sub-window whose parent tab is the
|
||||||
one currently shown, and the one-shot startup crown of the last
|
one currently shown, and the startup-selected last command-line tab
|
||||||
command-line command (only while nothing is focused yet). Every other
|
when it finally maps (it has been the active, black tab all along,
|
||||||
map stays hidden -- the overlay and cycling reach it, but it does not
|
revealed the moment its first frame arrives). Every other map stays
|
||||||
yank the active surface. */
|
hidden -- the overlay and cycling reach it, but it does not yank the
|
||||||
|
active surface. A slot's toplevel is set at claim time (before its
|
||||||
|
map), so "is it already shown?" is whether the scene node is enabled,
|
||||||
|
not whether active_toplevel() is set. */
|
||||||
bool activate = false;
|
bool activate = false;
|
||||||
if (app->origin == ORIGIN_ADD_TAB)
|
if (app->origin == ORIGIN_ADD_TAB)
|
||||||
activate = true;
|
activate = true;
|
||||||
else if (app->origin == ORIGIN_SUB && app->parent_app &&
|
else if (app->origin == ORIGIN_SUB && app->parent_app &&
|
||||||
app->parent_app->toplevel == server.active)
|
app->parent_app->toplevel == active_toplevel())
|
||||||
activate = true;
|
activate = true;
|
||||||
else if (app == server.crown_app && !server.active)
|
else if (app == server.active_app && app->tree &&
|
||||||
|
!app->tree->node.enabled)
|
||||||
activate = true;
|
activate = true;
|
||||||
|
|
||||||
|
/* A slot claimed while it was still black may have been drawn with a
|
||||||
|
placeholder icon; once a real client maps it, re-resolve the icon so
|
||||||
|
the overlay picks up its app_id. */
|
||||||
|
app->icon_resolved = false;
|
||||||
|
|
||||||
if (activate) {
|
if (activate) {
|
||||||
wlr_xdg_toplevel_set_activated(toplevel, true);
|
wlr_xdg_toplevel_set_activated(toplevel, true);
|
||||||
set_active_surface(toplevel);
|
set_active_surface(toplevel);
|
||||||
|
|
@ -2266,18 +2373,28 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
||||||
&cpid, NULL, NULL);
|
&cpid, NULL, NULL);
|
||||||
|
|
||||||
struct tabbed_app *app = NULL;
|
struct tabbed_app *app = NULL;
|
||||||
/* Prefer a slot prepared by add_tab() (Alt+Shift+T): its position was
|
/* A slot awaiting its toplevel -- an unclaimed command-line command or
|
||||||
chosen to sit right next to the tab the user launched it from, so an
|
a fresh Alt+Shift+T terminal -- belongs to exactly ONE client: the one
|
||||||
earlier stale free slot (a closed tab's hole) must not steal it. */
|
WE launched for it. launch_client() runs each command in its own
|
||||||
for (int i = 0; i < server.num_apps && !app; i++)
|
process group (setpgid(0,0)), so the connected client's group id
|
||||||
if (!server.apps[i]->toplevel && server.apps[i]->pending_claim)
|
equals the launcher pid app->pid, including through a shell wrapper
|
||||||
app = server.apps[i];
|
(`sh -c "foot -e htop"`). Matching by group -- not exact pid -- is
|
||||||
|
what keeps a wrapped command on its own command-line slot. A client
|
||||||
|
from a group we never created (an external one) never matches, so it
|
||||||
|
can't steal a still-loading tab's position. */
|
||||||
|
pid_t pgid = cpid > 0 ? getpgid(cpid) : -1;
|
||||||
|
for (int i = 0; i < server.num_apps && !app; i++) {
|
||||||
|
struct tabbed_app *a = server.apps[i];
|
||||||
|
if (a->toplevel || !a->pending_claim) continue;
|
||||||
|
if (a->pid > 0 && a->pid == pgid)
|
||||||
|
app = a;
|
||||||
|
}
|
||||||
/* A second toplevel from a client that already owns a tab is a sub-window
|
/* A second toplevel from a client that already owns a tab is a sub-window
|
||||||
of that tab: give it its own slot glued right after its parent (the
|
of that tab: give it its own slot glued right after its parent (the
|
||||||
protocol parent, else the owning tab), never touching a reserved
|
protocol parent, else the owning tab), never touching a reserved
|
||||||
pending or free CLI slot. Command-line commands are forked one at a
|
pending slot. Command-line commands are all forked at startup (see
|
||||||
time (see maybe_launch_next), so their clients never match here and
|
launch_all_cli); their clients match the pending loop above, and only
|
||||||
claim their own slots in command-line order below. */
|
then. */
|
||||||
if (!app && cpid > 0) {
|
if (!app && cpid > 0) {
|
||||||
struct tabbed_app *owner = sub_window_owner(cpid);
|
struct tabbed_app *owner = sub_window_owner(cpid);
|
||||||
if (owner) {
|
if (owner) {
|
||||||
|
|
@ -2294,16 +2411,19 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
||||||
app = sub;
|
app = sub;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Claim the first free slot: the serial CLI command that just connected
|
/* No pending slot matched (an external client, or one from a group we
|
||||||
lands on its own command-line slot (later commands have not even been
|
never created). Closed tabs are removed outright, so there are no
|
||||||
forked yet, so nothing can claim ahead of it), or an external client
|
free holes to fall back into; this loop only catches a slot that is
|
||||||
started from our own shell takes the earliest free hole. */
|
transiently without a toplevel and not reserved, so it usually falls
|
||||||
|
through to the ORIGIN_EXTERNAL append below. */
|
||||||
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) {
|
struct tabbed_app *a = server.apps[i];
|
||||||
app = server.apps[i];
|
if (!a->toplevel && !a->pending_claim && !a->closing) {
|
||||||
|
app = a;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!app) {
|
if (!app) {
|
||||||
/* No slot for this toplevel (an external client with every slot
|
/* No slot for this toplevel (an external client with every slot
|
||||||
taken). Give it its own tab so it appears on top and stays
|
taken). Give it its own tab so it appears on top and stays
|
||||||
|
|
@ -2318,7 +2438,7 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
||||||
struct tabbed_app *pa = toplevel->parent
|
struct tabbed_app *pa = toplevel->parent
|
||||||
? app_for_toplevel(toplevel->parent) : NULL;
|
? app_for_toplevel(toplevel->parent) : NULL;
|
||||||
if (!pa)
|
if (!pa)
|
||||||
pa = app_for_toplevel(server.active);
|
pa = app_for_toplevel(active_toplevel());
|
||||||
insert_app_after(extra, pa);
|
insert_app_after(extra, pa);
|
||||||
app = extra;
|
app = extra;
|
||||||
}
|
}
|
||||||
|
|
@ -2332,15 +2452,14 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
||||||
|
|
||||||
/* [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. The slot was chosen above by
|
can kill the tab that is actually active. The slot was chosen above by
|
||||||
add_tab()'s pending claim, by a serial-launched CLI command landing on
|
the process-group match against a pending command (CLI or add_tab), by
|
||||||
its own slot, by the sub-window/parent rule, or by the free-slot
|
the sub-window/parent rule, or by the ORIGIN_EXTERNAL append. */
|
||||||
fallback. */
|
/* The client that actually occupies this slot may be the command we
|
||||||
/* The client that actually occupies this slot may be the CLI command that
|
forked, an add_tab() tab, or an external client. Record its process
|
||||||
forked it, an add_tab() tab, or a fallback-claimed extra. Record its
|
group so a close signals the right one -- but only when that group is
|
||||||
process group so a close signals the right one -- but only when that
|
one WE own. An externally started client (e.g. from the compositor's
|
||||||
group is one WE own. An externally started client (e.g. from the
|
own terminal) shares our process group; adopting it would make
|
||||||
compositor's own terminal) shares our process group; adopting it would
|
close/shutdown group-signal the launcher's whole session. Such
|
||||||
make close/shutdown group-signal the launcher's whole session. Such
|
|
||||||
clients are killed by pid. */
|
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;
|
||||||
|
|
@ -2367,10 +2486,6 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
||||||
wl_signal_add(&toplevel->events.request_move, &app->request_move);
|
wl_signal_add(&toplevel->events.request_move, &app->request_move);
|
||||||
app->request_resize.notify = handle_request_resize;
|
app->request_resize.notify = handle_request_resize;
|
||||||
wl_signal_add(&toplevel->events.request_resize, &app->request_resize);
|
wl_signal_add(&toplevel->events.request_resize, &app->request_resize);
|
||||||
|
|
||||||
/* A command-line tab has connected: unblock the launcher so the next
|
|
||||||
command-line command can fork. */
|
|
||||||
maybe_launch_next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== Tab management ===================== */
|
/* ===================== Tab management ===================== */
|
||||||
|
|
@ -2494,54 +2609,77 @@ static void handle_new_popup(struct wl_listener *listener, void *data) {
|
||||||
create_popup(popup, parent_tree);
|
create_popup(popup, parent_tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_active_surface(struct wlr_xdg_toplevel *toplevel) {
|
/* Make `app` the active slot. A mapped slot shows its surface and takes
|
||||||
if (server.active == toplevel) return;
|
keyboard focus; a pending (black) slot shows the backdrop and drops the
|
||||||
|
seat focus until it maps. The old active slot is hidden and deactivated.
|
||||||
|
Called with the same app as already active only to promote it black ->
|
||||||
|
mapped (the one case with something still to do), so callers that want a
|
||||||
|
true no-op must guard first. */
|
||||||
|
static void set_active_app(struct tabbed_app *app) {
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
struct tabbed_app *old = app_for_toplevel(server.active);
|
struct tabbed_app *old = server.active_app;
|
||||||
if (old && old->tree)
|
if (old && old != app && old->toplevel) {
|
||||||
|
if (old->tree)
|
||||||
wlr_scene_node_set_enabled(&old->tree->node, false);
|
wlr_scene_node_set_enabled(&old->tree->node, false);
|
||||||
if (server.active)
|
wlr_xdg_toplevel_set_activated(old->toplevel, false);
|
||||||
wlr_xdg_toplevel_set_activated(server.active, false);
|
}
|
||||||
|
server.active_app = app;
|
||||||
|
|
||||||
server.active = toplevel;
|
if (app->toplevel) {
|
||||||
wlr_xdg_toplevel_set_activated(toplevel, true);
|
wlr_xdg_toplevel_set_activated(app->toplevel, true);
|
||||||
|
|
||||||
struct tabbed_app *app = app_for_toplevel(server.active);
|
|
||||||
if (app && app->tree) {
|
|
||||||
position_toplevel(app);
|
position_toplevel(app);
|
||||||
|
if (app->tree)
|
||||||
wlr_scene_node_set_enabled(&app->tree->node, true);
|
wlr_scene_node_set_enabled(&app->tree->node, true);
|
||||||
if (tabbed_log_level() >= TABBED_LOG_DEBUG)
|
if (tabbed_log_level() >= TABBED_LOG_DEBUG)
|
||||||
tabbed_log(TABBED_LOG_DEBUG, "[diag] active app=%d surface=%dx%d "
|
tabbed_log(TABBED_LOG_DEBUG, "[diag] active app=%d surface=%dx%d "
|
||||||
"node on\n", app_index(app),
|
"node on\n", app_index(app),
|
||||||
toplevel->base->surface->current.width,
|
app->toplevel->base->surface->current.width,
|
||||||
toplevel->base->surface->current.height);
|
app->toplevel->base->surface->current.height);
|
||||||
} else if (tabbed_log_level() >= TABBED_LOG_DEBUG) {
|
} else if (tabbed_log_level() >= TABBED_LOG_DEBUG) {
|
||||||
tabbed_log(TABBED_LOG_DEBUG, "[diag] active app=%d has NO scene node!\n",
|
tabbed_log(TABBED_LOG_DEBUG, "[diag] active slot=%d (black, awaiting map)\n",
|
||||||
app ? app_index(app) : -1);
|
app_index(app));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In standalone the seat keyboard is the emitting native device (set by
|
/* In standalone the seat keyboard is the emitting native device (set by
|
||||||
the standalone handlers); fall back to the virtual keyboard otherwise
|
the standalone handlers); fall back to the virtual keyboard otherwise
|
||||||
so the nested/ctl path keeps using it. */
|
so the nested/ctl path keeps using it. A black active tab has no
|
||||||
|
surface to enter, so clear the focus instead of leaving a stale one. */
|
||||||
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
||||||
if (!kbd) kbd = &server.keyboard;
|
if (!kbd) kbd = &server.keyboard;
|
||||||
wlr_seat_keyboard_notify_enter(server.seat, toplevel->base->surface,
|
if (app->toplevel)
|
||||||
|
wlr_seat_keyboard_notify_enter(server.seat,
|
||||||
|
app->toplevel->base->surface,
|
||||||
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
||||||
|
else
|
||||||
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void set_active_surface(struct wlr_xdg_toplevel *toplevel) {
|
||||||
|
struct tabbed_app *app = app_for_toplevel(toplevel);
|
||||||
|
if (!app) return;
|
||||||
|
/* Promoting the already-active black tab to mapped is the one same-app
|
||||||
|
call that must still run; everything else that's a true no-op returns
|
||||||
|
here, so redundant activations emit no [diag] churn. */
|
||||||
|
if (app == server.active_app && app->toplevel && app->tree &&
|
||||||
|
app->tree->node.enabled)
|
||||||
|
return;
|
||||||
|
set_active_app(app);
|
||||||
|
}
|
||||||
|
|
||||||
static void cycle_tab(int dir) {
|
static void cycle_tab(int dir) {
|
||||||
if (server.num_apps == 0) return;
|
if (server.num_apps == 0) return;
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
for (int i = 0; i < server.num_apps; i++)
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
if (server.apps[i]->toplevel == server.active) {
|
if (server.apps[i] == server.active_app) {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (int k = 1; k <= server.num_apps; k++) {
|
for (int k = 1; k <= server.num_apps; k++) {
|
||||||
int j = ((idx < 0 ? 0 : idx) + k * dir + server.num_apps) % server.num_apps;
|
int j = ((idx < 0 ? 0 : idx) + k * dir + server.num_apps) % server.num_apps;
|
||||||
if (server.apps[j]->toplevel && !server.apps[j]->closing) {
|
if (app_live(server.apps[j]) && !server.apps[j]->closing) {
|
||||||
set_active_surface(server.apps[j]->toplevel);
|
set_active_app(server.apps[j]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2550,18 +2688,18 @@ static void cycle_tab(int dir) {
|
||||||
/* Move the active tab one position up (dir>0: later in the list) or down
|
/* Move the active tab one position up (dir>0: later in the list) or down
|
||||||
(dir<0) within the overlay order, wrapping at the ends. The active
|
(dir<0) within the overlay order, wrapping at the ends. The active
|
||||||
surface is unchanged; only its slot in server.apps[] moves, which the
|
surface is unchanged; only its slot in server.apps[] moves, which the
|
||||||
overlay and cycling both iterate. Only slots that currently show a
|
overlay and cycling both iterate. Only live slots (mapped or pending
|
||||||
toplevel (and aren't mid-close, mirroring cycle_tab) count as positions;
|
black, mirroring cycle_tab) that aren't mid-close count as positions;
|
||||||
with fewer than two of them there is nowhere to move. */
|
with fewer than two of them there is nowhere to move. */
|
||||||
static void reorder_active_tab(int dir) {
|
static void reorder_active_tab(int dir) {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
for (int i = 0; i < server.num_apps; i++)
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing)
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
||||||
n++;
|
n++;
|
||||||
if (n < 2) return;
|
if (n < 2) return;
|
||||||
int from = -1;
|
int from = -1;
|
||||||
for (int i = 0; i < server.num_apps; i++)
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
if (server.apps[i]->toplevel == server.active) {
|
if (server.apps[i] == server.active_app) {
|
||||||
from = i;
|
from = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -2570,25 +2708,25 @@ static void reorder_active_tab(int dir) {
|
||||||
int neighbor = -1;
|
int neighbor = -1;
|
||||||
if (dir > 0) {
|
if (dir > 0) {
|
||||||
for (int i = from + 1; i < server.num_apps; i++)
|
for (int i = from + 1; i < server.num_apps; i++)
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
||||||
neighbor = i;
|
neighbor = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (neighbor < 0)
|
if (neighbor < 0)
|
||||||
for (int i = 0; i < server.num_apps; i++)
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
||||||
neighbor = i;
|
neighbor = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int i = from - 1; i >= 0; i--)
|
for (int i = from - 1; i >= 0; i--)
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
||||||
neighbor = i;
|
neighbor = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (neighbor < 0)
|
if (neighbor < 0)
|
||||||
for (int i = server.num_apps - 1; i >= 0; i--)
|
for (int i = server.num_apps - 1; i >= 0; i--)
|
||||||
if (server.apps[i]->toplevel && !server.apps[i]->closing) {
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
||||||
neighbor = i;
|
neighbor = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -3565,10 +3703,10 @@ static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
||||||
bridge_host_set_selection(server.seat->selection_source);
|
bridge_host_set_selection(server.seat->selection_source);
|
||||||
}
|
}
|
||||||
tabbed_log(TABBED_LOG_DEBUG, "[kbd] enter surface=%p serial=%u active=%p\n",
|
tabbed_log(TABBED_LOG_DEBUG, "[kbd] enter surface=%p serial=%u active=%p\n",
|
||||||
(void *)surface, serial, (void *)server.active);
|
(void *)surface, serial, (void *)active_toplevel());
|
||||||
if (server.active)
|
struct wlr_xdg_toplevel *tl = active_toplevel();
|
||||||
wlr_seat_keyboard_notify_enter(server.seat,
|
if (tl)
|
||||||
server.active->base->surface,
|
wlr_seat_keyboard_notify_enter(server.seat, tl->base->surface,
|
||||||
server.keyboard.keycodes, server.keyboard.num_keycodes,
|
server.keyboard.keycodes, server.keyboard.num_keycodes,
|
||||||
&server.keyboard.modifiers);
|
&server.keyboard.modifiers);
|
||||||
}
|
}
|
||||||
|
|
@ -4973,39 +5111,82 @@ static void launch_client(struct tabbed_app *app) {
|
||||||
}
|
}
|
||||||
if (pid > 0) {
|
if (pid > 0) {
|
||||||
app->pid = pid;
|
app->pid = pid;
|
||||||
app->launched = true;
|
|
||||||
tabbed_log(TABBED_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
|
tabbed_log(TABBED_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
|
||||||
app_index(app), pid, app->cmd);
|
app_index(app), pid, app->cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fork the next command-line command, one at a time, gated on the previous
|
/* Fork every command-line command at startup, in command-line order. Each
|
||||||
one's connection. Because only one such client is running at a time, the
|
slot keeps its position because handle_new_toplevel claims it by process
|
||||||
next connector is unambiguously the next command, so the tab order follows
|
group (launch_client runs each command in its own setpgid'd group, whose
|
||||||
the command-line order with no pid matching. A command that never
|
id equals app->pid), so the tab order mirrors the command line no matter
|
||||||
connects stalls the chain (and, for the last command, the startup crown)
|
which client connects first. A command that never connects leaves its
|
||||||
indefinitely, leaving the window black -- accepted behaviour. */
|
tab black; a command whose process group exits without ever connecting is
|
||||||
static void maybe_launch_next(void) {
|
swept away later (see sweep_dead_startup_groups). */
|
||||||
struct tabbed_app *next = NULL;
|
static void launch_all_cli(void) {
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *a = server.apps[i];
|
struct tabbed_app *a = server.apps[i];
|
||||||
if (a->origin != ORIGIN_CLI || a->launched) continue;
|
if (a->origin != ORIGIN_CLI) continue;
|
||||||
if (!next || a->cli_seq < next->cli_seq) next = a;
|
launch_client(a);
|
||||||
}
|
}
|
||||||
if (!next) return;
|
}
|
||||||
if (next->cli_seq > 0) {
|
|
||||||
bool prev_connected = false;
|
/* Drop black tabs whose command's whole process group has exited (e.g.
|
||||||
|
`tabbed-wlroots /bin/false` -- sh runs false, the group empties, and the
|
||||||
|
tab would otherwise stay black forever). Runs from the main loop after a
|
||||||
|
SIGCHLD, never from the handler: it frees slots. Group liveness, not the
|
||||||
|
launcher pid's, decides: `sh -c "cmd &"` returns while cmd is still
|
||||||
|
starting, but cmd holds the group open, so kill(-groupid,0) succeeds and
|
||||||
|
the tab survives. */
|
||||||
|
static void sweep_dead_startup_groups(void) {
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
struct tabbed_app *a = server.apps[i];
|
struct tabbed_app *a = server.apps[i];
|
||||||
if (a->origin == ORIGIN_CLI &&
|
if (a->toplevel || !a->pending_claim || a->closing || a->pid <= 0)
|
||||||
a->cli_seq == next->cli_seq - 1 && a->toplevel) {
|
continue;
|
||||||
prev_connected = true;
|
errno = 0;
|
||||||
|
if (kill(-a->pid, 0) == 0 || errno == EPERM)
|
||||||
|
continue; /* the group still has a member */
|
||||||
|
if (errno != ESRCH)
|
||||||
|
continue;
|
||||||
|
if (server.active_app == a) {
|
||||||
|
server.active_app = NULL;
|
||||||
|
struct tabbed_app *succ = pick_successor(a, -1);
|
||||||
|
if (!succ) succ = pick_successor(a, +1);
|
||||||
|
if (succ)
|
||||||
|
set_active_app(succ);
|
||||||
|
else {
|
||||||
|
/* the last tab died before ever connecting: tear down */
|
||||||
|
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
|
||||||
|
if (host.surface) wl_surface_destroy(host.surface);
|
||||||
|
shutdown_cleanup();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: dropping tab %d (pid %d) "
|
||||||
|
"-- command exited without connecting\n", i, (int)a->pid);
|
||||||
|
remove_app(i);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
/* If the sweep's succession handed active to a slot that this same pass
|
||||||
|
then removed, put the crown back on some live tab (or nothing). */
|
||||||
|
if (!server.active_app) {
|
||||||
|
struct tabbed_app *succ = NULL;
|
||||||
|
for (int i = 0; i < server.num_apps; i++)
|
||||||
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
||||||
|
succ = server.apps[i];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (succ)
|
||||||
|
set_active_app(succ);
|
||||||
}
|
}
|
||||||
if (!prev_connected) return;
|
}
|
||||||
}
|
|
||||||
launch_client(next);
|
/* Drain the SIGCHLD latch and reap dead startup groups; called from both
|
||||||
|
event loops after each dispatch. */
|
||||||
|
static void sweep_dead_startup(void) {
|
||||||
|
if (!sigchld_seen) return;
|
||||||
|
sigchld_seen = 0;
|
||||||
|
sweep_dead_startup_groups();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
|
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
|
||||||
|
|
@ -5022,7 +5203,7 @@ static int add_tab(const char *cmd) {
|
||||||
app->pending_claim = true;
|
app->pending_claim = true;
|
||||||
app->origin = ORIGIN_ADD_TAB;
|
app->origin = ORIGIN_ADD_TAB;
|
||||||
|
|
||||||
insert_app_after(app, app_for_toplevel(server.active));
|
insert_app_after(app, app_for_toplevel(active_toplevel()));
|
||||||
int idx = app_index(app);
|
int idx = app_index(app);
|
||||||
launch_client(app);
|
launch_client(app);
|
||||||
return idx;
|
return idx;
|
||||||
|
|
@ -5042,6 +5223,7 @@ static void crash_handler(int sig, siginfo_t *info, void *ctx) {
|
||||||
|
|
||||||
static void sigchld_handler(int sig) {
|
static void sigchld_handler(int sig) {
|
||||||
(void)sig;
|
(void)sig;
|
||||||
|
sigchld_seen = 1;
|
||||||
int status;
|
int status;
|
||||||
pid_t pid;
|
pid_t pid;
|
||||||
if (tabbed_log_level() >= TABBED_LOG_DEBUG) {
|
if (tabbed_log_level() >= TABBED_LOG_DEBUG) {
|
||||||
|
|
@ -5547,13 +5729,11 @@ int main(int argc, char *argv[]) {
|
||||||
app->cmd = strdup(default_tab ? term_cmd : appv[i]);
|
app->cmd = strdup(default_tab ? term_cmd : appv[i]);
|
||||||
app->pid = -1;
|
app->pid = -1;
|
||||||
app->origin = ORIGIN_CLI;
|
app->origin = ORIGIN_CLI;
|
||||||
app->cli_seq = i;
|
/* Live-but-black until the command claims the slot with a toplevel. */
|
||||||
|
app->pending_claim = true;
|
||||||
server.apps[i] = app;
|
server.apps[i] = app;
|
||||||
}
|
}
|
||||||
server.num_apps = total_apps;
|
server.num_apps = total_apps;
|
||||||
/* The last command-line command is the one the startup crown singles
|
|
||||||
out once its toplevel maps. */
|
|
||||||
server.crown_app = server.apps[total_apps - 1];
|
|
||||||
|
|
||||||
for (int i = 0; i < server.num_apps; i++) {
|
for (int i = 0; i < server.num_apps; i++) {
|
||||||
if (server.apps[i]->cmd[0] == '-') {
|
if (server.apps[i]->cmd[0] == '-') {
|
||||||
|
|
@ -5573,9 +5753,15 @@ int main(int argc, char *argv[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Launch the command-line commands one at a time, gated on each one
|
|
||||||
connecting (see maybe_launch_next). */
|
/* The last command-line command is active from the very first frame:
|
||||||
maybe_launch_next();
|
black, titled by its command line, until the client maps. The user
|
||||||
|
waits only on that one tab. */
|
||||||
|
set_active_app(server.apps[total_apps - 1]);
|
||||||
|
|
||||||
|
/* Fork every command-line command at once; the process-group match in
|
||||||
|
handle_new_toplevel gives each its own slot on arrival. */
|
||||||
|
launch_all_cli();
|
||||||
|
|
||||||
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
||||||
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "
|
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "
|
||||||
|
|
@ -5594,6 +5780,7 @@ int main(int argc, char *argv[]) {
|
||||||
ret = wl_event_loop_dispatch(server.loop, -1);
|
ret = wl_event_loop_dispatch(server.loop, -1);
|
||||||
} while (ret == -1 && errno == EINTR);
|
} while (ret == -1 && errno == EINTR);
|
||||||
wl_display_flush_clients(server.wl_display);
|
wl_display_flush_clients(server.wl_display);
|
||||||
|
sweep_dead_startup();
|
||||||
if (frame_pending || needs_redraw) {
|
if (frame_pending || needs_redraw) {
|
||||||
frame_pending = false;
|
frame_pending = false;
|
||||||
needs_redraw = false;
|
needs_redraw = false;
|
||||||
|
|
@ -5634,6 +5821,7 @@ int main(int argc, char *argv[]) {
|
||||||
wl_event_loop_dispatch(server.loop, 0);
|
wl_event_loop_dispatch(server.loop, 0);
|
||||||
}
|
}
|
||||||
wl_display_flush_clients(server.wl_display);
|
wl_display_flush_clients(server.wl_display);
|
||||||
|
sweep_dead_startup();
|
||||||
wl_display_dispatch_pending(host.display);
|
wl_display_dispatch_pending(host.display);
|
||||||
|
|
||||||
/* The headless output fires a frame signal at ~60Hz; render then
|
/* The headless output fires a frame signal at ~60Hz; render then
|
||||||
|
|
|
||||||
|
|
@ -249,13 +249,15 @@ class Session:
|
||||||
pytest.fail(f"active app never changed away from {away_from}")
|
pytest.fail(f"active app never changed away from {away_from}")
|
||||||
|
|
||||||
def wait_any_active(self, timeout=10.0):
|
def wait_any_active(self, timeout=10.0):
|
||||||
"""Wait until the startup crown has activated a tab, return its index.
|
"""Wait until the startup-selected tab has activated (its first map),
|
||||||
|
return its index.
|
||||||
|
|
||||||
Only the last command-line command is ever auto-activated on boot
|
The last command-line command is active from the very first frame --
|
||||||
(the "crown"); every earlier tab maps hidden with no `active app=`
|
black, titled by its command line -- and is the only tab that ever
|
||||||
line, so the first active line is the settled tab -- there is no
|
auto-activates on boot; every earlier tab maps hidden with no
|
||||||
churn to wait out. For the fixtures' N-tab sessions the crown is
|
``active app=`` line. The first active line is therefore the settled
|
||||||
always index N-1.
|
tab, with no churn to wait out. For the fixtures' N-tab sessions
|
||||||
|
that tab is index N-1.
|
||||||
"""
|
"""
|
||||||
rx = re.compile(r"active app=(\d+)")
|
rx = re.compile(r"active app=(\d+)")
|
||||||
since = time.monotonic()
|
since = time.monotonic()
|
||||||
|
|
@ -264,7 +266,23 @@ class Session:
|
||||||
if m:
|
if m:
|
||||||
return int(m[-1])
|
return int(m[-1])
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
pytest.fail("no tab was ever activated (startup crown never fired)")
|
pytest.fail("no tab was ever activated (startup tab never mapped)")
|
||||||
|
|
||||||
|
def wait_active_frame(self, timeout=10.0):
|
||||||
|
"""Wait until the active tab has painted a real frame: the composed
|
||||||
|
(or host-presented) frame's centre is no longer the black backdrop.
|
||||||
|
Guards single-read pixel asserts that need black to mean "overlay
|
||||||
|
shown" and probe areas to mean "pure client colour"."""
|
||||||
|
since = time.monotonic()
|
||||||
|
while time.monotonic() - since < timeout:
|
||||||
|
path = self.dump_host()
|
||||||
|
if path:
|
||||||
|
w, h, px = self.ppm_rgb(path)
|
||||||
|
i = 3 * (h // 2 * w + w // 2)
|
||||||
|
if (px[i], px[i + 1], px[i + 2]) != (0, 0, 0):
|
||||||
|
return
|
||||||
|
time.sleep(0.1)
|
||||||
|
pytest.fail("active tab never painted a frame (centre stayed black)")
|
||||||
|
|
||||||
def wait_app_active(self, index, timeout=10.0):
|
def wait_app_active(self, index, timeout=10.0):
|
||||||
"""Wait until the single app `index` reports itself active (for
|
"""Wait until the single app `index` reports itself active (for
|
||||||
|
|
@ -508,6 +526,7 @@ def test_standalone_headless_alt_overlay_scene(standalone_session):
|
||||||
whose centre darkens while Alt is held."""
|
whose centre darkens while Alt is held."""
|
||||||
s = standalone_session
|
s = standalone_session
|
||||||
s.wait_any_active()
|
s.wait_any_active()
|
||||||
|
s.wait_active_frame()
|
||||||
|
|
||||||
# Before Alt: the frame should be the client content (no dark overlay).
|
# Before Alt: the frame should be the client content (no dark overlay).
|
||||||
w, h, px = s.ppm_rgb(s.dump_host())
|
w, h, px = s.ppm_rgb(s.dump_host())
|
||||||
|
|
@ -577,6 +596,7 @@ def test_standalone_headless_cursor_visible(standalone_session):
|
||||||
through the same flip."""
|
through the same flip."""
|
||||||
s = standalone_session
|
s = standalone_session
|
||||||
s.wait_any_active()
|
s.wait_any_active()
|
||||||
|
s.wait_active_frame()
|
||||||
assert s.proc.poll() is None
|
assert s.proc.poll() is None
|
||||||
|
|
||||||
def patch_at(path, cx, cy, r=22):
|
def patch_at(path, cx, cy, r=22):
|
||||||
|
|
@ -636,6 +656,7 @@ def test_standalone_headless_cursor_relative_motion(standalone_session):
|
||||||
the cursor."""
|
the cursor."""
|
||||||
s = standalone_session
|
s = standalone_session
|
||||||
s.wait_any_active()
|
s.wait_any_active()
|
||||||
|
s.wait_active_frame()
|
||||||
assert s.proc.poll() is None
|
assert s.proc.poll() is None
|
||||||
|
|
||||||
def patch_at(path, cx, cy, r=22):
|
def patch_at(path, cx, cy, r=22):
|
||||||
|
|
@ -704,6 +725,7 @@ def test_standalone_headless_screen_mode_cursor_stays_aligned():
|
||||||
"build/wltest '#ff0000'")
|
"build/wltest '#ff0000'")
|
||||||
try:
|
try:
|
||||||
s.wait_app_active(0)
|
s.wait_app_active(0)
|
||||||
|
s.wait_active_frame()
|
||||||
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
|
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
|
||||||
|
|
||||||
def patch_at(path, cx, cy, r=22):
|
def patch_at(path, cx, cy, r=22):
|
||||||
|
|
@ -1155,6 +1177,7 @@ def test_alt_holds_tab_overlay(session):
|
||||||
box pixel appears in the centered window while Alt is held and reverts
|
box pixel appears in the centered window while Alt is held and reverts
|
||||||
after release."""
|
after release."""
|
||||||
session.wait_any_active()
|
session.wait_any_active()
|
||||||
|
session.wait_active_frame()
|
||||||
|
|
||||||
def has_dark(w, h, px):
|
def has_dark(w, h, px):
|
||||||
for y in range(h // 4, 3 * h // 4):
|
for y in range(h // 4, 3 * h // 4):
|
||||||
|
|
@ -1737,3 +1760,48 @@ def test_missing_arg_fails():
|
||||||
assert r.returncode != 0
|
assert r.returncode != 0
|
||||||
assert b"definitely-no-such-cmd-xyz" in (r.stdout + r.stderr)
|
assert b"definitely-no-such-cmd-xyz" in (r.stdout + r.stderr)
|
||||||
assert b"not found in PATH" in (r.stdout + r.stderr)
|
assert b"not found in PATH" in (r.stdout + r.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_black_tab_active_last():
|
||||||
|
"""Alt+Delete on a black tab (its command has not connected yet) closes
|
||||||
|
it outright: the surviving tab becomes active, the black slot is removed
|
||||||
|
(closed tabs leave no hole), and closing the survivor exits cleanly."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
# CLI #0 (RED) maps hidden; the active tab is the still-black `sleep`.
|
||||||
|
s = _nested_session(f"{WL} '{RED}'", "sleep 999")
|
||||||
|
try:
|
||||||
|
s.wait_for(r"\[diag\] map app=0")
|
||||||
|
s.chord(ALT, DELETE)
|
||||||
|
s.wait_for(r"tabbed-wlroots: closing tab 1")
|
||||||
|
# The black tab is gone; the RED survivor becomes active in its place.
|
||||||
|
s.wait_for(r"active app=0")
|
||||||
|
assert s.proc.poll() is None, \
|
||||||
|
"compositor exited after closing the black tab"
|
||||||
|
|
||||||
|
# Closing the last live tab tears the session down cleanly.
|
||||||
|
m = re.search(r"\[claim\] app=0 pid=(\d+)", s.read_log())
|
||||||
|
assert m, "RED tab never claimed a slot"
|
||||||
|
os.kill(int(m.group(1)), signal.SIGTERM)
|
||||||
|
s.proc.wait(timeout=5)
|
||||||
|
assert s.proc.returncode == 0, \
|
||||||
|
f"compositor exited with {s.proc.returncode} after last tab closed"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_never_connecting_drops_tab():
|
||||||
|
"""A command whose whole process group exits without ever connecting
|
||||||
|
(`/bin/false`) is swept away instead of leaving a permanent black tab;
|
||||||
|
the surviving command's tab stays active."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
s = _nested_session("/bin/false", f"{WL} '{BLUE}'")
|
||||||
|
try:
|
||||||
|
s.wait_for(r"dropping tab 0")
|
||||||
|
# The surviving tab claims the vacated index and becomes active.
|
||||||
|
s.wait_for(r"active app=0")
|
||||||
|
assert s.proc.poll() is None, \
|
||||||
|
"compositor exited after dropping the never-connecting tab"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue