Make Shell a category, the bar legible, and the dock a real dock
Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling, Workspaces — the home for everything Quickshell draws. The settings- management cluster moves to System as Sync & Backup, Appearance's Shell tab dissolves, and 24-hour time finally lives on Date & Time, which always owned it. The bar gets what it never had: a way to survive the wallpaper. A second neutral text family (follow theme, or forced light or dark), a one-layer shadow under every glyph, and a gradient scrim for wallpapers nothing else survives — all off by default, pixel-identical until asked. Widgets earn toggles (weather, media, clipboard, calendar countdown), the vitals cluster stops leaving a dead pill behind, and Control Center's sections learn to step aside. The dock graduates from MVP: a context menu with window rows, pin, unpin, quit and new-window; scroll an icon to cycle its windows; drag to reorder on the dock itself; hover previews with one-shot captures; and "Add App to Dock" in the launcher. Three real bugs died en route — menus that slid away with the autohide, a readonly-property crash on every menu open, and a drag that drifted half a slot per icon on side docks. The pinned-apps editor in Settings becomes a drag strip. 166 contracts; the full suite is green except two live display and switcher tests that cannot run behind a locked session — re-verified on unlock. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Every other surface the shell draws sits on a ground the theme chose. The bar
|
||||
# sits on the wallpaper, which the theme has never seen, so it is the one place
|
||||
# where a palette that is correct can still be unreadable. The Shell › Bar page
|
||||
# exists to fix that, and this contract pins the three halves of it that a
|
||||
# refactor can quietly undo:
|
||||
#
|
||||
# 1. The bar has a neutral family of its own (barFg/barFgDim/barFgMuted) and
|
||||
# every bar widget binds to it. A widget left on Theme.fg is invisible on
|
||||
# the exact wallpaper the user turned the tone control on for.
|
||||
# 2. The scrim and the shadow are PREFERENCE-DRIVEN. Both were hardcoded true
|
||||
# at one point during the build, which is not a cosmetic slip: it forces a
|
||||
# dark band and a whole extra layer on everyone, including the people whose
|
||||
# wallpaper never needed either.
|
||||
# 3. Turning a widget off removes it, and turning all of a widget's readouts
|
||||
# off removes the pill rather than leaving a padded gap reporting nothing.
|
||||
#
|
||||
# Static checks only: no compositor, no shell, nothing read from the live
|
||||
# desktop.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
shell_dir="$repo_dir/config/dot/quickshell"
|
||||
theme="$shell_dir/config/Theme.qml"
|
||||
settings="$shell_dir/config/Settings.qml"
|
||||
bar="$shell_dir/modules/bar/Bar.qml"
|
||||
vitals="$shell_dir/modules/bar/VitalsWidget.qml"
|
||||
|
||||
fail() {
|
||||
printf 'bar visibility contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The thirteen files that carry neutral bar text or glyphs. Two of them live
|
||||
# outside modules/bar -- the clipboard button and the focus indicator are drawn
|
||||
# into the bar by Bar.qml, so they answer to the bar's tone like the rest.
|
||||
bar_widgets=(
|
||||
"$shell_dir/modules/bar/ActivityIndicator.qml"
|
||||
"$shell_dir/modules/bar/AgentUsageWidget.qml"
|
||||
"$shell_dir/modules/bar/CalendarIndicator.qml"
|
||||
"$shell_dir/modules/bar/Clock.qml"
|
||||
"$shell_dir/modules/bar/MediaWidget.qml"
|
||||
"$shell_dir/modules/bar/StatusCluster.qml"
|
||||
"$shell_dir/modules/bar/StatusGlyph.qml"
|
||||
"$shell_dir/modules/bar/VitalsField.qml"
|
||||
"$shell_dir/modules/bar/WallpaperIndicator.qml"
|
||||
"$shell_dir/modules/bar/WeatherWidget.qml"
|
||||
"$shell_dir/modules/bar/Workspaces.qml"
|
||||
"$shell_dir/modules/clipboard/ClipboardWidget.qml"
|
||||
"$shell_dir/modules/focus/FocusIndicator.qml"
|
||||
)
|
||||
|
||||
for file in "$theme" "$settings" "$bar" "$vitals" "${bar_widgets[@]}"; do
|
||||
[[ -f "$file" ]] || fail "missing ${file#"$repo_dir/"}"
|
||||
done
|
||||
|
||||
# ── The bar's neutral family ────────────────────────────────────────────────
|
||||
# Left alone the family IS the fg family, by identity rather than by a copied
|
||||
# literal: the moment `theme` returns a hand-picked colour instead of root.fg,
|
||||
# the default bar stops following the theme and every custom palette is wrong
|
||||
# in the one place the user looks at most.
|
||||
python3 - "$theme" <<'PY' || fail 'the bar text tokens drifted from the fg family'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
|
||||
if 'DesktopPreferences.get("barTextTone")' not in text:
|
||||
raise SystemExit("Theme no longer reads barTextTone")
|
||||
|
||||
# token -> the fg role its "theme" branch must fall back to, unchanged.
|
||||
expected = {
|
||||
"barFg": "fg",
|
||||
"barFgDim": "fgDim",
|
||||
"barFgMuted": "fgMuted",
|
||||
}
|
||||
|
||||
for token, role in expected.items():
|
||||
block = re.search(
|
||||
r"readonly property color " + token + r":\s*\{(?P<body>.*?)\n \}",
|
||||
text,
|
||||
re.S,
|
||||
)
|
||||
if not block:
|
||||
raise SystemExit(f"Theme no longer defines {token}")
|
||||
body = re.sub(r"//.*", "", block.group("body"))
|
||||
|
||||
for tone in ("light", "dark"):
|
||||
if f'=== "{tone}"' not in body:
|
||||
raise SystemExit(f"{token} does not answer the {tone} tone")
|
||||
|
||||
# The fallthrough is the last return in the block, and it is the whole
|
||||
# promise of the default: follow theme means follow theme.
|
||||
returns = re.findall(r"return\s+([^;]+);", body)
|
||||
if not returns:
|
||||
raise SystemExit(f"{token} returns nothing")
|
||||
if returns[-1].strip() != f"root.{role}":
|
||||
raise SystemExit(
|
||||
f'{token} falls back to {returns[-1].strip()!r}, expected root.{role}'
|
||||
)
|
||||
|
||||
# The forced tones are anchored on one literal each and the two dims are MIXED
|
||||
# off it, so light and dark stay families rather than three unrelated colours
|
||||
# somebody has to keep in step by hand.
|
||||
for token in ("barFgDim", "barFgMuted"):
|
||||
block = re.search(
|
||||
r"readonly property color " + token + r":\s*\{(?P<body>.*?)\n \}",
|
||||
text,
|
||||
re.S,
|
||||
)
|
||||
body = block.group("body")
|
||||
if body.count("root.mix(root.barFg") != 2:
|
||||
raise SystemExit(f"{token} no longer derives both forced tones from barFg")
|
||||
PY
|
||||
|
||||
# ── Settings exposes what the page writes ───────────────────────────────────
|
||||
for key in barTextShadow barBackdrop showWeatherWidget showMediaWidget \
|
||||
showClipboardButton showCalendarCountdown; do
|
||||
rg -Fq "DesktopPreferences.get(\"$key\")" "$settings" \
|
||||
|| fail "Settings does not expose $key"
|
||||
done
|
||||
|
||||
# ── Every bar widget speaks in bar tones ────────────────────────────────────
|
||||
# Theme.alpha(Theme.fg, ...) is allowed: those are hover and separator FILLS
|
||||
# drawn against the widget's own pill, not text read against the wallpaper.
|
||||
# Semantic tones (warn/danger/accent/ok) are allowed for the same reason -- a
|
||||
# battery at 4% should be red whatever tone the neutrals were forced to.
|
||||
python3 - "${bar_widgets[@]}" <<'PY' || fail 'a bar widget still paints neutral text with the fg family'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
neutral = re.compile(r"Theme\.fg(?:Dim|Muted)?\b")
|
||||
fill = re.compile(r"Theme\.alpha\(\s*Theme\.fg(?:Dim|Muted)?\b")
|
||||
|
||||
for arg in sys.argv[1:]:
|
||||
path = Path(arg)
|
||||
text = re.sub(r"//.*", "", path.read_text(encoding="utf-8"))
|
||||
|
||||
if "Theme.barFg" not in text:
|
||||
raise SystemExit(f"{path.name} binds no text to the bar's own tone")
|
||||
|
||||
stripped = fill.sub("", text)
|
||||
leftover = neutral.findall(stripped)
|
||||
if leftover:
|
||||
raise SystemExit(
|
||||
f"{path.name} still uses {sorted(set(leftover))} for bar text; "
|
||||
"use barFg/barFgDim/barFgMuted so the tone control reaches it"
|
||||
)
|
||||
PY
|
||||
|
||||
# ── The scrim and the shadow are preferences, not decisions ─────────────────
|
||||
for binding in \
|
||||
'visible: Settings.barBackdrop' \
|
||||
'layer.enabled: Settings.barTextShadow' \
|
||||
'layer.effect: MultiEffect' \
|
||||
'shadowEnabled: true'; do
|
||||
rg -Fq "$binding" "$bar" || fail "Bar.qml is missing \`$binding\`"
|
||||
done
|
||||
|
||||
# The regression this catches actually happened: both landed as literal trues
|
||||
# during the build, so the scrim and the extra layer shipped to everybody
|
||||
# regardless of what the page said.
|
||||
for hardcoded in 'visible: true' 'layer.enabled: true'; do
|
||||
! rg -Fq "$hardcoded" "$bar" \
|
||||
|| fail "Bar.qml hardcodes \`$hardcoded\` instead of reading the preference"
|
||||
done
|
||||
|
||||
# One layer for the whole bar, not one per widget: the shadow is drawn under a
|
||||
# flattened copy of the content, so a widget added tomorrow picks it up without
|
||||
# opting in.
|
||||
[[ "$(rg -c 'layer.enabled' "$bar")" == "1" ]] \
|
||||
|| fail 'Bar.qml no longer flattens its content into exactly one shadow layer'
|
||||
|
||||
# The bar reserves its own height and nothing else. A computed zone here means
|
||||
# maximised windows either overlap the bar or leave a strip of wallpaper.
|
||||
rg -Fq 'exclusiveZone: Theme.barHeight' "$bar" \
|
||||
|| fail 'the bar no longer reserves exactly its own height'
|
||||
|
||||
# Nothing in the bar animates. It already repaints once a second for the clock;
|
||||
# anything that repaints continuously on top of that is a permanent GPU cost on
|
||||
# a surface that is always on screen. The backdrop in particular is a static
|
||||
# gradient by design.
|
||||
! rg -q '\b(Behavior|NumberAnimation|ColorAnimation|SequentialAnimation|ParallelAnimation|PropertyAnimation|AnimatedImage)\b' "$bar" \
|
||||
|| fail 'Bar.qml has grown an animation; the bar is always on screen and never animates'
|
||||
|
||||
# ── Widget gates ────────────────────────────────────────────────────────────
|
||||
# Each toggle is ANDed with the widget's own state condition rather than
|
||||
# replacing it, so switching one ON never conjures a pill with nothing in it.
|
||||
check_gate() {
|
||||
local file="$shell_dir/$1" needle="$2"
|
||||
rg -Fq "$needle" "$file" || fail "${1##*/} is missing \`$needle\`"
|
||||
}
|
||||
check_gate modules/bar/WeatherWidget.qml \
|
||||
'visible: Settings.showWeatherWidget && Weather.available'
|
||||
check_gate modules/bar/MediaWidget.qml \
|
||||
'visible: Settings.showMediaWidget && root.player !== null'
|
||||
check_gate modules/bar/CalendarIndicator.qml \
|
||||
'visible: Settings.showCalendarCountdown && CalendarAgenda.capsuleVisible'
|
||||
check_gate modules/clipboard/ClipboardWidget.qml \
|
||||
'visible: Settings.showClipboardButton'
|
||||
|
||||
# ── The vitals pill leaves when it has nothing to say ───────────────────────
|
||||
# An invisible child still occupies its Row, so gating the three fields alone
|
||||
# left a padded, empty pill sitting in the bar. The pill has to answer for
|
||||
# itself.
|
||||
rg -Fq 'visible: Settings.showCpu || Settings.showMemory || (Settings.showGpu && Vitals.gpuAvailable)' "$vitals" \
|
||||
|| fail 'the vitals pill does not disappear when all three readouts are off'
|
||||
for field in 'visible: Settings.showCpu' 'visible: Settings.showMemory' \
|
||||
'visible: Settings.showGpu && Vitals.gpuAvailable'; do
|
||||
rg -Fq "$field" "$vitals" || fail "the vitals row is missing \`$field\`"
|
||||
done
|
||||
|
||||
# ── Right-click lands where the toggles are ─────────────────────────────────
|
||||
# Both of these used to open the retired Desktop page. Whichever widget you
|
||||
# right-click, you should arrive at the card holding its own switch.
|
||||
rg -Fq 'ShellState.openSettings("bar")' "$vitals" \
|
||||
|| fail 'the vitals pill no longer jumps to Shell › Bar'
|
||||
rg -Fq 'ShellState.openSettings("bar")' "$shell_dir/modules/bar/AgentUsageWidget.qml" \
|
||||
|| fail 'the agent usage pill no longer jumps to Shell › Bar'
|
||||
|
||||
printf 'bar visibility contract: PASS (%d bar widgets on bar tones)\n' "${#bar_widgets[@]}"
|
||||
@@ -158,7 +158,7 @@ qml_package() {
|
||||
|
||||
# Provided by the base system or the shell itself; nothing installs these
|
||||
# separately, and listing them would be noise.
|
||||
QML_BASELINE='^(sh|bash|rm|test|systemd-inhibit|loginctl|timedatectl|gsettings|gapplication|systemctl|busctl)$'
|
||||
QML_BASELINE='^(sh|bash|rm|test|pkill|systemd-inhibit|loginctl|timedatectl|gsettings|gapplication|systemctl|busctl)$'
|
||||
|
||||
while IFS= read -r command_name; do
|
||||
[[ -n "$command_name" ]] || continue
|
||||
|
||||
@@ -94,6 +94,7 @@ package_for() {
|
||||
# x11 one cannot. desktop-packages declares it under that name.
|
||||
espanso) printf 'espanso-wayland' ;;
|
||||
rg) printf 'ripgrep' ;;
|
||||
cmp) printf 'diffutils' ;;
|
||||
xdg-mime|xdg-settings|xdg-open) printf 'xdg-utils' ;;
|
||||
update-desktop-database|desktop-file-validate) printf 'desktop-file-utils' ;;
|
||||
python3) printf 'python3' ;;
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
# centred while its reveal strip spans the whole edge -- so a region that
|
||||
# forgets the body's own offset lands somewhere the pointer is not, hover
|
||||
# drops on the frame the dock arrives, and it hides under a still cursor.
|
||||
# 8. The dock's own drag-to-reorder commits once, on release, and measures a
|
||||
# slot from a real icon rather than assuming one -- a DockItem is taller
|
||||
# than it is wide, so a constant is wrong on one of the two orientations.
|
||||
#
|
||||
# The geometry checks launch isolated shells against a temporary config. The
|
||||
# real settings are read to build them and never written.
|
||||
@@ -33,14 +36,14 @@ repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
shell_dir="$repo_dir/config/dot/quickshell"
|
||||
dock="$shell_dir/modules/dock/Dock.qml"
|
||||
body="$shell_dir/modules/dock/DockBody.qml"
|
||||
editor="$shell_dir/modules/settings/DockPinsEditor.qml"
|
||||
strip="$shell_dir/modules/settings/DockPinsStrip.qml"
|
||||
|
||||
fail() {
|
||||
printf 'dock position contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for path in "$dock" "$body" "$editor"; do
|
||||
for path in "$dock" "$body" "$strip"; do
|
||||
[[ -r "$path" ]] || fail "missing $path"
|
||||
done
|
||||
|
||||
@@ -57,12 +60,32 @@ grep -q 'implicitWidth: root.vertical ? ' "$dock" \
|
||||
grep -q 'wanted.length === 0' "$dock" \
|
||||
|| fail 'an empty screen list is not treated as every screen'
|
||||
|
||||
grep -q 'preventStealing: true' "$editor" \
|
||||
grep -q 'preventStealing: true' "$strip" \
|
||||
|| fail 'the drag grip does not set preventStealing, so the page will scroll instead of reordering'
|
||||
# The arrows are the keyboard-reachable path and predate the grip. A grip is not
|
||||
# a replacement for them.
|
||||
grep -q 'text: "↑"' "$editor" \
|
||||
|| fail 'the move-up button was removed, leaving no keyboard-reachable reorder'
|
||||
# A drag is not reachable from the keyboard, so the strip owes the keyboard its
|
||||
# own path. The old editor spelled it as ↑/↓ buttons; the strip spells it as
|
||||
# arrow keys on a focused icon. Either way it has to exist.
|
||||
grep -q 'Keys.onLeftPressed' "$strip" && grep -q 'Keys.onRightPressed' "$strip" \
|
||||
|| fail 'the pinned strip offers no keyboard-reachable reorder'
|
||||
grep -q 'activeFocusOnTab: true' "$strip" \
|
||||
|| fail 'a pinned icon cannot be reached by Tab, so the keyboard reorder is unreachable'
|
||||
|
||||
# ── 8. The dock's own drag ─────────────────────────────────────────────────
|
||||
# Dragging an icon along the dock moves the same pin the strip does, so it has
|
||||
# the same two ways to go wrong.
|
||||
|
||||
# One write per gesture. Committing per slot crossed rewrites settings.json a
|
||||
# dozen times for one drag, and every rewrite re-evaluates the model underneath
|
||||
# the gesture.
|
||||
[[ "$(grep -c 'DesktopPreferences.set("dockPinned"' "$body")" -eq 1 ]] \
|
||||
|| fail 'the dock commits its reorder more than once per gesture, or not through DesktopPreferences'
|
||||
|
||||
# A DockItem is taller than it is wide -- the running dots sit under the icon --
|
||||
# so a slot down a side dock is further than a slot across a bottom one. Reading
|
||||
# the pitch from the item that started the drag is what keeps both honest; a
|
||||
# constant here was wrong on one of the two orientations.
|
||||
grep -q 'signal dragStarted(real pitch)' "$shell_dir/modules/dock/DockItem.qml" \
|
||||
|| fail 'the dock drag assumes a slot size instead of measuring the item, so a side dock steps wrong'
|
||||
|
||||
# ── 5. Reordering keeps every entry, exactly once ───────────────────────────
|
||||
|
||||
|
||||
@@ -104,12 +104,12 @@ write_settings '{
|
||||
}'
|
||||
run_helper generate
|
||||
rg -Fq 'color = rgba(225, 226, 231, 1.0)' "$generated" || fail 'light solid mode does not match Theme.bg'
|
||||
rg -Fq 'inner_color = rgba(208, 213, 227, 0.85)' "$generated" || fail 'light password field does not match the themed fallback'
|
||||
rg -Fq 'inner_color = rgba(217, 218, 227, 0.85)' "$generated" || fail 'light password field does not match the themed fallback'
|
||||
rg -Fq 'font_color = rgba(55, 96, 191, 1.0)' "$generated" || fail 'light foreground does not match Theme.fg'
|
||||
rg -Fq 'outer_color = rgba(46, 125, 233, 0.9)' "$generated" || fail 'light focus ring does not match Theme.accent'
|
||||
rg -Fq 'check_color = rgba(46, 125, 233, 1.0)' "$generated" || fail 'light success color does not match Theme.accent'
|
||||
rg -Fq 'fail_color = rgba(245, 42, 101, 1.0)' "$generated" || fail 'light error color does not match Theme.red'
|
||||
rg -Fq 'foreground="##6172b0"' "$generated" || fail 'light placeholder markup retained the dark muted color'
|
||||
rg -Fq 'foreground="##848cb5"' "$generated" || fail 'light placeholder markup retained the dark muted color'
|
||||
rg -Fq 'foreground="##f52a65"' "$generated" || fail 'light failure markup retained the dark error color'
|
||||
rg -Fq 'blur_passes = 0' "$generated" || fail 'blur level zero did not disable passes'
|
||||
rg -Fq 'blur_size = 1' "$generated" || fail 'blur level zero did not use the safe size'
|
||||
@@ -118,17 +118,19 @@ if rg -q '^label \{' "$generated"; then
|
||||
fail 'hidden clock, date, and user labels were still generated'
|
||||
fi
|
||||
|
||||
# ── The focus ring follows the chosen accent, not just the scheme ───────────
|
||||
# A pinned blue literal only ever proves the DEFAULT accent renders correctly,
|
||||
# not that the helper actually reads accentName. Orchid is picked because its
|
||||
# hex is nowhere close to blue's in either scheme, so a helper that quietly
|
||||
# ignored accentName and kept emitting blue would be caught here.
|
||||
write_settings '{"accentName":"orchid","colorScheme":"dark"}'
|
||||
# ── The focus ring follows the active theme's accent, not just the scheme ──
|
||||
# A pinned blue literal only ever proves the DEFAULT accent renders correctly.
|
||||
# Since themes carry their accent pair, the helper reads the active profile's
|
||||
# accent; a custom orchid profile is used because its hex is nowhere close to
|
||||
# blue's, so a helper that quietly kept emitting the default would be caught.
|
||||
# (accentName alone can no longer disagree with the profile: every commit path
|
||||
# recomputes it from the active accent.)
|
||||
write_settings '{"colorScheme":"dark","accentName":"orchid","themeProfileId":"custom-orchid","themeProfiles":[{"id":"custom-orchid","name":"Orchid test","scheme":"dark","accent":"#c099ff","secondary":"#fca7ea","shipped":false}]}'
|
||||
run_helper generate
|
||||
rg -Fq 'outer_color = rgba(192, 153, 255, 0.9)' "$generated" || fail 'dark orchid accent did not drive the focus ring'
|
||||
rg -Fq 'check_color = rgba(192, 153, 255, 1.0)' "$generated" || fail 'dark orchid accent did not drive the success color'
|
||||
|
||||
write_settings '{"accentName":"orchid","colorScheme":"light"}'
|
||||
write_settings '{"colorScheme":"light","accentName":"orchid","themeProfileId":"custom-orchid-light","themeProfiles":[{"id":"custom-orchid-light","name":"Orchid light","scheme":"light","accent":"#7847bd","secondary":"#9854f1","shipped":false}]}'
|
||||
run_helper generate
|
||||
rg -Fq 'outer_color = rgba(120, 71, 189, 0.9)' "$generated" || fail 'light orchid accent did not drive the focus ring'
|
||||
rg -Fq 'check_color = rgba(120, 71, 189, 1.0)' "$generated" || fail 'light orchid accent did not drive the success color'
|
||||
|
||||
@@ -24,6 +24,7 @@ declare -A expected=(
|
||||
[open-clipboard]=clipboard
|
||||
[open-mission-control]=overview
|
||||
[open-settings]=settings
|
||||
[dock-add-app]=dock-pin
|
||||
[check-system-health]=health
|
||||
[toggle-dnd]=dnd
|
||||
[toggle-caffeine]=caffeine
|
||||
|
||||
@@ -137,8 +137,8 @@ assert_schema_and_redaction() {
|
||||
and (.summary.status | IN("healthy", "warning", "error"))
|
||||
and (.context.session | IN("hyprland", "other"))
|
||||
and (.context.versions | type == "array")
|
||||
and ([.checks[].id] | length == 29)
|
||||
and ([.checks[].id] | unique | length == 29)
|
||||
and ([.checks[].id] | length == 30)
|
||||
and ([.checks[].id] | unique | length == 30)
|
||||
and ([.checks[].status] | all(IN("ok", "warning", "error", "unconfigured")))' \
|
||||
>/dev/null <<<"$snapshot" || fail "invalid schema: $snapshot"
|
||||
[[ "$(jq -r '.checks[].id' <<<"$snapshot")" == "$expected_order" ]] \
|
||||
|
||||
@@ -63,8 +63,8 @@ done
|
||||
# contextual affordances must retain the original interaction and route to the
|
||||
# setting page that owns the controls.
|
||||
[[ -r "$dock_menu" ]] || fail 'dock has no contextual menu, so application actions cannot keep a final Dock settings action'
|
||||
grep -qF 'ShellState.openSettings("desktop")' "$dock_menu" \
|
||||
|| fail 'dock context menu does not open Desktop settings'
|
||||
grep -qF 'ShellState.openSettings("dock")' "$dock_menu" \
|
||||
|| fail 'dock context menu does not open Dock settings'
|
||||
grep -qF 'entry.actions' "$dock_menu" \
|
||||
|| fail 'dock context menu dropped application actions'
|
||||
actions_line="$(grep -nF 'entry.actions' "$dock_menu" | head -1 | cut -d: -f1)"
|
||||
|
||||
@@ -9,7 +9,7 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
pages=(Home MyHome Phone Displays Connectivity Sound Dictation Notifications ScreenIntelligence Health About)
|
||||
pages=(Home MyHome Phone Displays Connectivity Bar Dock ControlCenter Tiling Workspaces Sync Sound Dictation Notifications ScreenIntelligence Health About)
|
||||
for page in "${pages[@]}"; do
|
||||
page_file="$repo_dir/config/dot/quickshell/modules/settings/${page}Page.qml"
|
||||
[[ -f "$page_file" ]] || fail "${page}Page.qml is missing"
|
||||
@@ -50,12 +50,16 @@ PY
|
||||
home_page="$repo_dir/config/dot/quickshell/modules/settings/HomePage.qml"
|
||||
require_row "$home_page" ChoiceRow temperatureUnit
|
||||
require_row "$home_page" SliderRow weatherRefreshMinutes
|
||||
# vitalsIntervalMs moved to Appearance, beside the toggles it governs. It sat
|
||||
# on Home while showCpu/showMemory/showGpu sat on Appearance -- one concept
|
||||
# across two pages, which the ownership rule forbids and which made a search
|
||||
# for it open a page that did not contain it.
|
||||
appearance_page="$repo_dir/config/dot/quickshell/modules/settings/AppearancePage.qml"
|
||||
require_row "$appearance_page" SliderRow vitalsIntervalMs
|
||||
# vitalsIntervalMs sits beside the toggles it governs. It was on Home while
|
||||
# showCpu/showMemory/showGpu were on Appearance -- one concept across two
|
||||
# pages, which the ownership rule forbids and which made a search for it open
|
||||
# a page that did not contain it. Both landed on Bar when Appearance's Shell
|
||||
# tab dissolved: the vitals are bar content, not surface appearance.
|
||||
bar_page="$repo_dir/config/dot/quickshell/modules/settings/BarPage.qml"
|
||||
require_row "$bar_page" SliderRow vitalsIntervalMs
|
||||
for setting in showCpu showMemory showGpu; do
|
||||
require_row "$bar_page" ToggleRow "$setting"
|
||||
done
|
||||
|
||||
notifications_page="$repo_dir/config/dot/quickshell/modules/settings/NotificationsPage.qml"
|
||||
for setting in notificationTimeoutMs notificationTimeoutCriticalMs notificationHistoryLimit maxVisibleToasts; do
|
||||
@@ -307,7 +311,7 @@ shell_pid="$harness_pid"
|
||||
# four different categories, and the page the tab strip was introduced for.
|
||||
# Routing to a tab must land on that tab, not on whatever its category opens
|
||||
# first, which is the failure the SettingsRoutes resolution could introduce.
|
||||
pages=(home appearance displays connectivity my-home phone desktop sound dictation notifications screen-intelligence shortcuts services manual about)
|
||||
pages=(home appearance displays connectivity my-home phone bar dock control-center tiling workspaces sync sound dictation notifications screen-intelligence shortcuts services manual about)
|
||||
for page in "${pages[@]}"; do
|
||||
qs_for_test ipc call settings page "$page" >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
@@ -323,6 +327,17 @@ done
|
||||
qs_for_test ipc call settings page '__unsupported__' >/dev/null
|
||||
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "home" ]] || fail 'unsupported page did not fall back to Home'
|
||||
|
||||
# Desktop & Dock became five Shell tabs. Old Vicinae commands, shell history,
|
||||
# and muscle memory still hold the retired id, so it has to keep landing
|
||||
# somewhere sensible rather than falling back to Home.
|
||||
qs_for_test ipc call settings page desktop >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "bar" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "bar" ]] \
|
||||
|| fail 'the retired "desktop" id no longer resolves to the Bar tab'
|
||||
|
||||
/usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Settings" and .key == "I" and .modmask == 64)' >/dev/null \
|
||||
|| fail 'Super+I is not registered as Panama Settings'
|
||||
/usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Screen Intelligence" and .key == "S" and .modmask == 65)' >/dev/null \
|
||||
|
||||
@@ -29,8 +29,10 @@ hyprctl -j clients | jq -e '.[] | select(.title == "Settings" and .floating == f
|
||||
qs ipc call settings page displays >/dev/null
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "displays" ]] || fail 'Displays page did not route'
|
||||
|
||||
# The retired desktop id must keep routing — to bar, the Shell category's
|
||||
# first tab, which is where its content went.
|
||||
qs ipc call settings page desktop >/dev/null
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "desktop" ]] || fail 'Desktop page did not route'
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "bar" ]] || fail 'the retired desktop id did not route to bar'
|
||||
|
||||
address="$(hyprctl -j clients | jq -r '.[] | select(.title == "Settings") | .address')"
|
||||
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$address\" })" >/dev/null
|
||||
|
||||
@@ -88,7 +88,7 @@ for reason in ("root.manuallyPaused", "root.gamePaused", "root.batteryPaused"):
|
||||
|
||||
# The pause reaches mpv over its JSON IPC socket rather than by killing and
|
||||
# respawning the player, which would restart the video from the first frame.
|
||||
if 'JSON.stringify({ command: ["set_property", "pause", root.paused] })' not in stripped:
|
||||
if 'JSON.stringify({ "command": ["set_property", "pause", root.paused] })' not in stripped:
|
||||
raise SystemExit("pausing no longer goes over mpv's JSON IPC")
|
||||
if 'onPausedChanged: pauseSync.restart()' not in stripped:
|
||||
raise SystemExit("a change of pause state does not push to the player")
|
||||
@@ -122,7 +122,7 @@ rg -Fq 'mpvpaper' "$packages" \
|
||||
# reasoned about per output.
|
||||
rg -Fq 'playerRespawn.restart();' "$service" \
|
||||
|| fail 'a crashed player is not respawned'
|
||||
rg -Fq 'onOutputsChanged: if (root.active) playerRespawn.restart()' "$service" \
|
||||
rg -Fq 'onOutputSignatureChanged: if (root.active) playerRespawn.restart()' "$service" \
|
||||
|| fail 'a display hotplug does not respawn the players'
|
||||
|
||||
# ── 3. The hyprpaper handover ───────────────────────────────────────────────
|
||||
|
||||
@@ -22,7 +22,7 @@ set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
helper="$repo_dir/config/dot/quickshell/scripts/panama-project"
|
||||
desktop_page="$repo_dir/config/dot/quickshell/modules/settings/DesktopPage.qml"
|
||||
workspaces_page="$repo_dir/config/dot/quickshell/modules/settings/WorkspacesPage.qml"
|
||||
service="$repo_dir/config/dot/quickshell/services/Projects.qml"
|
||||
|
||||
findings=()
|
||||
@@ -156,9 +156,9 @@ grep -q 'hl.dsp.window.move' "$helper" \
|
||||
|
||||
# ── The page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
grep -q 'Projects.projects' "$desktop_page" \
|
||||
|| note 'the Desktop page does not list saved projects'
|
||||
grep -q 'confirmingProject' "$desktop_page" \
|
||||
grep -q 'Projects.projects' "$workspaces_page" \
|
||||
|| note 'the Workspaces page does not list saved projects'
|
||||
grep -q 'confirmingProject' "$workspaces_page" \
|
||||
|| note 'a project can be deleted without confirming'
|
||||
grep -q '"list"' "$service" \
|
||||
|| note 'the settings service never reads what is saved'
|
||||
|
||||
Reference in New Issue
Block a user