Fold thirty-one settings pages into fifteen categories with tabs
The sidebar was a flat scan of thirty-one rows; now it reads like a settings app. Multi-subject categories (Input, Network & Sharing, Applications, Users & Accounts, Privacy & Security, System) carry an Appearance-style tab strip above the page, drawn by the shell so the leaf pages themselves are untouched. The taxonomy lives in one new file, services/SettingsRoutes.qml; the sidebar, the strip, route validation, search breadcrumbs, and both generators derive from it. ShellState.settingsPage still holds leaf ids, so every deep link, IPC call, and search result keeps working — and now lands on the exact tab. Dictation moves out of Sound onto its own page under Input, with a handoff back to Sound for the microphone. The strip scrolls when System's nine tabs outgrow a tiled window. All 161 contracts pass. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -17,6 +17,10 @@ Usage:
|
||||
panama-settings-docs --check exit 1 if the committed copy is stale
|
||||
panama-settings-docs --stdout print without writing
|
||||
|
||||
Where each group of settings lives comes from SettingsSearch.qml's routing and
|
||||
SettingsRoutes.qml's taxonomy, so a page renamed or moved into another category
|
||||
is renamed here too rather than in a list somebody has to remember.
|
||||
|
||||
Parsing rather than importing: the schema is QML, there is no QML interpreter
|
||||
here, and a regex reader that FAILS LOUDLY when it stops recognising the file is
|
||||
better than a dependency on a shell that has to start a compositor.
|
||||
@@ -32,28 +36,50 @@ import sys
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||
SCHEMA = ROOT / "config/dot/quickshell/config/PreferenceSchema.qml"
|
||||
SEARCH = ROOT / "config/dot/quickshell/services/SettingsSearch.qml"
|
||||
ROUTES = ROOT / "config/dot/quickshell/services/SettingsRoutes.qml"
|
||||
OUTPUT = ROOT / "docs/settings.md"
|
||||
|
||||
# Page id -> the name a person sees in the sidebar.
|
||||
PAGE_TITLES = {
|
||||
"home": "Home", "appearance": "Appearance", "displays": "Displays",
|
||||
"connectivity": "Network & Devices", "home-phone": "Home & Phone",
|
||||
"desktop": "Desktop & Dock", "sound": "Sound",
|
||||
"notifications": "Notifications & Focus",
|
||||
"screen-intelligence": "Screen Intelligence", "shortcuts": "Keyboard",
|
||||
"mouse": "Mouse & Touchpad", "privacy": "Privacy & Security",
|
||||
"region": "Region & Language", "accounts": "Online Accounts",
|
||||
"accessibility": "Accessibility", "power": "Power & Lock",
|
||||
"datetime": "Date & Time", "applications": "Applications",
|
||||
"services": "System Health", "about": "About",
|
||||
"gaming": "Gaming",
|
||||
}
|
||||
CATEGORY = re.compile(
|
||||
r'\{\s*page:\s*"([a-z-]+)",\s*label:\s*"([^"]+)",\s*icon:\s*"[^"]*",\s*tabs:\s*\[([^\]]*)\]\s*\}',
|
||||
re.S)
|
||||
TAB = re.compile(r'\{\s*page:\s*"([a-z-]+)",\s*label:\s*"([^"]+)"\s*\}')
|
||||
|
||||
|
||||
class SchemaError(RuntimeError):
|
||||
"""The schema stopped looking the way this reader expects."""
|
||||
|
||||
|
||||
def read_titles():
|
||||
"""Leaf page id -> the name a person sees, from SettingsRoutes.
|
||||
|
||||
The sidebar is fifteen categories of tabs rather than a flat list, so a page
|
||||
is named the way a hit names it in search: "System › Storage" for a tab,
|
||||
"Displays" for a category that is a page by itself.
|
||||
"""
|
||||
text = ROUTES.read_text()
|
||||
block = re.search(r"readonly property var categories: \[(.*?)\n \]", text, re.S)
|
||||
if not block:
|
||||
raise SchemaError("could not find the categories array in SettingsRoutes.qml")
|
||||
titles = {}
|
||||
read = 0
|
||||
for page, label, tabs in CATEGORY.findall(block.group(1)):
|
||||
found = TAB.findall(tabs)
|
||||
read += 1 + len(found)
|
||||
if found:
|
||||
titles.update({tab: f"{label} › {tab_label}" for tab, tab_label in found})
|
||||
else:
|
||||
titles[page] = label
|
||||
# A category this reader stopped recognising would take its pages' names
|
||||
# with it, and the group that routed to one would then be documented as
|
||||
# living nowhere. Every `page:` in the array has to be accounted for.
|
||||
declared = len(re.findall(r'\bpage:\s*"', block.group(1)))
|
||||
if not titles or read != declared:
|
||||
raise SchemaError(
|
||||
f"read {read} of the {declared} pages in SettingsRoutes.qml; the "
|
||||
"categories array no longer looks the way this reader expects")
|
||||
return titles
|
||||
|
||||
|
||||
def read_routes():
|
||||
"""group -> page id, from SettingsSearch."""
|
||||
text = SEARCH.read_text()
|
||||
@@ -139,7 +165,7 @@ def read_entries():
|
||||
return entries
|
||||
|
||||
|
||||
def render(entries, routes):
|
||||
def render(entries, routes, titles):
|
||||
lines = [
|
||||
"# Settings reference",
|
||||
"",
|
||||
@@ -163,18 +189,19 @@ def render(entries, routes):
|
||||
if not visible:
|
||||
continue
|
||||
page = routes.get(group)
|
||||
# A routed page with no title used to fall back to the raw page id, so
|
||||
# A routed page with no name used to fall back to the raw page id, so
|
||||
# the group "gaming" documented itself as "Found on **gaming**" while
|
||||
# every other group named a real page. The staleness contract could not
|
||||
# see it: regenerating reproduced the same wrong file, so the copy was
|
||||
# current and wrong at once. Refusing to render is what makes the next
|
||||
# page added here impossible to miss.
|
||||
if page is not None and page not in PAGE_TITLES:
|
||||
# current and wrong at once. Refusing to render is what makes a group
|
||||
# routed at a page that no longer exists impossible to miss.
|
||||
if page is not None and page not in titles:
|
||||
raise SchemaError(
|
||||
f"group '{group}' routes to page '{page}', which has no entry in "
|
||||
"PAGE_TITLES; add one rather than letting the id be printed as a name"
|
||||
f"group '{group}' routes to page '{page}', which is not a leaf page "
|
||||
"in SettingsRoutes.qml; route it at one rather than letting the id "
|
||||
"be printed as a name"
|
||||
)
|
||||
title = PAGE_TITLES.get(page, page or "—")
|
||||
title = titles.get(page, page or "—")
|
||||
lines += [f"## {group}", "", f"Found on **{title}**.", ""]
|
||||
lines += ["| Setting | Default | What it does |", "|---|---|---|"]
|
||||
for entry in visible:
|
||||
@@ -210,7 +237,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
rendered = render(read_entries(), read_routes())
|
||||
rendered = render(read_entries(), read_routes(), read_titles())
|
||||
except SchemaError as error:
|
||||
# Loudly, and without writing. A partial reference is worse than a stale
|
||||
# one: stale is caught by --check, partial reads as complete.
|
||||
|
||||
Reference in New Issue
Block a user