Commit graph

72 commits

Author SHA1 Message Date
e3cba0f7d7 detect sub-windows by process ancestry, not exact pid
A second toplevel is now a sub-window when its client was forked --
directly or transitively -- from an already-connected tab's client,
walking /proc/<pid>/stat ppid links (depth-capped 8).  Exact pid
equality (a client opening a second toplevel) remains the depth-0
case.  Fixes apps launched from inside a terminal tab (e.g.
gnome-calculator) that previously mapped as external, hidden, and
never activated.
2026-08-18 11:46:03 +10:00
13154c78b5 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.
2026-08-18 11:20:29 +10:00
0e57ced16c claim CLI tabs by the command's pid so tab order follows argv order
Command-line tabs fork in a tight loop and connect concurrently, so the
first-free-slot claim left the tab order equal to connection order, an
unpredictable permutation of the command-line order.  Match the new
toplevel's client pid to the slot the compositor forked it from (the pid
survives the sh -c exec), keeping tab order == command-line order even
when clients finish loading out of sequence.  Wrapped commands whose
exec pid differs from the fork pid fall back to first-free-slot as
before.  The last tab to map still becomes the active tab.
2026-08-18 09:11:07 +10:00
6fa1c0dfb1 standalone: force whole-output damage when the overlay shrinks on tab close
Deleting a tab while the overlay is up drops its row, shrinking the box.
The overlay renders into the scene-output buffer outside the scene graph,
so its old pixels are invisible to scene damage tracking: the vacated
ring of the larger box stayed baked into the reused swapchain buffer.
Bump the overlay damage sentinel when a toplevel dies while the overlay
is shown, mirroring the reorder path.
2026-08-18 08:12:20 +10:00
e206e8433a prefer XKB_MOD_NAME_MOD4 where defined, fall back to XKB_MOD_NAME_LOGO
xkbcommon >= 1.7 removed the modern Mod4 macro and keeps only the legacy
deprecated name, so follow d8f6e39's swap but probe the header first to
avoid baking in the deprecated spelling on newer xkbcommon.
2026-08-18 07:57:48 +10:00
d8f6e39a68 xkbcommon 1.7: use XKB_MOD_NAME_LOGO instead of removed XKB_MOD_NAME_MOD4 2026-08-17 12:29:54 +10:00
e0af748a4b add --modifier alt|super to switch tab chords from Alt to Super
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.
2026-08-17 11:44:27 +10:00
f6c1a6b92e Grab Alt+arrows and rotate tabs when reorder wraps past an end
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.
2026-08-17 11:12:28 +10:00
6677012a9a Alt+Shift+Up/Down reorder the active tab within the overlay list
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).
2026-08-17 10:45:21 +10:00
fb29f03dc0 Alt+Delete closes the active tab; kill only pgids the compositor owns
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.
2026-08-17 10:26:48 +10:00
0e84ac328a Standalone cursor: use wlr_cursor position as single source of truth
The seat forward (host_ptr_motion) used a hand-clamped copy of the cursor
coordinates (server.cursor_x/y) clamped against host.width/height, while
the visible arrow came from wlroots' own cursor->x/y clamped against the
output layout.  On a DRM monitor with --screen-mode the requested mode can
differ from the geometry the layout derives from the committed state, so
the two drifted apart and the pointer appeared diagonally offset from
where clicks/hover took effect.

Drop the parallel bookkeeping and forward the wlr_cursor position through
the layout->output coordinate conversion for both relative motion, absolute
pointer events and the ctl warp, so the visible arrow and the seat can
never disagree.
2026-08-17 05:44:57 +10:00
6c98f985d8 Overlay: don't ellipsize titles that fit their row whole
fit_text() always carved out room for a trailing "...", so a row whose
text budget equals its title's natural width (budget = max_row_w - icon -
gap, and max_row_w is sized from the full title) always lost its last
characters: "foot" rendered as "fo...".

Copy the title verbatim when it already fits within max_px; only then run
the truncation loop for genuinely oversized titles, logging
'[overlay] ellipsis' on that path.  Covers both the nested GL overlay and
the standalone wlr pass (shared fit_text).

