Add effective desktop style controls

This commit is contained in:
Gabriel Brown
2026-08-18 12:57:54 -04:00
parent f9eba1e8c5
commit df1dcdfad5
10 changed files with 837 additions and 29 deletions
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# Desktop style stays split across three boundaries that can all be checked
# without starting the shell: the read-only icon catalog, the preference/schema
# wiring, and the QML surfaces that consume it. The catalog runs only against
# disposable XDG roots so this contract never reads or changes the live desktop.
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-desktop-style"
service="$repo_dir/config/dot/quickshell/services/DesktopStyle.qml"
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
appearance="$repo_dir/config/dot/quickshell/modules/settings/AppearancePage.qml"
mouse="$repo_dir/config/dot/quickshell/modules/settings/MousePage.qml"
accessibility="$repo_dir/config/dot/quickshell/services/Accessibility.qml"
search="$repo_dir/config/dot/quickshell/services/SettingsSearch.qml"
env_lua="$repo_dir/config/dot/hypr/env.lua"
looks_lua="$repo_dir/config/dot/hypr/looks.lua"
fail() {
printf 'desktop style contract: %s\n' "$1" >&2
exit 1
}
for file in "$helper" "$service" "$schema" "$appearance" "$mouse" \
"$accessibility" "$search" "$env_lua" "$looks_lua"; do
[[ -f "$file" ]] || fail "missing ${file#"$repo_dir/"}"
done
[[ -x "$helper" ]] || fail 'desktop-style catalog is not executable'
# ── Read-only catalog semantics ─────────────────────────────────────────────
fixture="$(mktemp -d /tmp/panama-desktop-style.XXXXXX)"
trap 'rm -rf "$fixture"' EXIT
home="$fixture/home"
data_home="$fixture/data-home"
data_one="$fixture/data-one"
data_two="$fixture/data-two"
mkdir -p "$home" \
"$data_home/icons/CursorOnly/cursors" \
"$data_one/icons/IconOnly/16x16/apps" \
"$data_one/icons/Both/cursors" \
"$data_one/icons/Both/scalable/apps" \
"$data_one/icons/EmptyDirectories" \
"$data_two/icons/Both/cursors" \
"$data_two/icons/Both/scalable/apps" \
"$data_two/icons/NoIndex/16x16/apps"
cat >"$data_home/icons/CursorOnly/index.theme" <<'EOF'
[Icon Theme]
Name=Cursor only
Directories=
EOF
cat >"$data_one/icons/IconOnly/index.theme" <<'EOF'
[Icon Theme]
Name=Icon only
Directories=16x16/apps
EOF
cat >"$data_one/icons/Both/index.theme" <<'EOF'
[Icon Theme]
Name=Both
Directories=scalable/apps
EOF
cat >"$data_one/icons/EmptyDirectories/index.theme" <<'EOF'
[Icon Theme]
Name=Not an icon catalog entry
Directories=
EOF
cp "$data_one/icons/Both/index.theme" "$data_two/icons/Both/index.theme"
catalog="$(
HOME="$home" \
XDG_DATA_HOME="$data_home" \
XDG_DATA_DIRS="$data_one:$data_two" \
"$helper"
)" || fail 'catalog helper failed against disposable XDG roots'
jq -e '
type == "object"
and (keys | sort) == ["cursorThemes", "iconThemes"]
and .cursorThemes == ["Both", "CursorOnly"]
and .iconThemes == ["Both", "IconOnly"]
and (all(.cursorThemes[]; type == "string" and length > 0))
and (all(.iconThemes[]; type == "string" and length > 0))
' <<<"$catalog" >/dev/null \
|| fail "catalog JSON shape or theme classification is wrong: $catalog"
if HOME="$home" XDG_DATA_HOME="$data_home" XDG_DATA_DIRS="$data_one:$data_two" \
"$helper" "$fixture/not-an-xdg-root" >/dev/null 2>&1; then
fail 'catalog accepted a caller-supplied path'
fi
# ── Schema, compositor replay, service, and UI wiring ───────────────────────
python3 - "$schema" <<'PY' || fail 'desktop-style schema entries are missing or malformed'
import re
import sys
text = open(sys.argv[1], encoding="utf-8").read()
expected = {
"cursorTheme": ("string", '"oreo_blue_cursors"', "themes"),
"iconTheme": ("string", '"Adwaita"', "themes"),
"applicationFont": ("string", '"Adwaita Sans"', "typography"),
"applicationFontSize": ("int", "11", "typography"),
"documentFont": ("string", '"Adwaita Sans"', "typography"),
"documentFontSize": ("int", "12", "typography"),
"monospaceFont": ("string", '"VictorMono Nerd Font"', "typography"),
"monospaceFontSize": ("int", "10", "typography"),
"fontHinting": ("enum", '"slight"', "typography"),
"fontAntialiasing": ("enum", '"rgba"', "typography"),
"titlebarButtonSide": ("enum", '"right"', "titlebar"),
"titlebarMaximizeButton": ("bool", "false", "titlebar"),
"titlebarDoubleClick": ("enum", '"toggle-maximize"', "titlebar"),
"middleClickPaste": ("bool", "true", "pointer"),
}
def block_for(key: str) -> str:
match = re.search(r"\{\s*\n\s*key:\s*\"" + re.escape(key) + r"\".*?\n\s{8}\}", text, re.S)
if not match:
raise SystemExit(f"missing {key}")
return match.group(0)
for key, (kind, default, group) in expected.items():
block = block_for(key)
if not re.search(rf'type:\s*"{re.escape(kind)}"', block):
raise SystemExit(f"{key} has wrong type")
if not re.search(rf'def:\s*{re.escape(default)}', block):
raise SystemExit(f"{key} has wrong default")
if not re.search(rf'group:\s*"{re.escape(group)}"', block):
raise SystemExit(f"{key} has wrong group")
enum_values = {
"fontHinting": ["none", "slight", "medium", "full"],
"fontAntialiasing": ["none", "grayscale", "rgba"],
"titlebarButtonSide": ["left", "right"],
"titlebarDoubleClick": ["toggle-maximize", "none"],
}
for key, values in enum_values.items():
block = block_for(key)
actual = re.findall(r'value:\s*"([^"]+)"', block)
if actual != values:
raise SystemExit(f"{key} options are {actual}, expected {values}")
middle = block_for("middleClickPaste")
for fragment in (
'path: ["misc", "middle_click_paste"]',
'option: "misc:middle_click_paste"',
'readAs: "bool"',
):
if fragment not in middle:
raise SystemExit(f"middleClickPaste is missing {fragment}")
PY
rg -q 'prefs\.get\("cursorTheme", "oreo_blue_cursors"\)' "$env_lua" \
|| fail 'Hyprland environment does not replay cursorTheme'
rg -q 'middle_click_paste\s*=\s*prefs\.get\("middleClickPaste", true\)' "$looks_lua" \
|| fail 'Hyprland misc does not replay middleClickPaste'
rg -q 'DesktopPreferences\.get\("cursorTheme"\)' "$accessibility" \
|| fail 'Accessibility does not use the stored cursor theme'
! rg -q 'gsettings.*cursor-theme|themeQuery' "$accessibility" \
|| fail 'Accessibility still queries cursor-theme from gsettings'
for needle in \
'property var cursorThemes' \
'property var iconThemes' \
'DesktopPreferences.revision' \
'Fonts.interfaceFonts' \
'Fonts.monospaceFonts' \
'gtk-enable-primary-paste' \
'button-layout' \
'action-double-click-titlebar'; do
rg -Fq "$needle" "$service" || fail "DesktopStyle is missing $needle"
done
! rg -q 'sh -c|bash -c' "$service" \
|| fail 'DesktopStyle routes gsettings through a shell'
for group in typography themes titlebar; do
rg -q "\"$group\": \"appearance\"" "$search" \
|| fail "settings search does not route $group to Appearance"
done
rg -q '"pointer": "mouse"' "$search" \
|| fail 'settings search no longer routes pointer controls to Mouse'
for needle in \
'title: "Application typography"' \
'setting: "applicationFontSize"' \
'setting: "documentFontSize"' \
'setting: "monospaceFontSize"' \
'setting: "fontHinting"' \
'setting: "fontAntialiasing"' \
'title: "Icons & pointer"' \
'DesktopStyle.cursorThemes' \
'DesktopStyle.iconThemes' \
'title: "Titlebars"' \
'setting: "titlebarButtonSide"' \
'setting: "titlebarMaximizeButton"' \
'setting: "titlebarDoubleClick"'; do
rg -Fq "$needle" "$appearance" || fail "Appearance is missing $needle"
done
! rg -q 'setting: "titlebarMinimize|GTK theme|Shell theme' "$appearance" \
|| fail 'Appearance exposes an inert or separately-owned theme control'
rg -q 'setting: "middleClickPaste"' "$mouse" \
|| fail 'Mouse does not expose middle-click paste'
rg -q 'GTK.*Wayland|Wayland.*GTK' "$mouse" \
|| fail 'Mouse does not explain the GTK and Wayland scope honestly'
printf 'desktop style contract: PASS\n'