From b42fe6643f14a83cb35b024b5813feadaed53dc4 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 7 Oct 2026 11:55:29 -0400 Subject: [PATCH] perf: gate idle work behind feature and capability state --- quickshell/Common/AppUsageHistoryData.qml | 2 +- quickshell/Common/Ref.qml | 7 +- quickshell/Common/SessionData.qml | 10 +- quickshell/Common/SettingsData.qml | 17 +- quickshell/Common/SettingsTabs.qml | 4 + quickshell/Common/Theme.qml | 1 - quickshell/Common/settings/Processes.qml | 183 +++++++++-------- quickshell/DMSShell.qml | 22 +-- quickshell/Modals/DLauncherV2/Controller.qml | 1 + .../BuiltinPlugins/VpnWidget.qml | 4 - .../ControlCenter/Details/BluetoothDetail.qml | 4 + .../ControlCenter/Widgets/BluetoothTile.qml | 4 + quickshell/Modules/DBar/DBarBody.qml | 25 --- .../Modules/DBar/DBarHoverController.qml | 6 + quickshell/Modules/DBar/Popouts/VpnPopout.qml | 4 - .../Modules/DBar/Widgets/PrivacyIndicator.qml | 4 + quickshell/Modules/DBar/Widgets/Vpn.qml | 4 - .../DIsland/Activities/HomeCompact.qml | 4 + .../Modules/DIsland/DIslandHostWindow.qml | 30 +-- .../DIsland/IslandNotificationSource.qml | 1 + .../Modules/DIsland/IslandSystemSource.qml | 8 + quickshell/Modules/DesktopWidgetLayer.qml | 7 +- quickshell/Modules/OSD/MediaPlaybackOSD.qml | 3 +- quickshell/Modules/Settings/AudioTab.qml | 1 + quickshell/Modules/Settings/AutoStartTab.qml | 1 + .../BarWidgetOptions/AppsDockOptions.qml | 2 + .../BarWidgetOptions/SystemMonitorOptions.qml | 2 + .../Modules/Settings/DBarAppearanceTab.qml | 2 + .../SystemMonitorSettings.qml | 2 + quickshell/Modules/Settings/KeybindsTab.qml | 2 + quickshell/Modules/Settings/LauncherTab.qml | 3 + .../Modules/Settings/ThemeSurfacesTab.qml | 5 +- .../Settings/Widgets/TerminalPickerRow.qml | 2 + .../Modules/Settings/WidgetsTabSection.qml | 2 + quickshell/Modules/WallpaperBackground.qml | 13 +- quickshell/Services/AppleMusicArtService.qml | 18 +- quickshell/Services/AudioService.qml | 18 +- quickshell/Services/BatteryService.qml | 4 +- quickshell/Services/BluetoothService.qml | 186 +++++++++++++++--- quickshell/Services/BlurService.qml | 18 +- quickshell/Services/BrightnessService.qml | 25 +-- quickshell/Services/CalendarDankBackend.qml | 23 ++- quickshell/Services/CalendarKhalBackend.qml | 16 +- quickshell/Services/CalendarService.qml | 2 + quickshell/Services/CavaService.qml | 10 +- quickshell/Services/ClipboardService.qml | 27 ++- quickshell/Services/CompositorService.qml | 38 +--- quickshell/Services/CupsService.qml | 2 + quickshell/Services/DMSNetworkService.qml | 76 +++---- quickshell/Services/DMSService.qml | 2 +- quickshell/Services/DSearchService.qml | 12 +- quickshell/Services/DesktopService.qml | 18 +- quickshell/Services/DgopService.qml | 69 ++----- quickshell/Services/FilesService.qml | 26 +++ quickshell/Services/FirstLaunchService.qml | 68 +++---- quickshell/Services/GreeterService.qml | 10 +- quickshell/Services/GreeterUsersService.qml | 6 +- quickshell/Services/HyprlandService.qml | 10 +- quickshell/Services/IconThemeService.qml | 12 +- quickshell/Services/IdleService.qml | 2 - quickshell/Services/KeybindsService.qml | 19 ++ quickshell/Services/Log.qml | 2 +- quickshell/Services/LyricsService.qml | 8 + quickshell/Services/MangoService.qml | 9 +- quickshell/Services/MediaAccentService.qml | 12 +- quickshell/Services/MprisController.qml | 24 ++- quickshell/Services/NightModeService.qml | 9 +- quickshell/Services/NiriOutputCycle.qml | 10 +- quickshell/Services/NiriService.qml | 57 +++--- quickshell/Services/NotepadStorageService.qml | 20 +- quickshell/Services/NotificationService.qml | 19 +- quickshell/Services/PortalService.qml | 5 +- quickshell/Services/PowerProfileWatcher.qml | 23 +-- quickshell/Services/PrivacyService.qml | 133 +++++++------ quickshell/Services/RefreshRateService.qml | 32 +-- quickshell/Services/SessionService.qml | 120 ++++------- quickshell/Services/SessionsService.qml | 60 ++++-- quickshell/Services/SettingsSearchService.qml | 6 +- quickshell/Services/ShellVersionService.qml | 32 +-- quickshell/Services/SystemUpdateService.qml | 22 ++- quickshell/Services/TailscaleService.qml | 2 +- quickshell/Services/ThemeAutoService.qml | 179 ++++++++--------- quickshell/Services/TrackArtService.qml | 35 ++-- quickshell/Services/TrashService.qml | 20 +- .../Services/WallpaperCyclingService.qml | 7 +- quickshell/Services/WeatherService.qml | 1 + quickshell/Services/WellbeingService.qml | 13 +- quickshell/Widgets/DSeekbar.qml | 17 ++ 88 files changed, 1190 insertions(+), 796 deletions(-) diff --git a/quickshell/Common/AppUsageHistoryData.qml b/quickshell/Common/AppUsageHistoryData.qml index 94724a688..cd6dd4a38 100644 --- a/quickshell/Common/AppUsageHistoryData.qml +++ b/quickshell/Common/AppUsageHistoryData.qml @@ -125,7 +125,7 @@ Singleton { path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json" blockLoading: true blockWrites: true - watchChanges: true + watchChanges: SettingsData.launcherHistoryEnabled onLoaded: { if (root._saving) return; diff --git a/quickshell/Common/Ref.qml b/quickshell/Common/Ref.qml index 0bb9cfa03..32423108a 100644 --- a/quickshell/Common/Ref.qml +++ b/quickshell/Common/Ref.qml @@ -12,15 +12,16 @@ QtObject { if (wanted === _held) return; _held = wanted; - if (modules === null) { + if (typeof service.addRef !== "function") { service.refCount += wanted ? 1 : -1; return; } + const held = modules ?? []; if (wanted) { - service.addRef(modules); + service.addRef(held); return; } - service.removeRef(modules); + service.removeRef(held); } onActiveChanged: sync(active) diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 2afa009d5..3b257783b 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -64,9 +64,15 @@ Singleton { return ""; } + function probeTerminals() { + if (terminalProbe.running || installedTerminals.length > 0) + return; + terminalProbe.running = true; + } + Process { id: terminalProbe - running: true + running: false command: ["sh", "-c", "for t in ghostty kitty foot alacritty wezterm konsole gnome-terminal xterm; do command -v \"$t\" >/dev/null 2>&1 && echo \"$t\"; done"] stdout: StdioCollector { onStreamFinished: { @@ -1856,7 +1862,7 @@ Singleton { FileView { id: greeterSessionFile - path: root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : "" + path: isGreeterMode && root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : "" preload: isGreeterMode blockLoading: false blockWrites: true diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 4f429a511..302278a1f 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -785,6 +785,10 @@ Singleton { property bool lockAtStartup: Spec.SPEC.lockAtStartup.def property bool enableFprint: Spec.SPEC.enableFprint.def + onEnableFprintChanged: { + if (enableFprint) + refreshAuthAvailability(); + } property int maxFprintTries: Spec.SPEC.maxFprintTries.def readonly property bool fprintdAvailable: Processes.fprintdAvailable readonly property bool lockFingerprintCanEnable: Processes.lockFingerprintCanEnable @@ -795,6 +799,10 @@ Singleton { readonly property string greeterFingerprintReason: Processes.greeterFingerprintReason readonly property string greeterFingerprintSource: Processes.greeterFingerprintSource property bool enableU2f: Spec.SPEC.enableU2f.def + onEnableU2fChanged: { + if (enableU2f) + refreshAuthAvailability(); + } property string u2fMode: Spec.SPEC.u2fMode.def readonly property bool u2fAvailable: Processes.u2fAvailable readonly property bool lockU2fCanEnable: Processes.lockU2fCanEnable @@ -1404,8 +1412,6 @@ Singleton { Processes.settingsRoot = root; loadSettings(); initializeListModels(); - refreshAuthAvailability(); - Processes.checkPluginSettings(); } function applyStoredTheme() { @@ -3795,7 +3801,7 @@ Singleton { FileView { id: greeterSettingsFile - path: root.greeterSettingsBaseDir ? (root.greeterSettingsBaseDir + "/settings.json") : "" + path: isGreeterMode && root.greeterSettingsBaseDir ? (root.greeterSettingsBaseDir + "/settings.json") : "" preload: isGreeterMode blockLoading: false blockWrites: true @@ -3823,13 +3829,16 @@ Singleton { onLoaded: { if (isGreeterMode) return; + pluginSettingsFileExists = true; parsePluginSettings(pluginSettingsFile.text()); } onLoadFailed: error => { if (isGreeterMode) return; const msg = String(error || ""); - if (!_isMissingPluginSettingsError(error)) + const missing = _isMissingPluginSettingsError(error); + pluginSettingsFileExists = !missing; + if (!missing) log.warn("Failed to load plugin_settings.json. Error:", msg); _resetPluginSettings(); } diff --git a/quickshell/Common/SettingsTabs.qml b/quickshell/Common/SettingsTabs.qml index 93000c98a..563e4faae 100644 --- a/quickshell/Common/SettingsTabs.qml +++ b/quickshell/Common/SettingsTabs.qml @@ -773,8 +773,12 @@ Singleton { return false; if (entry.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23)) return false; + if (entry.greeterOnly) + GreeterService.ensureDetected(); if (entry.greeterOnly && !GreeterService.available) return false; + if (entry.autostartOnly) + DesktopService.ensureProbed(); if (entry.autostartOnly && !DesktopService.autostartAvailable) return false; if (entry.cellularOnly && (NetworkService.cellularDevices?.length ?? 0) === 0) diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 87fe639e0..53cadf3f8 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -120,7 +120,6 @@ Singleton { } Component.onCompleted: { - Quickshell.execDetached(["mkdir", "-p", stateDir]); Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => { matugenAvailable = (code === 0) && !envDisableMatugen; generateSystemThemesFromCurrentTheme(); diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index 3a3c01f6b..3579e6542 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -64,9 +64,10 @@ Singleton { readonly property string homeDir: Quickshell.env("HOME") || "" readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : "" - readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== "" + property bool authProbeRequested: false + readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && (authWatchers.item?.u2fKeys.loaded ?? false) && u2fKeysText.trim() !== "" readonly property bool lockU2fCustomConfigDetected: PamStack.moduleEnabled(dankshellU2fPamText, "pam_u2f") - readonly property bool lockU2fCustomSourceDetected: (settingsRoot?.lockU2fPamPath || "") !== "" && customU2fPamWatcher.loaded + readonly property bool lockU2fCustomSourceDetected: (settingsRoot?.lockU2fPamPath || "") !== "" && (authWatchers.item?.customU2fPam.loaded ?? false) readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") @@ -250,9 +251,10 @@ Singleton { readonly property var _pamProbeCommand: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"] function detectAuthCapabilities() { + authProbeRequested = true; // FileView cannot watch paths that do not exist yet, so reload the U2F PAM - dankshellU2fPamWatcher.reload(); - u2fKeysWatcher.reload(); + authWatchers.item.dankshellU2fPam.reload(); + authWatchers.item.u2fKeys.reload(); if (forcedFprintAvailable === null) { fingerprintProbeFinalized = false; @@ -397,10 +399,6 @@ Singleton { return PamStack.stackHasModule(greetdPamText, includedPamStacks, moduleName); } - function checkPluginSettings() { - pluginSettingsCheckProcess.running = true; - } - Timer { id: authApplyDebounce interval: 300 @@ -541,102 +539,101 @@ Singleton { } } - FileView { - id: greetdPamWatcher - path: "/etc/pam.d/greetd" - printErrors: false - onLoaded: root.greetdPamText = text() - onLoadFailed: root.greetdPamText = "" - } + Loader { + id: authWatchers + active: root.authProbeRequested + sourceComponent: Item { + property alias dankshellU2fPam: dankshellU2fPamWatcher + property alias customU2fPam: customU2fPamWatcher + property alias u2fKeys: u2fKeysWatcher - FileView { - id: systemAuthPamWatcher - path: "/etc/pam.d/system-auth" - printErrors: false - onLoaded: root.systemAuthPamText = text() - onLoadFailed: root.systemAuthPamText = "" - } + FileView { + id: greetdPamWatcher + path: "/etc/pam.d/greetd" + printErrors: false + onLoaded: root.greetdPamText = text() + onLoadFailed: root.greetdPamText = "" + } - FileView { - id: commonAuthPamWatcher - path: "/etc/pam.d/common-auth" - printErrors: false - onLoaded: root.commonAuthPamText = text() - onLoadFailed: root.commonAuthPamText = "" - } + FileView { + id: systemAuthPamWatcher + path: "/etc/pam.d/system-auth" + printErrors: false + onLoaded: root.systemAuthPamText = text() + onLoadFailed: root.systemAuthPamText = "" + } - FileView { - id: passwordAuthPamWatcher - path: "/etc/pam.d/password-auth" - printErrors: false - onLoaded: root.passwordAuthPamText = text() - onLoadFailed: root.passwordAuthPamText = "" - } + FileView { + id: commonAuthPamWatcher + path: "/etc/pam.d/common-auth" + printErrors: false + onLoaded: root.commonAuthPamText = text() + onLoadFailed: root.commonAuthPamText = "" + } - FileView { - id: systemLoginPamWatcher - path: "/etc/pam.d/system-login" - printErrors: false - onLoaded: root.systemLoginPamText = text() - onLoadFailed: root.systemLoginPamText = "" - } + FileView { + id: passwordAuthPamWatcher + path: "/etc/pam.d/password-auth" + printErrors: false + onLoaded: root.passwordAuthPamText = text() + onLoadFailed: root.passwordAuthPamText = "" + } - FileView { - id: systemLocalLoginPamWatcher - path: "/etc/pam.d/system-local-login" - printErrors: false - onLoaded: root.systemLocalLoginPamText = text() - onLoadFailed: root.systemLocalLoginPamText = "" - } + FileView { + id: systemLoginPamWatcher + path: "/etc/pam.d/system-login" + printErrors: false + onLoaded: root.systemLoginPamText = text() + onLoadFailed: root.systemLoginPamText = "" + } - FileView { - id: commonAuthPcPamWatcher - path: "/etc/pam.d/common-auth-pc" - printErrors: false - onLoaded: root.commonAuthPcPamText = text() - onLoadFailed: root.commonAuthPcPamText = "" - } + FileView { + id: systemLocalLoginPamWatcher + path: "/etc/pam.d/system-local-login" + printErrors: false + onLoaded: root.systemLocalLoginPamText = text() + onLoadFailed: root.systemLocalLoginPamText = "" + } - FileView { - id: loginPamWatcher - path: "/etc/pam.d/login" - printErrors: false - onLoaded: root.loginPamText = text() - onLoadFailed: root.loginPamText = "" - } + FileView { + id: commonAuthPcPamWatcher + path: "/etc/pam.d/common-auth-pc" + printErrors: false + onLoaded: root.commonAuthPcPamText = text() + onLoadFailed: root.commonAuthPcPamText = "" + } - FileView { - id: dankshellU2fPamWatcher - path: "/etc/pam.d/dankshell-u2f" - watchChanges: true - printErrors: false - onLoaded: root.dankshellU2fPamText = text() - onLoadFailed: root.dankshellU2fPamText = "" - } + FileView { + id: loginPamWatcher + path: "/etc/pam.d/login" + printErrors: false + onLoaded: root.loginPamText = text() + onLoadFailed: root.loginPamText = "" + } - FileView { - id: customU2fPamWatcher - path: root.settingsRoot?.lockU2fPamPath || "" - printErrors: false - } + FileView { + id: dankshellU2fPamWatcher + path: "/etc/pam.d/dankshell-u2f" + watchChanges: true + printErrors: false + onLoaded: root.dankshellU2fPamText = text() + onLoadFailed: root.dankshellU2fPamText = "" + } - FileView { - id: u2fKeysWatcher - path: root.u2fKeysPath - watchChanges: true - printErrors: false - onLoaded: root.u2fKeysText = text() - onLoadFailed: root.u2fKeysText = "" - } + FileView { + id: customU2fPamWatcher + path: root.settingsRoot?.lockU2fPamPath || "" + printErrors: false + } - property var pluginSettingsCheckProcess: Process { - command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""] - running: false - - onExited: function (exitCode) { - if (!settingsRoot) - return; - settingsRoot.pluginSettingsFileExists = (exitCode === 0); + FileView { + id: u2fKeysWatcher + path: root.u2fKeysPath + watchChanges: true + printErrors: false + onLoaded: root.u2fKeysText = text() + onLoadFailed: root.u2fKeysText = "" + } } } } diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 32800f29e..573787502 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -30,11 +30,9 @@ import qs.Services Item { id: root readonly property var log: Log.scoped("DMSShell") - readonly property var _sessionsServiceRef: SessionsService readonly property var _nightModeServiceRef: NightModeService readonly property var _brightnessServiceRef: BrightnessService readonly property var _refreshRateServiceRef: RefreshRateService - readonly property var _displayServiceRef: DisplayService property var core: null @@ -1251,9 +1249,7 @@ Item { Variants { model: SettingsData.getFilteredScreens("toast") - delegate: Toast { - visible: ToastService.toastVisible - } + delegate: Toast {} } Loader { @@ -1264,37 +1260,37 @@ Item { sourceComponent: Component { Item { Variants { - model: root.legacySystemLevelOsdScreens + model: SettingsData.osdVolumeEnabled ? root.legacySystemLevelOsdScreens : [] delegate: VolumeOSD {} } Variants { - model: SettingsData.getFilteredScreens("osd") + model: SettingsData.osdMediaVolumeEnabled ? SettingsData.getFilteredScreens("osd") : [] delegate: MediaVolumeOSD {} } Variants { - model: SettingsData.getFilteredScreens("osd") + model: SettingsData.osdMediaPlaybackEnabled ? SettingsData.getFilteredScreens("osd") : [] delegate: MediaPlaybackOSD {} } Variants { - model: root.legacySystemLevelOsdScreens + model: (SettingsData.osdMicMuteEnabled || SettingsData.osdMicVolumeEnabled) ? root.legacySystemLevelOsdScreens : [] delegate: MicVolumeOSD {} } Variants { - model: root.legacySystemLevelOsdScreens + model: SettingsData.osdBrightnessEnabled ? root.legacySystemLevelOsdScreens : [] delegate: BrightnessOSD {} } Variants { - model: root.legacySystemLevelOsdScreens + model: SettingsData.osdIdleInhibitorEnabled ? root.legacySystemLevelOsdScreens : [] delegate: IdleInhibitorOSD {} } @@ -1306,13 +1302,13 @@ Item { } Variants { - model: root.legacySystemLevelOsdScreens + model: SettingsData.osdCapsLockEnabled ? root.legacySystemLevelOsdScreens : [] delegate: CapsLockOSD {} } Variants { - model: SettingsData.getFilteredScreens("osd") + model: SettingsData.osdAudioOutputEnabled ? SettingsData.getFilteredScreens("osd") : [] delegate: AudioOutputOSD {} } diff --git a/quickshell/Modals/DLauncherV2/Controller.qml b/quickshell/Modals/DLauncherV2/Controller.qml index d5fd4dde7..704e3823e 100644 --- a/quickshell/Modals/DLauncherV2/Controller.qml +++ b/quickshell/Modals/DLauncherV2/Controller.qml @@ -707,6 +707,7 @@ Item { _startDmenuSession(mode.slice("dmenu:".length)); return; } + DSearchService.ensureChecked(); if (appCategory !== "" || Object.keys(collapsedSections).length > 0) _clearModeCache(); appCategory = ""; diff --git a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml index dffc68f34..f74483e75 100644 --- a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml +++ b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml @@ -8,10 +8,6 @@ import qs.Modules.Plugins PluginComponent { id: root - Ref { - service: DMSNetworkService - } - readonly property bool vpnActivating: DMSNetworkService.vpnIsBusy || DMSNetworkService.activeState === "activating" readonly property bool vpnActivated: DMSNetworkService.connected && DMSNetworkService.activeState === "activated" diff --git a/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml b/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml index fd992e769..5954a0434 100644 --- a/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml @@ -14,6 +14,10 @@ import "../../../Common/QmlUtils.js" as QmlUtils Item { id: root + Ref { + service: BatteryService + } + implicitHeight: column.height LayoutMirroring.enabled: I18n.isRtl diff --git a/quickshell/Modules/ControlCenter/Widgets/BluetoothTile.qml b/quickshell/Modules/ControlCenter/Widgets/BluetoothTile.qml index 86b998803..e3523ef3f 100644 --- a/quickshell/Modules/ControlCenter/Widgets/BluetoothTile.qml +++ b/quickshell/Modules/ControlCenter/Widgets/BluetoothTile.qml @@ -27,6 +27,10 @@ CcTile { iconName: "bluetooth" iconBlinking: BluetoothService.connecting + + Ref { + service: BatteryService + } title: { if (!BluetoothService.available) return I18n.tr("Bluetooth", "bluetooth status"); diff --git a/quickshell/Modules/DBar/DBarBody.qml b/quickshell/Modules/DBar/DBarBody.qml index b38e195cc..057da3809 100644 --- a/quickshell/Modules/DBar/DBarBody.qml +++ b/quickshell/Modules/DBar/DBarBody.qml @@ -677,7 +677,6 @@ Item { readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0 Component.onCompleted: { - updateGpuTempConfig(); _updateHasMaximizedToplevel(); _updateShouldHideForWindows(); fitSettle.schedule(); @@ -695,27 +694,9 @@ Item { } } - function updateGpuTempConfig() { - const leftWidgets = barConfig?.leftWidgets || []; - const centerWidgets = barConfig?.centerWidgets || []; - const rightWidgets = barConfig?.rightWidgets || []; - const allWidgets = [...leftWidgets, ...centerWidgets, ...rightWidgets]; - - const hasGpuTempWidget = allWidgets.some(widget => { - const widgetId = typeof widget === "string" ? widget : widget.id; - const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false); - return widgetId === "gpuTemp" && widgetEnabled; - }); - - DgopService.gpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled || SessionData.nonNvidiaGpuTempEnabled; - DgopService.nvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled; - DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled; - } - readonly property var rootWindowBarConfig: rootWindow.barConfig onRootWindowBarConfigChanged: { - updateGpuTempConfig(); _updateHasMaximizedToplevel(); _updateShouldHideForWindows(); } @@ -732,12 +713,6 @@ Item { } } - readonly property bool sessionNvidiaGpuTempEnabled: SessionData.nvidiaGpuTempEnabled - readonly property bool sessionNonNvidiaGpuTempEnabled: SessionData.nonNvidiaGpuTempEnabled - - onSessionNvidiaGpuTempEnabledChanged: updateGpuTempConfig() - onSessionNonNvidiaGpuTempEnabledChanged: updateGpuTempConfig() - readonly property int barPos: barConfig?.position ?? 0 readonly property bool reserveExclusiveWhenAutoHidden: FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences) diff --git a/quickshell/Modules/DBar/DBarHoverController.qml b/quickshell/Modules/DBar/DBarHoverController.qml index 43ffc6671..9a1d7f0db 100644 --- a/quickshell/Modules/DBar/DBarHoverController.qml +++ b/quickshell/Modules/DBar/DBarHoverController.qml @@ -43,6 +43,7 @@ Item { onSectionsChanged: invalidateCandidateCache() onHoverPopoutsEnabledChanged: { + invalidateCandidateCache(); if (hoverPopoutsEnabled) return; cancelQueuedHitTest(); @@ -58,6 +59,7 @@ Item { Connections { target: root.widgetOwner || root.barContent + enabled: root.hoverPopoutsEnabled function onWidthChanged() { root.invalidateCandidateCache(); @@ -74,6 +76,7 @@ Item { Connections { target: PopoutManager + enabled: root.hoverPopoutsEnabled function onPopoutChanged() { root._reconcileClosedHoverSurface(); @@ -86,6 +89,7 @@ Item { Connections { target: BarWidgetService + enabled: root.hoverPopoutsEnabled function onWidgetRegistered(_widgetId, screenName) { if (screenName === root.barWindow?.screen?.name) @@ -455,6 +459,8 @@ Item { Connections { target: root.barWindow?.hostWindow ?? null + enabled: root.hoverPopoutsEnabled + function onMarginsChanged() { root.invalidateCandidateCache(); } diff --git a/quickshell/Modules/DBar/Popouts/VpnPopout.qml b/quickshell/Modules/DBar/Popouts/VpnPopout.qml index 8701b02f3..bdc6e2714 100644 --- a/quickshell/Modules/DBar/Popouts/VpnPopout.qml +++ b/quickshell/Modules/DBar/Popouts/VpnPopout.qml @@ -10,10 +10,6 @@ DPopout { layerNamespace: "dms:vpn" - Ref { - service: DMSNetworkService - } - property bool wasVisible: false property var triggerScreen: null diff --git a/quickshell/Modules/DBar/Widgets/PrivacyIndicator.qml b/quickshell/Modules/DBar/Widgets/PrivacyIndicator.qml index c85854e09..2fc801e5c 100644 --- a/quickshell/Modules/DBar/Widgets/PrivacyIndicator.qml +++ b/quickshell/Modules/DBar/Widgets/PrivacyIndicator.qml @@ -23,6 +23,10 @@ BasePill { opacity: hasActivePrivacy ? 1 : 0 + Ref { + service: PrivacyService + } + states: [ State { name: "hidden_horizontal" diff --git a/quickshell/Modules/DBar/Widgets/Vpn.qml b/quickshell/Modules/DBar/Widgets/Vpn.qml index 584f774cb..d139af93f 100644 --- a/quickshell/Modules/DBar/Widgets/Vpn.qml +++ b/quickshell/Modules/DBar/Widgets/Vpn.qml @@ -8,10 +8,6 @@ import qs.Widgets BasePill { id: root - Ref { - service: DMSNetworkService - } - property bool isHovered: clickArea.containsMouse signal toggleVpnPopup diff --git a/quickshell/Modules/DIsland/Activities/HomeCompact.qml b/quickshell/Modules/DIsland/Activities/HomeCompact.qml index 6a3901ff5..0bd411f36 100644 --- a/quickshell/Modules/DIsland/Activities/HomeCompact.qml +++ b/quickshell/Modules/DIsland/Activities/HomeCompact.qml @@ -18,6 +18,10 @@ Item { readonly property real textSize: root.tight ? Theme.fontSizeSmall : Theme.fontSizeMedium readonly property real iconSize: root.textSize + Theme.spacingXS readonly property real statusIconSize: root.textSize + Theme.spacingXXS + + Ref { + service: PrivacyService + } readonly property real groupSpacing: root.controller.homeSlotMargin readonly property bool isVertical: root.controller.isVertical readonly property string clockDisplay: root.controller.homeClockDisplay diff --git a/quickshell/Modules/DIsland/DIslandHostWindow.qml b/quickshell/Modules/DIsland/DIslandHostWindow.qml index 2bd464601..415c19737 100644 --- a/quickshell/Modules/DIsland/DIslandHostWindow.qml +++ b/quickshell/Modules/DIsland/DIslandHostWindow.qml @@ -87,9 +87,9 @@ Item { Item { id: islandHole - readonly property var mask: root.body?.inputMaskItem ?? null - x: (mask?.x ?? 0) + root.hostOriginX - y: (mask?.y ?? 0) + root.hostOriginY + readonly property var mask: dismissWindow.visible ? root.body?.inputMaskItem ?? null : null + x: mask ? mask.x + root.hostOriginX : 0 + y: mask ? mask.y + root.hostOriginY : 0 width: mask?.width ?? 0 height: mask?.height ?? 0 } @@ -97,9 +97,9 @@ Item { Item { id: fittsStripHole - readonly property var strip: root.body?.fittsStripItem ?? null - x: (strip?.x ?? 0) + root.hostOriginX - y: (strip?.y ?? 0) + root.hostOriginY + readonly property var strip: dismissWindow.visible ? root.body?.fittsStripItem ?? null : null + x: strip ? strip.x + root.hostOriginX : 0 + y: strip ? strip.y + root.hostOriginY : 0 width: strip?.visible ? strip.width : 0 height: strip?.visible ? strip.height : 0 } @@ -107,9 +107,9 @@ Item { Item { id: leadingSectionHole - readonly property var r: root.body?.leadingSectionRect ?? null - x: (r?.x ?? 0) + (root.body?.sectionOriginX ?? 0) - y: (r?.y ?? 0) + (root.body?.sectionOriginY ?? 0) + readonly property var r: dismissWindow.visible ? root.body?.leadingSectionRect ?? null : null + x: r ? r.x + root.body.sectionOriginX : 0 + y: r ? r.y + root.body.sectionOriginY : 0 width: r?.w ?? 0 height: r?.h ?? 0 } @@ -118,9 +118,9 @@ Item { Item { id: centerSectionHole - readonly property var r: root.body?.centerSectionRect ?? null - x: (r?.x ?? 0) + (root.body?.sectionOriginX ?? 0) - y: (r?.y ?? 0) + (root.body?.sectionOriginY ?? 0) + readonly property var r: dismissWindow.visible ? root.body?.centerSectionRect ?? null : null + x: r ? r.x + root.body.sectionOriginX : 0 + y: r ? r.y + root.body.sectionOriginY : 0 width: r?.w ?? 0 height: r?.h ?? 0 } @@ -128,9 +128,9 @@ Item { Item { id: trailingSectionHole - readonly property var r: root.body?.trailingSectionRect ?? null - x: (r?.x ?? 0) + (root.body?.sectionOriginX ?? 0) - y: (r?.y ?? 0) + (root.body?.sectionOriginY ?? 0) + readonly property var r: dismissWindow.visible ? root.body?.trailingSectionRect ?? null : null + x: r ? r.x + root.body.sectionOriginX : 0 + y: r ? r.y + root.body.sectionOriginY : 0 width: r?.w ?? 0 height: r?.h ?? 0 } diff --git a/quickshell/Modules/DIsland/IslandNotificationSource.qml b/quickshell/Modules/DIsland/IslandNotificationSource.qml index 5f51f6630..6ff990078 100644 --- a/quickshell/Modules/DIsland/IslandNotificationSource.qml +++ b/quickshell/Modules/DIsland/IslandNotificationSource.qml @@ -150,6 +150,7 @@ QtObject { property Connections notificationConnections: Connections { target: NotificationService + enabled: root.enabled function onVisibleNotificationsChanged() { root.syncVisibleNotifications(); diff --git a/quickshell/Modules/DIsland/IslandSystemSource.qml b/quickshell/Modules/DIsland/IslandSystemSource.qml index 38ad0c1ae..f0b67f4a1 100644 --- a/quickshell/Modules/DIsland/IslandSystemSource.qml +++ b/quickshell/Modules/DIsland/IslandSystemSource.qml @@ -159,6 +159,7 @@ QtObject { property Connections audioConnection: Connections { target: AudioService.sink?.audio ?? null + enabled: root.enabled function onVolumeChanged() { if (SettingsData.osdVolumeEnabled) @@ -173,6 +174,7 @@ QtObject { property Connections micConnection: Connections { target: AudioService.source?.audio ?? null + enabled: root.enabled function onMutedChanged() { if (SettingsData.osdMicMuteEnabled) @@ -182,6 +184,7 @@ QtObject { property Connections micServiceConnection: Connections { target: AudioService + enabled: root.enabled function onMicVolumeChanged() { if (SettingsData.osdMicVolumeEnabled) @@ -191,6 +194,7 @@ QtObject { property Connections brightnessConnection: Connections { target: BrightnessService + enabled: root.enabled function onBrightnessChanged(showOsd) { if (showOsd && SettingsData.osdBrightnessEnabled) @@ -200,6 +204,7 @@ QtObject { property Connections capsLockConnection: Connections { target: DMSService + enabled: root.enabled function onCapsLockStateChanged() { if (SettingsData.osdCapsLockEnabled) @@ -209,6 +214,7 @@ QtObject { property Connections powerProfileConnection: Connections { target: PowerProfileWatcher + enabled: root.enabled function onProfileChanged() { if (SettingsData.osdPowerProfileEnabled) @@ -218,6 +224,7 @@ QtObject { property Connections idleInhibitorConnection: Connections { target: SessionService + enabled: root.enabled function onInhibitorChanged() { if (SettingsData.osdIdleInhibitorEnabled) @@ -227,6 +234,7 @@ QtObject { property Connections batteryConnection: Connections { target: BatteryService + enabled: root.enabled function onIsPluggedInChanged() { if (root.chargingPulseEnabled && BatteryService.batteryAvailable) diff --git a/quickshell/Modules/DesktopWidgetLayer.qml b/quickshell/Modules/DesktopWidgetLayer.qml index 7ad3a1ddd..34f547457 100644 --- a/quickshell/Modules/DesktopWidgetLayer.qml +++ b/quickshell/Modules/DesktopWidgetLayer.qml @@ -96,7 +96,12 @@ Variants { model: ScriptModel { objectProp: "id" // Reversed so the top of the list maps last and renders in front. - values: screenDelegate.rebuilding ? [] : [...(SettingsData.desktopWidgetInstances || [])].reverse() + values: { + if (screenDelegate.rebuilding) + return []; + const instances = SettingsData.desktopWidgetInstances || []; + return instances.filter(inst => inst.enabled && DesktopWidgetRegistry.showsOnScreen(inst.config?.displayPreferences ?? ["all"], screenDelegate.screen)).reverse(); + } } DesktopPluginWrapper { diff --git a/quickshell/Modules/OSD/MediaPlaybackOSD.qml b/quickshell/Modules/OSD/MediaPlaybackOSD.qml index d360b5f18..c3b08ead9 100644 --- a/quickshell/Modules/OSD/MediaPlaybackOSD.qml +++ b/quickshell/Modules/OSD/MediaPlaybackOSD.qml @@ -104,7 +104,7 @@ DOSD { Image { id: artPreloader - source: TrackArtService.resolvedArtUrl + source: SettingsData.osdMediaPlaybackEnabled ? TrackArtService.resolvedArtUrl : "" visible: false asynchronous: true cache: true @@ -149,6 +149,7 @@ DOSD { Connections { target: player + enabled: SettingsData.osdMediaPlaybackEnabled function handleUpdate() { if (!root.player?.trackTitle) diff --git a/quickshell/Modules/Settings/AudioTab.qml b/quickshell/Modules/Settings/AudioTab.qml index cf62dc660..31421213a 100644 --- a/quickshell/Modules/Settings/AudioTab.qml +++ b/quickshell/Modules/Settings/AudioTab.qml @@ -86,6 +86,7 @@ Item { Component.onCompleted: { AudioService.addRef(); + AudioService.queryMonoSetting(() => {}); hiddenOutputDeviceNames = SessionData.hiddenOutputDeviceNames ?? []; hiddenInputDeviceNames = SessionData.hiddenInputDeviceNames ?? []; updateDeviceList(); diff --git a/quickshell/Modules/Settings/AutoStartTab.qml b/quickshell/Modules/Settings/AutoStartTab.qml index ea9def828..37b167710 100644 --- a/quickshell/Modules/Settings/AutoStartTab.qml +++ b/quickshell/Modules/Settings/AutoStartTab.qml @@ -343,6 +343,7 @@ Item { } Component.onCompleted: { + DesktopService.ensureProbed(); desktopApps = AppSearchService.getVisibleApplications() || []; autostartInitMkDirComp.createObject(root, { running: true diff --git a/quickshell/Modules/Settings/BarWidgetOptions/AppsDockOptions.qml b/quickshell/Modules/Settings/BarWidgetOptions/AppsDockOptions.qml index b9456177a..55e690ac0 100644 --- a/quickshell/Modules/Settings/BarWidgetOptions/AppsDockOptions.qml +++ b/quickshell/Modules/Settings/BarWidgetOptions/AppsDockOptions.qml @@ -14,6 +14,8 @@ Column { readonly property bool dockHosted: page?.dockHosted ?? false readonly property var apps: page.appStore + Component.onCompleted: TrashService.detectFileManagers() + readonly property var activeColorOptions: [({ "value": "primary", "label": I18n.tr("Primary") diff --git a/quickshell/Modules/Settings/BarWidgetOptions/SystemMonitorOptions.qml b/quickshell/Modules/Settings/BarWidgetOptions/SystemMonitorOptions.qml index 24eb151f1..5f6095357 100644 --- a/quickshell/Modules/Settings/BarWidgetOptions/SystemMonitorOptions.qml +++ b/quickshell/Modules/Settings/BarWidgetOptions/SystemMonitorOptions.qml @@ -8,6 +8,8 @@ Column { property var page: null + Component.onCompleted: DgopService.ensureMeta() + readonly property string type: page.widgetType readonly property var gpuLabels: (DgopService.availableGpus ?? []).map(gpu => (gpu.driver ? gpu.driver.toUpperCase() + " " : "") + (gpu.displayName ?? "")) readonly property var mountLabels: { diff --git a/quickshell/Modules/Settings/DBarAppearanceTab.qml b/quickshell/Modules/Settings/DBarAppearanceTab.qml index 481886039..1e09fc686 100644 --- a/quickshell/Modules/Settings/DBarAppearanceTab.qml +++ b/quickshell/Modules/Settings/DBarAppearanceTab.qml @@ -10,6 +10,8 @@ Item { LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true + Component.onCompleted: BlurService.probe() + property var parentModal: null readonly property bool widgetBackgroundEnabled: !(bar.selectedBarConfig?.noBackground ?? false) readonly property var outlineColors: ["surfaceText", "secondary", "primary"] diff --git a/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml b/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml index 440db5779..0387bfb94 100644 --- a/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml +++ b/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml @@ -9,6 +9,8 @@ DesktopWidgetInstanceSettings { id: root readonly property var gpus: DgopService.availableGpus ?? [] + + Component.onCompleted: DgopService.ensureMeta() readonly property var gpuLabels: gpus.map(gpu => (gpu.driver ? gpu.driver.toUpperCase() + " " : "") + (gpu.displayName ?? "")) SettingsToggleRow { diff --git a/quickshell/Modules/Settings/KeybindsTab.qml b/quickshell/Modules/Settings/KeybindsTab.qml index 762cd85e0..15c59adaa 100644 --- a/quickshell/Modules/Settings/KeybindsTab.qml +++ b/quickshell/Modules/Settings/KeybindsTab.qml @@ -95,6 +95,7 @@ Item { Component.onDestruction: { _editAlive = false; _editRequest++; + KeybindsService.removeRef(); } function beginEdit(binding, key) { @@ -470,6 +471,7 @@ Item { } Component.onCompleted: { + KeybindsService.addRef(); _ensureCurrentProvider(); Qt.callLater(_applyRequestedSearch); } diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index 4c076e5a8..923d1efb8 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -56,10 +56,13 @@ Item { } Component.onCompleted: { + KeybindsService.addRef(); if (KeybindsService.available) KeybindsService.loadBinds(false); } + Component.onDestruction: KeybindsService.removeRef() + SettingsPage { id: mainColumn diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index e69e7a2fd..de2fd48b0 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -16,7 +16,10 @@ Column { width: parent?.width ?? 0 spacing: Theme.spacingL - Component.onCompleted: CompositorService.refreshDmsWindowFloatingRule() + Component.onCompleted: { + BlurService.probe(); + CompositorService.refreshDmsWindowFloatingRule(); + } ConfigInclude { id: windowRulesInclude diff --git a/quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml b/quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml index 70bc9c9c2..3c96a63d4 100644 --- a/quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml +++ b/quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml @@ -26,6 +26,8 @@ SettingsDropdownRow { currentValue: SessionData.terminalOverride.length > 0 ? SessionData.terminalOverride : autoLabel + Component.onCompleted: SessionData.probeTerminals() + onValueChanged: label => { const next = label === autoLabel ? "" : label; SessionData.set("terminalOverride", next); diff --git a/quickshell/Modules/Settings/WidgetsTabSection.qml b/quickshell/Modules/Settings/WidgetsTabSection.qml index f04f70e91..9cb3f91dd 100644 --- a/quickshell/Modules/Settings/WidgetsTabSection.qml +++ b/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -11,6 +11,8 @@ Column { id: root readonly property var log: Log.scoped("WidgetsTabSection") + Component.onCompleted: DgopService.ensureMeta() + property var items: [] property var allWidgets: [] property string title: "" diff --git a/quickshell/Modules/WallpaperBackground.qml b/quickshell/Modules/WallpaperBackground.qml index 2c217d899..6851450a1 100644 --- a/quickshell/Modules/WallpaperBackground.qml +++ b/quickshell/Modules/WallpaperBackground.qml @@ -75,18 +75,17 @@ Variants { Connections { target: SessionData + enabled: SessionData.perModeWallpaper + function onIsLightModeChanged() { - if (SessionData.perModeWallpaper) { - var newSource = SessionData.getMonitorWallpaper(modelData.name) || ""; - if (newSource !== root.source) { - root.source = newSource; - } - } + const newSource = SessionData.getMonitorWallpaper(modelData.name) || ""; + if (newSource !== root.source) + root.source = newSource; } } Connections { - target: NiriService + target: CompositorService.isNiri ? NiriService : null enabled: CompositorService.isNiri && root.scrollingEnabled function onAllWorkspacesChanged() { diff --git a/quickshell/Services/AppleMusicArtService.qml b/quickshell/Services/AppleMusicArtService.qml index b5d650b71..830c99965 100644 --- a/quickshell/Services/AppleMusicArtService.qml +++ b/quickshell/Services/AppleMusicArtService.qml @@ -25,12 +25,24 @@ Singleton { readonly property string _album: MprisController.activePlayer?.trackAlbum || "" readonly property string _cacheKey: _artist !== "" && _album !== "" ? (_artist + "\n" + _album).toLowerCase() : "" + property bool _pruned: false + on_CacheKeyChanged: _schedule() - onEnabledChanged: _schedule() - Component.onCompleted: { - _prune(); + onEnabledChanged: { + _pruneOnce(); _schedule(); } + Component.onCompleted: { + _pruneOnce(); + _schedule(); + } + + function _pruneOnce() { + if (_pruned || !enabled) + return; + _pruned = true; + _prune(); + } // Downloaded covers are a few MB each; drop anything untouched for 30 days so the // cache can't grow without bound. diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index 7a0b5d83c..e5dee9857 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -62,8 +62,10 @@ Singleton { target: pipewireBackendLoader.item ignoreUnknownSignals: true function onNodesUpdated() { - root.rebuildTypedNodeLists(); root.adoptPendingCardSink(); + if (root.refCount === 0) + return; + root.rebuildTypedNodeLists(); root.deviceRevision++; } } @@ -259,6 +261,8 @@ Singleton { function setDefaultSinkByName(name) { if (!name) return false; + if (refCount === 0) + rebuildTypedNodeLists(); for (const node of typedSinks) { if (node?.name === name) return setSink(node); @@ -269,6 +273,8 @@ Singleton { function setDefaultSourceByName(name) { if (!name) return false; + if (refCount === 0) + rebuildTypedNodeLists(); for (const node of typedSources) { if (node?.name === name) return setSource(node); @@ -893,6 +899,7 @@ EOFCONFIG Connections { target: root.sink?.audio ?? null + enabled: SettingsData.soundsEnabled && SettingsData.soundVolumeChanged function onVolumeChanged() { if (SessionData.suppressOSD) @@ -1688,7 +1695,7 @@ EOFCONFIG // Re-apply the saved preference: the live PipeWire value is runtime-only and resets on restart function applyMonoStartupPreference() { - if (!SettingsData._hasLoaded) + if (!SettingsData._hasLoaded || !SettingsData.audioMono) return; queryMonoSetting((supported, currentValue) => { if (supported && currentValue !== SettingsData.audioMono) @@ -1771,8 +1778,11 @@ EOFCONFIG } onRefCountChanged: { - if (root.refCount === 1) - root.refreshFreeBsdDevices(); + if (root.refCount !== 1) + return; + root.rebuildTypedNodeLists(); + root.deviceRevision++; + root.refreshFreeBsdDevices(); } Timer { diff --git a/quickshell/Services/BatteryService.qml b/quickshell/Services/BatteryService.qml index 1ca0a5aef..09b1e58e0 100644 --- a/quickshell/Services/BatteryService.qml +++ b/quickshell/Services/BatteryService.qml @@ -729,7 +729,7 @@ Singleton { readonly property bool suggestPowerSaver: false - readonly property var peripheralDevices: UPower.devices.values.filter(dev => dev && dev.ready && !dev.isLaptopBattery && peripheralIcon(dev.type) !== "").map(dev => ({ + readonly property var peripheralDevices: refCount === 0 ? [] : UPower.devices.values.filter(dev => dev && dev.ready && !dev.isLaptopBattery && peripheralIcon(dev.type) !== "").map(dev => ({ "name": dev.model || UPowerDeviceType.toString(dev.type), "percentage": Math.round(dev.percentage * 100), "type": dev.type, @@ -738,6 +738,8 @@ Singleton { })) readonly property var bluetoothDevices: { + if (refCount === 0) + return []; const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers]; return peripheralDevices.filter(dev => bluetoothTypes.includes(dev.type)); diff --git a/quickshell/Services/BluetoothService.qml b/quickshell/Services/BluetoothService.qml index 579e4fc11..69a4f66f3 100644 --- a/quickshell/Services/BluetoothService.qml +++ b/quickshell/Services/BluetoothService.qml @@ -23,6 +23,7 @@ Singleton { property bool wpexecChecked: false property var pendingCodecActions: [] property bool _codecSignalsSubscribed: false + property var _codecSubscriptionIds: [] readonly property string bluezService: "org.bluez" readonly property string mediaTransportIface: "org.bluez.MediaTransport1" readonly property string mediaEndpointIface: "org.bluez.MediaEndpoint1" @@ -77,11 +78,22 @@ Singleton { }); } - Component.onCompleted: { - detectWpexecProcess.running = true; - maybeSubscribeCodecSignals(); + readonly property bool hasConnectedAudioDevice: { + if (!adapter?.devices) + return false; + return adapter.devices.values.some(dev => dev?.connected && isAudioDevice(dev)); } + onHasConnectedAudioDeviceChanged: { + if (hasConnectedAudioDevice) { + maybeSubscribeCodecSignals(); + return; + } + unsubscribeCodecSignals(); + } + + Component.onCompleted: maybeSubscribeCodecSignals() + function setBluetoothEnabled(enabled) { if (bluetoothBridgeAvailable) { DMSService.sendRequest("bluetooth.setPowered", { @@ -117,12 +129,18 @@ Singleton { function onConnectionStateChanged() { root._codecSignalsSubscribed = false; + root._codecSubscriptionIds = []; root.maybeSubscribeCodecSignals(); } function onCapabilitiesReceived() { root.maybeSubscribeCodecSignals(); } + } + + Connections { + target: DMSService + enabled: root._codecSignalsSubscribed function onDbusSignalReceived(subscriptionId, data) { root.handleCodecDbusSignal(data); @@ -143,12 +161,34 @@ Singleton { } function maybeSubscribeCodecSignals() { - if (!dbusBridgeAvailable || _codecSignalsSubscribed) + if (!dbusBridgeAvailable || _codecSignalsSubscribed || !hasConnectedAudioDevice) return; _codecSignalsSubscribed = true; - DMSService.dbusSubscribe("system", bluezService, "", objectManagerIface, "InterfacesAdded", null); - DMSService.dbusSubscribe("system", bluezService, "", objectManagerIface, "InterfacesRemoved", null); - DMSService.dbusSubscribe("system", bluezService, "", propertiesIface, "PropertiesChanged", null); + DMSService.dbusSubscribe("system", bluezService, "", objectManagerIface, "InterfacesAdded", _rememberCodecSubscription); + DMSService.dbusSubscribe("system", bluezService, "", objectManagerIface, "InterfacesRemoved", _rememberCodecSubscription); + DMSService.dbusSubscribe("system", bluezService, "", propertiesIface, "PropertiesChanged", _rememberCodecSubscription); + } + + function _rememberCodecSubscription(response) { + const id = response.result?.subscriptionId; + if (response.error || !id) + return; + if (!_codecSignalsSubscribed) { + DMSService.dbusUnsubscribe(id, null); + return; + } + _codecSubscriptionIds = _codecSubscriptionIds.concat([id]); + } + + function unsubscribeCodecSignals() { + if (!_codecSignalsSubscribed) + return; + _codecSignalsSubscribed = false; + const ids = _codecSubscriptionIds; + _codecSubscriptionIds = []; + if (!DMSService.isConnected) + return; + ids.forEach(id => DMSService.dbusUnsubscribe(id, null)); } function handleCodecDbusSignal(data) { @@ -466,28 +506,116 @@ Singleton { function codecMap() { return { - "LHDC_V5": { "name": "LHDC v5", "description": "Highest quality • Low latency", "qualityColor": "#4CAF50" }, - "LHDC_V3": { "name": "LHDC v3", "description": "High quality • Low latency", "qualityColor": "#FF9800" }, - "LDAC": { "name": "LDAC", "description": "Highest quality • Higher battery usage", "qualityColor": "#4CAF50" }, - "APTX_HD": { "name": "aptX HD", "description": "High quality • Balanced battery", "qualityColor": "#FF9800" }, - "APTX_LL": { "name": "aptX LL", "description": "Low latency • Gaming and video", "qualityColor": "#FF9800" }, - "APTX_ADAPTIVE": { "name": "aptX Adaptive", "description": "Adaptive quality and latency", "qualityColor": "#FF9800" }, - "APTX": { "name": "aptX", "description": "Good quality • Low latency", "qualityColor": "#FF9800" }, - "AAC_ELD": { "name": "AAC-ELD", "description": "Low-delay AAC • Voice and video", "qualityColor": "#2196F3" }, - "AAC": { "name": "AAC", "description": "Balanced quality and battery", "qualityColor": "#2196F3" }, - "OPUS_05": { "name": "Opus", "description": "High quality • Modern Bluetooth LE audio", "qualityColor": "#4CAF50" }, - "OPUS_G": { "name": "Opus", "description": "High quality • Modern Bluetooth LE audio", "qualityColor": "#4CAF50" }, - "OPUS": { "name": "Opus", "description": "High quality • Efficient streaming", "qualityColor": "#4CAF50" }, - "LC3": { "name": "LC3", "description": "LE Audio • Efficient high quality", "qualityColor": "#4CAF50" }, - "LC3_SWB": { "name": "LC3-SWB", "description": "Wideband speech • Hands-free calls", "qualityColor": "#9E9E9E" }, - "LC3_A127": { "name": "LC3", "description": "LE Audio speech • Hands-free calls", "qualityColor": "#9E9E9E" }, - "LC3PLUS_HR": { "name": "LC3plus HR", "description": "High resolution LE Audio", "qualityColor": "#4CAF50" }, - "SBC_XQ": { "name": "SBC-XQ", "description": "Enhanced SBC • Better compatibility", "qualityColor": "#2196F3" }, - "SBC": { "name": "SBC", "description": "Basic quality • Universal compatibility", "qualityColor": "#9E9E9E" }, - "MSBC": { "name": "mSBC", "description": "Modified SBC • Optimized for speech", "qualityColor": "#9E9E9E" }, - "CVSD": { "name": "CVSD", "description": "Basic speech codec • Legacy compatibility", "qualityColor": "#9E9E9E" }, - "G722": { "name": "G722", "description": "ASHA • Hearing aid audio", "qualityColor": "#9E9E9E" }, - "FASTSTREAM": { "name": "FastStream", "description": "Low latency SBC variant", "qualityColor": "#2196F3" } + "LHDC_V5": { + "name": "LHDC v5", + "description": "Highest quality • Low latency", + "qualityColor": "#4CAF50" + }, + "LHDC_V3": { + "name": "LHDC v3", + "description": "High quality • Low latency", + "qualityColor": "#FF9800" + }, + "LDAC": { + "name": "LDAC", + "description": "Highest quality • Higher battery usage", + "qualityColor": "#4CAF50" + }, + "APTX_HD": { + "name": "aptX HD", + "description": "High quality • Balanced battery", + "qualityColor": "#FF9800" + }, + "APTX_LL": { + "name": "aptX LL", + "description": "Low latency • Gaming and video", + "qualityColor": "#FF9800" + }, + "APTX_ADAPTIVE": { + "name": "aptX Adaptive", + "description": "Adaptive quality and latency", + "qualityColor": "#FF9800" + }, + "APTX": { + "name": "aptX", + "description": "Good quality • Low latency", + "qualityColor": "#FF9800" + }, + "AAC_ELD": { + "name": "AAC-ELD", + "description": "Low-delay AAC • Voice and video", + "qualityColor": "#2196F3" + }, + "AAC": { + "name": "AAC", + "description": "Balanced quality and battery", + "qualityColor": "#2196F3" + }, + "OPUS_05": { + "name": "Opus", + "description": "High quality • Modern Bluetooth LE audio", + "qualityColor": "#4CAF50" + }, + "OPUS_G": { + "name": "Opus", + "description": "High quality • Modern Bluetooth LE audio", + "qualityColor": "#4CAF50" + }, + "OPUS": { + "name": "Opus", + "description": "High quality • Efficient streaming", + "qualityColor": "#4CAF50" + }, + "LC3": { + "name": "LC3", + "description": "LE Audio • Efficient high quality", + "qualityColor": "#4CAF50" + }, + "LC3_SWB": { + "name": "LC3-SWB", + "description": "Wideband speech • Hands-free calls", + "qualityColor": "#9E9E9E" + }, + "LC3_A127": { + "name": "LC3", + "description": "LE Audio speech • Hands-free calls", + "qualityColor": "#9E9E9E" + }, + "LC3PLUS_HR": { + "name": "LC3plus HR", + "description": "High resolution LE Audio", + "qualityColor": "#4CAF50" + }, + "SBC_XQ": { + "name": "SBC-XQ", + "description": "Enhanced SBC • Better compatibility", + "qualityColor": "#2196F3" + }, + "SBC": { + "name": "SBC", + "description": "Basic quality • Universal compatibility", + "qualityColor": "#9E9E9E" + }, + "MSBC": { + "name": "mSBC", + "description": "Modified SBC • Optimized for speech", + "qualityColor": "#9E9E9E" + }, + "CVSD": { + "name": "CVSD", + "description": "Basic speech codec • Legacy compatibility", + "qualityColor": "#9E9E9E" + }, + "G722": { + "name": "G722", + "description": "ASHA • Hearing aid audio", + "qualityColor": "#9E9E9E" + }, + "FASTSTREAM": { + "name": "FastStream", + "description": "Low latency SBC variant", + "qualityColor": "#2196F3" + } }; } diff --git a/quickshell/Services/BlurService.qml b/quickshell/Services/BlurService.qml index 5ca4efc0f..43a98415f 100644 --- a/quickshell/Services/BlurService.qml +++ b/quickshell/Services/BlurService.qml @@ -12,7 +12,13 @@ Singleton { readonly property var log: Log.scoped("BlurService") property bool compositorSupported: false + property bool _probed: false readonly property bool available: compositorSupported + readonly property bool blurRequested: SettingsData.blurEnabled ?? false + onBlurRequestedChanged: { + if (blurRequested) + probe(); + } readonly property bool enabled: available && (SettingsData.blurEnabled ?? false) // These settings predate non-blurred surface borders, so keep their keys for compatibility. @@ -68,5 +74,15 @@ Singleton { } } - Component.onCompleted: blurProbe.running = true + Component.onCompleted: { + if (blurRequested) + probe(); + } + + function probe() { + if (_probed) + return; + _probed = true; + blurProbe.running = true; + } } diff --git a/quickshell/Services/BrightnessService.qml b/quickshell/Services/BrightnessService.qml index 63770959e..e32892368 100644 --- a/quickshell/Services/BrightnessService.qml +++ b/quickshell/Services/BrightnessService.qml @@ -520,20 +520,24 @@ Singleton { } Timer { - id: screenChangeRescanTimer - property int rescanAttempt: 0 - interval: 3000 + id: rescanTimer + property var schedule: [] + property int attempt: 0 repeat: false + function run(intervals) { + schedule = intervals; + attempt = 0; + interval = intervals[0]; + restart(); + } onTriggered: { rescanDevices(); - rescanAttempt++; - if (rescanAttempt < 3) { - interval = rescanAttempt === 1 ? 5000 : 8000; + attempt++; + if (attempt < schedule.length) { + interval = schedule[attempt]; restart(); return; } - rescanAttempt = 0; - interval = 3000; osdSuppressTimer.restart(); } } @@ -543,9 +547,7 @@ Singleton { function onScreensChanged() { suppressOsd = true; - screenChangeRescanTimer.rescanAttempt = 0; - screenChangeRescanTimer.interval = 3000; - screenChangeRescanTimer.restart(); + rescanTimer.run([3000, 5000, 8000]); } } @@ -575,6 +577,7 @@ Singleton { function onSessionResumed() { suppressOsd = true; osdSuppressTimer.restart(); + rescanTimer.run([400, 1400, 2600]); } } diff --git a/quickshell/Services/CalendarDankBackend.qml b/quickshell/Services/CalendarDankBackend.qml index 2cc0e72d9..29ec8d584 100644 --- a/quickshell/Services/CalendarDankBackend.qml +++ b/quickshell/Services/CalendarDankBackend.qml @@ -12,6 +12,9 @@ Item { readonly property var log: Log.scoped("CalendarDankBackend") property bool enabled: false + property bool preferred: false + property int _rediscoverMisses: 0 + readonly property int _rediscoverCap: 20 property string socketPath: "" readonly property bool socketFound: socketPath.length > 0 @@ -41,6 +44,7 @@ Item { onEnabledChanged: { if (enabled) { + _rediscoverMisses = 0; if (!connected) discoverProcess.running = true; return; @@ -51,8 +55,14 @@ Item { connected = false; } + onConnectedChanged: { + if (!connected) + _rediscoverMisses = 0; + } + Component.onCompleted: { - binaryCheck.running = true; + if (!enabled) + return; discoverProcess.running = true; } @@ -87,6 +97,8 @@ Item { subscribeSocket.connected = false; root.socketPath = ""; } + if (!root.binaryChecked) + binaryCheck.running = true; } } } @@ -95,8 +107,9 @@ Item { id: rediscoverTimer interval: 3000 repeat: true - running: root.enabled && !root.connected + running: root.enabled && !root.connected && (root.preferred || (root.binaryChecked && root.binaryExists)) && root._rediscoverMisses < root._rediscoverCap onTriggered: { + root._rediscoverMisses++; if (!discoverProcess.running) discoverProcess.running = true; } @@ -113,8 +126,10 @@ Item { default: return; } - if (enabled && !connected) - discoverProcess.running = true; + if (!enabled || connected) + return; + _rediscoverMisses = 0; + discoverProcess.running = true; } function _applySocketPath(path) { diff --git a/quickshell/Services/CalendarKhalBackend.qml b/quickshell/Services/CalendarKhalBackend.qml index 38e20f0b5..f4488c7d9 100644 --- a/quickshell/Services/CalendarKhalBackend.qml +++ b/quickshell/Services/CalendarKhalBackend.qml @@ -9,6 +9,8 @@ Item { id: root readonly property var log: Log.scoped("CalendarKhalBackend") + property bool enabled: false + property bool _checked: false property bool installed: false property var eventsByDate: ({}) property bool isLoading: false @@ -18,10 +20,14 @@ Item { property string dateFormat: "MM/dd/yyyy" function checkAvailability() { - if (!formatProcess.running) - formatProcess.running = true; + if (_checked || !enabled) + return; + _checked = true; + formatProcess.running = true; } + onEnabledChanged: checkAvailability() + function loadCurrentMonth() { let today = new Date(); let firstDay = new Date(today.getFullYear(), today.getMonth(), 1); @@ -61,8 +67,9 @@ Item { command: ["khal", "printformats"] running: false onExited: exitCode => { - if (exitCode !== 0) - checkProcess.running = true; + if (exitCode === 127) + return; + checkProcess.running = true; } stdout: StdioCollector { @@ -75,7 +82,6 @@ Item { root.dateFormat = root._parseDateFormat(formatExample); break; } - checkProcess.running = true; } } } diff --git a/quickshell/Services/CalendarService.qml b/quickshell/Services/CalendarService.qml index ad40b4567..3b785dc09 100644 --- a/quickshell/Services/CalendarService.qml +++ b/quickshell/Services/CalendarService.qml @@ -56,12 +56,14 @@ Singleton { CalendarKhalBackend { id: khalBackend + enabled: root.backendPref !== "dankcal" onEventsByDateChanged: root.mergeEvents() } CalendarDankBackend { id: dankBackend enabled: root.backendPref === "dankcal" || root.backendPref === "auto" + preferred: root.backendPref === "dankcal" onEventsByDateChanged: root.mergeEvents() onTasksByDateChanged: root.mergeEvents() onConnectedChanged: { diff --git a/quickshell/Services/CavaService.qml b/quickshell/Services/CavaService.qml index 54e7771e9..315150bf0 100644 --- a/quickshell/Services/CavaService.qml +++ b/quickshell/Services/CavaService.qml @@ -13,6 +13,9 @@ Singleton { property list values: Array(6) property int refCount: 0 property bool cavaAvailable: false + property bool _cavaChecked: false + readonly property bool cavaWanted: SettingsData.audioVisualizerEnabled || refCount > 0 + onCavaWantedChanged: _checkCava() readonly property string _confPath: `${Paths.strip(StandardPaths.writableLocation(StandardPaths.TempLocation))}/dms-cava-${Date.now()}-${Math.floor(Math.random() * 1000000)}.conf` Process { @@ -21,11 +24,16 @@ Singleton { command: ["sh", "-c", "command -v cava"] running: false onExited: exitCode => { + root._cavaChecked = true; root.cavaAvailable = exitCode === 0 && Quickshell.env("DMS_DISABLE_CAVA") !== "1"; } } - Component.onCompleted: { + Component.onCompleted: _checkCava() + + function _checkCava() { + if (!cavaWanted || _cavaChecked || cavaCheck.running) + return; cavaCheck.running = true; } diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index 3deb1001e..13f49e49f 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -32,6 +32,7 @@ Singleton { property int selectedIndex: 0 property bool keyboardNavigationActive: false property int refCount: 0 + property bool _pasteSupportChecked: false property bool _launcherCacheValid: false property string _launcherCachedQuery: "" property var _launcherCachedEntries: [] @@ -55,18 +56,40 @@ Singleton { Connections { target: DMSService + enabled: root.refCount > 0 function onIsConnectedChanged() { root.refreshPasteSupport(); } } - Component.onCompleted: refreshPasteSupport() + onRefCountChanged: { + if (refCount > 0) { + ensureSubscription(); + } else if (refCount === 0 && DMSService.activeSubscriptions.includes("clipboard")) { + DMSService.removeSubscription("clipboard"); + } + } + + function ensureSubscription() { + if (refCount <= 0) + return; + if (!_pasteSupportChecked) + refreshPasteSupport(); + if (DMSService.activeSubscriptions.includes("clipboard")) + return; + if (DMSService.activeSubscriptions.includes("all")) + return; + DMSService.addSubscription("clipboard"); + refresh(); + } function refreshPasteSupport() { if (!DMSService.isConnected) { pasteSupported = false; + _pasteSupportChecked = false; return; } + _pasteSupportChecked = true; DMSService.sendRequest("clipboard.pasteSupported", null, function (response) { root.pasteSupported = !response.error && response.result && response.result.supported === true; }); @@ -151,6 +174,8 @@ Singleton { if (!clipboardAvailable) { return; } + if (!_pasteSupportChecked) + refreshPasteSupport(); const trimmed = (query || "").toString().trim(); const maxItems = limit > 0 ? limit : 20; diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 5eb742409..0b783c8b6 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -65,6 +65,7 @@ Singleton { target: Quickshell function onScreensChanged() { root.probeOutputPower(); + root.refreshHyprlandMonitorLayout(); } } readonly property bool frameCompositorLayoutReady: (!isNiri || NiriService.frameLayoutReady) && (!isHyprland || HyprlandService.frameLayoutReady) @@ -194,9 +195,8 @@ Singleton { Connections { target: AqueousService + enabled: root.isAqueous function onStateChanged() { - if (!root.isAqueous) - return; root.scheduleSort(); if (AqueousService.available) root.workspaceStateChanged(); @@ -496,6 +496,7 @@ Singleton { } Connections { target: NiriService + enabled: root.isNiri function onWindowsChanged() { root.scheduleSort(); } @@ -506,23 +507,17 @@ Singleton { detectCompositor(); updateHyprlandVisibleSpecialWorkspaces(null); scheduleSort(); - Qt.callLater(() => { - NiriService.generateNiriLayoutConfig(); - HyprlandService.generateLayoutConfig(); - }); } Connections { target: MangoService + enabled: root.isMango function onStateChanged() { - if (!root.isMango) - return; root.scheduleSort(); root.workspaceStateChanged(); } function onWindowsChanged() { - if (isMango) - scheduleSort(); + root.scheduleSort(); } } @@ -1000,13 +995,6 @@ Singleton { } } - Connections { - target: Quickshell - function onScreensChanged() { - root.refreshHyprlandMonitorLayout(); - } - } - // Workspace moves can land before Hyprland announces the monitor and before the Wayland // output reaches us, so re-read both models once the screen set settles (#3133) function refreshHyprlandMonitorLayout() { @@ -1378,22 +1366,6 @@ Singleton { return toplevels.filter(w => map.get(w) === currentWorkspaceId); } - Timer { - id: compositorInitTimer - interval: 100 - running: true - repeat: false - onTriggered: { - detectCompositor(); - compositorDetected = true; - Qt.callLater(() => { - NiriService.generateNiriLayoutConfig(); - HyprlandService.generateLayoutConfig(); - MangoService.generateLayoutConfig(); - }); - } - } - // Primary detection asks the kernel which process owns the $WAYLAND_DISPLAY // socket — the compositor quickshell is actually connected to. Env vars like // HYPRLAND_INSTANCE_SIGNATURE / MANGO_INSTANCE_SIGNATURE can leak into the diff --git a/quickshell/Services/CupsService.qml b/quickshell/Services/CupsService.qml index b6f2d622a..1a369021e 100644 --- a/quickshell/Services/CupsService.qml +++ b/quickshell/Services/CupsService.qml @@ -223,6 +223,8 @@ Singleton { return; cupsAvailable = DMSService.capabilities.includes("cups"); + if (refCount <= 0) + return; if (cupsAvailable && !stateInitialized) { stateInitialized = true; getState(); diff --git a/quickshell/Services/DMSNetworkService.qml b/quickshell/Services/DMSNetworkService.qml index 6cc9a7bf7..b92a2b576 100644 --- a/quickshell/Services/DMSNetworkService.qml +++ b/quickshell/Services/DMSNetworkService.qml @@ -228,6 +228,7 @@ Singleton { property int refCount: 0 property bool stateInitialized: false + property var _lastRawState: null property string credentialsToken: "" property string credentialsSSID: "" @@ -330,9 +331,12 @@ Singleton { function addRef() { refCount++; - if (refCount === 1 && networkAvailable) { + if (refCount !== 1) + return; + if (_lastRawState) + syncLists(_lastRawState); + if (networkAvailable) startAutoScan(); - } } function removeRef() { @@ -350,7 +354,7 @@ Singleton { DMSService.sendRequest("network.getState", null, response => { if (response.result) { updateState(response.result); - if (!initialStateFetched && response.result.wifiEnabled && (!response.result.wifiNetworks || response.result.wifiNetworks.length === 0)) { + if (refCount > 0 && !initialStateFetched && response.result.wifiEnabled && (!response.result.wifiNetworks || response.result.wifiNetworks.length === 0)) { initialStateFetched = true; Qt.callLater(() => scanWifi()); } @@ -374,7 +378,41 @@ Singleton { return same ? previous : merged; } + function syncLists(state) { + ethernetDevices = keepUnchanged(ethernetDevices, state.ethernetDevices || []); + wiredConnections = keepUnchanged(wiredConnections, state.wiredConnections || []); + cellularDevices = keepUnchanged(cellularDevices, state.cellularDevices || []); + cellularConnections = keepUnchanged(cellularConnections, state.cellularConnections || []); + + if (state.wifiNetworks) { + wifiNetworks = keepUnchanged(wifiNetworks, state.wifiNetworks); + } + + if (!state.wifiNetworks && !state.savedWifiNetworks) + return; + const hasSavedWifiState = DMSService.apiVersion >= savedWifiStateApiVersion && Array.isArray(state.savedWifiNetworks); + const sourceSavedNetworks = hasSavedWifiState ? state.savedWifiNetworks : (state.wifiNetworks || []).filter(network => network.saved); + const normalized = sourceSavedNetworks.map(network => Object.assign({}, network, { + saved: true, + outOfRange: hasSavedWifiState ? network.outOfRange === true : false + })); + const saved = keepUnchanged(savedWifiNetworks, normalized); + if (saved !== savedWifiNetworks) { + const mapping = {}; + for (const network of saved) { + if (network?.ssid) + mapping[network.ssid] = network.ssid; + } + savedConnections = saved; + savedWifiNetworks = saved; + ssidToConnectionName = mapping; + } + + networksUpdated(); + } + function updateState(state) { + _lastRawState = state; const previousConnecting = isConnecting; const previousConnectingSSID = connectingSSID; @@ -387,9 +425,6 @@ Singleton { ethernetInterface = state.ethernetDevice || ""; ethernetConnected = state.ethernetConnected || false; ethernetConnectionUuid = state.ethernetConnectionUuid || ""; - ethernetDevices = keepUnchanged(ethernetDevices, state.ethernetDevices || []); - - wiredConnections = keepUnchanged(wiredConnections, state.wiredConnections || []); cellularIP = state.cellularIP || ""; cellularInterface = state.cellularDevice || ""; @@ -397,8 +432,6 @@ Singleton { cellularEnabled = state.cellularEnabled !== undefined ? state.cellularEnabled : true; cellularHardwareEnabled = state.cellularHardwareEnabled !== undefined ? state.cellularHardwareEnabled : true; cellularConnectionUuid = state.cellularConnectionUuid || ""; - cellularDevices = keepUnchanged(cellularDevices, state.cellularDevices || []); - cellularConnections = keepUnchanged(cellularConnections, state.cellularConnections || []); wifiIP = state.wifiIP || ""; wifiInterface = state.wifiDevice || ""; @@ -450,31 +483,8 @@ Singleton { currentWifiSSID = state.wifiSSID || ""; wifiSignalStrength = state.wifiSignal || 0; - if (state.wifiNetworks) { - wifiNetworks = keepUnchanged(wifiNetworks, state.wifiNetworks); - } - - if (state.wifiNetworks || state.savedWifiNetworks) { - const hasSavedWifiState = DMSService.apiVersion >= savedWifiStateApiVersion && Array.isArray(state.savedWifiNetworks); - const sourceSavedNetworks = hasSavedWifiState ? state.savedWifiNetworks : (state.wifiNetworks || []).filter(network => network.saved); - const normalized = sourceSavedNetworks.map(network => Object.assign({}, network, { - saved: true, - outOfRange: hasSavedWifiState ? network.outOfRange === true : false - })); - const saved = keepUnchanged(savedWifiNetworks, normalized); - if (saved !== savedWifiNetworks) { - const mapping = {}; - for (const network of saved) { - if (network?.ssid) - mapping[network.ssid] = network.ssid; - } - savedConnections = saved; - savedWifiNetworks = saved; - ssidToConnectionName = mapping; - } - - networksUpdated(); - } + if (refCount > 0) + syncLists(state); if (state.vpnProfiles) { vpnProfiles = keepUnchanged(vpnProfiles, state.vpnProfiles); diff --git a/quickshell/Services/DMSService.qml b/quickshell/Services/DMSService.qml index 92a72b780..9d04e075c 100644 --- a/quickshell/Services/DMSService.qml +++ b/quickshell/Services/DMSService.qml @@ -82,7 +82,7 @@ Singleton { property bool screensaverInhibited: false property var screensaverInhibitors: [] - property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate", "files"] + property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus"] property var connectionCapabilities: null Component.onCompleted: { diff --git a/quickshell/Services/DSearchService.qml b/quickshell/Services/DSearchService.qml index 63efcb065..3fadf5680 100644 --- a/quickshell/Services/DSearchService.qml +++ b/quickshell/Services/DSearchService.qml @@ -14,6 +14,7 @@ Singleton { property int indexVersion: 0 property bool supportsTypeFilter: false property bool versionChecked: false + property bool _checked: false signal searchResultsReceived(var results) signal statsReceived(var stats) @@ -22,7 +23,7 @@ Singleton { Process { id: checkProcess command: ["sh", "-c", "command -v dsearch"] - running: true + running: false stdout: SplitParser { onRead: line => { @@ -52,7 +53,14 @@ Singleton { }); } + function ensureChecked() { + if (_checked) + return; + rediscover(); + } + function ping(callback) { + ensureChecked(); if (!dsearchAvailable) { if (callback) { callback({ @@ -94,6 +102,7 @@ Singleton { return; } + ensureChecked(); if (!dsearchAvailable) { if (callback) { callback({ @@ -179,6 +188,7 @@ Singleton { } function rediscover() { + _checked = true; checkProcess.running = true; } } diff --git a/quickshell/Services/DesktopService.qml b/quickshell/Services/DesktopService.qml index bce9c8af2..c1a8ca091 100644 --- a/quickshell/Services/DesktopService.qml +++ b/quickshell/Services/DesktopService.qml @@ -15,17 +15,17 @@ Singleton { property bool isSystemd: false property bool systemdAutostartTargetActive: false - property bool systemdAutostartTargetChecked: false + property bool systemdAutostartTargetChecked: Qt.platform.os === "unix" readonly property bool autostartAvailable: root.systemdAutostartTargetChecked && (!root.isSystemd || root.systemdAutostartTargetActive) + property bool _probed: false - Component.onCompleted: { - Paths.desktopIconResolver = name => resolveIconPath(name); - if (Qt.platform.os === "unix") { - root.isSystemd = false; - root.systemdAutostartTargetChecked = true; - } else { - initSystemCheckProcess.running = true; - } + Component.onCompleted: Paths.desktopIconResolver = name => resolveIconPath(name) + + function ensureProbed() { + if (_probed || systemdAutostartTargetChecked) + return; + _probed = true; + initSystemCheckProcess.running = true; } Process { diff --git a/quickshell/Services/DgopService.qml b/quickshell/Services/DgopService.qml index 726f35064..257bffa1f 100644 --- a/quickshell/Services/DgopService.qml +++ b/quickshell/Services/DgopService.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import Quickshell.Io import Quickshell.Services.UPower import qs.Common import qs.Services @@ -15,14 +14,16 @@ Singleton { signal statsUpdated property int refCount: 0 - readonly property bool powerSaver: PowerProfileWatcher.currentProfile === PowerProfile.PowerSaver - property int updateInterval: refCount > 0 ? (powerSaver ? 6000 : 3000) : (powerSaver ? 60000 : 30000) + readonly property bool powerSaver: pollingActive && PowerProfileWatcher.currentProfile === PowerProfile.PowerSaver + property int updateInterval: powerSaver ? 6000 : 3000 property bool isUpdating: false property bool pendingUpdate: false property int subscriptionGeneration: 0 readonly property bool pollingActive: dgopAvailable && refCount > 0 && enabledModules.length > 0 readonly property bool dgopAvailable: DMSService.isConnected && DMSService.capabilities.includes("dgop") property bool sessionGpuIdsSeeded: false + property bool _metaWanted: false + property bool _metaLoaded: false property var moduleRefCounts: ({}) property var enabledModules: [] @@ -97,7 +98,17 @@ Singleton { "write": [] }) + function ensureMeta() { + _metaWanted = true; + if (_metaLoaded || !dgopAvailable) + return; + _metaLoaded = true; + initializeSystemMetadata(); + initializeGpuMetadata(); + } + function addRef(modules = null) { + ensureMeta(); refCount++; let modulesChanged = false; @@ -205,6 +216,7 @@ Singleton { } function addGpuPciId(pciId) { + ensureMeta(); const currentCount = gpuPciIdRefCounts[pciId] || 0; gpuPciIdRefCounts[pciId] = currentCount + 1; @@ -696,11 +708,13 @@ Singleton { } onDgopAvailableChanged: { - if (!dgopAvailable) + if (!dgopAvailable) { + _metaLoaded = false; return; + } - initializeSystemMetadata(); - initializeGpuMetadata(); + if (_metaWanted) + ensureMeta(); initializeDiskMounts(); if (!sessionGpuIdsSeeded && SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.length > 0) { @@ -710,47 +724,4 @@ Singleton { } } } - - Process { - id: osReleaseProcess - command: ["cat", "/etc/os-release"] - running: false - onExited: exitCode => { - if (exitCode !== 0) { - log.warn("Failed to read /etc/os-release"); - } - } - stdout: StdioCollector { - onStreamFinished: { - if (text.trim()) { - try { - const lines = text.trim().split('\n'); - let prettyName = ""; - let name = ""; - - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith('PRETTY_NAME=')) { - prettyName = trimmedLine.substring(12).replace(/^["']|["']$/g, ''); - } else if (trimmedLine.startsWith('NAME=')) { - name = trimmedLine.substring(5).replace(/^["']|["']$/g, ''); - } - } - - // Prefer PRETTY_NAME, fallback to NAME - const distroName = prettyName || name || "Linux"; - distribution = distroName; - log.info("Detected distribution:", distroName); - } catch (e) { - log.warn("Failed to parse /etc/os-release:", e); - distribution = "Linux"; - } - } - } - } - } - - Component.onCompleted: { - osReleaseProcess.running = true; - } } diff --git a/quickshell/Services/FilesService.qml b/quickshell/Services/FilesService.qml index e55c8463b..8fe05133b 100644 --- a/quickshell/Services/FilesService.qml +++ b/quickshell/Services/FilesService.qml @@ -15,9 +15,34 @@ Singleton { "trash": true }) property var userDirs: [] + property int refCount: 0 signal watchEvent(var data) + onRefCountChanged: { + if (refCount > 0) { + ensureSubscription(); + } else if (refCount === 0 && DMSService.activeSubscriptions.includes("files")) { + DMSService.removeSubscription("files"); + } + } + + function addRef() { + refCount++; + } + + function removeRef() { + refCount = Math.max(0, refCount - 1); + } + + function ensureSubscription() { + if (DMSService.activeSubscriptions.includes("files")) + return; + if (DMSService.activeSubscriptions.includes("all")) + return; + DMSService.addSubscription("files"); + } + onConnectedChanged: { if (connected) refreshUserDirs(); @@ -32,6 +57,7 @@ Singleton { } function watch(path, options, callback) { + ensureSubscription(); _send("files.watch", Object.assign({ "path": path }, _options(options)), callback); diff --git a/quickshell/Services/FirstLaunchService.qml b/quickshell/Services/FirstLaunchService.qml index f3aaa8660..b93218f8a 100644 --- a/quickshell/Services/FirstLaunchService.qml +++ b/quickshell/Services/FirstLaunchService.qml @@ -44,7 +44,16 @@ Singleton { } function checkFirstLaunch() { - firstLaunchCheckProcess.running = true; + markerProbe.path = root.firstLaunchMarkerPath; + } + + function _finishCheck(firstLaunch) { + isFirstLaunch = firstLaunch; + checkComplete = true; + if (!firstLaunch) + return; + log.info("First launch detected, greeter will be shown"); + greeterRequested(); } function markFirstLaunchComplete() { @@ -57,45 +66,32 @@ Singleton { greeterDismissed = true; } - Process { - id: firstLaunchCheckProcess - - command: ["sh", "-c", ` - SETTINGS='` + settingsPath + `' - MARKER='` + firstLaunchMarkerPath + `' - if [ -f "$MARKER" ]; then - echo 'skip' - elif [ -f "$SETTINGS" ]; then - echo 'existing_user' - else - echo 'first' - fi - `] - running: false - - stdout: SplitParser { - onRead: data => { - const result = data.trim(); - - if (result === "first") { - root.isFirstLaunch = true; - log.info("First launch detected, greeter will be shown"); - } else if (result === "existing_user") { - root.isFirstLaunch = false; - log.info("Existing user detected, silently creating marker"); - touchMarkerProcess.running = true; - } else { - root.isFirstLaunch = false; - } - - root.checkComplete = true; - - if (root.isFirstLaunch) - root.greeterRequested(); + FileView { + id: markerProbe + path: "" + printErrors: false + onLoaded: root._finishCheck(false) + onLoadFailed: error => { + if (error !== FileViewError.FileNotFound) { + root._finishCheck(false); + return; } + settingsProbe.path = root.settingsPath; } } + FileView { + id: settingsProbe + path: "" + printErrors: false + onLoaded: { + log.info("Existing user detected, silently creating marker"); + touchMarkerProcess.running = true; + root._finishCheck(false); + } + onLoadFailed: error => root._finishCheck(error === FileViewError.FileNotFound) + } + Process { id: touchMarkerProcess diff --git a/quickshell/Services/GreeterService.qml b/quickshell/Services/GreeterService.qml index 43dc7e19f..26d9d68f9 100644 --- a/quickshell/Services/GreeterService.qml +++ b/quickshell/Services/GreeterService.qml @@ -20,6 +20,7 @@ Singleton { property bool syncing: false property string syncStatus: "" property bool fallbackFromPrecheck: false + property bool _detected: false readonly property bool linked: slotState === "live" readonly property bool profileSyncSufficient: configured && inGroup @@ -27,9 +28,16 @@ Singleton { signal syncFinished function refresh() { + _detected = true; detectProcess.running = true; } + function ensureDetected() { + if (_detected) + return; + refresh(); + } + function sync() { if (!binaryExists || syncing) return; @@ -95,7 +103,7 @@ for f in "/var/cache/dms-greeter/users/$USER/settings.json" /var/cache/dms-greet done [ -f "/var/cache/dms-greeter/users/$USER/settings.json" ] && echo slot exit 0`] - running: true + running: false stdout: StdioCollector { onStreamFinished: { diff --git a/quickshell/Services/GreeterUsersService.qml b/quickshell/Services/GreeterUsersService.qml index 274d899b0..e239221a7 100644 --- a/quickshell/Services/GreeterUsersService.qml +++ b/quickshell/Services/GreeterUsersService.qml @@ -18,7 +18,11 @@ Singleton { property bool loaded: false property bool refreshing: false - Component.onCompleted: refresh() + Component.onCompleted: { + if (!SessionData.isGreeterMode) + return; + refresh(); + } function refresh() { if (refreshing) diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index a95d1c128..cc0a2d6fb 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -71,9 +71,9 @@ Singleton { try { const rules = JSON.parse(output); workspaceRules = Array.isArray(rules) ? rules.filter(rule => rule.monitor).map(rule => ({ - "workspaceString": rule.workspaceString, - "monitor": rule.monitor - })) : []; + "workspaceString": rule.workspaceString, + "monitor": rule.monitor + })) : []; } catch (error) { log.warn("workspacerules parse failed:", error); } @@ -105,9 +105,8 @@ Singleton { Connections { target: SettingsData + enabled: CompositorService.isHyprland function onBarConfigsChanged() { - if (!CompositorService.isHyprland) - return; const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)); if (newGaps === root._lastGapValue) return; @@ -123,6 +122,7 @@ Singleton { refreshLuaConfigStatus(); if (luaConfigActive) ensureDmsLuaConfigs(); + generateLayoutConfig(); return; } luaConfigDetected = false; diff --git a/quickshell/Services/IconThemeService.qml b/quickshell/Services/IconThemeService.qml index 83d094578..9050e72f0 100644 --- a/quickshell/Services/IconThemeService.qml +++ b/quickshell/Services/IconThemeService.qml @@ -70,17 +70,25 @@ Singleton { Component.onCompleted: { _initialized = true; Paths.iconResolver = name => resolve(name); - _probeSystemTheme(); + if (!settingsTheme) + _probeSystemTheme(); Qt.callLater(_rebuild); } + Timer { + id: rebuildDebounce + interval: 2000 + repeat: false + onTriggered: root._rebuild() + } + Connections { target: DesktopEntries function onApplicationsChanged() { if (!root.ready) return; - Qt.callLater(root._rebuild); + rebuildDebounce.restart(); } } diff --git a/quickshell/Services/IdleService.qml b/quickshell/Services/IdleService.qml index f211fdd37..d355b75f4 100644 --- a/quickshell/Services/IdleService.qml +++ b/quickshell/Services/IdleService.qml @@ -24,8 +24,6 @@ Singleton { readonly property int postLockMonitorTimeout: isOnBattery ? SettingsData.batteryPostLockMonitorTimeout : SettingsData.acPostLockMonitorTimeout readonly property bool postLockMonitorActive: isShellLocked && postLockMonitorTimeout > 0 - readonly property bool mediaPlaying: MprisController.activePlayer !== null && MprisController.activePlayer.isPlaying - onEnabledChanged: _applyMonitorEnableds() onPostLockMonitorActiveChanged: _applyMonitorEnableds() onMonitorTimeoutChanged: _rearmIdleMonitors() diff --git a/quickshell/Services/KeybindsService.qml b/quickshell/Services/KeybindsService.qml index 647eb20a4..04de3e40b 100644 --- a/quickshell/Services/KeybindsService.qml +++ b/quickshell/Services/KeybindsService.qml @@ -132,10 +132,29 @@ Singleton { } } + property int _consumers: 0 + property bool _bindsStale: false + + function addRef() { + _consumers++; + if (!_bindsStale) + return; + _bindsStale = false; + loadBinds(false); + } + + function removeRef() { + _consumers = Math.max(0, _consumers - 1); + } + Connections { target: NiriService enabled: CompositorService.isNiri function onConfigReloaded() { + if (root._consumers === 0) { + root._bindsStale = true; + return; + } Qt.callLater(root.loadBinds, false); } } diff --git a/quickshell/Services/Log.qml b/quickshell/Services/Log.qml index 7a2fcba41..61688e2d6 100644 --- a/quickshell/Services/Log.qml +++ b/quickshell/Services/Log.qml @@ -177,7 +177,7 @@ Singleton { return; const argList = Array.from(args); - const loc = _callerLocation(); + const loc = (lvl >= Log.Level.Warn || root.level === Log.Level.Debug) ? _callerLocation() : null; const msg = argList.map(_stringify).join(" "); let tag; diff --git a/quickshell/Services/LyricsService.qml b/quickshell/Services/LyricsService.qml index 4f471cd22..977760f1b 100644 --- a/quickshell/Services/LyricsService.qml +++ b/quickshell/Services/LyricsService.qml @@ -14,6 +14,14 @@ Singleton { property int refCount: 0 readonly property bool allowed: DashRegistry.option("media", "lyrics") === true && controller.available readonly property bool subscribed: refCount > 0 && controller.available + onSubscribedChanged: { + MediaAccentService.lyricsConsumers += subscribed ? 1 : -1; + if (subscribed) { + MprisController.addPositionRef(); + return; + } + MprisController.removePositionRef(); + } property var activePlayer: subscribed ? MprisController.activePlayer : null readonly property real stableLength: subscribed ? MprisController.activePlayerStableLength : 0 diff --git a/quickshell/Services/MangoService.qml b/quickshell/Services/MangoService.qml index 400cf625b..e5ff80f4a 100644 --- a/quickshell/Services/MangoService.qml +++ b/quickshell/Services/MangoService.qml @@ -105,7 +105,7 @@ Singleton { DCommon.DSocket { id: monitorsSocket path: root.socketPath - connected: root.available + connected: CompositorService.isMango && root.available onConnectionStateChanged: { if (linkUp) { @@ -122,7 +122,7 @@ Singleton { DCommon.DSocket { id: clientsSocket path: root.socketPath - connected: root.available + connected: CompositorService.isMango && root.available onConnectionStateChanged: { if (linkUp) @@ -139,7 +139,7 @@ Singleton { DCommon.DSocket { id: dispatchSocket path: root.socketPath - connected: root.available + connected: CompositorService.isMango && root.available reconnectBaseMs: 25 onConnectionStateChanged: { @@ -553,9 +553,8 @@ Singleton { Connections { target: SettingsData + enabled: CompositorService.isMango function onBarConfigsChanged() { - if (!CompositorService.isMango) - return; const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)); if (newGaps === root._lastGapValue) return; diff --git a/quickshell/Services/MediaAccentService.qml b/quickshell/Services/MediaAccentService.qml index 1f3f633a4..4233f0d87 100644 --- a/quickshell/Services/MediaAccentService.qml +++ b/quickshell/Services/MediaAccentService.qml @@ -13,21 +13,25 @@ Singleton { readonly property bool hasAccent: MediaOptions.albumArtAccent ? _accent !== null : true readonly property color accent: MediaOptions.albumArtAccent && _accent !== null ? _accent : Theme.primary - readonly property var lyricsHues: _lyricsHues() - readonly property var lyricsAccents: lyricsHues.map(color => _readableLyricColor(color)) - readonly property color lyricsGroupAccent: _readableLyricColor(_mixHues(lyricsHues[0], lyricsHues[1], lyricsHues[2])) + property int lyricsConsumers: 0 + readonly property bool lyricsLive: lyricsConsumers > 0 + readonly property var lyricsHues: lyricsLive ? _lyricsHues() : [Theme.primary, Theme.tertiary, Theme.secondary] + readonly property var lyricsAccents: lyricsLive ? lyricsHues.map(color => _readableLyricColor(color)) : lyricsHues + readonly property color lyricsGroupAccent: lyricsLive ? _readableLyricColor(_mixHues(lyricsHues[0], lyricsHues[1], lyricsHues[2])) : Theme.primary readonly property real lyricsChromaMin: 36 readonly property real lyricsContrast: 4.5 readonly property real lyricsTintMax: 0.8 readonly property color lyricsTint: Theme.withAlpha(Theme.cardSurface, Math.min(Theme.cardSurface.a * Theme.foregroundAlpha, lyricsTintMax)) readonly property var lyricsBackgrounds: { + if (!lyricsLive) + return []; const surface = Theme.withAlpha(Theme.cardSurface, 1); return [surface].concat(TrackArtService.artwork.colors.map(art => Theme.blend(Theme.withAlpha(art, 1), surface, lyricsTint.a))); } readonly property color accentContainer: _container(Theme.primaryContainer, Theme.isLightMode ? 0.3 : 0.55, Theme.isLightMode ? 0.9 : 0.42, 1.25) readonly property color accentSecondaryContainer: _container(Theme.secondaryContainer, Theme.isLightMode ? 0.12 : 0.22, Theme.isLightMode ? 0.94 : 0.3, 1.08) - readonly property color readableAccent: contrastTo(accent, Theme.onSurface, Theme.chipSurface, 4.5) + readonly property color readableAccent: lyricsLive ? contrastTo(accent, Theme.onSurface, Theme.chipSurface, 4.5) : accent property color onAccent property color onAccentContainer diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml index 6273d928f..f7f94812e 100644 --- a/quickshell/Services/MprisController.qml +++ b/quickshell/Services/MprisController.qml @@ -33,6 +33,15 @@ Singleton { property bool _mprisRequestInFlight: false property bool _mprisPublishDirty: false property int _mprisConnectionEpoch: 0 + property int positionConsumers: 0 + + function addPositionRef() { + positionConsumers++; + } + + function removePositionRef() { + positionConsumers = Math.max(0, positionConsumers - 1); + } Connections { target: root.activePlayer @@ -52,14 +61,23 @@ Singleton { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { root.activePlayerStableLength = root.activePlayer.length; } + } + function onPlaybackStateChanged() { + root._syncStableMeta(); + root._checkIdle(); + } + } + + Connections { + target: root.activePlayer + enabled: SettingsData.bluetoothMprisEnabled + function onLengthChanged() { root._scheduleMPRISPublish(); } function onMetadataChanged() { root._scheduleMPRISPublish(); } function onPlaybackStateChanged() { - root._syncStableMeta(); - root._checkIdle(); root._scheduleMPRISPublish(); } function onCanControlChanged() { @@ -378,7 +396,7 @@ Singleton { Timer { interval: 1000 - running: root.activePlayer?.playbackState === MprisPlaybackState.Playing + running: root.positionConsumers > 0 && root.activePlayer?.playbackState === MprisPlaybackState.Playing repeat: true onTriggered: root.activePlayer?.positionChanged() } diff --git a/quickshell/Services/NightModeService.qml b/quickshell/Services/NightModeService.qml index c919213bf..c6f752c6d 100644 --- a/quickshell/Services/NightModeService.qml +++ b/quickshell/Services/NightModeService.qml @@ -17,6 +17,7 @@ Singleton { property bool automationAvailable: false property bool gammaControlAvailable: false property int resumeRecoveryAttempt: 0 + readonly property bool _gammaNeeded: nightModeEnabled || SessionData.displayGamma !== 1.0 || SessionData.displayContrast !== 1.0 property var gammaState: ({}) property int gammaCurrentTemp: gammaState?.currentTemp ?? 0 @@ -343,7 +344,6 @@ Singleton { function runResumeRecoveryPass() { checkGammaControlAvailability(); - BrightnessService.rescanDevices(); if (nightModeEnabled) { evaluateNightMode(); @@ -375,7 +375,8 @@ Singleton { } else { gammaControlAvailable = true; automationAvailable = true; - applyGammaAdjustments(); + if (_gammaNeeded) + applyGammaAdjustments(); if (nightModeEnabled) { _setNightModeEnabled(!nightModePaused, true, success => { @@ -467,6 +468,7 @@ Singleton { Connections { target: SessionService + enabled: root._gammaNeeded function onSessionResumed() { resumeRecoveryAttempt = 0; @@ -541,6 +543,7 @@ Singleton { Connections { target: ToplevelManager + enabled: root.nightModeEnabled function onActiveToplevelChanged() { root.handleNightModeExceptions(); } @@ -548,6 +551,7 @@ Singleton { Connections { target: ToplevelManager.activeToplevel + enabled: root.nightModeEnabled function onFullscreenChanged() { root.handleNightModeExceptions(); } @@ -555,6 +559,7 @@ Singleton { Connections { target: CompositorService + enabled: root.nightModeEnabled function onInOverviewChanged() { root.handleNightModeExceptions(); } diff --git a/quickshell/Services/NiriOutputCycle.qml b/quickshell/Services/NiriOutputCycle.qml index bc152c36b..cddcee9eb 100644 --- a/quickshell/Services/NiriOutputCycle.qml +++ b/quickshell/Services/NiriOutputCycle.qml @@ -12,9 +12,11 @@ QtObject { property var _outputCycleState: OutputCycleState.emptyState() Component.onCompleted: resolveOutputCycle() + onIsNiriChanged: resolveOutputCycle() readonly property Connections outputConnection: Connections { target: root.wlrOutputService + enabled: root.isNiri function onStateChanged() { root.resolveOutputCycle(); @@ -23,6 +25,7 @@ QtObject { readonly property Connections socketConnection: Connections { target: root.socket + enabled: root.isNiri function onConnectionStateChanged() { if (root.socket.linkUp) @@ -31,9 +34,10 @@ QtObject { } function outputDescriptors() { - return wlrOutputService.outputs - .filter(output => output.name) - .map(output => ({ id: output.name, enabled: output.enabled })); + return wlrOutputService.outputs.filter(output => output.name).map(output => ({ + id: output.name, + enabled: output.enabled + })); } function sendOutputAction(outputName, action) { diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index c3dd58990..37336f172 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -92,17 +92,14 @@ Singleton { Connections { target: Theme + enabled: CompositorService.isNiri function onScreenTransitionNeeded() { - if (CompositorService.isNiri) { - root.doScreenTransition(); - } + root.doScreenTransition(); } function onThemeGenerationStarting() { - if (CompositorService.isNiri) { - root.suppressNextToast(); - } + root.suppressNextToast(); } } @@ -133,6 +130,7 @@ Singleton { Connections { target: SettingsData + enabled: CompositorService.isNiri function onBarConfigsChanged() { const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)); if (newGaps === root._lastGapValue) @@ -145,9 +143,10 @@ Singleton { Connections { target: CompositorService function onIsNiriChanged() { - if (CompositorService.isNiri) { - generateNiriInputConfig(); - } + if (!CompositorService.isNiri) + return; + generateNiriInputConfig(); + generateNiriLayoutConfig(); } } @@ -279,12 +278,15 @@ Singleton { connected: CompositorService.isNiri } - NiriOutputCycle { - id: outputCycle - wlrOutputService: WlrOutputService - socket: requestSocket - isNiri: CompositorService.isNiri - currentOutput: root.currentOutput + Loader { + id: outputCycleLoader + active: CompositorService.isNiri + sourceComponent: NiriOutputCycle { + wlrOutputService: WlrOutputService + socket: requestSocket + isNiri: CompositorService.isNiri + currentOutput: root.currentOutput + } } function fetchOutputs() { @@ -310,7 +312,9 @@ Singleton { } function cycleSingleOutput() { - return outputCycle.cycleSingleOutput(); + if (!outputCycleLoader.item) + return "OUTPUT_CYCLE_UNSUPPORTED"; + return outputCycleLoader.item.cycleSingleOutput(); } function updateDisplayScales() { @@ -1304,13 +1308,20 @@ window-rule { writeAlttabProcess.running = true; } - for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout", "input"]) { - const path = niriDmsDir + "/" + name + ".kdl"; - Proc.runCommand("niri-ensure-" + name, ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${path}" ] && touch "${path}" || true`], (output, exitCode) => { - if (exitCode !== 0) - log.warn("Failed to ensure " + name + ".kdl, exit code:", exitCode); - }); - } + ensureDmsConfigFiles(niriDmsDir); + } + + property bool _dmsConfigFilesEnsured: false + + function ensureDmsConfigFiles(niriDmsDir) { + if (_dmsConfigFilesEnsured) + return; + _dmsConfigFilesEnsured = true; + const script = `mkdir -p "${niriDmsDir}" && for f in outputs binds cursor windowrules colors alttab layout input; do [ -f "${niriDmsDir}/$f.kdl" ] || touch "${niriDmsDir}/$f.kdl"; done`; + Proc.runCommand("niri-ensure-dms-configs", ["sh", "-c", script], (output, exitCode) => { + if (exitCode !== 0) + log.warn("Failed to ensure dms config files, exit code:", exitCode); + }); } function generateNiriBlurrule() { diff --git a/quickshell/Services/NotepadStorageService.qml b/quickshell/Services/NotepadStorageService.qml index 9b1046170..e2aad48f5 100644 --- a/quickshell/Services/NotepadStorageService.qml +++ b/quickshell/Services/NotepadStorageService.qml @@ -71,10 +71,6 @@ Singleton { conflictDiskContent = ""; } - Component.onCompleted: { - ensureDirectories(); - } - FileView { id: metadataFile path: root.refCount > 0 ? root.metadataPath : "" @@ -100,13 +96,21 @@ Singleton { } onRefCountChanged: { - if (refCount === 1 && !metadataLoaded) { - metadataFile.path = ""; - metadataFile.path = root.metadataPath; - } + if (refCount !== 1) + return; + ensureDirectories(); + if (metadataLoaded) + return; + metadataFile.path = ""; + metadataFile.path = root.metadataPath; } + property bool _directoriesEnsured: false + function ensureDirectories() { + if (_directoriesEnsured) + return; + _directoriesEnsured = true; Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null); } diff --git a/quickshell/Services/NotificationService.qml b/quickshell/Services/NotificationService.qml index cb9971d34..d888df85f 100644 --- a/quickshell/Services/NotificationService.qml +++ b/quickshell/Services/NotificationService.qml @@ -70,13 +70,12 @@ Singleton { Component.onCompleted: { _recomputeGroups(); - Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]); - Quickshell.execDetached(["mkdir", "-p", imageCacheDir]); + Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache), imageCacheDir]); } FileView { id: historyFileView - path: root.historyFile + path: SettingsData.notificationHistoryEnabled ? root.historyFile : "" printErrors: false onLoaded: root.loadHistory() onLoadFailed: error => { @@ -173,6 +172,8 @@ Singleton { } function performSaveHistory() { + if (historyFileView.path === "") + return; try { historyAdapter.notifications = historyList; historyFileView.writeAdapter(); @@ -559,10 +560,17 @@ Singleton { } historyList = []; historyAdapter.notifications = []; + if (historyFileView.path === "") { + Quickshell.execDetached(["rm", "-f", root.historyFile]); + return; + } historyFileView.writeAdapter(); } + property bool overlayOpen: false + function onOverlayOpen() { + overlayOpen = true; popupsDisabled = true; markNotificationsSeen(); addGate.stop(); @@ -580,6 +588,7 @@ Singleton { } function onOverlayClose() { + overlayOpen = false; popupsDisabled = false; markNotificationsSeen(); processQueue(); @@ -600,7 +609,7 @@ Singleton { id: timeUpdateTimer interval: 30000 repeat: true - running: root.allWrappers.length > 0 || visibleNotifications.length > 0 + running: root.overlayOpen && (root.allWrappers.length > 0 || visibleNotifications.length > 0) triggeredOnStart: false onTriggered: { root.timeUpdateTick = !root.timeUpdateTick; @@ -1495,7 +1504,7 @@ Singleton { } Connections { - target: PrivacyService + target: SettingsData.notificationDndWhileScreenSharing ? PrivacyService : null function onScreensharingActiveChanged() { SessionData.syncScreenShareDnd(); } diff --git a/quickshell/Services/PortalService.qml b/quickshell/Services/PortalService.qml index 3e4798327..09ecc9b3f 100644 --- a/quickshell/Services/PortalService.qml +++ b/quickshell/Services/PortalService.qml @@ -144,10 +144,6 @@ Singleton { } function setSystemColorScheme(isLightMode) { - if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal === false) { - return; - } - const preferLight = isLightMode && systemColorScheme === 2; const targetScheme = isLightMode ? (preferLight ? "prefer-light" : "default") : "prefer-dark"; @@ -209,6 +205,7 @@ Singleton { Connections { target: Theme + enabled: SettingsData.syncModeWithPortal function onIsLightModeChanged() { root.setSystemColorScheme(Theme.isLightMode); diff --git a/quickshell/Services/PowerProfileWatcher.qml b/quickshell/Services/PowerProfileWatcher.qml index 58b786cd7..6fa80ec76 100644 --- a/quickshell/Services/PowerProfileWatcher.qml +++ b/quickshell/Services/PowerProfileWatcher.qml @@ -13,7 +13,7 @@ Singleton { property int previousProfile: -1 property bool daemonRunning: true - readonly property bool available: typeof PowerProfiles !== "undefined" && daemonRunning + readonly property bool available: daemonRunning function checkDaemon() { if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus")) @@ -87,17 +87,14 @@ Singleton { } Connections { - target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null + target: PowerProfiles function onProfileChanged() { - if (typeof PowerProfiles !== "undefined") { - root.daemonRunning = true; - root.previousProfile = root.currentProfile; - root.currentProfile = PowerProfiles.profile; - if (root.previousProfile !== -1) { - root.profileChanged(root.currentProfile); - } - } + root.daemonRunning = true; + root.previousProfile = root.currentProfile; + root.currentProfile = PowerProfiles.profile; + if (root.previousProfile !== -1) + root.profileChanged(root.currentProfile); } } @@ -110,9 +107,7 @@ Singleton { } Component.onCompleted: { - if (typeof PowerProfiles !== "undefined") { - root.currentProfile = PowerProfiles.profile; - root.previousProfile = PowerProfiles.profile; - } + root.currentProfile = PowerProfiles.profile; + root.previousProfile = PowerProfiles.profile; } } diff --git a/quickshell/Services/PrivacyService.qml b/quickshell/Services/PrivacyService.qml index 4a9a74cd4..a29d1643b 100644 --- a/quickshell/Services/PrivacyService.qml +++ b/quickshell/Services/PrivacyService.qml @@ -1,182 +1,195 @@ pragma Singleton - pragma ComponentBehavior: Bound import QtQuick import Quickshell -import Quickshell.Io +import qs.Common import qs.Services Singleton { id: root + property int refCount: 0 + readonly property bool watching: refCount > 0 || SettingsData.notificationDndWhileScreenSharing + + function addRef() { + refCount++; + } + + function removeRef() { + refCount = Math.max(0, refCount - 1); + } + readonly property bool microphoneActive: { - if (!AudioService.pipewireReady) { - return false + if (!watching || !AudioService.pipewireReady) { + return false; } for (let i = 0; i < AudioService.pipewireNodes.length; i++) { - const node = AudioService.pipewireNodes[i] + const node = AudioService.pipewireNodes[i]; if (!node) { - continue + continue; } if (node.properties?.["media.class"] === "Stream/Input/Audio") { if (!looksLikeSystemVirtualMic(node)) { if (node.audio && node.audio.muted) { - return false + return false; } - return true + return true; } } } - return false + return false; } - readonly property bool cameraActive: { - if (!AudioService.pipewireReady) { - return false + if (!watching || !AudioService.pipewireReady) { + return false; } for (let i = 0; i < AudioService.pipewireNodes.length; i++) { - const node = AudioService.pipewireNodes[i] + const node = AudioService.pipewireNodes[i]; if (!node || !node.ready || node.properties?.["media.role"] === "Screen") { - continue + continue; } if (node.properties && node.properties["media.class"] === "Stream/Input/Video") { if (node.properties["stream.is-live"] === "true") { - return true + return true; } } } - return false + return false; } readonly property bool screensharingActive: { + if (!watching) { + return false; + } + if (CompositorService.isNiri && NiriService.hasActiveCast) { - return true + return true; } if (!AudioService.pipewireReady) { - return false + return false; } for (let i = 0; i < AudioService.pipewireNodes.length; i++) { - const node = AudioService.pipewireNodes[i] + const node = AudioService.pipewireNodes[i]; if (!node || !node.ready) { - continue + continue; } - if (AudioService.isPipewireVideoSource(node)) { - if (looksLikeScreencast(node)) { - return true - } - } + if (AudioService.isPipewireVideoSource(node)) { + if (looksLikeScreencast(node)) { + return true; + } + } if (node.properties && node.properties["media.class"] === "Stream/Output/Video") { if (looksLikeScreencast(node)) { - return true + return true; } } if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") { - const mediaName = (node.properties["media.name"] || "").toLowerCase() - const appName = (node.properties["application.name"] || "").toLowerCase() + const mediaName = (node.properties["media.name"] || "").toLowerCase(); + const appName = (node.properties["application.name"] || "").toLowerCase(); if (mediaName.includes("desktop") || appName.includes("screen") || appName === "obs") { if (node.properties["stream.is-live"] === "true") { if (node.audio && node.audio.muted) { - return false + return false; } - return true + return true; } } } } - return false + return false; } readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive function looksLikeSystemVirtualMic(node) { if (!node) { - return false + return false; } - const name = (node.name || "").toLowerCase() - const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase() - const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() - const combined = name + " " + mediaName + " " + appName - return /cava|monitor|system/.test(combined) + const name = (node.name || "").toLowerCase(); + const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase(); + const appName = (node.properties && node.properties["application.name"] || "").toLowerCase(); + const combined = name + " " + mediaName + " " + appName; + return /cava|monitor|system/.test(combined); } function looksLikeScreencast(node) { if (!node) { - return false + return false; } - const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() - const nodeName = (node.name || "").toLowerCase() - const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase() - const combined = appName + " " + nodeName + " " + mediaName - return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined) + const appName = (node.properties && node.properties["application.name"] || "").toLowerCase(); + const nodeName = (node.name || "").toLowerCase(); + const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase(); + const combined = appName + " " + nodeName + " " + mediaName; + return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined); } function screencastSourceIds() { - const ids = [] + const ids = []; if (CompositorService.isNiri) { for (const cast of NiriService.casts) { if (cast && cast.is_active) { - ids.push("niri:" + cast.stream_id) + ids.push("niri:" + cast.stream_id); } } } if (!AudioService.pipewireReady) { - return ids + return ids; } for (let i = 0; i < AudioService.pipewireNodes.length; i++) { - const node = AudioService.pipewireNodes[i] + const node = AudioService.pipewireNodes[i]; if (!node || !node.ready) { - continue + continue; } - const isVideoSource = node.properties?.["media.class"] === "Video/Source" - const isVideoStream = node.properties && node.properties["media.class"] === "Stream/Output/Video" + const isVideoSource = node.properties?.["media.class"] === "Video/Source"; + const isVideoStream = node.properties && node.properties["media.class"] === "Stream/Output/Video"; if ((isVideoSource || isVideoStream) && looksLikeScreencast(node)) { - ids.push("pw:" + node.id) + ids.push("pw:" + node.id); } } - return ids.sort() + return ids.sort(); } function getMicrophoneStatus() { - return microphoneActive ? "active" : "inactive" + return microphoneActive ? "active" : "inactive"; } function getCameraStatus() { - return cameraActive ? "active" : "inactive" + return cameraActive ? "active" : "inactive"; } function getScreensharingStatus() { - return screensharingActive ? "active" : "inactive" + return screensharingActive ? "active" : "inactive"; } function getPrivacySummary() { - const active = [] + const active = []; if (microphoneActive) { - active.push("microphone") + active.push("microphone"); } if (cameraActive) { - active.push("camera") + active.push("camera"); } if (screensharingActive) { - active.push("screensharing") + active.push("screensharing"); } - return active.length > 0 ? `Privacy active: ${active.join(", ")}` : "No privacy concerns detected" + return active.length > 0 ? `Privacy active: ${active.join(", ")}` : "No privacy concerns detected"; } } diff --git a/quickshell/Services/RefreshRateService.qml b/quickshell/Services/RefreshRateService.qml index 39e89e726..ea64575cf 100644 --- a/quickshell/Services/RefreshRateService.qml +++ b/quickshell/Services/RefreshRateService.qml @@ -14,8 +14,8 @@ Singleton { readonly property int batteryRefreshRateTarget: 60000 readonly property int batteryRefreshRateTolerance: 1000 - property int powerSyncRecoveryAttempt: 0 - readonly property var powerSyncRecoveryIntervals: [1000, 5000, 15000, 30000] + property int _resumeRecoveryAttempt: 0 + readonly property var _resumeRecoveryIntervals: [1000, 5000, 15000, 30000] property var _lastAppliedTargets: ({}) Timer { @@ -38,38 +38,40 @@ Singleton { id: startupRefreshRateSync interval: 500 repeat: false - running: true + running: SettingsData.lowerDisplayRefreshRateOnBattery onTriggered: root.requestSync("startup") } Timer { - id: powerSyncRecoveryTimer - interval: root.powerSyncRecoveryIntervals[0] + id: resumeRecoveryTimer + interval: root._resumeRecoveryIntervals[0] repeat: false onTriggered: { root.requestSync("resume-reconcile"); - root.powerSyncRecoveryAttempt++; - if (root.powerSyncRecoveryAttempt < root.powerSyncRecoveryIntervals.length) { - interval = root.powerSyncRecoveryIntervals[root.powerSyncRecoveryAttempt]; + root._resumeRecoveryAttempt++; + if (root._resumeRecoveryAttempt < root._resumeRecoveryIntervals.length) { + interval = root._resumeRecoveryIntervals[root._resumeRecoveryAttempt]; restart(); return; } - root.powerSyncRecoveryAttempt = 0; - interval = root.powerSyncRecoveryIntervals[0]; + root._resumeRecoveryAttempt = 0; + interval = root._resumeRecoveryIntervals[0]; } } Connections { target: SessionService + enabled: SettingsData.lowerDisplayRefreshRateOnBattery function onSessionResumed() { - root.powerSyncRecoveryAttempt = 0; - powerSyncRecoveryTimer.interval = root.powerSyncRecoveryIntervals[0]; - powerSyncRecoveryTimer.restart(); + root._resumeRecoveryAttempt = 0; + resumeRecoveryTimer.interval = root._resumeRecoveryIntervals[0]; + resumeRecoveryTimer.restart(); } } Connections { target: BatteryService + enabled: SettingsData.lowerDisplayRefreshRateOnBattery function onIsPluggedInChanged() { root.requestSync("power-change"); } @@ -78,12 +80,14 @@ Singleton { Connections { target: SettingsData function onLowerDisplayRefreshRateOnBatteryChanged() { + resumeRecoveryTimer.stop(); root.requestSync("setting-change"); } } Connections { target: SessionData + enabled: SettingsData.lowerDisplayRefreshRateOnBattery function onActiveDisplayProfileChanged() { root.requestSync("profile-change"); } @@ -95,6 +99,7 @@ Singleton { Connections { target: NiriService + enabled: SettingsData.lowerDisplayRefreshRateOnBattery function onOutputsChanged() { root.requestSync("output-change"); } @@ -102,6 +107,7 @@ Singleton { Connections { target: WlrOutputService + enabled: SettingsData.lowerDisplayRefreshRateOnBattery function onStateChanged() { root.requestSync("output-change"); } diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index ab5619b99..1697630dd 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -64,12 +64,7 @@ Singleton { running: true repeat: false onTriggered: { - detectUwsmProcess.running = true; - detectElogindProcess.running = true; - detectLoginctlProcess.running = true; - detectSystemctlProcess.running = true; - detectHibernateProcess.running = true; - detectPrimeRunProcess.running = true; + detectEnvironmentProcess.running = true; if (!SettingsData.loginctlLockIntegration) { log.debug("loginctl lock integration disabled by user"); return; @@ -91,54 +86,49 @@ Singleton { } Process { - id: detectUwsmProcess + id: detectEnvironmentProcess running: false - command: ["sh", "-c", "command -v uwsm > /dev/null 2>&1 && systemctl --user is-active --quiet 'wayland-wm@*.service' 2> /dev/null"] + command: ["sh", "-c", `uwsm=0; command -v uwsm >/dev/null 2>&1 && systemctl --user is-active --quiet 'wayland-wm@*.service' 2>/dev/null && uwsm=1 +elogind=0; ps -eo comm= | grep -qE '^(elogind|elogind-daemon)$' && elogind=1 +loginctl=0; command -v loginctl >/dev/null 2>&1 && loginctl=1 +systemctl=0; usermgr=0 +if command -v systemctl >/dev/null 2>&1; then systemctl=1; systemctl --user --no-pager show-environment >/dev/null 2>&1 && usermgr=1; fi +hibernate=0; grep -q disk /sys/power/state 2>/dev/null && hibernate=1 +nvidia=; command -v prime-run >/dev/null 2>&1 && nvidia=prime-run +[ -z "$nvidia" ] && command -v nvidia-offload >/dev/null 2>&1 && nvidia=nvidia-offload +printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s\nnvidia=%s\n' "$uwsm" "$elogind" "$loginctl" "$systemctl" "$usermgr" "$hibernate" "$nvidia"`] - onExited: function (exitCode) { - hasUwsm = (exitCode === 0); - } - } - - Process { - id: detectElogindProcess - running: false - command: ["sh", "-c", "ps -eo comm= | grep -E '^(elogind|elogind-daemon)$'"] - - onExited: function (exitCode) { - log.debug("Elogind detection exited with code", exitCode); - isElogind = (exitCode === 0); - } - } - - Process { - id: detectLoginctlProcess - running: false - command: ["sh", "-c", "command -v loginctl"] - - onExited: function (exitCode) { - loginctlCommandAvailable = (exitCode === 0); - } - } - - Process { - id: detectSystemctlProcess - running: false - command: ["sh", "-c", "command -v systemctl > /dev/null || exit 1; systemctl --user show-environment > /dev/null 2>&1 || exit 2"] - - onExited: function (exitCode) { - systemctlCommandAvailable = (exitCode === 0 || exitCode === 2); - userManagerAvailable = (exitCode === 0); - } - } - - Process { - id: detectHibernateProcess - running: false - command: isBSD ? ["sh", "-c", "exit 1"] : ["grep", "-q", "disk", "/sys/power/state"] - - onExited: function (exitCode) { - hibernateSupported = (exitCode === 0); + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + const eq = line.indexOf("="); + if (eq <= 0) + return; + const value = line.substring(eq + 1).trim(); + switch (line.substring(0, eq)) { + case "uwsm": + root.hasUwsm = value === "1"; + return; + case "elogind": + root.isElogind = value === "1"; + return; + case "loginctl": + root.loginctlCommandAvailable = value === "1"; + return; + case "systemctl": + root.systemctlCommandAvailable = value === "1"; + return; + case "usermgr": + root.userManagerAvailable = value === "1"; + return; + case "hibernate": + root.hibernateSupported = !root.isBSD && value === "1"; + return; + case "nvidia": + root.nvidiaCommand = value; + return; + } + } } } @@ -163,32 +153,6 @@ Singleton { } } - Process { - id: detectPrimeRunProcess - running: false - command: ["sh", "-c", "command -v prime-run"] - - onExited: function (exitCode) { - if (exitCode === 0) { - nvidiaCommand = "prime-run"; - } else { - detectNvidiaOffloadProcess.running = true; - } - } - } - - Process { - id: detectNvidiaOffloadProcess - running: false - command: ["sh", "-c", "command -v nvidia-offload"] - - onExited: function (exitCode) { - if (exitCode === 0) { - nvidiaCommand = "nvidia-offload"; - } - } - } - Process { id: uwsmLogout property bool notRunning: false diff --git a/quickshell/Services/SessionsService.qml b/quickshell/Services/SessionsService.qml index 1f02af16d..a70cde644 100644 --- a/quickshell/Services/SessionsService.qml +++ b/quickshell/Services/SessionsService.qml @@ -14,6 +14,8 @@ Singleton { property string currentSessionId: "" property string currentSeat: "" property bool refreshing: false + property bool loaded: false + property var _afterLoad: [] property bool loginctlAvailable: false property bool isBSD: Qt.platform.os === "unix" @@ -45,6 +47,24 @@ Singleton { return sessions.filter(s => !s.current); } + function ensureLoaded(fn) { + if (loaded) { + fn(); + return; + } + _afterLoad = _afterLoad.concat([fn]); + refresh(); + } + + function _finishRefresh() { + refreshing = false; + loaded = true; + const queued = _afterLoad; + _afterLoad = []; + for (const fn of queued) + fn(); + } + function refresh() { if (refreshing) return; @@ -72,20 +92,22 @@ Singleton { const fields = (lines[0] || "self|0|").split("|"); root.currentSessionId = fields[0] || "self"; root.currentSeat = ""; - root.sessions = [{ - sessionId: root.currentSessionId, - uid: parseInt(fields[1] || "0", 10), - username: fields[2] || "", - seat: "", - tty: lines[1] || "", - type: (Quickshell.env("WAYLAND_DISPLAY") || "") !== "" ? "wayland" : "", - sessionClass: "user", - active: true, - state: "active", - remote: false, - current: true - }]; - root.refreshing = false; + root.sessions = [ + { + sessionId: root.currentSessionId, + uid: parseInt(fields[1] || "0", 10), + username: fields[2] || "", + seat: "", + tty: lines[1] || "", + type: (Quickshell.env("WAYLAND_DISPLAY") || "") !== "" ? "wayland" : "", + sessionClass: "user", + active: true, + state: "active", + remote: false, + current: true + } + ]; + root._finishRefresh(); }, 0); } @@ -136,7 +158,7 @@ Singleton { return parseInt(a.sessionId, 10) - parseInt(b.sessionId, 10); }); root.sessions = list; - root.refreshing = false; + root._finishRefresh(); }, 0); } @@ -239,6 +261,8 @@ Singleton { target: "sessions" function list(): string { + if (!root.loaded) + root.refresh(); const lines = []; for (let i = 0; i < root.sessions.length; i++) { const s = root.sessions[i]; @@ -261,17 +285,15 @@ Singleton { function activate(sessionId: string): string { if (!sessionId) return "ERROR: missing session id"; - root.activate(sessionId, null); + root.ensureLoaded(() => root.activate(sessionId, null)); return "ok"; } function switchTo(target: string): string { if (!target) return "ERROR: missing target (username or session id)"; - root.switchToUser(target, null); + root.ensureLoaded(() => root.switchToUser(target, null)); return "ok"; } } - - Component.onCompleted: refresh() } diff --git a/quickshell/Services/SettingsSearchService.qml b/quickshell/Services/SettingsSearchService.qml index be742d3d6..87ab3b852 100644 --- a/quickshell/Services/SettingsSearchService.qml +++ b/quickshell/Services/SettingsSearchService.qml @@ -20,6 +20,7 @@ Singleton { property var settingsIndex: [] property bool indexLoaded: false property var _translatedCache: [] + property bool _cacheDirty: false property int _scrollPass: 0 property int _stablePasses: 0 property real _appliedY: 0 @@ -339,6 +340,7 @@ Singleton { } function _rebuildTranslationCache() { + _cacheDirty = false; var cache = []; var items = settingsIndex.concat(_runtimeSearchEntries()); for (var i = 0; i < items.length; i++) { @@ -543,7 +545,7 @@ Singleton { function _refreshTranslatedCache() { if (!indexLoaded) return; - _rebuildTranslationCache(); + _cacheDirty = true; if (query) results = _searchEntries(query, 15); } @@ -553,6 +555,8 @@ Singleton { if (!queryLower) return []; + if (_cacheDirty) + _rebuildTranslationCache(); var querySquash = _squash(queryLower); var queryWords = queryLower.split(/\s+/).filter(w => w.length > 0); var scored = []; diff --git a/quickshell/Services/ShellVersionService.qml b/quickshell/Services/ShellVersionService.qml index dbc2bbe5d..27c63beba 100644 --- a/quickshell/Services/ShellVersionService.qml +++ b/quickshell/Services/ShellVersionService.qml @@ -19,30 +19,18 @@ Singleton { Process { id: versionDetection running: true - command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -d .git ]; then echo "(git) $(git rev-parse --short HEAD)"; elif [ -f VERSION ]; then cat VERSION; fi`] + command: ["sh", "-c", `cd "${Quickshell.shellDir}" || exit 0 +semver=$(cat VERSION 2>/dev/null); codename=$(cat CODENAME 2>/dev/null) +if [ -d .git ]; then shell="(git) $(git rev-parse --short HEAD)"; else shell=$semver; fi +printf '%s\\n%s\\n%s\\n' "$shell" "$semver" "$codename"`] stdout: StdioCollector { - onStreamFinished: shellVersion = text.trim() - } - } - - Process { - id: semverDetection - running: true - command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f VERSION ]; then cat VERSION; fi`] - - stdout: StdioCollector { - onStreamFinished: semverVersion = text.trim() - } - } - - Process { - id: codenameDetection - running: true - command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f CODENAME ]; then cat CODENAME; fi`] - - stdout: StdioCollector { - onStreamFinished: shellCodename = text.trim() + onStreamFinished: { + const lines = text.split("\n"); + shellVersion = (lines[0] || "").trim(); + semverVersion = (lines[1] || "").trim(); + shellCodename = (lines[2] || "").trim(); + } } } diff --git a/quickshell/Services/SystemUpdateService.qml b/quickshell/Services/SystemUpdateService.qml index 98b0bf19a..d85b90582 100644 --- a/quickshell/Services/SystemUpdateService.qml +++ b/quickshell/Services/SystemUpdateService.qml @@ -148,9 +148,11 @@ Singleton { const has = DMSService.capabilities.includes("sysupdate"); if (has && !sysupdateAvailable) { sysupdateAvailable = true; - requestState(); - // The daemon persists its last check but not the interval; re-apply it on every fresh connection. - setInterval(SettingsData.updaterIntervalSeconds); + if (pollWanted) { + requestState(); + // The daemon persists its last check but not the interval; re-apply it on every fresh connection. + setInterval(SettingsData.updaterIntervalSeconds); + } } else if (!has) { sysupdateAvailable = false; } @@ -400,6 +402,7 @@ Singleton { onPollWantedChanged: { if (!pollWanted) _startupCheckDone = false; + _syncSubscription(); Qt.callLater(() => root._syncAcquire()); Qt.callLater(() => root._maybeStartupCheck()); } @@ -415,6 +418,19 @@ Singleton { loadReleases(false); } + function _syncSubscription() { + if (!pollWanted) { + if (DMSService.activeSubscriptions.includes("sysupdate")) + DMSService.removeSubscription("sysupdate"); + return; + } + DMSService.addSubscription("sysupdate"); + if (!sysupdateAvailable) + return; + requestState(); + setInterval(SettingsData.updaterIntervalSeconds); + } + property bool _acquired: false // Releasing the ref parks the daemon scheduler; its deadline is kept, so checks resume on AC. readonly property bool pausedOnBattery: SettingsData.updaterPauseOnBattery && BatteryService.batteryAvailable && !BatteryService.isPluggedIn diff --git a/quickshell/Services/TailscaleService.qml b/quickshell/Services/TailscaleService.qml index 508f9df00..d8a6f9b6a 100644 --- a/quickshell/Services/TailscaleService.qml +++ b/quickshell/Services/TailscaleService.qml @@ -127,7 +127,7 @@ Singleton { if (!available) return; - if (!stateInitialized) { + if (refCount > 0 && !stateInitialized) { stateInitialized = true; getStatus(); } diff --git a/quickshell/Services/ThemeAutoService.qml b/quickshell/Services/ThemeAutoService.qml index a09dd43fa..e7c40c2a4 100644 --- a/quickshell/Services/ThemeAutoService.qml +++ b/quickshell/Services/ThemeAutoService.qml @@ -11,6 +11,16 @@ Singleton { readonly property var log: Log.scoped("ThemeAutoService") property bool active: false + readonly property bool backendAvailable: DMSService.isConnected && DMSService.capabilities.includes("theme.auto") + + onActiveChanged: { + if (active) { + DMSService.addSubscription("theme.auto"); + return; + } + if (DMSService.activeSubscriptions.includes("theme.auto")) + DMSService.removeSubscription("theme.auto"); + } Component.onCompleted: { if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) { @@ -20,7 +30,6 @@ Singleton { Connections { target: SessionData - enabled: typeof SessionData !== "undefined" function onThemeModeAutoEnabledChanged() { if (SessionData.themeModeAutoEnabled) { @@ -29,128 +38,112 @@ Singleton { root.stop(); } } + } + + Connections { + target: SessionData + enabled: root.active function onThemeModeAutoModeChanged() { - if (root.active) { - root.evaluate(); - root.syncTimeSchedule(); - root.syncLocationSchedule(); - } - } - - function onThemeModeStartHourChanged() { - if (root.active && !SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } - } - - function onThemeModeStartMinuteChanged() { - if (root.active && !SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } - } - - function onThemeModeEndHourChanged() { - if (root.active && !SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } - } - - function onThemeModeEndMinuteChanged() { - if (root.active && !SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } + root.evaluate(); + root.syncTimeSchedule(); + root.syncLocationSchedule(); } function onThemeModeShareGammaSettingsChanged() { - if (root.active) { - root.evaluate(); - root.syncTimeSchedule(); - root.syncLocationSchedule(); - } + root.evaluate(); + root.syncTimeSchedule(); + root.syncLocationSchedule(); + } + + function onThemeModeStartHourChanged() { + root.resyncOwnSchedule(); + } + + function onThemeModeStartMinuteChanged() { + root.resyncOwnSchedule(); + } + + function onThemeModeEndHourChanged() { + root.resyncOwnSchedule(); + } + + function onThemeModeEndMinuteChanged() { + root.resyncOwnSchedule(); } function onNightModeStartHourChanged() { - if (root.active && SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } + root.resyncSharedSchedule(); } function onNightModeStartMinuteChanged() { - if (root.active && SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } + root.resyncSharedSchedule(); } function onNightModeEndHourChanged() { - if (root.active && SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } + root.resyncSharedSchedule(); } function onNightModeEndMinuteChanged() { - if (root.active && SessionData.themeModeShareGammaSettings) { - root.evaluate(); - root.syncTimeSchedule(); - } + root.resyncSharedSchedule(); } function onLatitudeChanged() { - if (root.active && SessionData.themeModeAutoMode === "location") { - if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") { - DMSService.sendRequest("wayland.gamma.setLocation", { - "latitude": SessionData.latitude, - "longitude": SessionData.longitude - }); - } - root.evaluate(); - root.syncLocationSchedule(); - } + root.resyncCoordinates(); } function onLongitudeChanged() { - if (root.active && SessionData.themeModeAutoMode === "location") { - if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") { + root.resyncCoordinates(); + } + + function onNightModeUseIPLocationChanged() { + if (SessionData.themeModeAutoMode !== "location") + return; + DMSService.sendRequest("wayland.gamma.setUseIPLocation", { + "use": SessionData.nightModeUseIPLocation + }, response => { + if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }); } - root.evaluate(); - root.syncLocationSchedule(); - } + }); + root.evaluate(); + root.syncLocationSchedule(); } + } - function onNightModeUseIPLocationChanged() { - if (root.active && SessionData.themeModeAutoMode === "location") { - if (typeof DMSService !== "undefined") { - DMSService.sendRequest("wayland.gamma.setUseIPLocation", { - "use": SessionData.nightModeUseIPLocation - }, response => { - if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { - DMSService.sendRequest("wayland.gamma.setLocation", { - "latitude": SessionData.latitude, - "longitude": SessionData.longitude - }); - } - }); - } - root.evaluate(); - root.syncLocationSchedule(); - } + function resyncOwnSchedule() { + if (SessionData.themeModeShareGammaSettings) + return; + evaluate(); + syncTimeSchedule(); + } + + function resyncSharedSchedule() { + if (!SessionData.themeModeShareGammaSettings) + return; + evaluate(); + syncTimeSchedule(); + } + + function resyncCoordinates() { + if (SessionData.themeModeAutoMode !== "location") + return; + if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { + DMSService.sendRequest("wayland.gamma.setLocation", { + "latitude": SessionData.latitude, + "longitude": SessionData.longitude + }); } + evaluate(); + syncLocationSchedule(); } Connections { target: NightModeService - enabled: typeof NightModeService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !root.backendAvailable() + enabled: typeof NightModeService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !root.backendAvailable function onGammaIsDayChanged() { if (Theme.isLightMode !== NightModeService.gammaIsDay) { @@ -178,7 +171,7 @@ Singleton { root.syncLocationSchedule(); } - if (root.backendAvailable() && SessionData.themeModeAutoEnabled) { + if (root.backendAvailable && SessionData.themeModeAutoEnabled) { DMSService.sendRequest("theme.auto.getState", null, response => { if (response && response.result) { root.applyBackendState(response.result); @@ -235,17 +228,13 @@ Singleton { } function refresh() { - if (!backendAvailable()) { + if (!backendAvailable) { evaluate(); return; } DMSService.sendRequest("theme.auto.trigger", {}); } - function backendAvailable() { - return typeof DMSService !== "undefined" && DMSService.isConnected && Array.isArray(DMSService.capabilities) && DMSService.capabilities.includes("theme.auto"); - } - function applyBackendState(state) { if (!state) { return; @@ -349,7 +338,7 @@ Singleton { return; } - if (backendAvailable()) { + if (backendAvailable) { DMSService.sendRequest("theme.auto.getState", null, response => { if (response && response.result) { applyBackendState(response.result); diff --git a/quickshell/Services/TrackArtService.qml b/quickshell/Services/TrackArtService.qml index 002544e3c..724b1274f 100644 --- a/quickshell/Services/TrackArtService.qml +++ b/quickshell/Services/TrackArtService.qml @@ -291,22 +291,25 @@ Singleton { } } - Instantiator { - model: MprisController.availablePlayers - delegate: QtObject { - required property MprisPlayer modelData - readonly property bool playerIsPlaying: modelData.isPlaying - readonly property string playerTrackTitle: modelData.trackTitle - readonly property string playerTrackArtist: modelData.trackArtist - readonly property string playerTrackAlbum: modelData.trackAlbum - readonly property string playerTrackArtUrl: modelData.trackArtUrl - readonly property var playerMetadata: modelData.metadata - onPlayerIsPlayingChanged: root._updateArtUrl() - onPlayerTrackTitleChanged: root._updateArtUrl() - onPlayerTrackArtistChanged: root._updateArtUrl() - onPlayerTrackAlbumChanged: root._updateArtUrl() - onPlayerTrackArtUrlChanged: root._updateArtUrl() - onPlayerMetadataChanged: root._updateArtUrl() + Connections { + target: root.activePlayer + function onIsPlayingChanged() { + root._updateArtUrl(); + } + function onTrackTitleChanged() { + root._updateArtUrl(); + } + function onTrackArtistChanged() { + root._updateArtUrl(); + } + function onTrackAlbumChanged() { + root._updateArtUrl(); + } + function onTrackArtUrlChanged() { + root._updateArtUrl(); + } + function onMetadataChanged() { + root._updateArtUrl(); } } diff --git a/quickshell/Services/TrashService.qml b/quickshell/Services/TrashService.qml index fe98a515d..bb2719f87 100644 --- a/quickshell/Services/TrashService.qml +++ b/quickshell/Services/TrashService.qml @@ -20,6 +20,8 @@ Singleton { readonly property bool isEmpty: count === 0 property var availableFileManagers: ["default"] + property bool _fileManagersDetected: false + property var _pendingOpenOptions: null property string defaultFileManagerLabel: "default (xdg-open)" signal emptyTrashConfirmRequested(int itemCount) @@ -55,16 +57,27 @@ Singleton { onStreamFinished: { const detected = (text || "").split("\n").map(s => s.trim()).filter(s => s.length > 0); root.availableFileManagers = ["default"].concat(detected).concat(["custom"]); + if (!root._pendingOpenOptions) + return; + const options = root._pendingOpenOptions; + root._pendingOpenOptions = null; + root.openTrash(options); } } } Component.onCompleted: { Paths.trashHandler = (path, callback) => trashPath(path, callback); - detectProc.running = true; refreshCount(); } + function detectFileManagers() { + if (_fileManagersDetected) + return; + _fileManagersDetected = true; + detectProc.running = true; + } + function refreshCount() { if (!monitoring) return; @@ -105,6 +118,11 @@ Singleton { openCustom(options); return; } + if (!_fileManagersDetected) { + _pendingOpenOptions = options; + detectFileManagers(); + return; + } if (availableFileManagers.indexOf(choice) < 0) { ToastService.showInfo(I18n.tr("Cannot open trash: '%1' is not installed", "dock trash toast, %1 is the file manager name").arg(choice), I18n.tr("Pick a different file manager in Settings → Dock → Trash.")); return; diff --git a/quickshell/Services/WallpaperCyclingService.qml b/quickshell/Services/WallpaperCyclingService.qml index c8c7c1001..b8a46bf66 100644 --- a/quickshell/Services/WallpaperCyclingService.qml +++ b/quickshell/Services/WallpaperCyclingService.qml @@ -13,8 +13,9 @@ Singleton { id: root property bool cyclingActive: false + property bool _lastPushedEnabled: true readonly property bool fullscreenShowing: { - if (!ToplevelManager.toplevels?.values) + if (!cyclingActive || !ToplevelManager.toplevels?.values) return false; for (const toplevel of ToplevelManager.toplevels.values) { if (toplevel.fullscreen && toplevel.activated) @@ -53,6 +54,7 @@ Singleton { function onCapabilitiesReceived() { lastCycleSeq = -1; + _lastPushedEnabled = true; updateCyclingState(); } } @@ -95,6 +97,9 @@ Singleton { function updateCyclingState() { cyclingActive = serverSchedulingAvailable && (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper); + if (!cyclingActive && !_lastPushedEnabled) + return; + _lastPushedEnabled = cyclingActive; pushConfigToServer(); } diff --git a/quickshell/Services/WeatherService.qml b/quickshell/Services/WeatherService.qml index f6462d0c7..5f751ed0e 100644 --- a/quickshell/Services/WeatherService.qml +++ b/quickshell/Services/WeatherService.qml @@ -1080,6 +1080,7 @@ Singleton { Connections { target: LocationService + enabled: root.refCount > 0 && SettingsData.weatherEnabled function onLocationChanged(data) { if (!SettingsData.useAutoLocation) diff --git a/quickshell/Services/WellbeingService.qml b/quickshell/Services/WellbeingService.qml index c8707dddc..2233a9dd1 100644 --- a/quickshell/Services/WellbeingService.qml +++ b/quickshell/Services/WellbeingService.qml @@ -47,9 +47,17 @@ Singleton { enabled: root.tracking } - onManagerActiveToplevelChanged: updateFocusedApp() + onManagerActiveToplevelChanged: { + if (!tracking) + return; + updateFocusedApp(); + } onPushedStateChanged: pushState() - onTrackingChanged: ensureSubscription() + onTrackingChanged: { + ensureSubscription(); + if (tracking) + updateFocusedApp(); + } onLimitParamsChanged: pushLimits() Component.onCompleted: { @@ -59,6 +67,7 @@ Singleton { Connections { target: CompositorService + enabled: root.tracking function onToplevelsChanged() { root.updateFocusedApp(); diff --git a/quickshell/Widgets/DSeekbar.qml b/quickshell/Widgets/DSeekbar.qml index 7d4cf4b3f..65517a173 100644 --- a/quickshell/Widgets/DSeekbar.qml +++ b/quickshell/Widgets/DSeekbar.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell.Services.Mpris import qs.Common import qs.DCommon.Widgets +import qs.Services import qs.Widgets Item { @@ -16,6 +17,22 @@ Item { readonly property real minimumValue: 0 readonly property real maximumValue: 1 readonly property real stepSize: stableLength > 0 ? 5 / stableLength : 0 + readonly property bool wantsPosition: visible && (Window.window?.visible ?? false) + property bool _positionHeld: false + onWantsPositionChanged: _syncPositionRef(wantsPosition) + Component.onCompleted: _syncPositionRef(wantsPosition) + Component.onDestruction: _syncPositionRef(false) + + function _syncPositionRef(wanted) { + if (wanted === _positionHeld) + return; + _positionHeld = wanted; + if (wanted) { + MprisController.addPositionRef(); + return; + } + MprisController.removePositionRef(); + } activeFocusOnTab: canSeek Accessible.role: Accessible.Slider Accessible.name: I18n.tr("Playback position", "media seekbar accessible name")