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.
626 lines
No EOL
32 KiB
Markdown
626 lines
No EOL
32 KiB
Markdown
# 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`. |