Appearance now opens on Themes: light and dark side by side, each remembering its own choice, over galleries of ten shipped themes — Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and Everforest in both modes. A theme is a complete palette: the catalog lives in themes.json, Theme.qml reads every color token from the active record, and one render pipeline carries it to kitty, tmux, btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme editor builds new ones from four wells — wheel, hex, or eyedropper — with derived surfaces, a saturation slider, debounced fine-tune, and effects that save with the theme. Custom edits finally keep GNOME's accent, kitty's border, and hyprlock in sync. Wallpapers can be video: mpvpaper per output, hardware-decoded, muted and looped, supervised and respawned. Panama owns the pausing — games, battery, and a bar pill for right now — because the compositor rebuilds full-screen blur for every frame a video wallpaper draws. The lock screen gets a still frame. Titlebars stop lying. GNOME apps get close-only on your chosen side, the maximize and double-click settings are gone, the Settings window obeys the same rules, and its titlebar can be turned off entirely. Typography becomes five labeled dropdowns instead of a wall of samples. Contracts updated and written throughout (165 now); per the redesign workflow none were executed — the full sweep runs once at the end. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
169 lines
7.7 KiB
Bash
Executable File
169 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
# The one titlebar Panama draws.
|
||
#
|
||
# Settings is the only Panama window with a titlebar of its own, and it used to
|
||
# show three buttons: minimize, maximize, close. Two of those were lies.
|
||
# Hyprland has no minimize -- it receives the request and does nothing with it
|
||
# -- and maximize is meaningless in a tiler. Pressing them looked exactly like
|
||
# pressing a button that works.
|
||
#
|
||
# So the bar is close-only, it follows the same button-side preference GNOME
|
||
# applications get, and it can be turned off entirely: with `panamaTitlebar`
|
||
# false the window is pure Hyprland -- Super+Q closes it, Super+drag moves it,
|
||
# and Escape still works.
|
||
#
|
||
# What must hold:
|
||
#
|
||
# 1. No minimize or maximize control anywhere in the settings chrome.
|
||
# 2. The bar collapses to nothing rather than merely hiding, or the page
|
||
# below keeps a 48px hole where the bar used to be.
|
||
# 3. The title and the close button swap sides together.
|
||
# 4. The close button is reachable and identifiable without a mouse or
|
||
# sight: it is a "×" glyph and nothing else.
|
||
# 5. Escape closes the window whether or not the bar is drawn, so turning
|
||
# the titlebar off never traps somebody in a window with no visible way
|
||
# out.
|
||
|
||
set -euo pipefail
|
||
|
||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
shell_dir="$repo_dir/config/dot/quickshell"
|
||
shell_ui="$shell_dir/modules/settings/SettingsShell.qml"
|
||
window="$shell_dir/modules/settings/SettingsWindow.qml"
|
||
appearance="$shell_dir/modules/settings/AppearancePage.qml"
|
||
schema="$shell_dir/config/PreferenceSchema.qml"
|
||
|
||
fail() {
|
||
printf 'settings titlebar contract: %s\n' "$1" >&2
|
||
exit 1
|
||
}
|
||
|
||
for file in "$shell_ui" "$window" "$appearance" "$schema"; do
|
||
[[ -f "$file" ]] || fail "missing ${file#"$repo_dir/"}"
|
||
done
|
||
|
||
# ── 1. Nothing that cannot work ─────────────────────────────────────────────
|
||
python3 - "$shell_ui" "$window" <<'PY' || fail 'the settings chrome offers a control Hyprland cannot honour'
|
||
import re
|
||
import sys
|
||
|
||
for path in sys.argv[1:]:
|
||
text = open(path, encoding="utf-8").read()
|
||
# Comments may say the words -- explaining why they are absent is the
|
||
# point. Code may not.
|
||
code = re.sub(r"//.*", "", text)
|
||
for word in ("minimize", "minimise", "maximize", "maximise"):
|
||
if re.search(word, code, re.I):
|
||
raise SystemExit(f"{path.split('/')[-1]} still has a {word} control")
|
||
PY
|
||
|
||
# ── 2. Off means gone, not merely invisible ─────────────────────────────────
|
||
rg -Fq 'readonly property bool shown: DesktopPreferences.get("panamaTitlebar") !== false' "$shell_ui" \
|
||
|| fail 'the titlebar does not follow the panamaTitlebar preference'
|
||
rg -Fq 'height: shown ? 48 : 0' "$shell_ui" \
|
||
|| fail 'the titlebar hides without collapsing, leaving a hole above the page'
|
||
rg -Fq 'visible: shown' "$shell_ui" \
|
||
|| fail 'the hidden titlebar is still rendered'
|
||
# Everything below is anchored to the bar's bottom edge, so collapsing it is
|
||
# what actually reclaims the space.
|
||
[[ "$(rg -c 'anchors.top: titlebar.bottom' "$shell_ui")" -ge 3 ]] \
|
||
|| fail 'the sidebar, tab strip and page no longer follow the titlebar edge'
|
||
|
||
# ── 3. Title and close button swap together ─────────────────────────────────
|
||
rg -Fq 'readonly property bool buttonsLeft: DesktopPreferences.get("titlebarButtonSide") === "left"' "$shell_ui" \
|
||
|| fail 'the titlebar does not follow the button-side preference'
|
||
python3 - "$shell_ui" <<'PY' || fail 'the titlebar is not side-aware'
|
||
import re
|
||
import sys
|
||
|
||
text = open(sys.argv[1], encoding="utf-8").read()
|
||
pairs = re.findall(r'anchors\.(left|right): titlebar\.buttonsLeft \? (\S+) : (\S+)', text)
|
||
if len(pairs) < 4:
|
||
raise SystemExit("fewer than two side-aware elements; the title or the button is pinned")
|
||
# For each element, exactly one edge is released and the other taken, in both
|
||
# states -- binding both edges leaves the label squeezed into what is left.
|
||
for index in range(0, len(pairs), 2):
|
||
first, second = pairs[index], pairs[index + 1]
|
||
if first[0] == second[0]:
|
||
raise SystemExit("a side-aware element anchors the same edge twice")
|
||
if "undefined" not in (first[1] + first[2]) or "undefined" not in (second[1] + second[2]):
|
||
raise SystemExit("a side-aware element never releases an anchor")
|
||
PY
|
||
|
||
# ── 4. The close button is reachable and identifiable ───────────────────────
|
||
python3 - "$shell_ui" <<'PY' || fail 'the close button is mouse-and-sight only'
|
||
import re
|
||
import sys
|
||
|
||
text = open(sys.argv[1], encoding="utf-8").read()
|
||
block = re.search(r'Rectangle \{\s*\n\s*id: closeButton(.*?)\n \}\n', text, re.S)
|
||
if not block:
|
||
raise SystemExit("there is no close button")
|
||
body = block.group(1)
|
||
for needle in (
|
||
"activeFocusOnTab: true",
|
||
"Accessible.role: Accessible.Button",
|
||
'Accessible.name: "Close Settings"',
|
||
"Keys.onReturnPressed: ShellState.closeSettings()",
|
||
"Keys.onSpacePressed: ShellState.closeSettings()",
|
||
"onClicked: ShellState.closeSettings()",
|
||
):
|
||
if needle not in body:
|
||
raise SystemExit(f"the close button is missing {needle}")
|
||
# A focused button that looks identical to an unfocused one is not keyboard
|
||
# operable in any useful sense.
|
||
if "activeFocus" not in body:
|
||
raise SystemExit("the close button has no visible focus treatment")
|
||
PY
|
||
|
||
# It is the only button in the bar.
|
||
[[ "$(rg -c 'Accessible.role: Accessible.Button' "$shell_ui")" == "1" ]] \
|
||
|| fail 'the titlebar has grown a second button'
|
||
|
||
# ── 5. The window is still movable and still closable ───────────────────────
|
||
rg -Fq 'startSystemMove()' "$shell_ui" \
|
||
|| fail 'the titlebar cannot drag the window'
|
||
python3 - "$shell_ui" <<'PY' || fail 'Escape no longer closes the window'
|
||
import re
|
||
import sys
|
||
|
||
text = open(sys.argv[1], encoding="utf-8").read()
|
||
shortcut = re.search(r'Shortcut \{(.*?)\}', text, re.S)
|
||
if not shortcut or 'sequence: "Escape"' not in shortcut.group(1):
|
||
raise SystemExit("there is no Escape shortcut")
|
||
if 'ShellState.closeSettings()' not in shortcut.group(1):
|
||
raise SystemExit("Escape does not close settings")
|
||
# It is declared on the shell, not inside the titlebar, so hiding the bar
|
||
# cannot take it away.
|
||
if text.index("Shortcut {") < text.index("id: titlebar"):
|
||
raise SystemExit("the Escape shortcut is declared before the shell content")
|
||
titlebar = re.search(r'Rectangle \{\s*\n\s*id: titlebar(.*?)\n \}\n', text, re.S)
|
||
if titlebar and "Shortcut" in titlebar.group(1):
|
||
raise SystemExit("Escape is scoped inside the titlebar, so hiding the bar removes it")
|
||
PY
|
||
|
||
# ── The preference exists and is reachable ──────────────────────────────────
|
||
python3 - "$schema" <<'PY' || fail 'the panamaTitlebar schema entry is missing or malformed'
|
||
import re
|
||
import sys
|
||
|
||
text = open(sys.argv[1], encoding="utf-8").read()
|
||
block = re.search(r"\{\s*\n\s*key:\s*\"panamaTitlebar\".*?\n\s{8}\}", text, re.S)
|
||
if not block:
|
||
raise SystemExit("panamaTitlebar is not in the schema")
|
||
body = block.group(0)
|
||
for needle in ('type: "bool"', "def: true", 'group: "titlebar"'):
|
||
if needle not in body:
|
||
raise SystemExit(f"panamaTitlebar is missing {needle}")
|
||
if "internal: true" in body:
|
||
raise SystemExit("panamaTitlebar is internal, so nobody can turn it off")
|
||
PY
|
||
|
||
rg -Fq 'setting: "panamaTitlebar"' "$appearance" \
|
||
|| fail 'Appearance does not expose the titlebar toggle'
|
||
rg -Fq 'Super+Q closes' "$schema" \
|
||
|| fail 'the toggle does not say what happens once the titlebar is gone'
|
||
|
||
printf 'settings titlebar contract: PASS\n'
|