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
204 lines
7.4 KiB
Python
Executable File
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:]))
|