// Software updates. // // Three sources that fail independently -- packages, applications, firmware -- // so each is counted and applied on its own. Blending them into one number // would hide the case that matters: a flatpak mirror being down says nothing // about whether a security fix is waiting. // // Applying packages takes a snapshot first, named after what is about to // 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. import Quickshell import QtQuick import qs.config import qs.services SettingsPage { id: root objectName: "updates" title: "Software Update" lede: "Packages, applications, and firmware, each from the place it actually comes from." property string expandedSource: "" Component.onCompleted: { Updates.refresh(); // A first visit with nothing cached should not show a confident "up to // date" it has no basis for, so it goes and finds out. if (!Updates.everChecked && !Updates.checking) Updates.check(); } TextRow { visible: Updates.lastError !== "" label: "Updates need attention" detail: Updates.lastError value: "" divider: false } TextRow { visible: Updates.lastApplied !== null label: "Updated" detail: Updates.lastApplied ? Updates.sourceLabel(String(Updates.lastApplied.source ?? "")) + (String(Updates.lastApplied.restorePoint ?? "") !== "" ? " · a snapshot was taken first, number " + String(Updates.lastApplied.restorePoint) : "") : "" value: "" 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." action: Updates.checking ? "Checking…" : "Check now" enabled: !Updates.busy divider: Updates.rebootNeeded onTriggered: Updates.check() } // The honest version of "restart required": the running kernel is not // the newest installed one, so a reboot would change which kernel runs. TextRow { visible: Updates.rebootNeeded label: "Restart to finish" detail: "A newer kernel is installed than the one running. " + String(Updates.kernel?.running ?? "") + " → " + String(Updates.kernel?.newestInstalled ?? "") value: "Restart needed" divider: false } } // ── One card per source ────────────────────────────────────────────────── SettingsCard { title: "System packages" subtitle: Updates.dnf?.available === false ? "dnf is not available on this machine." : (Number(Updates.dnf?.count ?? 0) === 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" } Repeater { model: root.expandedSource === "dnf" ? (Updates.dnf?.packages ?? []) : [] delegate: TextRow { required property var modelData required property int index width: parent.width label: String(modelData.name ?? "") detail: String(modelData.repository ?? "") value: String(modelData.version ?? "") divider: index < (Updates.dnf?.packages ?? []).length - 1 } } TextRow { visible: Number(Updates.dnf?.count ?? 0) === 0 && Updates.everChecked label: "Packages are current" detail: "Nothing from the system repositories is waiting" value: "" divider: false } } 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") 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 } } 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") } SwitchRow { label: "Update applications automatically" detail: "Once a day, in the background. Applications are not a security boundary the way packages are, so this is safe to leave on; packages still ask." checked: Updates.automatic?.flatpakEnabled === true enabled: !Updates.busy && Updates.automatic?.flatpakAvailable === true divider: false onToggled: value => Updates.setAutomaticFlatpak(value) } } 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") Repeater { model: Updates.firmware?.devices ?? [] delegate: TextRow { required property var modelData required property int index width: parent.width label: String(modelData.name ?? "") detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "") + (modelData.needsReboot ? " · installs on restart" : "") value: "" divider: true } } ActionRow { visible: Number(Updates.firmware?.count ?? 0) > 0 label: "Install firmware" detail: "Some devices only finish updating after a restart" action: Updates.applying ? "Working…" : "Install" enabled: !Updates.busy divider: false onTriggered: Updates.apply("firmware") } // Reported rather than offered. dnf-automatic is a package this machine // does not have, and installing software is not a settings action. 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 } } }