Resolve cursor themes the way wlroots loads them (Inherits, left_ptr, ~/.icons)

The previous pre-check demanded a literal <icon-dir>/<theme>/cursors/default
file, which is stricter than the wlroots 0.19 loader: it loads every cursor
in cursors/, follows index.theme Inherits= chains, and falls back
default to left_ptr. On Debian/Pi 'default' is only an alias directory
whose index.theme Inherits= names the system-selected theme, so an
unconfigured run refused to start even with cursors installed.

Rewrite resolution to mirror the loader: build the exact search path
(XCURSOR_PATH, else ~/.local/share/icons + ~/.icons + the compiled-in
dirs), accept either a default or left_ptr sprite, follow Inherits=
recursively (cycle-bounded). If the requested theme (or the 'default'
alias) still doesn't resolve, auto-pick the first installed theme that
provides a sprite and log the choice; only hard-fail when no cursor theme
exists anywhere, with a message that names the searched dirs and the
Debian update-alternatives hint.

Tests: default->Inherits alias boots, left_ptr-only theme boots, auto-pick
logs its choice, and the missing-theme case still exits rc 1.
This commit is contained in:
Andrew Hamilton 2026-08-16 12:45:42 +10:00
parent 9db00591ca
commit 53dd196602
2 changed files with 311 additions and 51 deletions

View file

