Launch CLI tabs serially at the connection gate; crown the last command

Commands fork one at a time, each gated on the previous one's client
connecting, so tab order always mirrors the command-line order (no pid
matching, no /proc ancestry).  The window stays black until the last
command's tab maps, which is then crowned active.  A new tab never
steals focus on its own: only Ctrl+Shift+T terminals, sub-windows of
the tab currently shown, and the startup crown activate.  Sub-windows
(a second toplevel from an already-connected client) get their own slot
glued right after their parent tab.  A command that never connects
stalls the chain and the crown, leaving the window black (accepted).

Tests: wait_any_active waits for the single deterministic crown; an
externally-spawned client is now born hidden (cycle to it).  Add wltest
--extra (same-client second toplevel) plus tests for sub-window
placement/focus and Ctrl+Shift+T focus-on-map.  Update README.
This commit is contained in:
Andrew Hamilton 2026-08-18 11:20:29 +10:00
parent 0e57ced16c
commit 13154c78b5
4 changed files with 397 additions and 88 deletions

View file

@ -92,6 +92,29 @@ Notes:
only closes their tab; closing the host tabbed-wlroots window, or sending
SIGINT/SIGTERM, shuts the whole session down and kills all client children.
### 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 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.
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 (`Ctrl+Shift+T` still works) until you close it.
Later tab openings follow the same no-steal rule: a brand-new tab never yanks
the active surface on its own. `Ctrl+Shift+T` still focuses its fresh
terminal, and a sub-window of the tab you are currently looking at becomes
active; every other new window or tab just appears in the list, hidden, until
you cycle to it.
## Key bindings
| Key | Action |
@ -127,8 +150,8 @@ librsvg); tabs without a resolvable icon get a generic placeholder.
`Ctrl+Shift+T` launches a fresh terminal (`$TERMINAL`, default
`alacritty`) as a new tab placed immediately to the right of the current tab,
and it becomes the active tab once it maps — exactly as if its command had been
given on the command line.
and it becomes the active tab once it maps (the one exception to the
no-map-steal rule at startup, along with sub-windows of the shown tab).
The tab-cycling hotkey only emits a physical press for the first tab switch;
repeats are synthesized, so holding `Alt+Up/Down` keeps stepping through

View file