Test: standalone headless holds Alt and asserts no ellipsis line for short
titles. 33/33.
2026-08-16 22:29:43 +10:00
361a0b23b1 Nested mode: re-create host seat keyboard/pointer on capabilities changes
labwc (wlroots) invalidates every object created from a wl_seat when it
tears down its input backend across a VT round-trip; we fetched
host.keyboard/host.pointer once at host_init and never re-created them,
so the nested client kept running silently with no keyboard events ever
arriving ([kbd] lines stopped, Alt bindings dead).

Bind a wl_seat listener at registry time and move the one-time seats
objects into a capabilities handler: drop the keyboard/pointer objects
when the capability disappears, re-create (plus re-add listeners) when it
returns. The compositor re-sends keymap/enter on the fresh wl_keyboard,
and host_kbd_enter re-focuses our nested seat, restoring input.

Also handle wl_seat global remove/re-add (some compositors recreate the
global rather than toggling capabilities): null the seat objects and
reset the clipboard bridge so a fresh seat re-runs bridge_init() --
which is now re-entrant. Added [seat]/[kbd] DEBUG lines for Pi triage.

Tests: 32/32.
2026-08-16 22:02:55 +10:00
6ccb3e8d12 Add --screen-mode WxH[@Hz] to select the native output mode
The RPi5 projector reports a 4K preferred mode although it is really 1080p;
previously only wlr_output_preferred_mode() was tried.  --screen-mode picks
an exact resolution (highest refresh among matches, or an exact @Hz when a
refresh is given) from the output's mode list and, if the request cannot be
satisfied, prints every reported mode and exits before launching anything.

main(): consumes '--screen-mode <val>' and '--screen-mode=<val>' while
compacting the app list so the flag never becomes a tab launcher; malformed
values exit rc 2 with a usage hint.  handle_new_output(): resolution/@Hz
match with list-and-exit fallback.  On the headless backend the request
sizes the output, which also makes the flag testable.

tests: screen_mode_sets_output_size (dump PPM == 480x320), not consumed as
an app (both tabs boot, no 'starts with -' warning), bad values exit rc 2.
2026-08-16 20:36:38 +10:00
61c863a08b cursor: move wlr_cursor on native pointer motion
wlroots 0.19's wlr_cursor device wiring only re-emits motion on
cursor->events.motion; the compositor must apply wlr_cursor_move() itself.
Our standalone pointer listeners only forwarded to the seat, so on a real
libinput mouse the client hover/selection followed the pointer while the
software-composited cursor image stayed pinned wherever it had been warped.

native_ptr_motion()/native_ptr_abs() now move wlr_cursor (relative via
wlr_cursor_move, absolute via wlr_cursor_warp_absolute, honouring the
per-device calibration) alongside the seat forward, and both libinput
listeners route through them.  A new CTL_CMD_PTR_MOTION stdin feed drives
the same code path (NULL device -> layout closest-point), so the headless
tests exercise the real handler; it falls back to the layout box.

test: test_standalone_headless_cursor_relative_motion warps to a clean
target, then sends a +200,+120 delta and asserts the composited cursor
moved there (old spot restored to client colour).  Fails on the previous
build (cursor pinned), passes now.
2026-08-16 13:44:32 +10:00
56d4e00c4d cursor: trust wlroots for theme resolution; warn on builtin fallback
Stop mirroring the xcursor loader (search path, Inherits chains, autopick)
and hard-failing on a missing theme. wlroots guarantees a visible pointer
(builtin 'default' arrow) when nothing is installed, so a no-theme box now
boots. Report substitution via the public wlr_xcursor_theme name, whose
'scaled_themes' entry is renamed to 'default' when the builtin is used.

main(): read XCURSOR_THEME/XCURSOR_SIZE (default 'default'/24), create/load
the manager with scale 1, log 'theme not found; builtin in use' for any
requested theme except the literal 'default', and warn-and-continue (no
return 1) if no cursor is usable. Deletes cursor_theme_* helpers and the
<dirent.h> include.

tests: replaces the mirror-era tests with three Option-A ones -- missing
named theme boots and logs the substitution, no theme at all boots
silently, installed Adwaita (host-guarded) boots with no warning.
2026-08-16 13:01:15 +10:00
53dd196602 Resolve cursor themes the way wlroots loads them (Inherits, left_ptr, ~/.icons)
The previous pre-check demanded a literal <icon-dir>/<theme>/cursors/default
file, which is stricter than the wlroots 0.19 loader: it loads every cursor
in cursors/, follows index.theme Inherits= chains, and falls back
default to left_ptr. On Debian/Pi 'default' is only an alias directory
whose index.theme Inherits= names the system-selected theme, so an
unconfigured run refused to start even with cursors installed.

