Files
Panama/config/dot/quickshell/modules/settings/HomePage.qml
T
Gabriel Brown 1b845c0126 Rebuild Home around what this desktop already knows
Home led with a diagram of the attached monitor -- DP-2, 4500 x 3000, 1.13x
scale, XRGB2101010 -- which is Displays-page data, was the largest thing on the
screen, and has never been needed there. Under it sat a permanently open search
field for a weather location that is set about once a year, and then roughly
half a page of nothing.

It now opens with the two or three things somebody would open Home to do:
starting or ending a focus session, Do Not Disturb, and what is running. The Do
Not Disturb switch is disabled and says why while a mode is holding it, so it
cannot appear to be a control that is being ignored.

Findings sit above a reassurance line, the same shape the Firewall and
Containers pages use. Each finding names the page that can resolve it, because a
home page reporting a problem it cannot help with is only an alarm. On this
machine that is one finding today -- PostgreSQL and Redis reachable from the
network -- above "27 health checks pass, 522 GB free, snapshots ran at 02:00".
"Do next" appears only when there is something in it.

Weather stays, as the greeting's second line rather than a card with a search
box open, and its picker is collapsed behind the current location.

FocusModes was missing `import qs.config`, so DesktopPreferences was undefined
and the modes list came back undefined with it -- shipped two commits ago with
nothing noticing, because until Home referenced the service no isolated shell
had ever instantiated it. Focus modes would have quietly had no modes. Pulling
these services together on one page is what surfaced it.

LocationPicker gained a picked signal, the same way DisplayModePicker did, so a
container can put the search away without reaching into it.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-20 09:15:01 -04:00

290 lines
11 KiB
QML

