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:
@@ -8,11 +8,16 @@ searching there. These commands close that gap, so typing "night light" into the
|
||||
launcher opens the page that owns it.
|
||||
|
||||
The vocabulary is derived from the same sources the in-app search uses --
|
||||
the page list in SettingsSidebar.qml, the group routing in SettingsSearch.qml,
|
||||
the taxonomy in SettingsRoutes.qml, the group routing in SettingsSearch.qml,
|
||||
and the labels in PreferenceSchema.qml -- so a setting that is searchable inside
|
||||
Settings is searchable from the launcher without anyone maintaining a second
|
||||
list.
|
||||
|
||||
One command per *leaf* page, which is what the sidebar's categories bottom out
|
||||
in: a category with tabs contributes its tabs, a category without them is a leaf
|
||||
itself. Landing the launcher on a category would mean landing on whichever tab
|
||||
happened to be first, which is not what the person typing "printers" asked for.
|
||||
|
||||
panama-settings-commands write the commands
|
||||
panama-settings-commands --check fail if what is on disk is stale
|
||||
|
||||
@@ -27,7 +32,7 @@ from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
SHELL = REPO / "config/dot/quickshell"
|
||||
SIDEBAR = SHELL / "modules/settings/SettingsSidebar.qml"
|
||||
ROUTES = SHELL / "services/SettingsRoutes.qml"
|
||||
SEARCH = SHELL / "services/SettingsSearch.qml"
|
||||
SCHEMA = SHELL / "config/PreferenceSchema.qml"
|
||||
OUTPUT_DIR = REPO / "config/local/share/vicinae/scripts"
|
||||
@@ -55,13 +60,51 @@ def read(path: Path) -> str:
|
||||
raise ParseError(f"could not read {path}") from error
|
||||
|
||||
|
||||
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*\}')
|
||||
|
||||
|
||||
def categories() -> list[tuple[str, str, list[tuple[str, str]]]]:
|
||||
"""The sidebar taxonomy, in order, as (id, label, tabs)."""
|
||||
source = read(ROUTES)
|
||||
block = re.search(r"readonly property var categories: \[(.*?)\n \]", source, re.S)
|
||||
if not block:
|
||||
raise ParseError("categories not found in SettingsRoutes.qml")
|
||||
found = [(page, label, TAB.findall(tabs))
|
||||
for page, label, tabs in CATEGORY.findall(block.group(1))]
|
||||
# A category whose literal stops matching -- a reordered field, an icon
|
||||
# written as anything but a string -- would otherwise go missing without a
|
||||
# word, taking its pages' commands with it. So every `page:` written in the
|
||||
# array has to be accounted for by something this reader recognised.
|
||||
declared = len(re.findall(r'\bpage:\s*"', block.group(1)))
|
||||
parsed = sum(1 + len(tabs) for _page, _label, tabs in found)
|
||||
if not found or parsed != declared:
|
||||
raise ParseError(
|
||||
f"read {parsed} of the {declared} pages in SettingsRoutes.qml; the "
|
||||
"categories array no longer looks the way this reader expects")
|
||||
return found
|
||||
|
||||
|
||||
def pages() -> list[tuple[str, str]]:
|
||||
"""The settings pages, in sidebar order, as (id, label)."""
|
||||
source = read(SIDEBAR)
|
||||
found = re.findall(r'\{ page: "([a-z-]+)", label: "([^"]+)"', source)
|
||||
if not found:
|
||||
raise ParseError("no pages found in SettingsSidebar.qml")
|
||||
return [(page, label) for page, label in found if page not in SKIP_PAGES]
|
||||
"""The leaf pages, in sidebar order, as (id, label).
|
||||
|
||||
A category with tabs is a group, not somewhere anyone lands: its tabs are
|
||||
the pages, and each tab is named by its own label rather than the group's.
|
||||
A category without tabs is a leaf itself.
|
||||
"""
|
||||
leaves: list[tuple[str, str]] = []
|
||||
for page, label, tabs in categories():
|
||||
leaves += tabs or [(page, label)]
|
||||
ids = [page for page, _label in leaves]
|
||||
duplicated = sorted({page for page in ids if ids.count(page) > 1})
|
||||
if duplicated:
|
||||
# Two leaves sharing an id write one file between them, so the second
|
||||
# would quietly overwrite the first's title.
|
||||
raise ParseError("these leaf pages are declared twice in SettingsRoutes.qml: "
|
||||
+ ", ".join(duplicated))
|
||||
return [(page, label) for page, label in leaves if page not in SKIP_PAGES]
|
||||
|
||||
|
||||
def group_pages() -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user