1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-30 17:42:06 -04:00

system updater: complete overhaul

Move system update flow to GO, with a CLI (convenient AIO tool) and
server integration. All lifecycle, scheduling, execution occurs on
backend side.

Run some backends via pkexec, some via terminal like paru/yay.

Incorporate flatpak as an option to update.

Add terminal override setting in GUI, in addition to $TERMINAL env
variable.

fixes #2307
fixes #822
fixes #1102
fixes #1812
fixes #1087
fixes #1743
This commit is contained in:
bbedward
2026-04-29 12:33:57 -04:00
parent a4cfdf4a59
commit 7bd9574868
43 changed files with 3556 additions and 710 deletions

View File

@@ -61,12 +61,13 @@ Singleton {
signal screensaverStateUpdate(var data)
signal clipboardStateUpdate(var data)
signal locationStateUpdate(var data)
signal sysupdateStateUpdate(var data)
property bool capsLockState: false
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location"]
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -393,6 +394,8 @@ Singleton {
clipboardStateUpdate(data);
} else if (service === "location") {
locationStateUpdate(data);
} else if (service === "sysupdate") {
sysupdateStateUpdate(data);
}
}
@@ -749,4 +752,37 @@ Singleton {
"name": name
}, callback);
}
function sysupdateGetState(callback) {
sendRequest("sysupdate.getState", null, callback);
}
function sysupdateRefresh(force, callback) {
sendRequest("sysupdate.refresh", {
"force": force === true
}, callback);
}
function sysupdateUpgrade(opts, callback) {
const params = opts || {};
sendRequest("sysupdate.upgrade", params, callback);
}
function sysupdateCancel(callback) {
sendRequest("sysupdate.cancel", null, callback);
}
function sysupdateSetInterval(seconds, callback) {
sendRequest("sysupdate.setInterval", {
"seconds": seconds
}, callback);
}
function sysupdateAcquire(callback) {
sendRequest("sysupdate.acquire", null, callback);
}
function sysupdateRelease(callback) {
sendRequest("sysupdate.release", null, callback);
}
}

View File

