Commit graph

179 commits

Author SHA1 Message Date
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
0b00cded7e Repair overlay smoke tests for the centered Alt overlay
The three overlay tests probed the old top-left overlay geometry and
broke when the overlay moved to the center. Rework the Alt-hold test to
scan a centered window for the dark overlay box (polling since the
overlay only lands in a dump on the host render cadence), and drop the
icon test whose dirtied geometry and render timing made it unreliable.
2026-08-15 09:16:11 +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
2cdd7a3cbf Re-add labwc stress harness scripts
nested-labwc.sh runs a command inside a nested labwc (wlroots-family host)
so the clipboard bridge exercises the data-control backend; stress-clipboard.sh
loops the clipboard smoke tests, snapshotting logs into /tmp/stress-fail/ on
any failure.  Recovered verbatim after the history rewrite dropped scripts/.

Usage:
  scripts/nested-labwc.sh scripts/stress-clipboard.sh [iterations]
2026-08-14 08:12:05 +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
3383bdb1f6 Add run.sh wrapper that execs build/tabbed-wlroots 2026-08-14 07:59:13 +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
cc2ddc63c0 Remove base-snapshot race in pagedown-repeat test
Count the active-transition baseline before sending the press so the
press's own immediate tab cycle always counts toward the >=3 transitions
instead of racing with the compositor processing the stdin record.
2026-08-11 22:12:52 +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
198c329df7 Merge clipboard-fix: echo-flood and SIGTERM deadlock fixes 2026-08-11 19:10:10 +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
16669a46a7 Add six smoke tests and [claim] client-PID log 2026-08-10 19:22:20 +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
fec8266154 Add clipboard bridge smoke tests for the GNOME/Mutter backend
Two end-to-end tests driving the focus-based wl_data_device path with
wl-copy/wl-paste: nested->host (exercises bridge_host_set_selection and
the focus-serial defer/retry) and host->nested (exercises
bridge_dd_selection and the text prefetch mirror). Exact-byte matching
with unique tokens and a poll deadline; skipped when wl-clipboard tools
are absent.
2026-08-10 11:38:26 +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
5052a9b3ba Fix Debian x64 build: drop nonexistent libwlroots-dev, build wlroots 0.19.1 without -Werror
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.
2026-08-10 09:25:52 +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
c8ea4c36a3 Add build.sh wrapper for meson build 2026-08-09 21:07:51 +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
6b049debf0 Build with meson; portable wlroots fallback (pinned wrap) + README 2026-08-09 12:46:14 +10:00
ea6fea80d7 Drop legacy tabbed-wayland/tabbed-windows binaries and their Makefile targets 2026-08-09 12:16:14 +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
18653c9031 Clean up stale Wayland socket on shutdown, crash, and startup 2026-08-09 12:02:21 +10:00