1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22: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

@@ -30,9 +30,36 @@ Singleton {
property bool isLightMode: false
property bool doNotDisturb: false
property real doNotDisturbUntil: 0
property string terminalOverride: ""
property bool isSwitchingMode: false
property bool suppressOSD: true
readonly property var terminalOptions: ["ghostty", "kitty", "foot", "alacritty", "wezterm", "konsole", "gnome-terminal", "xterm"]
property var installedTerminals: []
function resolveTerminal() {
if (terminalOverride && terminalOverride.length > 0) {
return terminalOverride;
}
const env = Quickshell.env("TERMINAL");
if (env && env.length > 0) {
return env;
}
return "";
}
Process {
id: terminalProbe
running: true
command: ["sh", "-c", "for t in ghostty kitty foot alacritty wezterm konsole gnome-terminal xterm; do command -v \"$t\" >/dev/null 2>&1 && echo \"$t\"; done"]
stdout: StdioCollector {
onStreamFinished: {
const found = text.trim().split("\n").filter(line => line.length > 0);
root.installedTerminals = found;
}
}
}
Timer {
id: dndExpireTimer
repeat: false

View File

@@ -640,6 +640,9 @@ Singleton {
property bool updaterUseCustomCommand: false
property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: ""
property int updaterIntervalSeconds: 1800
property bool updaterIncludeFlatpak: true
property bool updaterAllowAUR: true
property string displayNameMode: "system"
property var screenPreferences: ({})

View File

@@ -4,6 +4,7 @@ var SPEC = {
isLightMode: { def: false },
doNotDisturb: { def: false },
doNotDisturbUntil: { def: 0 },
terminalOverride: { def: "" },
wallpaperPath: { def: "" },
perMonitorWallpaper: { def: false },

View File

@@ -428,6 +428,9 @@ var SPEC = {
updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" },
updaterIntervalSeconds: { def: 1800 },
updaterIncludeFlatpak: { def: true },
updaterAllowAUR: { def: true },
displayNameMode: { def: "system" },
screenPreferences: { def: {} },

View File

@@ -895,7 +895,12 @@ Item {
SystemUpdatePopout {
id: systemUpdatePopout
onPopoutClosed: PopoutService.unloadSystemUpdate()
onPopoutClosed: {
if (systemUpdatePopout._reopenAfterUpgrade) {
return;
}
PopoutService.unloadSystemUpdate();
}
Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout;

View File

@@ -1881,7 +1881,7 @@ Item {
function openTerminal(path) {
if (!path)
return;
var terminal = Quickshell.env("TERMINAL") || "xterm";
var terminal = SessionData.resolveTerminal() || "xterm";
Quickshell.execDetached({
command: [terminal],
workingDirectory: path

View File

@@ -164,7 +164,8 @@ Rectangle {
"id": "updater",
"text": I18n.tr("System Updater"),
"icon": "refresh",
"tabIndex": 20
"tabIndex": 20,
"updaterOnly": true
},
{
"id": "desktop_widgets",
@@ -340,6 +341,8 @@ Rectangle {
return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
return false;
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
return false;
return true;
}

View File

@@ -0,0 +1,441 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: systemUpdatePopout
layerNamespace: "dms:system-update"
property var parentWidget: null
property var triggerScreen: null
Ref {
service: SystemUpdateService
}
property bool _reopenAfterUpgrade: false
Connections {
target: SystemUpdateService
function onIsUpgradingChanged() {
if (SystemUpdateService.isUpgrading) {
return;
}
if (!systemUpdatePopout._reopenAfterUpgrade) {
return;
}
systemUpdatePopout._reopenAfterUpgrade = false;
systemUpdatePopout.open();
}
}
popupWidth: 440
popupHeight: 560
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: close()
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
return;
}
const stale = !SystemUpdateService.lastCheckUnix || (Date.now() / 1000 - SystemUpdateService.lastCheckUnix) > 300;
if (stale && !SystemUpdateService.isChecking && !SystemUpdateService.isUpgrading) {
SystemUpdateService.checkForUpdates();
}
}
content: Component {
Rectangle {
id: updaterPanel
color: "transparent"
focus: true
readonly property bool hasTerminalBackend: (SystemUpdateService.backends || []).some(b => b.runsInTerminal === true)
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
systemUpdatePopout.close();
event.accepted = true;
}
}
Component.onCompleted: {
if (systemUpdatePopout.shouldBeVisible) {
forceActiveFocus();
}
}
Item {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
height: 40
StyledText {
text: I18n.tr("System Updates")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: {
switch (true) {
case SystemUpdateService.isUpgrading:
return I18n.tr("Upgrading...");
case SystemUpdateService.isChecking:
return I18n.tr("Checking...");
case SystemUpdateService.hasError:
return I18n.tr("Error");
case SystemUpdateService.updateCount === 0:
return I18n.tr("Up to date");
case SystemUpdateService.updateCount === 1:
return I18n.tr("%1 update").arg(SystemUpdateService.updateCount);
default:
return I18n.tr("%1 updates").arg(SystemUpdateService.updateCount);
}
}
font.pixelSize: Theme.fontSizeMedium
color: SystemUpdateService.hasError ? Theme.error : Theme.surfaceVariantText
}
DankActionButton {
id: refreshButton
buttonSize: 28
iconName: "refresh"
iconSize: 18
iconColor: Theme.surfaceText
enabled: !SystemUpdateService.isChecking && !SystemUpdateService.isUpgrading
opacity: enabled ? 1.0 : 0.5
onClicked: SystemUpdateService.checkForUpdates()
RotationAnimation {
target: refreshButton
property: "rotation"
from: 0
to: 360
duration: 1000
running: SystemUpdateService.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
refreshButton.rotation = 0;
}
}
}
}
}
}
StyledText {
id: backendsRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingS
visible: SystemUpdateService.backends.length > 0 && !SystemUpdateService.isUpgrading
text: {
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
return I18n.tr("Backends: %1").arg(names);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Row {
id: buttonsRow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingL
spacing: Theme.spacingM
height: 44
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: primaryMouseArea.containsMouse && primaryMouseArea.enabled ? Theme.primaryHover : Theme.secondaryHover
opacity: primaryMouseArea.enabled ? 1.0 : 0.5
StyledText {
anchors.centerIn: parent
text: SystemUpdateService.isUpgrading ? I18n.tr("Cancel") : I18n.tr("Update All")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
}
MouseArea {
id: primaryMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: SystemUpdateService.isUpgrading || SystemUpdateService.updateCount > 0
onClicked: {
if (SystemUpdateService.isUpgrading) {
SystemUpdateService.cancelUpdates();
return;
}
const opts = {
includeFlatpak: SettingsData.updaterIncludeFlatpak,
includeAUR: SettingsData.updaterAllowAUR,
terminal: SessionData.terminalOverride
};
if (updaterPanel.hasTerminalBackend) {
systemUpdatePopout._reopenAfterUpgrade = true;
SystemUpdateService.runUpdates(opts);
systemUpdatePopout.close();
return;
}
SystemUpdateService.runUpdates(opts);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
StyledText {
anchors.centerIn: parent
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: systemUpdatePopout.close()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
id: bodyArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
anchors.bottom: buttonsRow.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
StyledText {
id: statusText
anchors.fill: parent
anchors.margins: Theme.spacingM
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
visible: !SystemUpdateService.isUpgrading && (SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking)
text: {
switch (true) {
case SystemUpdateService.hasError:
return I18n.tr("Failed: %1").arg(SystemUpdateService.errorMessage);
case !SystemUpdateService.helperAvailable:
return I18n.tr("No supported package manager found.");
case SystemUpdateService.isChecking:
return I18n.tr("Checking for updates...");
default:
return I18n.tr("Your system is up to date!");
}
}
font.pixelSize: Theme.fontSizeMedium
color: SystemUpdateService.hasError ? Theme.errorText : Theme.surfaceText
wrapMode: Text.WordWrap
}
DankListView {
id: packagesList
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: !SystemUpdateService.isUpgrading && SystemUpdateService.updateCount > 0 && !SystemUpdateService.hasError && !SystemUpdateService.isChecking
clip: true
spacing: Theme.spacingXS
model: SystemUpdateService.availableUpdates
delegate: Rectangle {
width: ListView.view.width
height: 48
radius: Theme.cornerRadius
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
required property var modelData
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 64
height: 18
radius: 9
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18)
StyledText {
anchors.centerIn: parent
text: modelData.repo || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 64 - Theme.spacingS
spacing: 2
StyledText {
width: parent.width
text: modelData.name || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: {
const from = modelData.fromVersion || "";
const to = modelData.toVersion || "";
if (from && to) {
return `${from} ${to}`;
}
return to || from || "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
}
MouseArea {
id: packageMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.changelogUrl ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (modelData.changelogUrl) {
Qt.openUrlExternally(modelData.changelogUrl);
}
}
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
visible: SystemUpdateService.isUpgrading && updaterPanel.hasTerminalBackend
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "terminal"
size: 32
color: Theme.primary
}
StyledText {
width: parent.width
text: I18n.tr("Running in terminal")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: parent.width
text: I18n.tr("AUR helpers are interactive — see the terminal window for prompts. This popout will return to idle when the upgrade exits.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: SystemUpdateService.isUpgrading && !updaterPanel.hasTerminalBackend
contentWidth: width
contentHeight: logText.implicitHeight
clip: true
onContentHeightChanged: {
if (contentHeight > height) {
contentY = contentHeight - height;
}
}
StyledText {
id: logText
width: parent.width
text: (SystemUpdateService.recentLog || []).join("\n")
font.family: Theme.monoFontFamily || "monospace"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.NoWrap
}
}
}
}
}
}

View File

@@ -7,41 +7,33 @@ import qs.Widgets
BasePill {
id: root
property var widgetData: null
property bool isActive: false
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking
readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError
readonly property bool isClean: SystemUpdateService.sysupdateAvailable && !hasUpdates && !isChecking && !SystemUpdateService.hasError
readonly property bool hideWhenIdle: widgetData?.hideWhenIdle === true
readonly property bool shouldHide: hideWhenIdle && isClean
width: shouldHide ? 0 : (isVerticalOrientation ? barThickness : visualWidth)
height: shouldHide ? 0 : (isVerticalOrientation ? visualHeight : barThickness)
visible: !shouldHide
opacity: shouldHide ? 0 : 1
states: [
State {
name: "hidden_horizontal"
when: root.shouldHide && !isVerticalOrientation
PropertyChanges {
target: root
width: 0
}
},
State {
name: "hidden_vertical"
when: root.shouldHide && isVerticalOrientation
PropertyChanges {
target: root
height: 0
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
]
}
transitions: [
Transition {
NumberAnimation {
properties: "width,height"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
]
}
Behavior on opacity {
NumberAnimation {

View File

@@ -189,10 +189,10 @@ Item {
StyledText {
text: {
if (!SystemUpdateService.shellVersion && !DMSService.cliVersion)
if (!ShellVersionService.shellVersion && !DMSService.cliVersion)
return "dms";
let version = SystemUpdateService.shellVersion || "";
let version = ShellVersionService.shellVersion || "";
let cliVersion = DMSService.cliVersion || "";
// Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84
@@ -218,7 +218,7 @@ Item {
let baseVersion = extractBaseVersion(cliVersion);
if (!baseVersion)
baseVersion = extractBaseVersion(SystemUpdateService.semverVersion);
baseVersion = extractBaseVersion(ShellVersionService.semverVersion);
if (baseVersion) {
return `dms (git) v${baseVersion}-${match[1]}`;
}
@@ -253,8 +253,8 @@ Item {
}
StyledText {
visible: SystemUpdateService.shellCodename.length > 0
text: `"${SystemUpdateService.shellCodename}"`
visible: ShellVersionService.shellCodename.length > 0
text: `"${ShellVersionService.shellCodename}"`
font.pixelSize: Theme.fontSizeMedium
font.italic: true
color: Theme.surfaceVariantText

View File

@@ -325,6 +325,8 @@ Item {
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
onTextEdited: SettingsData.set("launchPrefix", text)
}
TerminalPickerRow {}
}
SettingsCard {

View File

@@ -1,11 +1,53 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
readonly property var intervalOptions: [
{
label: I18n.tr("Every 15 minutes"),
seconds: 900
},
{
label: I18n.tr("Every 30 minutes"),
seconds: 1800
},
{
label: I18n.tr("Every hour"),
seconds: 3600
},
{
label: I18n.tr("Every 4 hours"),
seconds: 14400
},
{
label: I18n.tr("Once a day"),
seconds: 86400
}
]
function intervalLabelFor(seconds) {
for (const opt of intervalOptions) {
if (opt.seconds === seconds) {
return opt.label;
}
}
return intervalOptions[1].label;
}
function intervalSecondsFor(label) {
for (const opt of intervalOptions) {
if (opt.label === label) {
return opt.seconds;
}
}
return 1800;
}
DankFlickable {
anchors.fill: parent
clip: true
@@ -25,18 +67,60 @@ Item {
title: I18n.tr("System Updater")
settingKey: "systemUpdater"
SettingsToggleRow {
text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found")
description: I18n.tr("When updater widget is used, then hide it if no update found")
checked: SettingsData.updaterHideWidget
onToggled: checked => {
SettingsData.set("updaterHideWidget", checked);
StyledText {
width: parent.width - Theme.spacingM * 2
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SystemUpdateService.backends.length > 0
text: {
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
return I18n.tr("Detected backends: %1").arg(names);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
SettingsDropdownRow {
text: I18n.tr("Check interval")
description: I18n.tr("How often the server polls for new updates.")
options: root.intervalOptions.map(o => o.label)
currentValue: root.intervalLabelFor(SettingsData.updaterIntervalSeconds)
onValueChanged: label => {
const secs = root.intervalSecondsFor(label);
SettingsData.set("updaterIntervalSeconds", secs);
SystemUpdateService.setInterval(secs);
}
}
SettingsToggleRow {
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")
text: I18n.tr("Include Flatpak updates")
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
checked: SettingsData.updaterIncludeFlatpak
onToggled: checked => SettingsData.set("updaterIncludeFlatpak", checked)
}
SettingsToggleRow {
text: I18n.tr("Include AUR updates")
description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.")
visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay")
checked: SettingsData.updaterAllowAUR
onToggled: checked => SettingsData.set("updaterAllowAUR", checked)
}
TerminalPickerRow {}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Advanced")
settingKey: "systemUpdaterAdvanced"
SettingsToggleRow {
text: I18n.tr("Use custom command")
description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
@@ -49,11 +133,32 @@ Item {
}
}
Rectangle {
width: parent.width - Theme.spacingM * 2
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
height: warnText.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
StyledText {
id: warnText
anchors.fill: parent
anchors.margins: Theme.spacingS
text: I18n.tr("Custom command and terminal params are split on whitespace; paths with spaces will break.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
wrapMode: Text.WordWrap
}
}
FocusScope {
width: parent.width - Theme.spacingM * 2
height: customCommandColumn.implicitHeight
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
Column {
id: customCommandColumn
@@ -61,7 +166,7 @@ Item {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("System update custom command")
text: I18n.tr("Custom update command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
@@ -69,7 +174,7 @@ Item {
DankTextField {
id: updaterCustomCommand
width: parent.width
placeholderText: "myPkgMngr --sysupdate"
placeholderText: "topgrade --no-retry"
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
@@ -98,6 +203,7 @@ Item {
height: terminalParamsColumn.implicitHeight
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
Column {
id: terminalParamsColumn
@@ -105,7 +211,7 @@ Item {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Terminal custom additional parameters")
text: I18n.tr("Terminal additional parameters")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
@@ -113,7 +219,7 @@ Item {
DankTextField {
id: updaterTerminalCustomClass
width: parent.width
placeholderText: "-T udpClass"
placeholderText: "-T updater"
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary

View File

@@ -0,0 +1,31 @@
import QtQuick
import qs.Common
SettingsDropdownRow {
id: root
readonly property string autoLabel: I18n.tr("Auto")
text: I18n.tr("Terminal")
settingKey: "terminalOverride"
options: {
const opts = [autoLabel];
const installed = SessionData.installedTerminals || [];
const list = installed.length > 0 ? installed : SessionData.terminalOptions;
for (const t of list) {
opts.push(t);
}
if (SessionData.terminalOverride && !opts.includes(SessionData.terminalOverride)) {
opts.push(SessionData.terminalOverride);
}
return opts;
}
currentValue: SessionData.terminalOverride.length > 0 ? SessionData.terminalOverride : autoLabel
onValueChanged: label => {
const next = label === autoLabel ? "" : label;
SessionData.set("terminalOverride", next);
}
}

View File

@@ -246,7 +246,8 @@ Item {
"text": I18n.tr("System Update"),
"description": I18n.tr("Check for system updates"),
"icon": "update",
"enabled": SystemUpdateService.distributionSupported
"enabled": SystemUpdateService.sysupdateAvailable,
"warning": SystemUpdateService.sysupdateAvailable ? undefined : I18n.tr("Requires DMS server with sysupdate capability")
},
{
"id": "powerMenuButton",
@@ -430,7 +431,7 @@ Item {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -579,6 +580,17 @@ Item {
setWidgetsForSection(sectionId, widgets);
}
function handleHideWhenIdleChanged(sectionId, widgetIndex, enabled) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
return;
}
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.hideWhenIdle = enabled;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleDiskUsageModeChanged(sectionId, widgetIndex, mode) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
@@ -714,6 +726,8 @@ Item {
item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
if (widget.hideWhenIdle !== undefined)
item.hideWhenIdle = widget.hideWhenIdle;
}
widgets.push(item);
});
@@ -1003,6 +1017,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
}
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
}
}
@@ -1070,6 +1087,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
}
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
}
}
@@ -1137,6 +1157,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
}
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
}
}
}

View File

@@ -33,6 +33,7 @@ Column {
signal showInGbChanged(string sectionId, int widgetIndex, bool enabled)
signal diskUsageModeChanged(string sectionId, int widgetIndex, int mode)
signal overflowSettingChanged(string sectionId, int widgetIndex, string settingName, var value)
signal hideWhenIdleChanged(string sectionId, int widgetIndex, bool enabled)
function cloneWidgetData(widget) {
var result = {
@@ -335,6 +336,25 @@ Column {
}
}
DankActionButton {
id: hideWhenIdleButton
buttonSize: 28
visible: modelData.id === "systemUpdate"
iconName: "visibility_off"
iconSize: 16
iconColor: (modelData.hideWhenIdle === true) ? Theme.primary : Theme.outline
onClicked: {
root.hideWhenIdleChanged(root.sectionId, index, modelData.hideWhenIdle !== true);
}
onEntered: {
const tooltipText = modelData.hideWhenIdle === true ? "Hide when no updates: ON" : "Hide when no updates: OFF";
sharedTooltip.show(tooltipText, hideWhenIdleButton, 0, 0, "bottom");
}
onExited: {
sharedTooltip.hide();
}
}
DankActionButton {
id: memMenuButton
visible: modelData.id === "memUsage"

View File

@@ -1,329 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: systemUpdatePopout
layerNamespace: "dms:system-update"
property var parentWidget: null
property var triggerScreen: null
Ref {
service: SystemUpdateService
}
popupWidth: 400
popupHeight: 500
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: close()
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
if (SystemUpdateService.updateCount === 0 && !SystemUpdateService.isChecking) {
SystemUpdateService.checkForUpdates();
}
}
}
content: Component {
Rectangle {
id: updaterPanel
color: "transparent"
Column {
width: parent.width - Theme.spacingL * 2
height: parent.height - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingL
Item {
width: parent.width
height: 40
StyledText {
text: I18n.tr("System Updates")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: {
if (SystemUpdateService.isChecking)
return I18n.tr("Checking...");
if (SystemUpdateService.hasError)
return I18n.tr("Error");
if (SystemUpdateService.updateCount === 0)
return I18n.tr("Up to date");
return SystemUpdateService.updateCount === 1
? I18n.tr("%1 update").arg(SystemUpdateService.updateCount)
: I18n.tr("%1 updates").arg(SystemUpdateService.updateCount);
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (SystemUpdateService.hasError)
return Theme.error;
return Theme.surfaceText;
}
}
DankActionButton {
id: checkForUpdatesButton
buttonSize: 28
iconName: "refresh"
iconSize: 18
z: 15
iconColor: Theme.surfaceText
enabled: !SystemUpdateService.isChecking
opacity: enabled ? 1.0 : 0.5
onClicked: {
SystemUpdateService.checkForUpdates();
}
RotationAnimation {
target: checkForUpdatesButton
property: "rotation"
from: 0
to: 360
duration: 1000
running: SystemUpdateService.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
checkForUpdatesButton.rotation = 0;
}
}
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingL;
usedHeight += 48 + Theme.spacingL;
return parent.height - usedHeight;
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 0
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: 0
StyledText {
id: statusText
width: parent.width
text: {
if (SystemUpdateService.hasError) {
return I18n.tr("Failed to check for updates:\n%1").arg(SystemUpdateService.errorMessage);
}
if (!SystemUpdateService.helperAvailable) {
return I18n.tr("No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.");
}
if (SystemUpdateService.isChecking) {
return I18n.tr("Checking for updates...");
}
if (SystemUpdateService.updateCount === 0) {
return I18n.tr("Your system is up to date!");
}
return SystemUpdateService.updateCount === 1
? I18n.tr("Found %1 package to update:").arg(SystemUpdateService.updateCount)
: I18n.tr("Found %1 packages to update:").arg(SystemUpdateService.updateCount);
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (SystemUpdateService.hasError)
return Theme.errorText;
return Theme.surfaceText;
}
wrapMode: Text.WordWrap
visible: SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking
}
DankListView {
id: packagesList
width: parent.width
height: parent.height - (SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking ? statusText.height + Theme.spacingM : 0)
visible: SystemUpdateService.updateCount > 0 && !SystemUpdateService.isChecking && !SystemUpdateService.hasError
clip: true
spacing: Theme.spacingXS
model: SystemUpdateService.availableUpdates
delegate: Rectangle {
width: ListView.view.width - Theme.spacingM
height: 48
radius: Theme.cornerRadius
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
border.color: Theme.outlineLight
border.width: 0
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.spacingM
spacing: 2
StyledText {
width: parent.width
text: modelData.name || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: `${modelData.currentVersion} ${modelData.newVersion}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
MouseArea {
id: packageMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
Row {
width: parent.width
height: 48
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: updateMouseArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover
opacity: SystemUpdateService.updateCount > 0 ? 1.0 : 0.5
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "system_update_alt"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Update All")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: updateMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: SystemUpdateService.updateCount > 0
onClicked: {
SystemUpdateService.runUpdates();
systemUpdatePopout.close();
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "close"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
systemUpdatePopout.close();
}
}
}
}
}
}
}
}

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;
}
}
}