From 44f23582c0fcee1c50dcd696d320bc7020237132 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 15 Oct 2026 12:21:03 +0200 Subject: 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 --- desktop/modules/updates/UpdatesModule.qml | 97 ++++++++++++++++++++++ desktop/modules/updates/UpdatesPage.qml | 131 ++++++++++++++++++++++++++++++ desktop/shell.qml | 2 + 3 files changed, 230 insertions(+) create mode 100644 desktop/modules/updates/UpdatesModule.qml create mode 100644 desktop/modules/updates/UpdatesPage.qml (limited to 'desktop') 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. +// +// 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 + } + } + } +} diff --git a/desktop/modules/updates/UpdatesPage.qml b/desktop/modules/updates/UpdatesPage.qml new file mode 100644 index 0000000..967b0e0 --- /dev/null +++ b/desktop/modules/updates/UpdatesPage.qml @@ -0,0 +1,131 @@ +// Copyright (C) 2026 Danilo M. +// +// 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 QtQuick +import "../.." + +// One section per action, in the order they are done, each hidden when it +// has nothing and no error. Read-only: installing and building stay in the +// terminal. +Column { + id: page + + required property var up + readonly property var d: up.doc + + spacing: 16 + + function tracked(list) { + return list.map(r => ({ name: r.name, repo: r.repo, ver: r.from + " → " + r.to })); + } + + readonly property var sections: !d ? [] : [ + { title: "Slackware upgrades", err: d.errors.mirror || "", + rows: d.slackware.upgrade.map(r => ({ name: r.name, repo: "", ver: r.from + " → " + r.to })) }, + { title: "New in Slackware", err: "", + rows: d.slackware.new.map(r => ({ name: r.name, repo: "", ver: r.version })) }, + { title: "Removed from Slackware", err: "", + rows: d.slackware.removed.map(r => ({ name: r.name, repo: "", ver: "removed " + r.date })) }, + { title: "Upgrade here", err: d.errors.repo || "", rows: tracked(d.upgrade) }, + { title: "Awaiting build", err: "", rows: tracked(d.build) }, + { title: "Upstream newer", err: d.errors.nvchecker || "", rows: tracked(d.upstream) }, + { title: "App updates", err: "", rows: tracked(d.apps) }, + ] + + function ago(t) { + const m = Math.round((Date.now() / 1000 - t) / 60); + return m < 1 ? "just now" : m < 60 ? m + " min ago" : Math.round(m / 60) + " h ago"; + } + + Text { + visible: page.d !== null && page.up.toInstall === 0 && page.up.toBuild === 0 + && page.d.upstream.length === 0 && page.d.apps.length === 0 + text: "Up to date" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + } + + Repeater { + model: page.sections + + Column { + required property var modelData + width: page.width + spacing: 4 + visible: modelData.rows.length > 0 || modelData.err !== "" + + Text { + text: modelData.title + (modelData.rows.length ? " " + modelData.rows.length : "") + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } + } + + Text { + visible: modelData.err !== "" + text: modelData.err + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3; italic: true } + } + + Repeater { + model: modelData.rows + + Item { + required property var modelData + width: page.width + height: 22 + + Text { + anchors { left: parent.left; right: repo.left; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + textFormat: Text.PlainText + text: modelData.name + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Text { + id: repo + anchors { right: ver.left; rightMargin: 12; verticalCenter: parent.verticalCenter } + text: modelData.repo + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + Text { + id: ver + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + textFormat: Text.PlainText + text: modelData.ver + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + } + } + } + } + + Row { + spacing: 10 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: page.up.checking ? "checking…" + : page.d && page.d.checked ? "checked " + page.ago(page.d.checked) : "" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + + Button { + text: "Refresh" + enabled: !page.up.checking + onClicked: page.up.run(true) + } + } +} diff --git a/desktop/shell.qml b/desktop/shell.qml index 537ce71..55e7377 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -27,6 +27,7 @@ import "modules/network" import "modules/perf" import "modules/sound" import "modules/status" +import "modules/updates" import "modules/vm" import "island" @@ -62,6 +63,7 @@ ShellRoot { StatusModule {}, PerfModule {}, ClipboardModule {}, + UpdatesModule {}, VmModule {}, AiModule {}, AppearanceModule {}, -- cgit v1.2.3