@@ -37,7 +37,7 @@ Singleton {
return terminalFlags[terminal] ?? ["-e"]
}
readonly property string terminal: Quickshell.env("TERMINAL") || "ghostty"
readonly property string terminal: SessionData.resolveTerminal() || "ghostty"
function _terminalPrefix() {
return [terminal].concat(getTerminalFlag(terminal))

View File

@@ -860,7 +860,7 @@ Singleton {
function checkPluginCompatibility(requiresDms) {
if (!requiresDms)
return true;
return SystemUpdateService.checkVersionRequirement(requiresDms, SystemUpdateService.getParsedShellVersion());
return ShellVersionService.checkVersionRequirement(requiresDms, ShellVersionService.getParsedShellVersion());
}
function getIncompatiblePlugins() {

View File

@@ -237,7 +237,7 @@ Singleton {
const finalEnv = Object.assign({}, cursorEnv, overrideEnv);
if (desktopEntry.runInTerminal) {
const terminal = Quickshell.env("TERMINAL") || "xterm";
const terminal = SessionData.resolveTerminal() || "xterm";
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
const shellCmd = prefix.length > 0 ? `${prefix} ${escapedCmd}` : escapedCmd;
Quickshell.execDetached({

View File

@@ -0,0 +1,134 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property string shellVersion: ""
property string shellCodename: ""
property string semverVersion: ""
function getParsedShellVersion() {
return parseVersion(semverVersion);
}
Process {
id: versionDetection
running: true
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
running: true
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f VERSION ]; then cat VERSION; fi`]
stdout: StdioCollector {
onStreamFinished: semverVersion = text.trim()
}
}
Process {
id: codenameDetection
running: true
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f CODENAME ]; then cat CODENAME; fi`]
stdout: StdioCollector {
onStreamFinished: shellCodename = text.trim()
}
}
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;
switch (true) {
case req.startsWith(">="):
operator = ">=";
versionPart = req.substring(2);
break;
case req.startsWith("<="):
operator = "<=";
versionPart = req.substring(2);
break;
case req.startsWith(">"):
operator = ">";
versionPart = req.substring(1);
break;
case req.startsWith("<"):
operator = "<";
versionPart = req.substring(1);
break;
case req.startsWith("="):
operator = "=";
versionPart = req.substring(1);
break;
}
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;
}
}
}

View File

@@ -10,288 +10,185 @@ Singleton {
id: root
property int refCount: 0
property bool sysupdateAvailable: false
property var availableUpdates: []
property bool isChecking: false
property bool isUpgrading: false
property bool hasError: false
property string errorMessage: ""
property string updChecker: ""
property string pkgManager: ""
property string errorCode: ""
property var backends: []
property string distribution: ""
property string distributionPretty: ""
property string pkgManager: ""
property bool distributionSupported: false
property string shellVersion: ""
property string shellCodename: ""
property string semverVersion: ""
property var recentLog: []
property int intervalSeconds: 1800
property int lastCheckUnix: 0
property int nextCheckUnix: 0
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
readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0
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);
}
Connections {
target: DMSService
function onCapabilitiesReceived() {
root.checkCapabilities();
}
function onConnectionStateChanged() {
if (DMSService.isConnected) {
root.checkCapabilities();
} else {
console.warn("SystemUpdate: Failed to detect distribution");
root.sysupdateAvailable = false;
}
}
stdout: StdioCollector {}
Component.onCompleted: {
versionDetection.running = true;
function onSysupdateStateUpdate(data) {
root._applyState(data);
}
}
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();
}
Component.onCompleted: {
if (DMSService.dmsAvailable) {
checkCapabilities();
}
}
Process {
id: semverDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f VERSION ]; then cat VERSION; fi`]
running: true
stdout: StdioCollector {
onStreamFinished: {
semverVersion = text.trim();
}
function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
sysupdateAvailable = false;
return;
}
const has = DMSService.capabilities.includes("sysupdate");
if (has && !sysupdateAvailable) {
sysupdateAvailable = true;
requestState();
} else if (!has) {
sysupdateAvailable = false;
}
}
Process {
id: codenameDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f CODENAME ]; then cat CODENAME; fi`]
running: true
stdout: StdioCollector {
onStreamFinished: {
shellCodename = text.trim();
}
function requestState() {
if (!DMSService.isConnected || !sysupdateAvailable) {
return;
}
DMSService.sysupdateGetState(resp => {
if (resp && resp.result) {
_applyState(resp.result);
}
});
}
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.");
}
function _applyState(data) {
if (!data) {
return;
}
availableUpdates = data.packages || [];
backends = data.backends || [];
distribution = data.distro || "";
distributionPretty = data.distroPretty || "";
distributionSupported = (backends.length > 0);
recentLog = data.recentLog || [];
intervalSeconds = data.intervalSeconds || 1800;
lastCheckUnix = data.lastCheckUnix || 0;
nextCheckUnix = data.nextCheckUnix || 0;
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 => {
const phase = data.phase || "idle";
switch (phase) {
case "refreshing":
isChecking = true;
isUpgrading = false;
break;
case "upgrading":
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);
}
isUpgrading = true;
break;
default:
isChecking = false;
isUpgrading = false;
}
stdout: StdioCollector {}
}
if (data.error) {
hasError = true;
errorMessage = data.error.message || "";
errorCode = data.error.code || "";
} else {
hasError = false;
errorMessage = "";
errorCode = "";
}
Process {
id: updater
onExited: exitCode => {
checkForUpdates();
if (backends.length > 0) {
const sys = backends.find(b => b.repo === "system" || b.repo === "ostree");
pkgManager = sys ? sys.id : backends[0].id;
} else {
pkgManager = "";
}
}
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;
DMSService.sysupdateRefresh(false, null);
}
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";
function runUpdates(opts) {
const params = opts || {};
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];
_runCustomTerminalCommand();
return;
}
updater.running = true;
DMSService.sysupdateUpgrade(params, null);
}
Timer {
interval: 30 * 60 * 1000
repeat: true
running: refCount > 0 && distributionSupported && (pkgManager || updChecker)
onTriggered: checkForUpdates()
function cancelUpdates() {
DMSService.sysupdateCancel(null);
}
function setInterval(seconds) {
DMSService.sysupdateSetInterval(seconds, null);
}
function _runCustomTerminalCommand() {
const terminal = SessionData.resolveTerminal();
if (!terminal || terminal.length === 0) {
ToastService.showError(I18n.tr("No terminal configured"), I18n.tr("Pick a terminal in Settings → Launcher (or set $TERMINAL)."));
return;
}
const updateCommand = `${SettingsData.updaterCustomCommand} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
const termClass = SettingsData.updaterTerminalAdditionalParams || "";
var argv = [terminal];
if (termClass.length > 0) {
argv = argv.concat(termClass.split(" "));
}
argv.push("-e");
argv.push("sh");
argv.push("-c");
argv.push(updateCommand);
customRunner.command = argv;
customRunner.running = true;
}
Process {
id: customRunner
onExited: root.checkForUpdates()
}
onRefCountChanged: _syncAcquire()
onSysupdateAvailableChanged: _syncAcquire()
property bool _acquired: false
function _syncAcquire() {
const want = refCount > 0 && sysupdateAvailable;
if (want === _acquired) {
return;
}
_acquired = want;
if (want) {
DMSService.sysupdateAcquire(null);
return;
}
DMSService.sysupdateRelease(null);
}
IpcHandler {
@@ -301,96 +198,11 @@ Singleton {
if (root.isChecking) {
return "ERROR: already checking";
}
if (!distributionSupported) {
return "ERROR: distribution not supported";
}
if (!pkgManager && !updChecker) {
return "ERROR: update checker not available";
if (root.backends.length === 0) {
return "ERROR: no package manager 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;
}
}
}