Add effective desktop style controls
This commit is contained in:
Executable
+210
@@ -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'
|
||||
Reference in New Issue
Block a user