Files
Panama/config/dot/quickshell/services/SettingsRoutes.qml
T
Gabriel Brown 5490fd285d 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
2026-08-23 20:21:31 -04:00

132 lines
6.2 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pragma Singleton
// The settings taxonomy: which categories the sidebar shows, and which leaf
// pages live inside each as tabs.
//
// The sidebar used to be a flat list of thirty-one pages, which made finding
// Printers a scan of the whole column. Categories group them the way macOS
// groups System Settings; the tab strip above a page switches between the
// leaves of its category. ShellState.settingsPage always holds a *leaf* id —
// the ids callers have always used — so every existing deep link, IPC call,
// and search result keeps working unchanged.
//
// This file is the only place the taxonomy is written down. The sidebar, the
// tab strip, ShellState's route validation, and two generators —
// scripts/panama-settings-commands and scripts/panama-settings-docs, which
// regex-parse the categories array below (keep its literal shape) — all
// derive from it.
import Quickshell
import QtQuick
Singleton {
id: root
// A category with no tabs is a leaf itself. A category with tabs is
// addressed by its first available tab; its own page id is accepted as an
// alias. Three category ids ("applications", "users", "privacy") double as
// the id of their first tab, which resolves to the same place either way.
readonly property var categories: [
{ page: "home", label: "Home", icon: "\u{F02DC}", tabs: [] },
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}", tabs: [] },
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}", tabs: [] },
{ page: "displays", label: "Displays", icon: "\u{F0379}", tabs: [] },
{ page: "sound", label: "Sound", icon: "\u{F057E}", tabs: [] },
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}", tabs: [] },
{ page: "input", label: "Input", icon: "\u{F030C}", tabs: [
{ page: "shortcuts", label: "Keyboard" },
{ page: "mouse", label: "Mouse & Touchpad" },
{ page: "dictation", label: "Dictation" }
] },
{ page: "network", label: "Network & Sharing", icon: "\u{F08D4}", tabs: [
{ page: "connectivity", label: "Connections" },
{ page: "firewall", label: "Firewall" },
{ page: "sharing", label: "Sharing" },
{ page: "printers", label: "Printers" }
] },
{ page: "home-phone", label: "Home & Phone", icon: "\u{F02DC}", tabs: [] },
{ page: "applications", label: "Applications", icon: "\u{F003B}", tabs: [
{ page: "applications", label: "Applications" },
{ page: "gaming", label: "Gaming" },
{ page: "screen-intelligence", label: "Screen Intelligence" }
] },
{ page: "users", label: "Users & Accounts", icon: "\u{F0004}", tabs: [
{ page: "users", label: "Users" },
{ page: "accounts", label: "Online Accounts" }
] },
{ page: "privacy", label: "Privacy & Security", icon: "\u{F0483}", tabs: [
{ page: "privacy", label: "Privacy" },
{ page: "ssh-keys", label: "SSH Keys" }
] },
{ page: "power", label: "Power & Lock", icon: "\u{F0425}", tabs: [] },
{ page: "accessibility", label: "Accessibility", icon: "\u{F0208}", tabs: [] },
{ page: "system", label: "System", icon: "\u{F02FD}", tabs: [
{ page: "about", label: "About" },
{ page: "updates", label: "Software Update" },
{ page: "services", label: "System Health" },
{ page: "storage", label: "Storage" },
{ page: "snapshots", label: "Snapshots" },
{ page: "containers", label: "Containers" },
{ page: "datetime", label: "Date & Time" },
{ page: "region", label: "Region & Language" },
{ page: "manual", label: "Manual" }
] }
]
// Pages whose entire backing stack can be absent hide once a scan has
// proven it absent: a Containers tab with no podman and a Snapshots tab
// with no snapper configuration render permanently empty, which reads as
// broken rather than inapplicable. Until the scan lands they stay visible,
// so machines that have the stack never see a blink — both scans fire when
// Settings opens (HomePage.onCompleted).
function pageAvailable(page: string): bool {
switch (page) {
case "containers":
return !Containers.scanned || Containers.available;
case "snapshots":
return !Snapshots.scanned || Snapshots.configs.length > 0;
}
return true;
}
// The category owning a leaf. Tab membership is checked before category
// ids so the three doubled ids land on their category either way.
function categoryOf(leaf: string): var {
const byTab = root.categories.find(cat => cat.tabs.some(tab => tab.page === leaf));
if (byTab)
return byTab;
return root.categories.find(cat => cat.page === leaf) ?? root.categories[0];
}
// Any id a caller may hold — leaf, category, or garbage — to the leaf that
// should render: a leaf resolves to itself, a category to its first
// available tab, anything unknown to home.
function resolve(id: string): string {
const asLeaf = root.categories.some(cat => (cat.page === id && cat.tabs.length === 0)
|| cat.tabs.some(tab => tab.page === id));
if (asLeaf)
return id;
const category = root.categories.find(cat => cat.page === id);
if (category) {
const tab = category.tabs.find(t => root.pageAvailable(t.page));
if (tab)
return tab.page;
}
return "home";
}
// The available tabs of the category owning a leaf, for the strip above
// the page. One entry (or none) means no strip is worth rendering.
function tabsFor(leaf: string): var {
return root.categoryOf(leaf).tabs.filter(tab => root.pageAvailable(tab.page));
}
// "System Storage" for a tabbed leaf, "Displays" for a lone one. This is
// what search results show, so a hit says where it will land.
function breadcrumb(leaf: string): string {
const category = root.categoryOf(leaf);
const tab = category.tabs.find(t => t.page === leaf);
return tab ? category.label + " " + tab.label : category.label;
}
}