@ -27,6 +27,7 @@
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <dirent.h>
#include <drm_fourcc.h>
@ -4489,52 +4490,155 @@ static bool backends_include(const char *env, const char *name) {
return false;
}
/* True if `theme` resolves to a real cursors file on disk. Mirrors the
loader's search: XCURSOR_PATH verbatim, else $XDG_DATA_HOME/icons (or
~/.local/share/icons) plus the compiled-in paths, each expected to contain
<dir>/<theme>/cursors/. wlroots 0.19 silently substitutes a builtin cursor
when none is found, so a missing theme must be detected here for the
hard-fail path. */
static bool standalone_cursor_theme_resolvable(const char *theme) {
/* Cursor theme resolution. wlroots 0.19 loads every file in
<path>/<theme>/cursors/, then follows Inherits= in index.theme to the
inherited themes, and finally substitutes a builtin arrow if nothing was
found -- so a missing theme can't be detected from the manager afterwards.
These helpers mirror the loader's search (cursor paths and semantics) and
resolve only against real installed sprite files, so the caller can still
hard-fail instead of silently presenting an invisible pointer. */
#define CURSOR_SEARCH_DEFAULT \
"~/.icons:/usr/share/icons:/usr/share/pixmaps:" \
"~/.cursors:/usr/share/cursors/xorg-x11:/usr/X11R6/lib/X11/icons"
/* ':'-joined search path, exactly like wlroots' xcursor_library_path(). */
static char *cursor_search_path(void) {
const char *env = getenv("XCURSOR_PATH");
char *path = NULL;
if (env && *env) {
path = strdup(env);
} else {
const char *xdg = getenv("XDG_DATA_HOME");
const char *udata = (xdg && xdg[0] == '/') ? xdg : "~/.local/share";
size_t n = strlen(udata) + strlen(
"/icons:/usr/share/icons:/usr/share/pixmaps:"
"~/.cursors:/usr/share/cursors/xorg-x11:"
"/usr/X11R6/lib/X11/icons") + 1;
path = malloc(n);
if (path)
snprintf(path, n, "%s%s", udata,
"/icons:/usr/share/icons:/usr/share/pixmaps:"
"~/.cursors:/usr/share/cursors/xorg-x11:"
"/usr/X11R6/lib/X11/icons");
if (env && *env)
return strdup(env);
const char *xdg = getenv("XDG_DATA_HOME");
const char *udata = (xdg && xdg[0] == '/') ? xdg : "~/.local/share";
size_t n = strlen(udata) + sizeof(CURSOR_SEARCH_DEFAULT) + 8;
char *path = malloc(n);
if (path)
snprintf(path, n, "%s/icons:%s", udata, CURSOR_SEARCH_DEFAULT);
return path;
}
/* Resolve a "~" prefix in `dir` into $HOME, writing into `out`. */
static int cursor_expand_dir(const char *dir, char *out, size_t outsz) {
if (dir[0] == '~') {
const char *home = getenv("HOME");
return home ? snprintf(out, outsz, "%s%s", home, dir + 1) : -1;
}
if (!path)
return snprintf(out, outsz, "%s", dir);
}
/* Does `theme` ship a default pointer file under dir? wlroots' get_cursor
falls back from "default" to "left_ptr" (wlr_xcursor.c), so either sprite
satisfies the check. */
static bool cursor_theme_has_sprite(const char *dir, const char *theme) {
char p[1024];
int n = snprintf(p, sizeof(p), "%s/%s/cursors/default", dir, theme);
if (n > 0 && n < (int)sizeof(p) && access(p, F_OK) == 0)
return true;
n = snprintf(p, sizeof(p), "%s/%s/cursors/left_ptr", dir, theme);
if (n > 0 && n < (int)sizeof(p) && access(p, F_OK) == 0)
return true;
return false;
}
/* True when an installed cursor sprite is reachable from `theme`, walking the
search path and the theme's index.theme Inherits= chain like the loader.
`depth` bounds the recursion so inherits cycles can't loop forever. */
static bool cursor_theme_resolves(const char *path, const char *theme,
int depth) {
if (depth > 8)
return false;
bool found = false;
char candid[1024];
for (char *dir = strtok(path, ":"); dir; dir = strtok(NULL, ":")) {
if (dir[0] == '~') {
const char *home = getenv("HOME");
if (!home) continue;
snprintf(candid, sizeof(candid), "%s%s", home, dir + 1);
dir = candid;
}
size_t n = snprintf(candid, sizeof(candid), "%s/%s/cursors/default",
dir, theme);
if (n < sizeof(candid) && access(candid, F_OK) == 0) {
found = true;
break;
char buffer[1024];
char p[1024];
for (const char *s = path; s && *s; ) {
const char *colon = strchr(s, ':');
size_t len = colon ? (size_t)(colon - s) : strlen(s);
if (len && len < sizeof(buffer)) {
char dir[1024];
snprintf(dir, sizeof(dir), "%.*s", (int)len, s);
if (cursor_expand_dir(dir, buffer, sizeof(buffer)) < 0)
goto next;
if (cursor_theme_has_sprite(buffer, theme))
return true;
int n = snprintf(p, sizeof(p), "%s/%s/index.theme", buffer, theme);
if (n > 0 && n < (int)sizeof(p)) {
FILE *f = fopen(p, "r");
if (f) {
char line[512];
while (fgets(line, sizeof(line), f)) {
const char *l = line;
if (strncmp(l, "Inherits", 8))
continue;
l += 8;
while (*l == ' ')
l++;
if (*l != '=')
continue;
l++;
while (*l == ' ')
l++;
/* one or more names split by whitespace , ; */
for (;;) {
while (*l == ' ' || *l == ',' || *l == ';')
l++;
if (!*l || *l == '\n')
break;
char name[128];
int k = 0;
while (*l && *l != ' ' && *l != ',' && *l != ';'
&& *l != '\n' && k < (int)sizeof(name) - 1)
name[k++] = *l++;
name[k] = '\0';
if (cursor_theme_resolves(path, name, depth + 1)) {
fclose(f);
return true;
}
}
}
fclose(f);
}
}
}
next:
s = colon ? colon + 1 : NULL;
}
free(path);
return found;
return false;
}
/* First (sorted) installed theme with a default/left_ptr sprite, in search
path order; returns 1 and fills `out` when one exists. */
static bool cursor_autopick(const char *path, char *out, size_t outsz) {
char buffer[1024];
for (const char *s = path; s && *s; ) {
const char *colon = strchr(s, ':');
size_t len = colon ? (size_t)(colon - s) : strlen(s);
if (len && len < sizeof(buffer)) {
char dir[1024];
snprintf(dir, sizeof(dir), "%.*s", (int)len, s);
if (cursor_expand_dir(dir, buffer, sizeof(buffer)) >= 0) {
struct dirent **list = NULL;
int n = scandir(buffer, &list, NULL, alphasort);
for (int i = 0; i < n; i++) {
const char *name = list[i]->d_name;
if (name[0] == '.' || name[0] == '\0')
continue;
if (cursor_theme_has_sprite(buffer, name)) {
snprintf(out, outsz, "%s", name);
for (int j = 0; j < n; j++)
free(list[j]);
free(list);
return true;
}
}
for (int j = 0; j < n; j++)
free(list[j]);
free(list);
}
}
s = colon ? colon + 1 : NULL;
}
return false;
}
/* ===================== Main ===================== */
@ -4744,7 +4848,25 @@ int main(int argc, char *argv[]) {
wlr_cursor_attach_output_layout(server.cursor, server.output_layout);
const char *theme_env = getenv("XCURSOR_THEME");
const char *theme = theme_env && *theme_env ? theme_env : "default";
/* The loader reads the theme dir via its first ':'-component, so
truncate "A:B" fallback lists to "A" to match where sprites would
actually live. */
char theme_name[128];
{
const char *raw = theme_env && *theme_env ? theme_env : "default";
size_t tl = strlen(raw);
const char *comma = strchr(raw, ':');
if (comma)
tl = (size_t)(comma - raw);
if (tl >= sizeof(theme_name))
tl = sizeof(theme_name) - 1;
memcpy(theme_name, raw, tl);
theme_name[tl] = '\0';
}
const char *theme = theme_name;
if (!theme[0])
theme = "default";
uint32_t size = 24;
const char *size_env = getenv("XCURSOR_SIZE");
if (size_env) {
@ -4753,18 +4875,40 @@ int main(int argc, char *argv[]) {
if (end != size_env && v > 0)
size = (uint32_t)v;
}
/* wlroots 0.19 silently substitutes a builtin cursor when the theme
directory holds nothing, so a missing theme can't be detected from
the manager afterwards. Check the theme dirs ourselves, following
the same search path the loader uses (XCURSOR_PATH, else the
compiled default), and require a real 'default' sprite. */
if (!standalone_cursor_theme_resolvable(theme)) {
fprintf(stderr,
"tabbed-wlroots: no cursor theme '%s' is installed (need a "
"'default' sprite in <icon-dir>/%s/cursors). Install a cursor "
"theme or set XCURSOR_THEME/XCURSOR_PATH\n", theme, theme);
/* wlroots would silently substitute a builtin cursor, so resolve the
theme ourselves (real sprites only, Inherits chains included) and
refuse to run when none exists anywhere. Themes named "default"
resolve through Debian's /usr/share/icons/default index.theme
(-> /etc/alternatives/x-cursor-theme); if even that is absent, pick
the first theme actually installed instead of failing on a naming
mismatch. */
char *search = cursor_search_path();
if (!search) {
fprintf(stderr, "tabbed-wlroots: out of memory resolving cursor "
"theme\n");
return 1;
}
char picked[128];
if (!cursor_theme_resolves(search, theme, 0)) {
if (cursor_autopick(search, picked, sizeof(picked))) {
tabbed_log(TABBED_LOG_INFO,
"[cursor] theme '%s' has no default sprite; picked "
"installed theme '%s'\n", theme, picked);
theme = picked;
} else {
fprintf(stderr,
"tabbed-wlroots: no cursor theme is installed "
"('%s' does not resolve; searched %s). Install a cursor "
"theme or set XCURSOR_THEME/XCURSOR_PATH; on Debian also "
"check `update-alternatives --config x-cursor-theme`\n",
theme, search);
free(search);
return 1;
}
}
free(search);
server.xcursor_mgr = wlr_xcursor_manager_create(theme, size);
if (server.xcursor_mgr)
wlr_xcursor_manager_load(server.xcursor_mgr, 1);