feat: add --cursor-size flag for standalone pointer size

New --cursor-size N (8..128px, both --cursor-size N and --cursor-size=N forms) sizing the standalone wlr_cursor. Precedence: CLI flag over $XCURSOR_SIZE over the 24px default. Out-of-range, missing or non-numeric values are startup errors (rc 2) naming the flag, matching the existing --start-cmd-index/--tab-groups handling. Nested mode is unaffected (the host draws the pointer there); the flag is silently ignored. Help text, README usage + cursor-shapes section, and a test_cursor_size_flag_errors regression test included. Verified: debug + release builds warning-free; cursor_size + tab-groups flag-error tests pass; --cursor-size 32 boots headless, --cursor-size 999 exits rc 2.
This commit is contained in:
Andrew Hamilton 2026-10-01 09:41:36 +10:00
parent 1c1bc5a230
commit b04f6a2826
3 changed files with 83 additions and 4 deletions

View file

@ -75,7 +75,7 @@ install works too.
## Usage
```
kick [--start-cmd-index N] [--focus-priority] [prog...]
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.
@ -428,10 +428,12 @@ 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).
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.
(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`),

49
kick.c
View file

@ -928,6 +928,11 @@ static bool opt_focus_priority = false;
text row. Off by default. */
static bool opt_overlay_previews = false;
/* --cursor-size N: standalone pointer size in px (8..128). -1 means unset:
fall back to $XCURSOR_SIZE, then the 24px default. Nested mode has no
cursor of its own (the host draws the pointer), so it is ignored there. */
static int opt_cursor_size = -1;
/* ===================== Clipboard bridge ===================== */
enum bridge_backend {
@ -13901,6 +13906,13 @@ int main(int argc, char *argv[]) {
"that starts\n"
" focused; without it the "
"last command does.\n"
" --cursor-size N Standalone pointer size "
"in px (8..128;\n"
" default 24, or "
"$XCURSOR_SIZE when set).\n"
" Ignored when nested: the "
"host draws the\n"
" pointer there.\n"
" --tab-groups on|off How a client's related "
"windows group with their tab:\n"
" 'on' (default) stacks them "
@ -14101,6 +14113,37 @@ int main(int argc, char *argv[]) {
}
start_cmd_index = (int)idx - 1;
continue;
} else if (strcmp(argv[i], "--cursor-size") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "kick: --cursor-size needs a "
"value, e.g. --cursor-size 32\n");
free(appv);
return 2;
}
const char *n = argv[++i];
char *end = NULL;
long sz = strtol(n, &end, 10);
if (end == n || *end != '\0' || sz < 8 || sz > 128) {
fprintf(stderr, "kick: bad --cursor-size '%s' "
"(use 8..128, e.g. --cursor-size 32)\n", n);
free(appv);
return 2;
}
opt_cursor_size = (int)sz;
continue;
} else if (strncmp(argv[i], "--cursor-size=",
sizeof("--cursor-size=") - 1) == 0) {
const char *n = argv[i] + sizeof("--cursor-size=") - 1;
char *end = NULL;
long sz = strtol(n, &end, 10);
if (end == n || *end != '\0' || sz < 8 || sz > 128) {
fprintf(stderr, "kick: bad --cursor-size '%s' "
"(use 8..128, e.g. --cursor-size 32)\n", n);
free(appv);
return 2;
}
opt_cursor_size = (int)sz;
continue;
}
if (val) {
if (!parse_screen_mode(val)) {
@ -14734,7 +14777,8 @@ int main(int argc, char *argv[]) {
/* Standalone has no host compositor to draw a pointer, so give the
output a real wlr_cursor. Theme/size follow the XDG cursor convention:
XCURSOR_THEME (default "default") and XCURSOR_SIZE (default 24px).
XCURSOR_THEME (default "default") and XCURSOR_SIZE (default 24px),
with --cursor-size overriding the size when given.
wlroots resolves the theme itself (search path, Inherits chains) and,
when nothing is installed, substitutes a builtin arrow -- so the pointer
is never invisible. We only report when the requested theme was
@ -14758,6 +14802,9 @@ int main(int argc, char *argv[]) {
if (end != size_env && v > 0)
size = (uint32_t)v;
}
/* Explicit --cursor-size wins over the environment. */
if (opt_cursor_size > 0)
size = (uint32_t)opt_cursor_size;
server.xcursor_mgr = wlr_xcursor_manager_create(theme, size);
if (server.xcursor_mgr)

View file

@ -4229,6 +4229,36 @@ def test_sub_windows_flag_errors():
f"error should mention --tab-groups:\n{out}"
def test_cursor_size_flag_errors():
"""A missing or bad --cursor-size value is a startup error (rc 2) naming
the flag, before any window or backend work."""
env = dict(os.environ)
cases = (["--cursor-size"],
["--cursor-size", "0"],
["--cursor-size", "7"],
["--cursor-size", "129"],
["--cursor-size", "big"],
["--cursor-size=foo"])
for case in cases:
proc = subprocess.Popen(
["env", "-i",
"PATH=" + env["PATH"],
"HOME=" + env.get("HOME", "/"),
"XDG_RUNTIME_DIR=" + env.get("XDG_RUNTIME_DIR", "/tmp"),
"KICK_MODE=standalone",
"WLR_BACKENDS=headless",
"stdbuf", "-oL",
BIN] + case,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()),
)
out, _ = proc.communicate(timeout=15)
assert proc.returncode == 2, \
f"--cursor-size {case} should exit rc 2; rc={proc.returncode}\n{out}"
assert b"--cursor-size" in out, \
f"error should mention --cursor-size:\n{out}"
@pytest.mark.flaky
def test_dead_command_explains_and_exits():
"""A sole command that exits before opening a window is dropped with a