Rewrite resolution to mirror the loader: build the exact search path
(XCURSOR_PATH, else ~/.local/share/icons + ~/.icons + the compiled-in
dirs), accept either a default or left_ptr sprite, follow Inherits=
recursively (cycle-bounded). If the requested theme (or the 'default'
alias) still doesn't resolve, auto-pick the first installed theme that
provides a sprite and log the choice; only hard-fail when no cursor theme
exists anywhere, with a message that names the searched dirs and the
Debian update-alternatives hint.

Tests: default->Inherits alias boots, left_ptr-only theme boots, auto-pick
logs its choice, and the missing-theme case still exits rc 1.
2026-08-16 12:45:42 +10:00
9db00591ca Show a real cursor in standalone mode (wlr_cursor, software-composited)
Standalone has no host compositor to draw a pointer, so give the output a
real wlr_cursor driven by XCURSOR_THEME/XCURSOR_SIZE.  Force software
cursors on every standalone output (the headless backend's set_cursor hook
would trick wlroots into skipping the scene pass while drawing nothing, and
a hardware plane would float the cursor outside the composited buffer).

Refuse to run when no 'default' sprite resolves: wlroots 0.19 substitutes
a builtin cursor silently, so check the theme dirs ourselves along the
loader's XCURSOR_PATH search and hard-fail instead of presenting an
invisible pointer.

Whole-output damage on every pointer move/warp, because the scene renderer
only composites the software cursor inside the damaged region and raw
cursor motion damages only the output.  The stdin feed gains CTL_CMD_WARP
so tests can place the pointer; the visible-cursor test probes through the
GL dump's bottom-up flip, and a missing-theme regression test asserts rc 1.
2026-08-16 12:20:08 +10:00
c4a4b73393 Force whole-output repaint when the standalone overlay shows or clears
The standalone overlay is rendered into the scene output buffer in a second
pass outside the scene graph, so wlroots' damage tracking never learns about
its pixels; a reused swapchain buffer therefore keeps the stale overlay baked
in after Alt is released.  A transparent full-output sentinel rect is
transiently raised to the top so the reorder traversal repopulates its
full-screen visible region, then its size is bumped to re-damage that region
through the public scene API, and the z-order is restored so it never
intercepts pointer hit-testing.  Both swapchain slots then repaint within the
next two frames and the overlay fully clears on release.

The set_enabled-based alternatives produce no output damage (wlroots only
damages the post-change visible region, which is empty for a disabled rect),
and the sentinel's visible region is otherwise emptied by the opaque client
occluding the shared update region before it is reached.
2026-08-16 11:33:54 +10:00
98879be1fc Build the standalone overlay glyph atlas so tab titles render next to icons
overlay_font_init() built the atlas only via the nested init_egl() path,
leaving overlay_font_ok false in standalone mode: overlay_text_width()
returned 0, so the overlay box sized to the icons alone, and
overlay_pass_text() drew no glyphs.  Call it from main() once the wlr
renderer exists (one-shot guard added so the two call sites never
rebuild the atlas), and cover it with a regression test asserting the
'[overlay] font' log line from a standalone headless session.
2026-08-16 10:29:10 +10:00
de252181af Guard nested-only host-window proxies so standalone exits cleanly on last tab close 2026-08-16 08:31:51 +10:00
872356eaa5 Per-device standalone pointer listeners so VT switch survives pointer teardown 2026-08-16 08:10:30 +10:00
131950d6e0 Survive session loss by detaching listeners before wlroots tears down output and input devices 2026-08-16 08:01:34 +10:00
382809b126 Handle Ctrl-Alt-Fn virtual-terminal switching in standalone mode 2026-08-16 07:39:37 +10:00
39151d2511 Route standalone keyboard events through the emitting device
The standalone libinput path kept a single global keyboard slot that was
overwritten for every attached device, so modifier state came from whichever
keyboard attached last. Hosts exposing several keyboard nodes (e.g. a
consumer-control or HDMI-CEC device beside the real keyboard) made the seat
deliver shift=0 permanently, so terminals only ever printed lowercase, and
Alt-based hotkeys never registered.

