Files
Panama/config/dot/quickshell/services/SettingsRoutes.qml
T

195 lines
9.1 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. Four category ids ("notifications", "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: "home", label: "Overview" },
{ page: "my-home", label: "My Home" },
{ page: "phone", label: "Phone" }
] },
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}", tabs: [] },
{ page: "shell", label: "Shell", icon: "\u{F04A4}", tabs: [
{ page: "bar", label: "Bar" },
{ page: "dock", label: "Dock" },
{ page: "control-center", label: "Control Center" },
{ page: "tiling", label: "Tiling" },
{ page: "workspaces", label: "Workspaces" }
] },
{ 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: "notifications", label: "Notifications" },
{ page: "focus", label: "Focus" }
] },
{ 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: "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: "agents", label: "Agents" },
{ page: "storage", label: "Storage" },
{ page: "snapshots", label: "Snapshots" },
{ page: "containers", label: "Containers" },
{ page: "datetime", label: "Date, Time & Region" },
{ page: "sync", label: "Sync & Backup" }
] }
]
// Leaves that are routable but are not tabs.
//
// The manual is reference material rather than a control surface: it is
// reached from About's Manual card, from a deep link, or from the search
// box, and a tenth tab spent on something nobody switches to while
// adjusting a setting made the System strip harder to read than the pages
// it addressed. It still needs to be a leaf -- every one of those callers
// hands `resolve()` the id "manual" and expects the page, not Home.
//
// Deliberately a separate array rather than a field inside a category: the
// two generators that regex-parse `categories` require each category to end
// `tabs: [...] }` and cross-check that every `page:` inside the array is
// accounted for, so a hidden leaf declared in there would break both.
readonly property var hiddenLeaves: [
{ page: "manual", label: "Manual", category: "system" }
]
// 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 hidden leaf with this id, or undefined.
function hiddenLeaf(page: string): var {
return root.hiddenLeaves.find(leaf => leaf.page === page);
}
// 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;
const hidden = root.hiddenLeaf(leaf);
if (hidden) {
const owner = root.categories.find(cat => cat.page === hidden.category);
if (owner)
return owner;
}
return root.categories.find(cat => cat.page === leaf) ?? root.categories[0];
}
// Retired page ids keep resolving forever: old Vicinae commands, shell
// history, and muscle memory all hold them. Each maps to the leaf that
// absorbed its content.
readonly property var retired: ({
"home-phone": "my-home",
"desktop": "bar",
// Region & Language merged into Date, Time & Region: one tab now owns
// the clock, the timezone, the language and the per-category formats,
// which were three answers to "what does this machine consider local".
"region": "datetime"
})
// Any id a caller may hold — leaf, category, retired id, 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 {
// hasOwnProperty guard: `retired` is a plain JS object, so a bare
// index would answer for prototype names like "toString" and leak a
// function into settingsPage.
if (Object.prototype.hasOwnProperty.call(root.retired, id))
return root.retired[id];
const asLeaf = root.categories.some(cat => (cat.page === id && cat.tabs.length === 0)
|| cat.tabs.some(tab => tab.page === id));
if (asLeaf || root.hiddenLeaf(id) !== undefined)
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.
//
// A hidden leaf still gets its category's strip, with nothing selected:
// the manual is somewhere you arrive on purpose and leave again, and the
// strip is how you leave.
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)
?? root.hiddenLeaves.find(entry => entry.page === leaf);
return tab ? category.label + " " + tab.label : category.label;
}
}