Rework ArchUpdater logic, update UI

ArchUpdater: remove pacman poll fully and rely on paru/yay
ArchUpdaterPanel: Remove scrollbar, remove UI blocking
README: Add `TERMINAL` env var info (again), add DiscoCevapi as Donator
This commit is contained in:
Ly-sec 2025-08-31 07:33:03 +02:00
parent 87f9afbd85
commit f19eaf689b
4 changed files with 190 additions and 75 deletions

View file

@ -13,12 +13,7 @@ NPanel {
panelHeight: 500 * scaling panelHeight: 500 * scaling
panelAnchorRight: true panelAnchorRight: true
// When the panel opens
onOpened: {
console.log("ArchUpdaterPanel: Panel opened, refreshing package lists...")
// Always refresh when panel opens to ensure we have the latest data
ArchUpdaterService.forceRefresh()
}
panelContent: Rectangle { panelContent: Rectangle {
color: Color.mSurface color: Color.mSurface
@ -75,8 +70,7 @@ NPanel {
// Update summary (only show when packages are available) // Update summary (only show when packages are available)
NText { NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated" text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
@ -86,8 +80,7 @@ NPanel {
// Package selection info (only show when not updating and have packages) // Package selection info (only show when not updating and have packages)
NText { NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected" text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
@ -188,8 +181,7 @@ NPanel {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates === 0
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates === 0
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
@ -225,18 +217,17 @@ NPanel {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
visible: (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) && !ArchUpdaterService.updateInProgress visible: ArchUpdaterService.aurBusy && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed && !ArchUpdaterService.updateFailed
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NBusyIndicator {
text: "refresh"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
size: Style.fontSizeXXXL * scaling
color: Color.mPrimary
} }
NText { NText {
@ -260,8 +251,7 @@ NPanel {
// Package list (only show when not in any special state) // Package list (only show when not in any special state)
NBox { NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -274,9 +264,7 @@ NPanel {
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling) cacheBuffer: Math.round(300 * scaling)
clip: true clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
model: parent.items model: parent.items
delegate: Rectangle { delegate: Rectangle {
width: unifiedList.width width: unifiedList.width
@ -356,14 +344,15 @@ NPanel {
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Refresh package lists" tooltipText: ArchUpdaterService.aurBusy ? "Checking for updates..." :
(!ArchUpdaterService.canPoll ? "Refresh available soon" : "Refresh package lists")
onClicked: { onClicked: {
ArchUpdaterService.forceRefresh() ArchUpdaterService.forceRefresh()
} }
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
Layout.fillWidth: true Layout.fillWidth: true
enabled: !ArchUpdaterService.busy && !ArchUpdaterService.aurBusy enabled: !ArchUpdaterService.aurBusy
} }
NIconButton { NIconButton {

View file

@ -20,7 +20,7 @@ NIconButton {
// Icon states // Icon states
icon: { icon: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { if (ArchUpdaterService.aurBusy) {
return "sync" return "sync"
} }
if (ArchUpdaterService.totalUpdates > 0) { if (ArchUpdaterService.totalUpdates > 0) {
@ -31,7 +31,7 @@ NIconButton {
// Tooltip with repo vs AUR breakdown and sample lists // Tooltip with repo vs AUR breakdown and sample lists
tooltipText: { tooltipText: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { if (ArchUpdaterService.aurBusy) {
return "Checking for updates…" return "Checking for updates…"
} }
@ -57,12 +57,7 @@ NIconButton {
} }
onClicked: { onClicked: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { // Always allow panel to open, never block
ToastService.showNotice("ArchUpdater", "Still fetching updates...")
return
}
PanelService.getPanel("archUpdaterPanel").toggle(screen, this) PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
ArchUpdaterService.doPoll()
} }
} }

View file

@ -79,9 +79,17 @@ Features a modern modular architecture with a status bar, notification system, c
- `cava` - Audio visualizer component - `cava` - Audio visualizer component
- `wlsunset` - To be able to use NightLight - `wlsunset` - To be able to use NightLight
> There are 2 more optional dependencies. > There is one more optional dependency.
> Any `polkit agent` to be able to use the ArchUpdater widget. > `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder.
> And also any `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder.
If you want to use the `ArchUpdater` widget, you will have to set your `TERMINAL` environment variable.
Example command (you can edit the /etc/environment file manually too):
`sudo sed -i '/^TERMINAL=/d' /etc/environment && echo 'TERMINAL=/usr/bin/kitty' | sudo tee -a /etc/environment
`
Please do not forget to edit `TERMINAL=/usr/bin/kitty` to match your terminal.
--- ---
@ -348,6 +356,7 @@ While I actually didn't want to accept donations, more and more people are askin
Thank you to everyone who supports me and this project 💜! Thank you to everyone who supports me and this project 💜!
* Gohma * Gohma
* <a href="https://pika-os.com/" target="_blank">PikaOS</a> * <a href="https://pika-os.com/" target="_blank">PikaOS</a>
* DiscoCevapi
--- ---

View file

@ -17,6 +17,7 @@ Singleton {
property var aurPackages: [] property var aurPackages: []
property var selectedPackages: [] property var selectedPackages: []
property int selectedPackagesCount: 0 property int selectedPackagesCount: 0
property string allUpdatesOutput: ""
// Update state // Update state
property bool updateInProgress: false property bool updateInProgress: false
@ -24,11 +25,15 @@ Singleton {
property string lastUpdateError: "" property string lastUpdateError: ""
// Computed properties // Computed properties
readonly property bool busy: checkupdatesProcess.running readonly property bool aurBusy: checkAurUpdatesProcess.running || checkAurOnlyProcess.running
readonly property bool aurBusy: checkParuUpdatesProcess.running
readonly property int updates: repoPackages.length readonly property int updates: repoPackages.length
readonly property int aurUpdates: aurPackages.length readonly property int aurUpdates: aurPackages.length
readonly property int totalUpdates: updates + aurUpdates readonly property int totalUpdates: updates + aurUpdates
// Polling cooldown (prevent excessive polling)
property int lastPollTime: 0
readonly property int pollCooldownMs: 5 * 60 * 1000 // 5 minutes
readonly property bool canPoll: (Date.now() - lastPollTime) > pollCooldownMs
// ============================================================================ // ============================================================================
// TIMERS // TIMERS
@ -98,7 +103,7 @@ Singleton {
// Process to check for errors in log file (only when update is in progress) // Process to check for errors in log file (only when update is in progress)
Process { Process {
id: errorCheckProcess id: errorCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'error\\|failed\\|failed to build\\|ERROR_DETECTED' /tmp/archupdater_output.log | tail -1; fi"] command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'error\\|failed to build\\|could not resolve\\|unable to satisfy\\|failed to install\\|failed to upgrade' /tmp/archupdater_output.log | grep -v 'ERROR_DETECTED' | tail -1; fi"]
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress) { if (exitCode === 0 && updateInProgress) {
// Error found in log // Error found in log
@ -109,6 +114,38 @@ Singleton {
updateMonitorTimer.stop() updateMonitorTimer.stop()
lastUpdateError = "Build or update error detected" lastUpdateError = "Build or update error detected"
// Read full log file for debugging
logReaderProcess.running = true
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim() !== "") {
console.log("ArchUpdater: Captured error from log:", text.trim())
}
}
}
}
// Process to check for successful completion
Process {
id: successCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i ':: Running post-transaction hooks\\|:: Processing package changes\\|upgrading.*\\.\\.\\.\\|installing.*\\.\\.\\.\\|removing.*\\.\\.\\.' /tmp/archupdater_output.log | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress) {
// Success indicators found
console.log("ArchUpdater: Update completed successfully")
updateInProgress = false
updateFailed = false
updateCompleteTimer.stop()
updateMonitorTimer.stop()
lastUpdateError = ""
// Refresh to check actual state // Refresh to check actual state
Qt.callLater(() => { Qt.callLater(() => {
doPoll() doPoll()
@ -117,15 +154,33 @@ Singleton {
} }
} }
// Timer to check for errors more frequently when update is in progress // Process to read full log file for debugging
Process {
id: logReaderProcess
command: ["cat", "/tmp/archupdater_output.log"]
onExited: function (exitCode) {
if (exitCode === 0) {
console.log("ArchUpdater: Full log file contents:")
}
}
stdout: StdioCollector {
onStreamFinished: {
if (text) {
console.log(text)
}
}
}
}
// Timer to check for success more frequently when update is in progress
Timer { Timer {
id: errorCheckTimer id: errorCheckTimer
interval: 5000 // Check every 5 seconds interval: 5000 // Check every 5 seconds
repeat: true repeat: true
running: updateInProgress running: updateInProgress
onTriggered: { onTriggered: {
if (updateInProgress && !errorCheckProcess.running) { if (updateInProgress && !successCheckProcess.running) {
errorCheckProcess.running = true successCheckProcess.running = true
} }
} }
} }
@ -155,45 +210,57 @@ Singleton {
// Initial check // Initial check
Component.onCompleted: { Component.onCompleted: {
getAurHelper() getAurHelper()
doPoll() // Initial poll without cooldown restriction
const aurHelper = getAurHelper()
if (aurHelper) {
checkAurUpdatesProcess.command = [aurHelper, "-Qu"]
checkAurOnlyProcess.command = [aurHelper, "-Qua"]
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
}
} }
// ============================================================================ // ============================================================================
// PACKAGE CHECKING PROCESSES // PACKAGE CHECKING PROCESSES
// ============================================================================ // ============================================================================
// Process for checking repo updates
// Process for checking all updates with AUR helper (repo + AUR)
Process { Process {
id: checkupdatesProcess id: checkAurUpdatesProcess
command: ["checkupdates", "--nosync"] command: []
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0 && exitCode !== 2) { if (exitCode !== 0) {
Logger.warn("ArchUpdater", "checkupdates failed (code:", exitCode, ")") Logger.warn("ArchUpdater", "AUR helper check failed (code:", exitCode, ")")
aurPackages = []
repoPackages = [] repoPackages = []
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
parseCheckupdatesOutput(text) allUpdatesOutput = text
Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) to upgrade") // Now get AUR-only updates to compare
checkAurOnlyProcess.running = true
} }
} }
} }
// Process for checking AUR updates with paru specifically // Process for checking AUR-only updates (to separate from repo updates)
Process { Process {
id: checkParuUpdatesProcess id: checkAurOnlyProcess
command: ["paru", "-Qua"] command: []
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) { if (exitCode !== 0) {
Logger.warn("ArchUpdater", "paru check failed (code:", exitCode, ")") Logger.warn("ArchUpdater", "AUR helper AUR-only check failed (code:", exitCode, ")")
aurPackages = [] aurPackages = []
repoPackages = []
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
parseAurUpdatesOutput(text) parseAllUpdatesOutput(allUpdatesOutput, text)
Logger.log("ArchUpdater", "found", aurPackages.length, "AUR package(s) to upgrade") Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) and", aurPackages.length, "AUR package(s) to upgrade")
} }
} }
} }
@ -230,25 +297,68 @@ Singleton {
} }
} }
// Parse checkupdates output // Parse all updates output (repo + AUR packages)
function parseCheckupdatesOutput(output) { function parseAllUpdatesOutput(allOutput, aurOnlyOutput) {
parsePackageOutput(output, "repo") const allLines = allOutput.trim().split('\n').filter(line => line.trim())
} const aurOnlyLines = aurOnlyOutput.trim().split('\n').filter(line => line.trim())
// Create a set of AUR package names for quick lookup
const aurPackageNames = new Set()
for (const line of aurOnlyLines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
aurPackageNames.add(m[1])
}
}
const repoPackages = []
const aurPackages = []
// Parse AUR updates output for (const line of allLines) {
function parseAurUpdatesOutput(output) { const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
parsePackageOutput(output, "aur") if (m) {
const packageInfo = {
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`
}
// Check if this package is in the AUR-only list
if (aurPackageNames.has(m[1])) {
packageInfo.source = "aur"
aurPackages.push(packageInfo)
} else {
packageInfo.source = "repo"
repoPackages.push(packageInfo)
}
}
}
// Update the package lists
if (repoPackages.length > 0 || aurPackages.length > 0 || allOutput.trim() === "") {
updateService.repoPackages = repoPackages
updateService.aurPackages = aurPackages
}
} }
function doPoll() { function doPoll() {
// Start repo updates check // Prevent excessive polling
if (!busy) { if (aurBusy || !canPoll) {
checkupdatesProcess.running = true return
} }
// Start AUR updates check // Get the AUR helper and set commands
if (!aurBusy) { const aurHelper = getAurHelper()
checkParuUpdatesProcess.running = true if (aurHelper) {
checkAurUpdatesProcess.command = [aurHelper, "-Qu"]
checkAurOnlyProcess.command = [aurHelper, "-Qua"]
// Start AUR updates check (includes both repo and AUR packages)
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
Logger.warn("ArchUpdater", "No AUR helper found (yay or paru)")
} }
} }
@ -365,10 +475,10 @@ Singleton {
doPoll() doPoll()
} }
// Manual refresh function // Manual refresh function (bypasses cooldown)
function forceRefresh() { function forceRefresh() {
// Prevent multiple simultaneous refreshes // Prevent multiple simultaneous refreshes
if (busy || aurBusy) { if (aurBusy) {
return return
} }
@ -376,8 +486,18 @@ Singleton {
updateFailed = false updateFailed = false
lastUpdateError = "" lastUpdateError = ""
// Just refresh the package lists without syncing databases // Get the AUR helper and set commands
doPoll() const aurHelper = getAurHelper()
if (aurHelper) {
checkAurUpdatesProcess.command = [aurHelper, "-Qu"]
checkAurOnlyProcess.command = [aurHelper, "-Qua"]
// Force refresh by bypassing cooldown
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
Logger.warn("ArchUpdater", "No AUR helper found (yay or paru)")
}
} }
// ============================================================================ // ============================================================================
@ -391,6 +511,7 @@ Singleton {
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode === 0) { if (exitCode === 0) {
cachedAurHelper = "yay" cachedAurHelper = "yay"
console.log("ArchUpdater: Found yay AUR helper")
} }
} }
} }
@ -403,6 +524,7 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
if (cachedAurHelper === "") { if (cachedAurHelper === "") {
cachedAurHelper = "paru" cachedAurHelper = "paru"
console.log("ArchUpdater: Found paru AUR helper")
} }
} }
} }
@ -526,13 +648,13 @@ Singleton {
// AUTO-POLL TIMER // AUTO-POLL TIMER
// ============================================================================ // ============================================================================
// Auto-poll every 15 minutes // Auto-poll every 15 minutes (respects cooldown)
Timer { Timer {
interval: 15 * 60 * 1000 // 15 minutes interval: 15 * 60 * 1000 // 15 minutes
repeat: true repeat: true
running: true running: true
onTriggered: { onTriggered: {
if (!updateInProgress) { if (!updateInProgress && canPoll) {
doPoll() doPoll()
} }
} }