mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-13 09:12:08 -04:00
* 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>
397 lines
13 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|