aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/updates/UpdatesModule.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-15 12:21:03 +0200
committerDanilo M. <danix@danix.xyz>2026-10-15 12:21:03 +0200
commit44f23582c0fcee1c50dcd696d320bc7020237132 (patch)
tree8d2bd49b423b218388e8d41069bac2fa9c073e8e /desktop/modules/updates/UpdatesModule.qml
parent7db7405c80e8f00008bc5196abfda2c500071a98 (diff)
downloadquickshell-44f23582c0fcee1c50dcd696d320bc7020237132.tar.gz
quickshell-44f23582c0fcee1c50dcd696d320bc7020237132.zip
feat(updates): the drawer tile and page
The tile counts what can be installed now (Slackware and the binary repo) and what only needs a build, and goes accent when there is something to install. It runs updatesctl while the drawer is open and not otherwise; a result over 30 minutes old is followed by a refresh that runs nvchecker, never after a refresh, so a failing nvchecker cannot loop. The page has one section per next action: Slackware upgrades, new and removed first, then upgrade here, awaiting build, upstream newer, app updates, the order the work is done in. Each row names the package, the repo it comes from and the one version jump its action covers. A source that could not be read says so under the section it empties instead of reading as nothing pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/updates/UpdatesModule.qml')
-rw-r--r--desktop/modules/updates/UpdatesModule.qml97
1 files changed, 97 insertions, 0 deletions
diff --git a/desktop/modules/updates/UpdatesModule.qml b/desktop/modules/updates/UpdatesModule.qml
new file mode 100644
index 0000000..f7b1f39
--- /dev/null
+++ b/desktop/modules/updates/UpdatesModule.qml
@@ -0,0 +1,97 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import "../.."
+
+// Pending updates, sorted by updatesctl into the next action. Nothing runs
+// while the drawer is closed: the tile's lifetime is the check's, as with
+// KDE Connect. A result older than 30 minutes is followed by a refresh,
+// which runs nvchecker.
+Module {
+ id: mod
+
+ name: "updates"
+ label: "Updates"
+
+ // linux-slackware, present in Inconsolata Nerd Font.
+ icon: "\uf318"
+
+ property var doc: null
+ property bool checking: false
+
+ readonly property int toInstall: doc
+ ? doc.slackware.upgrade.length + doc.slackware.new.length
+ + doc.slackware.removed.length + doc.upgrade.length
+ : 0
+ readonly property int toBuild: doc ? doc.build.length : 0
+
+ active: toInstall > 0
+
+ property bool polling: false
+ onPollingChanged: if (polling) run(false)
+
+ function run(refresh) {
+ if (proc.running && mod.checking) return; // never cut nvchecker short
+ proc.command = ["/usr/bin/python3",
+ `${Quickshell.shellDir}/modules/updates/updatesctl`].concat(refresh ? ["--refresh"] : []);
+ proc.running = false;
+ // After the stop: a killed run can still finish its stream, and
+ // its handler clears `checking`.
+ mod.checking = refresh;
+ proc.running = true;
+ }
+
+ property Process proc: Process {
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const refreshed = mod.checking;
+ mod.checking = false;
+ try { mod.doc = JSON.parse(this.text); } catch (e) { return; }
+ // Only after a cached read: a refresh whose nvchecker failed
+ // leaves `checked` old and would otherwise loop.
+ if (!refreshed && mod.polling && Date.now() / 1000 - mod.doc.checked > 1800)
+ mod.run(true);
+ }
+ }
+ }
+
+ tileContent: Component {
+ Text {
+ Component.onCompleted: mod.polling = true
+ Component.onDestruction: mod.polling = false
+
+ width: parent ? parent.width : implicitWidth
+ elide: Text.ElideRight
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: mod.active ? Theme.text : Theme.subtext
+ text: {
+ if (!mod.doc) return "checking…";
+ const parts = [];
+ if (mod.toInstall) parts.push(mod.toInstall + " to install");
+ if (mod.toBuild) parts.push(mod.toBuild + " to build");
+ return parts.join(" · ") || "up to date";
+ }
+ }
+ }
+
+ page: Component {
+ Page {
+ title: "Updates"
+ UpdatesPage {
+ width: parent.width
+ up: mod
+ }
+ }
+ }
+}