feat: per-compositor app config in kick, slim kick-pi
This commit is contained in:
parent
61f9227683
commit
240c1cea4f
4 changed files with 342 additions and 124 deletions
10
README.md
10
README.md
|
|
@ -346,6 +346,16 @@ Wayland compositor and draws every tab inside that single window.
|
||||||
standalone on wlroots' headless backend (no DRM needed), useful in CI or
|
standalone on wlroots' headless backend (no DRM needed), useful in CI or
|
||||||
when no TTY is available.
|
when no TTY is available.
|
||||||
|
|
||||||
|
### Per-compositor app config
|
||||||
|
|
||||||
|
Clients see a scoped view of `~/.config`: at startup kick builds a shadow
|
||||||
|
config home (symlinked passthrough, so nothing diverges) with only GTK
|
||||||
|
window controls pinned to close-only, plus a `DCONF_PROFILE` overlay
|
||||||
|
pinning the same key (dconf wins over settings files). Kick never
|
||||||
|
minimizes for anyone, so client-drawn minimize/maximize buttons would be
|
||||||
|
dead -- other compositors sharing the home directory are unaffected.
|
||||||
|
Set `KICK_NO_CONFIG_SHADOW=1` to disable the scoped view entirely.
|
||||||
|
|
||||||
`test_nested_host_primary_to_kick` exercises the host->nested primary
|
`test_nested_host_primary_to_kick` exercises the host->nested primary
|
||||||
selection path, which only exists over the ext-data-control bridge; it skips
|
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,
|
on hosts without it (e.g. GNOME), so use an ext-data-control host (labwc,
|
||||||
|
|
|
||||||
127
kick-pi
127
kick-pi
|
|
@ -2,12 +2,16 @@
|
||||||
# kick-pi -- Raspberry Pi session entry for kick.
|
# kick-pi -- Raspberry Pi session entry for kick.
|
||||||
#
|
#
|
||||||
# A labwc-pi analogue for running kick as the Pi's compositor instead of
|
# A labwc-pi analogue for running kick as the Pi's compositor instead of
|
||||||
# labwc: sets up the Pi OS session context, scopes GTK window controls to
|
# labwc: sets up the Pi OS session context, then execs kick, passing all
|
||||||
# close-only for kick sessions (shadow XDG_CONFIG_HOME plus a DCONF_PROFILE
|
# arguments through untouched (screen mode, --panel, tabs, ...).
|
||||||
# overlay database -- see kick_shadow_config below), then execs kick,
|
|
||||||
# passing all arguments through untouched (screen mode, --panel, tabs, ...).
|
|
||||||
# Environment (KICK_MODE, KICK_LOG, ...) passes through equally untouched.
|
# Environment (KICK_MODE, KICK_LOG, ...) passes through equally untouched.
|
||||||
#
|
#
|
||||||
|
# Per-compositor GTK window controls (close-only under kick, full under
|
||||||
|
# labwc) live in kick itself now: it builds a shadow config home plus a
|
||||||
|
# dconf overlay for its own children, so this wrapper carries none of that
|
||||||
|
# machinery. (A wrapper-level copy would only duplicate it; version mixes
|
||||||
|
# still behave: new wrapper + old kick simply scopes nothing.)
|
||||||
|
#
|
||||||
# Deliberately NOT done here (kept minimal, by choice):
|
# Deliberately NOT done here (kept minimal, by choice):
|
||||||
# - logging (see kick-session for per-boot logs),
|
# - logging (see kick-session for per-boot logs),
|
||||||
# - labwc autostart files (kick-only: panels and apps come from kick's
|
# - labwc autostart files (kick-only: panels and apps come from kick's
|
||||||
|
|
@ -43,121 +47,6 @@ else
|
||||||
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
|
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Per-compositor GTK window controls without touching shared state.
|
|
||||||
# Wayland leaves decoration policy to clients with no per-compositor
|
|
||||||
# channel, and kick never minimizes/maximizes for anyone, so client-drawn
|
|
||||||
# minimize/maximize buttons are dead under kick yet live under labwc --
|
|
||||||
# while both sessions share one $HOME. Fix it with a shadow config home:
|
|
||||||
# every entry of ~/.config is symlinked in (writes pass through to the
|
|
||||||
# real files, nothing diverges), except gtk-3.0/settings.ini and
|
|
||||||
# gtk-4.0/settings.ini, which are regenerated from the real ones with only
|
|
||||||
# gtk-decoration-layout forced to close-only. labwc sessions keep seeing
|
|
||||||
# the real ~/.config (full buttons); kick sessions see the shadow.
|
|
||||||
# Crash-safe (rebuilt every login, never authoritative) and idempotent.
|
|
||||||
kick_pin_gtk_layout() {
|
|
||||||
# $1 = settings.ini path: force gtk-decoration-layout=:close under
|
|
||||||
# [Settings], creating file/section as needed, keeping everything else.
|
|
||||||
[ -f "$1" ] || : > "$1" 2>/dev/null || return 0
|
|
||||||
tmp="$1.kick-pin-tmp"
|
|
||||||
awk '
|
|
||||||
BEGIN { in_s = 0; seen_s = 0; done = 0 }
|
|
||||||
/^\[[^]]*\]/ {
|
|
||||||
seen_s = seen_s || ($0 == "[Settings]")
|
|
||||||
in_s = ($0 == "[Settings]")
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
in_s && !done && /^[[:space:]]*gtk-decoration-layout[[:space:]]*=/ {
|
|
||||||
print "gtk-decoration-layout=:close"
|
|
||||||
done = 1
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
if (!seen_s) print "[Settings]"
|
|
||||||
print "gtk-decoration-layout=:close"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$1" > "$tmp" 2>/dev/null && mv -f "$tmp" "$1" 2>/dev/null
|
|
||||||
rm -f "$tmp" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Per-compositor GTK window controls, part two: dconf.
|
|
||||||
# GtkSettings binds gtk-decoration-layout to org.gnome.desktop.wm.preferences
|
|
||||||
# button-layout, and the dconf binding wins over settings.ini -- so the
|
|
||||||
# shadow settings.ini alone cannot hide the buttons while the database
|
|
||||||
# carries the schema default (appmenu:minimize,maximize,close). Layer a
|
|
||||||
# kick-only user database in front via DCONF_PROFILE: reads fall through
|
|
||||||
# to the real databases unchanged, writes land in the overlay file, and
|
|
||||||
# labwc sessions (no DCONF_PROFILE) never see it. Tradeoff, stated plainly:
|
|
||||||
# settings changed under kick are written to the overlay database and are
|
|
||||||
# invisible under labwc; everything merely read behaves identically.
|
|
||||||
# $1 = shadow config dir. Skipped without the dconf CLI or a session bus.
|
|
||||||
kick_dconf_overlay() {
|
|
||||||
shadow_dir=$1
|
|
||||||
command -v dconf >/dev/null 2>&1 || return 0
|
|
||||||
[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ] || return 0
|
|
||||||
profile="$shadow_dir/dconf-profile"
|
|
||||||
# NOTE: the database name must be a valid D-Bus object path element
|
|
||||||
# (alphanumerics only): dconf maps it to /ca/desrt/dconf/Writer/<name>
|
|
||||||
# and aborts on anything else -- so no hyphens here, ever.
|
|
||||||
{
|
|
||||||
echo "user-db:kickoverrides"
|
|
||||||
if [ -f /etc/dconf/profile/user ]; then
|
|
||||||
grep -v "^user-db:" /etc/dconf/profile/user 2>/dev/null || true
|
|
||||||
grep "^user-db:" /etc/dconf/profile/user 2>/dev/null || true
|
|
||||||
else
|
|
||||||
printf 'user-db:user\nsystem-db:local\nsystem-db:site\nsystem-db:distro\n'
|
|
||||||
fi
|
|
||||||
} > "$profile.kick-new" 2>/dev/null || return 0
|
|
||||||
mv -f "$profile.kick-new" "$profile" 2>/dev/null || return 0
|
|
||||||
export DCONF_PROFILE="$profile"
|
|
||||||
# Seed the pinned layout (idempotent; lands in kickoverrides, the
|
|
||||||
# first user database, leaving the real databases untouched).
|
|
||||||
dconf write /org/gnome/desktop/wm/preferences/button-layout \
|
|
||||||
"':close'" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
kick_shadow_config() {
|
|
||||||
[ -n "${HOME:-}" ] || return 0
|
|
||||||
real_config="$HOME/.config"
|
|
||||||
shadow="${XDG_CACHE_HOME:-$HOME/.cache}/kick/config"
|
|
||||||
mkdir -p "$shadow" 2>/dev/null || return 0
|
|
||||||
if [ -d "$real_config" ]; then
|
|
||||||
for entry in "$real_config"/* "$real_config"/.*; do
|
|
||||||
case "$entry" in
|
|
||||||
"$real_config/."|"$real_config/..") continue ;;
|
|
||||||
esac
|
|
||||||
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
||||||
base=${entry##*/}
|
|
||||||
[ -e "$shadow/$base" ] || [ -L "$shadow/$base" ] || \
|
|
||||||
ln -s "$entry" "$shadow/$base" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
# Shadow copies (real directories, never symlinks): refreshed from the
|
|
||||||
# real files on every login so theme/icons/fonts track labwc-side edits,
|
|
||||||
# then pinned -- only the layout line ever diverges. When no real file
|
|
||||||
# exists the shadow copy is left alone.
|
|
||||||
for gtkdir in gtk-3.0 gtk-4.0; do
|
|
||||||
if [ -L "$shadow/$gtkdir" ]; then
|
|
||||||
rm -f "$shadow/$gtkdir" 2>/dev/null || continue
|
|
||||||
fi
|
|
||||||
mkdir -p "$shadow/$gtkdir" 2>/dev/null || continue
|
|
||||||
if [ -f "$real_config/$gtkdir/settings.ini" ]; then
|
|
||||||
cp -f "$real_config/$gtkdir/settings.ini" \
|
|
||||||
"$shadow/$gtkdir/settings.ini" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
kick_pin_gtk_layout "$shadow/$gtkdir/settings.ini"
|
|
||||||
done
|
|
||||||
# Export last so earlier failures keep the real config rather than a
|
|
||||||
# half-built shadow.
|
|
||||||
export XDG_CONFIG_HOME="$shadow"
|
|
||||||
kick_dconf_overlay "$shadow"
|
|
||||||
}
|
|
||||||
kick_shadow_config
|
|
||||||
unset -f kick_shadow_config kick_pin_gtk_layout kick_dconf_overlay 2>/dev/null || true
|
|
||||||
|
|
||||||
# Software rendering on Pi models whose GPU lacks an MMU (same guard as
|
# Software rendering on Pi models whose GPU lacks an MMU (same guard as
|
||||||
# labwc-pi; kick's wlroots renderer autocreate honours WLR_RENDERER).
|
# labwc-pi; kick's wlroots renderer autocreate honours WLR_RENDERER).
|
||||||
if command -v raspi-config >/dev/null 2>&1; then
|
if command -v raspi-config >/dev/null 2>&1; then
|
||||||
|
|
|
||||||
268
kick.c
268
kick.c
|
|
@ -41,6 +41,8 @@ const char *__ubsan_default_options(void) {
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#include <drm_fourcc.h>
|
#include <drm_fourcc.h>
|
||||||
|
|
||||||
|
|
@ -11563,6 +11565,21 @@ frame_done:
|
||||||
|
|
||||||
/* ===================== Launch client ===================== */
|
/* ===================== Launch client ===================== */
|
||||||
|
|
||||||
|
/* Per-compositor session config shadowing (the kick-pi mechanism, owned
|
||||||
|
here so plain `kick` behaves the same everywhere): a shadow config home
|
||||||
|
where every ~/.config entry is symlinked in (writes pass through),
|
||||||
|
except gtk-3.0/gtk-4.0 settings.ini, regenerated with only
|
||||||
|
gtk-decoration-layout forced to close-only, plus a DCONF_PROFILE overlay
|
||||||
|
database pinning the same key (dconf wins over settings.ini). Labwc
|
||||||
|
sessions never see it; nested kick children do (per-compositor scoping).
|
||||||
|
Best-effort throughout: anything failing leaves config_shadow_ok false
|
||||||
|
and children inherit the plain environment. */
|
||||||
|
static char shadow_config[1024];
|
||||||
|
static char shadow_dconf_profile[1024];
|
||||||
|
static bool config_shadow_ok;
|
||||||
|
static void config_shadow_build(void);
|
||||||
|
static void config_seed_dconf(void);
|
||||||
|
|
||||||
/* D-Bus/systemd activation environment, mirroring what labwc does on
|
/* D-Bus/systemd activation environment, mirroring what labwc does on
|
||||||
launch (DRM backend): D-Bus-activated services and systemd user units
|
launch (DRM backend): D-Bus-activated services and systemd user units
|
||||||
otherwise keep whatever WAYLAND_DISPLAY etc. they were started with --
|
otherwise keep whatever WAYLAND_DISPLAY etc. they were started with --
|
||||||
|
|
@ -11585,8 +11602,11 @@ static void activation_run(bool clear) {
|
||||||
/* WAYLAND_DISPLAY is always exported (our socket, set below); the rest
|
/* WAYLAND_DISPLAY is always exported (our socket, set below); the rest
|
||||||
only when present in our own environment, so absent values are never
|
only when present in our own environment, so absent values are never
|
||||||
pushed as empty over a live session's settings -- except on clear,
|
pushed as empty over a live session's settings -- except on clear,
|
||||||
where empty is exactly labwc's documented way of blanking them. */
|
where empty is exactly labwc's documented way of blanking them. The
|
||||||
const char *args[2 + 7 + 1];
|
shadow config paths ride along the same way when shadowing is up, so
|
||||||
|
D-Bus-activated apps see the same per-compositor view as our own
|
||||||
|
children (standalone only, like the rest of this call). */
|
||||||
|
const char *args[2 + 9 + 1];
|
||||||
int argc = 0;
|
int argc = 0;
|
||||||
args[argc++] = "dbus-update-activation-environment";
|
args[argc++] = "dbus-update-activation-environment";
|
||||||
args[argc++] = "--systemd";
|
args[argc++] = "--systemd";
|
||||||
|
|
@ -11610,6 +11630,15 @@ static void activation_run(bool clear) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
char shadow_values[2][1100];
|
||||||
|
if (config_shadow_ok) {
|
||||||
|
snprintf(shadow_values[0], sizeof(shadow_values[0]),
|
||||||
|
"XDG_CONFIG_HOME=%s", clear ? "" : shadow_config);
|
||||||
|
args[argc++] = shadow_values[0];
|
||||||
|
snprintf(shadow_values[1], sizeof(shadow_values[1]),
|
||||||
|
"DCONF_PROFILE=%s", clear ? "" : shadow_dconf_profile);
|
||||||
|
args[argc++] = shadow_values[1];
|
||||||
|
}
|
||||||
args[argc] = NULL;
|
args[argc] = NULL;
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
|
|
@ -11651,6 +11680,225 @@ static void activation_update(void) {
|
||||||
atexit(activation_clear_env);
|
atexit(activation_clear_env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* mkdir -p for the shadow tree (best-effort; failure just skips shadowing). */
|
||||||
|
static void shadow_mkdir_p(const char *path) {
|
||||||
|
char tmp[1024];
|
||||||
|
if (snprintf(tmp, sizeof(tmp), "%s", path) >= (int)sizeof(tmp))
|
||||||
|
return;
|
||||||
|
for (char *p = tmp + 1; *p; p++) {
|
||||||
|
if (*p == '/') {
|
||||||
|
*p = '\0';
|
||||||
|
mkdir(tmp, 0755);
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mkdir(tmp, 0755);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force gtk-decoration-layout=:close under [Settings], creating
|
||||||
|
file/section as needed and keeping every other line byte-identical. */
|
||||||
|
static void shadow_pin_gtk_layout(const char *path) {
|
||||||
|
static const char *const key = "gtk-decoration-layout=:close";
|
||||||
|
char tmp[1024 + 32];
|
||||||
|
if (snprintf(tmp, sizeof(tmp), "%s.kick-pin-%d-tmp", path,
|
||||||
|
(int)getpid()) >= (int)sizeof(tmp))
|
||||||
|
return;
|
||||||
|
FILE *in = fopen(path, "r");
|
||||||
|
FILE *out = fopen(tmp, "w");
|
||||||
|
if (!out) {
|
||||||
|
if (in) fclose(in);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool in_settings = false, seen_settings = false, done = false;
|
||||||
|
char *line = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
ssize_t n;
|
||||||
|
while (in && (n = getline(&line, &cap, in)) >= 0) {
|
||||||
|
if (line[0] == '[') {
|
||||||
|
size_t m = 0;
|
||||||
|
while (line[m] && line[m] != ']' && line[m] != '\n') m++;
|
||||||
|
in_settings = (m == 10 && memcmp(line, "[Settings]", 10) == 0);
|
||||||
|
seen_settings = seen_settings || in_settings;
|
||||||
|
fwrite(line, 1, (size_t)n, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_settings && !done) {
|
||||||
|
size_t k = 0;
|
||||||
|
while (line[k] == ' ' || line[k] == '\t') k++;
|
||||||
|
size_t kl = strlen("gtk-decoration-layout");
|
||||||
|
if (strncmp(line + k, "gtk-decoration-layout", kl) == 0) {
|
||||||
|
size_t j = k + kl;
|
||||||
|
while (line[j] == ' ' || line[j] == '\t') j++;
|
||||||
|
if (line[j] == '=') {
|
||||||
|
fprintf(out, "%s\n", key);
|
||||||
|
done = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fwrite(line, 1, (size_t)n, out);
|
||||||
|
}
|
||||||
|
free(line);
|
||||||
|
if (in) fclose(in);
|
||||||
|
if (!done) {
|
||||||
|
if (!seen_settings)
|
||||||
|
fputs("[Settings]\n", out);
|
||||||
|
fprintf(out, "%s\n", key);
|
||||||
|
}
|
||||||
|
fclose(out);
|
||||||
|
rename(tmp, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy a regular file byte for byte; missing source is fine (skip). */
|
||||||
|
static void shadow_copy_file(const char *src, const char *dst) {
|
||||||
|
FILE *in = fopen(src, "rb");
|
||||||
|
if (!in) return;
|
||||||
|
FILE *out = fopen(dst, "wb");
|
||||||
|
if (!out) {
|
||||||
|
fclose(in);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[8192];
|
||||||
|
size_t n;
|
||||||
|
while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
|
||||||
|
fwrite(buf, 1, n, out);
|
||||||
|
fclose(in);
|
||||||
|
fclose(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write the DCONF_PROFILE overlay: our kick-only user database first, then
|
||||||
|
the system profile verbatim (user databases last, like the wrapper did).
|
||||||
|
dconf database names must stay alphanumeric: dconf maps the name into a
|
||||||
|
D-Bus object path and aborts on anything else, so no hyphens, ever. */
|
||||||
|
static void shadow_write_profile(const char *path) {
|
||||||
|
/* Atomic via temp + rename: parallel sessions share one shadow dir. */
|
||||||
|
char tmp[1024 + 32];
|
||||||
|
if (snprintf(tmp, sizeof(tmp), "%s.kick-prof-%d-tmp", path,
|
||||||
|
(int)getpid()) >= (int)sizeof(tmp))
|
||||||
|
return;
|
||||||
|
FILE *out = fopen(tmp, "w");
|
||||||
|
if (!out) return;
|
||||||
|
fputs("user-db:kickoverrides\n", out);
|
||||||
|
FILE *sys = fopen("/etc/dconf/profile/user", "r");
|
||||||
|
if (sys) {
|
||||||
|
char *line = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
ssize_t n;
|
||||||
|
/* Non-user lines first, user lines last (stable stacking order). */
|
||||||
|
while ((n = getline(&line, &cap, sys)) >= 0)
|
||||||
|
if (strncmp(line, "user-db:", 8) != 0)
|
||||||
|
fwrite(line, 1, (size_t)n, out);
|
||||||
|
rewind(sys);
|
||||||
|
while ((n = getline(&line, &cap, sys)) >= 0)
|
||||||
|
if (strncmp(line, "user-db:", 8) == 0)
|
||||||
|
fwrite(line, 1, (size_t)n, out);
|
||||||
|
free(line);
|
||||||
|
fclose(sys);
|
||||||
|
} else {
|
||||||
|
fputs("user-db:user\nsystem-db:local\nsystem-db:site\n"
|
||||||
|
"system-db:distro\n", out);
|
||||||
|
}
|
||||||
|
fclose(out);
|
||||||
|
rename(tmp, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build the shadow config home (see the block comment above
|
||||||
|
shadow_config): symlinked passthrough plus pinned GTK layouts plus the
|
||||||
|
dconf overlay profile. Runs once at startup, before first clients. */
|
||||||
|
static void config_shadow_build(void) {
|
||||||
|
if (getenv("KICK_NO_CONFIG_SHADOW"))
|
||||||
|
return;
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (!home || !home[0])
|
||||||
|
return;
|
||||||
|
const char *cache = getenv("XDG_CACHE_HOME");
|
||||||
|
char base[1024];
|
||||||
|
if (cache && cache[0]) {
|
||||||
|
if (snprintf(base, sizeof(base), "%s", cache) >= (int)sizeof(base))
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (snprintf(base, sizeof(base), "%s/.cache", home) >= (int)sizeof(base))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snprintf(shadow_config, sizeof(shadow_config), "%s/kick/config",
|
||||||
|
base) >= (int)sizeof(shadow_config))
|
||||||
|
return;
|
||||||
|
shadow_mkdir_p(shadow_config);
|
||||||
|
DIR *d = NULL;
|
||||||
|
{
|
||||||
|
char real[1024];
|
||||||
|
if (snprintf(real, sizeof(real), "%s/.config", home) >= (int)sizeof(real))
|
||||||
|
return;
|
||||||
|
d = opendir(real);
|
||||||
|
if (d) {
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d)) != NULL) {
|
||||||
|
if (strcmp(de->d_name, ".") == 0 ||
|
||||||
|
strcmp(de->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
char src[2048], dst[2048];
|
||||||
|
if (snprintf(src, sizeof(src), "%s/%s", real,
|
||||||
|
de->d_name) >= (int)sizeof(src))
|
||||||
|
continue;
|
||||||
|
if (snprintf(dst, sizeof(dst), "%s/%s", shadow_config,
|
||||||
|
de->d_name) >= (int)sizeof(dst))
|
||||||
|
continue;
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(dst, &st) == 0)
|
||||||
|
continue;
|
||||||
|
symlink(src, dst);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static const char *const gtkdirs[] = { "gtk-3.0", "gtk-4.0" };
|
||||||
|
for (size_t gi = 0; gi < sizeof(gtkdirs) / sizeof(gtkdirs[0]); gi++) {
|
||||||
|
char sub[2048], rfile[2048], sfile[2048];
|
||||||
|
if (snprintf(sub, sizeof(sub), "%s/%s", shadow_config,
|
||||||
|
gtkdirs[gi]) >= (int)sizeof(sub))
|
||||||
|
continue;
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(sub, &st) == 0 && S_ISLNK(st.st_mode))
|
||||||
|
unlink(sub);
|
||||||
|
shadow_mkdir_p(sub);
|
||||||
|
if (snprintf(rfile, sizeof(rfile), "%s/.config/%s/settings.ini",
|
||||||
|
home, gtkdirs[gi]) >= (int)sizeof(rfile))
|
||||||
|
continue;
|
||||||
|
if (snprintf(sfile, sizeof(sfile), "%s/settings.ini",
|
||||||
|
sub) >= (int)sizeof(sfile))
|
||||||
|
continue;
|
||||||
|
shadow_copy_file(rfile, sfile);
|
||||||
|
shadow_pin_gtk_layout(sfile);
|
||||||
|
}
|
||||||
|
if (snprintf(shadow_dconf_profile, sizeof(shadow_dconf_profile),
|
||||||
|
"%s/dconf-profile", shadow_config) >=
|
||||||
|
(int)sizeof(shadow_dconf_profile))
|
||||||
|
return;
|
||||||
|
shadow_write_profile(shadow_dconf_profile);
|
||||||
|
config_shadow_ok = true;
|
||||||
|
kick_log(KICK_LOG_INFO, "[config] shadow ready\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seed the pinned dconf layout into the overlay database (idempotent;
|
||||||
|
lands in kickoverrides, leaving real databases untouched). Needs the
|
||||||
|
session bus like everything dconf; skipped silently without it. */
|
||||||
|
static void config_seed_dconf(void) {
|
||||||
|
if (!config_shadow_ok)
|
||||||
|
return;
|
||||||
|
if (!getenv("DBUS_SESSION_BUS_ADDRESS"))
|
||||||
|
return;
|
||||||
|
if (!cmd_in_path("dconf"))
|
||||||
|
return;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setenv("DCONF_PROFILE", shadow_dconf_profile, 1);
|
||||||
|
execlp("dconf", "dconf", "write",
|
||||||
|
"/org/gnome/desktop/wm/preferences/button-layout",
|
||||||
|
"':close'", (char *)NULL);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Does the first whitespace-delimited word of `cmd` resolve to an executable
|
/* Does the first whitespace-delimited word of `cmd` resolve to an executable
|
||||||
on PATH? Mirrors how `sh -c <cmd>` resolves the first word, so this is a
|
on PATH? Mirrors how `sh -c <cmd>` resolves the first word, so this is a
|
||||||
cheap pre-launch "which" check. Returns 1 if found (or uncheckable, e.g.
|
cheap pre-launch "which" check. Returns 1 if found (or uncheckable, e.g.
|
||||||
|
|
@ -11715,6 +11963,13 @@ static void launch_client(struct kick_window *w) {
|
||||||
if (!xdg_runtime) xdg_runtime = "/tmp";
|
if (!xdg_runtime) xdg_runtime = "/tmp";
|
||||||
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
||||||
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
|
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
|
||||||
|
/* Per-compositor config view (shadow home + dconf overlay), built
|
||||||
|
at startup; falls back to the inherited environment when
|
||||||
|
shadowing is off. */
|
||||||
|
if (config_shadow_ok) {
|
||||||
|
setenv("XDG_CONFIG_HOME", shadow_config, 1);
|
||||||
|
setenv("DCONF_PROFILE", shadow_dconf_profile, 1);
|
||||||
|
}
|
||||||
/* Toolkits self-select their connection: Wayland-capable ones see
|
/* Toolkits self-select their connection: Wayland-capable ones see
|
||||||
only our socket above, X11-only ones (emacs-gtk, xmessage) take
|
only our socket above, X11-only ones (emacs-gtk, xmessage) take
|
||||||
DISPLAY below. Never force GDK_BACKEND/QT_QPA_PLATFORM to
|
DISPLAY below. Never force GDK_BACKEND/QT_QPA_PLATFORM to
|
||||||
|
|
@ -13112,9 +13367,12 @@ int main(int argc, char *argv[]) {
|
||||||
set_active_tab(focus_tab);
|
set_active_tab(focus_tab);
|
||||||
|
|
||||||
/* Fork every command-line command at once; the process-group match in
|
/* Fork every command-line command at once; the process-group match in
|
||||||
handle_new_toplevel gives each its own slot on arrival. Refresh the
|
handle_new_toplevel gives each its own slot on arrival. Build the
|
||||||
D-Bus/systemd activation environment first so services started from
|
shadow config first (children inherit the scoped view), then refresh
|
||||||
here on see this session's socket. */
|
the D-Bus/systemd activation environment so services started from
|
||||||
|
here on see this session's socket (and the same scoped view). */
|
||||||
|
config_shadow_build();
|
||||||
|
config_seed_dconf();
|
||||||
activation_update();
|
activation_update();
|
||||||
launch_all_cli();
|
launch_all_cli();
|
||||||
/* The --panel command, if any: a layer-shell client, forked separately since
|
/* The --panel command, if any: a layer-shell client, forked separately since
|
||||||
|
|
|
||||||
|
|
@ -1300,6 +1300,11 @@ def _spawn_standalone(extra_env, *apps, log=None):
|
||||||
for key in ("XCURSOR_PATH", "XCURSOR_THEME"):
|
for key in ("XCURSOR_PATH", "XCURSOR_THEME"):
|
||||||
if env.get(key):
|
if env.get(key):
|
||||||
cmd.append("%s=%s" % (key, env[key]))
|
cmd.append("%s=%s" % (key, env[key]))
|
||||||
|
# Opt-outs under test (e.g. KICK_NO_CONFIG_SHADOW) ride the same
|
||||||
|
# explicit-var discipline as everything else above: env -i wipes the
|
||||||
|
# dict, so each var the compositor needs must be listed.
|
||||||
|
if env.get("KICK_NO_CONFIG_SHADOW"):
|
||||||
|
cmd.append("KICK_NO_CONFIG_SHADOW=" + env["KICK_NO_CONFIG_SHADOW"])
|
||||||
# NOTE: env -i wipes the dict above, so every var the compositor needs
|
# NOTE: env -i wipes the dict above, so every var the compositor needs
|
||||||
# must be listed explicitly -- a missing KICK_DUMP_DIR silently dumps
|
# must be listed explicitly -- a missing KICK_DUMP_DIR silently dumps
|
||||||
# all standalone sessions into shared /tmp/host_N.ppm paths, and
|
# all standalone sessions into shared /tmp/host_N.ppm paths, and
|
||||||
|
|
@ -2992,6 +2997,62 @@ def test_background_fullscreen_keeps_normal_layout(tmp_path):
|
||||||
_assert_alive(s, "compositor died")
|
_assert_alive(s, "compositor died")
|
||||||
|
|
||||||
|
|
||||||
|
def test_standalone_shadow_config_scopes_children(tmp_path):
|
||||||
|
"""Standalone kick builds a shadow config home and scopes children to
|
||||||
|
it: a tab sees XDG_CONFIG_HOME/DCONF_PROFILE under the shadow, the
|
||||||
|
shadow gtk settings carry the pinned close-only layout plus the real
|
||||||
|
theme, and the dconf profile layers the kick-only database first."""
|
||||||
|
home = tmp_path / "home"
|
||||||
|
(home / ".config" / "gtk-3.0").mkdir(parents=True)
|
||||||
|
(home / ".config" / "gtk-3.0" / "settings.ini").write_text(
|
||||||
|
"[Settings]\ngtk-theme=Adwaita\n")
|
||||||
|
out = tmp_path / "child-env.out"
|
||||||
|
with _run_standalone(
|
||||||
|
{"HOME": str(home)},
|
||||||
|
"sh -c 'echo XDG_CONFIG_HOME=$XDG_CONFIG_HOME > \"%s\"; "
|
||||||
|
"echo DCONF_PROFILE=$DCONF_PROFILE >> \"%s\"'" % (out, out),
|
||||||
|
"%s '#ff0000'" % WL,
|
||||||
|
wait="any") as s:
|
||||||
|
assert _wait_out(out, "XDG_CONFIG_HOME=", timeout=15.0)
|
||||||
|
text = out.read_text()
|
||||||
|
shadow = os.path.join(str(home), ".cache", "kick", "config")
|
||||||
|
assert ("XDG_CONFIG_HOME=%s\n" % shadow) in text, \
|
||||||
|
f"child missed shadow config home: {text!r}"
|
||||||
|
assert ("DCONF_PROFILE=%s/dconf-profile\n" % shadow) in text, \
|
||||||
|
f"child missed dconf overlay profile: {text!r}"
|
||||||
|
pinned = open(os.path.join(shadow, "gtk-3.0",
|
||||||
|
"settings.ini")).read()
|
||||||
|
assert "gtk-decoration-layout=:close" in pinned, \
|
||||||
|
f"layout not pinned: {pinned!r}"
|
||||||
|
assert "gtk-theme=Adwaita" in pinned, \
|
||||||
|
f"real theme lost: {pinned!r}"
|
||||||
|
profile = open(os.path.join(shadow, "dconf-profile")).read()
|
||||||
|
assert "user-db:kickoverrides" in profile.splitlines(), \
|
||||||
|
f"overlay db not first: {profile!r}"
|
||||||
|
_assert_alive(s, "compositor died")
|
||||||
|
|
||||||
|
|
||||||
|
def test_standalone_shadow_config_opt_out(tmp_path):
|
||||||
|
"""KICK_NO_CONFIG_SHADOW disables the scoped view: children inherit
|
||||||
|
the plain environment with no XDG_CONFIG_HOME/DCONF_PROFILE override."""
|
||||||
|
home = tmp_path / "home"
|
||||||
|
home.mkdir()
|
||||||
|
out = tmp_path / "child-env.out"
|
||||||
|
with _run_standalone(
|
||||||
|
{"HOME": str(home), "KICK_NO_CONFIG_SHADOW": "1"},
|
||||||
|
"sh -c 'echo XDG_CONFIG_HOME=$XDG_CONFIG_HOME > \"%s\"; "
|
||||||
|
"echo DCONF_PROFILE=$DCONF_PROFILE >> \"%s\"'" % (out, out),
|
||||||
|
"%s '#ff0000'" % WL,
|
||||||
|
wait="any") as s:
|
||||||
|
assert _wait_out(out, "XDG_CONFIG_HOME=", timeout=15.0)
|
||||||
|
text = out.read_text()
|
||||||
|
assert "XDG_CONFIG_HOME=\n" in text, \
|
||||||
|
f"opt-out leaked a shadow config home: {text!r}"
|
||||||
|
assert "DCONF_PROFILE=\n" in text, \
|
||||||
|
f"opt-out leaked a dconf overlay profile: {text!r}"
|
||||||
|
_assert_alive(s, "compositor died")
|
||||||
|
|
||||||
|
|
||||||
def test_alt_f11_toggles_host_fullscreen(tmp_path):
|
def test_alt_f11_toggles_host_fullscreen(tmp_path):
|
||||||
"""prefix+F11 toggles the kick window's fullscreen state in the host;
|
"""prefix+F11 toggles the kick window's fullscreen state in the host;
|
||||||
the resulting configure fullscreens the active tab only (other tabs
|
the resulting configure fullscreens the active tab only (other tabs
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue