Files
Panama/config/dot/quickshell/services/SettingsRoutes.qml
T
Gabriel Brown 7578348db1 Merge Home & Phone into a three-tab Home that knows your house
Home is now Overview | My Home | Phone. Overview leads with quick-action
tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the
findings card — updates fold in, the reclaim-space prompt is gone on
purpose — and adds glance cards, the next calendar event, and weather.

My Home groups every light by Home Assistant area: the helper gained an
`areas` command (one REST template render, no websocket), and the rooms
degrade to a flat list on setups without areas. The favorites editor and
connection card moved intact. Phone gains a vitals strip — battery and
cell signal read from KDE Connect's plugin D-Bus objects, where absence
is data, not an error — beside ring, clipboard, send-a-file, and the
BlueBubbles handoff.

The retired home-phone id resolves to my-home forever via a new alias
map in SettingsRoutes (with a hasOwnProperty guard so prototype names
cannot leak into settingsPage). Storage no longer claims 0 B free — the
old page read a field the disks helper never emitted.

Contracts updated alongside; per the new workflow, the full suite runs
once at the end of the redesign (see the test backlog note).

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 22:06:18 -04:00

145 lines
6.8 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: "home", label: "Overview" },
{ page: "my-home", label: "My Home" },
{ page: "phone", label: "Phone" }
] },
{ 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: "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];
}
// 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" })
// 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)
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;
}
}