211 lines
7.9 KiB
Bash
Executable File
211 lines
7.9 KiB
Bash
Executable File
#!/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'
|