Kick advertised only pointer+keyboard and ignored every touch device, so
clients never saw a touchscreen and taps were silently dropped.
The seat now advertises WL_SEAT_CAPABILITY_TOUCH. Standalone mode wires
libinput touch devices through wlr_cursor with normalized-absolute warping
and delivers via the seat touch API with slot ids preserved (multi-touch).
Nested mode binds wl_touch when the host seat offers the capability,
forwards events through the same coordinate space as the pointer path, and
ends live sequences cleanly on capability loss or host cancel.
The ctl test feed gains CTL_TOUCH so headless tests can inject taps, and
wltest learns --touch-log to record received events; an end-to-end
regression test proves down/motion/up reach the client with correct slot
and coordinates.
clients_set_fullscreen only told each tab's main surface, so members
stacked before a toggle never learned they were fullscreen, and members
that mapped during fullscreen were never released from it. walk each
tab's member list with a member_set_fullscreen helper (mirroring the app
one) and give xdg members the same re-maximize/resize mains get on the
way out; X11 members keep their own geometry. logged as
"members fullscreen=" next to the existing line.
a grabbed prefix+F11 chord flips the host window's fullscreen state via
libdecor; the resulting configure fans out to every tab through the
usual host->clients propagation, exactly like a host-compositor
fullscreen. latched against key repeats and a no-op standalone, where
there is no host window. plain unprefixed F11 still passes through to
clients.
GDK_BACKEND/QT_QPA_PLATFORM forced to wayland stripped toolkits' fallback,
killing X11-only builds like emacs-gtk ('cannot open display' against a
healthy Xwayland). Children now self-select from WAYLAND_DISPLAY/DISPLAY,
and XAUTHORITY is cleared.
Separately, X11 membership decisions moved from window-creation time to
first map: GTK leader windows never map, yet claimed the tab slot at
creation, leaving a permanent black tab while the real frame was demoted
to a stacked member -- and Alt-cycling onto that phantom main member
crashed on wlr_seat_keyboard_notify_enter(NULL). Never-mapped windows now
free silently; keyboard-entry paths guard on live surfaces. New
tests/xleader.c probe (unmapped leader + mapped frame) locks the behaviour
in.
the tab overlay now lists every window of a tab (main plus stack members)
with a per-window icon and title instead of one icon and title per tab, and
highlights only the active window so the overlay cursor can move across all
of a tab's windows. titles are also centered within each row.
A second toplevel from an already-connected client is a sub-window of
that client's tab. tabs (default) keeps today's behavior: its own slot
glued right of the parent. stack never creates a slot: the window rides
above the parent tab's surface as a scene-tree child, sharing its
visibility, holding the keyboard while the tab is shown, and falling
back to the tab's own surface when it closes. Stacked windows are
invisible to cycling and the overlay; closing the tab closes them.
Overlay rows follow what sits on top of the tab: the row title and icon
resolve from the topmost mapped stacked window and revert when it goes,
with a [title] log line per row-title change for observability.
wltest grows an 'X' key that closes its --extra toplevel so the unstack
path is testable.
test_clipboard_nested_to_host and test_clipboard_host_to_nested were
marked flaky because of the bridge stall now fixed; re-enable them in
the default run. Drop the host-colour readback and never-connecting
tests along with their helpers, and mark the primary-selection bridge
test flaky instead. Update the README's flaky-test note.
test_nested_host_primary_to_kick only has a host->nested primary channel
when the host exposes wlr-data-control v2; a focus-based wl_data_device
host (GNOME/mutter) has no primary bridge at all, so on those desktops the
demand-expect tab can never receive the host primary and the test timed
out. Wait for the bridge_init backend decision and skip unless the
data-control v2 backend is in use; the round-trip is still exercised by
./test-headless.sh, whose headless host is kick itself.
Nested clients that switch the pointer cursor's shape now keep a correct
cursor over kick's window: advertise wp_cursor_shape_manager_v1, apply the
requested shape to the compositor cursor in standalone mode, and forward it
to the host pointer in nested mode via a hand-rolled client call (kick stays
a single C file, so the generated client header is not included). Shapes
are cached per tab and reset to default on tab switch so a text cursor never
leaks between apps. Legacy wl_pointer.set_cursor surfaces are not bridged.
The ctl pointer feed now also drives the nested seat directly, so the harness
can give a nested client a pointer enter. wltest gains --cursor-shape and an
explicit flush (wl_display_dispatch does not flush queued requests), plus the
now-required frame handler. New tests cover apply, tab-switch reset, and
nested forwarding; tail of README documents the protocol and its limits.
The last command-line command is active-and-black from the first frame by
default; --start-cmd-index N (1-based) instead makes command N the one the
program starts focused on. The 1-based user value is converted to the
program's 0-based index as soon as it is parsed; a value outside 1..N is a
startup error (rc 2) naming the flag.
- 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.
Change Ctrl+Shift+T to Alt+Shift+T (Super+Shift+T under --modifier super),
matching the other tab-management chords. It is now grabbed: process_key_event
returns true so the focused client never sees the chord, instead of passing it
through.
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.
Standalone/DRM and labwc-style hosts (e.g. RPi5) keep Super free, while
GNOME steals it, so Alt stays the default. The tab-management prefix
modifier (hold=overlay, Up/Down=cycle, Shift+Up/Down=reorder,
Delete=close) is now chosen at startup from the xkb keymap (Mod1 or
Mod4) with a raw wl_keyboard bit fallback when xkb init fails;
Ctrl+Shift+T is unchanged.
A focused terminal acted on Alt+Shift+Up/Down (selection/typing) because
the reorder chord was forwarded like plain Alt+Up/Down cycling was. Both
are now grabbed: tabbed consumes them so the focused client never sees
them.
Reordering off an end previously swapped the moved tab with the far-end
tab, leaving the middle of the list in place ([A,B,C,D] moving D off the
end gave [D,B,C,A]). reorder_active_tab now shifts every slot between the
two positions, so the whole list rotates: [D,A,B,C]. Adjacent moves are
unchanged (rotation is a swap there).
The reorder test now runs three tabs to distinguish swap from rotation
([R,G,B] moving B off the end must land [B,R,G]) and keeps the
Ctrl+Shift+T anchor-slot check. The clipboard tests got more patience
(retry on stalled wl-paste; longer socket-start wait) since a stalled host
GNOME display was flaking them.
Full suite 5/5 green.
Move the active tab's slot in the tab order without switching the active
surface: the tab stays active and only its position changes, wrapping from
either end, and holding the chord keeps moving it via the same synthesized
repeat as cycling. Only live tabs (a toplevel, not mid-close) count as
positions, mirroring cycle_tab.
The repeat latch is generalized with a repeat_kind so one timer serves both
cycle and reorder. Ctrl+Shift+T inside the moved tab anchors right after
its new position (add_tab uses the active slot's index), which the new
test verifies.
Tests: 5x standalone + full suite 3x green (40 passed, 1 skipped).
Close the active tab on Alt+Delete: the tab vanishes immediately (active
moves to the right neighbor, or the left one for a rightmost tab), the
client's whole process group gets SIGTERM with a short SIGKILL escalation
if it ignores the TERM. Grabbed and repeat-latched so a single hold closes
one tab.
Slots are claimed in connection order (not launch order), so close signals
the process group recorded at claim time. Only groups the compositor owns
are group-signaled; a client started from the compositor's own terminal
shares our group and is killed by pid instead, so teardown can't group-kill
the launcher's session (a pytest run died 143/137 for exactly this).
Tests: settle the active-tab log before cycling (wait_any_active +
cycle_ack quiesce), spawn the extra client in its own session, retry
wl-paste when the host clipboard stalls, and only skip the host-colour
assertion when the host pixel is unobservable. Full suite 5/5 green.
TABBED_MODE=standalone (or a visible WLR_BACKENDS) makes tabbed-wlroots its
own Wayland compositor rendering onto the first native output instead of a
nested host window. DRM+libinput is the default backend set; a headless
backend (created directly, since wlr_backend_autocreate wraps backends in a
multi backend that wlr_headless_add_output rejects) keeps the mode testable
in CI over the existing stdin feed.
Standalone:
- server_init creates a headless or autocreated backend and attaches a
new_input listener; skips seat bridging (no host client to mirror).
- handle_new_output keeps the first output only, drives it at its preferred
mode, and sizes the swapchain to the output instead of a host window.
- Font/icon overlays build CPU wlr_textures (premultiplied ARGB) alongside
the existing GL textures so the tab overlay can be drawn by a second
wlr_renderer pass over the committed output buffer.
- draw_tab_overlay_pass mirrors draw_tab_overlay's geometry with pass
rects/textures; render_standalone builds scene state, applies the overlay,
commits the buffer, and sends frame done. SIGUSR1 now also readbacks the
composited frame (GLES2 FBO, or CPU data-ptr fallback) as /tmp/host_N.ppm.
- Native keyboards/pointers are attached to the seat and share the existing
process_key_event/process_modifiers hotkey handling and the main loop's
standalone branch (blocking dispatch + render on frame/redraw).
The nested mode is unchanged. Adds standalone smoke tests (boot/cycle and
Alt overlay readback under WLR_BACKENDS=headless); full suite 21 passing.
README documents the new mode.
Adds a new-tab binding that launches a terminal (default alacritty,
overridable via the $TERMINAL environment variable, e.g.
TERMINAL="foot -e htop" tabbed-wlroots ...), inserting it right of the
active tab and activating it once it maps.
Also lets handle_new_toplevel claim exactly the add_tab()-prepared slot
instead of an earlier stale free one (a closed tab's hole), so the new
terminal keeps its position next to the tab it was launched from.
Stop grabbing F9/F10/F11 so they reach the focused client (e.g. a
browser's F11); frame dumps move to SIGUSR1 only. Honor client
fullscreen requests and mirror state both ways: a fullscreen client
fullscreens the host window, and a fullscreen host window fullscreens
every tab (anything else re-maximizes them).
wltest gains a wl_keyboard binding that toggles fullscreen on 'F', and
the smoke tests cover the full round trip plus function-key passthrough.
Ctrl+PageUp/PageDown and Ctrl+Tab are needed by real apps (browser tabs,
terminal scrollback), and a tabbed-wlroots hotkey consumes the combo before
the nested client ever sees it. Alt+Up/Down is free on both the
GNOME host (no wm/mutter/shell binding) and labwc, so the keys reach the
compositor instead of being grabbed or swallowed. Alt+Up/Down is used in
some apps but in minor ways. I tried to use Alt+PageUp/PageDown but it
fails on some laptops like mine.
Debian >= trixie packages wlroots versioned (libwlroots-0.20-dev,
pkg-config wlroots-0.20), which falls outside the >=0.19,<0.20 pin, so
the bundled 0.19.1 source build is used. Fix the README apt line
accordingly and build the wlroots subproject with werror=false so the
newer libinput (1.31.3) enum LIBINPUT_SWITCH_KEYPAD_SLIDE is only a
warning, not a fatal -Werror=switch error.