1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-13 09:12:08 -04:00
Files
DankMaterialShell/quickshell/Services/SystemUpdateService.qml
Thomas Kroll 049266271a fix(system-update): popout first-click and AUR package listing (#2183)
* fix(system-update): open popout on first click

The SystemUpdate widget required two clicks to open its popout.

On the first click, the LazyLoader was activated but popoutTarget
(bound to the loader's item) was still null in the MouseArea handler,
so setTriggerPosition was never called. The popout's open() then
returned early because screen was unset.

Restructure the onClicked handler to call setTriggerPosition directly
on the loaded item (matching the pattern used by Clock, Clipboard, and
other bar widgets) and use PopoutManager.requestPopout() instead of
toggle() for consistent popout management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(system-update): include AUR packages in update list

When paru or yay is the package manager, the update list only showed
official repo packages (via checkupdates or -Qu) while the upgrade
command (paru/yay -Syu) also processes AUR packages. This mismatch
meant AUR updates appeared as a surprise during the upgrade.

Combine the repo update listing with the AUR helper's -Qua flag so
both official and AUR packages are shown in the popout before the
user triggers the upgrade. The output format is identical for both
sources, so the existing parser works unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:49:15 -04:00

397 lines
13 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property int refCount: 0
property var availableUpdates: []
property bool isChecking: false
property bool hasError: false
property string errorMessage: ""
property string updChecker: ""
property string pkgManager: ""
property string distribution: ""
property bool distributionSupported: false
property string shellVersion: ""
property string shellCodename: ""
property string semverVersion: ""
function getParsedShellVersion() {
return parseVersion(semverVersion);
}
readonly property var archBasedUCSettings: {
"listUpdatesSettings": {
"params": [],
"correctExitCodes": [0, 2] // Exit code 0 = updates available, 2 = no updates
},
"parserSettings": {
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": match[2],
"newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}`
};
}
}
}
readonly property var archBasedPMSettings: function(requiresSudo) {
return {
"listUpdatesSettings": {
"params": ["-Qu"],
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
},
"upgradeSettings": {
"params": ["-Syu"],
"requiresSudo": requiresSudo
},
"parserSettings": {
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": match[2],
"newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}`
};
}
}
}
}
readonly property var fedoraBasedPMSettings: {
"listUpdatesSettings": {
"params": ["list", "--upgrades", "--quiet", "--color=never"],
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
},
"upgradeSettings": {
"params": ["upgrade"],
"requiresSudo": true
},
"parserSettings": {
"lineRegex": /^([^\s]+)\s+([^\s]+)\s+.*$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": "",
"newVersion": match[2],
"description": `${match[1]} ${match[2]}`
};
}
}
}
readonly property var updateCheckerParams: {
"checkupdates": archBasedUCSettings
}
readonly property var packageManagerParams: {
"yay": archBasedPMSettings(false),
"paru": archBasedPMSettings(false),
"pacman": archBasedPMSettings(true),
"dnf": fedoraBasedPMSettings
}
readonly property list<string> supportedDistributions: ["arch", "artix", "cachyos", "manjaro", "endeavouros", "fedora"]
readonly property int updateCount: availableUpdates.length
readonly property bool helperAvailable: pkgManager !== "" && distributionSupported
Process {
id: distributionDetection
command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"]
running: true
onExited: exitCode => {
if (exitCode === 0) {
distribution = stdout.text.trim().toLowerCase();
distributionSupported = supportedDistributions.includes(distribution);
if (distributionSupported) {
updateFinderDetection.running = true;
pkgManagerDetection.running = true;
checkForUpdates();
} else {
console.warn("SystemUpdate: Unsupported distribution:", distribution);
}
} else {
console.warn("SystemUpdate: Failed to detect distribution");
}
}
stdout: StdioCollector {}
Component.onCompleted: {
versionDetection.running = true;
}
}
Process {
id: versionDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -d .git ]; then echo "(git) $(git rev-parse --short HEAD)"; elif [ -f VERSION ]; then cat VERSION; fi`]
stdout: StdioCollector {
onStreamFinished: {
shellVersion = text.trim();
}
}
}
Process {
id: semverDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f VERSION ]; then cat VERSION; fi`]
running: true
stdout: StdioCollector {
onStreamFinished: {
semverVersion = text.trim();
}
}
}
Process {
id: codenameDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f CODENAME ]; then cat CODENAME; fi`]
running: true
stdout: StdioCollector {
onStreamFinished: {
shellCodename = text.trim();
}
}
}
Process {
id: updateFinderDetection
command: ["sh", "-c", "which checkupdates"]
onExited: exitCode => {
if (exitCode === 0) {
const exeFound = stdout.text.trim();
updChecker = exeFound.split('/').pop();
} else {
console.warn("SystemUpdate: No update checker found. Will use package manager.");
}
}
stdout: StdioCollector {}
}
Process {
id: pkgManagerDetection
command: ["sh", "-c", "which paru || which yay || which pacman || which dnf"]
onExited: exitCode => {
if (exitCode === 0) {
const exeFound = stdout.text.trim();
pkgManager = exeFound.split('/').pop();
} else {
console.warn("SystemUpdate: No package manager found");
}
}
stdout: StdioCollector {}
}
Process {
id: updateChecker
onExited: exitCode => {
isChecking = false;
const correctExitCodes = updChecker.length > 0 ? [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.correctExitCodes) : [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.correctExitCodes);
if (correctExitCodes.includes(exitCode)) {
parseUpdates(stdout.text);
hasError = false;
errorMessage = "";
} else {
hasError = true;
errorMessage = "Failed to check for updates";
console.warn("SystemUpdate: Update check failed with code:", exitCode);
}
}
stdout: StdioCollector {}
}
Process {
id: updater
onExited: exitCode => {
checkForUpdates();
}
}
function checkForUpdates() {
if (!distributionSupported || (!pkgManager && !updChecker) || isChecking)
return;
isChecking = true;
hasError = false;
if (pkgManager === "paru" || pkgManager === "yay") {
const repoCmd = updChecker.length > 0 ? updChecker : `${pkgManager} -Qu`;
updateChecker.command = ["sh", "-c", `(${repoCmd} 2>/dev/null; ${pkgManager} -Qua 2>/dev/null) || true`];
} else if (updChecker.length > 0) {
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
} else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);
}
updateChecker.running = true;
}
function parseUpdates(output) {
const lines = output.trim().split('\n').filter(line => line.trim());
const updates = [];
const regex = packageManagerParams[pkgManager].parserSettings.lineRegex;
const entryProducer = packageManagerParams[pkgManager].parserSettings.entryProducer;
for (const line of lines) {
const match = line.match(regex);
if (match) {
updates.push(entryProducer(match));
}
}
availableUpdates = updates;
}
function runUpdates() {
if (!distributionSupported || !pkgManager || updateCount === 0)
return;
const terminal = Quickshell.env("TERMINAL") || "xterm";
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
const updateCommand = `${SettingsData.updaterCustomCommand} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
const termClass = SettingsData.updaterTerminalAdditionalParams;
var finalCommand = [terminal];
if (termClass.length > 0) {
finalCommand = finalCommand.concat(termClass.split(" "));
}
finalCommand.push("-e");
finalCommand.push("sh");
finalCommand.push("-c");
finalCommand.push(updateCommand);
updater.command = finalCommand;
} else {
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ");
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : "";
const updateCommand = `${sudo} ${pkgManager} ${params} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
updater.command = [terminal, "-e", "sh", "-c", updateCommand];
}
updater.running = true;
}
Timer {
interval: 30 * 60 * 1000
repeat: true
running: refCount > 0 && distributionSupported && (pkgManager || updChecker)
onTriggered: checkForUpdates()
}
IpcHandler {
target: "systemupdater"
function updatestatus(): string {
if (root.isChecking) {
return "ERROR: already checking";
}
if (!distributionSupported) {
return "ERROR: distribution not supported";
}
if (!pkgManager && !updChecker) {
return "ERROR: update checker not available";
}
root.checkForUpdates();
return "SUCCESS: Now checking...";
}
}
function parseVersion(versionStr) {
if (!versionStr || typeof versionStr !== "string")
return {
major: 0,
minor: 0,
patch: 0
};
let v = versionStr.trim();
if (v.startsWith("v"))
v = v.substring(1);
const dashIdx = v.indexOf("-");
if (dashIdx !== -1)
v = v.substring(0, dashIdx);
const plusIdx = v.indexOf("+");
if (plusIdx !== -1)
v = v.substring(0, plusIdx);
const parts = v.split(".");
return {
major: parseInt(parts[0], 10) || 0,
minor: parseInt(parts[1], 10) || 0,
patch: parseInt(parts[2], 10) || 0
};
}
function compareVersions(v1, v2) {
if (v1.major !== v2.major)
return v1.major - v2.major;
if (v1.minor !== v2.minor)
return v1.minor - v2.minor;
return v1.patch - v2.patch;
}
function checkVersionRequirement(requirementStr, currentVersion) {
if (!requirementStr || typeof requirementStr !== "string")
return true;
const req = requirementStr.trim();
let operator = "";
let versionPart = req;
if (req.startsWith(">=")) {
operator = ">=";
versionPart = req.substring(2);
} else if (req.startsWith("<=")) {
operator = "<=";
versionPart = req.substring(2);
} else if (req.startsWith(">")) {
operator = ">";
versionPart = req.substring(1);
} else if (req.startsWith("<")) {
operator = "<";
versionPart = req.substring(1);
} else if (req.startsWith("=")) {
operator = "=";
versionPart = req.substring(1);
} else {
operator = ">=";
}
const reqVersion = parseVersion(versionPart);
const cmp = compareVersions(currentVersion, reqVersion);
switch (operator) {
case ">=":
return cmp >= 0;
case ">":
return cmp > 0;
case "<=":
return cmp <= 0;
case "<":
return cmp < 0;
case "=":
return cmp === 0;
default:
return cmp >= 0;
}
}
}