Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -24,6 +24,9 @@ set -uo pipefail
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
manual_dir="$repo_dir/config/dot/quickshell/manual"
|
||||
page="$repo_dir/config/dot/quickshell/modules/settings/ManualPage.qml"
|
||||
# The chapter list and the titles live in one place that both the reader and
|
||||
# About's Manual card instantiate, so neither can drift from the other.
|
||||
contents="$repo_dir/config/dot/quickshell/modules/settings/ManualChapters.qml"
|
||||
routes="$repo_dir/config/dot/quickshell/services/SettingsRoutes.qml"
|
||||
shell_ui="$repo_dir/config/dot/quickshell/modules/settings/SettingsShell.qml"
|
||||
qmldir="$repo_dir/config/dot/quickshell/modules/settings/qmldir"
|
||||
@@ -40,16 +43,18 @@ note() { findings+=("$1"); }
|
||||
mapfile -t on_disk < <(find "$manual_dir" -maxdepth 1 -name '*.md' -printf '%f\n' | sort)
|
||||
(( ${#on_disk[@]} > 0 )) || note 'the manual has no chapters'
|
||||
|
||||
mapfile -t listed < <(grep -oE 'file: "[^"]+\.md"' "$page" | sed 's/file: "//; s/"//' | sort)
|
||||
(( ${#listed[@]} > 0 )) || note 'the manual page lists no chapters'
|
||||
[[ -r "$contents" ]] \
|
||||
|| note 'ManualChapters.qml is missing, so neither the reader nor About knows what the chapters are'
|
||||
mapfile -t listed < <(grep -oE 'file: "[^"]+\.md"' "$contents" | sed 's/file: "//; s/"//' | sort)
|
||||
(( ${#listed[@]} > 0 )) || note 'the chapter list names no chapters'
|
||||
|
||||
for file in "${on_disk[@]}"; do
|
||||
printf '%s\n' "${listed[@]}" | grep -qx "$file" \
|
||||
|| note "$file exists but the manual page never shows it"
|
||||
|| note "$file exists but the chapter list never names it"
|
||||
done
|
||||
for file in "${listed[@]}"; do
|
||||
[[ -r "$manual_dir/$file" ]] \
|
||||
|| note "the manual page lists $file, which does not exist, so that chapter renders an error"
|
||||
|| note "the chapter list names $file, which does not exist, so that chapter renders an error"
|
||||
done
|
||||
|
||||
# A chapter that is only a heading is a chapter somebody forgot to write.
|
||||
@@ -60,6 +65,43 @@ for file in "${on_disk[@]}"; do
|
||||
|| note "$file does not begin with a heading, so it has no title of its own"
|
||||
done
|
||||
|
||||
# ── 1b. The tab titles come from the files ───────────────────────────────────
|
||||
#
|
||||
# The page carried a comment saying the title shown is the first heading of
|
||||
# each chapter, "so a chapter cannot be renamed in one place and not the
|
||||
# other". It was not true: the titles were a second copy, written beside the
|
||||
# filenames, and the two agreed only because nobody had renamed anything yet.
|
||||
# A comment that describes a property the code does not have is worse than no
|
||||
# comment, because it is the reason the next person does not check.
|
||||
#
|
||||
# So: no authored label beside a chapter file, and something that actually
|
||||
# reads a leading heading out of one.
|
||||
python3 - "$contents" <<'PY' || note 'the chapter list still carries hand-written titles beside the filenames, so a renamed heading and a tab label can disagree'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
block = re.search(r'chapters:\s*\[(.*?)\n \]', text, re.S)
|
||||
if not block:
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(1 if re.search(r'label:\s*"', block.group(1)) else 0)
|
||||
PY
|
||||
grep -qE '\^#' "$contents" \
|
||||
|| note 'nothing reads a leading heading, so the chapter titles cannot be coming from the files'
|
||||
grep -q 'Quickshell.shellDir + "/manual/"' "$contents" \
|
||||
|| note 'the chapter titles are not read through the shell directory, so they would break on a clone elsewhere'
|
||||
grep -q 'ManualChapters {' "$page" \
|
||||
|| note 'the manual reader keeps its own idea of what the chapters are'
|
||||
|
||||
# And the titles the files offer have to be usable as tab labels: a first line
|
||||
# that is a paragraph would render a tab strip nobody can read.
|
||||
for file in "${on_disk[@]}"; do
|
||||
heading="$(head -1 "$manual_dir/$file" | sed 's/^#\+[[:space:]]*//')"
|
||||
[[ -n "$heading" ]] || note "$file has an empty first heading, so its tab would have no name"
|
||||
(( ${#heading} <= 40 )) \
|
||||
|| note "$file's first heading is ${#heading} characters; it is the tab label, so it has to be a title"
|
||||
done
|
||||
|
||||
# ── 2 & 3. How it renders ────────────────────────────────────────────────────
|
||||
|
||||
grep -q 'textFormat: Text.MarkdownText' "$page" \
|
||||
@@ -68,6 +110,75 @@ grep -q 'root.chapters\[root.current\]' "$page" \
|
||||
|| note 'the page does not render one chapter at a time; a single long Text goes blank rather than erroring'
|
||||
grep -q 'onLinkActivated' "$page" \
|
||||
|| note 'links in the manual do nothing when clicked'
|
||||
|
||||
# ── 3b. A link to a settings page opens the settings page ────────────────────
|
||||
#
|
||||
# The whole reason the manual is rendered inside Settings rather than in a
|
||||
# browser is that a chapter can point at a page and have that mean something.
|
||||
# Every link went to Qt.openUrlExternally, so "open Displays" handed a
|
||||
# panama:// URL to xdg-open, which has no handler for it: the click did
|
||||
# nothing, silently, which is the worst of the three possible outcomes.
|
||||
grep -q 'panama://settings/' "$page" \
|
||||
|| note 'the manual has no in-app link scheme, so a chapter cannot point at a settings page'
|
||||
python3 - "$page" <<'PY' || note 'the manual link handler does not route in-app links through ShellState.openSettings while still sending everything else out of the desktop'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
|
||||
|
||||
def block_at(index: int) -> str:
|
||||
"""The braced body starting at the first { on or after index."""
|
||||
start = text.index("{", index)
|
||||
depth = 0
|
||||
for position in range(start, len(text)):
|
||||
if text[position] == "{":
|
||||
depth += 1
|
||||
elif text[position] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[start:position + 1]
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
# Either shape is fine: the branch written inline, or delegated to a named
|
||||
# function on the page. What matters is what is on the path, so the path is
|
||||
# followed rather than one line being read and guessed at.
|
||||
handler = re.search(r'onLinkActivated:\s*link\s*=>\s*(?P<tail>.)', text)
|
||||
if not handler:
|
||||
raise SystemExit(1)
|
||||
if handler.group("tail") == "{":
|
||||
body = block_at(handler.start("tail"))
|
||||
else:
|
||||
called = re.match(r'(?:root\.)?([A-Za-z][A-Za-z0-9]*)\(', text[handler.start("tail"):])
|
||||
if not called:
|
||||
raise SystemExit(1)
|
||||
declaration = re.search(rf'function {called.group(1)}\s*\(', text)
|
||||
if not declaration:
|
||||
raise SystemExit(1)
|
||||
body = block_at(declaration.end())
|
||||
|
||||
raise SystemExit(0 if "ShellState.openSettings" in body
|
||||
and "Qt.openUrlExternally" in body
|
||||
and ("panama://settings/" in body or "linkScheme" in body) else 1)
|
||||
PY
|
||||
|
||||
# Every in-app link a chapter actually writes must name a page that exists.
|
||||
# A typo here is invisible: the scheme matches, the handler fires, and
|
||||
# openSettings resolves an unknown id to Home -- so the link works, just not
|
||||
# the way the sentence around it promised.
|
||||
leaves="$( {
|
||||
grep -oE '\{ page: "[a-z-]+", label: "[^"]*", icon: "[^"]*", tabs: \[\] \}' "$routes"
|
||||
grep -oE '\{ page: "[a-z-]+", label: "[^"]*" \}' "$routes"
|
||||
sed -n '/property var hiddenLeaves:/,/\]/p' "$routes" | grep -oE 'page: "[a-z-]+"'
|
||||
} | sed -E 's/.*page: "([a-z-]+)".*/\1/' | sort -u)"
|
||||
[[ -n "$leaves" ]] || note 'no leaves could be read from SettingsRoutes, so in-app manual links prove nothing'
|
||||
while read -r target; do
|
||||
[[ -n "$target" ]] || continue
|
||||
grep -qx "$target" <<<"$leaves" \
|
||||
|| note "a chapter links to panama://settings/$target, which is not a page anyone can land on"
|
||||
done < <(grep -rhoE 'panama://settings/[a-z-]+' "$manual_dir" \
|
||||
| sed -E 's|panama://settings/||' | sort -u)
|
||||
grep -q 'onLoadFailed' "$page" \
|
||||
|| note 'a chapter that cannot be read fails silently instead of saying so'
|
||||
|
||||
@@ -80,18 +191,31 @@ grep -q '\.\./\.\./\.\.' "$page" \
|
||||
&& note 'the manual path walks upward out of the shell directory, which is only correct by accident'
|
||||
|
||||
# ── 4. Registered everywhere a settings page has to be ───────────────────────
|
||||
# The manual is a tab of the System category rather than a sidebar row of its
|
||||
# own -- it is reference material, not a control surface, and it belongs beside
|
||||
# About for the same reason. SettingsRoutes is what makes it reachable at all:
|
||||
# a leaf missing from the taxonomy cannot be opened, searched, or linked to.
|
||||
# The manual is a routable leaf with no tab of its own. It was a tab of System
|
||||
# until the strip reached ten and had to come back down to eight, and it is the
|
||||
# right one to lose: it is reference material rather than a control surface, so
|
||||
# it is opened deliberately -- from About's Manual card, from a deep link, from
|
||||
# a search result -- rather than found by scanning a row of tabs.
|
||||
#
|
||||
# That makes the taxonomy entry the only thing holding it up. A leaf missing
|
||||
# from SettingsRoutes cannot be opened, searched, or linked to, and because it
|
||||
# draws no tab anywhere, nothing on screen would show it had gone.
|
||||
|
||||
grep -q '{ page: "manual", label: ' "$routes" || note 'the manual is not a leaf in SettingsRoutes, so nothing can navigate to it'
|
||||
python3 - "$routes" <<'PY' || note 'the manual is no longer a tab of the System category, so it has drifted out of the group it belongs to'
|
||||
import re, sys
|
||||
python3 - "$routes" <<'PY' || note 'the manual is not a hidden leaf in SettingsRoutes, so nothing can navigate to it'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
block = re.search(r'\{ page: "system",.*?tabs: \[(.*?)\] \}', text, re.S)
|
||||
raise SystemExit(0 if block and '{ page: "manual"' in block.group(1) else 1)
|
||||
block = re.search(r'hiddenLeaves:\s*\[(.*?)\]', text, re.S)
|
||||
raise SystemExit(0 if block and re.search(r'page: "manual"', block.group(1)) else 1)
|
||||
PY
|
||||
python3 - "$routes" <<'PY' || note 'the manual is a System tab again, taking back a slot the eight-tab strip does not have'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
block = re.search(r'\{ page: "system",.*?tabs: \[(.*?)\n \] \}', text, re.S)
|
||||
raise SystemExit(1 if block and '{ page: "manual"' in block.group(1) else 0)
|
||||
PY
|
||||
grep -q 'case "manual": return manualPage;' "$shell_ui" || note 'SettingsShell does not route to the manual'
|
||||
grep -q 'Component { id: manualPage; ManualPage {} }' "$shell_ui" || note 'SettingsShell never declares the manual component'
|
||||
@@ -102,6 +226,21 @@ grep -q '^ManualPage 1.0 ManualPage.qml$' "$qmldir" || note 'ManualPage is not r
|
||||
grep -q 'SettingsRoutes.resolve(' "$state" || note 'ShellState does not resolve pages through SettingsRoutes, so openSettings("manual") has no defined destination'
|
||||
grep -q 'page: "manual"' "$search" || note 'the manual is not searchable from the settings search box'
|
||||
|
||||
# With no tab of its own, About's Manual card is the only place the manual is
|
||||
# offered rather than looked up. If that card stops opening it, the page is
|
||||
# still reachable in principle and undiscoverable in practice.
|
||||
about="$repo_dir/config/dot/quickshell/modules/settings/AboutPage.qml"
|
||||
if [[ -r "$about" ]]; then
|
||||
grep -q 'openSettings("manual"' "$about" \
|
||||
|| note 'About does not open the manual, which is now the only place it is offered rather than searched for'
|
||||
grep -q 'ManualChapters {' "$about" \
|
||||
|| note 'About keeps its own idea of what the chapters are, so its card and the reader can disagree'
|
||||
grep -qE 'file: "[^"]+\.md"' "$about" \
|
||||
&& note 'About names chapter files itself instead of reading the shared list'
|
||||
else
|
||||
note 'AboutPage.qml is missing, so the manual has nowhere to be opened from'
|
||||
fi
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'manual contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
|
||||
Reference in New Issue
Block a user