kick/README.md
Andrew Hamilton 80e4e288ad feat: keep hidden overlay-preview thumbnails live while held
Hidden tabs live in disabled scene trees, so
wlr_scene_output_send_frame_done() never reaches them and
frame-callback-throttled clients stop committing: only the shown
window's thumbnail tracked live while the rest sat at their last
frame.

Fan frame-done out to every hidden tab window once per output frame
while the overlay is held (overlay_send_hidden_frames after the scene
frame-done in both render_standalone and render_and_present). The
walk covers main plus subsurface trees for both xdg and X11 windows
via window_surface(), and skips the shown window the scene path
already handles.

Gated on --overlay-previews plus overlay_visible, so the steady state
with the menu hidden costs nothing and background clients are not
woken. The existing thumb_capture snapshot and draw-time tree
composite paths are unchanged; they simply receive the commits they
were missing.

Tests: new test_standalone_headless_overlay_previews_hidden_live uses
a hidden --pulse tab and asserts the red thumbnail count swings while
Alt is held. README overlay-previews paragraph updated from freeze to
live-while-held.

Verified: ./build.sh clean; overlay-previews suite 6 passed; full
suite 179 passed with -n4.
2026-10-02 10:24:27 +10:00

626 lines
No EOL
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# kick
A nested Wayland compositor (built on wlroots) that presents several
client apps as tabs in a single window on the host Wayland compositor.
Each command-line argument launches one tab.
## Building
The project builds with [Meson](https://mesonbuild.com/) + Ninja. On most
distributions the non-wlroots dependencies are available in the standard
repositories; `wlroots` is used from the system where one is installed, and
otherwise is fetched and built automatically from a pinned source tarball
(`subprojects/wlroots.wrap`).
```sh
meson setup build
meson compile -C build
```
The resulting binary is `build/kick`.
The Wayland protocol stubs (`xdg-shell`, `xdg-decoration`,
`cursor-shape-v1` and `tablet-v2`, `ext-data-control-v1`,
`ext-image-copy-capture-v1`, `ext-image-capture-source-v1`) are not
committed; they are generated into the build directory by `wayland-scanner`
from the system `wayland-protocols` (and the bundled
`wlr-layer-shell-unstable-v1.xml`), so they always match the installed
protocol headers. This adds no extra dependency beyond what the wlroots
fallback already requires. (`tablet-v2` is generated only because
cursor-shape's implementation references `wl_tablet_tool_v2_interface`.)
### Debian / Ubuntu
```sh
sudo apt install build-essential meson ninja-build pkg-config \
libwayland-dev libwayland-bin wayland-protocols \
libxkbcommon-dev libpixman-1-dev libpango1.0-dev \
libegl1-mesa-dev libgles2-mesa-dev
```
No wlroots package is needed: this project targets wlroots 0.20, and
where the distribution ships it as a versioned package (e.g.
`libwlroots-0.20-dev` on Debian >= trixie, pkg-config name `wlroots-0.20`)
Meson uses the system copy directly; otherwise it builds the bundled
0.20.2 instead (see below).
### Arch
```sh
sudo pacman -S base-devel meson ninja pkgconf wayland wayland-protocols \
wlroots xkbcommon mesa pixman pango
```
### Fedora
```sh
sudo dnf install gcc meson ninja-build pkgconf-pkg-config \
wayland-devel wayland-protocols-devel wlroots-devel pango-devel \
libxkbcommon-devel mesa-libEGL-devel mesa-libGLES-devel pixman-devel
```
### When the system wlroots dep is missing / the fallback can't find one
The Meson setup searches for `wlroots-0.20` then `wlroots`; if neither is
installed it builds the pinned 0.20.2 from `subprojects/wlroots.wrap`, which
only needs the wayland, wayland-protocols, xkbcommon and EGL/GLES2 dev
packages listed above. On older Debian/Ubuntu the packaged wlroots is a
versioned library for another series (e.g. `libwlroots-0.19-dev`, pkg-config
`wlroots-0.19`) whose version falls outside this project's `>=0.20,<0.21`
pin, so the fallback build is used automatically and no wlroots package
needs to be installed. Optional wlroots features (Vulkan, X11, libinput, etc.) are
auto-detected and skipped when their build deps aren't present, so a minimal
install works too.
## Usage
```
kick [--start-cmd-index N] [--focus-priority] [--cursor-size N] [prog...]
```
Each `<prog>` is one shell command; quote it when it takes arguments, e.g.
`kick "foot -e htop" "firefox -no-remote"`. With no arguments, a
single terminal tab is opened (see `$TERMINAL` below). `--start-cmd-index
N` starts focused on command `N` (1-based; the last command by default).
`--focus-priority` favors the active window: it runs at normal CPU/IO priority
(nice 0, best-effort/4) while every other window's process group is pushed
down (nice +10, best-effort/7). Off by default; needs no sudo because
background groups are only ever deprioritized. Rebalanced on every focus
change, including each Alt+Tab preview step. Works nested and standalone
(strongest standalone, where no host scheduler competes).
`kick -h` prints this usage.
Every argument starts a new tab. There is no tab-count limit. Client apps run
as children of kick with `WAYLAND_DISPLAY` pointed at the kick compositor and
`XAUTHORITY` cleared; `KICK_MODE`/`WLR_BACKENDS` are scrubbed too, since they
are kick's own config -- a nested kick started from a tab must auto-detect
instead of inheriting standalone. Toolkits then self-select — Wayland-capable
apps connect to kick directly, X11-only ones (emacs-gtk, xmessage) use
`DISPLAY` at the embedded Xwayland when XWayland support is available
(see below).
kick never forces `GDK_BACKEND`/`QT_QPA_PLATFORM`: an explicit backend value
removes a toolkit's fallback and breaks X11-only builds. If a particular app
misbehaves under native Wayland, wrap its command, e.g.
`"GDK_BACKEND=x11 myapp"`.
### XWayland
X11 applications work as tabs alongside Wayland clients: `kick xmessage hi`
opens xmessage in a tab like any other command. kick embeds an Xwayland
server (started lazily on first use) and claims each X11 window by the
process group of its `_NET_WM_PID`, using the same pgid matching as Wayland
clients — so an X11 program's windows land in the tab reserved for its
command-line argument.
- Managed X11 windows are resized to fill the output and follow every tab
rule (focus, cycling, Alt+Delete close via WM_DELETE_WINDOW with the usual
SIGTERM/SIGKILL escalation).
- Override-redirect windows (menus, tooltips, popups) float above the tabs
at their requested position; they take keyboard focus only when they ask
for it.
- Transient/dialog X11 windows follow the same rules as Wayland subsurface
dialogs (`--tab-groups on|off`).
- Set `KICK_NO_XWAYLAND=1` to disable the embedded X server entirely
(`DISPLAY` is then unset again).
Requirements: wlroots built with XWayland support plus `xcb`, `xcb-icccm`
and `xcb-ewmh` dev packages; if any are missing, kick builds without X11
support and behaves exactly as before. Known limitation: X11 cursor themes
are not applied inside X11 windows (kick does not bridge the legacy
`wl_pointer.set_cursor` path).
`Alt+Shift+T` opens the terminal named by the `$TERMINAL` environment variable
when it is set (see Key bindings); otherwise it falls back to `alacritty`. The
value is used verbatim, so it may carry arguments, e.g.
`TERMINAL="foot -e htop" kick ...`.
Notes:
- An argument written bare (starting with `-`) is taken as a tab launcher too
(unless quoted); a warning is printed when it looks like a stray option.
- Each command (an argument or the default `$TERMINAL`) is resolved on `PATH`
before the session starts: if its program isn't found, kick prints
an error and exits non-zero without opening any window.
- The client applications run inside the session, so closing their windows
only closes their tab; closing the host kick window, or sending
SIGINT/SIGTERM, shuts the whole session down and kills all client children.
### Startup order
The command-line commands are all launched at the same time, in the order
you wrote them. Each command runs in its own process group, and when its
client connects it claims its own slot by that group — so every tab keeps
its command-line position even with wrapped commands like `sh -c "foot -e
htop"`, no matter which program takes longest to open its window.
Until a client connects, its tab is black and listed in the overlay under
its **command line** instead of a window title. From the very first frame
the **last** command's tab is the active one, so you wait only on that
single tab: when it maps, the window reveals it and nothing steals focus
before that, so you don't flicker through every intermediate tab. This
"last command wins" rule also means the tab you see on startup is always
the one from the last argument. Pass `--start-cmd-index N` to start focused
on a different command instead: `N` is 1-based (the first command is 1),
and that command's tab is black-and-active from the first frame just like
the default last one.
If a command never connects — e.g. `sleep 999` — the rest of the command
line still starts (they all launched together) and the window shows the
last tab as soon as it is ready. A command whose whole process group exits
without ever connecting (e.g. `kick /bin/false`) drops its tab
entirely instead of leaving a permanent black hole.
Closing a tab takes it out of the list for good: its slot is removed and
the remaining tabs reflow into place — there are no holes for later clients
to fall into, so externally started windows are always appended, not
absorbed into a dead tab's position.
Later tab openings follow the same no-steal rule: a brand-new tab never yanks
the active surface on its own. `Alt+Shift+T` still focuses its fresh
terminal, and a tab-group member of the tab you are currently looking at
becomes active; every other new window or tab just appears in the list, hidden, until
you cycle to it.
### Tab groups: `--tab-groups on|off`
A window is a *tab-group member* of an existing tab when its client process is
a (direct or indirect) child of that tab's client process — kick walks the
parent-PID chain (`/proc`) up to a few levels to find the owning tab. So when a
terminal tab's shell launches `gnome-calculator`, or an app opens a dialog or
palette, those windows are "related" to the tab they came from (the whole
forked process tree, not just a single process). `--tab-groups` picks how they
appear:
- `on` (default): related windows never get a tab. They render stacked above
their parent tab's surface, share the tab's visibility, and hold the keyboard
while that tab is shown. They are invisible to cycling and to the
Alt-held overlay, and closing the tab closes them with it (same process
tree). When a stacked window closes, focus falls back to the tab's own
surface.
- `off`: each related window gets its own tab glued immediately to the
right of its parent tab. It activates only if the parent tab is the one
being shown; otherwise it waits hidden until you cycle to it.
`--tab-groups on` groups a tab with its related windows the way a browser groups
tabs: the dialogs and auxiliaries feel like part of the one window. `off` keeps
every surface individually reachable, which is kick's original model.
### Panel (`--panel`)
`--panel CMD` launches a wlr-layer-shell panel (e.g. waybar) as an
always-on-top bar that sizes the tabs underneath it. The panel is launched
with a plain exec and shares the login session's D-Bus bus, like every other
compositor's panels. A single-instance panel therefore cannot nest: if the
host session already runs one (e.g. `wf-panel-pi` >= 1.31, which owns
`com.raspberrypi.wfpanelpi`), the nested copy defers to it and exits without
ever mapping, so no bar appears. Use a multi-instance panel, stop the host
copy, or run kick as the session itself (standalone), where no competitor
exists.
## Key bindings
| Key | Action |
|----------------------------|---------------------------------------------------|
| `Alt` (hold) | Show the tab-title overlay (centered) |
| `Alt+Up` | Previous tab (hold to keep cycling) |
| `Alt+Down` | Next tab (hold to keep cycling) |
| `Alt+Tab` / `Alt+1` | Walk windows most-recently-used first (hold to keep walking; the stack commits on release) |
| `Alt+Shift+Up` | Move the active tab earlier in the list |
| `Alt+Shift+Down` | Move the active tab later in the list |
| `Alt+Delete` | Close the active window (or the tab when it's the only window) |
| `Alt+Shift+T` | Open a new terminal tab next to the current one |
| `Alt+Left` / `Alt+Right` | Cycle the active tab's tab-group windows (Left = previous, Right = next; hold to keep cycling) |
| `Alt+F11` | Toggle fullscreen: the kick window in the host when nested, every client directly when standalone |
The tab-management prefix is `Alt` by default. Pass `--modifier super` to
use `Super` (Windows/Meta key, Mod4) instead: `Super` hold shows the
overlay, `Super+Up/Down` cycle, `Super+Tab`/`Super+1` walk recent windows,
`Super+Shift+Up/Down` reorder,
`Super+Delete` closes, and `Super+Shift+T` opens a new terminal -- the
new-tab chord follows the prefix like every other binding.
The `Super` variant is meant for standalone/DRM and labwc-style hosts
(including the Raspberry Pi, where nothing reserves it); under GNOME the
host grabs most `Super` chords before the nested compositor ever sees them
(e.g. `Super+Up` maximizes the kick window), so keep the default `Alt`
there. labwc's own defaults also leave `Super+Shift+arrow` and
`Super+Delete` free even when kick is nested inside labwc.
Holding the tab-management modifier (`Alt`, or `Super` with `--modifier
super`) shows a passive overlay listing every tab; the list updates live as
you move the focus and disappears on release. It is drawn by kick itself, so
it works on hosts with or without a title bar (the overlay text is rendered
with Pango from a system font family; set `KICK_FONT` to pick one, e.g.
`KICK_FONT="DejaVu Sans"`, default `Sans`). Each tab row lists that
tab's windows -- its main surface plus any tab-group members -- each shown
with its own icon (resolved from the window's `app_id` via the desktop files
and icon themes in `XDG_DATA_DIRS`, PNG via libpng, SVG via librsvg; a window
without a resolvable icon gets a generic placeholder). The focused window is
highlighted like a cursor: `Alt+Up`/`Alt+Down` switch tabs and
`Alt+Left`/`Alt+Right` move between windows within the active tab, so the
highlight walks through every window in the session.
Pass `--overlay-previews` to list window thumbnails instead of titles:
each row is the window's icon followed by a small view of the window as
it last appeared (no captions). Rows divide the content-sized menu, which
stays centered with a 50 pixel top and bottom margin; no thumbnail grows
past a quarter of the screen height. A header row at the top of the menu
names the selected window (icon plus ellipsized title) without affecting
the menu size. The
shown window keeps its thumbnail live -- it commits continuously, so the
preview tracks it. While the overlay is held, hidden windows are sent
frame callbacks too, so every thumbnail tracks live; with the overlay
hidden they freeze again (no background wakeups), sitting at the last
frame from the hold until they paint again on their own (a newly mapped
window still appears mid-hold). Both software (SHM) and GPU (DMA-BUF) clients are covered, and
a tab with no mapped window keeps its command-line text row. The plain
title list never wakes background clients this way.
`Alt+Tab` (and its synonym `Alt+1`, the chord that survives hosts like
GNOME which grab `Alt+Tab`, `Alt+Esc` *and* `Alt+Backtick` -- the last from
the metacity-era `switch_group` default) walk the session's
windows most-recently-used first: each press previews the next window after
the one you are looking at, wrapping at the bottom, and holding the chord
keeps stepping via the same synthesized repeat as tab cycling. While the
modifier is held the ordering does not change — every focus change is only a
preview — so the stack itself updates on release: releasing after moving
commits the shown window to the front of the MRU order. A bare hold that
never navigates leaves the order untouched. With the modifier released,
focus changes (e.g. switching to a window) promote that window to the front
immediately. Like every navigation chord, `Alt+Tab`/`Alt+1` are grabbed
so the focused client never sees them; `Shift` is ignored (there is no
reverse direction).
`Alt+Shift+T` launches a fresh terminal (`$TERMINAL`, default
`alacritty`) as a new tab placed immediately to the right of the current tab,
and it becomes the active tab once it maps (the one exception to the
no-map-steal rule at startup, along with tab-group members of the shown tab).
The tab-cycling hotkey only emits a physical press for the first tab switch;
repeats are synthesized, so holding `Alt+Up/Down` keeps stepping through
tabs while any other key stops it.
`Alt+Shift+Up/Down` reorder the overlay list instead of switching: the
active tab stays active, it just moves one position earlier or later in the
order. Moving off either end wraps around and the whole list shifts — a
rightmost tab moved past the end lands first, with every other tab pushed
one position, not merely swapped with the first. Holding the chord keeps
moving it (same synthesized repeat as cycling). An `Alt+Shift+T` inside the
moved tab opens exactly where the reordered list says it should — immediately
right of the tab's new position.
Both `Alt+Up/Down` and `Alt+Shift+Up/Down` are grabbed: kick consumes them
so the focused client never receives the chord (e.g. a terminal can't act on
`Shift`+arrow while you reorder).
`Alt+Delete` closes the active window. When the tab holds more than one window
(a tab group), only the focused window closes and the tab stays up; when it is
the tab's only window, the whole tab disappears immediately (the tab to its
right becomes active, or the left neighbor if it was the rightmost); the
client's process group gets a `SIGTERM`, escalated to `SIGKILL` after a short
grace if it ignores the signal. A window or tab that closes on its own (its
program exits) instead hands active off to the left neighbor.
Function keys are **not** grabbed: `F9`/`F10`/`F11` (and anything else) are
forwarded to the focused client, so apps keep their own bindings (e.g. a
browser's `F11`).
## Fullscreen
## Fullscreen
Fullscreen is per overlay entry:
- Fullscreen the kick window (your host compositor's fullscreen keybinding,
or `Alt+F11`) and only the focused overlay entry is told it is
fullscreen, so it drops decorations and fills the screen. Layout and
panels follow the shown entry: while the fullscreen entry is shown the
bar is hidden and tabs take the whole output; switch to a normal entry
and the bar returns with the panel-respecting usable layout, even while
the fullscreen entry waits in the background. A black tab (client not
yet mapped) picks fullscreen up when it maps.
- A shown client that requests fullscreen itself (e.g. Firefox F11) takes
the host window with it: one action fills the whole monitor, still for
that entry only. Exiting (second F11) drops the host chrome again. A
hidden tab's request is remembered via the protocol and live when you
cycle to it, without touching the host.
- Client chrome buttons follow the same rule, since every tab lives
maximized: an app's maximize/restore toggle fullscreens its entry (a
request arriving before first display stays plain maximize, so fresh
windows never open fullscreened), and minimize steps a fullscreen entry
back down to maximized. Taskbar buttons behave the same way.
- Leaving fullscreen returns every window to maximized (X11 tab-group
members keep their own geometry).
## Standalone mode
At a Linux virtual console, plain `kick` automatically runs **standalone**:
it becomes its own Wayland compositor, rendering directly onto the native
display (DRM via wlroots) — the first output only; extra monitors are
ignored. In this mode kick takes over the screen, and the tab overlay is
composited onto the output in a second render pass instead of being drawn
into a host window.
On a real DRM output the overlay (the `Alt` tab list and the confirm-quit
modal) is presented on a **hardware plane** instead: its own small buffer is
scanned out by libliftoff (wlroots' `wlr_output_layer` path), so the main
buffer never carries the overlay and direct scanout of a focused fullscreen
client keeps running underneath it. The plane is probed once per output: the
first overlay hold commits to the plane, and whether libliftoff accepted the
layer (fixed hardware gives a fixed answer) sets the verdict for the whole
session -- every frame after that just asks the plane with a buffer or
without, and the commit itself arms or disarms it. If the probe finds no
plane (too few planes, format or placement constraints, multi-GPU rendering),
kick stays on the composite second pass for the session; the headless backend
is always composite (it would claim every layer "accepted" while drawing
nothing). `KICK_OVERLAY_PLANE=0` forces the composite path everywhere for A/B
comparison; the verdict is logged by `[render] overlay plane available` /
`[render] overlay plane not available, compositing`.
The auto-detection fires only when none of `KICK_MODE`, `WLR_BACKENDS`,
`WAYLAND_DISPLAY` or `DISPLAY` is set **and** either kick's stdin is a real
VT (`/dev/tty<digits>`) or the login advertises a Wayland session
(`XDG_SESSION_TYPE=wayland`, as every display manager/greeter sets): there
must be nothing to nest into, and SSH sessions, terminal emulators and
pipes match neither test, so they keep the nested default. The chosen mode
is logged at startup as `[mode] standalone|nested (reason)`. Escape hatches,
in precedence order:
- `KICK_MODE=standalone` forces standalone everywhere.
- `KICK_MODE=nested` pins nested even at a console.
- Any non-empty `WLR_BACKENDS` forces standalone and selects the backends.
Otherwise kick runs **nested** by default: it opens a window on the host
Wayland compositor and draws every tab inside that single window.
- `WAYLAND_DISPLAY` is ignored in standalone mode (it never connects to an
outer compositor), except as part of the requested `WLR_BACKENDS`.
- Real keyboard/pointer devices are attached to the kick seat directly
(native input); the `Alt+Shift+T` and `Alt`-overlay hotkeys still work off
those devices. Touchscreens work the same way: standalone mode delivers
native touch events to the focused client, and nested mode forwards touch
from the host seat. Multi-touch slots are preserved; touches do not move
the mouse pointer.
- `Ctrl-Alt-F1..F12` switch virtual terminals while running on the DRM
backend (via libseat/logind); the terminal sees nothing of the chord.
- The stdin event feed used by the test harness works identically in both
modes, so the same synthesized-input tests cover nested and standalone runs.
- Headless testing: `KICK_MODE=standalone WLR_BACKENDS=headless` runs
standalone on wlroots' headless backend (no DRM needed), useful in CI or
when no TTY is available.
`test_nested_host_primary_to_kick` exercises the host->nested primary
selection path, which only exists over the ext-data-control bridge; it skips
on hosts without it (e.g. GNOME), so use an ext-data-control host (labwc,
sway) for it.
Known-flaky tests are marked `@pytest.mark.flaky`. `pytest.ini` defaults to
`-m "not flaky"`, so they are excluded from every plain run (`./test.sh` or
direct `pytest`). Run them on demand with `./test.sh -m flaky`.
Parallel runs: `./test.sh -n4` splits the suite across workers
(pytest-xdist), cutting wall time ~3x (156s to ~47s here). Every spawn
uses unique log paths and per-session dump directories (`KICK_DUMP_DIR`),
and sockets are pid-unique, so workers stay isolated. The serial run
remains the deterministic gate: a few timing-sensitive tests can flake
under parallel load (they pass alone and serially).
## Cursor shapes (`cursor-shape-v1`)
Kick advertises the `cursor-shape-v1` protocol (as `wp_cursor_shape_manager_v1`,
version 1). Nested clients that switch the pointer's cursor shape are honoured:
- **Standalone** mode applies the requested shape to the compositor's own
cursor, looked up in the current `XCURSOR_THEME` (so e.g. GTK's text cursor
works over a galculator entry). Pass `--cursor-size N` (8..128px,
default 24 or `$XCURSOR_SIZE` when set) to size that cursor.
- **Nested** mode has no cursor of its own: the shape is forwarded to the host
compositor's pointer via `wp_cursor_shape_device_v1.set_shape`, and the host
(which draws the cursor over our window) applies it. `--cursor-size` is
ignored there.
The shape is remembered per tab and the cursor is reset to `default` when you
switch to a tab whose client last asked for nothing (or asked for `default`),
so a text cursor never leaks from one app into the next.
Legacy `wl_pointer.set_cursor` surfaces are **not** bridged: a nested client
that still uses the old protocol (some older toolkits) keeps no visible cursor
over the window, and a host without `cursor-shape-v1` simply draws its default
cursor (logged as `[cursor] host shape n/a` at debug level).
## Text input / input method (`text-input-v3`, `input-method-v2`)
Kick advertises `zwp_text_input_manager_v3` and `zwp_input_method_manager_v2`,
so in-session clients (toolkit text entries) and in-session input methods
(e.g. fcitx5, on-screen keyboards) work with each other in both nested and
standalone mode. The focused tab's field state (surrounding text, content
type) is relayed to the input method, and its preedit/commit strings back to
the field; focus follows tab switches, and the IME's candidate popup floats
above the tabs. While the IME holds its keyboard grab, keys go to it instead
of the focused client (the `Alt` tab-management chords still run first).
Relay activity is logged with the `[im]` prefix at `KICK_LOG=2`.
## Content type / single-pixel buffers
Kick advertises `wp_content_type_manager_v1`, so video players and games can
tag their surfaces (`photo`/`video`/`game`; the type is recorded in the
`[diag] map` line at `KICK_LOG=2`), and
`wp_single_pixel_buffer_manager_v1`, so clients can back a solid-color
surface with a 1×1 buffer (scaled up via `wp_viewporter`, which kick also
advertises) instead of a shared-memory pool.
## Gamma control (`wlr-gamma-control`)
Kick advertises `zwlr_gamma_control_manager_v1`, so night-light tools
(wlsunset, gammastep) can set the output's gamma ramp. Ramps are committed
to the single output via the standard wlroots apply path (a departing
client resets it); outputs with no gamma LUT report `failed` instead, so
on headless/nested outputs such tools learn the output is unsupported
rather than hanging. Like all hardware LUTs, this only takes visible
effect on DRM in standalone mode.
## Modal dialogs (`xdg-dialog`)
Kick advertises `xdg_wm_dialog_v1`, so toolkits can mark a toplevel modal
instead of relying on process ancestry alone. A modal dialog demands
attention: when it maps (or is set modal while mapped) its tab is shown
and the dialog takes window focus -- the deliberate exception to the
no-steal rule, alongside `Alt+Shift+T`. Placement is unchanged
(`--tab-groups on` still stacks it with its tab; `off` still gives it its
own reachable tab), and modality works identically nested and standalone
(it never depends on the host compositor). Session lock still wins over a
modal.
## Pointer gestures (`pointer-gestures`)
Kick advertises `zwp_pointer_gestures_v1`, so touchpad clients (browsers,
drawing apps, map viewers) receive swipe/pinch/hold gestures. Gestures fan
out to whichever client holds seat pointer focus. In standalone mode real
libinput touchpad gestures feed this; in nested mode the host only offers
`wl_pointer` (no gesture source), so gestures there come from clients that
synthesize them. Relay activity is logged with the `[gesture]` prefix at
`KICK_LOG=2`.
## Tearing control / fractional scale
A surface opting into `wp_tearing_control_v1` async presentation drives
the output commit's async-flip flag from the shown tab's hint (logged as
`[tearing] async|vsync` at `KICK_LOG=2`, transitions only): on DRM this
becomes a real tearing page flip for game tabs, while headless and nested
outputs correctly ignore it. Every surface is also told the output scale
via `wp_fractional_scale_v1` when claimed, so clients never have to guess
(120 = 1.0).
## Window icons (`xdg-toplevel-icon`)
Kick advertises `xdg_toplevel_icon_manager_v1` (preferring 28 px, the
overlay's draw size). A client-supplied icon beats the desktop-file guess:
the largest uploaded buffer becomes the tab's overlay texture, so exact
artwork shows where clients provide it. The 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, and windows that never
set one behave exactly as before.
## Output management (`wlr-output-management`)
Kick advertises `zwlr_output_manager_v1`, so display tools (kanshi,
wdisplays) can list the single head and request sizes. Standalone honors
custom sizes through the normal resize path (tabs reflow, clients learn
the new size); nested refuses anything but the live size, because the
host window owns the geometry there. Disabling, scale and transform
changes are refused everywhere: one fixed output per session.
## Core fixes (`wl_fixes`)
Kick advertises `wl_fixes` (version 1), the core-protocol repair global:
clients such as Firefox use it to destroy registries cleanly instead of
tripping protocol errors on teardown. wlroots serves the requests; the
compositor only advertises the global.
## Window tags (`xdg-toplevel-tag`)
Kick advertises `xdg_toplevel_tag_manager_v1`, so toolkits can label a
toplevel with an untranslated tag ("settings", ...) plus a translated
description. Tags show in the overlay row ("title [tag]") and, with
`--tab-groups on`, windows carrying the same tag as an existing tab's
window stack onto that tab instead of opening their own (browser-style
tag groups). A tag set in the initial commit is picked up at claim; later
updates rewrite the live window in place without moving it between tabs.
## Color representation (`color-representation-v1`)
Kick advertises `wp_color_representation_manager_v1`, so clients learn
the alpha modes and matrix-coefficient/range pairs the session supports
(premultiplied-electrical alpha, identity coefficients in full range --
honest SDR sRGB) and can declare their surface's representation (validated
by wlroots).
The GLES2 renderer speaks SDR sRGB, so this is mostly a correctness
handshake for color-aware clients rather than visible behavior today.
## Color management (`color-management-v1`)
Kick advertises `wp_color_manager_v1`, so color-aware clients can
negotiate image descriptions with the session (perceptual render
intent, extended sRGB transfer characteristics and sRGB primaries -- honest SDR
sRGB, no ICC/parametric features). The scene is linked to the manager
so per-surface feedback flows; like color representation, this is a
correctness handshake on the GLES2 renderer rather than visible
color work today.
## System bell (`xdg-system-bell-v1`)
Kick advertises `xdg_system_bell_v1`, so clients can ring the system
bell on their behalf. The protocol leaves the rendering up to the
compositor (sound, visual flash, or nothing); kick logs each ring as
`[bell] ring` at `KICK_LOG=1` and otherwise renders nothing, keeping
the quiet-by-default contract.
## Alpha modifier (`alpha-modifier-v1`)
Kick advertises `wp_alpha_modifier_v1`, so clients can multiply their
surface's alpha (fades, translucent video) without repainting.
wlroots applies the multiplier when rendering, so like color
representation this is a passive correctness handshake rather than
visible compositor work.
## Window decorations (`xdg-decoration`)
Kick advertises `zxdg_decoration_manager_v1`, so toolkit clients
(libdecor, Qt) can negotiate who draws window frames. Every tab is
maximized and kick draws no frames of its own, so new toplevels are
pinned to client-side and later server-side requests are ignored:
client headerbars keep working, and nothing wastes tab space on
compositor-drawn borders.
## Logging (`KICK_LOG`)
Quiet by default: a plain launch prints nothing to the launching terminal
except fatal errors. Set `KICK_LOG` before starting:
| Value | Meaning |
|-------|------------------------------------------------------------------|
| unset/`0` | Quiet. Only fatal errors are shown. |
| `1` | Info. Status lines, dump summaries, host notes (e.g. socket). |
| `2` | Debug. Adds a per-frame `[diag]` trace and EGL/render/import messages, and client stdout/stderr flows through. |
At debug level (`2`) client programs also inherit the launching terminal's
stdout/stderr instead of being silenced, which makes client errors visible.
## Frame dumps (SIGUSR1 / KICK_AUTODUMP)
The `SIGUSR1` signal and `KICK_AUTODUMP` write raw `P6` portable-pixmap
files to `/tmp` regardless of the log level:
- `SIGUSR1` writes `/tmp/scene_N.ppm` — the composed scene (whatever
kick rendered for the host).
- `SIGUSR1` writes `/tmp/host_N.ppm` — the host compositor's back
buffer, so you can see what the host actually displayed. In standalone
mode this is the composited frame being presented to the output (scene
plus the `Alt` overlay when the overlay rides the composite path; when it
is scanned out on a hardware plane it lives outside the frame buffer, so
dumps won't show it).
- `N` is an incrementing per-run counter; a single `SIGUSR1` produces a
matching scene/host pair.
- With `KICK_AUTODUMP` (any value, e.g. `KICK_AUTODUMP=1`), a scene and
host dump are taken automatically once the first frame in which a client
committed a CPU-readable (shared-memory) buffer — handy for capturing
exactly what a newly launched client drew.
- At `KICK_LOG=2` the diagnostic that detects the client's own shm buffer
additionally writes that buffer to `/tmp/client_N.ppm`.