Attach one listener struct per libinput keyboard and resolve the device from
the listener in both handlers, then seat that device directly (tinywl-style)
so clients receive its real keymap and xkb-derived modifiers. Also fixes the
latent corruption of wl_signal_add()ing the same listener into many device
signals, and frees the per-device structs in shutdown_cleanup. The nested/ctl
virtual keyboard path is unchanged; 21 tests pass.
2026-08-16 07:24:19 +10:00
72b95f1204 Support standalone (native-output) mode alongside nested hosting
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.
2026-08-15 18:06:55 +10:00
33f8626bde Activate the left-neighbor tab when a tab's program dies 2026-08-15 12:35:51 +10:00
5be0b23e68 Open a terminal tab by default and fail fast on missing commands 2026-08-15 11:29:53 +10:00
c661117a71 Draw a 1px border inside the Alt overlay box edge 2026-08-15 10:28:14 +10:00
726838bfbc Truncate overlay rows so the menu stays narrower than the window 2026-08-15 10:28:14 +10:00
0632f8e3d0 Open a new terminal tab with Ctrl+Shift+T ($TERMINAL)
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.
2026-08-15 09:16:09 +10:00
348c22ce4d Center popup overlay instead of top-left anchored 2026-08-15 07:57:08 +10:00
b8875f7292 Show per-tab app icons in the Alt-hold overlay 2026-08-14 12:55:42 +10:00
db6bbac84c Draw passive Alt-hold tab-title overlay with FreeType text 2026-08-14 12:39:51 +10:00
e52ac98afe Pass function keys through and mirror fullscreen between host and clients
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.
2026-08-14 11:31:32 +10:00
54c3c99e1d Cycle tabs with Alt+Up/Down matched by keysym
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.
2026-08-14 10:31:50 +10:00
a86a21d76e Merge 'Drop redundant manual popup compositing' from origin/master 2026-08-14 08:19:19 +10:00
c2fdb7b6e1 Fix second N2H copy hang: ignore transient NULL dc selection
On the second nested->host copy, bridge_host_set_selection destroys the
previous data-control source before setting the new one.  The compositor
answers the destroy with a brief selection(NULL) which is delivered in the
same round-trip as the echo of the new source.  The old code treated that
NULL as a real clear: it cleared the pending-echo flags, so the genuine
echo arrived untagged, was misread as a real host selection and mirrored
back into a self-feeding set_selection/receive loop that left host
wl-paste spinning forever.

Ignore a NULL selection while our echo is still pending; a real host clear
never sets a pending flag, so it cannot be masked.  The dd path already
handles this via its focus-blip NULL early return.
2026-08-14 08:12:02 +10:00
b7e1c650dc Drop redundant manual popup compositing
Popup/menu surfaces are regular wlr_scene_xdg_surface nodes under their
parent's tree, so wlroots' scene renderer already composites them into the
output buffer like any other surface.  The host-side second draw
(composite_popups/popup_draw_cb + popup_tex) was a redundant double-blend
that predated the scene nodes; with it removed, menus and sub-menus render
correctly through the scene path.  Keep the popup scene-node wiring,
unconstrain, and commit/reposition/destroy listeners.
2026-08-12 10:40:05 +10:00
dc6f8f97c2 Harden pagedown-repeat test: TABBED_SYNTH_INPUT mode + stall-tolerant polling
TABBED_SYNTH_INPUT=1 makes the stdin event feed the only source of
key/modifier input: host_kbd_key and host_kbd_modifiers drop real host
events (kbd != NULL) while enter/leave/repeat_info still drive the
clipboard bridge.  A stray physical key (e.g. Alt) no longer cancels a
synthesized repeat or clobbers the injected modifier state mid-test.

The repeat test now polls for base+3 tab transitions with a 5s deadline
instead of a fixed 1.3s sleep, so a present/frame stall no longer causes
spurious failures.
2026-08-11 21:51:52 +10:00
ea8770c887 Port [claim]/[launch] client-PID logging and the 13-test smoke suite
Brings in master's client-PID logging ([claim] ties an app slot to the
connected client's PID, [launch] records the spawn PID) and its expanded
smoke suite, so the fixed tree can run and pass the full merged suite.
2026-08-11 19:07:42 +10:00
3cc2ab38fd Fix SIGTERM shutdown deadlock: run teardown from an event-loop signal source
SIGTERM/SIGINT ran shutdown_cleanup() straight from a raw signal handler.
The teardown path calls malloc/free and libwayland proxy destroys, which
are not async-signal-safe: a SIGTERM landing inside launch_client()'s
fork() left libc's allocator lock held, and bridge_cleanup()'s calloc()
then blocked on it forever, so the compositor never exited and tests that
send SIGTERM timed out.