// Home — the page you open to do something, not to read a report.
//
// It used to lead with a diagram of the attached monitor: DP-2, 4500 x 3000,
// 1.13x scale, XRGB2101010. That is Displays-page data, it was the largest
// thing on the screen, and nobody has ever needed it here. Below it sat a
// permanently open search field for a weather location that is set about once a
// year, and then roughly half a page of nothing.
//
// What Panama already knows is the useful material: whether anything needs
// attention, what is running, and the two or three things worth doing next. A
// finding is shown when there is one and the page is quiet when there is not,
// which is the same shape the Firewall and Containers pages use.
//
// Weather stays. It is the one genuinely ambient thing here, and it belongs
// next to a greeting rather than in a card of its own with a search box open.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
readonly property int openedHour: new Date().getHours()
readonly property string greeting: openedHour < 12
? "Good morning"
: (openedHour < 18 ? "Good afternoon" : "Good evening")
// Findings, in the order they deserve attention. Each names the page that
// can actually resolve it, because a home page that reports a problem it
// cannot help with is just an alarm.
readonly property var findings: {
const found = [];
const exposed = Firewall.exposedDataStores ?? [];
if (exposed.length > 0) {
found.push({
label: exposed.length === 1
? "A database is reachable from your network"
: "Databases are reachable from your network",
detail: exposed.map(entry => String(entry.name)).join(" and ")
+ ", published on every interface",
page: "firewall",
action: "Review"
});
}
if (Updates.securityCount > 0) {
found.push({
label: Updates.securityCount + " update"
+ (Updates.securityCount === 1 ? "" : "s") + " carry a security advisory",
detail: "Worth installing before the rest",
page: "updates",
action: "Open"
});
}
if (Updates.rebootNeeded) {
found.push({
label: "A newer kernel is installed than the one running",
detail: "Restart to use it",
page: "updates",
action: "Open"
});
}
if (Health.summary?.errors > 0 || Health.summary?.warnings > 0) {
const count = Number(Health.summary?.errors ?? 0) + Number(Health.summary?.warnings ?? 0);
found.push({
label: count + " health check" + (count === 1 ? "" : "s") + " need attention",
detail: "Something the desktop owns is not in its expected state",
page: "services",
action: "Open"
});
}
return found;
}
// One line of everything that is fine, so the quiet case still says
// something rather than showing an empty page.
readonly property string reassurance: {
const parts = [];
if (Health.checks.length > 0)
parts.push(Health.checks.length + " health checks pass");
if (Disks.rootFilesystem)
parts.push(Disks.formatBytes(Number(Disks.rootFilesystem.available ?? 0)) + " free");
const newest = Snapshots.configs.length > 0
? (Snapshots.configs[0].snapshots ?? [])[0]
: null;
if (newest)
parts.push("snapshots ran at " + root.clockOf(String(newest.date ?? "")));
return parts.join(" · ");
}
// snapper prints a full date; only the time is wanted in a summary line.
function clockOf(stamp: string): string {
const match = /(\d{1,2}:\d{2})(?::\d{2})?\s*(AM|PM)?/i.exec(stamp);
if (!match)
return stamp;
return match[2] ? match[1] + " " + match[2].toUpperCase() : match[1];
}
title: `${root.greeting}, Gabriel`
lede: Weather.available
? Math.round(Weather.temperature) + Weather.unitSuffix + " and "
+ Weather.description.toLowerCase() + " in " + Settings.weatherLocation
: "Your desktop is configured and ready."
Component.onCompleted: {
if (!Firewall.scanned)
Firewall.refresh();
if (!Containers.scanned)
Containers.refresh();
if (!Snapshots.scanned)
Snapshots.refresh();
if (!Disks.scanned)
Disks.refresh();
}
// ── What you came here to do ────────────────────────────────────────────
Grid {
id: doing
width: parent.width
columns: width >= 720 ? 2 : 1
columnSpacing: 16
rowSpacing: 16
SettingsCard {
width: doing.columns === 2
? (doing.width - doing.columnSpacing) / 2
: doing.width
title: "Focus"
subtitle: FocusSession.active
? "A session is running · " + FocusSession.remainingText + " left"
: (FocusModes.active
? FocusModes.activeName + " is on because " + FocusModes.activeReason
: (Notifs.doNotDisturb
? "Do Not Disturb is on"
: "Nothing is quieting this machine"))
SettingRow {
label: FocusSession.active ? "End the session" : "Start a focus session"
detail: FocusSession.active
? "Puts Do Not Disturb and Caffeine back as they were"
: Settings.focusDurationMinutes + " minutes · Super+Shift+F"
controlWidth: 120
SettingsButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: FocusSession.active ? "End" : "Start"
tone: FocusSession.active ? "normal" : "accent"
onClicked: FocusSession.active ? FocusSession.end(false) : FocusSession.startDefault()
}
}
SettingRow {
label: "Do Not Disturb"
detail: FocusModes.active
? "Held on by the " + FocusModes.activeName + " mode"
: "Banners are held; the notification centre still fills"
divider: false
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
enabled: !FocusModes.active && !FocusSession.active
checked: Notifs.doNotDisturb
onToggled: value => Notifs.doNotDisturb = value
}
}
}
SettingsCard {
width: doing.columns === 2
? (doing.width - doing.columnSpacing) / 2
: doing.width
title: "Right now"
subtitle: Containers.running > 0
? Containers.running + " of " + Containers.total + " containers running"
: "No containers are running"
TextRow {
label: Containers.running > 0
? Containers.projects.filter(project => project.running > 0)
.map(project => String(project.title)).join(", ")
: "Nothing to report"
detail: Containers.running > 0
? "Started for development, still up"
: "Start a stack from the Containers page"
value: ""
}
ActionRow {
label: "Containers"
detail: "Projects, what they expose, and what they cost"
action: "Open"
divider: false
onTriggered: ShellState.settingsPage = "containers"
}
}
}
// ── This machine ────────────────────────────────────────────────────────
SettingsCard {
title: "This machine"
subtitle: root.reassurance
TextRow {
visible: root.findings.length === 0
label: "Nothing needs your attention"
detail: "Anything worth acting on would appear here."
value: ""
divider: false
}
Repeater {
model: root.findings
delegate: SettingRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.label ?? "")
detail: String(modelData.detail ?? "")
controlWidth: 110
divider: index < root.findings.length - 1
SettingsButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: String(modelData.action ?? "Open")
onClicked: ShellState.settingsPage = String(modelData.page)
}
}
}
}
// ── Worth doing, when there is something ────────────────────────────────
SettingsCard {
visible: Updates.total > 0 || Containers.reclaimable > 0
title: "Do next"
subtitle: "Small things this machine is waiting on."
ActionRow {
visible: Updates.total > 0
label: "Install " + Updates.total + " update" + (Updates.total === 1 ? "" : "s")
detail: "Packages, applications and firmware, from wherever each comes from"
action: "Open"
divider: Containers.reclaimable > 0
onTriggered: ShellState.settingsPage = "updates"
}
ActionRow {
visible: Containers.reclaimable > 0
label: "Reclaim " + Containers.formatBytes(Containers.reclaimable)
detail: "Container images and volumes nothing references"
action: "Review"
divider: false
onTriggered: ShellState.settingsPage = "containers"
}
}
// ── Weather ─────────────────────────────────────────────────────────────
SettingsCard {
title: "Weather"
subtitle: "Shown above and in the date menu."
// The location is set roughly once a year, so the search that changes it
// is behind a press rather than permanently open.
PickerRow {
id: locationPicker
label: "Location"
detail: "Only the search term is sent; the name is a label kept on this machine"
value: Settings.weatherLocation
LocationPicker {
width: parent.width
onPicked: locationPicker.collapse()
}
}
ChoiceRow { setting: "temperatureUnit" }
SliderRow { setting: "weatherRefreshMinutes"; divider: false }
}
}