@ -99,10 +99,11 @@
struct tabbed_app {
char *cmd;
pid_t pid;
/* Process group the *connected* client lives in. Slots are claimed in
connection order (see handle_new_toplevel), which is not the launch
order used for app->pid, so a close must signal the group recorded at
claim time rather than the launcher's pid. */
/* 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
is claimed by its own client in command-line order; sub-windows and
external clients claim later. A close must signal the group recorded
at claim time rather than the launcher's pid. */
pid_t pgid;
/* 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
@ -134,6 +135,22 @@ struct tabbed_app {
"right of the current tab" position. */
bool pending_claim;
/* 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
tab order mirrors the command line. ORIGIN_ADD_TAB is a Ctrl+Shift+T
terminal. ORIGIN_SUB is a second toplevel from an already-connected
client and is glued right of its parent tab. 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;
/* 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
belongs to (the "parent" tab its slot is glued right of). */
struct tabbed_app *parent_app;
/* The user closed this tab (Alt+Delete). While set, cycle_tab skips the
slot and the SIGKILL escalation timer (close_timer) may still be armed;
cleared once the toplevel is destroyed or the slot is reused. */
@ -172,6 +189,11 @@ struct tabbed_server {
struct wl_event_source *ctl_source;
struct wlr_xdg_toplevel *active;
/* The last command-line command's slot. While the window is still black
it stays inactive; the moment its toplevel maps it is "crowned" active
(unless the user already focused something) -- the only map that ever
auto-focuses on its own. */
struct tabbed_app *crown_app;
struct tabbed_app **apps;
int num_apps;
int apps_cap;
@ -384,6 +406,9 @@ static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
static int app_index(struct tabbed_app *app);
static int add_tab(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 launch_client(struct tabbed_app *app);
static void maybe_launch_next(void);
/* Logging levels: quiet by default so a plain launch writes nothing to the
launching terminal except fatal errors. TABBED_LOG=1 shows status lines,
@ -437,8 +462,8 @@ static void tabbed_log_level_init(void) {
}
/* The process group to signal for an app: the group its claimed client lives
in (set at claim time; slots are claimed in connection order, not launch
order), falling back to the launcher pid for slots with no client yet. */
in (set at claim time), falling back to the launcher pid for slots with no
client yet. */
static pid_t app_signal_group(struct tabbed_app *app) {
return (app && app->pgid > 0) ? app->pgid : (app ? app->pid : -1);
}
@ -2146,14 +2171,50 @@ static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
}
}
/* Insert `app` in the slot order immediately after `anchor`, shifting the
slots after it right; append at the far right if anchor is NULL or no
longer present. */
static void insert_app_after(struct tabbed_app *app, struct tabbed_app *anchor) {
int at = server.num_apps;
if (anchor)
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i] == anchor) {
at = i + 1;
break;
}
if (at < server.num_apps)
memmove(&server.apps[at + 1], &server.apps[at],
(server.num_apps - at) * sizeof(*server.apps));
server.apps[at] = app;
server.num_apps++;
}
static void handle_toplevel_map(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_app *app = wl_container_of(listener, app, map);
struct wlr_xdg_toplevel *toplevel = app->toplevel;
if (tabbed_log_level() >= TABBED_LOG_DEBUG)
tabbed_log(TABBED_LOG_DEBUG, "[diag] map app=%d\n", app_index(app));
wlr_xdg_toplevel_set_activated(toplevel, true);
set_active_surface(toplevel);
/* A new tab never steals focus on its own. The exceptions: a
Ctrl+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
command-line command (only while nothing is focused yet). Every other
map stays hidden -- the overlay and cycling reach it, but it does not
yank the active surface. */
bool activate = false;
if (app->origin == ORIGIN_ADD_TAB)
activate = true;
else if (app->origin == ORIGIN_SUB && app->parent_app &&
app->parent_app->toplevel == server.active)
activate = true;
else if (app == server.crown_app && !server.active)
activate = true;
if (activate) {
wlr_xdg_toplevel_set_activated(toplevel, true);
set_active_surface(toplevel);
}
}
static void handle_new_toplevel(struct wl_listener *listener, void *data) {
@ -2172,19 +2233,38 @@ 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];
/* 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)
/* 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
protocol parent, else the owning tab), never touching a reserved
pending or free CLI slot. Command-line commands are forked one at a
time (see maybe_launch_next), so their clients never match here and
claim their own slots in command-line order below. */
if (!app && cpid > 0) {
struct tabbed_app *owner = NULL;
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel && server.apps[i]->pid == cpid) {
app = server.apps[i];
if (server.apps[i]->toplevel &&
server.apps[i]->client_pid == cpid) {
owner = 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 (owner) {
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
struct tabbed_app *sub = calloc(1, sizeof(*sub));
if (!sub) return;
sub->pid = -1;
sub->cmd = NULL;
sub->origin = ORIGIN_SUB;
sub->parent_app = owner;
struct tabbed_app *pa = toplevel->parent
? app_for_toplevel(toplevel->parent) : NULL;
insert_app_after(sub, pa ? pa : owner);
app = sub;
}
}
/* Claim the first free slot: the serial CLI command that just connected
lands on its own command-line slot (later commands have not even been
forked yet, so nothing can claim ahead of it), or an external client
started from our own shell takes the earliest free hole. */
if (!app)
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) {
@ -2192,38 +2272,22 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
break;
}
if (!app) {
/* No slot for this toplevel (e.g. a dialog/second window opened by
an already-running client). Give it its own tab so it appears on
top and stays reachable via tab cycling. Place the new tab right
after the tab that created it (protocol parent, else the active
tab); fall back to the far right if neither can be identified. */
/* No slot for this toplevel (an external client with every slot
taken). Give it its own tab so it appears on top and stays
reachable via tab cycling, right after the tab that created it
(protocol parent, else the active tab); far right if unidentified. */
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
app = calloc(1, sizeof(*app));
if (!app) return;
app->pid = -1;
app->cmd = NULL;
int anchor = -1;
struct tabbed_app *extra = calloc(1, sizeof(*extra));
if (!extra) return;
extra->pid = -1;
extra->cmd = NULL;
extra->origin = ORIGIN_EXTERNAL;
struct tabbed_app *pa = toplevel->parent
? app_for_toplevel(toplevel->parent) : NULL;
if (!pa)
pa = app_for_toplevel(server.active);
if (pa)
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i] == pa) {
anchor = i;
break;
}
if (anchor < 0) {
server.apps[server.num_apps] = app;
} else {
int at = anchor + 1;
if (at < server.num_apps)
memmove(&server.apps[at + 1], &server.apps[at],
(server.num_apps - at) * sizeof(*server.apps));
server.apps[at] = app;
}
server.num_apps++;
insert_app_after(extra, pa);
app = extra;
}
app->toplevel = toplevel;
@ -2235,8 +2299,9 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
/* [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
pid match (CLI tabs keep command-line order), by add_tab()'s pending
claim, or by the first-free-slot fallback. */
add_tab()'s pending claim, by a serial-launched CLI command landing on
its own slot, by the sub-window/parent rule, or by the 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
@ -2269,6 +2334,10 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
wl_signal_add(&toplevel->events.request_move, &app->request_move);
app->request_resize.notify = handle_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 ===================== */
@ -4846,7 +4915,7 @@ static int cmd_in_path(const char *cmd) {
return 0;
}
static void launch_client(int idx) {
static void launch_client(struct tabbed_app *app) {
pid_t pid = fork();
if (pid == 0) {
/* Own process group so we can kill the whole client tree on exit. */
@ -4868,16 +4937,46 @@ static void launch_client(int idx) {
if (nullfd > STDERR_FILENO) close(nullfd);
}
}
execlp("/bin/sh", "sh", "-c", server.apps[idx]->cmd, NULL);
execlp("/bin/sh", "sh", "-c", app->cmd, NULL);
_exit(127);
}
if (pid > 0) {
server.apps[idx]->pid = pid;
app->pid = pid;
app->launched = true;
tabbed_log(TABBED_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
idx, pid, server.apps[idx]->cmd);
app_index(app), pid, app->cmd);
}
}
/* Fork the next command-line command, one at a time, gated on the previous
one's connection. Because only one such client is running at a time, the
next connector is unambiguously the next command, so the tab order follows
the command-line order with no pid matching. A command that never
connects stalls the chain (and, for the last command, the startup crown)
indefinitely, leaving the window black -- accepted behaviour. */
static void maybe_launch_next(void) {
struct tabbed_app *next = NULL;
for (int i = 0; i < server.num_apps; i++) {
struct tabbed_app *a = server.apps[i];
if (a->origin != ORIGIN_CLI || a->launched) continue;
if (!next || a->cli_seq < next->cli_seq) next = a;
}
if (!next) return;
if (next->cli_seq > 0) {
bool prev_connected = false;
for (int i = 0; i < server.num_apps; i++) {
struct tabbed_app *a = server.apps[i];
if (a->origin == ORIGIN_CLI &&
a->cli_seq == next->cli_seq - 1 && a->toplevel) {
prev_connected = true;
break;
}
}
if (!prev_connected) return;
}
launch_client(next);
}
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
given as a command-line argument. The tab is inserted in the slot order
right after the currently active tab (falling back to the far right if
@ -4890,23 +4989,11 @@ static int add_tab(const char *cmd) {
app->cmd = strdup(cmd);
app->pid = -1;
app->pending_claim = true;
app->origin = ORIGIN_ADD_TAB;
int at = server.num_apps;
struct tabbed_app *active_app = app_for_toplevel(server.active);
if (active_app)
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i] == active_app) {
at = i + 1;
break;
}
if (at < server.num_apps)
memmove(&server.apps[at + 1], &server.apps[at],
(server.num_apps - at) * sizeof(*server.apps));
server.apps[at] = app;
int idx = at;
server.num_apps++;
launch_client(idx);
insert_app_after(app, app_for_toplevel(server.active));
int idx = app_index(app);
launch_client(app);
return idx;
}
@ -5428,9 +5515,14 @@ int main(int argc, char *argv[]) {
}
app->cmd = strdup(default_tab ? term_cmd : appv[i]);
app->pid = -1;
app->origin = ORIGIN_CLI;
app->cli_seq = i;
server.apps[i] = app;
}
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++) {
if (server.apps[i]->cmd[0] == '-') {
@ -5449,8 +5541,10 @@ int main(int argc, char *argv[]) {
server.apps[i]->cmd, server.apps[i]->cmd);
}
}
launch_client(i);
}
/* Launch the command-line commands one at a time, gated on each one
connecting (see maybe_launch_next). */
maybe_launch_next();
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "

