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
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
// 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
|
||||
@@ -6,119 +22,101 @@ SettingsPage {
|
||||
id: root
|
||||
|
||||
readonly property int openedHour: new Date().getHours()
|
||||
readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening")
|
||||
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: "Your desktop is configured and ready."
|
||||
lede: Weather.available
|
||||
? Math.round(Weather.temperature) + Weather.unitSuffix + " and "
|
||||
+ Weather.description.toLowerCase() + " in " + Settings.weatherLocation
|
||||
: "Your desktop is configured and ready."
|
||||
|
||||
SettingsCard {
|
||||
title: SystemSettings.monitorDescription || "Active display"
|
||||
subtitle: SystemSettings.monitorName || "Detecting your display…"
|
||||
|
||||
Grid {
|
||||
id: monitorLayout
|
||||
|
||||
width: parent.width
|
||||
columns: width >= 620 ? 2 : 1
|
||||
columnSpacing: 28
|
||||
rowSpacing: 16
|
||||
|
||||
Item {
|
||||
width: monitorLayout.columns === 2
|
||||
? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47
|
||||
: monitorLayout.width
|
||||
height: 164
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(parent.width - 24, 260)
|
||||
height: width * 0.64
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 5
|
||||
radius: 12
|
||||
color: Theme.alpha(Theme.bgDark, 0.94)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.accent, 0.34)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
radius: 7
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0; color: Theme.mix(Theme.bg, Theme.accent, 0.08) }
|
||||
GradientStop { position: 1; color: Theme.mix(Theme.bg, Theme.accentSecondary, 0.08) }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: SystemSettings.monitorName || "DISPLAY"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.letterSpacing: 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: monitorLayout.columns === 2
|
||||
? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47
|
||||
: monitorLayout.width
|
||||
height: monitorLayout.columns === 2 ? 164 : implicitHeight
|
||||
spacing: 13
|
||||
|
||||
Text {
|
||||
text: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight}`
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
Text {
|
||||
text: `${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale`
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
Text {
|
||||
text: `${SystemSettings.monitorFormat || "Detecting format"} · ${SystemSettings.colorPreset || "standard color"}`
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
Text {
|
||||
text: SystemSettings.autoHdr ? "Game-aware HDR is ready" : "Game-aware HDR is off"
|
||||
color: SystemSettings.autoHdr ? Theme.ok : Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (!Firewall.scanned)
|
||||
Firewall.refresh();
|
||||
if (!Containers.scanned)
|
||||
Containers.refresh();
|
||||
if (!Snapshots.scanned)
|
||||
Snapshots.refresh();
|
||||
if (!Disks.scanned)
|
||||
Disks.refresh();
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Weather"
|
||||
subtitle: "Local conditions in the date menu"
|
||||
TextRow {
|
||||
label: "Location"
|
||||
detail: "Only the search term is sent; the name below is a label kept on this machine"
|
||||
value: Settings.weatherLocation
|
||||
}
|
||||
|
||||
LocationPicker {
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
ChoiceRow { setting: "temperatureUnit" }
|
||||
SliderRow { setting: "weatherRefreshMinutes"; divider: false }
|
||||
}
|
||||
// ── What you came here to do ────────────────────────────────────────────
|
||||
|
||||
Grid {
|
||||
id: summaryCards
|
||||
id: doing
|
||||
|
||||
width: parent.width
|
||||
columns: width >= 720 ? 2 : 1
|
||||
@@ -126,20 +124,46 @@ SettingsPage {
|
||||
rowSpacing: 16
|
||||
|
||||
SettingsCard {
|
||||
width: summaryCards.columns === 2
|
||||
? (summaryCards.width - summaryCards.columnSpacing) / 2
|
||||
: summaryCards.width
|
||||
title: "Quiet focus"
|
||||
subtitle: "Notifications and focused work"
|
||||
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: Notifs.doNotDisturb ? "Banners are currently quiet" : "Notification banners are visible"
|
||||
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
|
||||
}
|
||||
@@ -147,18 +171,119 @@ SettingsPage {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: summaryCards.columns === 2
|
||||
? (summaryCards.width - summaryCards.columnSpacing) / 2
|
||||
: summaryCards.width
|
||||
title: "Desktop services"
|
||||
subtitle: "The essentials are running"
|
||||
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: "Sync & remote access"
|
||||
detail: `${SystemSettings.nextcloudActive ? "Nextcloud ready" : "Nextcloud stopped"} · ${SystemSettings.rustdeskActive ? "RustDesk ready" : "RustDesk stopped"}`
|
||||
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
|
||||
value: SystemSettings.nextcloudActive && SystemSettings.rustdeskActive ? "Healthy" : "Review"
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import qs.modules.clipboard
|
||||
Column {
|
||||
id: root
|
||||
|
||||
// Emitted once a place has been chosen, so a container can put the search
|
||||
// away. Choosing is still this component's job; closing is not.
|
||||
signal picked
|
||||
|
||||
spacing: 0
|
||||
|
||||
SearchField {
|
||||
@@ -37,8 +41,10 @@ Column {
|
||||
divider: place.index < Geocoding.results.length - 1
|
||||
activatable: true
|
||||
onActivated: {
|
||||
if (Geocoding.choose(place.modelData))
|
||||
if (Geocoding.choose(place.modelData)) {
|
||||
query.text = "";
|
||||
root.picked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pragma Singleton
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
Reference in New Issue
Block a user