diff --git a/README.md b/README.md index ea1135c..57f0117 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ docs/ Settings reference, and the design specs behind the work ## Tests -143 of them, under `tests/`. Run the lot, or a subset by pattern: +144 of them, under `tests/`. Run the lot, or a subset by pattern: ```sh panama test # everything diff --git a/config/dot/hypr/looks.lua b/config/dot/hypr/looks.lua index 7ac02f0..c03306a 100644 --- a/config/dot/hypr/looks.lua +++ b/config/dot/hypr/looks.lua @@ -14,18 +14,17 @@ local prefs = require("prefs") --- Mirrors config/Theme.qml's `accents` map. Lua has no import path into a QML --- singleton, so the hex pairs are restated here -- just the four hex strings --- each named accent needs, not the labels, which stay UI-only. -local accents = { - blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" }, - orchid = { dark = "c099ff", darkSecondary = "fca7ea", light = "7847bd", lightSecondary = "9854f1" }, - teal = { dark = "86e1fc", darkSecondary = "82aaff", light = "007197", lightSecondary = "2e7de9" }, - green = { dark = "c3e88d", darkSecondary = "86e1fc", light = "587539", lightSecondary = "007197" }, - amber = { dark = "ffc777", darkSecondary = "ff966c", light = "8c6c3e", lightSecondary = "b15c00" }, - orange = { dark = "ff966c", darkSecondary = "ff757f", light = "b15c00", lightSecondary = "c64343" }, - rose = { dark = "ff757f", darkSecondary = "c099ff", light = "f52a65", lightSecondary = "9854f1" }, - slate = { dark = "828bb8", darkSecondary = "82aaff", light = "6172b0", lightSecondary = "2e7de9" }, +-- The eight accents, from config/palette.json -- the one place they are +-- written down for everything outside QML. They used to be restated here, +-- which made adding a ninth accent a five-file edit; the file that got +-- forgotten was always the one that fails silently. +-- +-- Falls back to the shipped blue pair if the palette cannot be read, for the +-- same reason prefs never raises: a missing file costs the accent colour, not +-- the compositor config. +local palette = prefs.readJson(os.getenv("HOME") .. "/.config/quickshell/config/palette.json") +local accents = palette.accents or { + blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" }, } -- The accent pair a fresh session or `hyprctl reload` starts from. diff --git a/config/dot/hypr/prefs.lua b/config/dot/hypr/prefs.lua index bfe9432..01f0068 100644 --- a/config/dot/hypr/prefs.lua +++ b/config/dot/hypr/prefs.lua @@ -244,6 +244,33 @@ function prefs.getInt(key, fallback) return math.floor(value + 0.5) end +-- Read and decode any JSON file, using the same never-raise parser the +-- settings store uses. Returns an empty table for a file that is missing, +-- empty, or malformed, so a caller can index the result without checking. +-- +-- Exists so config/palette.json can be read by looks.lua rather than the eight +-- accents being written out a second time in Lua. A bad palette costs the +-- accent colours, never the compositor config. +function prefs.readJson(path) + if type(path) ~= "string" or path == "" then + return {} + end + local file = io.open(path, "r") + if not file then + return {} + end + local text = file:read("*a") + file:close() + if not text or text:match("^%s*$") then + return {} + end + local ok, parsed = pcall(decode, text) + if not ok or type(parsed) ~= "table" then + return {} + end + return parsed +end + -- True when a settings file was actually read. Useful from overrides.lua. function prefs.loaded() return next(values) ~= nil diff --git a/config/dot/quickshell/config/palette.json b/config/dot/quickshell/config/palette.json new file mode 100644 index 0000000..42fb42d --- /dev/null +++ b/config/dot/quickshell/config/palette.json @@ -0,0 +1,70 @@ +{ + "_comment": "Generated-by-hand source of truth for every consumer outside QML. See config/dot/quickshell/config/PALETTE.md.", + "default": "blue", + "accents": { + "blue": { + "dark": "82aaff", + "darkSecondary": "b172b0", + "light": "2e7de9", + "lightSecondary": "9854f1", + "label": "Prism blue", + "gnome": "blue" + }, + "orchid": { + "dark": "c099ff", + "darkSecondary": "fca7ea", + "light": "7847bd", + "lightSecondary": "9854f1", + "label": "Orchid", + "gnome": "purple" + }, + "teal": { + "dark": "86e1fc", + "darkSecondary": "82aaff", + "light": "007197", + "lightSecondary": "2e7de9", + "label": "Teal", + "gnome": "teal" + }, + "green": { + "dark": "c3e88d", + "darkSecondary": "86e1fc", + "light": "587539", + "lightSecondary": "007197", + "label": "Green", + "gnome": "green" + }, + "amber": { + "dark": "ffc777", + "darkSecondary": "ff966c", + "light": "8c6c3e", + "lightSecondary": "b15c00", + "label": "Amber", + "gnome": "yellow" + }, + "orange": { + "dark": "ff966c", + "darkSecondary": "ff757f", + "light": "b15c00", + "lightSecondary": "c64343", + "label": "Orange", + "gnome": "orange" + }, + "rose": { + "dark": "ff757f", + "darkSecondary": "c099ff", + "light": "f52a65", + "lightSecondary": "9854f1", + "label": "Rose", + "gnome": "red" + }, + "slate": { + "dark": "828bb8", + "darkSecondary": "82aaff", + "light": "6172b0", + "lightSecondary": "2e7de9", + "label": "Slate", + "gnome": "slate" + } + } +} diff --git a/config/dot/quickshell/scripts/panama-lock b/config/dot/quickshell/scripts/panama-lock index ed62d1a..fb91e1f 100755 --- a/config/dot/quickshell/scripts/panama-lock +++ b/config/dot/quickshell/scripts/panama-lock @@ -8,6 +8,10 @@ set -euo pipefail +# The accents, from the one file that holds them. +# shellcheck source=./panama-palette +source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/panama-palette" + config_home="${XDG_CONFIG_HOME:-$HOME/.config}" state_home="${XDG_STATE_HOME:-$HOME/.local/state}" settings="$config_home/panama/settings.json" @@ -102,25 +106,8 @@ resolve_wallpaper_path() { # hue per scheme only: the lock screen shows a flat focus ring, not the # two-stop gradient the window border does. Falls back to blue for an unknown # name, matching Theme.accentPair's own fallback. -accent_hex() { - local name="$1" scheme="$2" - case "$name" in - orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;; - teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;; - green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;; - amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;; - orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;; - rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;; - slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;; - *) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;; - esac -} # hyprlock wants "R, G, B" decimal, not hex. -hex_to_rgb() { - local hex="$1" - printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}" -} load_preferences() { if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then diff --git a/config/dot/quickshell/scripts/panama-palette b/config/dot/quickshell/scripts/panama-palette new file mode 100755 index 0000000..7b4ac9c --- /dev/null +++ b/config/dot/quickshell/scripts/panama-palette @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# The accent palette, for the shell scripts that need it. +# +# Sourced, not run: +# +# source "$(dirname "$0")/panama-palette" +# accent_hex orchid dark # -> c099ff +# hex_to_rgb c099ff # -> 192, 153, 255 +# +# Why this exists: the same eight accents were written out by hand in five +# places -- Theme.qml through ThemeProfileModel.js, hypr/looks.lua, +# panama-theme-apps and panama-lock -- and panama-theme-apps said so in a +# comment admitting "there is no shared source between QML and a shell script". +# Adding a ninth accent meant editing five files, and the one most likely to be +# missed was the lock screen, which fails silently: the machine locks with a +# stale color and nothing says why. +# +# config/palette.json is now that shared source. QML still carries the table in +# ThemeProfileModel.js, because a .js module imported into QML cannot read a +# file -- but the two are checked against each other by +# tests/quickshell/palette-contract, so drift is a failing test rather than a +# lock screen in last season's colour. + +# The palette lives beside the shell config. Resolved from this script's own +# location so it is correct wherever the repository is checked out. +PANAMA_PALETTE="${PANAMA_PALETTE:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)/config/palette.json}" + +# The hex for one accent in one scheme, without the leading '#'. +# +# Falls back to blue for an unknown or omitted name, matching Theme.qml's own +# accentPair fallback: a caller that only ever knew about the scheme still gets +# a real accent rather than an empty string reaching sed. +accent_hex() { + local name="${1:-}" scheme="${2:-dark}" key value + [[ "$scheme" == "light" ]] && key="light" || key="dark" + + value="$(jq -r --arg n "$name" --arg k "$key" \ + '.accents[$n][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)" + if [[ -z "$value" ]]; then + value="$(jq -r --arg k "$key" \ + '.accents[.default][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)" + fi + # Last resort, for a palette file that is missing or unreadable. Shipping + # blue is better than shipping an empty string into a config generator. + [[ -n "$value" ]] || value="$([[ "$key" == light ]] && echo 2e7de9 || echo 82aaff)" + printf '%s' "$value" +} + +# The second half of the pair, for the two-stop gradient the window border uses. +accent_secondary_hex() { + local name="${1:-}" scheme="${2:-dark}" key value + [[ "$scheme" == "light" ]] && key="lightSecondary" || key="darkSecondary" + + value="$(jq -r --arg n "$name" --arg k "$key" \ + '.accents[$n][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)" + if [[ -z "$value" ]]; then + value="$(jq -r --arg k "$key" \ + '.accents[.default][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)" + fi + [[ -n "$value" ]] || value="$([[ "$key" == lightSecondary ]] && echo 9854f1 || echo b172b0)" + printf '%s' "$value" +} + +# GNOME's accent-color is a FIXED enum of nine names -- there is no hex to +# match, only a nearest member to pick -- so palette.json records which one +# each Panama accent maps to. libadwaita applications are told this, so +# choosing an accent here recolours Files and Papers too. +accent_gnome() { + local name="${1:-}" value + value="$(jq -r --arg n "$name" '.accents[$n].gnome // empty' "$PANAMA_PALETTE" 2>/dev/null)" + [[ -n "$value" ]] || value="$(jq -r '.accents[.default].gnome // empty' "$PANAMA_PALETTE" 2>/dev/null)" + [[ -n "$value" ]] || value="blue" + printf '%s' "$value" +} + +# hyprlock wants "R, G, B" decimal, not hex. +hex_to_rgb() { + local hex="${1#\#}" + printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}" +} diff --git a/config/dot/quickshell/scripts/panama-theme-apps b/config/dot/quickshell/scripts/panama-theme-apps index b63037d..62e6660 100755 --- a/config/dot/quickshell/scripts/panama-theme-apps +++ b/config/dot/quickshell/scripts/panama-theme-apps @@ -24,11 +24,16 @@ # nothing until you open a new window. # # kitty's active_border_color and hyprlock's focus ring are also the accent -# role, not just the scheme -- see accent_hex() below, which mirrors the same -# lookup hypr/looks.lua and scripts/panama-lock carry for the same reason. +# role, not just the scheme. The lookup lives in scripts/panama-palette, +# which reads config/palette.json -- the one place the eight accents are +# written down for everything outside QML. set -euo pipefail +# The accents, from the one file that holds them. +# shellcheck source=./panama-palette +source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/panama-palette" + scheme="${1:-dark}" case "$scheme" in dark|light) ;; @@ -49,25 +54,8 @@ esac # hand -- there is no shared source between QML and a shell script), primary # hue per scheme only: both consumers below draw a flat color, not the # two-stop gradient the window border does. -accent_hex() { - local name="$1" scheme="$2" - case "$name" in - orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;; - teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;; - green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;; - amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;; - orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;; - rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;; - slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;; - *) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;; - esac -} # hyprlock wants "R, G, B" decimal, not hex. -hex_to_rgb() { - local hex="$1" - printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}" -} accent_border_hex="$(accent_hex "$accent" "$scheme")" @@ -210,29 +198,19 @@ done # render in GNOME blue no matter which accent the desktop is set to. # # GNOME's accent-color is a FIXED enum of nine names -- there is no hex here to -# match, only a nearest member to pick -- so this maps our eight to theirs. Kept -# in sync by hand with config/Theme.qml's `gnome` field, for the same reason -# accent_hex() above is: there is no shared source between QML and shell. +# match, only a nearest member to pick. Which one each Panama accent maps to is +# recorded in config/palette.json alongside its colours, for the same reason +# accent lookup once was: there was no shared source between QML and shell. +# There is now -- config/palette.json -- and palette-contract fails when the +# QML table and it disagree. # # The portal must route Settings to the gnome backend for this to reach # anything; xdg-desktop-portal-gtk cannot serve accent-color at all. See # config/dot/xdg-desktop-portal/hyprland-portals.conf. -gnome_accent() { - case "$1" in - orchid) printf 'purple' ;; - teal) printf 'teal' ;; - green) printf 'green' ;; - amber) printf 'yellow' ;; - orange) printf 'orange' ;; - rose) printf 'red' ;; - slate) printf 'slate' ;; - *) printf 'blue' ;; - esac -} status_adwaita="skipped" if command -v gsettings >/dev/null 2>&1; then - adwaita_accent="$(gnome_accent "$accent")" + adwaita_accent="$(accent_gnome "$accent")" # Writing the same value emits no change signal, so applications already # showing this accent are not asked to restyle for nothing. if [[ "$(gsettings get org.gnome.desktop.interface accent-color 2>/dev/null)" == "'$adwaita_accent'" ]]; then diff --git a/tests/quickshell/adwaita-accent-contract b/tests/quickshell/adwaita-accent-contract index 4cc6c34..0c09206 100755 --- a/tests/quickshell/adwaita-accent-contract +++ b/tests/quickshell/adwaita-accent-contract @@ -70,36 +70,39 @@ if command -v gsettings >/dev/null 2>&1; then fi # ── The shell helper must agree with the table ─────────────────────────────── -# panama-theme-apps carries its own copy of this mapping because a shell script -# cannot read a QML object. A copy that drifts is exactly how an accent ends up -# correct in Panama's own surfaces and wrong everywhere else. -shell_member() { - local name="$1" body - body="$(sed -n '/^gnome_accent() {/,/^}/p' "$script")" - [[ -n "$body" ]] || fail 'gnome_accent() is missing from panama-theme-apps' - local line - line="$(grep -E "^\s+${name}\)" <<<"$body" | head -1)" - if [[ -z "$line" ]]; then - # Not listed means it falls through to the default branch. - line="$(grep -E '^\s+\*\)' <<<"$body" | head -1)" - fi - sed -E "s/.*printf '([a-z]+)'.*/\1/" <<<"$line" -} +# The mapping used to be a second copy of this table, written out as a case +# statement in panama-theme-apps. It now lives in config/palette.json beside +# each accent's colours, and scripts/panama-palette reads it -- so the shell +# and QML resolve the same member from the same file rather than from two +# lists somebody has to remember to edit together. +# +# palette-contract checks palette.json against the QML table value by value; +# this checks that the shell actually resolves through it, which is the half +# that would let an accent be correct in Panama's own surfaces and wrong +# everywhere else. +helper="$repo_dir/config/dot/quickshell/scripts/panama-palette" +[[ -r "$helper" ]] || fail 'scripts/panama-palette is missing, so nothing maps accents for the shell' +grep -q 'accent_gnome()' "$helper" \ + || fail 'panama-palette does not resolve a GNOME accent member' +grep -q 'accent_gnome' "$script" \ + || fail 'panama-theme-apps no longer asks the palette for the GNOME accent member' +grep -q 'gnome_accent() {' "$script" \ + && fail 'panama-theme-apps has grown its own copy of the mapping again' while IFS=$'\t' read -r name member; do [[ -n "$name" ]] || continue - actual="$(shell_member "$name")" + actual="$(bash -c "source '$helper'; accent_gnome '$name'")" [[ "$actual" == "$member" ]] \ - || fail "Theme.qml maps \"$name\" to \"$member\" but panama-theme-apps maps it to \"$actual\"" + || fail "Theme.qml maps \"$name\" to \"$member\" but the shell resolves \"$actual\"" done <<<"$mapping" -# The helper also validates the accent name it is handed; an accent the table -# knows but the helper rejects silently degrades to blue. -accepted="$(grep -oE '^\s+blue\|[a-z|]+\)' "$script" | head -1 | tr -d ' )' )" +# An accent the table knows but the palette does not would silently degrade to +# blue, which is the failure this pair of files exists to prevent. +palette="$repo_dir/config/dot/quickshell/config/palette.json" while IFS=$'\t' read -r name member; do [[ -n "$name" ]] || continue - grep -qE "(^|\|)$name(\||$)" <<<"$accepted" \ - || fail "panama-theme-apps does not accept the \"$name\" accent, so choosing it falls back to blue" + jq -e --arg n "$name" '.accents | has($n)' "$palette" >/dev/null \ + || fail "the palette has no \"$name\" accent, so choosing it falls back to blue" done <<<"$mapping" # ── The portal has to be able to serve it ──────────────────────────────────── diff --git a/tests/quickshell/palette-contract b/tests/quickshell/palette-contract new file mode 100755 index 0000000..ab05c21 --- /dev/null +++ b/tests/quickshell/palette-contract @@ -0,0 +1,146 @@ +#!/usr/bin/env bash + +# The eight accents, and the one place they are written down. +# +# They used to be written down five times: ThemeProfileModel.js for QML, +# hypr/looks.lua for the compositor, and again in panama-theme-apps and +# panama-lock. panama-theme-apps said so in its own comment -- "there is no +# shared source between QML and a shell script" -- which is an accurate +# description of a bug waiting to happen. Adding a ninth accent meant editing +# five files, and the one most likely to be missed was the lock screen, which +# fails silently: the machine locks in a stale colour and nothing says why. +# +# config/palette.json is now that source. Everything outside QML reads it: +# looks.lua through prefs.readJson, the two shell generators through +# scripts/panama-palette. +# +# QML still carries the table, because a .js module imported into QML cannot +# read a file. That is a copy, and copies drift, so this contract exists to +# make the drift a failing test instead of a wrong lock screen. It compares +# them value by value rather than checking that both merely exist. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +palette="$repo_dir/config/dot/quickshell/config/palette.json" +model="$repo_dir/config/dot/quickshell/services/ThemeProfileModel.js" +helper="$repo_dir/config/dot/quickshell/scripts/panama-palette" +looks="$repo_dir/config/dot/hypr/looks.lua" +theme_apps="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps" +lock="$repo_dir/config/dot/quickshell/scripts/panama-lock" + +findings=() +note() { findings+=("$1"); } + +[[ -r "$palette" ]] || { printf 'palette contract: %s is missing\n' "$palette" >&2; exit 1; } +jq -e . "$palette" >/dev/null 2>&1 || { printf 'palette contract: palette.json is not valid JSON\n' >&2; exit 1; } + +# ── The QML table and the palette agree, value by value ────────────────────── + +python3 - "$palette" "$model" <<'PY' || note 'the QML accent table and config/palette.json disagree; see the lines above' +import json, re, sys + +palette = json.load(open(sys.argv[1]))["accents"] +source = open(sys.argv[2], encoding="utf-8").read() +block = source[source.index("var CURATED = {"):] +block = block[:block.index("\n};") + 3] + +qml = {} +for match in re.finditer( + r'(\w+):\s*\{\s*dark:\s*"#([0-9a-f]{6})",\s*darkSecondary:\s*"#([0-9a-f]{6})",' + r'\s*light:\s*"#([0-9a-f]{6})",\s*lightSecondary:\s*"#([0-9a-f]{6})",' + r'\s*label:\s*"([^"]*)",\s*gnome:\s*"([^"]*)"', block): + name, dark, dark2, light, light2, label, gnome = match.groups() + qml[name] = {"dark": dark, "darkSecondary": dark2, "light": light, + "lightSecondary": light2, "label": label, "gnome": gnome} + +problems = [] +for name in sorted(set(palette) | set(qml)): + if name not in palette: + problems.append(f"{name}: in the QML table but not in palette.json") + continue + if name not in qml: + problems.append(f"{name}: in palette.json but not in the QML table") + continue + for field in ("dark", "darkSecondary", "light", "lightSecondary", "label", "gnome"): + if palette[name].get(field) != qml[name].get(field): + problems.append( + f"{name}.{field}: palette.json has {palette[name].get(field)!r}, " + f"the QML table has {qml[name].get(field)!r}") + +if problems: + print("\n".join(" " + p for p in problems), file=sys.stderr) + raise SystemExit(1) +PY + +# ── Every consumer reads it rather than restating it ───────────────────────── + +grep -q 'palette.json' "$helper" || note 'the shell palette helper does not read palette.json' +grep -q 'panama-palette' "$theme_apps" || note 'panama-theme-apps does not use the shared palette helper' +grep -q 'panama-palette' "$lock" || note 'panama-lock does not use the shared palette helper' +grep -q 'prefs.readJson' "$looks" || note 'looks.lua does not read the palette, so the compositor has its own copy again' + +# A restated table is the thing this exists to prevent. Each of these once held +# all eight; a file holding more than a single fallback pair has grown one back. +for file in "$theme_apps" "$lock"; do + count="$(grep -cE '^\s*(orchid|teal|green|amber|orange|rose|slate)\)' "$file" || true)" + (( count == 0 )) || note "$(basename "$file") has an accent table again ($count entries)" +done +looks_accents="$(grep -cE '^\s+(orchid|teal|green|amber|orange|rose|slate)\s*=' "$looks" || true)" +(( looks_accents == 0 )) || note "looks.lua has an accent table again ($looks_accents entries)" + +# ── The helper resolves what the palette says ──────────────────────────────── + +while read -r name; do + for scheme in dark light; do + want="$(jq -r --arg n "$name" --arg s "$scheme" '.accents[$n][$s]' "$palette")" + got="$(bash -c "source '$helper'; accent_hex '$name' '$scheme'")" + [[ "$got" == "$want" ]] \ + || note "the helper resolves $name/$scheme as '$got'; the palette says '$want'" + done +done < <(jq -r '.accents | keys[]' "$palette") + +# An unknown accent must fall back rather than produce an empty string, which +# would reach sed and generate a config with a colour of "". +fallback="$(bash -c "source '$helper'; accent_hex 'not-an-accent' dark")" +[[ "$fallback" == "$(jq -r '.accents[.default].dark' "$palette")" ]] \ + || note "an unknown accent resolves to '$fallback' rather than the default" +empty="$(bash -c "source '$helper'; accent_hex '' dark")" +[[ -n "$empty" ]] || note 'an empty accent name resolves to an empty string, which would reach a config generator' + +# ── Lua reads it too ───────────────────────────────────────────────────────── + +if command -v lua >/dev/null 2>&1; then + lua_result="$(cd "$repo_dir/config/dot/hypr" && PANAMA_TEST_PALETTE="$palette" lua -e ' + package.path = "./?.lua;" .. package.path + local prefs = require("prefs") + local parsed = prefs.readJson(os.getenv("PANAMA_TEST_PALETTE")) + local accents = parsed.accents + if not accents then print("none") os.exit(0) end + local count = 0 + for _ in pairs(accents) do count = count + 1 end + print(count .. " " .. tostring(accents.orchid and accents.orchid.dark)) + ' 2>/dev/null)" + expected_count="$(jq -r '.accents | length' "$palette")" + expected_orchid="$(jq -r '.accents.orchid.dark' "$palette")" + [[ "$lua_result" == "$expected_count $expected_orchid" ]] \ + || note "Lua reads the palette as '$lua_result', expected '$expected_count $expected_orchid'" + + # A palette that cannot be read must cost the colours and nothing else. + lua_missing="$(cd "$repo_dir/config/dot/hypr" && PANAMA_TEST_PALETTE="/nonexistent/palette.json" lua -e ' + package.path = "./?.lua;" .. package.path + local prefs = require("prefs") + local parsed = prefs.readJson(os.getenv("PANAMA_TEST_PALETTE")) + print(type(parsed) == "table" and "table" or "raised") + ' 2>&1)" + [[ "$lua_missing" == "table" ]] \ + || note 'reading a missing palette does not return an empty table, so a bad file would break the compositor config' +fi + +if (( ${#findings[@]} > 0 )); then + printf 'palette contract: %d finding(s)\n' "${#findings[@]}" >&2 + printf ' - %s\n' "${findings[@]}" >&2 + exit 1 +fi + +printf 'palette contract: PASS (%s accents, one source)\n' "$(jq -r '.accents | length' "$palette")"