Commit graph

317 commits

Author SHA1 Message Date
1f35f90a95 Alt+Tab / Alt+1 walks a window-level MRU stack, commits on release
Prefix+Tab and prefix+1 preview the session's windows most-recently-used
first: each press steps to the next window after the shown one in server.mru
order, wrapping at the bottom, without mutating the stack until the modifier
is released. Releasing after navigating commits the shown window to the
front; a bare hold leaves the order untouched. Focus changes with the
modifier up move the entered window to the front immediately.

Alt+1 is the chord that survives hosts like GNOME, which grab Alt+Tab,
Alt+Esc and Alt+Backtick (the latter via the metacity-era switch_group
default) before a nested compositor ever sees them; Alt+Tab is kept for
hosts that leave it free. Windows join the stack least-recent on allocation
and are removed on teardown; reachable stops mirror cycle_tab (live or
pending-claim, not closing). Holding the chord repeats via the existing
synthesized-repeat machinery.

Tests cover stepping, commit-on-release, the Alt+1 synonym, and hold-to-
repeat.
2026-09-06 23:52:03 +10:00
ca7f1f2852 tests: cover quit dialog, constraint/text-input/dialog teardown, virtual abs+mods, mgmt probe 2026-09-05 10:59:49 +10:00
32de4866a6 tests: cover X11 requests, OR move, Xwayland restart, idle release; fix inhibitor destroy 2026-09-05 10:24:49 +10:00
b004cfb66c tests: cover data-control clipboard/primary bridge via kick-in-kick; ignore SIGPIPE 2026-09-04 20:47:12 +10:00
7d2bda5800 tests: cover overlay icon desktop/PNG/SVG/absolute/missing paths 2026-09-04 20:20:44 +10:00
3446b79bb3 session-lock: drop finished lock on unlock so retakeover works; cover unlock teardown 2026-09-04 20:16:45 +10:00
7955e98150 tests: cover taskbar activate/maximize/fullscreen requests 2026-09-04 19:57:15 +10:00
b0858e15be tests: cover client maximize/minimize/move/resize, popup reposition, true SIGKILL escalation 2026-09-04 19:49:08 +10:00
21becaf17b coverage: gcc/gcov line coverage of kick.c unioned over the suite 2026-09-04 18:13:32 +10:00
5a9464c454 build: add a 'release' target for an optimized, stripped binary
./build.sh release builds into build-release/ with --buildtype=release
and strips the binary in place (meson's strip only applies on install),
yielding a ~270KB binary for size benchmarking against distro
compositors like labwc.  The default ./build.sh debug path and the
build/ tree used by the test suite are untouched.
2026-09-04 13:07:40 +10:00
9d1e3e802c smoke tests: gate on surface map / painted frame before capture
Two more tests had the same latent race fixed for
test_standalone_headless_virtual_pointer_motion_click: injecting an
end-to-end capture too soon after wait_any_active/[claim], when the
surface may not be mapped/composited yet, so the event/capture is
silently lost under load.

- test_touch_delivery_to_client: dropped its time.sleep(1.0) and now
  waits on wait_surface_mapped(0) before the touch down/motion/up, so a
  pick never races surface mapping.
- test_standalone_headless_output_power_blank: waits on
  wait_active_frame() before the baseline screencopy, so the ',content'
  assert never sees the pre-map black placeholder.
2026-09-04 12:27:41 +10:00
7194e59e8c smoke test: wait for surface map before virtual pointer motion/click
test_standalone_headless_virtual_pointer_motion_click fired its virtual
pointer motion+click straight after wait_any_active, which only means the
window object exists -- before its Wayland surface has necessarily mapped
(still the black placeholder, surface=0x0).  A pointer event landing
before the surface maps reaches no surface, so --pointer-out logged no
'enter'/'button' and the test intermittently failed under load (the race
documented in wait_surface_mapped).  Wait on the [diag] map line first,
matching the other input-injecting tests.
2026-09-04 12:21:37 +10:00
05c3e5dd2b tests: anchor layer bars left+right so they map
A zero width without the opposite edge anchors is a protocol error,
so wlroots killed the test bar before it mapped and the exclusive
test failed (800 -> 800) while the zero-exclusive sibling passed
vacuously. Both bars now anchor left+right, the zero-exclusive test
asserts [layer] map so it cannot pass without a mapped bar, and
wltest documents the anchor rule.
2026-09-04 11:05:26 +10:00
d8164ce315 protocols: expose output-management, honor resizes standalone
Advertise zwlr_output_manager_v1 with the single head, published on
setup and re-published from the commit path whenever the size lands
(resizes apply lazily, so the commit -- not the request -- is where
the new size becomes observable).

Client sizes apply standalone through the normal resize path (tabs
reflow, swapchain follows); nested refuses anything but the live
size since the host owns the geometry. Disabling, scale and
transform changes are refused; superseded configs are destroyed
after feedback.

Standalone output_resize now owns the canvas size so client resizes
reach the lazy modeset. wltest gains --output-mgmt; smoke tests
cover the global, head listing, and an 800x600 apply with tab
reflow.
2026-09-04 10:52:16 +10:00
fcd3bca53b protocols: prefer xdg-toplevel-icon artwork in the overlay
Advertise xdg_toplevel_icon_manager_v1 (28px, the overlay draw size)
and use the largest uploaded buffer as the tab's overlay texture.
The desktop-file guesser stays as the fallback -- and the only source
for X11 windows -- so every window still gets an icon; clearing the
icon drops back to guessing.

wltest gains --icon; smoke tests cover the global and the end-to-end
protocol-icon preference.
2026-09-04 10:35:34 +10:00
50a660860d protocols: honor tearing hints and fractional scale
Drive the output commit's async-flip flag from the shown surface's
tearing hint (a real tearing flip on DRM; correctly ignored on
headless/nested), with per-object hint tracking that requests a frame
on change. Notify each surface of the output scale at claim so
clients never guess preferred_scale.

wltest gains --tearing and --fractional-out (plus linking the two
protocol stubs it now uses); smoke tests cover the async commit
transition and the notified scale value.
2026-09-04 10:21:30 +10:00
8a71a062ce protocols: forward pointer-gestures swipe/pinch/hold
Advertise zwp_pointer_gestures_v1 (v3, with hold) and fan libinput
touchpad gestures out to the client holding seat pointer focus.
The stdin test feed gains swipe/pinch/hold messages driving the same
forwarders in any mode (nested has no host gesture source).

wltest gains --gesture-log plus swipe/pinch/hold feed helpers; smoke
tests cover the global and an exact-value round trip of every gesture
and cancel path.
2026-09-04 10:13:13 +10:00
b189318124 protocols: support xdg-dialog modal toplevels
Advertise xdg_wm_dialog_v1 and mirror each dialog object onto its
window. A modal dialog demands attention: on map (or when set modal
while mapped) its tab is shown and it takes window focus, in both
--tab-groups modes; placement itself is unchanged and session lock
still wins.

wltest gains --dialog-modal/--extra-modal/--extra-delay; smoke tests
cover the global, the background-modal steal with tab-groups on, and
the own-tab steal with tab-groups off.
2026-09-04 10:00:34 +10:00
42853dcc18 protocols: honor wlr-gamma-control for night-light tools
Advertise zwlr_gamma_control_manager_v1 and commit client ramps to
the single output (reset when the control goes away); other outputs
and failed commits fail the control so clients retry. Outputs with
no LUT (headless/nested) are failed by wlroots itself.

wltest gains a --gamma mode reporting gamma_size/failed; smoke tests
cover the global and the clean headless failure path.
2026-09-04 09:43:04 +10:00
b9194addf4 protocols: input-method relay, content-type, single-pixel
Advertise zwp_text_input_manager_v3 + zwp_input_method_manager_v2 and
relay the focused field to the in-session IME and back (enter/leave on
seat keyboard focus, keyboard-grab forwarding, IME popups above tabs).

Also advertise wp_content_type_manager_v1 (logged on map) and
wp_single_pixel_buffer_manager_v1 (scaled via viewporter).

wltest gains --text-input/--input-method/--content-type/--single-pixel
modes; smoke tests cover globals, the IME round trip, focus follow,
single-pixel rendering and the content-type log line.
2026-09-04 09:37:13 +10:00
a7a38fadca x11: guard set_class NULL app_id/title for taskbar handles
Deleting a window's WM_CLASS emits set_class with class==NULL while the
title is set; handle_xs_set_class passed the raw NULLs into
wlr_foreign_toplevel_handle_v1_set_app_id / ext update_state, which
strdup(NULL) -> SIGSEGV (exit 139; UBSan flags the nonnull argument
first). The sibling set_title handler and every other mirror site use
foreign_str(); apply the same guard here.

Add tests/xclass.c (title set, WM_CLASS set-then-deleted probe) and a
headless smoke test asserting the compositor survives the delete with
no crash indicators (fails pre-fix: compositor dead).
2026-09-04 08:39:37 +10:00
35ce107a23 tests: nested xdg popup chain client + kill-mid-chain coverage
Add a --popup wltest mode (toplevel + popup P1 + nested sub-popup P2,
each buffer-backed so all three map) and a headless smoke test proving
a full chain maps, the client can be kill -9'd mid-chain, and a fresh
chain maps afterwards with the compositor alive and crash-free.

Writing the mode exposed two client-side protocol bugs in wltest
itself (both fixed here): the final get_popup+commit batch was never
flushed (dispatch_pending+poll neither sends), and the hold loop never
READ new events (dispatch_pending drains only the already-read queue),
so the nested popup's configure -- proven sent via server WAYLAND_DEBUG
-- starved unread and P2 never mapped. The hold loop now roundtrips
periodically, which reads, dispatches and flushes.

Investigated as a potential live_popups use-after-free (submenu scene
outliving its parent): wlroots cascades child-popup destruction on
parent teardown and kick removes entries on popup destroy, so no stale
scene reference survives; 6 kill cycles under ASan/UBSan stayed clean.
No compositor change needed.
2026-09-04 08:24:02 +10:00
6c5df47bf0 x11: clear or_focus_owner when its tab is destroyed
An override-redirect menu that takes keyboard focus stashes the active
tab in server.or_focus_owner; dismissing the menu later reactivates it.
If the owner tab was closed first, the release activated freed memory
(heap-use-after-free in set_active_tab via tab_current_window, caught
by ASan: free in remove_tab <- drop_tab <- window_destroy_common <-
handle_xs_destroy; use in or_window_focus_release <- or_handle_unmap).

Clear the stash in remove_tab (the single live tab-free choke point),
so the release drops keyboard focus instead.  Without ASan the bug is
silent (stale memory usually still mapped), which is why it survived:
repro needs a focus-stealing OR menu (real menus paint content; an
unpainted probe never completes the wl map handshake).

Add tests/xormenu.c (frame + OR menu in separate processes, after the
xpopup pattern) and a headless smoke test driving the exact sequence:
menu takes focus, owner tab closed, menu dismissed, compositor alive
and crash-indicator-free.
2026-09-03 23:24:46 +10:00
9ea0766bd3 xdg-activation: focus windows by token
Let launchers focus existing windows via xdg-activation-v1.  On
request_activate, find the kick window owning the surface and switch to
its tab (set_active_tab + window_enter, both gated on session_locked so
activation can never bypass the lock screen).

Add a --activate wltest mode (two windows; second steals focus, then a
token activates the first) and two smoke tests proving the global is
advertised and the inactive window receives an activated configure.
2026-09-03 14:11:11 +10:00
0475216913 virtual-input: feed zwp_virtual_keyboard/pointer into the seat
Let synthesised input (wtype & co.) drive the compositor through the
exact same paths as real devices: virtual keys flow through
process_key_event (so Alt+Up tab-cycles, chords swallow under lock/
inhibit), virtual pointer motion/buttons drive native_ptr_* and the
seat.  A missing client keymap falls back to the server's so modifiers
resolve; cleanup uses resource-destroy listeners (run before wlroots'
finish asserts).

Add --virtual-key (code:state sequence with xkbcommon us keymap),
--virtual-motion/--virtual-click, and --pointer-out (window logs pointer
events) wltest modes; three smoke tests prove the globals are advertised,
a virtual Alt+Up cycles tabs, and virtual motion+click reach the window.
2026-09-03 14:08:04 +10:00
146f9b6b5c output-power: honor zwlr_output_power_management_v1
Let a client (wlopm) blank/unblank the single output.  OFF overwrites the
composited scene buffer with opaque black before commit (a dedicated
render-pass fill in both render paths), so the committed frame is truly
dark rather than a stale scene frame.  ON forces a full scene repaint
(the swapchain recycles the black buffer, so without whole-damage it
would re-commit stale black) and resumes normal compositing.

Extend --screencopy to report full-buffer blackness (black vs content),
add a --power on|off wltest mode, and three smoke tests: the global is
advertised, and a power off/on cycle blanks then restores the visible
window (verified via screencopy).
2026-09-03 14:01:02 +10:00
45df9d4818 shortcuts-inhibit: honor zwp_keyboard_shortcuts_inhibit_v1
Let a focused fullscreen client (game/VM) grab the compositor's keyboard
shortcuts.  New inhibitors for the focused surface activate immediately;
a lazy sync at grab-chord time (de)activates across focus changes so all
focus paths are covered without extra listeners.  While honored, the
tab/window grab chords are swallowed in process_key_event (quit and VT
switch stay live so a stuck client can't trap the session).

Add a --inhibit wltest mode (window + inhibitor, reports active) and two
smoke tests asserting the global is advertised and the inhibitor becomes
active on the focused surface.
2026-09-03 13:56:56 +10:00
c64a10e8d9 protocols: advertise tearing-control + fractional-scale
Add wp_tearing_control_manager_v1 and wp_fractional_scale_manager_v1 as
pass-through advertisements for client convenience (games, HiDPI).  kick
drives a vsynced scene at scale 1.0, so nothing is honoured: a new
tearing object is only logged at DEBUG.  Bundle the output-power XML
locally too (needed by a later commit) and generate the server headers
the wlroots headers' quoted includes require.
2026-09-03 13:54:25 +10:00
044c7543bb screencopy: advertise wlr-screencopy-unstable-v1
Composite the scene and let wlroots run the whole capture flow off the
output commit path: the manager is created in server init and every
commit carrying a rendered buffer becomes a copyable frame (both the
standalone and nested render paths already commit a buffer, and
wlr_output_update_needs_frame drives the render gate).

Bundle the wlr-screencopy XML locally (a copy of the one vendored in the
pinned wlroots subproject) since Meson forbids the parent project from
referencing a nested subproject's files; add a --screencopy wltest mode
that captures the output into a shm buffer and reports the frame
geometry, plus two smoke tests asserting the global is advertised and a
rendered frame is delivered.
2026-09-03 07:57:59 +10:00
143a1053c2 session lock: ext-session-lock-v1
Blank the screen and hold input when a lock client (swaylock) grabs
ext_session_lock_manager_v1.  While locked an opaque black rect covers the
output with the lock client's per-output surface floating above it; no
window may take keyboard focus and the compositor grab chords are swallowed
so tabs can't be cycled behind the lock.  kick manages a single output, so a
single lock surface is tracked.  If the lock client dies without unlocking
the screen stays blanked (a deliberate security choice); only a fresh lock
request may take over.  On unlock the previously active tab is refocused.

Includes a wltest --lock mode (binds the manager, locks, draws a buffer on
the lock surface, and optionally --lock-unlock) and two smoke tests:
advertised-globals and lock/surface-config acceptance.
2026-09-03 07:39:25 +10:00
4ea5df0e62 taskbar: foreign toplevel + idle protocols
- wlr-foreign-toplevel-management-v1: per-window handles for taskbars
  with activate/close/maximize/fullscreen requests
- ext-foreign-toplevel-list-v1: compositor-pushed handle list
- ext-idle-notify-v1: client-requested idle notifications
- idle-inhibit-v1: suppress idle while a surface is visible
- wltest --foreign mode (taskbar client, state logging, --foreign-close)
- wltest --idle mode (idle notification + inhibitor testing)
- 5 smoke tests: globals, state roundtrip, close, idle notify, idle inhibit
2026-09-03 01:47:35 +10:00
e539be4338 popup fallback: mount bar menus at output origin only for foreign clients
Parentless xdg_popups (menus opened by layer-shell bars such as
wf-panel-pi) must hang off the output-origin popup tree so they render
flush under the bar instead of being shifted down by the bar's reserved
edge.  But a parentless popup from the active tab's own client (an
in-tab menu whose parent surface was missed) must keep hanging under
that window's tree to preserve its old alignment.

Resolve the popup client's pid and choose the mount accordingly;
same-client popups keep the window tree, foreign clients get the origin
tree.

Also add two KICK_LOG_DEBUG signals the deterministic regression test
keys on: [layer] map pid=N when a layer surface maps (the bar is ready
for input) and [popup] out=x,y geo=w x h pcpid=N with the popup's
scene-exact output position once it maps.  test_smoke.py's
test_panel_menu_flush_under_bar waits on those lines (no pixel diffing,
no fixed sleeps) and asserts the wf-panel-pi menu's output top is flush
under the 52px bar; against a build with the fix reverted it fails with
the menu at y=109.
2026-09-02 10:26:43 +10:00
ebae43242f fix --panel menu offset: mount parentless popups at output origin
Bars (wf-panel-pi) open menus as parentless xdg_popup surfaces. kick's
fallback previously hung them under the active window's scene tree,
which is offset past the bar's reserved edge (server.layout_y), so the
menu rendered a full panel-height too low.

Add a dedicated popup_tree scene tree rooted at the output origin: it is
created at scene setup, kept raised to top alongside layer_tree, and used
as the mount point for parentless popups so their positioner geometry
maps to real output coordinates.
2026-09-01 23:18:12 +10:00
7605187c43 smoke tests: layer exclusive-zone reserves tabs, ez=0 does not
Spawn a 40px top bar via wltest --layer against a live kick session:
exclusive-zone 40 shrinks RED's committed geometry by exactly 40px,
exclusive-zone 0 leaves it unchanged (the zero-ez notification case).
2026-09-01 22:31:26 +10:00
dc2938e5ae wltest: add --layer mode for layer-shell work-area tests
--layer <bg|bottom|top|overlay> creates a zwlr_layer_surface_v1 instead
of an xdg toplevel, with --layer-anchor/--exclusive-zone/--layer-size
control.  Wire the wlr-layer-shell-unstable-v1 client protocol into the
wltest target.  Lets the smoke tests exercise kick's exclusive-zone
work-area computation without depending on an external panel.
2026-09-01 22:31:18 +10:00
5dc0c76cd1 layer layout: reserve only exclusive-zone surfaces (two-pass, labwc-style)
Replace the layer_reserve_box self-reservation heuristic with labwc's
layers_arrange(): configure exclusive-zone surfaces first (highest layer
down) so they reserve, then the zero-exclusive-zone remainder (lowest
layer up) positioned within the residue without shrinking it.  A transient
zero-ez surface (e.g. a panel notification) no longer resizes the tabs.
2026-09-01 22:31:04 +10:00
bc98746207 fuzz tests: exercise --panel with waybar (launch, cycle, teardown) 2026-09-01 21:29:02 +10:00
5d91d6c766 fix layer-shell crash: unregister commit/destroy listeners on bar teardown
Destroying a wlr-layer-shell surface left kick's commit listener on the
underlying wl_surface's commit signal registered and free'd the kick_layer
without unlinking it, so a late commit during teardown dereferenced freed
memory and corrupted the surface's destroy listener list.  wlroots then
tripped its assertion (wlr_layer_shell_v1.c:54) on the next layer-surface
destroy, crashing kick ~seconds after '--panel' launched (e.g. wf-panel-pi).

Remove both the commit and destroy listeners in layer_handle_destroy before
releasing the kick_layer, matching popup/window teardown.  Reproduced the
crash under gdb, verified the fix survives 75s+ with wf-panel-pi, and the
full suite still passes (79 passed).
2026-09-01 20:56:26 +10:00
6adccd6ef6 rename --bar to --panel (argument, identifiers, and log tag) 2026-09-01 18:30:08 +10:00
844c88e5bb remove test-headless.sh; de-race fullscreen, cursor, alt-delete, and primary-selection tests against suite-load flakes 2026-09-01 13:37:38 +10:00
c908701a7e flaky standalone tests: de-race cursor-shape enter, child-env echo, overlay ellipsis 2026-09-01 13:37:38 +10:00
8b02869942 build: touch binaries after compile to stop perpetual rebuilds 2026-09-01 10:53:53 +10:00
fd8c41b71f add wlr-layer-shell bar support and fix popup-parent crash
--bar CMD launches an always-on-top layer-shell panel (e.g. waybar) in a
dedicated launch slot.  The bar's footprint reserves the usable area,
shrinking and offsetting maximized tabs below it even when the client never
sends an exclusive zone; the bar's scene tree is held above every window.

Fix a use-after-free in xdg popup handling exposed by panel clients: resolve
a popup's parent by address comparison against kick's live surfaces rather
than dereferencing popup->parent (wlroots may hand a popup whose parent
wl_surface was already freed outside the xdg teardown cascade).  Popups whose
parent is no longer live fall back to the active window's tree so they still
get their initial configure instead of hanging the client.

Bar child stdout/stderr is /dev/null'd unless KICK_LOG is at debug level, so
waybar's chatter no longer pollutes kick's console (same convention as CLI
clients).
2026-09-01 10:38:57 +10:00
fd7ed766ef confirm-before-quit: ask Y/Enter (quit) or N/Escape (cancel) for Ctrl+Alt+Q and host close when more than one window is open 2026-08-31 10:48:04 +10:00
a8e8106761 flaky tests: fix focus-follows and stacked-member Alt+Delete, un-flake the stable suite 2026-08-31 10:02:17 +10:00
4ee153aa00 tab-groups off: give a related window on its own tab real crown, geometry, and close semantics
With --tab-groups off a related window (e.g. gnome-terminal launched from
the shell inside a terminal tab) is given its own fresh tab, but inherited
the grouped-on-parent treatment of --tab-groups on.  It showed on screen
but never took the crown, so the overlay cursor stayed on the tab it was
launched from.  Apart the two cases with the structural ownership test
w->tab == w->parent_tab (on: shares the parent tab; off: owns its own):

- both map handlers promote the own-tab window (set_active_tab) instead of
  entering it without a transition, so the crown and overlay cursor follow
  what is shown;
- own-tab X11 windows get the maximized-tab geometry, not the transient
  float;
- closing an own-tab X11 window tears its tab down instead of ignoring the
  close;
- they log a real [claim] slot rather than [sub] stack, and are re-maximized
  when the host leaves fullscreen.
2026-08-31 09:34:42 +10:00
be45053631 refactor: unify kick_app/kick_tab_group_member into a single kick_window
Windows now own the scene wiring uniformly; a tab is a pure list head of
identical kick_window structs with no privileged "main" member.  X11 and
xdg paths share one map/focus/claim/destroy tail, the tab-group and
external-to-own-tab behaviours ride on a single origin flag, and tab
membership is just window list order.

Bugs fixed in the port:
- pick_successor took -1 as a truthy prefer_right, returning NULL when the
  rightmost tab died and tearing the session down; the parameter is now a
  bool so the misuse cannot recur.
- reorder_active_tab wrote a tab back into its old slot because
  wl_list_insert() inserts AFTER its anchor; the middle-slot case now
  anchors on the predecessor.
- claim_xdg_window left the freshly created scene tree ENABLED (unlike the
  X11 path's explicit disable), so a still-black window of the active tab
  read as "already drawn" and its first map skipped the seat keyboard
  enter -- the fix that makes forwarded keys (and the client fullscreen
  mirror) reach the client in nested sessions.
- the active tab's first map no longer re-announces a fresh active
  transition: the boot crown already owns the tab, so prefer_one stable
  active_count under load.
- restored the "[x11] member associate app=%d" log for grouped windows so
  the old log contract is preserved.
2026-08-31 09:02:47 +10:00
469e31115b tab-groups: let a tab survive when its main surface dies but sub-members live
When the main surface (X11 or XDG) is destroyed, remove the stale main
wrapper from the member list instead of tearing down the whole tab.
Sub-members are the tab's new content; only when the member list is
empty do we finish the destroy.

Changes:
- app_has_window(): also return true for mapped sub-members
- handle_toplevel_destroy / handle_xs_destroy: remove the main wrapper,
  keep app->stack alive, return early if sub-members remain
- member_destroy_common: tear down the tab when the last member goes and
  the main surface is also dead
- set_active_app: hide the old tab's stack unconditionally (not just when
  old->toplevel is set) so a survivor tab is properly hidden on switch

Fixes: libreoffice crash (splash screen destroy triggered exit)
Fixes: black window after closing libreoffice (sub-member teardown missing)
2026-08-27 11:49:54 +10:00
4367ed29b5 add fuzz tests: launch real programs inside kick to find crashes
14 fuzz tests (pytest -m fuzz) that launch foot, alacritty,
gnome-calculator, evince, xmessage, and combinations thereof inside
kick, then exercise basic interactions (cycle, overlay, close, rapid
cycling, tab-groups stacking, unresponsive close).

Excluded from the default pytest run via the fuzz marker, like flaky.
2026-08-27 10:19:52 +10:00
fc89b1ad27 refactor: eliminate three code duplications in kick.c
1. Four identical set_title/set_app_id handlers (toplevel, sub, xdg)
   collapsed into handle_overlay_refresh().

2. Map activation decision extracted to map_should_activate(), shared
   by both the xdg and X11 map handlers.

3. Timer cleanup removed from finish_app_destroy() -- remove_app()
   already handles it unconditionally.
2026-08-27 09:40:12 +10:00