Finish the wonderland: System told truthfully, in eight tabs instead of ten

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 23:31:52 -04:00
parent 9ffaf45a4d
commit be0e55214b
57 changed files with 5040 additions and 925 deletions
@@ -9,6 +9,14 @@
// happen, so the Snapshots page shows "before 32 package updates" rather than a
// timestamp. That is the thing neither macOS nor Windows does cleanly, and it
// is nearly free here.
//
// A package row can say what it changes. "1 update available" tells you nothing
// about whether to install it now or after the meeting; the changelog does, and
// it is fetched per package on request rather than for a list nobody read.
//
// The automatic switches sit in their own card. The one that downloads packages
// used to live at the bottom of Firmware, which is where somebody put it once
// and nobody could find it since.
import Quickshell
import QtQuick
@@ -20,9 +28,16 @@ SettingsPage {
objectName: "updates"
title: "Software Update"
lede: "Packages, applications, and firmware, each from the place it actually comes from."
lede: root.headline
property string expandedSource: ""
// Rows are shown a screenful at a time. A dnf update is routinely thirty
// packages, and a page that opens on thirty rows is a page nobody reads.
readonly property int previewCount: 6
property bool showAllPackages: false
// "dnf:name" or "flatpak:id" -- the one row whose changelog is open.
property string expandedPackage: ""
Component.onCompleted: {
Updates.refresh();
@@ -32,6 +47,50 @@ SettingsPage {
Updates.check();
}
function joined(parts: var): string {
return parts.filter(part => String(part ?? "") !== "").join(" · ");
}
readonly property string headline: {
if (Updates.checking)
return "Checking every source…";
const security = Updates.securityCount > 0
? Updates.securityCount + " carrying a security advisory"
: (Updates.total > 0 ? "nothing security-critical" : "");
return root.joined([
Updates.summary(), security,
Updates.lastCheckedText().replace("Checked", "checked")
]) + ".";
}
readonly property var packages: Updates.dnf?.packages ?? []
readonly property var shownPackages: root.showAllPackages
? root.packages
: root.packages.slice(0, root.previewCount)
readonly property var applications: Updates.flatpak?.applications ?? []
readonly property var firmwareDevices: Updates.firmware?.devices ?? []
function toggleChangelog(source: string, name: string): void {
const key = source + ":" + name;
root.expandedPackage = root.expandedPackage === key ? "" : key;
}
// Asking for a changelog is what starts fetching one -- the page requests
// it by rendering it, and Updates keeps the answer for the life of the
// shell. Null means the fetch is still out.
function changelogText(source: string, name: string): string {
const record = Updates.changelogFor(source, name);
if (record === null || record === undefined)
return "Reading the changelog…";
if (String(record.text ?? "") !== "")
return String(record.text);
return String(record.error ?? "") !== ""
? String(record.error)
: "No changelog published for this update.";
}
// ── What just happened ───────────────────────────────────────────────────
TextRow {
visible: Updates.lastError !== ""
label: "Updates need attention"
@@ -54,43 +113,7 @@ SettingsPage {
divider: false
}
// ── The headline ─────────────────────────────────────────────────────────
SettingsCard {
Column {
width: parent.width
spacing: 10
Row {
width: parent.width
spacing: 12
Text {
text: Updates.checking ? "Checking…" : Updates.summary()
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.DemiBold
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: Updates.securityCount > 0
text: Updates.securityCount + " carry a security advisory"
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
}
Text {
text: Updates.lastCheckedText()
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
ActionRow {
label: "Check for updates"
detail: "Refreshes package metadata, application remotes, and firmware. Takes a few seconds."
@@ -113,55 +136,90 @@ SettingsPage {
}
}
// ── One card per source ──────────────────────────────────────────────────
// ── System packages ──────────────────────────────────────────────────────
SettingsCard {
title: "System packages"
subtitle: Updates.dnf?.available === false
? "dnf is not available on this machine."
: (Number(Updates.dnf?.count ?? 0) === 0
: (root.packages.length === 0
? "Nothing waiting."
: Updates.dnf.count + " package"
+ (Updates.dnf.count === 1 ? "" : "s") + " ready to install"
+ (Updates.securityCount > 0
? ", " + Updates.securityCount + " carrying an advisory" : ""))
ActionRow {
visible: Number(Updates.dnf?.count ?? 0) > 0
label: "Install package updates"
detail: "Asks for your password, and takes a snapshot first so this can be undone"
action: Updates.applying ? "Working…" : "Install"
enabled: !Updates.busy
onTriggered: Updates.apply("dnf")
}
ActionRow {
visible: Number(Updates.dnf?.count ?? 0) > 0
label: "What would change"
detail: root.expandedSource === "dnf"
? "Every package that would be replaced"
: Updates.dnf.count + " packages"
action: root.expandedSource === "dnf" ? "Hide" : "Show"
divider: root.expandedSource === "dnf"
onTriggered: root.expandedSource = root.expandedSource === "dnf" ? "" : "dnf"
}
: root.joined([
root.packages.length + " ready to install",
Updates.securityCount > 0
? Updates.securityCount + " carrying an advisory" : "",
Updates.sourceDownloadSize("dnf") === ""
? "" : Updates.sourceDownloadSize("dnf") + " to download"
]))
Repeater {
model: root.expandedSource === "dnf" ? (Updates.dnf?.packages ?? []) : []
id: packageRows
model: root.shownPackages
Column {
id: packageRow
delegate: TextRow {
required property var modelData
required property int index
readonly property string name: String(packageRow.modelData.name ?? "")
readonly property bool open: root.expandedPackage === "dnf:" + packageRow.name
width: parent.width
label: String(modelData.name ?? "")
detail: String(modelData.repository ?? "")
value: String(modelData.version ?? "")
divider: index < (Updates.dnf?.packages ?? []).length - 1
ActionRow {
width: parent.width
label: packageRow.name
detail: root.joined([
String(packageRow.modelData.version ?? ""),
String(packageRow.modelData.repository ?? "") === ""
? "" : "from " + packageRow.modelData.repository
])
action: packageRow.open ? "Hide" : "Changelog"
divider: !packageRow.open && packageRow.index < packageRows.count - 1
onTriggered: root.toggleChangelog("dnf", packageRow.name)
}
Text {
width: parent.width
visible: packageRow.open
text: root.changelogText("dnf", packageRow.name)
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
lineHeight: 1.3
topPadding: 2
bottomPadding: 14
}
}
}
ActionRow {
visible: root.packages.length > root.previewCount
label: root.showAllPackages
? "Showing all " + root.packages.length + " packages"
: (root.packages.length - root.previewCount) + " more waiting"
detail: root.showAllPackages ? "" : "Each one can say what it changes"
action: root.showAllPackages ? "Show fewer" : "Show all"
onTriggered: {
root.expandedPackage = "";
root.showAllPackages = !root.showAllPackages;
}
}
ActionRow {
visible: root.packages.length > 0
label: "Install package updates"
detail: "Asks for your password, and takes a snapshot first — undo lives in System Snapshots"
action: Updates.applying ? "Working…" : "Install updates"
enabled: !Updates.busy
divider: false
onTriggered: Updates.apply("dnf")
}
TextRow {
visible: Number(Updates.dnf?.count ?? 0) === 0 && Updates.everChecked
visible: root.packages.length === 0 && Updates.everChecked
label: "Packages are current"
detail: "Nothing from the system repositories is waiting"
value: ""
@@ -169,36 +227,31 @@ SettingsPage {
}
}
// ── Automatic ────────────────────────────────────────────────────────────
//
// Both switches, in the one card about the same idea. The package one used
// to sit at the bottom of Firmware.
SettingsCard {
title: "Applications"
subtitle: Updates.flatpak?.available === false
? "Flatpak is not installed."
: (Number(Updates.flatpak?.count ?? 0) === 0
? "Nothing waiting."
: Updates.flatpak.count + " application"
+ (Updates.flatpak.count === 1 ? "" : "s") + " ready to update")
title: "Automatic"
Repeater {
model: Updates.flatpak?.applications ?? []
delegate: TextRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.id ?? "")
detail: "Flatpak"
value: String(modelData.version ?? "")
divider: true
}
SwitchRow {
visible: Updates.automatic?.dnfAutomaticAvailable === true
label: "Download package updates automatically"
detail: "Fetches them in the background each morning so installing is quick. It does not install them: a machine that updates packages unattended can reboot into a kernel nobody chose."
checked: Updates.automatic?.dnfAutomaticEnabled === true
enabled: !Updates.busy
onToggled: value => Updates.setAutomaticDnf(value)
}
ActionRow {
visible: Number(Updates.flatpak?.count ?? 0) > 0
label: "Update applications"
detail: "Needs no password: these are installed for your account"
action: Updates.applying ? "Working…" : "Update"
enabled: !Updates.busy
onTriggered: Updates.apply("flatpak")
// Offered only when the machinery exists. When it does not, this says
// so rather than showing a switch that could not do anything --
// installing software is not a settings action.
TextRow {
visible: Updates.automatic?.dnfAutomaticAvailable === false
label: "Download package updates automatically"
detail: "Not set up. dnf-automatic is not installed, and Settings does not install software."
value: "Off"
}
SwitchRow {
@@ -211,32 +264,122 @@ SettingsPage {
}
}
// ── Applications & firmware ──────────────────────────────────────────────
SettingsCard {
title: "Firmware"
subtitle: Updates.firmware?.available === false
? "Firmware updating is not available on this machine."
: (Number(Updates.firmware?.count ?? 0) === 0
? "No firmware updates are offered for this hardware."
: Updates.firmware.count + " device"
+ (Updates.firmware.count === 1 ? "" : "s") + " have firmware available")
title: "Applications & firmware"
TextRow {
visible: Updates.flatpak?.available === false
label: "Flatpak"
detail: "Flatpak is not installed."
value: "Unavailable"
}
TextRow {
visible: Updates.flatpak?.available !== false && root.applications.length === 0
label: "Flatpak"
detail: "Everything current — each application gets its own row when an update appears"
value: "Current"
}
// Per application, because they update independently and one of them
// being 400 MB is a reason to do the other three now and that one
// later.
Repeater {
model: Updates.firmware?.devices ?? []
id: applicationRows
model: root.applications
Column {
id: applicationRow
delegate: TextRow {
required property var modelData
required property int index
readonly property string appId: String(applicationRow.modelData.id ?? "")
readonly property bool open: root.expandedPackage === "flatpak:" + applicationRow.appId
width: parent.width
label: String(modelData.name ?? "")
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
+ (modelData.needsReboot ? " · installs on restart" : "")
value: ""
divider: true
ActionRow {
width: parent.width
label: applicationRow.appId
detail: root.joined([
String(applicationRow.modelData.version ?? ""), "Flatpak",
Updates.formatBytes(Number(applicationRow.modelData.downloadBytes ?? 0))
])
action: Updates.applying ? "Working…" : "Update"
enabled: !Updates.busy
divider: false
onTriggered: Updates.applyFlatpakApp(applicationRow.appId)
}
ActionRow {
width: parent.width
label: "What changed"
detail: applicationRow.open
? ""
: "Release notes, when the application publishes them"
action: applicationRow.open ? "Hide" : "Changelog"
divider: !applicationRow.open
onTriggered: root.toggleChangelog("flatpak", applicationRow.appId)
}
Text {
width: parent.width
visible: applicationRow.open
text: root.changelogText("flatpak", applicationRow.appId)
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
lineHeight: 1.3
topPadding: 2
bottomPadding: 14
}
}
}
ActionRow {
visible: Number(Updates.firmware?.count ?? 0) > 0
visible: root.applications.length > 1
label: "Update every application"
detail: "Needs no password: these are installed for your account"
action: Updates.applying ? "Working…" : "Update all"
enabled: !Updates.busy
onTriggered: Updates.apply("flatpak")
}
TextRow {
visible: Updates.firmware?.available === false
label: "Firmware"
detail: "Firmware updating is not available on this machine."
value: "Unavailable"
divider: false
}
TextRow {
visible: Updates.firmware?.available !== false && root.firmwareDevices.length === 0
label: "Firmware"
detail: "No firmware updates are offered for this hardware"
value: "Current"
divider: false
}
Repeater {
model: root.firmwareDevices
TextRow {
required property var modelData
label: String(modelData.name ?? "")
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
+ (modelData.needsReboot ? " · installs on restart" : "")
value: ""
}
}
ActionRow {
visible: root.firmwareDevices.length > 0
label: "Install firmware"
detail: "Some devices only finish updating after a restart"
action: Updates.applying ? "Working…" : "Install"
@@ -244,27 +387,6 @@ SettingsPage {
divider: false
onTriggered: Updates.apply("firmware")
}
// Offered only when the machinery exists. When it does not, this says
// so rather than showing a switch that could not do anything --
// installing software is not a settings action.
SwitchRow {
visible: Updates.automatic?.dnfAutomaticAvailable === true
label: "Download package updates automatically"
detail: "Fetches them in the background each morning so installing is quick. It does not install them: a machine that updates packages unattended can reboot into a kernel nobody chose."
checked: Updates.automatic?.dnfAutomaticEnabled === true
enabled: !Updates.busy
divider: false
onToggled: value => Updates.setAutomaticDnf(value)
}
TextRow {
visible: Updates.automatic?.dnfAutomaticAvailable === false
label: "Automatic package updates"
detail: "Not set up. dnf-automatic is not installed, and Settings does not install software."
value: "Off"
divider: false
}
}
// Worth showing because automatic updates leave no other trace. Something
@@ -288,10 +410,10 @@ SettingsPage {
Repeater {
model: Updates.history
delegate: TextRow {
TextRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.summary ?? "")
detail: Updates.agoText(Number(modelData.at ?? 0))
value: Updates.describeHistory(modelData)