The page had a row for the case where dnf-automatic is absent and nothing for the case where it is present, so installing it made the setting disappear rather than appear. The switch enables downloading only, which is the shipped default and the right one to keep: a machine that installs packages unattended can reboot into a kernel nobody chose. Downloading ahead of time is what makes the install quick when someone does choose it, and the row says exactly that rather than implying updates apply themselves. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
170 lines
5.8 KiB
QML
170 lines
5.8 KiB
QML
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()
|
|
}
|
|
}
|
|
}
|