Files
Panama/config/dot/quickshell/scripts/panama-settings-commands
T
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec.

159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor
contracts. A shebang and the executable bit already select the interpreter. The
extension only ever added something that had to stay in sync, and the rename
proved the point twice over in the space of an hour.

The spec's stated risk was Vicinae's script discovery. One script was renamed and
reloaded on its own before the other 46 followed; it came back as
scripts:panama.capture and all 47 resolve. What the probe turned up instead is
that the extension was never only a filename: Vicinae's command IDs embed it, so
every ID changed. Nothing in this repository refers to them, so nothing breaks.
The only trace is Vicinae's metadata.json, whose visited map had two Panama
entries that are now orphaned -- two commands lost their usage ranking and will
earn it back. Worth knowing before anyone renames these again on a machine that
has a keybind pointing at one.

Rewriting the references by exact filename missed two things it structurally
could not see: a name built from a variable, settings-$page.sh, and a glob,
-name '*.sh'. Both were in the contract that counts the generated commands, which
promptly reported 47 expected and 0 found. The mechanical part of a rename is the
part that looks finished.

The three subcommands. panama doctor fronts a health check that already existed
and already ran at the end of every install but could not be reached from a
terminal. panama upgrade re-runs the installer from anywhere. panama test runs
the suite, which had no entry point at all -- 121 files that were the main safety
net in this repository and were invisible in it.

Writing that runner found three tests nothing was running.
calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test
are unittest suites without the executable bit, so no contract invoked them and
the first draft of the runner skipped them silently. All three pass, and have
passed unobserved for weeks. The runner collects *_test.py as well now, because a
runner with a blind spot is worse than no runner for the same reason a dependency
checker with one is: it reports PASS.

Six worktrees pruned. Each was re-checked rather than trusted to the spec's list,
and two needed it: panama-commands is not on feat/panama-commands but on
feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead
of its remote, not of main, with every commit patch-equivalent to landed work.
roadmap-completion stays; it has five commits that are genuinely unlanded. The
branches are left alone: pruning a worktree costs nothing, deleting a branch is a
decision.

121 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:55:55 -04:00

204 lines
7.4 KiB
Python
Executable File

#!/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}"] = command_for(page, label, words)
return files
def existing() -> set[Path]:
return {path for path in OUTPUT_DIR.glob("settings-*")}
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:]))