diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 679b592..160cc38 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -16,6 +16,7 @@ NPanel { // When the panel opens onOpened: { ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } panelContent: Rectangle { @@ -61,8 +62,7 @@ NPanel { // Update summary Text { - text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length - !== 1 ? "s" : "") + " can be updated" + text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated" font.pointSize: Style.fontSizeL * scaling font.family: Settings.data.ui.fontDefault font.weight: Style.fontWeightMedium @@ -72,31 +72,35 @@ NPanel { // Package selection info Text { - text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected" + text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected" font.pointSize: Style.fontSizeS * scaling font.family: Settings.data.ui.fontDefault color: Color.mOnSurfaceVariant Layout.fillWidth: true } - // Package list + // Unified list Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Color.mSurfaceVariant radius: Style.radiusM * scaling + // Combine repo and AUR lists in order: repos first, then AUR + property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || []) + ListView { - id: packageListView + id: unifiedList anchors.fill: parent anchors.margins: Style.marginS * scaling clip: true - model: ArchUpdaterService.updatePackages + model: parent.items spacing: Style.marginXS * scaling + cacheBuffer: 300 * scaling delegate: Rectangle { - width: packageListView.width - height: 50 * scaling + width: unifiedList.width + height: 56 * scaling color: Color.transparent radius: Style.radiusS * scaling @@ -105,34 +109,16 @@ NPanel { anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling - // Checkbox for selection + // Checkbox for selection (pure bindings; no imperative state) NIconButton { id: checkbox - icon: "check_box_outline_blank" - onClicked: { - const isSelected = ArchUpdaterService.isPackageSelected(modelData.name) - if (isSelected) { - ArchUpdaterService.togglePackageSelection(modelData.name) - icon = "check_box_outline_blank" - colorFg = Color.mOnSurfaceVariant - } else { - ArchUpdaterService.togglePackageSelection(modelData.name) - icon = "check_box" - colorFg = Color.mPrimary - } - } + icon: ArchUpdaterService.isPackageSelected(modelData.name) ? "check_box" : "check_box_outline_blank" + onClicked: ArchUpdaterService.togglePackageSelection(modelData.name) colorBg: Color.transparent - colorFg: Color.mOnSurfaceVariant + colorFg: ArchUpdaterService.isPackageSelected( + modelData.name) ? ((modelData.source === "aur") ? Color.mSecondary : Color.mPrimary) : Color.mOnSurfaceVariant Layout.preferredWidth: 30 * scaling Layout.preferredHeight: 30 * scaling - - Component.onCompleted: { - // Set initial state - if (ArchUpdaterService.isPackageSelected(modelData.name)) { - icon = "check_box" - colorFg = Color.mPrimary - } - } } // Package info @@ -140,13 +126,38 @@ NPanel { Layout.fillWidth: true spacing: Style.marginXXS * scaling - Text { - text: modelData.name - font.pointSize: Style.fontSizeM * scaling - font.family: Settings.data.ui.fontDefault - font.weight: Style.fontWeightMedium - color: Color.mOnSurface + RowLayout { Layout.fillWidth: true + spacing: Style.marginXS * scaling + + Text { + text: modelData.name + font.pointSize: Style.fontSizeM * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true + } + + // Source badge (custom rectangle) + Rectangle { + visible: !!modelData.source + radius: 9999 + color: modelData.source === "aur" ? Color.mSecondary : Color.mPrimary + Layout.alignment: Qt.AlignVCenter + implicitHeight: Math.max(Style.fontSizeXS * 1.7 * scaling, 16 * scaling) + // Width based on label content + horizontal padding + implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling) + + NText { + id: badgeText + anchors.centerIn: parent + text: modelData.source === "aur" ? "AUR" : "Repo" + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: modelData.source === "aur" ? Color.mOnSecondary : Color.mOnPrimary + } + } } Text { @@ -176,6 +187,7 @@ NPanel { tooltipText: "Check for updates" onClicked: { ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 576152c..4fcdf97 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -14,62 +14,79 @@ NIconButton { sizeRatio: 0.8 colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface + // Highlight color based on update source + colorFg: { + if (ArchUpdaterService.totalUpdates === 0) + return Color.mOnSurface + if (ArchUpdaterService.updates > 0 && ArchUpdaterService.aurUpdates > 0) + return Color.mPrimary + if (ArchUpdaterService.updates > 0) + return Color.mPrimary + return Color.mSecondary + } colorBorder: Color.transparent colorBorderHover: Color.transparent - // Enhanced icon states with better visual feedback + // Icon states icon: { - if (ArchUpdaterService.busy) + if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) return "sync" - if (ArchUpdaterService.updatePackages.length > 0) { - // Show different icons based on update count - const count = ArchUpdaterService.updatePackages.length + if (ArchUpdaterService.totalUpdates > 0) { + const count = ArchUpdaterService.totalUpdates if (count > 50) - return "system_update_alt" // Many updates + return "system_update_alt" if (count > 10) - return "system_update" // Moderate updates - return "system_update" // Few updates + return "system_update" + return "system_update" } return "task_alt" } - // Enhanced tooltip with more information + // Tooltip with repo vs AUR breakdown and sample lists tooltipText: { - if (ArchUpdaterService.busy) + if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) return "Checking for updates…" - var count = ArchUpdaterService.updatePackages.length - if (count === 0) + const repoCount = ArchUpdaterService.updates + const aurCount = ArchUpdaterService.aurUpdates + const total = ArchUpdaterService.totalUpdates + + if (total === 0) return "System is up to date ✓" - var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:") + let header = total === 1 ? "One package can be upgraded:" : (total + " packages can be upgraded:") - var list = ArchUpdaterService.updatePackages || [] - var s = "" - var limit = Math.min(list.length, 8) - // Reduced to 8 for better readability - for (var i = 0; i < limit; ++i) { - var p = list[i] - s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion) + function sampleList(arr, n, colorLabel) { + const limit = Math.min(arr.length, n) + let s = "" + for (var i = 0; i < limit; ++i) { + const p = arr[i] + s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion) + } + if (arr.length > limit) + s += "\n… and " + (arr.length - limit) + " more" + return (colorLabel ? (colorLabel + "\n") : "") + (s || "None") } - if (list.length > 8) - s += "\n… and " + (list.length - 8) + " more" - return header + "\n\n" + s + "\n\nClick to update system" + const repoHeader = repoCount > 0 ? ("Repo (" + repoCount + "):") : "Repo: 0" + const aurHeader = aurCount > 0 ? ("AUR (" + aurCount + "):") : "AUR: 0" + + const repoBlock = repoCount > 0 ? (repoHeader + "\n\n" + sampleList(ArchUpdaterService.repoPackages, + 5)) : repoHeader + const aurBlock = aurCount > 0 ? (aurHeader + "\n\n" + sampleList(ArchUpdaterService.aurPackages, 5)) : aurHeader + + return header + "\n\n" + repoBlock + "\n\n" + aurBlock + "\n\nClick to update system" } - // Enhanced click behavior with confirmation onClicked: { - if (ArchUpdaterService.busy) + if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) return - if (ArchUpdaterService.updatePackages.length > 0) { - // Show confirmation dialog for updates + if (ArchUpdaterService.totalUpdates > 0) { PanelService.getPanel("archUpdaterPanel").toggle(screen, this) } else { - // Just refresh if no updates available ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } } } diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index bfc4565..84c8228 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -10,29 +10,54 @@ Singleton { // Core properties readonly property bool busy: checkupdatesProcess.running - readonly property int updates: updatePackages.length - property var updatePackages: [] + readonly property bool aurBusy: checkAurUpdatesProcess.running + readonly property int updates: repoPackages.length + readonly property int aurUpdates: aurPackages.length + readonly property int totalUpdates: updates + aurUpdates + property var repoPackages: [] + property var aurPackages: [] property var selectedPackages: [] property int selectedPackagesCount: 0 property bool updateInProgress: false // Initial check - Component.onCompleted: doPoll() + Component.onCompleted: { + doPoll() + doAurPoll() + } - // Process for checking updates + // Process for checking repo updates Process { id: checkupdatesProcess command: ["checkupdates"] onExited: function (exitCode) { if (exitCode !== 0 && exitCode !== 2) { Logger.warn("ArchUpdater", "checkupdates failed (code:", exitCode, ")") - updatePackages = [] + repoPackages = [] } } stdout: StdioCollector { onStreamFinished: { parseCheckupdatesOutput(text) - Logger.log("ArchUpdater", "found", updatePackages.length, "upgradable package(s)") + Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) to upgrade") + } + } + } + + // Process for checking AUR updates + Process { + id: checkAurUpdatesProcess + command: ["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Qua || command -v paru >/dev/null 2>&1 && paru -Qua || echo ''"] + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.warn("ArchUpdater", "AUR check failed (code:", exitCode, ")") + aurPackages = [] + } + } + stdout: StdioCollector { + onStreamFinished: { + parseAurUpdatesOutput(text) + Logger.log("ArchUpdater", "found", aurPackages.length, "AUR package(s) to upgrade") } } } @@ -49,12 +74,34 @@ Singleton { "name": m[1], "oldVersion": m[2], "newVersion": m[3], - "description": `${m[1]} ${m[2]} -> ${m[3]}` + "description": `${m[1]} ${m[2]} -> ${m[3]}`, + "source": "repo" }) } } - updatePackages = packages + repoPackages = packages + } + + // Parse AUR updates output + function parseAurUpdatesOutput(output) { + const lines = output.trim().split('\n').filter(line => line.trim()) + const packages = [] + + for (const line of lines) { + const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/) + if (m) { + packages.push({ + "name": m[1], + "oldVersion": m[2], + "newVersion": m[3], + "description": `${m[1]} ${m[2]} -> ${m[3]}`, + "source": "aur" + }) + } + } + + aurPackages = packages } // Check for updates @@ -64,15 +111,26 @@ Singleton { checkupdatesProcess.running = true } - // Update all packages + // Check for AUR updates + function doAurPoll() { + if (aurBusy) + return + checkAurUpdatesProcess.running = true + } + + // Update all packages (repo + AUR) function runUpdate() { - if (updates === 0) { + if (totalUpdates === 0) { doPoll() + doAurPoll() return } updateInProgress = true + // Update repos first, then AUR Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"]) + Quickshell.execDetached( + ["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Sua --noconfirm || command -v paru >/dev/null 2>&1 && paru -Sua --noconfirm || true"]) // Refresh after updates with multiple attempts refreshAfterUpdate() @@ -84,8 +142,29 @@ Singleton { return updateInProgress = true - const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages) - Quickshell.execDetached(command) + // Split selected packages by source + const repoPkgs = selectedPackages.filter(pkg => { + const repoPkg = repoPackages.find(p => p.name === pkg) + return repoPkg && repoPkg.source === "repo" + }) + const aurPkgs = selectedPackages.filter(pkg => { + const aurPkg = aurPackages.find(p => p.name === pkg) + return aurPkg && aurPkg.source === "aur" + }) + + // Update repo packages + if (repoPkgs.length > 0) { + const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs) + Quickshell.execDetached(repoCommand) + } + + // Update AUR packages + if (aurPkgs.length > 0) { + const aurCommand = ["sh", "-c", `command -v yay >/dev/null 2>&1 && yay -S ${aurPkgs.join( + ' ')} --noconfirm || command -v paru >/dev/null 2>&1 && paru -S ${aurPkgs.join( + ' ')} --noconfirm || true`] + Quickshell.execDetached(aurCommand) + } // Clear selection and refresh selectedPackages = [] @@ -105,7 +184,7 @@ Singleton { } function selectAllPackages() { - selectedPackages = updatePackages.map(pkg => pkg.name) + selectedPackages = [...repoPackages.map(pkg => pkg.name), ...aurPackages.map(pkg => pkg.name)] selectedPackagesCount = selectedPackages.length } @@ -123,22 +202,26 @@ Singleton { // First refresh attempt after 3 seconds Qt.callLater(() => { doPoll() + doAurPoll() }, 3000) // Second refresh attempt after 8 seconds Qt.callLater(() => { doPoll() + doAurPoll() }, 8000) // Third refresh attempt after 15 seconds Qt.callLater(() => { doPoll() + doAurPoll() updateInProgress = false }, 15000) // Final refresh attempt after 30 seconds Qt.callLater(() => { doPoll() + doAurPoll() }, 30000) } @@ -152,6 +235,9 @@ Singleton { interval: 15 * 60 * 1000 // 15 minutes repeat: true running: true - onTriggered: doPoll() + onTriggered: { + doPoll() + doAurPoll() + } } }