Route SIGTERM/SIGINT through wl_event_loop_add_signal() (signalfd) so the
teardown runs from a normal event-loop callback, where no allocator or
stdio lock can be held across it.  Also make sigchld_handler() async-signal-
safe (snprintf+write instead of fprintf, which takes the FILE lock).
2026-08-11 19:07:30 +10:00
8a46e92e03 Fix clipboard echo flood: keep pending-echo guards across focus blips
The nested wl_data_device selection handler treated the host's own echo of
our set_selection as a real selection whenever a focus blip (dd selection
with a NULL offer) had cleared the pending-echo flags first.  That echoed
our selection back to the host and started a self-feeding set_selection /
dc_source_send loop that never converged and never timed out.

- bridge_dd_selection: a NULL offer is a focus blip - keep the last-known
  selection and leave the pending-echo flags intact instead of clearing
  them and mirroring back.
- dc/dd echo swallow: clear the matching pending-echo flag when we swallow
  our own echo (previously only the dc path reset anything).
- host_kbd_enter: only reset the pending-echo flags for the focus-based
  wl_data_device backend, where a stale serial can orphan a guard.  The
  data-control backend echoes independently of focus, so its in-flight
  guard must survive keyboard-enter events.
2026-08-11 19:07:02 +10:00
ad46508962 Fix wlr-data-control clipboard: handle primary_selection and echo ordering
labwc sends the primary_selection event (opcode 3) on bind; leaving the
listener unset made libwayland abort. Acknowledge and destroy the offer
(primary selection is not bridged).

Echo handling used one shared pending_echo flag: when the host selection
is set via data-control, the compositor echoes the seat change over both
the wl_data_device and data-control channels, so the first echo consumed
the shared flag and the second was misread as a real host selection,
clobbering the nested source via mirror_to_nested. Track the pending
echo per backend and clear both on any real selection.

Verified: clipboard tests pass on labwc (data-control) and GNOME/Mutter
(wl_data_device); full 7-test suite green on both hosts.
2026-08-10 19:10:58 +10:00
d05ded946b Add bidirectional clipboard bridge with text prefetch
Bridge the nested wlroots seat's clipboard with the host compositor.
Uses wlr-data-control when the host exposes it, otherwise a focus-based
wl_data_device fallback (Mutter). Text selections are eagerly prefetched
at mirror time so every paste is served instantly from a compositor-side
buffer instead of paying the host source's latency per paste; non-text
MIMEs keep the zero-copy live relay.

Also fix a critical bug: prefetch_fd was zero-initialized instead of -1,
so the first bridge_prefetch_cancel() closed stdin, breaking the test
event feed and spinning the main loop on EBADF.
2026-08-10 11:29:07 +10:00
2b034f990f Add stdin event feed and smoke test suite
Compositor: read raw keyboard records from stdin and dispatch them into
the real host_kbd_key/host_kbd_modifiers handlers, so keybindings (F11,
Ctrl+PageUp/Down, F9/F10 dumps) can be driven without a focused host
window or wtype. Binary wire: 1|u32 key|u32 state, 2|u32 mods|...

Tests: tests/wltest.c solid-colour xdg_toplevel client, plus a pytest
suite (tests/test_smoke.py) that launches nested instances per test and
feeds events through the new channel. test.sh runs it. F11 fullscreen
toggle now logs its new state.
2026-08-10 07:29:10 +10:00
80f19c1836 Crop CSD gutter from client geometry instead of hardcoded 26px 2026-08-09 19:15:50 +10:00
df2e61a52f Forward discrete scroll wheel steps to clients 2026-08-09 18:43:10 +10:00
8acd384666 Document usage, keybindings, TABBED_LOG/dump semantics; fix F8->F9/F10 comment 2026-08-09 13:02:46 +10:00
49db6e9834 Remove tab limit: growable apps array (drop MAX_APPS and dead MAX_WIN defines) 2026-08-09 12:15:13 +10:00