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: { "listUpdatesSettings": { "params": ["-Qu"], "correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates }, "upgradeSettings": { "params": ["-Syu"], "requiresSudo": false }, "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, "paru": archBasedPMSettings, "dnf": fedoraBasedPMSettings } readonly property list 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 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 (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 "Updates complete! 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 "Updates complete! 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; } } }