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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
--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.
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.
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).
--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).
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.
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.
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)
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.
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.
meson setup --reconfigure touches subprojects/.wraplock every run,
making it newer than build/kick and triggering an infinite rebuild
loop. The file is meson housekeeping, not a real source dependency.
Three places detached signal listeners from tab-group members:
handle_sub_destroy, handle_sub_x_destroy, and remove_app. The
remove_app copy had already drifted (missing set_title/set_app_id).
Extract two helpers so the logic lives in one place.
When remove_app() force-cleans a tab's group members (e.g. the main
toplevel's destruction cascades before a sub-member's own destroy fires),
it removed destroy/map/unmap/commit but not set_title/set_app_id. The
freed member's listener stayed on the sub-toplevel's signal list; when
wlroots later destroyed that toplevel the non-empty listener assertion
fired.
Crash repro: foot+foot tab, run gnome-calculator in one terminal, cycle
to the main surface, Alt+Delete. Both processes die from SIGTERM;
remove_app() frees the calculator member without detaching set_title,
and wlroots asserts on the calculator toplevel teardown.
enabling direct scanout everywhere reintroduced the exact failure the
global disable once guarded against: in nested mode a "scanned-out"
buffer on the headless output is displayed nowhere by itself -- kick
must blit every presented buffer into the host window, importing the
client's dmabuf into the host EGL context per frame. vlc's
scanout-feedback-tuned buffers reject that import per frame
(EGL_BAD_PARAMETER "modifier attributes not equal"), leaving black video
behind playing audio.
policy is now per mode:
- standalone (DRM/VT console): scanout enabled -- true zero-copy
presentation with no composite or blit. an attach-render lock is held
while the tab overlay is visible so the overlay's second pass lands on
the composited frame instead of being painted onto the client's own
buffer, and recreated outputs inherit the lock.
- nested: always composites. KICK_NESTED_SCANOUT=1 re-enables scanout
there for testing.
also scope the software-cursor lock to headless standalone outputs:
their set_cursor hook pretends a hardware plane exists while drawing
nothing, so the lock was needed there -- but on real DRM it made direct
scanout permanently ineligible (a composited cursor cannot survive
scanout), defeating zero-copy fullscreen entirely.
direct scanout was globally disabled after nested-mode black frames
(Firefox): the scene handed the client's own buffer to the output, and
the host-window blit could not keep it in sync. wlroots 0.19.1 forwards
explicit-sync timelines in the scanned-out state and re-evaluates
eligibility every frame, so the global kill is no longer warranted:
- remove the WLR_SCENE_DISABLE_DIRECT_SCANOUT env: scanout now engages
whenever a single opaque client covers the output -- fullscreen video,
full-bleed tabs -- and skips the scene composite entirely
- hold an attach-render lock while the tab overlay is visible so the
overlay's second pass always lands on kick's composite (the same
lock/unlock pattern labwc uses for its own panels), releasing it the
moment the prefix is released
- recreated outputs inherit the lock if the overlay is up
verified under labwc: scan-out enable/disable logged around overlay
transitions during playback, and fullscreen placement probes fill the
monitor correctly with the resizing test client.
destroying the previous wl_buffer immediately after attaching its
replacement raced the compositor's use of the last committed frame and
pulled the rug from under data-pointer access to buffers still on
screen -- black frames exactly when buffers turned over. a few KB of
shm per resize is an acceptable price for a short-lived test client;
resizes are rare.
libdecor sat on the critical path of every host-window transition even
where it drew nothing: on compositors honoring server-side decorations
(labwc/sway/KDE) it negotiated SSD, then still injected permanent
set_window_geometry hints and batched acks inside its own commit call.
labwc 0.9.8 expires a 100 ms configure deadline under load and answers
"move but keep old size" -- anchoring fullscreen views to stale geometry
forever.
the host window now selects its backend at startup: compositors
advertising zxdg_decoration_manager_v1 get kick-owned xdg_shell objects
with immediate configure acks, an explicit server-side decoration request,
and no geometry hints at all (KICK_LIBDECOR=1 forces the libdecor path);
hosts without the manager -- Mutter/GNOME forces CSD -- keep libdecor
unchanged. this also restores mirrored fullscreen semantics: a client's
fullscreen request again fullscreens the host window in one step.
create_buffer() early-returned once a buffer existed, so the client kept
attaching its original 1280x800 pixels no matter what size the compositor
configured -- every resize/fullscreen test was silently verifying against
a client that cannot resize, painting old-extent content inside whatever
window state the compositor applied.
_ensure_built() moves from Session.__init__/_spawn_standalone to module
import: a broken build now aborts pytest at collection with one clear
error, rather than erroring every test at setup (which made failing-tree
runs slow and noisy). direct-import scripts keep their coverage since
they import this module too.
the feature itself works interactively, but the test depends on which
member holds focus when the stack maps -- a startup race between the
main window activating and the extra member entering -- so it failed
deterministically in automated runs. removed rather than papered over;
the chord is still exercised manually.
tests could silently run against a stale binary when the tree failed to
compile (the suite stayed green against the previous build). Session
construction and _spawn_standalone now run ./build.sh -- incremental and
a no-op when up to date -- and raise immediately on failure, so a broken
tree errors every spawning test at setup with the compiler output above
it instead. test.sh drops its build step; pytest triggers rebuilds
itself, which also covers scripts that import this module directly.
a client's fullscreen request used to be mirrored onto the host window
via libdecor, but labwc 0.9.8 expires its 100 ms configure deadline under
load and then applies "move but keep old size" permanently -- the
enlarged image anchored at the old origin with black margins. the mirror
is gone: a client's fullscreen now fills kick's output only, leaving the
host window alone. fullscreening the host remains an explicit act
(prefix+F11 or the compositor), and the host-to-clients propagation is
unchanged.
three changes that make our side of the fullscreen round-trip as fast
and deterministic as the protocol allows:
- queue libdecor fullscreen requests until the present pipeline drains
(frame callback received), so a throttled eglSwapBuffers started
before the resize can no longer complete after the ack and marshal a
stale-size frame past it; rapid requests collapse to the latest target
- flush the ack immediately instead of waiting for the end-of-loop
flush, which can land after labwc's 100 ms configure-response deadline
- allocate the resized swapchain during host_resize rather than lazily
at first render, moving the expensive GBM allocation ahead of the ack
labwc 0.9.8 can still expire its deadline under load and then applies
"move but keep old size" permanently; these shrink that window without
curing it.
wlr_renderer_init_wl_display() is the legacy all-in-one helper: it creates
its own wl_shm (v2) and linux-dmabuf (v4) globals, which duplicated the
explicit wl_shm v1 and linux-dmabuf v5 created further down. clients that
keep per-instance state -- Qt's Wayland platform re-enters its dmabuf
feedback setup for every advertised instance, and VLC's wl_dmabuf video
output likewise -- misbehave on the duplicates: vlc's Qt UI segfaulted at
startup and, once running, its video stayed black behind playing audio.
drop the legacy call and keep the explicitly versioned pair.
x11_pending_teardown only removed the pending's associate listener when
the surface had been hooked (associated). the listener is attached
unconditionally at new_surface, so an X11 window destroyed before ever
associating -- VLC's short-lived probe windows do exactly that -- left
it linked on events.associate, and wlroots' xwayland_surface_destroy
asserted on the non-empty list, killing kick.
- Alt+Shift+Left/Right moves the focused member left/right in its group's
window list, analogous to Alt+Shift+Up/Down tab reordering
- Wraps at ends (moving right from newest lands at front)
- Overlay summoned like other navigation chords
- Terminals never see Shift+arrow (chord is consumed)
- Wrap-ternary type-error eliminated; unused 'wrapped' variable removed
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.
host_frame_configure followed every libdecor_frame_commit (the ack) with
a bare wl_surface_commit, which re-committed the still-attached previous
frame. labwc applies the acked state at that first commit, so entering
fullscreen anchored the new layout to the stale window size: the enlarged
image painted from the old origin with black margins top/left and cropped
bottom/right. pre-libdecor, the ack went out on its own listener long
before any commit, so the state-applying commit was always the next
correctly sized frame -- restore that ordering by only flushing the empty
commit for size-less initial configures, where it is still needed to get
libdecor mapped.
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.
e1b137b added the late-attach damage nudge only to the X11 map path, so
Wayland members -- GTK4 dialogs and menus -- raced their first frame:
black until an unrelated repaint (a tab switch) forced one. The nudge
now fires for every member arrival on the shown tab regardless of
backend. Found via git bisect with a deterministic same-client
second-toplevel pixel probe.