203 lines
9.3 KiB
Bash
Executable File
203 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The sidebar search indexes the schema, not the page names.
|
|
#
|
|
# Before this, the field filtered twelve page labels, so "gaps", "wallpaper",
|
|
# and "screenshot" all returned nothing on an app that has all three. That is
|
|
# the single clearest way a settings app feels smaller than it is, and it
|
|
# regresses invisibly: nothing breaks, results just quietly stop appearing.
|
|
#
|
|
# This pins that every schema entry is reachable by its own label, that
|
|
# shortcuts are searchable by what they do, and that results route to a page
|
|
# that actually exists.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
harness="$repo_dir/config/dot/quickshell/settings-search-harness.qml"
|
|
|
|
fail() {
|
|
printf 'settings search contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
qs_for_harness() {
|
|
qs -p "$harness" "$@"
|
|
}
|
|
|
|
cleanup() {
|
|
qs_for_harness kill >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
qs_for_harness --daemonize >/dev/null
|
|
for _ in $(seq 1 40); do
|
|
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-search-test$' && break
|
|
sleep 0.1
|
|
done
|
|
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-search-test$' || fail 'test IPC target did not start'
|
|
|
|
find_top() {
|
|
qs_for_harness ipc call settings-search-test find "$1"
|
|
}
|
|
|
|
# ── An empty query is navigation, not a search ───────────────────────────────
|
|
[[ "$(find_top '' | jq -r .count)" == "0" ]] || fail 'an empty query returned results'
|
|
# Whitespace is an empty query wearing a hat. Tokenizing it produces no tokens,
|
|
# which must mean "no search" rather than "every setting matches nothing".
|
|
[[ "$(find_top ' ' | jq -r .count)" == "0" ]] || fail 'a whitespace query returned results'
|
|
|
|
# ── Real settings are findable by what they are ──────────────────────────────
|
|
while IFS='|' read -r query expect_label expect_page; do
|
|
result="$(find_top "$query")"
|
|
got_label="$(jq -r .top <<<"$result")"
|
|
got_page="$(jq -r .topPage <<<"$result")"
|
|
[[ "$got_label" == "$expect_label" ]] \
|
|
|| fail "searching '$query' put '$got_label' first, expected '$expect_label'"
|
|
[[ "$got_page" == "$expect_page" ]] \
|
|
|| fail "searching '$query' routes to '$got_page', expected '$expect_page'"
|
|
done <<'CASES'
|
|
wallpaper|Wallpaper|appearance
|
|
blur|Blur|appearance
|
|
timezone|Timezone|datetime
|
|
repeat delay|Repeat delay|shortcuts
|
|
system health|System Health|services
|
|
doctor|Copy health report|services
|
|
lock screen background|Lock screen background|appearance
|
|
password field|Password field|appearance
|
|
slideshow|Wallpaper mode|appearance
|
|
shuffle|Shuffle|appearance
|
|
per-display wallpaper|Per-display wallpaper|appearance
|
|
arrange displays|Arrange displays|displays
|
|
monitor position|Monitor position|displays
|
|
primary display|Primary display|displays
|
|
themes|Themes|appearance
|
|
theme editor|Theme editor|appearance
|
|
dark mode|Dark mode|appearance
|
|
catppuccin|Catppuccin|appearance
|
|
gruvbox|Gruvbox|appearance
|
|
video wallpaper|Video wallpaper|appearance
|
|
titlebar|Titlebar on Panama windows|appearance
|
|
CASES
|
|
|
|
! rg -Fq 'Startup & Services' "$repo_dir/config/dot/quickshell/services/SettingsSearch.qml" \
|
|
|| fail 'search index still uses the retired Startup & Services name'
|
|
|
|
# ── Words, not one contiguous substring ──────────────────────────────────────
|
|
#
|
|
# The 21 cases above are all single-token or exactly-adjacent, and they were
|
|
# passing before tokenization -- they are here to prove tokenizing did not move
|
|
# what was already right. These are the ones that were returning nothing at all:
|
|
# every word is present in the index, just not adjacent and not in that order.
|
|
#
|
|
# "wifi password" is the sharpest: the words live in two different fields of the
|
|
# same entry, so no substring of any haystack contains the query as typed.
|
|
while IFS='|' read -r query expect_label expect_page; do
|
|
result="$(find_top "$query")"
|
|
got_label="$(jq -r .top <<<"$result")"
|
|
got_page="$(jq -r .topPage <<<"$result")"
|
|
[[ "$got_label" == "$expect_label" ]] \
|
|
|| fail "searching '$query' put '$got_label' first, expected '$expect_label'"
|
|
[[ "$got_page" == "$expect_page" ]] \
|
|
|| fail "searching '$query' routes to '$got_page', expected '$expect_page'"
|
|
done <<'TOKENS'
|
|
log out|Log out|power
|
|
metered|Metered connection|connectivity
|
|
ethernet|Ethernet|connectivity
|
|
gestures|Gestures|mouse
|
|
saved networks|Saved networks|connectivity
|
|
hidden network|Hidden network|connectivity
|
|
static ip|Static IP address|connectivity
|
|
color filter|Color filter|accessibility
|
|
custom shortcut|Custom shortcut|shortcuts
|
|
application window rules|Application window rules|tiling
|
|
window rules|Window rules|tiling
|
|
do not disturb|Do Not Disturb|notifications
|
|
four-finger swipe|Four-finger swipe|mouse
|
|
TOKENS
|
|
|
|
# Every word must match, in any order and in any field. Order was the
|
|
# accidental part of substring matching, and it was doing the most damage.
|
|
for query in 'wifi password' 'password wifi'; do
|
|
[[ "$(find_top "$query" | jq -r .count)" != "0" ]] \
|
|
|| fail "searching '$query' found nothing, on a page that shows the Wi-Fi password"
|
|
[[ "$(find_top "$query" | jq -r .topPage)" == "connectivity" ]] \
|
|
|| fail "searching '$query' did not lead with a network result"
|
|
done
|
|
|
|
# AND, not OR. A query holding a word the index does not have anywhere must
|
|
# return nothing, or tokenizing has only made the field louder.
|
|
[[ "$(find_top 'wallpaper zzzznotathing' | jq -r .count)" == "0" ]] \
|
|
|| fail 'a query containing an unmatchable word still returned results'
|
|
|
|
# ── A result may name the tab it lives on ────────────────────────────────────
|
|
#
|
|
# Landing on Appearance's Themes tab after searching "theme editor" is the
|
|
# search half-working: the page is right and the thing searched for is behind
|
|
# another click. Results that name no section route exactly as before, which is
|
|
# every result the schema produces.
|
|
[[ "$(find_top 'theme editor' | jq -r .topSection)" == "editor" ]] \
|
|
|| fail 'the theme editor result does not name the tab it lives on'
|
|
[[ "$(find_top 'video wallpaper' | jq -r .topSection)" == "background" ]] \
|
|
|| fail 'the video wallpaper result does not name the tab it lives on'
|
|
[[ "$(find_top 'timezone' | jq -r .topSection)" == "" ]] \
|
|
|| fail 'a result on a page with no tabs still names a section'
|
|
|
|
# And the sidebar has to consume it, or the field is decorative.
|
|
sidebar="$repo_dir/config/dot/quickshell/modules/settings/SettingsSidebar.qml"
|
|
rg -Fq 'ShellState.openSettingsSection' "$sidebar" \
|
|
|| fail 'the sidebar never opens a result at its section'
|
|
rg -Fq 'root.pageRequested' "$sidebar" \
|
|
|| fail 'the sidebar lost the plain page route, which most results still use'
|
|
|
|
# ── Shortcuts are searchable by what they do ─────────────────────────────────
|
|
[[ "$(find_top screenshot | jq -r .topPage)" == "shortcuts" ]] \
|
|
|| fail 'searching a shortcut description did not route to the shortcuts page'
|
|
|
|
# ── Every non-internal schema entry is reachable by its own label ────────────
|
|
# A setting that cannot be found by typing its name is a setting the user
|
|
# cannot discover, which is the failure this whole page structure exists to fix.
|
|
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
|
missing=0
|
|
while IFS= read -r label; do
|
|
[[ -n "$label" ]] || continue
|
|
count="$(find_top "$label" | jq -r .count)"
|
|
if [[ "$count" == "0" ]]; then
|
|
printf 'settings search contract: no result for schema label "%s"\n' "$label" >&2
|
|
missing=$((missing + 1))
|
|
fi
|
|
done < <(python3 - "$schema" <<'EXTRACT'
|
|
import re, sys
|
|
# Entries marked `internal` are shell state the user never edits, so they are
|
|
# deliberately excluded from search. Extract only the user-facing labels.
|
|
text = open(sys.argv[1]).read()
|
|
for block in re.findall(r"\{\s*\n\s+key:.*?\n\s{8}\}", text, re.S):
|
|
if "internal: true" in block:
|
|
continue
|
|
match = re.search(r'label: "([^"]+)"', block)
|
|
if match:
|
|
print(match.group(1))
|
|
EXTRACT
|
|
)
|
|
[[ "$missing" -eq 0 ]] || fail "$missing schema label(s) are not findable by search"
|
|
|
|
# ── Results only ever route to leaves the shell can open ─────────────────────
|
|
# A search result carries a leaf id, never a category: it has to land on the
|
|
# exact tab holding the setting, not on whichever tab that category opens
|
|
# first. SettingsRoutes is where the leaves are declared -- a tabless category
|
|
# is one, and so is every tab.
|
|
routes="$repo_dir/config/dot/quickshell/services/SettingsRoutes.qml"
|
|
leaves="$( {
|
|
grep -oE '\{ page: "[a-z-]+", label: "[^"]*", icon: "[^"]*", tabs: \[\] \}' "$routes"
|
|
grep -oE '\{ page: "[a-z-]+", label: "[^"]*" \}' "$routes"
|
|
} | sed -E 's/\{ page: "([a-z-]+)".*/\1/' | sort -u)"
|
|
[[ -n "$leaves" ]] || fail 'no leaf pages could be read from SettingsRoutes, so this proves nothing'
|
|
for query in wallpaper blur timezone screenshot pointer lock volume gaps dictation; do
|
|
page="$(find_top "$query" | jq -r .topPage)"
|
|
grep -qx "$page" <<<"$leaves" || fail "search routed '$query' to '$page', which is not a page anyone can land on"
|
|
done
|
|
|
|
trap - EXIT
|
|
cleanup
|
|
printf 'settings search contract: PASS\n'
|