Make every settings page reachable from the launcher

Settings had a search index and the launcher had script commands, and
neither knew about the other: finding a setting meant opening Settings
first and searching there. One command per page is now generated from
the same sources the in-app search reads -- the sidebar page list, the
group routing, and the schema labels -- so a setting searchable inside
Settings is searchable from the launcher without a second list to
maintain.

Generating it surfaced a gap in the in-app search as well. Six pages had
no vocabulary at all, because their contents come from the system rather
than our schema, so searching "volume" found nothing in Settings either.
They now carry entries, which improves both surfaces.

The product prefix is dropped from every hand-written command title. The
generated ones are qualified "Settings: <page>" instead: a bare page
label collides with the feature of the same name, and two commands
sharing a title are indistinguishable in a launcher.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-19 09:47:07 -04:00
parent 2b3b762793
commit a68e4f6dcd
44 changed files with 480 additions and 28 deletions
+14 -1
View File
@@ -109,6 +109,19 @@ case "$action" in
clipboard) qs ipc call clipboard open ;;
overview) qs ipc call overview open ;;
settings) qs ipc call settings open ;;
# Jump straight to one settings page. The launcher's per-page commands are
# the only caller, and they are generated from the page list itself, so the
# pattern below is a boundary check rather than a whitelist: an unknown page
# would leave the settings window showing nothing at all.
settings-page)
page="${2:-}"
if [[ ! "$page" =~ ^[a-z][a-z-]*$ ]]; then
printf 'Usage: panama-action settings-page PAGE\n' >&2
exit 2
fi
qs ipc call settings page "$page"
;;
health) qs ipc call health open ;;
dnd)
@@ -149,7 +162,7 @@ case "$action" in
;;
*)
printf 'Usage: panama-action {%s}\n' \
'control-center|notifications|calendar|clipboard|overview|settings|health|dnd|caffeine|night-light|focus-start|focus-end|capture|intelligence|screenshot|microphone|gallery|restart-shell' >&2
'control-center|notifications|calendar|clipboard|overview|settings|settings-page PAGE|health|dnd|caffeine|night-light|focus-start|focus-end|capture|intelligence|screenshot|microphone|gallery|restart-shell' >&2
exit 2
;;
esac
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Generate one launcher command per settings page.
Settings has a search index and the launcher has script commands, but they did
not know about each other: finding a setting meant opening Settings first and
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,
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.
panama-settings-commands write the commands
panama-settings-commands --check fail if what is on disk is stale
Run it after adding a settings page or renaming a setting.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[4]
SHELL = REPO / "config/dot/quickshell"
SIDEBAR = SHELL / "modules/settings/SettingsSidebar.qml"
SEARCH = SHELL / "services/SettingsSearch.qml"
SCHEMA = SHELL / "config/PreferenceSchema.qml"
OUTPUT_DIR = REPO / "config/local/share/vicinae/scripts"
# Home is what the plain "Open Settings" command already lands on, so a second
# command for it would be a duplicate under a different name.
SKIP_PAGES = {"home"}
# Enough vocabulary to find the page, not so much that one page matches
# everything. Ordered by the schema, so the settings at the top of a page --
# the ones it is named for -- are the ones that survive the cut.
MAX_KEYWORDS = 12
GENERATED_MARKER = "# Generated by scripts/panama-settings-commands -- do not edit by hand."
class ParseError(RuntimeError):
"""A source file did not look the way this generator expects."""
def read(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except OSError as error:
raise ParseError(f"could not read {path}") from error
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]
def group_pages() -> dict[str, str]:
source = read(SEARCH)
table = re.search(r"readonly property var groupPages: \(\{(.*?)\n \}\)", source, re.S)
if not table:
raise ParseError("groupPages not found in SettingsSearch.qml")
return dict(re.findall(r'"([A-Za-z]+)":\s*"([a-z-]+)"', table.group(1)))
def extra_labels() -> dict[str, list[str]]:
"""Settings the system owns rather than Panama, which have no schema entry."""
source = read(SEARCH)
table = re.search(r"readonly property var extraEntries: \[(.*?)\n \]", source, re.S)
if not table:
return {}
labels: dict[str, list[str]] = {}
for label, page in re.findall(r'\{ label: "([^"]+)".*?page: "([a-z-]+)" \}', table.group(1)):
labels.setdefault(page, []).append(label)
return labels
def schema_labels() -> list[tuple[str, str]]:
"""(group, label) for every user-facing setting, in schema order."""
source = read(SCHEMA)
entries: list[tuple[str, str]] = []
chunks = source.split("key: ")
for chunk in chunks[1:]:
# Internal state is not a setting anyone searches for.
if re.search(r"internal:\s*true", chunk[:600]):
continue
group = re.search(r'group:\s*"([A-Za-z]+)"', chunk[:600])
label = re.search(r'label:\s*"([^"]+)"', chunk[:600])
if group and label:
entries.append((group.group(1), label.group(1)))
if not entries:
raise ParseError("no labelled settings found in PreferenceSchema.qml")
return entries
def keywords_for(page: str, routing: dict[str, str], schema: list[tuple[str, str]],
extras: dict[str, list[str]]) -> list[str]:
words: list[str] = []
for group, label in schema:
if routing.get(group) == page:
lowered = label.lower()
if lowered not in words:
words.append(lowered)
for label in extras.get(page, []):
lowered = label.lower()
if lowered not in words:
words.append(lowered)
return words[:MAX_KEYWORDS]
def command_for(page: str, label: str, words: list[str]) -> str:
"""One launcher command, titled "Settings: <page>".
The qualifier is not product branding -- that was deliberately dropped from
every command title. It is here because a bare page label collides with the
feature of the same name: "Screen Intelligence" is both a thing you open and
a page of settings about it, and two commands sharing one title are
indistinguishable in a launcher. Qualifying the page also groups all of them
under one word, which is what typing "settings" is for.
"""
# "settings" is always a keyword so typing it lists every page at once.
vocabulary = ["settings"] + [word for word in words if word != "settings"]
keywords = ", ".join(f'"{word}"' for word in vocabulary)
return f"""#!/usr/bin/env bash
{GENERATED_MARKER}
# @vicinae.schemaVersion 1
# @vicinae.title Settings: {label}
# @vicinae.mode silent
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
# @vicinae.description Open {label} in Settings.
# @vicinae.keywords [{keywords}]
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page {page}
"""
def build() -> dict[Path, str]:
routing = group_pages()
schema = schema_labels()
extras = extra_labels()
files: dict[Path, str] = {}
for page, label in pages():
words = keywords_for(page, routing, schema, extras)
files[OUTPUT_DIR / f"settings-{page}.sh"] = command_for(page, label, words)
return files
def existing() -> set[Path]:
return {path for path in OUTPUT_DIR.glob("settings-*.sh")}
def main(arguments: list[str]) -> int:
check = arguments == ["--check"]
if arguments and not check:
print(__doc__, file=sys.stderr)
return 2
try:
files = build()
except ParseError as error:
# Loud and empty-handed: writing a partial set would silently drop the
# pages whose source stopped parsing.
print(f"panama-settings-commands: {error}", file=sys.stderr)
return 2
stale = [path for path, body in files.items()
if not path.is_file() or path.read_text(encoding="utf-8") != body]
orphaned = sorted(existing() - set(files))
if check:
for path in stale:
print(f"stale: {path.name}", file=sys.stderr)
for path in orphaned:
print(f"orphaned: {path.name}", file=sys.stderr)
if stale or orphaned:
print("panama-settings-commands: run it without --check to regenerate",
file=sys.stderr)
return 1
print(f"panama-settings-commands: {len(files)} commands are current")
return 0
for path in orphaned:
path.unlink()
print(f"removed {path.name}")
for path, body in files.items():
path.write_text(body, encoding="utf-8")
path.chmod(0o755)
print(f"panama-settings-commands: wrote {len(files)} commands"
+ (f", removed {len(orphaned)}" if orphaned else ""))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))