View file

@ -249,21 +249,22 @@ class Session:
pytest.fail(f"active app never changed away from {away_from}")
def wait_any_active(self, timeout=10.0):
"""Wait until the active tab has settled, return its index.
"""Wait until the startup crown has activated a tab, return its index.
The two test clients map in nondeterministic order, so the first
`active app=` line can name the first-mapped tab before the other
maps. Only once *both* tabs have logged an active line is the
last one the settled active tab.
Only the last command-line command is ever auto-activated on boot
(the "crown"); every earlier tab maps hidden with no `active app=`
line, so the first active line is the settled tab -- there is no
churn to wait out. For the fixtures' N-tab sessions the crown is
always index N-1.
"""
rx = re.compile(r"active app=(\d+)")
since = time.monotonic()
while time.monotonic() - since < timeout:
seen = set(rx.findall(self.read_log()))
if {"0", "1"}.issubset(seen):
return self.active()
m = rx.findall(self.read_log())
if m:
return int(m[-1])
time.sleep(0.1)
pytest.fail("both tabs never logged an active line")
pytest.fail("no tab was ever activated (startup crown never fired)")
def wait_app_active(self, index, timeout=10.0):
"""Wait until the single app `index` reports itself active (for
@ -1251,8 +1252,12 @@ def test_tab_death_activates_left_neighbor(session):
session.read_log()).group(1))
pid0 = pids[0]
# Rightmost tab dies: fall to its left neighbor (index 1), not 0.
# The externally-spawned client is born hidden (no map-autofocus
# anymore), so cycle to it: active is 1 (the crown), +1 reaches 2.
session.cycle(+1)
session.wait_active(2)
# Rightmost tab dies: fall to its left neighbor (index 1), not 0.
os.kill(pid2, signal.SIGTERM)
assert session.wait_active_change(2) == 1, \
"active did not fall to index 1 (left neighbor) after index 2 died"
@ -1295,6 +1300,90 @@ def _nested_session(*tabs, extra_args=()):
return Session(proc)
def test_ctrl_shift_t_becomes_active(session):
"""Ctrl+Shift+T's fresh terminal still becomes the active tab on map
(the add_tab exception to the no-steal rule), while other new tabs never
steal focus."""
old_term = os.environ.get("TERMINAL")
os.environ["TERMINAL"] = f"{WL} '#ffff00'"
try:
session.wait_any_active()
before = session.active_count()
session.chord(CTRL | SHIFT, 20)
deadline = time.monotonic() + 15.0
while session.active_count() <= before:
assert session.proc.poll() is None, \
"compositor exited after Ctrl+Shift+T"
assert time.monotonic() < deadline, \
"Ctrl+Shift+T's terminal never became active"
time.sleep(0.1)
assert session.active_count() == before + 1, \
"Ctrl+Shift+T should produce exactly one focus transition"
assert session.proc.poll() is None
finally:
if old_term is None:
os.environ.pop("TERMINAL", None)
else:
os.environ["TERMINAL"] = old_term
def test_sub_window_hidden_when_parent_not_active():
"""A second toplevel from an already-running client is a sub-window of
that tab: it gets its own slot glued right after the parent tab, and
because the parent is not the tab being shown it must NOT steal focus
(no churn -- the crowned tab stays active)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
# Tab 0 (RED) opens a green extra toplevel from the same client.
s = _nested_session(f"{WL} --extra '{GREEN}' '{RED}'", f"{WL} '{BLUE}'")
try:
s.wait_any_active()
p0 = s.launch_pids()[0]
# The extra claims the slot right after tab 0 (app=1) with the same
# client pid as RED. BLUE can also claim app=1 (before the extra
# inserts and shifts it), so match app=1 by pid on the extra.
deadline = time.monotonic() + 15.0
extra_claimed = False
while time.monotonic() < deadline:
for a, p in re.findall(r"\[claim\] app=(\d+) pid=(\d+)",
s.read_log()):
if a == "1" and int(p) == p0:
extra_claimed = True
break
if extra_claimed:
break
assert s.proc.poll() is None, "compositor exited while extra mapped"
time.sleep(0.1)
assert extra_claimed, \
"sub-window never claimed a slot right of its parent"
# The extra must have appeared hidden: only the crown's active line
# may exist.
time.sleep(0.5)
assert len(ACTIVE_RE.findall(s.read_log())) == 1, \
"sub-window stole focus from the crowned tab"
assert s.proc.poll() is None
finally:
s.close()
if s.proc.poll() is None:
s.proc.terminate()
def test_sub_window_activates_when_parent_active():
"""A sub-window whose parent tab is the one being shown becomes the
active tab on map (the second exception to the no-steal rule)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
s = _nested_session(f"{WL} --extra '{GREEN}' '{RED}'")
try:
s.wait_any_active() # the single tab is crowned, then ...
s.wait_for(r"active app=1") # ... its sub-window activates on map.
assert s.proc.poll() is None
finally:
s.close()
if s.proc.poll() is None:
s.proc.terminate()
def test_alt_delete_activates_right_neighbor():
"""Alt+Delete closes the active tab, making the tab to its right active
(SIGTERM, escalated to SIGKILL only if the client lingers). With three

View file

@ -33,7 +33,21 @@ static bool fullscreen;
static int W = 1280, H = 800;
static uint8_t PIXEL[4] = { 0xff, 0x00, 0x00, 0xff }; /* RGBA */
/* Optional second toplevel from the SAME client (--extra <hex>): lets the
smoke tests exercise the compositor's sub-window handling (a second
toplevel of an already-structured tab gets its own slot glued right after
its parent). */
static bool want_extra;
static bool extra_done;
static uint8_t EXTRA_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff };
static int EX_W = 640, EX_H = 400;
static struct wl_surface *ex_surface;
static struct xdg_surface *ex_xdg;
static struct xdg_toplevel *ex_toplevel;
static struct wl_buffer *ex_buffer;
static void create_buffer(void);
static void extra_open(void);
static void keyboard_keymap(void *data, struct wl_keyboard *kb,
uint32_t format, int32_t fd, uint32_t size) {
@ -103,6 +117,10 @@ static void xdg_surface_configure(void *data, struct xdg_surface *xs, uint32_t s
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, W, H);
wl_surface_commit(surface);
if (want_extra && !extra_done) {
extra_done = true;
extra_open();
}
}
static const struct xdg_surface_listener xdg_surface_listener = {
@ -201,22 +219,107 @@ static void create_buffer(void) {
wl_shm_pool_destroy(pool);
}
/* ---- the extra (second, same-process) toplevel ---- */
static void extra_toplevel_configure(void *data, struct xdg_toplevel *xp,
int32_t w, int32_t h, struct wl_array *states) {
(void)data; (void)xp; (void)w; (void)h; (void)states;
}
static const struct xdg_toplevel_listener extra_toplevel_listener = {
.configure = extra_toplevel_configure,
.close = xdg_toplevel_close,
};
static void extra_create_buffer(void) {
if (ex_buffer) return;
size_t stride = (size_t)EX_W * 4;
size_t size = stride * (size_t)EX_H;
int fd;
struct wl_shm_pool *pool = make_pool(size, &fd);
if (!pool) {
fprintf(stderr, "wltest: extra shm pool failed\n");
exit(1);
}
uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
fprintf(stderr, "wltest: extra mmap failed\n");
exit(1);
}
for (int y = 0; y < EX_H; y++) {
uint8_t *row = data + (size_t)y * stride;
for (int x = 0; x < EX_W; x++) {
row[x*4+0] = EXTRA_PIXEL[2]; /* B */
row[x*4+1] = EXTRA_PIXEL[1]; /* G */
row[x*4+2] = EXTRA_PIXEL[0]; /* R */
row[x*4+3] = EXTRA_PIXEL[3]; /* A */
}
}
munmap(data, size);
close(fd);
ex_buffer = wl_shm_pool_create_buffer(pool, 0, EX_W, EX_H,
(int32_t)stride, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
}
static void extra_xdg_configure(void *data, struct xdg_surface *xs, uint32_t serial) {
(void)data;
xdg_surface_ack_configure(xs, serial);
extra_create_buffer();
wl_surface_attach(ex_surface, ex_buffer, 0, 0);
wl_surface_damage_buffer(ex_surface, 0, 0, EX_W, EX_H);
wl_surface_commit(ex_surface);
}
static const struct xdg_surface_listener extra_xdg_listener = {
.configure = extra_xdg_configure,
};
static void extra_open(void) {
ex_surface = wl_compositor_create_surface(compositor);
ex_xdg = xdg_wm_base_get_xdg_surface(wm_base, ex_surface);
ex_toplevel = xdg_surface_get_toplevel(ex_xdg);
xdg_toplevel_set_title(ex_toplevel, "extra");
xdg_surface_add_listener(ex_xdg, &extra_xdg_listener, NULL);
xdg_toplevel_add_listener(ex_toplevel, &extra_toplevel_listener, NULL);
wl_surface_commit(ex_surface);
}
static void print_usage(const char *argv0) {
fprintf(stderr, "usage: %s [--app-id <id>] #RRGGBB [WIDTHxHEIGHT]\n"
" %s shows a solid-color xdg_toplevel, e.g. #ff0000\n",
fprintf(stderr, "usage: %s [--app-id <id>] [--extra #RRGGBB] #RRGGBB [WIDTHxHEIGHT]\n"
" %s shows a solid-color xdg_toplevel, e.g. #ff0000\n"
" --extra also opens a second toplevel from the same client\n",
argv0, argv0);
}
int main(int argc, char **argv) {
const char *app_id = NULL;
int i = 1;
while (i < argc && strcmp(argv[i], "--app-id") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --app-id needs a value\n");
return 1;
while (i < argc) {
if (strcmp(argv[i], "--app-id") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --app-id needs a value\n");
return 1;
}
app_id = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--extra") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --extra needs a value\n");
return 1;
}
const char *x = argv[i + 1];
if (*x == '#') x++;
if (strlen(x) != 6 || sscanf(x, "%2hhx%2hhx%2hhx",
&EXTRA_PIXEL[0], &EXTRA_PIXEL[1], &EXTRA_PIXEL[2]) != 3) {
fprintf(stderr, "wltest: bad extra color '%s'\n", argv[i + 1]);
return 1;
}
want_extra = true;
i += 2;
} else {
break;
}
app_id = argv[i + 1];
i += 2;
}
if (i >= argc || argv[i][0] != '#') {
print_usage(argv[0]);