2026-08-19 11:46:25 +10:00
|
|
|
# kick
|
2026-08-09 12:46:14 +10:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
The resulting binary is `build/kick`.
|
2026-08-09 12:46:14 +10:00
|
|
|
|
2026-08-19 09:54:11 +10:00
|
|
|
The Wayland protocol stubs (`xdg-shell`, `xdg-decoration`,
|
2026-08-19 15:08:31 +10:00
|
|
|
`cursor-shape-v1` and `tablet-v2`, `wlr-data-control-unstable-v1`) are not
|
|
|
|
|
committed; they are generated into the build directory by `wayland-scanner`
|
|
|
|
|
from the system `wayland-protocols` (and the bundled
|
|
|
|
|
`wlr-data-control-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`.)
|
2026-08-19 09:54:11 +10:00
|
|
|
|
2026-08-09 12:46:14 +10:00
|
|
|
### Debian / Ubuntu
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo apt install build-essential meson ninja-build pkg-config \
|
|
|
|
|
libwayland-dev libwayland-bin wayland-protocols \
|
2026-08-10 09:25:52 +10:00
|
|
|
libxkbcommon-dev libpixman-1-dev \
|
|
|
|
|
libegl1-mesa-dev libgles2-mesa-dev
|
2026-08-09 12:46:14 +10:00
|
|
|
```
|
|
|
|
|
|
2026-08-10 09:25:52 +10:00
|
|
|
No wlroots package is needed: this project targets wlroots 0.19, while
|
|
|
|
|
Debian/Ubuntu ship another version as a versioned package (e.g.
|
|
|
|
|
`libwlroots-0.20-dev` on Debian >= trixie, pkg-config name `wlroots-0.20`),
|
|
|
|
|
so Meson builds the bundled 0.19.1 instead (see below).
|
2026-08-09 12:46:14 +10:00
|
|
|
|
|
|
|
|
### Arch
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo pacman -S base-devel meson ninja pkgconf wayland wayland-protocols \
|
|
|
|
|
wlroots xkbcommon mesa pixman
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Fedora
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo dnf install gcc meson ninja-build pkgconf-pkg-config \
|
|
|
|
|
wayland-devel wayland-protocols-devel wlroots-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.19` then `wlroots`; if neither is
|
|
|
|
|
installed it builds the pinned 0.19.1 from `subprojects/wlroots.wrap`, which
|
|
|
|
|
only needs the wayland, wayland-protocols, xkbcommon and EGL/GLES2 dev
|
2026-08-10 09:25:52 +10:00
|
|
|
packages listed above. On newer Debian/Ubuntu the packaged wlroots is a
|
|
|
|
|
versioned library (e.g. `libwlroots-0.20-dev`, pkg-config `wlroots-0.20`)
|
|
|
|
|
whose version falls outside this project's `>=0.19,<0.20` 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.
|
2026-08-09 12:46:14 +10:00
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
|
|
```
|
2026-08-19 11:46:25 +10:00
|
|
|
kick [--start-cmd-index N] [prog...]
|
2026-08-09 12:46:14 +10:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Each `<prog>` is one shell command; quote it when it takes arguments, e.g.
|
2026-08-19 11:46:25 +10:00
|
|
|
`kick "foot -e htop" "firefox -no-remote"`. With no arguments, a
|
2026-08-19 09:21:46 +10:00
|
|
|
single terminal tab is opened (see `$TERMINAL` below). `--start-cmd-index
|
|
|
|
|
N` starts focused on command `N` (1-based; the last command by default).
|
2026-08-19 11:46:25 +10:00
|
|
|
`kick -h` prints this usage.
|
2026-08-09 13:02:46 +10:00
|
|
|
|
|
|
|
|
Every argument starts a new tab. There is no tab-count limit. Client apps run
|
2026-08-19 11:46:25 +10:00
|
|
|
as children of kick and connect to its private Wayland socket, so
|
|
|
|
|
`DISPLAY` is unset for them and their `WAYLAND_DISPLAY` points at the kick
|
2026-08-09 13:02:46 +10:00
|
|
|
compositor (clients are launched with the GDK and Qt Wayland backends forced).
|
|
|
|
|
|
2026-08-18 13:47:00 +10:00
|
|
|
`Alt+Shift+T` opens the terminal named by the `$TERMINAL` environment variable
|
2026-08-15 09:16:09 +10:00
|
|
|
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.
|
2026-08-19 11:46:25 +10:00
|
|
|
`TERMINAL="foot -e htop" kick ...`.
|
2026-08-15 09:16:09 +10:00
|
|
|
|
2026-08-09 13:02:46 +10:00
|
|
|
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.
|
2026-08-15 11:29:53 +10:00
|
|
|
- Each command (an argument or the default `$TERMINAL`) is resolved on `PATH`
|
2026-08-19 11:46:25 +10:00
|
|
|
before the session starts: if its program isn't found, kick prints
|
2026-08-15 11:29:53 +10:00
|
|
|
an error and exits non-zero without opening any window.
|
2026-08-09 13:02:46 +10:00
|
|
|
- The client applications run inside the session, so closing their windows
|
2026-08-19 11:46:25 +10:00
|
|
|
only closes their tab; closing the host kick window, or sending
|
2026-08-09 13:02:46 +10:00
|
|
|
SIGINT/SIGTERM, shuts the whole session down and kills all client children.
|
|
|
|
|
|
2026-08-18 11:20:29 +10:00
|
|
|
### Startup order
|
|
|
|
|
|
Launch CLI tabs in parallel; slot-claim by process group
- All command-line tabs fork at startup; the last one is active and shown
black until its first frame maps, matching the existing README contract
that the last command's window appears immediately.
- Claim incoming toplevels by process group, not exact pid, so wrapped
commands (sh -c "foot -e htop") keep their own slot even though the
sh wrapper forks the real client.
- Closed tabs are fully removed: remove_app() compacts the array, frees the
slot, clears parent refs, and prefers the left neighbor as the successor
(right on explicit close, mirroring the old behavior on destroy).
- Black (pending) tabs are live slots: cycle/reorder walk them, activation
logs '[diag] active slot=N (black, awaiting map)', and Alt+Delete closes
one outright. A SIGCHLD-latched sweep drops black tabs whose whole
process group exits before ever connecting (e.g. /bin/false).
- The map-reveal rule keys on the scene node still being disabled, since a
slot's toplevel is set at claim time (before its map event).
- Update the standalone/headless and nested tests to wait for a mapped
frame (the startup tab is active before it maps, so the old 'no tab was
ever activated' wait no longer fires), and add two tests covering
Alt+Delete on a black active tab and the never-connecting-command sweep.
2026-08-18 22:41:27 +10:00
|
|
|
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
|
2026-08-19 09:21:46 +10:00
|
|
|
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.
|
Launch CLI tabs in parallel; slot-claim by process group
- All command-line tabs fork at startup; the last one is active and shown
black until its first frame maps, matching the existing README contract
that the last command's window appears immediately.
- Claim incoming toplevels by process group, not exact pid, so wrapped
commands (sh -c "foot -e htop") keep their own slot even though the
sh wrapper forks the real client.
- Closed tabs are fully removed: remove_app() compacts the array, frees the
slot, clears parent refs, and prefers the left neighbor as the successor
(right on explicit close, mirroring the old behavior on destroy).
- Black (pending) tabs are live slots: cycle/reorder walk them, activation
logs '[diag] active slot=N (black, awaiting map)', and Alt+Delete closes
one outright. A SIGCHLD-latched sweep drops black tabs whose whole
process group exits before ever connecting (e.g. /bin/false).
- The map-reveal rule keys on the scene node still being disabled, since a
slot's toplevel is set at claim time (before its map event).
- Update the standalone/headless and nested tests to wait for a mapped
frame (the startup tab is active before it maps, so the old 'no tab was
ever activated' wait no longer fires), and add two tests covering
Alt+Delete on a black active tab and the never-connecting-command sweep.
2026-08-18 22:41:27 +10:00
|
|
|
|
|
|
|
|
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
|
2026-08-19 11:46:25 +10:00
|
|
|
without ever connecting (e.g. `kick /bin/false`) drops its tab
|
Launch CLI tabs in parallel; slot-claim by process group
- All command-line tabs fork at startup; the last one is active and shown
black until its first frame maps, matching the existing README contract
that the last command's window appears immediately.
- Claim incoming toplevels by process group, not exact pid, so wrapped
commands (sh -c "foot -e htop") keep their own slot even though the
sh wrapper forks the real client.
- Closed tabs are fully removed: remove_app() compacts the array, frees the
slot, clears parent refs, and prefers the left neighbor as the successor
(right on explicit close, mirroring the old behavior on destroy).
- Black (pending) tabs are live slots: cycle/reorder walk them, activation
logs '[diag] active slot=N (black, awaiting map)', and Alt+Delete closes
one outright. A SIGCHLD-latched sweep drops black tabs whose whole
process group exits before ever connecting (e.g. /bin/false).
- The map-reveal rule keys on the scene node still being disabled, since a
slot's toplevel is set at claim time (before its map event).
- Update the standalone/headless and nested tests to wait for a mapped
frame (the startup tab is active before it maps, so the old 'no tab was
ever activated' wait no longer fires), and add two tests covering
Alt+Delete on a black active tab and the never-connecting-command sweep.
2026-08-18 22:41:27 +10:00
|
|
|
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.
|
2026-08-18 11:20:29 +10:00
|
|
|
|
|
|
|
|
Later tab openings follow the same no-steal rule: a brand-new tab never yanks
|
2026-08-18 13:47:00 +10:00
|
|
|
the active surface on its own. `Alt+Shift+T` still focuses its fresh
|
2026-08-18 11:20:29 +10:00
|
|
|
terminal, and a sub-window 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.
|
|
|
|
|
|
2026-08-21 11:35:21 +10:00
|
|
|
### Sub-windows: `--sub-windows tabs|stack`
|
|
|
|
|
|
|
|
|
|
A client's second window (a dialog, palette, or another main window from the
|
|
|
|
|
same process) is a *sub-window* of that client's tab. `--sub-windows` picks
|
|
|
|
|
how it appears:
|
|
|
|
|
|
|
|
|
|
- `tabs` (default): each sub-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.
|
|
|
|
|
- `stack`: sub-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).
|
|
|
|
|
When a stacked window closes, focus falls back to the tab's own surface.
|
|
|
|
|
|
|
|
|
|
`--sub-windows stack` suits apps whose dialogs feel like part of the window;
|
|
|
|
|
`tabs` keeps every surface individually reachable, which is kick's original
|
|
|
|
|
model.
|
|
|
|
|
|
2026-08-09 13:02:46 +10:00
|
|
|
## Key bindings
|
|
|
|
|
|
|
|
|
|
| Key | Action |
|
|
|
|
|
|----------------------------|---------------------------------------------------|
|
2026-08-15 09:16:09 +10:00
|
|
|
| `Alt` (hold) | Show the tab-title overlay (centered) |
|
2026-08-14 08:48:28 +10:00
|
|
|
| `Alt+Up` | Previous tab (hold to keep cycling) |
|
|
|
|
|
| `Alt+Down` | Next tab (hold to keep cycling) |
|
2026-08-17 10:45:21 +10:00
|
|
|
| `Alt+Shift+Up` | Move the active tab earlier in the list |
|
|
|
|
|
| `Alt+Shift+Down` | Move the active tab later in the list |
|
2026-08-17 10:26:48 +10:00
|
|
|
| `Alt+Delete` | Close the active tab (SIGTERM, SIGKILL fallback) |
|
2026-08-18 13:47:00 +10:00
|
|
|
| `Alt+Shift+T` | Open a new terminal tab next to the current one |
|
2026-08-09 13:02:46 +10:00
|
|
|
|
2026-08-17 11:44:27 +10:00
|
|
|
The tab-management prefix is `Alt` by default. Pass `--modifier super` to
|
|
|
|
|
use `Super` (Windows/Meta key, Mod4) instead: `Super` hold shows the
|
2026-08-18 13:47:00 +10:00
|
|
|
overlay, `Super+Up/Down` cycle, `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.
|
2026-08-17 11:44:27 +10:00
|
|
|
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
|
2026-08-19 11:46:25 +10:00
|
|
|
(e.g. `Super+Up` maximizes the kick window), so keep the default `Alt`
|
2026-08-17 11:44:27 +10:00
|
|
|
there. labwc's own defaults also leave `Super+Shift+arrow` and
|
2026-08-19 11:46:25 +10:00
|
|
|
`Super+Delete` free even when kick is nested inside labwc.
|
2026-08-17 11:44:27 +10:00
|
|
|
|
|
|
|
|
Holding the tab-management modifier (`Alt`, or `Super` with `--modifier
|
|
|
|
|
super`) shows a passive overlay listing every tab's title with the
|
2026-08-14 12:39:51 +10:00
|
|
|
active tab highlighted; the list updates live as you cycle with `Up`/`Down`
|
2026-08-19 11:46:25 +10:00
|
|
|
and disappears on release. It is drawn by kick itself, so it works
|
2026-08-14 12:39:51 +10:00
|
|
|
on hosts with or without a title bar (the overlay text uses a system font
|
2026-08-19 11:46:25 +10:00
|
|
|
via FreeType; set `KICK_FONT` to pick one). Each row also shows the
|
2026-08-14 12:55:42 +10:00
|
|
|
tab's app icon, resolved from its Wayland `app_id` via the desktop
|
|
|
|
|
files and icon themes in `XDG_DATA_DIRS` (PNG via libpng, SVG via
|
|
|
|
|
librsvg); tabs without a resolvable icon get a generic placeholder.
|
2026-08-14 12:39:51 +10:00
|
|
|
|
2026-08-18 13:47:00 +10:00
|
|
|
`Alt+Shift+T` launches a fresh terminal (`$TERMINAL`, default
|
2026-08-15 09:16:09 +10:00
|
|
|
`alacritty`) as a new tab placed immediately to the right of the current tab,
|
2026-08-18 11:20:29 +10:00
|
|
|
and it becomes the active tab once it maps (the one exception to the
|
|
|
|
|
no-map-steal rule at startup, along with sub-windows of the shown tab).
|
2026-08-15 09:16:09 +10:00
|
|
|
|
2026-08-09 13:02:46 +10:00
|
|
|
The tab-cycling hotkey only emits a physical press for the first tab switch;
|
2026-08-14 08:48:28 +10:00
|
|
|
repeats are synthesized, so holding `Alt+Up/Down` keeps stepping through
|
|
|
|
|
tabs while any other key stops it.
|
2026-08-09 13:02:46 +10:00
|
|
|
|
2026-08-17 10:45:21 +10:00
|
|
|
`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
|
Grab Alt+arrows and rotate tabs when reorder wraps past an end
A focused terminal acted on Alt+Shift+Up/Down (selection/typing) because
the reorder chord was forwarded like plain Alt+Up/Down cycling was. Both
are now grabbed: tabbed consumes them so the focused client never sees
them.
Reordering off an end previously swapped the moved tab with the far-end
tab, leaving the middle of the list in place ([A,B,C,D] moving D off the
end gave [D,B,C,A]). reorder_active_tab now shifts every slot between the
two positions, so the whole list rotates: [D,A,B,C]. Adjacent moves are
unchanged (rotation is a swap there).
The reorder test now runs three tabs to distinguish swap from rotation
([R,G,B] moving B off the end must land [B,R,G]) and keeps the
Ctrl+Shift+T anchor-slot check. The clipboard tests got more patience
(retry on stalled wl-paste; longer socket-start wait) since a stalled host
GNOME display was flaking them.
Full suite 5/5 green.
2026-08-17 11:12:28 +10:00
|
|
|
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
|
2026-08-18 13:47:00 +10:00
|
|
|
moving it (same synthesized repeat as cycling). An `Alt+Shift+T` inside the
|
Grab Alt+arrows and rotate tabs when reorder wraps past an end
A focused terminal acted on Alt+Shift+Up/Down (selection/typing) because
the reorder chord was forwarded like plain Alt+Up/Down cycling was. Both
are now grabbed: tabbed consumes them so the focused client never sees
them.
Reordering off an end previously swapped the moved tab with the far-end
tab, leaving the middle of the list in place ([A,B,C,D] moving D off the
end gave [D,B,C,A]). reorder_active_tab now shifts every slot between the
two positions, so the whole list rotates: [D,A,B,C]. Adjacent moves are
unchanged (rotation is a swap there).
The reorder test now runs three tabs to distinguish swap from rotation
([R,G,B] moving B off the end must land [B,R,G]) and keeps the
Ctrl+Shift+T anchor-slot check. The clipboard tests got more patience
(retry on stalled wl-paste; longer socket-start wait) since a stalled host
GNOME display was flaking them.
Full suite 5/5 green.
2026-08-17 11:12:28 +10:00
|
|
|
moved tab opens exactly where the reordered list says it should — immediately
|
|
|
|
|
right of the tab's new position.
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
Both `Alt+Up/Down` and `Alt+Shift+Up/Down` are grabbed: kick consumes them
|
Grab Alt+arrows and rotate tabs when reorder wraps past an end
A focused terminal acted on Alt+Shift+Up/Down (selection/typing) because
the reorder chord was forwarded like plain Alt+Up/Down cycling was. Both
are now grabbed: tabbed consumes them so the focused client never sees
them.
Reordering off an end previously swapped the moved tab with the far-end
tab, leaving the middle of the list in place ([A,B,C,D] moving D off the
end gave [D,B,C,A]). reorder_active_tab now shifts every slot between the
two positions, so the whole list rotates: [D,A,B,C]. Adjacent moves are
unchanged (rotation is a swap there).
The reorder test now runs three tabs to distinguish swap from rotation
([R,G,B] moving B off the end must land [B,R,G]) and keeps the
Ctrl+Shift+T anchor-slot check. The clipboard tests got more patience
(retry on stalled wl-paste; longer socket-start wait) since a stalled host
GNOME display was flaking them.
Full suite 5/5 green.
2026-08-17 11:12:28 +10:00
|
|
|
so the focused client never receives the chord (e.g. a terminal can't act on
|
|
|
|
|
`Shift`+arrow while you reorder).
|
2026-08-17 10:45:21 +10:00
|
|
|
|
2026-08-17 10:26:48 +10:00
|
|
|
`Alt+Delete` closes the active tab. The 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 tab that closes on its own (its
|
|
|
|
|
program exits) instead hands active off to the left neighbor.
|
|
|
|
|
|
2026-08-14 11:31:32 +10:00
|
|
|
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 mirrors between the host window and the clients:
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
- Fullscreen the kick window (use your host compositor's fullscreen
|
2026-08-14 11:31:32 +10:00
|
|
|
keybinding, or an app's own `F11`) and every tab is told it is fullscreen,
|
|
|
|
|
so clients drop their decorations and fill the screen.
|
|
|
|
|
- A client that requests fullscreen itself (e.g. a video player) also
|
2026-08-19 11:46:25 +10:00
|
|
|
fullscreens the kick host window.
|
2026-08-14 11:31:32 +10:00
|
|
|
- Leaving fullscreen returns every tab to maximized.
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
## Standalone mode (`KICK_MODE=standalone`)
|
2026-08-15 18:06:55 +10:00
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
By default kick runs **nested**: it opens a window on the host
|
2026-08-15 18:06:55 +10:00
|
|
|
Wayland compositor and draws every tab inside that single window. Set
|
2026-08-19 11:46:25 +10:00
|
|
|
`KICK_MODE=standalone` to instead become its own Wayland compositor,
|
2026-08-15 18:06:55 +10:00
|
|
|
rendering directly onto a native display (DRM via wlroots) — the first output
|
2026-08-19 11:46:25 +10:00
|
|
|
only; extra monitors are ignored. In this mode kick takes over the
|
2026-08-15 18:06:55 +10:00
|
|
|
screen, and the tab overlay is composited onto the output in a second render
|
|
|
|
|
pass instead of being drawn into a host window.
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
- `KICK_MODE=standalone WLR_BACKENDS=...` selects the wlroots backends
|
2026-08-15 18:06:55 +10:00
|
|
|
(wlroots' own `WLR_BACKENDS` variable is passed through). With no
|
|
|
|
|
`WLR_BACKENDS` it defaults to `drm,libinput`.
|
|
|
|
|
- `WAYLAND_DISPLAY` is ignored in standalone mode (it never connects to an
|
|
|
|
|
outer compositor), except as part of the requested `WLR_BACKENDS`.
|
2026-08-19 11:46:25 +10:00
|
|
|
- Real keyboard/pointer devices are attached to the kick seat directly
|
2026-08-18 13:47:00 +10:00
|
|
|
(native input); the `Alt+Shift+T` and `Alt`-overlay hotkeys still work off
|
2026-08-15 18:06:55 +10:00
|
|
|
those devices.
|
2026-08-16 07:39:37 +10:00
|
|
|
- `Ctrl-Alt-F1..F12` switch virtual terminals while running on the DRM
|
|
|
|
|
backend (via libseat/logind); the terminal sees nothing of the chord.
|
2026-08-15 18:06:55 +10:00
|
|
|
- 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.
|
2026-08-19 11:46:25 +10:00
|
|
|
- Headless testing: `KICK_MODE=standalone WLR_BACKENDS=headless` runs
|
2026-08-15 18:06:55 +10:00
|
|
|
standalone on wlroots' headless backend (no DRM needed), useful in CI or
|
|
|
|
|
when no TTY is available.
|
|
|
|
|
|
2026-08-19 10:27:43 +10:00
|
|
|
### Running the nested tests without a visible window
|
|
|
|
|
|
|
|
|
|
`./test.sh` runs the nested suite against the desktop, so each test briefly
|
|
|
|
|
opens the compositor's host window. `./test-headless.sh` re-hosts those
|
|
|
|
|
windows inside an invisible compositor: it boots a second, standalone
|
2026-08-19 11:46:25 +10:00
|
|
|
instance of the same binary (`KICK_MODE=standalone WLR_BACKENDS=headless`,
|
2026-08-19 10:27:43 +10:00
|
|
|
output shown nowhere) and points `WAYLAND_DISPLAY` at it for the pytest run,
|
|
|
|
|
so nothing flashes on the desktop. No extra dependencies.
|
|
|
|
|
|
|
|
|
|
The headless host exposes `wlr-data-control`, so the clipboard tests run
|
|
|
|
|
through the (preferred) data-control bridge instead of the focus-based one
|
|
|
|
|
that the desktop session uses. Standalone mode needs that data-control
|
|
|
|
|
`set_selection` request handled too, which is the case regardless of which
|
|
|
|
|
runner you use.
|
|
|
|
|
|
2026-08-20 17:28:59 +10:00
|
|
|
`test_nested_host_primary_to_kick` exercises the host->nested primary
|
|
|
|
|
selection path, which only exists over the data-control v2 bridge; it skips
|
|
|
|
|
on hosts without it (e.g. GNOME), so use `./test-headless.sh` or a
|
|
|
|
|
data-control host (labwc, sway) for it.
|
|
|
|
|
|
2026-08-21 07:44:00 +10:00
|
|
|
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`,
|
|
|
|
|
`./test-headless.sh`, or direct `pytest`. Run them on demand with
|
|
|
|
|
`./test.sh -m flaky`.
|
2026-08-20 18:00:48 +10:00
|
|
|
|
2026-08-19 15:08:31 +10:00
|
|
|
## 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).
|
|
|
|
|
- **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.
|
|
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
## Logging (`KICK_LOG`)
|
2026-08-09 13:02:46 +10:00
|
|
|
|
|
|
|
|
Quiet by default: a plain launch prints nothing to the launching terminal
|
2026-08-19 11:46:25 +10:00
|
|
|
except fatal errors. Set `KICK_LOG` before starting:
|
2026-08-09 13:02:46 +10:00
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
## Frame dumps (SIGUSR1 / KICK_AUTODUMP)
|
2026-08-09 13:02:46 +10:00
|
|
|
|
2026-08-19 11:46:25 +10:00
|
|
|
The `SIGUSR1` signal and `KICK_AUTODUMP` write raw `P6` portable-pixmap
|
2026-08-14 11:31:32 +10:00
|
|
|
files to `/tmp` regardless of the log level:
|
2026-08-09 13:02:46 +10:00
|
|
|
|
2026-08-14 11:31:32 +10:00
|
|
|
- `SIGUSR1` writes `/tmp/scene_N.ppm` — the composed scene (whatever
|
2026-08-19 11:46:25 +10:00
|
|
|
kick rendered for the host).
|
2026-08-15 18:06:55 +10:00
|
|
|
- `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).
|
2026-08-09 13:02:46 +10:00
|
|
|
- `N` is an incrementing per-run counter; a single `SIGUSR1` produces a
|
|
|
|
|
matching scene/host pair.
|
2026-08-19 11:46:25 +10:00
|
|
|
- With `KICK_AUTODUMP` (any value, e.g. `KICK_AUTODUMP=1`), a scene and
|
2026-08-09 13:02:46 +10:00
|
|
|
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.
|
2026-08-19 11:46:25 +10:00
|
|
|
- At `KICK_LOG=2` the diagnostic that detects the client's own shm buffer
|
2026-08-09 13:02:46 +10:00
|
|
|
additionally writes that buffer to `/tmp/client_N.ppm`.
|