From b8f4c350a83e1607cf287157ccab4ba96f67b2b2 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 11 May 2026 13:04:29 -0400 Subject: [PATCH] quickshell: drop support for 0.2, require 0.3+ - Remove all compat code - Rewire LegacyNetworkService to use Quickshell.Networking - Add parentWindow to settings child windows --- core/cmd/dms/shell.go | 3 - core/internal/config/embedded/hyprland.conf | 4 - core/internal/config/embedded/niri.kdl | 6 - core/internal/distros/arch.go | 23 +- core/internal/distros/base.go | 2 +- quickshell/Common/CacheUtils.qml | 23 +- quickshell/Common/Proc.qml | 39 +- quickshell/DMSShell.qml | 2 + .../Modals/Changelog/ChangelogModal.qml | 2 +- .../Modals/FileBrowser/FileBrowserModal.qml | 1 + quickshell/Modals/Greeter/GreeterModal.qml | 2 +- quickshell/Modals/PolkitAuthContent.qml | 378 +++++ quickshell/Modals/PolkitAuthModal.qml | 409 +----- quickshell/Modals/PolkitAuthSurfaceModal.qml | 51 + quickshell/Modals/ProcessListModal.qml | 2 +- quickshell/Modals/Settings/SettingsModal.qml | 2 +- quickshell/Modals/WifiPasswordModal.qml | 2 +- quickshell/Modals/WorkspaceRenameModal.qml | 4 +- .../Modules/BlurredWallpaperBackground.qml | 3 +- quickshell/Modules/DankBar/DankBarWindow.qml | 106 +- .../DankBar/Popouts/SystemUpdatePopout.qml | 27 +- .../Modules/DankBar/Widgets/SystemTrayBar.qml | 9 +- quickshell/Modules/Frame/FrameWindow.qml | 1 + quickshell/Modules/Lock/FadeToLockWindow.qml | 23 +- quickshell/Modules/Lock/VideoScreensaver.qml | 67 +- .../Modules/Lock/VideoScreensaverPlayer.qml | 8 + .../Modules/Notepad/NotepadTextEditor.qml | 64 +- .../Notifications/Popup/NotificationPopup.qml | 11 +- .../Modules/Settings/DesktopWidgetBrowser.qml | 3 +- quickshell/Modules/Settings/PluginBrowser.qml | 4 +- quickshell/Modules/Settings/PowerSleepTab.qml | 8 - quickshell/Modules/Settings/ThemeBrowser.qml | 3 +- .../Modules/Settings/ThemeColorsTab.qml | 1 + .../Modules/Settings/WidgetSelectionPopup.qml | 3 +- quickshell/Modules/WallpaperBackground.qml | 3 +- quickshell/Services/AudioService.qml | 205 +-- quickshell/Services/AudioSoundPlayers.qml | 80 + quickshell/Services/BlurService.qml | 54 +- quickshell/Services/IdleService.qml | 177 +-- quickshell/Services/KeybindsService.qml | 18 +- quickshell/Services/LegacyNetworkService.qml | 1288 ++++++----------- quickshell/Services/MultimediaProbe.qml | 7 + quickshell/Services/MultimediaService.qml | 31 +- quickshell/Services/PluginService.qml | 112 +- quickshell/Services/PolkitService.qml | 31 +- quickshell/Services/SessionService.qml | 77 +- quickshell/Widgets/DankAlbumArt.qml | 7 +- quickshell/Widgets/DankPopoutStandalone.qml | 27 +- quickshell/Widgets/FloatingWindowControls.qml | 23 +- quickshell/Widgets/KeybindItem.qml | 39 +- quickshell/Widgets/WindowBlur.qml | 59 +- quickshell/shell.qml | 2 +- 52 files changed, 1472 insertions(+), 2064 deletions(-) create mode 100644 quickshell/Modals/PolkitAuthContent.qml create mode 100644 quickshell/Modals/PolkitAuthSurfaceModal.qml create mode 100644 quickshell/Modules/Lock/VideoScreensaverPlayer.qml create mode 100644 quickshell/Services/AudioSoundPlayers.qml create mode 100644 quickshell/Services/MultimediaProbe.qml diff --git a/core/cmd/dms/shell.go b/core/cmd/dms/shell.go index b481d7b3..b5bcc615 100644 --- a/core/cmd/dms/shell.go +++ b/core/cmd/dms/shell.go @@ -202,9 +202,6 @@ func runShellInteractive(session bool) { } } - // ! TODO - remove when QS 0.3 is up and we can use the pragma - cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms") - if isSessionManaged && hasSystemdRun() { cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") } diff --git a/core/internal/config/embedded/hyprland.conf b/core/internal/config/embedded/hyprland.conf index f4e7a03e..d9cadb26 100644 --- a/core/internal/config/embedded/hyprland.conf +++ b/core/internal/config/embedded/hyprland.conf @@ -107,10 +107,6 @@ windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts) windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$ windowrule = float on, match:class ^(zoom)$ -# DMS windows floating by default -# ! Hyprland doesn't size these windows correctly so disabling by default here -# windowrule = float on, match:class ^(org.quickshell)$ - layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^dms:.* diff --git a/core/internal/config/embedded/niri.kdl b/core/internal/config/embedded/niri.kdl index 0a759e46..13d08594 100644 --- a/core/internal/config/embedded/niri.kdl +++ b/core/internal/config/embedded/niri.kdl @@ -250,12 +250,6 @@ window-rule { match app-id="zoom" open-floating true } -// Open dms windows as floating by default -window-rule { - match app-id=r#"org.quickshell$"# - match app-id=r#"com.danklinux.dms$"# - open-floating true -} debug { honor-xdg-activation-with-invalid-serial } diff --git a/core/internal/distros/arch.go b/core/internal/distros/arch.go index 7388b339..997479d1 100644 --- a/core/internal/distros/arch.go +++ b/core/internal/distros/arch.go @@ -208,8 +208,7 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac if forceQuickshellGit || variant == deps.VariantGit { return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} } - // ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell - return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} + return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem} } func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { @@ -332,6 +331,12 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" }) } + if slices.Contains(systemPkgs, "quickshell") && a.packageInstalled("quickshell-git") { + if err := a.removeQuickshellGit(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to remove quickshell-git: %w", err) + } + } + // Phase 3: System Packages if len(systemPkgs) > 0 { progressChan <- InstallProgressMsg{ @@ -449,6 +454,20 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm return systemPkgs, aurPkgs, manualPkgs, variantMap } +func (a *ArchDistribution) removeQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.33, + Step: "Removing quickshell-git...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo pacman -Rdd --noconfirm quickshell-git", + LogOutput: "Removing quickshell-git so stable quickshell can be installed", + } + cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell-git") + return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.33, 0.35) +} + func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if a.packageInstalled("quickshell-git") { return nil diff --git a/core/internal/distros/base.go b/core/internal/distros/base.go index 140a68b3..c1627759 100644 --- a/core/internal/distros/base.go +++ b/core/internal/distros/base.go @@ -232,7 +232,7 @@ func (b *BaseDistribution) detectQuickshell() deps.Dependency { } versionStr := string(output) - versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`) + versionRegex := regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`) matches := versionRegex.FindStringSubmatch(versionStr) if len(matches) < 2 { diff --git a/quickshell/Common/CacheUtils.qml b/quickshell/Common/CacheUtils.qml index 1f03f0e3..85d4f2b4 100644 --- a/quickshell/Common/CacheUtils.qml +++ b/quickshell/Common/CacheUtils.qml @@ -1,40 +1,29 @@ pragma Singleton pragma ComponentBehavior: Bound +import QtQuick import Quickshell +import qs.Common Singleton { id: root - // Clear all image cache function clearImageCache() { Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]); Paths.mkdir(Paths.imagecache); } - // Clear cache older than specified minutes function clearOldCache(ageInMinutes) { Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]); } - // Clear cache for specific size function clearCacheForSize(size) { Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]); } - // Get cache size in MB function getCacheSize(callback) { - var process = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["du", "-sm", "${Paths.stringify(Paths.imagecache)}"] - running: true - stdout: StdioCollector { - onStreamFinished: { - var sizeMB = parseInt(text.split("\\t")[0]) || 0 - callback(sizeMB) - } - } - } - `, root); + Proc.runCommand("cache_size", ["du", "-sm", Paths.stringify(Paths.imagecache)], function (output, exitCode) { + const sizeMB = parseInt(output.split("\t")[0]) || 0; + callback(sizeMB); + }); } } diff --git a/quickshell/Common/Proc.qml b/quickshell/Common/Proc.qml index e84239ff..b38f5140 100644 --- a/quickshell/Common/Proc.qml +++ b/quickshell/Common/Proc.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.Io import qs.Services Singleton { @@ -21,7 +22,7 @@ Singleton { const isRandomId = !id; if (!_procDebouncers[procId]) { - const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root); + const t = debounceTimerComp.createObject(root); t.triggered.connect(function () { _launchProc(procId, isRandomId); }); @@ -49,14 +50,10 @@ Singleton { const entry = _procDebouncers[id]; if (!entry) return; - const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root); - const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc); - const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc); - const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root); - - proc.stdout = out; - proc.stderr = err; - proc.command = entry.command; + const proc = procComp.createObject(root, { + command: entry.command + }); + const timeoutTimer = debounceTimerComp.createObject(root); let capturedOut = ""; let capturedErr = ""; @@ -77,9 +74,9 @@ Singleton { } }); - out.streamFinished.connect(function () { + proc.stdout.streamFinished.connect(function () { try { - capturedOut = out.text || ""; + capturedOut = proc.stdout.text || ""; } catch (e) { capturedOut = ""; } @@ -87,9 +84,9 @@ Singleton { maybeComplete(); }); - err.streamFinished.connect(function () { + proc.stderr.streamFinished.connect(function () { try { - capturedErr = err.text || ""; + capturedErr = proc.stderr.text || ""; } catch (e) { capturedErr = ""; } @@ -140,4 +137,20 @@ Singleton { if (entry.timeoutMs !== noTimeout) timeoutTimer.start(); } + + Component { + id: debounceTimerComp + Timer { + repeat: false + } + } + + Component { + id: procComp + Process { + running: false + stdout: StdioCollector {} + stderr: StdioCollector {} + } + } } diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 184417c6..4ece67af 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -523,6 +523,8 @@ Item { enabled: PolkitService.polkitAvailable function onAuthenticationRequestStarted() { + if (PopoutService.systemUpdatePopout?.shouldBeVisible) + return; polkitAuthModalLoader.active = true; if (polkitAuthModalLoader.item) polkitAuthModalLoader.item.show(); diff --git a/quickshell/Modals/Changelog/ChangelogModal.qml b/quickshell/Modals/Changelog/ChangelogModal.qml index 524298b9..fe9c41ce 100644 --- a/quickshell/Modals/Changelog/ChangelogModal.qml +++ b/quickshell/Modals/Changelog/ChangelogModal.qml @@ -67,7 +67,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported && windowControls.canMaximize + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText diff --git a/quickshell/Modals/FileBrowser/FileBrowserModal.qml b/quickshell/Modals/FileBrowser/FileBrowserModal.qml index c7686ca7..df0c5552 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserModal.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserModal.qml @@ -16,6 +16,7 @@ FloatingWindow { property bool saveMode: false property string defaultFileName: "" property var parentModal: null + parentWindow: parentModal property bool shouldHaveFocus: visible property bool allowFocusOverride: false property bool shouldBeVisible: visible diff --git a/quickshell/Modals/Greeter/GreeterModal.qml b/quickshell/Modals/Greeter/GreeterModal.qml index 49581fab..2c8d0714 100644 --- a/quickshell/Modals/Greeter/GreeterModal.qml +++ b/quickshell/Modals/Greeter/GreeterModal.qml @@ -215,7 +215,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported && windowControls.canMaximize + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText diff --git a/quickshell/Modals/PolkitAuthContent.qml b/quickshell/Modals/PolkitAuthContent.qml new file mode 100644 index 00000000..f1c33184 --- /dev/null +++ b/quickshell/Modals/PolkitAuthContent.qml @@ -0,0 +1,378 @@ +import QtQuick +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +FocusScope { + id: root + + property var currentFlow: PolkitService.agent?.flow + property string passwordInput: "" + property bool isLoading: false + property bool awaitingFprintForPassword: false + property var windowControls: null + readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 + + property string polkitEtcPamText: "" + property string polkitLibPamText: "" + property string systemAuthPamText: "" + property string commonAuthPamText: "" + property string passwordAuthPamText: "" + readonly property bool polkitPamHasFprint: { + const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText; + if (!polkitText) + return false; + return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd")); + } + + signal closeRequested + signal authenticationSucceeded + + focus: true + + Keys.onEscapePressed: event => { + cancelAuth(); + event.accepted = true; + } + + function stripPamComment(line) { + if (!line) + return ""; + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) + return ""; + const hashIdx = trimmed.indexOf("#"); + if (hashIdx >= 0) + return trimmed.substring(0, hashIdx).trim(); + return trimmed; + } + + function pamModuleEnabled(pamText, moduleName) { + if (!pamText || !moduleName) + return false; + const lines = pamText.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const line = stripPamComment(lines[i]); + if (line && line.includes(moduleName)) + return true; + } + return false; + } + + function focusPasswordField() { + passwordField.forceActiveFocus(); + } + + function reset() { + passwordInput = ""; + isLoading = false; + awaitingFprintForPassword = false; + } + + function _commitSubmit() { + isLoading = true; + awaitingFprintForPassword = false; + currentFlow.submit(passwordInput); + passwordInput = ""; + } + + function submitAuth() { + if (!currentFlow || isLoading) + return; + if (!currentFlow.isResponseRequired) { + awaitingFprintForPassword = true; + return; + } + _commitSubmit(); + } + + function cancelAuth() { + if (isLoading) + return; + awaitingFprintForPassword = false; + if (currentFlow) { + currentFlow.cancelAuthenticationRequest(); + return; + } + closeRequested(); + } + + Connections { + target: root.currentFlow + enabled: root.currentFlow !== null + + function onIsResponseRequiredChanged() { + if (!root.currentFlow.isResponseRequired) + return; + if (root.awaitingFprintForPassword && root.passwordInput !== "") { + root._commitSubmit(); + return; + } + root.awaitingFprintForPassword = false; + root.isLoading = false; + root.passwordInput = ""; + passwordField.forceActiveFocus(); + } + + function onAuthenticationSucceeded() { + root.authenticationSucceeded(); + root.closeRequested(); + } + + function onAuthenticationFailed() { + root.isLoading = false; + } + + function onAuthenticationRequestCancelled() { + root.closeRequested(); + } + } + + FileView { + path: "/etc/pam.d/polkit-1" + printErrors: false + onLoaded: root.polkitEtcPamText = text() + onLoadFailed: root.polkitEtcPamText = "" + } + + FileView { + path: "/usr/lib/pam.d/polkit-1" + printErrors: false + onLoaded: root.polkitLibPamText = text() + onLoadFailed: root.polkitLibPamText = "" + } + + FileView { + path: "/etc/pam.d/system-auth" + printErrors: false + onLoaded: root.systemAuthPamText = text() + onLoadFailed: root.systemAuthPamText = "" + } + + FileView { + path: "/etc/pam.d/common-auth" + printErrors: false + onLoaded: root.commonAuthPamText = text() + onLoadFailed: root.commonAuthPamText = "" + } + + FileView { + path: "/etc/pam.d/password-auth" + printErrors: false + onLoaded: root.passwordAuthPamText = text() + onLoadFailed: root.passwordAuthPamText = "" + } + + Item { + id: headerSection + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight) + + MouseArea { + anchors.fill: parent + enabled: root.windowControls !== null + onPressed: { + if (root.windowControls) + root.windowControls.tryStartMove(); + } + onDoubleClicked: { + if (root.windowControls) + root.windowControls.tryToggleMaximize(); + } + } + + Column { + id: titleColumn + anchors.left: parent.left + anchors.right: windowButtonRow.left + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Authentication Required") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: root.currentFlow?.message ?? "" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + visible: text !== "" + } + + StyledText { + text: root.currentFlow?.supplementaryMessage ?? "" + font.pixelSize: Theme.fontSizeSmall + color: (root.currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + opacity: (root.currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8 + visible: text !== "" + } + } + + Row { + id: windowButtonRow + anchors.right: parent.right + anchors.top: parent.top + spacing: Theme.spacingXS + + DankActionButton { + visible: root.windowControls?.supported === true && root.windowControls?.canMaximize === true + iconName: (root.windowControls?.targetWindow?.maximized ?? false) ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: { + if (root.windowControls) + root.windowControls.tryToggleMaximize(); + } + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + enabled: !root.isLoading + opacity: enabled ? 1 : 0.5 + onClicked: root.cancelAuth() + } + } + } + + Column { + id: bottomSection + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + StyledText { + text: root.currentFlow?.inputPrompt ?? "" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + visible: text !== "" + } + + DankTextField { + id: passwordField + + width: parent.width + height: root.inputFieldHeight + backgroundColor: Theme.surfaceHover + normalBorderColor: Theme.outlineStrong + focusedBorderColor: Theme.primary + borderWidth: 1 + focusedBorderWidth: 2 + leftIconName: root.polkitPamHasFprint ? "fingerprint" : "" + leftIconSize: 20 + leftIconColor: Theme.primary + leftIconFocusedColor: Theme.primary + opacity: root.isLoading ? 0.5 : 1 + font.pixelSize: Theme.fontSizeMedium + textColor: Theme.surfaceText + text: root.passwordInput + showPasswordToggle: !(root.currentFlow?.responseVisible ?? false) + echoMode: (root.currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password + placeholderText: "" + enabled: !root.isLoading + onTextEdited: root.passwordInput = text + onAccepted: root.submitAuth() + } + + StyledText { + text: I18n.tr("Authentication failed, please try again") + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + width: parent.width + visible: root.currentFlow?.failed ?? false + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Rectangle { + width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent" + border.color: Theme.surfaceVariantAlpha + border.width: 1 + enabled: !root.isLoading + opacity: enabled ? 1 : 0.5 + + StyledText { + id: cancelText + anchors.centerIn: parent + text: I18n.tr("Cancel") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: cancelArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: parent.enabled + onClicked: root.cancelAuth() + } + } + + Rectangle { + width: Math.max(80, authText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + enabled: !root.isLoading + opacity: enabled ? 1 : 0.5 + + StyledText { + id: authText + anchors.centerIn: parent + text: I18n.tr("Authenticate") + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: authArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: parent.enabled + onClicked: root.submitAuth() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } +} diff --git a/quickshell/Modals/PolkitAuthModal.qml b/quickshell/Modals/PolkitAuthModal.qml index 4d99820c..6e93335d 100644 --- a/quickshell/Modals/PolkitAuthModal.qml +++ b/quickshell/Modals/PolkitAuthModal.qml @@ -1,427 +1,68 @@ import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Wayland import qs.Common import qs.Services import qs.Widgets -PanelWindow { +FloatingWindow { id: root property bool disablePopupTransparency: true - property string passwordInput: "" - property var currentFlow: PolkitService.agent?.flow - property bool isLoading: false - property bool awaitingFprintForPassword: false - readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 - property string polkitEtcPamText: "" - property string polkitLibPamText: "" - property string systemAuthPamText: "" - property string commonAuthPamText: "" - property string passwordAuthPamText: "" - readonly property bool polkitPamHasFprint: { - const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText; - if (!polkitText) - return false; - return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd")); - } - - function stripPamComment(line) { - if (!line) - return ""; - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) - return ""; - const hashIdx = trimmed.indexOf("#"); - if (hashIdx >= 0) - return trimmed.substring(0, hashIdx).trim(); - return trimmed; - } - - function pamModuleEnabled(pamText, moduleName) { - if (!pamText || !moduleName) - return false; - const lines = pamText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (line && line.includes(moduleName)) - return true; - } - return false; - } - - function focusPasswordField() { - passwordField.forceActiveFocus(); - } - - function show(targetScreen) { - if (targetScreen) - screen = targetScreen; - passwordInput = ""; - isLoading = false; - awaitingFprintForPassword = false; + function show() { + if (contentLoader.item) + contentLoader.item.reset(); visible = true; - Qt.callLater(focusPasswordField); + Qt.callLater(focusContent); } function hide() { visible = false; } - function _commitSubmit() { - isLoading = true; - awaitingFprintForPassword = false; - currentFlow.submit(passwordInput); - passwordInput = ""; - } - - function submitAuth() { - if (!currentFlow || isLoading) - return; - if (!currentFlow.isResponseRequired) { - awaitingFprintForPassword = true; - return; - } - _commitSubmit(); - } - - function cancelAuth() { - if (isLoading) - return; - awaitingFprintForPassword = false; - if (currentFlow) { - currentFlow.cancelAuthenticationRequest(); - return; - } - hide(); + function focusContent() { + if (contentLoader.item) + contentLoader.item.focusPasswordField(); } objectName: "polkitAuthModal" - - screen: Quickshell.screens[0] - color: "transparent" + title: I18n.tr("Authentication") + minimumSize: Qt.size(460, 220) + maximumSize: Qt.size(460, 220) + color: Theme.surfaceContainer visible: false - WlrLayershell.namespace: "dms:polkit-auth" - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - - anchors { - left: true - top: true - right: true - bottom: true - } - onVisibleChanged: { if (visible) { - Qt.callLater(focusPasswordField); + Qt.callLater(focusContent); return; } - passwordInput = ""; - isLoading = false; - awaitingFprintForPassword = false; + if (contentLoader.item) + contentLoader.item.reset(); } Connections { target: PolkitService.agent enabled: PolkitService.polkitAvailable - function onAuthenticationRequestStarted() { - show(); - } - function onIsActiveChanged() { if (!(PolkitService.agent?.isActive ?? false)) - hide(); + root.hide(); } } - Connections { - target: currentFlow - enabled: currentFlow !== null - - function onIsResponseRequiredChanged() { - if (!currentFlow.isResponseRequired) - return; - if (awaitingFprintForPassword && passwordInput !== "") { - _commitSubmit(); - return; - } - awaitingFprintForPassword = false; - isLoading = false; - passwordInput = ""; - passwordField.forceActiveFocus(); - } - - function onAuthenticationSucceeded() { - hide(); - } - - function onAuthenticationFailed() { - isLoading = false; - } - - function onAuthenticationRequestCancelled() { - hide(); - } - } - - FileView { - path: "/etc/pam.d/polkit-1" - printErrors: false - onLoaded: root.polkitEtcPamText = text() - onLoadFailed: root.polkitEtcPamText = "" - } - - FileView { - path: "/usr/lib/pam.d/polkit-1" - printErrors: false - onLoaded: root.polkitLibPamText = text() - onLoadFailed: root.polkitLibPamText = "" - } - - FileView { - path: "/etc/pam.d/system-auth" - printErrors: false - onLoaded: root.systemAuthPamText = text() - onLoadFailed: root.systemAuthPamText = "" - } - - FileView { - path: "/etc/pam.d/common-auth" - printErrors: false - onLoaded: root.commonAuthPamText = text() - onLoadFailed: root.commonAuthPamText = "" - } - - FileView { - path: "/etc/pam.d/password-auth" - printErrors: false - onLoaded: root.passwordAuthPamText = text() - onLoadFailed: root.passwordAuthPamText = "" - } - - // Dim overlay — clicking outside cancels authentication - Rectangle { + Loader { + id: contentLoader anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.45) - - MouseArea { - anchors.fill: parent - onClicked: cancelAuth() + active: root.visible + sourceComponent: PolkitAuthContent { + windowControls: windowControls + onCloseRequested: root.hide() } } - // Centered dialog box - Rectangle { - id: dialogBox - width: 460 - height: 220 - anchors.centerIn: parent - color: Theme.surfaceContainer - radius: Theme.cornerRadius - - FocusScope { - id: contentFocusScope - anchors.fill: parent - focus: true - - Keys.onEscapePressed: event => { - cancelAuth(); - event.accepted = true; - } - - Item { - id: headerSection - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingM - height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight) - - Column { - id: titleColumn - anchors.left: parent.left - anchors.right: windowButtonRow.left - anchors.rightMargin: Theme.spacingM - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Authentication Required") - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - StyledText { - text: currentFlow?.message ?? "" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceTextMedium - width: parent.width - wrapMode: Text.Wrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text !== "" - } - - StyledText { - text: currentFlow?.supplementaryMessage ?? "" - font.pixelSize: Theme.fontSizeSmall - color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium - width: parent.width - wrapMode: Text.Wrap - maximumLineCount: 2 - elide: Text.ElideRight - opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8 - visible: text !== "" - } - } - - Row { - id: windowButtonRow - anchors.right: parent.right - anchors.top: parent.top - spacing: Theme.spacingXS - - DankActionButton { - iconName: "close" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - enabled: !isLoading - opacity: enabled ? 1 : 0.5 - onClicked: cancelAuth() - } - } - } - - Column { - id: bottomSection - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - StyledText { - text: currentFlow?.inputPrompt ?? "" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - width: parent.width - visible: text !== "" - } - - DankTextField { - id: passwordField - - width: parent.width - height: inputFieldHeight - backgroundColor: Theme.surfaceHover - normalBorderColor: Theme.outlineStrong - focusedBorderColor: Theme.primary - borderWidth: 1 - focusedBorderWidth: 2 - leftIconName: polkitPamHasFprint ? "fingerprint" : "" - leftIconSize: 20 - leftIconColor: Theme.primary - leftIconFocusedColor: Theme.primary - opacity: isLoading ? 0.5 : 1 - font.pixelSize: Theme.fontSizeMedium - textColor: Theme.surfaceText - text: passwordInput - showPasswordToggle: !(currentFlow?.responseVisible ?? false) - echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password - placeholderText: "" - enabled: !isLoading - onTextEdited: passwordInput = text - onAccepted: submitAuth() - } - - StyledText { - text: I18n.tr("Authentication failed, please try again") - font.pixelSize: Theme.fontSizeSmall - color: Theme.error - width: parent.width - visible: currentFlow?.failed ?? false - } - - Item { - width: parent.width - height: 36 - - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - - Rectangle { - width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2) - height: 36 - radius: Theme.cornerRadius - color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent" - border.color: Theme.surfaceVariantAlpha - border.width: 1 - enabled: !isLoading - opacity: enabled ? 1 : 0.5 - - StyledText { - id: cancelText - anchors.centerIn: parent - text: I18n.tr("Cancel") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - MouseArea { - id: cancelArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: parent.enabled - onClicked: cancelAuth() - } - } - - Rectangle { - width: Math.max(80, authText.contentWidth + Theme.spacingM * 2) - height: 36 - radius: Theme.cornerRadius - color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary - enabled: !isLoading - opacity: enabled ? 1 : 0.5 - - StyledText { - id: authText - anchors.centerIn: parent - text: I18n.tr("Authenticate") - font.pixelSize: Theme.fontSizeMedium - color: Theme.background - font.weight: Font.Medium - } - - MouseArea { - id: authArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: parent.enabled - onClicked: submitAuth() - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - } - } - } - } + FloatingWindowControls { + id: windowControls + targetWindow: root } } diff --git a/quickshell/Modals/PolkitAuthSurfaceModal.qml b/quickshell/Modals/PolkitAuthSurfaceModal.qml new file mode 100644 index 00000000..dd25fd59 --- /dev/null +++ b/quickshell/Modals/PolkitAuthSurfaceModal.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell.Wayland +import qs.Common +import qs.Modals.Common +import qs.Services + +DankModal { + id: root + + property var parentPopout: null + + layerNamespace: "dms:polkit-auth-surface" + modalWidth: 460 + modalHeight: 220 + backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + closeOnEscapeKey: true + closeOnBackgroundClick: false + allowStacking: true + keepPopoutsOpen: true + + onOpened: { + if (parentPopout) + parentPopout.customKeyboardFocus = WlrKeyboardFocus.None; + Qt.callLater(() => { + if (contentLoader.item) { + contentLoader.item.reset(); + contentLoader.item.focusPasswordField(); + } + }); + } + + onDialogClosed: { + if (parentPopout) + parentPopout.customKeyboardFocus = null; + } + + Connections { + target: PolkitService.agent + enabled: PolkitService.polkitAvailable + + function onIsActiveChanged() { + if (!(PolkitService.agent?.isActive ?? false)) + root.close(); + } + } + + content: PolkitAuthContent { + focus: true + onCloseRequested: root.close() + } +} diff --git a/quickshell/Modals/ProcessListModal.qml b/quickshell/Modals/ProcessListModal.qml index dc872033..05c63fa5 100644 --- a/quickshell/Modals/ProcessListModal.qml +++ b/quickshell/Modals/ProcessListModal.qml @@ -274,7 +274,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize circular: false iconName: processListModal.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 9ce91530..366c8669 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -235,7 +235,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize circular: false iconName: settingsModal.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 diff --git a/quickshell/Modals/WifiPasswordModal.qml b/quickshell/Modals/WifiPasswordModal.qml index 92689f20..d60fe05b 100644 --- a/quickshell/Modals/WifiPasswordModal.qml +++ b/quickshell/Modals/WifiPasswordModal.qml @@ -381,7 +381,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported && windowControls.canMaximize + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText diff --git a/quickshell/Modals/WorkspaceRenameModal.qml b/quickshell/Modals/WorkspaceRenameModal.qml index 9e250e94..fc91d8c1 100644 --- a/quickshell/Modals/WorkspaceRenameModal.qml +++ b/quickshell/Modals/WorkspaceRenameModal.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Io import qs.Common import qs.Services import qs.Widgets @@ -97,7 +96,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported && windowControls.canMaximize + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText @@ -213,5 +212,4 @@ FloatingWindow { id: windowControls targetWindow: root } - } diff --git a/quickshell/Modules/BlurredWallpaperBackground.qml b/quickshell/Modules/BlurredWallpaperBackground.qml index 6a5c6e86..d05eec72 100644 --- a/quickshell/Modules/BlurredWallpaperBackground.qml +++ b/quickshell/Modules/BlurredWallpaperBackground.qml @@ -85,8 +85,7 @@ Variants { } Component.onCompleted: { - if (typeof blurWallpaperWindow.updatesEnabled !== "undefined") - blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); + blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); isInitialized = true; } diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index b7ea7560..171bd163 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -152,6 +152,20 @@ PanelWindow { onTriggered: barBlur.rebuild() } + Component { + id: blurRegionComp + Region {} + } + + Component { + id: blurSubRegionComp + Region { + property Item w + item: w + radius: Theme.cornerRadius + } + } + Item { id: barBlur visible: false @@ -173,33 +187,32 @@ PanelWindow { if (!hasBar && widgets.length === 0) return; - const cr = Theme.cornerRadius; - let qml = 'import QtQuick; import Quickshell; Region {'; + const region = blurRegionComp.createObject(barWindow); + if (!region) { + log.warn("BarBlur: Failed to create blur region"); + return; + } + + if (hasBar) { + region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x); + region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y); + region.width = Qt.binding(() => barUnitInset.width); + region.height = Qt.binding(() => barUnitInset.height); + region.radius = Qt.binding(() => barBackground.rt); + } + + const subRegions = []; for (let i = 0; i < widgets.length; i++) { - qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`; + const sub = blurSubRegionComp.createObject(region, { + w: widgets[i] + }); + if (sub) + subRegions.push(sub); } - qml += '}'; + region.regions = subRegions; - try { - const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion"); - - if (hasBar) { - region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x); - region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y); - region.width = Qt.binding(() => barUnitInset.width); - region.height = Qt.binding(() => barUnitInset.height); - region.radius = Qt.binding(() => barBackground.rt); - } - - for (let i = 0; i < widgets.length; i++) { - region[`w${i}`] = widgets[i]; - } - - barWindow.BackgroundEffect.blurRegion = region; - barWindow.blurRegion = region; - } catch (e) { - log.warn("BarBlur: Failed to create blur region:", e); - } + barWindow.BackgroundEffect.blurRegion = region; + barWindow.blurRegion = region; } function teardown() { @@ -529,27 +542,17 @@ PanelWindow { implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 color: "transparent" - property var nativeInhibitor: null - Component.onCompleted: { updateGpuTempConfig(); _updateBackgroundAlpha(); _updateHasMaximizedToplevel(); _updateHasFullscreenToplevel(); _updateShouldHideForWindows(); - - inhibitorInitTimer.start(); } - Timer { - id: inhibitorInitTimer - interval: 300 - repeat: false - onTriggered: { - if (SessionService.nativeInhibitorAvailable) { - createNativeInhibitor(); - } - } + IdleInhibitor { + window: barWindow + enabled: SessionService.idleInhibited } Connections { @@ -581,35 +584,6 @@ PanelWindow { DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled; } - function createNativeInhibitor() { - if (!SessionService.nativeInhibitorAvailable) { - return; - } - - try { - const qmlString = ` - import QtQuick - import Quickshell.Wayland - - IdleInhibitor { - enabled: false - } - `; - - nativeInhibitor = Qt.createQmlObject(qmlString, barWindow, "DankBar.NativeInhibitor"); - nativeInhibitor.window = barWindow; - nativeInhibitor.enabled = Qt.binding(() => SessionService.idleInhibited); - nativeInhibitor.enabledChanged.connect(function () { - if (SessionService.idleInhibited !== nativeInhibitor.enabled) { - SessionService.idleInhibited = nativeInhibitor.enabled; - SessionService.inhibitorChanged(); - } - }); - } catch (e) { - nativeInhibitor = null; - } - } - Connections { function onBarConfigChanged() { barWindow.updateGpuTempConfig(); diff --git a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml index a37d7d67..584a8d70 100644 --- a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml +++ b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell.Wayland import qs.Common +import qs.Modals import qs.Services import qs.Widgets @@ -18,9 +19,23 @@ DankPopout { property bool _reopenAfterUpgrade: false - readonly property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false + readonly property bool polkitModalOpen: polkitAuthSurfaceModal.shouldBeVisible readonly property bool anyModalOpen: polkitModalOpen + Connections { + target: PolkitService.agent + enabled: PolkitService.polkitAvailable && systemUpdatePopout.shouldBeVisible + + function onAuthenticationRequestStarted() { + polkitAuthSurfaceModal.open(); + } + } + + PolkitAuthSurfaceModal { + id: polkitAuthSurfaceModal + parentPopout: systemUpdatePopout + } + backgroundInteractive: !anyModalOpen customKeyboardFocus: { @@ -33,16 +48,6 @@ DankPopout { return WlrKeyboardFocus.Exclusive; } - Connections { - target: PolkitService.agent - enabled: PolkitService.polkitAvailable && triggerScreen !== null - - function onAuthenticationRequestStarted() { - if (PopoutService.polkitAuthModal && triggerScreen) - PopoutService.polkitAuthModal.screen = triggerScreen; - } - } - Connections { target: SystemUpdateService function onIsUpgradingChanged() { diff --git a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml index 0f79b049..687e9d2f 100644 --- a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml +++ b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml @@ -1397,6 +1397,13 @@ BasePill { close(); } + Timer { + id: pendingActionCloseTimer + interval: 80 + repeat: false + onTriggered: menuRoot.closeWithAction() + } + function showSubMenu(entry) { if (!entry || !entry.hasChildren) return; @@ -1853,7 +1860,7 @@ BasePill { } else if (typeof menuEntry.triggered === "function") { menuEntry.triggered(); } - Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot); + pendingActionCloseTimer.restart(); } } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 54a0ac2a..4a3568ce 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -18,6 +18,7 @@ PanelWindow { screen: targetScreen visible: _frameActive + updatesEnabled: _connectedActive WlrLayershell.namespace: "dms:frame" WlrLayershell.layer: WlrLayer.Top diff --git a/quickshell/Modules/Lock/FadeToLockWindow.qml b/quickshell/Modules/Lock/FadeToLockWindow.qml index c73a59ab..ae92c97c 100644 --- a/quickshell/Modules/Lock/FadeToLockWindow.qml +++ b/quickshell/Modules/Lock/FadeToLockWindow.qml @@ -4,11 +4,13 @@ import QtQuick import Quickshell import Quickshell.Wayland import qs.Common +import qs.Services PanelWindow { id: root property bool active: false + property bool _completed: false signal fadeCompleted signal fadeCancelled @@ -35,7 +37,8 @@ PanelWindow { opacity: 0 onOpacityChanged: { - if (opacity >= 0.99 && root.active) { + if (opacity >= 0.99 && root.active && !root._completed) { + root._completed = true; root.fadeCompleted(); } } @@ -58,6 +61,7 @@ PanelWindow { function startFade() { if (!SettingsData.fadeToLockEnabled) return; + _completed = false; active = true; fadeOverlay.opacity = 0.0; fadeSeq.stop(); @@ -65,12 +69,29 @@ PanelWindow { } function cancelFade() { + if (_completed) + return; fadeSeq.stop(); fadeOverlay.opacity = 0.0; active = false; fadeCancelled(); } + function dismiss() { + fadeSeq.stop(); + fadeOverlay.opacity = 0.0; + active = false; + _completed = false; + } + + Connections { + target: IdleService + function onIsShellLockedChanged() { + if (!IdleService.isShellLocked && root._completed) + root.dismiss(); + } + } + MouseArea { anchors.fill: parent enabled: root.active diff --git a/quickshell/Modules/Lock/VideoScreensaver.qml b/quickshell/Modules/Lock/VideoScreensaver.qml index a04fe545..66ed3242 100644 --- a/quickshell/Modules/Lock/VideoScreensaver.qml +++ b/quickshell/Modules/Lock/VideoScreensaver.qml @@ -15,7 +15,7 @@ Item { property bool inputEnabled: false property point lastMousePos: Qt.point(-1, -1) property bool mouseInitialized: false - property var videoPlayer: null + readonly property var videoPlayer: playerLoader.item signal dismissed @@ -27,6 +27,24 @@ Item { anchors.fill: parent color: "black" visible: root.active + + Loader { + id: playerLoader + anchors.fill: parent + active: false + source: "VideoScreensaverPlayer.qml" + onLoaded: { + item.errorOccurred.connect((error, errorString) => { + log.warn("playback error:", errorString); + ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString); + root.dismiss(); + }); + if (root.videoSource) { + item.source = root.videoSource; + item.play(); + } + } + } } Timer { @@ -82,43 +100,6 @@ Item { } } - function createVideoPlayer() { - if (videoPlayer) - return true; - - try { - videoPlayer = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - Video { - anchors.fill: parent - fillMode: VideoOutput.PreserveAspectCrop - loops: MediaPlayer.Infinite - volume: 0 - } - `, background, "VideoScreensaver.VideoPlayer"); - - videoPlayer.errorOccurred.connect((error, errorString) => { - log.warn("playback error:", errorString); - ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString); - root.dismiss(); - }); - - return true; - } catch (e) { - log.warn("Failed to create video player:", e); - return false; - } - } - - function destroyVideoPlayer() { - if (videoPlayer) { - videoPlayer.stop(); - videoPlayer.destroy(); - videoPlayer = null; - } - } - function start() { if (!SettingsData.lockScreenVideoEnabled || !SettingsData.lockScreenVideoPath) return; @@ -128,8 +109,12 @@ Item { return; } - if (!createVideoPlayer()) + playerLoader.active = true; + if (playerLoader.status === Loader.Error) { + log.warn("Failed to load video player"); + playerLoader.active = false; return; + } videoPicker.result = ""; videoPicker.folder = ""; @@ -144,7 +129,9 @@ Item { function dismiss() { if (!active) return; - destroyVideoPlayer(); + if (videoPlayer) + videoPlayer.stop(); + playerLoader.active = false; inputEnabled = false; active = false; videoSource = ""; diff --git a/quickshell/Modules/Lock/VideoScreensaverPlayer.qml b/quickshell/Modules/Lock/VideoScreensaverPlayer.qml new file mode 100644 index 00000000..e24e75e6 --- /dev/null +++ b/quickshell/Modules/Lock/VideoScreensaverPlayer.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtMultimedia + +Video { + fillMode: VideoOutput.PreserveAspectCrop + loops: MediaPlayer.Infinite + volume: 0 +} diff --git a/quickshell/Modules/Notepad/NotepadTextEditor.qml b/quickshell/Modules/Notepad/NotepadTextEditor.qml index 3d92d696..3ae86797 100644 --- a/quickshell/Modules/Notepad/NotepadTextEditor.qml +++ b/quickshell/Modules/Notepad/NotepadTextEditor.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell.Io import qs.Common import qs.Services import qs.Widgets @@ -230,44 +231,41 @@ Column { if (!inlinePreviewVisible || !textArea.text) return; const content = textArea.text; - if (content.length > 0) { - const proc = Qt.createQmlObject(` - import QtQuick - import Quickshell.Io - Process { - property string content: "" - command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"] - environment: { "CONTENT": content } - running: false - }`, root, "copyProc"); - proc.content = content; - proc.running = true; - proc.exited.connect(() => { - ToastService.showInfo(I18n.tr("Copied to clipboard")); - proc.destroy(); - }); - } + if (content.length === 0) + return; + const proc = clipboardCopyProcComp.createObject(root, { + content: content, + running: true + }); + proc.exited.connect(() => { + ToastService.showInfo(I18n.tr("Copied to clipboard")); + proc.destroy(); + }); } function copyHtmlToClipboard() { if (!inlinePreviewVisible || !pluginHighlightedHtml) return; - if (pluginHighlightedHtml.length > 0) { - const proc = Qt.createQmlObject(` - import QtQuick - import Quickshell.Io - Process { - property string content: "" - command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"] - environment: { "CONTENT": content } - running: false - }`, root, "copyProcHtml"); - proc.content = pluginHighlightedHtml; - proc.running = true; - proc.exited.connect(() => { - ToastService.showInfo(I18n.tr("HTML copied to clipboard")); - proc.destroy(); - }); + if (pluginHighlightedHtml.length === 0) + return; + const proc = clipboardCopyProcComp.createObject(root, { + content: pluginHighlightedHtml, + running: true + }); + proc.exited.connect(() => { + ToastService.showInfo(I18n.tr("HTML copied to clipboard")); + proc.destroy(); + }); + } + + Component { + id: clipboardCopyProcComp + Process { + property string content: "" + command: ["sh", "-c", "printf '%s' \"$CONTENT\" | dms clipboard copy"] + environment: ({ + "CONTENT": content + }) } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index a29ecfa6..f1866b5a 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -586,10 +586,11 @@ PanelWindow { width: alignedWidth height: alignedHeight visible: !win._finalized && !chromeOnlyExit - scale: (!win.inlineHeightAnimating && cardHoverHandler.hovered) ? 1.01 : 1.0 transformOrigin: Item.Center - Behavior on scale { + property real chromeScale: (!win.inlineHeightAnimating && cardHoverHandler.hovered) ? 1.01 : 1.0 + + Behavior on chromeScale { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing @@ -650,6 +651,8 @@ PanelWindow { id: bgShadowLayer anchors.fill: parent anchors.margins: -content.shadowRenderPadding + scale: content.chromeScale + transformOrigin: Item.Center level: content.elevLevel fallbackOffset: 6 shadowBlurPx: content.shadowBlurPx @@ -684,6 +687,8 @@ PanelWindow { visible: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical opacity: 1 clip: true + scale: content.chromeScale + transformOrigin: Item.Center gradient: Gradient { orientation: Gradient.Horizontal @@ -713,6 +718,8 @@ PanelWindow { border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth z: 100 + scale: content.chromeScale + transformOrigin: Item.Center } Item { diff --git a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml index 4c235964..619c3037 100644 --- a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml +++ b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml @@ -15,6 +15,7 @@ FloatingWindow { property int selectedIndex: -1 property bool keyboardNavigationActive: false property var parentModal: null + parentWindow: parentModal signal widgetAdded(string widgetType) @@ -233,7 +234,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize circular: false iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 diff --git a/quickshell/Modules/Settings/PluginBrowser.qml b/quickshell/Modules/Settings/PluginBrowser.qml index 4bc539c8..f5837e2f 100644 --- a/quickshell/Modules/Settings/PluginBrowser.qml +++ b/quickshell/Modules/Settings/PluginBrowser.qml @@ -17,6 +17,7 @@ FloatingWindow { property bool keyboardNavigationActive: false property bool isLoading: false property var parentModal: null + parentWindow: parentModal property bool pendingInstallHandled: false property string typeFilter: "" @@ -295,7 +296,7 @@ FloatingWindow { } DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 2 iconColor: Theme.outline @@ -723,6 +724,7 @@ FloatingWindow { id: thirdPartyConfirmModal property bool disablePopupTransparency: true + parentWindow: root function show() { visible = true; diff --git a/quickshell/Modules/Settings/PowerSleepTab.qml b/quickshell/Modules/Settings/PowerSleepTab.qml index d9814947..b56569f1 100644 --- a/quickshell/Modules/Settings/PowerSleepTab.qml +++ b/quickshell/Modules/Settings/PowerSleepTab.qml @@ -370,14 +370,6 @@ Item { } } } - - StyledText { - text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version") - font.pixelSize: Theme.fontSizeSmall - color: Theme.error - anchors.horizontalCenter: parent.horizontalCenter - visible: !IdleService.idleMonitorAvailable - } } SettingsCard { diff --git a/quickshell/Modules/Settings/ThemeBrowser.qml b/quickshell/Modules/Settings/ThemeBrowser.qml index f7659fa2..60933beb 100644 --- a/quickshell/Modules/Settings/ThemeBrowser.qml +++ b/quickshell/Modules/Settings/ThemeBrowser.qml @@ -17,6 +17,7 @@ FloatingWindow { property bool keyboardNavigationActive: false property bool isLoading: false property var parentModal: null + parentWindow: parentModal property bool pendingInstallHandled: false property string pendingApplyThemeId: "" @@ -264,7 +265,7 @@ FloatingWindow { } DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 2 iconColor: Theme.outline diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 6888a332..a418f3e2 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -3064,6 +3064,7 @@ Item { ThemeBrowser { id: themeBrowserItem + parentModal: themeColorsTab.parentModal } } diff --git a/quickshell/Modules/Settings/WidgetSelectionPopup.qml b/quickshell/Modules/Settings/WidgetSelectionPopup.qml index c441426a..446f3376 100644 --- a/quickshell/Modules/Settings/WidgetSelectionPopup.qml +++ b/quickshell/Modules/Settings/WidgetSelectionPopup.qml @@ -14,6 +14,7 @@ FloatingWindow { property int selectedIndex: -1 property bool keyboardNavigationActive: false property var parentModal: null + parentWindow: parentModal readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.36 : 0.78) : 1.0 readonly property real fieldAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.18 : 0.62) : 1.0 @@ -238,7 +239,7 @@ FloatingWindow { spacing: Theme.spacingXS DankActionButton { - visible: windowControls.supported + visible: windowControls.canMaximize circular: false iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconSize: Theme.iconSize - 4 diff --git a/quickshell/Modules/WallpaperBackground.qml b/quickshell/Modules/WallpaperBackground.qml index 15763ed4..2a7ec520 100644 --- a/quickshell/Modules/WallpaperBackground.qml +++ b/quickshell/Modules/WallpaperBackground.qml @@ -222,8 +222,7 @@ Variants { } Component.onCompleted: { - if (typeof wallpaperWindow.updatesEnabled !== "undefined") - wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); + wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); if (!source) { root._renderSettling = false; diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index 7c9a47ea..9769925b 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -22,17 +22,30 @@ Singleton { property string currentSoundTheme: "" property var soundFilePaths: ({}) - property var volumeChangeSound: null - property var powerPlugSound: null - property var powerUnplugSound: null - property var normalNotificationSound: null - property var criticalNotificationSound: null - property var loginSound: null + readonly property var volumeChangeSound: soundsLoader.item?.volumeChangeSound ?? null + readonly property var powerPlugSound: soundsLoader.item?.powerPlugSound ?? null + readonly property var powerUnplugSound: soundsLoader.item?.powerUnplugSound ?? null + readonly property var normalNotificationSound: soundsLoader.item?.normalNotificationSound ?? null + readonly property var criticalNotificationSound: soundsLoader.item?.criticalNotificationSound ?? null + readonly property var loginSound: soundsLoader.item?.loginSound ?? null + readonly property var mediaDevices: soundsLoader.item?.mediaDevices ?? null property real notificationsVolume: 1.0 property bool notificationsAudioMuted: false - property var mediaDevices: null - property var mediaDevicesConnections: null + Loader { + id: soundsLoader + active: root.soundsAvailable + source: "AudioSoundPlayers.qml" + onLoaded: { + item.volume = Qt.binding(() => root.notificationsVolume); + item.volumeChangeSource = Qt.binding(() => root.getSoundPath("audio-volume-change")); + item.powerPlugSource = Qt.binding(() => root.getSoundPath("power-plug")); + item.powerUnplugSource = Qt.binding(() => root.getSoundPath("power-unplug")); + item.normalNotificationSource = Qt.binding(() => root.getSoundPath("message")); + item.criticalNotificationSource = Qt.binding(() => root.getSoundPath("message-new-instant")); + item.loginSource = Qt.binding(() => root.getSoundPath("desktop-login")); + } + } property var deviceAliases: ({}) property string wireplumberConfigPath: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf" @@ -452,10 +465,6 @@ EOFCONFIG function discoverSoundFiles(themeName) { if (!themeName) { soundFilePaths = {}; - if (soundsAvailable) { - destroySoundPlayers(); - createSoundPlayers(); - } return; } @@ -514,11 +523,6 @@ EOFCONFIG } } soundFilePaths = paths; - - if (soundsAvailable) { - destroySoundPlayers(); - createSoundPlayers(); - } }, 0); } @@ -559,159 +563,6 @@ EOFCONFIG discoverSoundFiles(currentSoundTheme); } else { soundFilePaths = {}; - if (soundsAvailable) { - destroySoundPlayers(); - createSoundPlayers(); - } - } - } - - function setupMediaDevices() { - if (!soundsAvailable || mediaDevices) { - return; - } - - try { - mediaDevices = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaDevices { - id: devices - Component.onCompleted: { - log.debug("MediaDevices initialized, default output:", defaultAudioOutput?.description) - } - } - `, root, "AudioService.MediaDevices"); - - if (mediaDevices) { - mediaDevicesConnections = Qt.createQmlObject(` - import QtQuick - Connections { - target: root.mediaDevices - function onDefaultAudioOutputChanged() { - log.debug("Default audio output changed, recreating sound players") - root.destroySoundPlayers() - root.createSoundPlayers() - } - } - `, root, "AudioService.MediaDevicesConnections"); - } - } catch (e) { - log.debug("MediaDevices not available, using default audio output"); - mediaDevices = null; - } - } - - function destroySoundPlayers() { - if (volumeChangeSound) { - volumeChangeSound.destroy(); - volumeChangeSound = null; - } - if (powerPlugSound) { - powerPlugSound.destroy(); - powerPlugSound = null; - } - if (powerUnplugSound) { - powerUnplugSound.destroy(); - powerUnplugSound = null; - } - if (normalNotificationSound) { - normalNotificationSound.destroy(); - normalNotificationSound = null; - } - if (criticalNotificationSound) { - criticalNotificationSound.destroy(); - criticalNotificationSound = null; - } - if (loginSound) { - loginSound.destroy(); - loginSound = null; - } - } - - function createSoundPlayers() { - if (!soundsAvailable) { - return; - } - - setupMediaDevices(); - - try { - const deviceProperty = mediaDevices ? `device: root.mediaDevices.defaultAudioOutput\n ` : ""; - - const volumeChangePath = getSoundPath("audio-volume-change"); - volumeChangeSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${volumeChangePath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.VolumeChangeSound"); - - const powerPlugPath = getSoundPath("power-plug"); - powerPlugSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${powerPlugPath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.PowerPlugSound"); - - const powerUnplugPath = getSoundPath("power-unplug"); - powerUnplugSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${powerUnplugPath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.PowerUnplugSound"); - - const messagePath = getSoundPath("message"); - normalNotificationSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${messagePath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.NormalNotificationSound"); - - const messageNewInstantPath = getSoundPath("message-new-instant"); - criticalNotificationSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${messageNewInstantPath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.CriticalNotificationSound"); - - const loginPath = getSoundPath("desktop-login"); - loginSound = Qt.createQmlObject(` - import QtQuick - import QtMultimedia - MediaPlayer { - source: "${loginPath}" - audioOutput: AudioOutput { - ${deviceProperty}volume: notificationsVolume - } - } - `, root, "AudioService.LoginSound"); - } catch (e) { - log.warn("Error creating sound players:", e); } } @@ -955,16 +806,6 @@ EOFCONFIG objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream) } - Connections { - target: Pipewire - function onDefaultAudioSinkChanged() { - if (soundsAvailable) { - Qt.callLater(root.destroySoundPlayers); - Qt.callLater(root.createSoundPlayers); - } - } - } - function setVolume(percentage) { if (!root.sink?.audio) return "No audio sink available"; @@ -1127,10 +968,8 @@ EOFCONFIG Component.onCompleted: { rebuildTypedNodeLists(); - if (soundsAvailable) { + if (soundsAvailable) checkGsettings(); - Qt.callLater(createSoundPlayers); - } loadDeviceAliases(); } diff --git a/quickshell/Services/AudioSoundPlayers.qml b/quickshell/Services/AudioSoundPlayers.qml new file mode 100644 index 00000000..3f8c2818 --- /dev/null +++ b/quickshell/Services/AudioSoundPlayers.qml @@ -0,0 +1,80 @@ +import QtQuick +import QtMultimedia + +Item { + id: root + + property real volume: 1.0 + property url volumeChangeSource + property url powerPlugSource + property url powerUnplugSource + property url normalNotificationSource + property url criticalNotificationSource + property url loginSource + + readonly property alias mediaDevices: devices + readonly property alias volumeChangeSound: volumeChangePlayer + readonly property alias powerPlugSound: powerPlugPlayer + readonly property alias powerUnplugSound: powerUnplugPlayer + readonly property alias normalNotificationSound: normalNotificationPlayer + readonly property alias criticalNotificationSound: criticalNotificationPlayer + readonly property alias loginSound: loginPlayer + + MediaDevices { + id: devices + } + + MediaPlayer { + id: volumeChangePlayer + source: root.volumeChangeSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } + + MediaPlayer { + id: powerPlugPlayer + source: root.powerPlugSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } + + MediaPlayer { + id: powerUnplugPlayer + source: root.powerUnplugSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } + + MediaPlayer { + id: normalNotificationPlayer + source: root.normalNotificationSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } + + MediaPlayer { + id: criticalNotificationPlayer + source: root.criticalNotificationSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } + + MediaPlayer { + id: loginPlayer + source: root.loginSource + audioOutput: AudioOutput { + device: devices.defaultAudioOutput + volume: root.volume + } + } +} diff --git a/quickshell/Services/BlurService.qml b/quickshell/Services/BlurService.qml index 1b511bb6..cb7f3bb9 100644 --- a/quickshell/Services/BlurService.qml +++ b/quickshell/Services/BlurService.qml @@ -4,7 +4,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Wayland // ! Import is needed despite what qmlls says import qs.Common import qs.Services @@ -12,9 +11,8 @@ Singleton { id: root readonly property var log: Log.scoped("BlurService") - property bool quickshellSupported: false property bool compositorSupported: false - property bool available: quickshellSupported && compositorSupported + readonly property bool available: compositorSupported readonly property bool enabled: available && (SettingsData.blurEnabled ?? false) readonly property color borderColor: { @@ -42,41 +40,6 @@ Singleton { return Theme.withAlpha(baseColor, hoverAlpha ?? 0.15); } - function createBlurRegion(targetWindow) { - if (!available) - return null; - - try { - const region = Qt.createQmlObject(` - import Quickshell - Region {} - `, targetWindow, "BlurRegion"); - targetWindow.BackgroundEffect.blurRegion = region; - return region; - } catch (e) { - log.warn("Failed to create blur region:", e); - return null; - } - } - - function reapplyBlurRegion(targetWindow, region) { - if (!region || !available) - return; - try { - targetWindow.BackgroundEffect.blurRegion = region; - region.changed(); - } catch (e) {} - } - - function destroyBlurRegion(targetWindow, region) { - if (!region) - return; - try { - targetWindow.BackgroundEffect.blurRegion = null; - } catch (e) {} - region.destroy(); - } - Process { id: blurProbe running: false @@ -98,18 +61,5 @@ Singleton { } } - Component.onCompleted: { - try { - const test = Qt.createQmlObject(` - import Quickshell - Region { radius: 0 } - `, root, "BlurAvailabilityTest"); - test.destroy(); - quickshellSupported = true; - log.info("Quickshell blur support available"); - blurProbe.running = true; - } catch (e) { - log.info("BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell."); - } - } + Component.onCompleted: blurProbe.running = true } diff --git a/quickshell/Services/IdleService.qml b/quickshell/Services/IdleService.qml index 00283942..abadc151 100644 --- a/quickshell/Services/IdleService.qml +++ b/quickshell/Services/IdleService.qml @@ -11,25 +11,8 @@ Singleton { id: root readonly property var log: Log.scoped("IdleService") - readonly property bool idleMonitorAvailable: { - try { - return typeof IdleMonitor !== "undefined"; - } catch (e) { - return false; - } - } - - readonly property bool idleInhibitorAvailable: { - try { - return typeof IdleInhibitor !== "undefined"; - } catch (e) { - return false; - } - } - property bool enabled: true property bool respectInhibitors: true - property bool _enableGate: true readonly property bool externalInhibitActive: DMSService.screensaverInhibited @@ -43,17 +26,28 @@ Singleton { readonly property bool mediaPlaying: MprisController.activePlayer !== null && MprisController.activePlayer.isPlaying + onEnabledChanged: _applyMonitorEnableds() + onPostLockMonitorActiveChanged: _applyMonitorEnableds() onMonitorTimeoutChanged: _rearmIdleMonitors() onLockTimeoutChanged: _rearmIdleMonitors() onSuspendTimeoutChanged: _rearmIdleMonitors() onPostLockMonitorTimeoutChanged: _rearmIdleMonitors() onIsShellLockedChanged: _rearmIdleMonitors() + function _applyMonitorEnableds() { + const base = enabled; + monitorOffMonitor.enabled = base && monitorTimeout > 0 && !postLockMonitorActive; + postLockMonitorOffMonitor.enabled = base && postLockMonitorActive; + lockMonitor.enabled = base && lockTimeout > 0; + suspendMonitor.enabled = base && suspendTimeout > 0; + } + function _rearmIdleMonitors() { - _enableGate = false; - Qt.callLater(() => { - _enableGate = true; - }); + monitorOffMonitor.enabled = false; + postLockMonitorOffMonitor.enabled = false; + lockMonitor.enabled = false; + suspendMonitor.enabled = false; + Qt.callLater(_applyMonitorEnableds); } signal lockRequested @@ -65,10 +59,6 @@ Singleton { signal requestMonitorOn signal requestSuspend - property var monitorOffMonitor: null - property var postLockMonitorOffMonitor: null - property var lockMonitor: null - property var suspendMonitor: null property var lockComponent: null property bool monitorsOff: false property bool isShellLocked: false @@ -82,84 +72,69 @@ Singleton { CompositorService.powerOffMonitors(); } - function createIdleMonitors() { - if (!idleMonitorAvailable) { - log.info("IdleMonitor not available, skipping creation"); - return; - } - - try { - const qmlString = ` - import QtQuick - import Quickshell.Wayland - - IdleMonitor { - enabled: false - respectInhibitors: true - timeout: 0 - } - `; - - monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor"); - monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout > 0 ? root.monitorTimeout : 86400); - monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors); - monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0 && !root.postLockMonitorActive); - monitorOffMonitor.isIdleChanged.connect(function () { - if (monitorOffMonitor.isIdle) { - if (SettingsData.fadeToDpmsEnabled) { - root.fadeToDpmsRequested(); - } else { - root.requestMonitorOff(); - } + IdleMonitor { + id: monitorOffMonitor + timeout: root.monitorTimeout > 0 ? root.monitorTimeout : 86400 + respectInhibitors: root.respectInhibitors + enabled: false + onIsIdleChanged: { + if (isIdle) { + if (SettingsData.fadeToDpmsEnabled) { + root.fadeToDpmsRequested(); } else { - if (SettingsData.fadeToDpmsEnabled) { - root.cancelFadeToDpms(); - } - root.requestMonitorOn(); - } - }); - - postLockMonitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.PostLockMonitorOffMonitor"); - postLockMonitorOffMonitor.timeout = Qt.binding(() => root.postLockMonitorTimeout > 0 ? root.postLockMonitorTimeout : 86400); - postLockMonitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors); - postLockMonitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.postLockMonitorActive); - postLockMonitorOffMonitor.isIdleChanged.connect(function () { - if (postLockMonitorOffMonitor.isIdle) { root.requestMonitorOff(); - } else { - root.requestMonitorOn(); } - }); + } else { + if (SettingsData.fadeToDpmsEnabled) { + root.cancelFadeToDpms(); + } + root.requestMonitorOn(); + } + } + } - lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor"); - lockMonitor.timeout = Qt.binding(() => root.lockTimeout > 0 ? root.lockTimeout : 86400); - lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors); - lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0); - lockMonitor.isIdleChanged.connect(function () { - if (lockMonitor.isIdle) { - if (SettingsData.fadeToLockEnabled) { - root.fadeToLockRequested(); - } else { - root.lockRequested(); - } - } else { - if (SettingsData.fadeToLockEnabled) { - root.cancelFadeToLock(); - } - } - }); + IdleMonitor { + id: postLockMonitorOffMonitor + timeout: root.postLockMonitorTimeout > 0 ? root.postLockMonitorTimeout : 86400 + respectInhibitors: root.respectInhibitors + enabled: false + onIsIdleChanged: { + if (isIdle) { + root.requestMonitorOff(); + } else { + root.requestMonitorOn(); + } + } + } - suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor"); - suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout > 0 ? root.suspendTimeout : 86400); - suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors); - suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0); - suspendMonitor.isIdleChanged.connect(function () { - if (suspendMonitor.isIdle) { - root.requestSuspend(); + IdleMonitor { + id: lockMonitor + timeout: root.lockTimeout > 0 ? root.lockTimeout : 86400 + respectInhibitors: root.respectInhibitors + enabled: false + onIsIdleChanged: { + if (isIdle) { + if (SettingsData.fadeToLockEnabled) { + root.fadeToLockRequested(); + } else { + root.lockRequested(); } - }); - } catch (e) { - log.warn("Error creating IdleMonitors:", e); + } else { + if (SettingsData.fadeToLockEnabled) { + root.cancelFadeToLock(); + } + } + } + } + + IdleMonitor { + id: suspendMonitor + timeout: root.suspendTimeout > 0 ? root.suspendTimeout : 86400 + respectInhibitors: root.respectInhibitors + enabled: false + onIsIdleChanged: { + if (isIdle) + root.requestSuspend(); } } @@ -194,13 +169,7 @@ Singleton { } Component.onCompleted: { - if (!idleMonitorAvailable) { - log.warn("IdleMonitor not available - power management disabled. This requires a newer version of Quickshell."); - } else { - log.info("Initialized with idle monitoring support"); - createIdleMonitors(); - } - + _applyMonitorEnableds(); if (externalInhibitActive) { const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", "); SessionService.idleInhibited = true; diff --git a/quickshell/Services/KeybindsService.qml b/quickshell/Services/KeybindsService.qml index 6d58893e..25376bae 100644 --- a/quickshell/Services/KeybindsService.qml +++ b/quickshell/Services/KeybindsService.qml @@ -5,8 +5,6 @@ import QtCore import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Wayland -// ! Even though qmlls says this is unused, it is wrong import qs.Common import qs.Services import "../Common/KeybindActions.js" as Actions @@ -15,21 +13,7 @@ Singleton { id: root readonly property var log: Log.scoped("KeybindsService") - Component.onCompleted: { - if (!shortcutInhibitorAvailable) { - log.warn("ShortcutInhibitor is not available in this environment, keybinds editor disabled."); - } - } - - readonly property bool shortcutInhibitorAvailable: { - try { - return typeof ShortcutInhibitor !== "undefined"; - } catch (e) { - return false; - } - } - - property bool available: (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) && shortcutInhibitorAvailable + property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl property string currentProvider: { if (CompositorService.isNiri) return "niri"; diff --git a/quickshell/Services/LegacyNetworkService.qml b/quickshell/Services/LegacyNetworkService.qml index f4cca39f..decc82f7 100644 --- a/quickshell/Services/LegacyNetworkService.qml +++ b/quickshell/Services/LegacyNetworkService.qml @@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Networking import qs.Common import qs.Services @@ -12,30 +13,92 @@ Singleton { readonly property var log: Log.scoped("LegacyNetworkService") property bool isActive: false - property string networkStatus: "disconnected" - property string primaryConnection: "" + readonly property string backend: Networking.backend === NetworkBackendType.NetworkManager ? "networkmanager" : "" + readonly property string primaryConnection: "" + + readonly property var allDevices: Networking.devices?.values ?? [] + + readonly property var wifiDevices: allDevices.filter(d => d.type === DeviceType.Wifi).map(d => ({ + "name": d.name + })) + readonly property var ethernetDevices: allDevices.filter(d => d.type === DeviceType.Wired).map(d => ({ + "name": d.name + })) + + property string wifiDeviceOverride: SessionData.wifiDeviceOverride || "" + + readonly property var wifiDevice: { + const list = allDevices.filter(d => d.type === DeviceType.Wifi); + if (wifiDeviceOverride) { + const match = list.find(d => d.name === wifiDeviceOverride); + if (match) { + return match; + } + } + return list[0] ?? null; + } + + readonly property var wiredDevice: allDevices.find(d => d.type === DeviceType.Wired) ?? null + + readonly property bool ethernetConnected: wiredDevice?.connected ?? false + readonly property string ethernetInterface: wiredDevice?.name ?? "" property string ethernetIP: "" - property string ethernetInterface: "" - property bool ethernetConnected: false - property string ethernetConnectionUuid: "" - - property var wiredConnections: [] + readonly property string ethernetConnectionUuid: { + const net = wiredDevice?.network; + if (!net) { + return ""; + } + const settings = net.nmSettings; + return settings.length > 0 ? settings[0].uuid : ""; + } + readonly property var wiredConnections: [] + readonly property bool wifiAvailable: wifiDevice !== null + readonly property bool wifiEnabled: Networking.wifiEnabled + readonly property string wifiInterface: wifiDevice?.name ?? "" + readonly property bool wifiConnected: wifiDevice?.connected ?? false property string wifiIP: "" - property string wifiInterface: "" - property bool wifiConnected: false - property bool wifiEnabled: true - property string wifiConnectionUuid: "" - property string wifiDevicePath: "" - property string activeAccessPointPath: "" + readonly property string wifiDevicePath: wifiDevice?.name ?? "" + readonly property string activeAccessPointPath: "" + readonly property string connectingDevice: "" - property string currentWifiSSID: "" - property int wifiSignalStrength: 0 - property var wifiNetworks: [] - property var savedConnections: [] - property var ssidToConnectionName: {} - property var wifiSignalIcon: { + readonly property var connectedWifiNetwork: { + const dev = wifiDevice; + if (!dev) { + return null; + } + const list = dev.networks?.values ?? []; + for (const net of list) { + if (net.connected) { + return net; + } + } + return null; + } + + readonly property string currentWifiSSID: connectedWifiNetwork?.name ?? "" + readonly property int wifiSignalStrength: Math.round((connectedWifiNetwork?.signalStrength ?? 0) * 100) + readonly property string wifiConnectionUuid: { + const net = connectedWifiNetwork; + if (!net) { + return ""; + } + const settings = net.nmSettings; + return settings.length > 0 ? settings[0].uuid : ""; + } + + readonly property string networkStatus: { + if (ethernetConnected) { + return "ethernet"; + } + if (wifiConnected) { + return "wifi"; + } + return "disconnected"; + } + + readonly property string wifiSignalIcon: { if (!wifiConnected || networkStatus !== "wifi") { return "wifi_off"; } @@ -48,29 +111,68 @@ Singleton { return "wifi_1_bar"; } - property string userPreference: "auto" // "auto", "wifi", "ethernet" + readonly property var wifiNetworks: { + const dev = wifiDevice; + if (!dev) { + return []; + } + const list = dev.networks?.values ?? []; + const result = []; + const seen = new Set(); + for (const net of list) { + if (!net?.name || seen.has(net.name)) { + continue; + } + seen.add(net.name); + result.push({ + "ssid": net.name, + "signal": Math.round(net.signalStrength * 100), + "secured": net.security !== WifiSecurityType.Open, + "bssid": "", + "connected": net.connected, + "saved": net.known + }); + } + result.sort((a, b) => b.signal - a.signal); + return result; + } + + readonly property var savedConnections: wifiNetworks.filter(n => n.saved).map(n => ({ + "ssid": n.ssid, + "saved": true + })) + readonly property var savedWifiNetworks: savedConnections + readonly property var ssidToConnectionName: { + const map = {}; + for (const n of wifiNetworks) { + if (n.saved) { + map[n.ssid] = n.ssid; + } + } + return map; + } + + property string userPreference: "auto" property bool isConnecting: false property string connectingSSID: "" property string connectionError: "" + property string lastConnectionError: "" + property string connectionStatus: "" + property bool passwordDialogShouldReopen: false - property bool isScanning: false - property bool wifiAvailable: true + readonly property bool isScanning: wifiDevice?.scannerEnabled ?? false + property bool autoScan: false + property bool autoRefreshEnabled: false property bool wifiToggling: false property bool changingPreference: false property string targetPreference: "" - property var savedWifiNetworks: [] - property string connectionStatus: "" - property string lastConnectionError: "" - property bool passwordDialogShouldReopen: false property string wifiPassword: "" property string forgetSSID: "" - - readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"] + property int refCount: 0 property string networkInfoSSID: "" property string networkInfoDetails: "" property bool networkInfoLoading: false - property string networkWiredInfoUUID: "" property string networkWiredInfoDetails: "" property bool networkWiredInfoLoading: false @@ -78,900 +180,376 @@ Singleton { signal networksUpdated signal connectionChanged - function splitNmcliFields(line) { - const parts = []; - let cur = ""; - let escape = false; - for (var i = 0; i < line.length; i++) { - const ch = line[i]; - if (escape) { - cur += ch; - escape = false; - } else if (ch === '\\') { - escape = true; - } else if (ch === ':') { - parts.push(cur); - cur = ""; - } else { - cur += ch; - } + readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"] + + property var _pendingNetwork: null + property string _pendingSSID: "" + property bool _pendingWithPsk: false + + onWifiNetworksChanged: networksUpdated() + onNetworkStatusChanged: { + connectionChanged(); + refreshIPs(); + } + onCurrentWifiSSIDChanged: { + connectionChanged(); + refreshIPs(); + } + onEthernetInterfaceChanged: refreshIPs() + onWifiInterfaceChanged: refreshIPs() + onWifiEnabledChanged: { + if (wifiEnabled && autoScan && wifiDevice) { + wifiDevice.scannerEnabled = true; + } + if (wifiToggling) { + wifiToggling = false; + ToastService.showInfo(wifiEnabled ? I18n.tr("WiFi enabled") : I18n.tr("WiFi disabled")); } - parts.push(cur); - return parts; } Component.onCompleted: { - root.userPreference = SettingsData.networkPreference; + userPreference = SettingsData.networkPreference; + } + + Connections { + target: root._pendingNetwork + enabled: root._pendingNetwork !== null + + function onConnectionFailed(reason) { + root._handleConnectionFailed(reason); + } + + function onConnectedChanged() { + if (root._pendingNetwork?.connected) { + root._handleConnectionSuccess(); + } + } } function activate() { - if (!isActive) { - isActive = true; - log.info("Activating..."); - doRefreshNetworkState(); + if (isActive) { + return; + } + isActive = true; + log.info("Activating..."); + refreshIPs(); + if (wifiDevice && wifiEnabled) { + wifiDevice.scannerEnabled = true; } } - function doRefreshNetworkState() { - updatePrimaryConnection(); - updateDeviceStates(); - updateActiveConnections(); - updateWifiState(); - } - - function updatePrimaryConnection() { - primaryConnectionQuery.running = true; - } - - Process { - id: primaryConnectionQuery - command: lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager", "PrimaryConnection"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/objectpath '([^']+)'/); - if (match && match[1] !== '/') { - root.primaryConnection = match[1]; - getPrimaryConnectionType.running = true; - } else { - root.primaryConnection = ""; - root.networkStatus = "disconnected"; - } - } + function addRef() { + refCount++; + if (refCount === 1) { + startAutoScan(); } } - Process { - id: getPrimaryConnectionType - command: root.primaryConnection ? lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", root.primaryConnection, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Connection.Active", "Type"]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.includes("802-3-ethernet")) { - root.networkStatus = "ethernet"; - } else if (text.includes("802-11-wireless")) { - root.networkStatus = "wifi"; - } - root.connectionChanged(); - } + function removeRef() { + refCount = Math.max(0, refCount - 1); + if (refCount === 0) { + stopAutoScan(); } } - function updateDeviceStates() { - getEthernetDevice.running = true; - getWifiDevice.running = true; - } - - Process { - id: getEthernetDevice - command: lowPriorityCmd.concat(["nmcli", "-t", "-f", "DEVICE,TYPE", "device"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - const lines = text.trim().split('\n'); - let ethernetInterface = ""; - - for (const line of lines) { - const splitParts = line.split(':'); - const device = splitParts[0]; - const type = splitParts.length > 1 ? splitParts[1] : ""; - if (type === "ethernet") { - ethernetInterface = device; - break; - } - } - - if (ethernetInterface) { - root.ethernetInterface = ethernetInterface; - getEthernetDevicePath.command = lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", ethernetInterface]); - getEthernetDevicePath.running = true; - } else { - root.ethernetInterface = ""; - root.ethernetConnected = false; - } - } + function startAutoScan() { + autoScan = true; + autoRefreshEnabled = true; + if (wifiDevice && wifiEnabled) { + wifiDevice.scannerEnabled = true; } } - Process { - id: getEthernetDevicePath - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/objectpath '([^']+)'/); - if (match && match[1] !== '/') { - checkEthernetState.command = lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", match[1], "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Device", "State"]); - checkEthernetState.running = true; - } else { - root.ethernetInterface = ""; - root.ethernetConnected = false; - } - } - } - - onExited: exitCode => { - if (exitCode !== 0) { - root.ethernetInterface = ""; - root.ethernetConnected = false; - } - } - } - - Process { - id: checkEthernetState - running: false - - stdout: StdioCollector { - onStreamFinished: { - const isConnected = text.includes("uint32 100"); - root.ethernetConnected = isConnected; - if (isConnected) { - getEthernetIP.running = true; - } else { - root.ethernetIP = ""; - } - } - } - } - - Process { - id: getEthernetIP - command: root.ethernetInterface ? lowPriorityCmd.concat(["ip", "-4", "addr", "show", root.ethernetInterface]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/); - if (match) { - root.ethernetIP = match[1]; - } - } - } - } - - Process { - id: getWifiDevice - command: lowPriorityCmd.concat(["nmcli", "-t", "-f", "DEVICE,TYPE", "device"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - const lines = text.trim().split('\n'); - let wifiInterface = ""; - - for (const line of lines) { - const splitParts = line.split(':'); - const device = splitParts[0]; - const type = splitParts.length > 1 ? splitParts[1] : ""; - if (type === "wifi") { - wifiInterface = device; - break; - } - } - - if (wifiInterface) { - root.wifiInterface = wifiInterface; - getWifiDevicePath.command = lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", wifiInterface]); - getWifiDevicePath.running = true; - } else { - root.wifiInterface = ""; - root.wifiConnected = false; - } - } - } - } - - Process { - id: getWifiDevicePath - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/objectpath '([^']+)'/); - if (match && match[1] !== '/') { - root.wifiDevicePath = match[1]; - checkWifiState.command = lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", match[1], "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Device", "State"]); - checkWifiState.running = true; - } else { - root.wifiInterface = ""; - root.wifiConnected = false; - root.wifiDevicePath = ""; - root.activeAccessPointPath = ""; - } - } - } - - onExited: exitCode => { - if (exitCode !== 0) { - root.wifiInterface = ""; - root.wifiConnected = false; - } - } - } - - Process { - id: checkWifiState - running: false - - stdout: StdioCollector { - onStreamFinished: { - root.wifiConnected = text.includes("uint32 100"); - if (root.wifiConnected) { - getWifiIP.running = true; - getCurrentWifiInfo.running = true; - getActiveAccessPoint.running = true; - if (root.currentWifiSSID === "") { - if (root.wifiConnectionUuid) { - resolveWifiSSID.running = true; - } - if (root.wifiInterface) { - resolveWifiSSIDFromDevice.running = true; - } - } - } else { - root.wifiIP = ""; - root.currentWifiSSID = ""; - root.wifiSignalStrength = 0; - root.activeAccessPointPath = ""; - } - } - } - } - - Process { - id: getWifiIP - command: root.wifiInterface ? lowPriorityCmd.concat(["ip", "-4", "addr", "show", root.wifiInterface]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/); - if (match) { - root.wifiIP = match[1]; - } - } - } - } - - Process { - id: getActiveAccessPoint - command: root.wifiDevicePath ? lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", root.wifiDevicePath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint"]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const match = text.match(/objectpath '([^']+)'/); - if (match && match[1] !== '/') { - root.activeAccessPointPath = match[1]; - } else { - root.activeAccessPointPath = ""; - } - } - } - } - - Process { - id: getCurrentWifiInfo - command: root.wifiInterface ? lowPriorityCmd.concat(["nmcli", "-t", "-f", "ACTIVE,SIGNAL,SSID", "device", "wifi", "list", "ifname", root.wifiInterface, "--rescan", "no"]) : [] - running: false - - stdout: SplitParser { - splitMarker: "\n" - onRead: line => { - if (line.startsWith("yes:")) { - const rest = line.substring(4); - const parts = root.splitNmcliFields(rest); - if (parts.length >= 2) { - const signal = parseInt(parts[0]); - log.debug("Current WiFi signal strength:", signal); - root.wifiSignalStrength = isNaN(signal) ? 0 : signal; - root.currentWifiSSID = parts[1]; - log.debug("Current WiFi SSID:", root.currentWifiSSID); - } - return; - } - } - } - } - - function updateActiveConnections() { - getActiveConnections.running = true; - } - - Process { - id: getActiveConnections - command: lowPriorityCmd.concat(["nmcli", "-t", "-f", "UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - const lines = text.trim().split('\n'); - for (const line of lines) { - const parts = line.split(':'); - if (parts.length >= 4) { - const uuid = parts[0]; - const type = parts[1]; - const device = parts[2]; - const state = parts[3]; - if (type === "802-3-ethernet" && state === "activated") { - root.ethernetConnectionUuid = uuid; - } else if (type === "802-11-wireless" && state === "activated") { - root.wifiConnectionUuid = uuid; - } - } - } - } - } - } - - // Resolve SSID from active WiFi connection UUID when scans don't mark any row as ACTIVE. - Process { - id: resolveWifiSSID - command: root.wifiConnectionUuid ? lowPriorityCmd.concat(["nmcli", "-g", "802-11-wireless.ssid", "connection", "show", "uuid", root.wifiConnectionUuid]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const ssid = text.trim(); - if (ssid) { - root.currentWifiSSID = ssid; - } - } - } - } - - // Fallback 2: Resolve SSID from device info (GENERAL.CONNECTION usually matches SSID for WiFi) - Process { - id: resolveWifiSSIDFromDevice - command: root.wifiInterface ? lowPriorityCmd.concat(["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", root.wifiInterface]) : [] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (!root.currentWifiSSID) { - const name = text.trim(); - if (name) { - root.currentWifiSSID = name; - } - } - } - } - } - - function updateWifiState() { - checkWifiEnabled.running = true; - } - - Process { - id: checkWifiEnabled - command: lowPriorityCmd.concat(["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager", "WirelessEnabled"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - root.wifiEnabled = text.includes("true"); - root.wifiAvailable = true; // Always available if we can check it - } + function stopAutoScan() { + autoScan = false; + autoRefreshEnabled = false; + if (wifiDevice) { + wifiDevice.scannerEnabled = false; } } function scanWifi() { - if (root.isScanning || !root.wifiEnabled) { + if (!wifiDevice || !wifiEnabled) { return; } - - root.isScanning = true; - requestWifiScan.running = true; - } - - Process { - id: requestWifiScan - command: root.wifiInterface ? lowPriorityCmd.concat(["nmcli", "dev", "wifi", "rescan", "ifname", root.wifiInterface]) : [] - running: false - - onExited: exitCode => { - if (exitCode === 0) { - scanWifiNetworks(); - } else { - log.warn("WiFi scan request failed"); - root.isScanning = false; - } - } + wifiDevice.scannerEnabled = true; } function scanWifiNetworks() { - if (!root.wifiInterface) { - root.isScanning = false; - return; - } - - getWifiNetworks.running = true; - getSavedConnections.running = true; + scanWifi(); } - Process { - id: getWifiNetworks - command: lowPriorityCmd.concat(["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,BSSID", "dev", "wifi", "list", "ifname", root.wifiInterface]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - const networks = []; - const lines = text.trim().split('\n'); - const seen = new Set(); - - for (const line of lines) { - const parts = root.splitNmcliFields(line); - if (parts.length >= 4 && parts[0]) { - const ssid = parts[0]; - if (!seen.has(ssid)) { - seen.add(ssid); - const signal = parseInt(parts[1]) || 0; - - networks.push({ - "ssid": ssid, - "signal": signal, - "secured": parts[2] !== "", - "bssid": parts[3], - "connected": ssid === root.currentWifiSSID, - "saved": false - }); - } - } - } - - networks.sort((a, b) => b.signal - a.signal); - root.wifiNetworks = networks; - root.isScanning = false; - root.networksUpdated(); - } + function _findNetworkBySSID(ssid) { + const dev = wifiDevice; + if (!dev) { + return null; } + return dev.networks?.values?.find(n => n.name === ssid) ?? null; } - Process { - id: getSavedConnections - command: lowPriorityCmd.concat(["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1 | while read name; do ssid=$(nmcli -g 802-11-wireless.ssid connection show \"$name\"); echo \"$ssid:$name\"; done"]) - running: false + function _handleConnectionFailed(reason) { + const ssid = _pendingSSID; + let invalidPsk = false; - stdout: StdioCollector { - onStreamFinished: { - const saved = []; - const mapping = {}; - const lines = text.trim().split('\n'); - - for (const line of lines) { - const parts = line.trim().split(':'); - if (parts.length >= 2) { - const ssid = parts[0]; - const connectionName = parts[1]; - if (ssid && ssid.length > 0 && connectionName && connectionName.length > 0) { - saved.push({ - "ssid": ssid, - "saved": true - }); - mapping[ssid] = connectionName; - } - } - } - - root.savedConnections = saved; - root.savedWifiNetworks = saved; - root.ssidToConnectionName = mapping; - - const updated = [...root.wifiNetworks]; - for (const network of updated) { - network.saved = saved.some(s => s.ssid === network.ssid); - } - root.wifiNetworks = updated; - } - } - } - - function connectToWifi(ssid, password = "", username = "") { - if (root.isConnecting) { - return; + switch (reason) { + case ConnectionFailReason.NoSecrets: + invalidPsk = _pendingWithPsk; + break; + case ConnectionFailReason.WifiAuthTimeout: + invalidPsk = true; + break; } - root.isConnecting = true; - root.connectingSSID = ssid; - root.connectionError = ""; - root.connectionStatus = "connecting"; + connectionStatus = invalidPsk ? "invalid_password" : "failed"; + connectionError = ConnectionFailReason.toString(reason); + lastConnectionError = connectionError; + passwordDialogShouldReopen = invalidPsk; + isConnecting = false; + connectingSSID = ""; - if (!password && root.ssidToConnectionName[ssid]) { - const connectionName = root.ssidToConnectionName[ssid]; - wifiConnector.command = lowPriorityCmd.concat(["nmcli", "connection", "up", connectionName]); - } else if (password) { - wifiConnector.command = lowPriorityCmd.concat(["nmcli", "dev", "wifi", "connect", ssid, "password", password]); + if (invalidPsk) { + ToastService.showError(I18n.tr("Invalid password for %1").arg(ssid)); } else { - wifiConnector.command = lowPriorityCmd.concat(["nmcli", "dev", "wifi", "connect", ssid]); + ToastService.showError(I18n.tr("Failed to connect to %1").arg(ssid)); } - wifiConnector.running = true; + + _pendingNetwork = null; + _pendingSSID = ""; + _pendingWithPsk = false; } - Process { - id: wifiConnector - running: false + function _handleConnectionSuccess() { + const ssid = _pendingSSID; + connectionStatus = "connected"; + connectionError = ""; + isConnecting = false; + connectingSSID = ""; + passwordDialogShouldReopen = false; - property bool connectionSucceeded: false + ToastService.showInfo(I18n.tr("Connected to %1").arg(ssid)); - stdout: StdioCollector { - onStreamFinished: { - if (text.includes("successfully")) { - wifiConnector.connectionSucceeded = true; - ToastService.showInfo(`Connected to ${root.connectingSSID}`); - root.connectionError = ""; - root.connectionStatus = "connected"; + if (userPreference === "wifi" || userPreference === "auto") { + setConnectionPriority("wifi"); + } - if (root.userPreference === "wifi" || root.userPreference === "auto") { - setConnectionPriority("wifi"); - } - } + _pendingNetwork = null; + _pendingSSID = ""; + _pendingWithPsk = false; + } + + function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") { + if (isConnecting) { + return; + } + + const network = _findNetworkBySSID(ssid); + if (!network) { + log.warn("SSID not found in scan results:", ssid); + ToastService.showError(I18n.tr("Network not found"), ssid); + return; + } + + isConnecting = true; + connectingSSID = ssid; + connectionError = ""; + connectionStatus = "connecting"; + passwordDialogShouldReopen = false; + + _pendingNetwork = network; + _pendingSSID = ssid; + + if (password) { + const sec = network.security; + switch (sec) { + case WifiSecurityType.WpaPsk: + case WifiSecurityType.Wpa2Psk: + case WifiSecurityType.Sae: + _pendingWithPsk = true; + network.connectWithPsk(password); + return; + default: + log.warn("Security type not supported with PSK, falling back to connect():", WifiSecurityType.toString(sec)); } } - stderr: StdioCollector { - onStreamFinished: { - root.connectionError = text; - root.lastConnectionError = text; - if (!wifiConnector.connectionSucceeded && text.trim() !== "") { - if (text.includes("password") || text.includes("authentication")) { - root.connectionStatus = "invalid_password"; - root.passwordDialogShouldReopen = true; - } else { - root.connectionStatus = "failed"; - } - } - } - } - - onExited: exitCode => { - if (exitCode === 0 || wifiConnector.connectionSucceeded) { - if (!wifiConnector.connectionSucceeded) { - ToastService.showInfo(`Connected to ${root.connectingSSID}`); - root.connectionStatus = "connected"; - } - } else { - if (root.connectionStatus === "") { - root.connectionStatus = "failed"; - } - if (root.connectionStatus === "invalid_password") { - ToastService.showError(`Invalid password for ${root.connectingSSID}`); - } else { - ToastService.showError(`Failed to connect to ${root.connectingSSID}`); - } - } - - wifiConnector.connectionSucceeded = false; - root.isConnecting = false; - root.connectingSSID = ""; - doRefreshNetworkState(); - } + _pendingWithPsk = false; + network.connect(); } function disconnectWifi() { - if (!root.wifiInterface) { + if (!wifiDevice) { return; } - - wifiDisconnector.command = lowPriorityCmd.concat(["nmcli", "dev", "disconnect", root.wifiInterface]); - wifiDisconnector.running = true; - } - - Process { - id: wifiDisconnector - running: false - - onExited: exitCode => { - if (exitCode === 0) { - ToastService.showInfo("Disconnected from WiFi"); - root.currentWifiSSID = ""; - root.connectionStatus = ""; - } - doRefreshNetworkState(); - } + wifiDevice.disconnect(); + connectionStatus = ""; + ToastService.showInfo(I18n.tr("Disconnected from WiFi")); } function forgetWifiNetwork(ssid) { - root.forgetSSID = ssid; - const connectionName = root.ssidToConnectionName[ssid] || ssid; - networkForgetter.command = lowPriorityCmd.concat(["nmcli", "connection", "delete", connectionName]); - networkForgetter.running = true; - } - - Process { - id: networkForgetter - running: false - - onExited: exitCode => { - if (exitCode === 0) { - ToastService.showInfo(`Forgot network ${root.forgetSSID}`); - - root.savedConnections = root.savedConnections.filter(s => s.ssid !== root.forgetSSID); - root.savedWifiNetworks = root.savedWifiNetworks.filter(s => s.ssid !== root.forgetSSID); - - const updated = [...root.wifiNetworks]; - for (const network of updated) { - if (network.ssid === root.forgetSSID) { - network.saved = false; - if (network.connected) { - network.connected = false; - root.currentWifiSSID = ""; - } - } - } - root.wifiNetworks = updated; - root.networksUpdated(); - doRefreshNetworkState(); - } - root.forgetSSID = ""; + forgetSSID = ssid; + const network = _findNetworkBySSID(ssid); + if (network) { + network.forget(); + ToastService.showInfo(I18n.tr("Forgot network %1").arg(ssid)); + } else { + log.warn("Cannot forget, SSID not found:", ssid); } + forgetSSID = ""; } function toggleWifiRadio() { - if (root.wifiToggling) { + if (wifiToggling) { return; } - - root.wifiToggling = true; - const targetState = root.wifiEnabled ? "off" : "on"; - wifiRadioToggler.targetState = targetState; - wifiRadioToggler.command = lowPriorityCmd.concat(["nmcli", "radio", "wifi", targetState]); - wifiRadioToggler.running = true; + wifiToggling = true; + Networking.wifiEnabled = !Networking.wifiEnabled; } - Process { - id: wifiRadioToggler - running: false - - property string targetState: "" - - onExited: exitCode => { - root.wifiToggling = false; - if (exitCode === 0) { - ToastService.showInfo(targetState === "on" ? "WiFi enabled" : "WiFi disabled"); - } - doRefreshNetworkState(); + function enableWifiDevice() { + if (!Networking.wifiEnabled) { + Networking.wifiEnabled = true; + ToastService.showInfo(I18n.tr("WiFi enabled")); } } function setNetworkPreference(preference) { - root.userPreference = preference; - root.changingPreference = true; - root.targetPreference = preference; + userPreference = preference; + targetPreference = preference; + changingPreference = true; SettingsData.set("networkPreference", preference); - - if (preference === "wifi") { - setConnectionPriority("wifi"); - } else if (preference === "ethernet") { - setConnectionPriority("ethernet"); - } + setConnectionPriority(preference); + changingPreference = false; + targetPreference = ""; } function setConnectionPriority(type) { - if (type === "wifi") { - setRouteMetrics.command = lowPriorityCmd.concat(["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 50 ipv6.route-metric 50'; " + "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 100 ipv6.route-metric 100'"]); - } else if (type === "ethernet") { - setRouteMetrics.command = lowPriorityCmd.concat(["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 50 ipv6.route-metric 50'; " + "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 100 ipv6.route-metric 100'"]); - } - setRouteMetrics.running = true; - } + const wifiMetric = type === "wifi" ? 50 : 100; + const wiredMetric = type === "ethernet" ? 50 : 100; - Process { - id: setRouteMetrics - running: false - - onExited: exitCode => { - log.debug("Set route metrics process exited with code:", exitCode); - if (exitCode === 0) { - restartConnections.running = true; + for (const device of allDevices) { + let metric = -1; + switch (device.type) { + case DeviceType.Wifi: + metric = wifiMetric; + break; + case DeviceType.Wired: + metric = wiredMetric; + break; + default: + continue; } - } - } - Process { - id: restartConnections - command: lowPriorityCmd.concat(["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | " + "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | " + "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"]) - running: false - - onExited: { - root.changingPreference = false; - root.targetPreference = ""; - doRefreshNetworkState(); - } - } - - function fetchNetworkInfo(ssid) { - root.networkInfoSSID = ssid; - root.networkInfoLoading = true; - root.networkInfoDetails = "Loading network information..."; - wifiInfoFetcher.running = true; - } - - Process { - id: wifiInfoFetcher - command: lowPriorityCmd.concat(["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,FREQ,RATE,MODE,CHAN,WPA-FLAGS,RSN-FLAGS,ACTIVE,BSSID", "dev", "wifi", "list"]) - running: false - - stdout: StdioCollector { - onStreamFinished: { - let details = ""; - if (text.trim()) { - const lines = text.trim().split('\n'); - const bands = []; - - for (const line of lines) { - const parts = line.split(':'); - if (parts.length >= 11 && parts[0] === root.networkInfoSSID) { - const signal = parts[1] || "0"; - const security = parts[2] || "Open"; - const freq = parts[3] || "Unknown"; - const rate = parts[4] || "Unknown"; - const channel = parts[6] || "Unknown"; - const isActive = parts[9] === "yes"; - let colonCount = 0; - let bssidStart = -1; - for (var i = 0; i < line.length; i++) { - if (line[i] === ':') { - colonCount++; - if (colonCount === 10) { - bssidStart = i + 1; - break; - } - } - } - const bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : ""; - - let band = "Unknown"; - const freqNum = parseInt(freq); - if (freqNum >= 2400 && freqNum <= 2500) { - band = "2.4 GHz"; - } else if (freqNum >= 5000 && freqNum <= 6000) { - band = "5 GHz"; - } else if (freqNum >= 6000) { - band = "6 GHz"; - } - - bands.push({ - "band": band, - "freq": freq, - "channel": channel, - "signal": signal, - "rate": rate, - "security": security, - "isActive": isActive, - "bssid": bssid - }); + for (const net of device.networks?.values ?? []) { + for (const settings of net.nmSettings) { + settings.write({ + "ipv4": { + "route-metric": metric + }, + "ipv6": { + "route-metric": metric } - } - - if (bands.length > 0) { - bands.sort((a, b) => { - if (a.isActive && !b.isActive) { - return -1; - } - if (!a.isActive && b.isActive) { - return 1; - } - return parseInt(b.signal) - parseInt(a.signal); - }); - - for (var i = 0; i < bands.length; i++) { - const b = bands[i]; - if (b.isActive) { - details += "● " + b.band + " (Connected) - " + b.signal + "%\\n"; - } else { - details += " " + b.band + " - " + b.signal + "%\\n"; - } - details += " Channel " + b.channel + " (" + b.freq + " MHz) • " + b.rate + " Mbit/s\\n"; - details += " " + b.bssid; - if (i < bands.length - 1) { - details += "\\n\\n"; - } - } - } + }); } - - if (details === "") { - details = "Network information not found or network not available."; - } - - root.networkInfoDetails = details; - root.networkInfoLoading = false; - } - } - - onExited: exitCode => { - root.networkInfoLoading = false; - if (exitCode !== 0) { - root.networkInfoDetails = "Failed to fetch network information"; } } } - function enableWifiDevice() { - wifiDeviceEnabler.running = true; - } - - Process { - id: wifiDeviceEnabler - command: lowPriorityCmd.concat(["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); if [ -n \"$WIFI_DEV\" ]; then nmcli device connect \"$WIFI_DEV\"; else echo \"No WiFi device found\"; exit 1; fi"]) - running: false - - onExited: exitCode => { - if (exitCode === 0) { - ToastService.showInfo("WiFi enabled"); - } else { - ToastService.showError("Failed to enable WiFi"); - } - doRefreshNetworkState(); - } - } - - function connectToWifiAndSetPreference(ssid, password) { - connectToWifi(ssid, password); + function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") { + connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch); setNetworkPreference("wifi"); } function toggleNetworkConnection(type) { - if (type === "ethernet") { - if (root.networkStatus === "ethernet") { - ethernetDisconnector.running = true; - } else { - ethernetConnector.running = true; + if (type !== "ethernet" || !wiredDevice) { + return; + } + if (ethernetConnected) { + wiredDevice.disconnect(); + } else if (wiredDevice.network) { + wiredDevice.network.connect(); + } + } + + function disconnectEthernetDevice(deviceName) { + for (const dev of allDevices) { + if (dev.type === DeviceType.Wired && dev.name === deviceName) { + dev.disconnect(); + return; } } } - Process { - id: ethernetDisconnector - command: lowPriorityCmd.concat(["sh", "-c", "nmcli device disconnect $(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1)"]) - running: false - - onExited: function (exitCode) { - doRefreshNetworkState(); + function setWifiDeviceOverride(deviceName) { + SessionData.setWifiDeviceOverride(deviceName || ""); + if (wifiEnabled) { + scanWifi(); } } - Process { - id: ethernetConnector - command: lowPriorityCmd.concat(["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); if [ -n \"$ETH_DEV\" ]; then nmcli device connect \"$ETH_DEV\"; else echo \"No ethernet device found\"; exit 1; fi"]) - running: false - - onExited: function (exitCode) { - doRefreshNetworkState(); + function setWifiAutoconnect(ssid, autoconnect) { + const network = _findNetworkBySSID(ssid); + if (!network) { + return; } + for (const settings of network.nmSettings) { + settings.write({ + "connection": { + "autoconnect": autoconnect + } + }); + } + ToastService.showInfo(autoconnect ? I18n.tr("Autoconnect enabled") : I18n.tr("Autoconnect disabled")); + } + + function fetchNetworkInfo(ssid) { + networkInfoSSID = ssid; + networkInfoLoading = false; + + const network = _findNetworkBySSID(ssid); + if (!network) { + networkInfoDetails = "Network information not found or network not available."; + return; + } + + const signalPct = Math.round(network.signalStrength * 100); + const secLabel = WifiSecurityType.toString(network.security); + const statusPrefix = network.connected ? "● " : " "; + const statusSuffix = network.connected ? " (Connected)" : ""; + + let details = statusPrefix + signalPct + "%" + statusSuffix + "\\n"; + details += " Security: " + secLabel + "\\n"; + if (network.known) { + details += " Status: Saved network\\n"; + } + networkInfoDetails = details; + } + + function fetchWiredNetworkInfo(uuid) { + networkWiredInfoUUID = uuid; + networkWiredInfoLoading = false; + + const dev = wiredDevice; + if (!dev) { + networkWiredInfoDetails = "Network information not found or network not available."; + return; + } + + let details = ""; + details += "Interface: " + (dev.name || "-") + "\\n"; + details += "MAC Addr: " + (dev.address || "-") + "\\n"; + details += "Speed: " + (dev.linkSpeed || 0) + " Mb/s\\n\\n"; + + details += "IPv4 information:\\n"; + details += " IPv4 address: " + (ethernetIP || "-") + "\\n"; + + networkWiredInfoDetails = details; } function getNetworkInfo(ssid) { - const network = root.wifiNetworks.find(n => n.ssid === ssid); + const network = wifiNetworks.find(n => n.ssid === ssid); if (!network) { return null; } - return { "ssid": network.ssid, "signal": network.signal, @@ -981,4 +559,64 @@ Singleton { "bssid": network.bssid }; } + + function getWiredNetworkInfo(uuid) { + return { + "uuid": uuid + }; + } + + function refreshNetworkState() { + refreshIPs(); + } + + function connectToSpecificWiredConfig(uuid) { + } + + function submitCredentials(token, secrets, save) { + } + + function cancelCredentials(token) { + } + + function refreshIPs() { + getEthernetIP.running = false; + getWifiIP.running = false; + + if (ethernetInterface && ethernetConnected) { + getEthernetIP.command = lowPriorityCmd.concat(["ip", "-4", "addr", "show", ethernetInterface]); + getEthernetIP.running = true; + } else { + ethernetIP = ""; + } + + if (wifiInterface && wifiConnected) { + getWifiIP.command = lowPriorityCmd.concat(["ip", "-4", "addr", "show", wifiInterface]); + getWifiIP.running = true; + } else { + wifiIP = ""; + } + } + + Process { + id: getEthernetIP + running: false + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/); + root.ethernetIP = match ? match[1] : ""; + } + } + } + + Process { + id: getWifiIP + running: false + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/); + root.wifiIP = match ? match[1] : ""; + } + } + } } diff --git a/quickshell/Services/MultimediaProbe.qml b/quickshell/Services/MultimediaProbe.qml new file mode 100644 index 00000000..c23a1bec --- /dev/null +++ b/quickshell/Services/MultimediaProbe.qml @@ -0,0 +1,7 @@ +import QtQuick +// qmllint disable unused-imports +import QtMultimedia + +// qmllint enable unused-imports + +Item {} diff --git a/quickshell/Services/MultimediaService.qml b/quickshell/Services/MultimediaService.qml index 1f4f0372..b2df832e 100644 --- a/quickshell/Services/MultimediaService.qml +++ b/quickshell/Services/MultimediaService.qml @@ -8,30 +8,15 @@ Singleton { id: root readonly property var log: Log.scoped("MultimediaService") - property bool available: false + readonly property bool available: probeLoader.status === Loader.Ready - function detectAvailability() { - try { - const testObj = Qt.createQmlObject(` - import QtQuick - import QtMultimedia -import qs.Services - Item {} - `, root, "MultimediaService.TestComponent"); - if (testObj) { - testObj.destroy(); - } - available = true; - return true; - } catch (e) { - available = false; - return false; - } - } - - Component.onCompleted: { - if (!detectAvailability()) { - log.warn("QtMultimedia not available"); + Loader { + id: probeLoader + source: "MultimediaProbe.qml" + active: true + onStatusChanged: { + if (status === Loader.Error) + log.warn("QtMultimedia not available"); } } } diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 1f9efa0e..0e1c556b 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -5,6 +5,7 @@ import QtCore import QtQuick import Qt.labs.folderlistmodel import Quickshell +import Quickshell.Io import qs.Common import qs.Services @@ -154,35 +155,43 @@ Singleton { } function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) { - const manifestId = "m_" + Math.random().toString(36).slice(2); - const qml = ` - import QtQuick - import Quickshell.Io - FileView { - id: fv - property string absPath: "" - onLoaded: { - try { - let raw = text() - if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1) - const manifest = JSON.parse(raw) - root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs}) - } catch (e) { - log.error("bad manifest", absPath, e.message) - knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true } - } - fv.destroy() - } - onLoadFailed: (err) => { - log.warn("manifest load failed", absPath, err) - fv.destroy() - } - } - `; + const loader = manifestFvComp.createObject(root, { + absPath: manifestPathNoScheme, + path: manifestPathNoScheme, + sourceTag: sourceTag, + mtimeEpochMs: mtimeEpochMs + }); + } - const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId); - loader.absPath = manifestPathNoScheme; - loader.path = manifestPathNoScheme; + Component { + id: manifestFvComp + FileView { + id: fv + property string absPath: "" + property string sourceTag: "" + property double mtimeEpochMs: 0 + onLoaded: { + try { + let raw = text(); + if (raw.charCodeAt(0) === 0xFEFF) + raw = raw.slice(1); + const manifest = JSON.parse(raw); + root._onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs); + } catch (e) { + root.log.error("bad manifest", absPath, e.message); + root.knownManifests[absPath] = { + mtime: mtimeEpochMs, + source: sourceTag, + bad: true + }; + } + fv.destroy(); + } + onLoadFailed: err => { + root.log.warn("manifest load failed", absPath, err); + fv.destroy(); + } + } } function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) { @@ -670,10 +679,10 @@ Singleton { _stateLoaded[pluginId] = true; _ensureStateDir(); const path = getPluginStatePath(pluginId); - const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); try { - const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockLoading: true; blockWrites: true; atomicWrites: true }'; - const fv = Qt.createQmlObject(qml, root, "sf_" + pluginId); + const fv = stateLoadFvComp.createObject(root, { + path: path + }); const raw = fv.text(); if (raw && raw.trim()) { _stateCache[pluginId] = JSON.parse(raw); @@ -694,10 +703,10 @@ Singleton { return; } const path = getPluginStatePath(pluginId); - const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); try { - const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockWrites: true; atomicWrites: true }'; - const fv = Qt.createQmlObject(qml, root, "sw_" + pluginId); + const fv = stateSaveFvComp.createObject(root, { + path: path + }); _stateWriters[pluginId] = fv; fv.loaded.connect(function () { fv.setText(content); @@ -710,6 +719,23 @@ Singleton { } } + Component { + id: stateLoadFvComp + FileView { + blockLoading: true + blockWrites: true + atomicWrites: true + } + } + + Component { + id: stateSaveFvComp + FileView { + blockWrites: true + atomicWrites: true + } + } + function _flushDirtyStates() { const dirty = _stateDirtyPlugins; _stateDirtyPlugins = {}; @@ -748,22 +774,8 @@ Singleton { } function createPluginDirectory() { - const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }"); - if (mkdirProcess.status === Component.Ready) { - const process = mkdirProcess.createObject(root); - process.command = ["mkdir", "-p", pluginDirectory]; - process.exited.connect(function (exitCode) { - if (exitCode !== 0) { - log.error("Failed to create plugin directory, exit code:", exitCode); - } - process.destroy(); - }); - process.running = true; - return true; - } else { - log.error("Failed to create mkdir process"); - return false; - } + Quickshell.execDetached(["mkdir", "-p", pluginDirectory]); + return true; } // Launcher plugin helper functions diff --git a/quickshell/Services/PolkitService.qml b/quickshell/Services/PolkitService.qml index 0762596a..5adc4af4 100644 --- a/quickshell/Services/PolkitService.qml +++ b/quickshell/Services/PolkitService.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.Services.Polkit Singleton { id: root @@ -10,33 +11,15 @@ Singleton { readonly property bool disablePolkitIntegration: Quickshell.env("DMS_DISABLE_POLKIT") === "1" - property bool polkitAvailable: false - property var agent: null + readonly property bool polkitAvailable: !disablePolkitIntegration + readonly property alias agent: polkitAgentInstance - function createPolkitAgent() { - try { - const qmlString = ` - import QtQuick - import Quickshell.Services.Polkit -import qs.Services - - PolkitAgent { - } - ` - - agent = Qt.createQmlObject(qmlString, root, "PolkitService.Agent") - polkitAvailable = true - log.info("Initialized successfully") - } catch (e) { - polkitAvailable = false - log.warn("Polkit not available - authentication prompts disabled. This requires a newer version of Quickshell.") - } + PolkitAgent { + id: polkitAgentInstance } Component.onCompleted: { - if (disablePolkitIntegration) { - return - } - createPolkitAgent() + if (!disablePolkitIntegration) + log.info("Initialized successfully"); } } diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index 160a4f11..88dbe627 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -6,7 +6,6 @@ import Quickshell import Quickshell.Io import Quickshell.Hyprland import Quickshell.I3 -import Quickshell.Wayland import qs.Common import qs.Services @@ -22,14 +21,6 @@ Singleton { property string inhibitReason: "Keep system awake" property string nvidiaCommand: "" - readonly property bool nativeInhibitorAvailable: { - try { - return typeof IdleInhibitor !== "undefined"; - } catch (e) { - return false; - } - } - property bool loginctlAvailable: false property bool wtypeAvailable: false property string sessionId: "" @@ -66,8 +57,6 @@ Singleton { detectHibernateProcess.running = true; detectPrimeRunProcess.running = true; detectWtypeProcess.running = true; - cleanupOrphanedInhibitors(); - log.info("Native inhibitor available:", nativeInhibitorAvailable); if (!SettingsData.loginctlLockIntegration) { log.debug("loginctl lock integration disabled by user"); return; @@ -396,19 +385,15 @@ Singleton { signal inhibitorChanged function enableIdleInhibit() { - if (idleInhibited) { + if (idleInhibited) return; - } - log.debug("Enabling idle inhibit (native:", nativeInhibitorAvailable, ")"); idleInhibited = true; inhibitorChanged(); } function disableIdleInhibit() { - if (!idleInhibited) { + if (!idleInhibited) return; - } - log.debug("Disabling idle inhibit (native:", nativeInhibitorAvailable, ")"); idleInhibited = false; inhibitorChanged(); } @@ -423,64 +408,6 @@ Singleton { function setInhibitReason(reason) { inhibitReason = reason; - - if (idleInhibited && !nativeInhibitorAvailable) { - const wasActive = idleInhibited; - idleInhibited = false; - - Qt.callLater(() => { - if (wasActive) { - idleInhibited = true; - } - }); - } - } - - Process { - id: idleInhibitProcess - - command: { - if (!idleInhibited || nativeInhibitorAvailable) { - return ["true"]; - } - - log.debug("Starting systemd/elogind inhibit process"); - return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"]; - } - - running: idleInhibited && !nativeInhibitorAvailable - - onRunningChanged: { - log.debug("Inhibit process running:", running, "(native:", nativeInhibitorAvailable, ")"); - } - - onExited: function (exitCode) { - if (idleInhibited && exitCode !== 0 && !nativeInhibitorAvailable) { - log.warn("Inhibitor process crashed with exit code:", exitCode); - idleInhibited = false; - ToastService.showWarning("Idle inhibitor failed"); - } - } - } - - // Kill orphaned idle inhibitor processes left behind by previous quickshell sessions. - // When quickshell crashes or is force-killed, the child systemd-inhibit process gets - // reparented to PID 1 and continues to block idle indefinitely. - function cleanupOrphanedInhibitors() { - if (nativeInhibitorAvailable) return; - orphanCleanupProcess.running = true; - } - - Process { - id: orphanCleanupProcess - running: false - command: ["pkill", "-f", "systemd-inhibit --what=idle --who=quickshell.*sleep infinity"] - - onExited: function (exitCode) { - if (exitCode === 0) { - log.info("Cleaned up orphaned idle inhibitor process(es) from a previous session"); - } - } } Connections { diff --git a/quickshell/Widgets/DankAlbumArt.qml b/quickshell/Widgets/DankAlbumArt.qml index d5f8d109..539d8c19 100644 --- a/quickshell/Widgets/DankAlbumArt.qml +++ b/quickshell/Widgets/DankAlbumArt.qml @@ -71,8 +71,13 @@ Item { PathCubic {} } + Component { + id: pathMoveComp + PathMove {} + } + Component.onCompleted: { - shapePath.pathElements.push(Qt.createQmlObject('import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath)); + shapePath.pathElements.push(pathMoveComp.createObject(shapePath)); for (let i = 0; i < segments; i++) { const seg = cubicSegment.createObject(shapePath); diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 34ac62e2..349581c9 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -182,14 +182,19 @@ Item { setBarContext(pos, bottomGap); } - // Briefly forces backgroundWindow.updatesEnabled true while the surface - // body changes, so the contentHoleRect mask carve-out commits to the - // compositor — otherwise the input region stays stuck at the popup's - // initial size and clicks in any newly-grown area dismiss the popup. - // Cleared by the frameSwapped Connections below as soon as the dirty - // frame ships, so the bg window goes back to skipping buffer updates. + // Holds backgroundWindow.updatesEnabled true while the surface body is + // changing so the contentHoleRect mask carve-out tracks the popup body — + // otherwise clicks in newly-grown areas hit the bg window and dismiss. + // Debounced off ~250ms after the last change so a stable popup doesn't + // keep the bg window in active-update mode. property bool _bgCommitWindow: false + Timer { + id: bgCommitSettleTimer + interval: 250 + onTriggered: root._bgCommitWindow = false + } + function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) { const newX = Theme.snap(bodyX, dpr); const newY = Theme.snap(bodyY, dpr); @@ -206,15 +211,7 @@ Item { _surfaceH = _surfaceBodyH + shadowBuffer * 2; if (changed && backgroundWindow.visible) { _bgCommitWindow = true; - } - } - - Connections { - target: backgroundWindow - ignoreUnknownSignals: true - function onFrameSwapped() { - if (root._bgCommitWindow) - root._bgCommitWindow = false; + bgCommitSettleTimer.restart(); } } diff --git a/quickshell/Widgets/FloatingWindowControls.qml b/quickshell/Widgets/FloatingWindowControls.qml index b56291e6..edea06af 100644 --- a/quickshell/Widgets/FloatingWindowControls.qml +++ b/quickshell/Widgets/FloatingWindowControls.qml @@ -5,31 +5,28 @@ Item { readonly property real edgeSize: 8 required property var targetWindow - property bool supported: typeof targetWindow.startSystemMove === "function" readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height anchors.fill: parent function tryStartMove() { - if (!supported) - return; targetWindow.startSystemMove(); } function tryStartResize(edges) { - if (!supported || !canMaximize) + if (!canMaximize) return; targetWindow.startSystemResize(edges); } function tryToggleMaximize() { - if (!supported || !canMaximize) + if (!canMaximize) return; targetWindow.maximized = !targetWindow.maximized; } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize height: root.edgeSize anchors.left: parent.left anchors.right: parent.right @@ -41,7 +38,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize anchors.left: parent.left anchors.top: parent.top @@ -53,7 +50,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize anchors.right: parent.right anchors.top: parent.top @@ -65,7 +62,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize height: root.edgeSize anchors.left: parent.left @@ -75,7 +72,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize height: root.edgeSize anchors.right: parent.right @@ -85,7 +82,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize height: root.edgeSize anchors.left: parent.left anchors.right: parent.right @@ -97,7 +94,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize height: root.edgeSize anchors.left: parent.left @@ -107,7 +104,7 @@ Item { } MouseArea { - visible: root.supported && root.canMaximize + visible: root.canMaximize width: root.edgeSize height: root.edgeSize anchors.right: parent.right diff --git a/quickshell/Widgets/KeybindItem.qml b/quickshell/Widgets/KeybindItem.qml index 6f33db5e..d768bede 100644 --- a/quickshell/Widgets/KeybindItem.qml +++ b/quickshell/Widgets/KeybindItem.qml @@ -41,17 +41,8 @@ Item { property string _actionType: "" property bool addingNewKey: false property bool useCustomCompositor: false - property var _shortcutInhibitor: null property bool _altShiftGhost: false - readonly property bool _shortcutInhibitorAvailable: { - try { - return typeof ShortcutInhibitor !== "undefined"; - } catch (e) { - return false; - } - } - readonly property var keys: bindData.keys || [] readonly property bool hasOverride: { for (var i = 0; i < keys.length; i++) { @@ -251,41 +242,17 @@ Item { addingNewKey = false; } - function _createShortcutInhibitor() { - if (!_shortcutInhibitorAvailable || _shortcutInhibitor) - return; - const qmlString = ` - import QtQuick - import Quickshell.Wayland - - ShortcutInhibitor { - enabled: false - window: null - } - `; - - _shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor"); - _shortcutInhibitor.enabled = Qt.binding(() => root.recording); - _shortcutInhibitor.window = Qt.binding(() => root.panelWindow); - } - - function _destroyShortcutInhibitor() { - if (_shortcutInhibitor) { - _shortcutInhibitor.enabled = false; - _shortcutInhibitor.destroy(); - _shortcutInhibitor = null; - } + ShortcutInhibitor { + window: root.panelWindow + enabled: root.recording } function startRecording() { - _destroyShortcutInhibitor(); - _createShortcutInhibitor(); recording = true; } function stopRecording() { recording = false; - _destroyShortcutInhibitor(); } Column { diff --git a/quickshell/Widgets/WindowBlur.qml b/quickshell/Widgets/WindowBlur.qml index fdf8cdda..5f1c9227 100644 --- a/quickshell/Widgets/WindowBlur.qml +++ b/quickshell/Widgets/WindowBlur.qml @@ -1,4 +1,6 @@ import QtQuick +import Quickshell +import Quickshell.Wayland import qs.Common import qs.Services @@ -8,7 +10,6 @@ Item { visible: false required property var targetWindow - property var blurItem: null property bool blurEnabled: Theme.connectedSurfaceBlurEnabled property real blurX: 0 property real blurY: 0 @@ -16,54 +17,38 @@ Item { property real blurHeight: 0 property real blurRadius: 0 - property var _region: null + readonly property bool _active: blurEnabled && BlurService.enabled && !!targetWindow + + Region { + id: blurRegion + x: root.blurX + y: root.blurY + width: root.blurWidth + height: root.blurHeight + radius: root.blurRadius + } function _apply() { - if (!blurEnabled || !BlurService.enabled || !targetWindow) { - _cleanup(); + if (!targetWindow) return; - } - - if (!_region) - _region = BlurService.createBlurRegion(targetWindow); - - if (!_region) - return; - - _region.item = Qt.binding(() => root.blurItem); - _region.x = Qt.binding(() => root.blurX); - _region.y = Qt.binding(() => root.blurY); - _region.width = Qt.binding(() => root.blurWidth); - _region.height = Qt.binding(() => root.blurHeight); - _region.radius = Qt.binding(() => root.blurRadius); + targetWindow.BackgroundEffect.blurRegion = _active ? blurRegion : null; } - function _cleanup() { - if (!_region) - return; - BlurService.destroyBlurRegion(targetWindow, _region); - _region = null; - } - - onBlurEnabledChanged: _apply() - - Connections { - target: BlurService - function onEnabledChanged() { - root._apply(); - } - } + on_ActiveChanged: _apply() + onTargetWindowChanged: _apply() Connections { target: root.targetWindow ?? null + ignoreUnknownSignals: true function onVisibleChanged() { - if (root.targetWindow && root.targetWindow.visible) { - root._region = null; + if (root.targetWindow && root.targetWindow.visible) root._apply(); - } } } Component.onCompleted: _apply() - Component.onDestruction: _cleanup() + Component.onDestruction: { + if (targetWindow) + targetWindow.BackgroundEffect.blurRegion = null; + } } diff --git a/quickshell/shell.qml b/quickshell/shell.qml index ae22e866..7c84fc2c 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -5,7 +5,7 @@ //@ pragma Env QT_WAYLAND_DISABLE_WINDOWDECORATION=1 //@ pragma Env QT_QUICK_CONTROLS_STYLE=Material //@ pragma UseQApplication -// ! TODO - replace pragma AppId when next QS releases, remove from GO launch injection. +//@ pragma AppId com.danklinux.dms import QtQuick import Quickshell