pragma Singleton // Software updates, from the three sources this machine actually uses. // // They are counted and applied separately because they fail separately: a // flatpak mirror being down says nothing about whether a kernel security fix is // waiting. Blending them into one number would hide exactly the case that // matters. // // Checking costs about nine seconds. So the page opens on the last result and // says when that was, the way every mature updater does, and refreshes in the // background rather than making someone watch a spinner to learn there is // nothing to do. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-updates" property var dnf: ({}) property var flatpak: ({}) property var firmware: ({}) property var kernel: ({}) property var automatic: ({}) property int checkedAt: 0 property bool scanned: false property string lastError: "" // Set after a successful apply, so the page can say what was done and // whether a restore point was taken. property var lastApplied: null // Guards read the Process objects directly; a derived binding is stale // inside the handler that changes it. See DefaultApps.qml. readonly property bool checking: checkProcess.running readonly property bool applying: applyProcess.running readonly property bool busy: root.checking || root.applying readonly property int total: Number(root.dnf?.count ?? 0) + Number(root.flatpak?.count ?? 0) + Number(root.firmware?.count ?? 0) readonly property int securityCount: Number(root.dnf?.securityCount ?? 0) readonly property bool rebootNeeded: root.kernel?.rebootNeeded === true readonly property bool everChecked: root.checkedAt > 0 // A count nobody has verified is not a count. Saying "up to date" on the // strength of a check that never ran is the one wrong answer that looks // reassuring. function summary(): string { if (!root.everChecked) return "Not checked yet"; if (root.total === 0) return "Up to date"; return root.total + " update" + (root.total === 1 ? "" : "s") + " available"; } function lastCheckedText(): string { if (!root.everChecked) return "Never checked"; const seconds = Math.max(0, Math.floor(Date.now() / 1000) - root.checkedAt); if (seconds < 90) return "Checked just now"; if (seconds < 3600) return "Checked " + Math.floor(seconds / 60) + " minutes ago"; if (seconds < 172800) return "Checked " + Math.floor(seconds / 3600) + " hours ago"; return "Checked " + Math.floor(seconds / 86400) + " days ago"; } function sourceLabel(source: string): string { switch (source) { case "dnf": return "System packages"; case "flatpak": return "Applications"; case "firmware": return "Firmware"; default: return source; } } function refresh(): void { if (query.running) return; query.command = [root.helperPath, "snapshot"]; query.running = true; } // The slow one, on request. function check(): void { if (checkProcess.running) return; root.lastError = ""; checkProcess.command = [root.helperPath, "check"]; checkProcess.running = true; } function apply(source: string): void { if (applyProcess.running) return; root.lastError = ""; root.lastApplied = null; applyProcess.command = [root.helperPath, "apply", source]; applyProcess.running = true; } function setAutomaticDnf(enabled: bool): void { if (applyProcess.running) return; root.lastError = ""; applyProcess.command = [root.helperPath, "set-auto-dnf", enabled ? "true" : "false"]; applyProcess.running = true; } function setAutomaticFlatpak(enabled: bool): void { if (applyProcess.running) return; root.lastError = ""; applyProcess.command = [root.helperPath, "set-auto-flatpak", enabled ? "true" : "false"]; applyProcess.running = true; } function absorb(text: string): void { try { const parsed = JSON.parse(text); root.dnf = parsed.dnf ?? ({}); root.flatpak = parsed.flatpak ?? ({}); root.firmware = parsed.firmware ?? ({}); root.kernel = parsed.kernel ?? ({}); root.automatic = parsed.automatic ?? ({}); root.checkedAt = Number(parsed.checkedAt ?? 0); root.lastError = String(parsed.error ?? ""); if (parsed.applied) root.lastApplied = parsed.applied; } catch (error) { root.lastError = "Could not read the update helper's answer."; console.warn("Updates: could not parse helper output:", error); } root.scanned = true; } Component.onCompleted: root.refresh() Process { id: query stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: checkProcess stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: applyProcess stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } }