pragma Singleton pragma ComponentBehavior: Bound import QtCore import Qt.labs.folderlistmodel import QtQuick import "../DCommon/Common/Shape.js" as Shape import Quickshell import Quickshell.Io import qs.Common import qs.Common.settings import qs.Services import "GSettings.js" as GSettings import "LayoutResolver.js" as LayoutResolver import "NotificationRuleExpiry.js" as RuleExpiry import "settings/SettingsSpec.js" as Spec import "settings/SettingsStore.js" as Store import "../DCommon/Common/settings/SpecUtil.js" as SpecUtil import "settings/BarWidgetDefaults.js" as WidgetDefaults import "settings/DockConfig.js" as DockConfig Singleton { id: root readonly property var log: Log.scoped("SettingsData") readonly property int settingsConfigVersion: 42 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" enum Position { Top, Bottom, Left, Right, TopCenter, BottomCenter, LeftCenter, RightCenter } enum AnimationSpeed { None, Short, Medium, Long, Custom } enum AnimationEffect { Standard, // 0 — M3: scale-in, rises from below Directional, // 1 — pure large slide, no scale Depth, // 2 — medium slide with deep depth scale pop Fluid } enum SuspendBehavior { Suspend, Hibernate, SuspendThenHibernate } enum WidgetColorMode { Default, Colorful } enum TextRenderType { Qt, Native, Curve } enum TextRenderQuality { Default, Low, Normal, High, VeryHigh } readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation) readonly property string _configDir: Paths.strip(_configUrl) readonly property string pluginSettingsPath: _configDir + "/dms/plugin_settings.json" readonly property bool qtengineActive: Quickshell.env("QT_QPA_PLATFORMTHEME") === "qtengine" || Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") === "qtengine" property bool _loading: false property bool _pluginSettingsLoading: false property bool _parseError: false property bool _pluginParseError: false property bool _hasLoaded: false property bool isReadOnly: false property bool unsavedUserChanges: false property var pluginSettings: ({}) property var builtInPluginSettings: Spec.SPEC.builtInPluginSettings.def function getBuiltInPluginSetting(pluginId, key, defaultValue) { if (!builtInPluginSettings[pluginId]) return defaultValue; return builtInPluginSettings[pluginId][key] !== undefined ? builtInPluginSettings[pluginId][key] : defaultValue; } function setBuiltInPluginSetting(pluginId, key, value) { const updated = JSON.parse(JSON.stringify(builtInPluginSettings)); if (!updated[pluginId]) updated[pluginId] = {}; updated[pluginId][key] = value; builtInPluginSettings = updated; if (Store.SESSION_BACKED_PLUGIN_IDS.includes(pluginId)) { SessionData.setBuiltInPluginState(pluginId, updated[pluginId]); return; } saveSettings(); } property bool clipboardClickToPaste: Spec.SPEC.clipboardClickToPaste.def property bool clipboardEnterToPaste: Spec.SPEC.clipboardEnterToPaste.def property bool clipboardRememberTypeFilter: Spec.SPEC.clipboardRememberTypeFilter.def property bool clipboardUseOverlayLayer: Spec.SPEC.clipboardUseOverlayLayer.def property string clipboardSize: Spec.SPEC.clipboardSize.def property string clipboardTypeFilter: Spec.SPEC.clipboardTypeFilter.def property var clipboardVisibleEntryActions: Spec.SPEC.clipboardVisibleEntryActions.def property var launcherPluginVisibility: Spec.SPEC.launcherPluginVisibility.def function getPluginAllowWithoutTrigger(pluginId) { if (!launcherPluginVisibility[pluginId]) return true; return launcherPluginVisibility[pluginId].allowWithoutTrigger !== false; } function setPluginAllowWithoutTrigger(pluginId, allow) { const updated = JSON.parse(JSON.stringify(launcherPluginVisibility)); if (!updated[pluginId]) updated[pluginId] = {}; updated[pluginId].allowWithoutTrigger = allow; launcherPluginVisibility = updated; saveSettings(); } property var launcherKeybinds: Spec.SPEC.launcherKeybinds.def property var launcherPluginOrder: Spec.SPEC.launcherPluginOrder.def onLauncherPluginOrderChanged: saveSettings() function setLauncherPluginOrder(order) { launcherPluginOrder = order; } function getOrderedLauncherPlugins(allPlugins) { if (!launcherPluginOrder || launcherPluginOrder.length === 0) return allPlugins; const orderMap = {}; for (let i = 0; i < launcherPluginOrder.length; i++) orderMap[launcherPluginOrder[i]] = i; return allPlugins.slice().sort((a, b) => { const aOrder = orderMap[a.id] ?? 9999; const bOrder = orderMap[b.id] ?? 9999; if (aOrder !== bOrder) return aOrder - bOrder; return a.name.localeCompare(b.name); }); } property string currentThemeName: Spec.SPEC.currentThemeName.def property string currentThemeCategory: Spec.SPEC.currentThemeCategory.def property string customThemeFile: Spec.SPEC.customThemeFile.def property var registryThemeVariants: Spec.SPEC.registryThemeVariants.def property bool generateThemeAtStartup: Spec.SPEC.generateThemeAtStartup.def property string matugenScheme: Spec.SPEC.matugenScheme.def property bool matugenSmartMode: Spec.SPEC.matugenSmartMode.def property string matugenSourceMode: Spec.SPEC.matugenSourceMode.def property real matugenContrast: Spec.SPEC.matugenContrast.def property string matugenSeedColor: Spec.SPEC.matugenSeedColor.def property string matugenSpec: Spec.SPEC.matugenSpec.def property bool runUserMatugenTemplates: Spec.SPEC.runUserMatugenTemplates.def property string matugenTargetMonitor: Spec.SPEC.matugenTargetMonitor.def property real popupTransparency: Spec.SPEC.popupTransparency.def property bool floatingWindowSyncGlobal: Spec.SPEC.floatingWindowSyncGlobal.def property bool floatingWindowTitleBars: Spec.SPEC.floatingWindowTitleBars.def property real floatingWindowTransparency: Spec.SPEC.floatingWindowTransparency.def property bool floatingWindowForegroundLayers: Spec.SPEC.floatingWindowForegroundLayers.def property real floatingWindowForegroundTransparency: Spec.SPEC.floatingWindowForegroundTransparency.def property var dmsWindowsFloatingSeeded: Spec.SPEC.dmsWindowsFloatingSeeded.def property string hostSurfaceColor: Spec.SPEC.hostSurfaceColor.def property string hostSurfaceCustomColor: Spec.SPEC.hostSurfaceCustomColor.def property string cardSurfaceColor: Spec.SPEC.cardSurfaceColor.def property string cardSurfaceCustomColor: Spec.SPEC.cardSurfaceCustomColor.def property string chipSurfaceColor: Spec.SPEC.chipSurfaceColor.def property string chipSurfaceCustomColor: Spec.SPEC.chipSurfaceCustomColor.def property string chipSurfaceNestedColor: Spec.SPEC.chipSurfaceNestedColor.def property string chipSurfaceNestedCustomColor: Spec.SPEC.chipSurfaceNestedCustomColor.def property string widgetBackgroundColor: Spec.SPEC.widgetBackgroundColor.def property string widgetBackgroundCustomColor: Spec.SPEC.widgetBackgroundCustomColor.def property real widgetBackgroundCustomStrength: Spec.SPEC.widgetBackgroundCustomStrength.def property string widgetColorMode: Spec.SPEC.widgetColorMode.def property string controlCenterTileColorMode: Spec.SPEC.controlCenterTileColorMode.def property string buttonColorMode: Spec.SPEC.buttonColorMode.def property int containerSaturation: Spec.SPEC.containerSaturation.def property int radiusStrength: Spec.SPEC.radiusStrength.def property string radiusMode: Spec.SPEC.radiusMode.def property int fixedRadius: Spec.SPEC.fixedRadius.def readonly property real cornerRadius: Shape.radius("m", Shape.scaleForStrength(radiusStrength), radiusMode === "fixed" ? fixedRadius : -1) property int niriLayoutGapsOverride: Spec.SPEC.niriLayoutGapsOverride.def property int niriLayoutRadiusOverride: Spec.SPEC.niriLayoutRadiusOverride.def property int niriLayoutBorderSize: Spec.SPEC.niriLayoutBorderSize.def property int hyprlandLayoutGapsOverride: Spec.SPEC.hyprlandLayoutGapsOverride.def property int hyprlandLayoutGapsOutOverride: Spec.SPEC.hyprlandLayoutGapsOutOverride.def property int hyprlandLayoutRadiusOverride: Spec.SPEC.hyprlandLayoutRadiusOverride.def property int hyprlandLayoutBorderSize: Spec.SPEC.hyprlandLayoutBorderSize.def property bool hyprlandResizeOnBorder: Spec.SPEC.hyprlandResizeOnBorder.def property int hyprlandWindowOpacity: Spec.SPEC.hyprlandWindowOpacity.def property int hyprlandBorderGrabArea: Spec.SPEC.hyprlandBorderGrabArea.def property string hyprlandBlurVariant: Spec.SPEC.hyprlandBlurVariant.def property int hyprlandBlurAcrylicClarity: Spec.SPEC.hyprlandBlurAcrylicClarity.def property int hyprlandBlurAuroraIntensity: Spec.SPEC.hyprlandBlurAuroraIntensity.def property int hyprlandBlurAuroraSpeed: Spec.SPEC.hyprlandBlurAuroraSpeed.def property int hyprlandBlurHazeIntensity: Spec.SPEC.hyprlandBlurHazeIntensity.def property int hyprlandBlurHazeIridescence: Spec.SPEC.hyprlandBlurHazeIridescence.def property int hyprlandBlurRippleStrength: Spec.SPEC.hyprlandBlurRippleStrength.def property int hyprlandBlurWaterStrength: Spec.SPEC.hyprlandBlurWaterStrength.def property bool hyprlandGlowEnabled: Spec.SPEC.hyprlandGlowEnabled.def property int hyprlandGlowRange: Spec.SPEC.hyprlandGlowRange.def property int hyprlandGlowRenderPower: Spec.SPEC.hyprlandGlowRenderPower.def property bool hyprlandWobbleEnabled: Spec.SPEC.hyprlandWobbleEnabled.def property int hyprlandWobbleIntensity: Spec.SPEC.hyprlandWobbleIntensity.def property int hyprlandWobbleStiffness: Spec.SPEC.hyprlandWobbleStiffness.def property bool hyprlandMotionBlurEnabled: Spec.SPEC.hyprlandMotionBlurEnabled.def property int hyprlandMotionBlurSamples: Spec.SPEC.hyprlandMotionBlurSamples.def property bool hyprlandGroupbarBlur: Spec.SPEC.hyprlandGroupbarBlur.def property int hyprlandGroupbarTextPadding: Spec.SPEC.hyprlandGroupbarTextPadding.def property bool hyprlandGroupbarMiddleClickClose: Spec.SPEC.hyprlandGroupbarMiddleClickClose.def property bool hyprlandGroupbarDisableWhenOnly: Spec.SPEC.hyprlandGroupbarDisableWhenOnly.def property string hyprlandTilingLayout: Spec.SPEC.hyprlandTilingLayout.def property bool hyprlandDwindlePreserveSplit: Spec.SPEC.hyprlandDwindlePreserveSplit.def property bool hyprlandDwindleSmartSplit: Spec.SPEC.hyprlandDwindleSmartSplit.def property int hyprlandDwindleForceSplit: Spec.SPEC.hyprlandDwindleForceSplit.def property string hyprlandMasterOrientation: Spec.SPEC.hyprlandMasterOrientation.def property string hyprlandMasterNewStatus: Spec.SPEC.hyprlandMasterNewStatus.def property bool hyprlandMasterNewOnTop: Spec.SPEC.hyprlandMasterNewOnTop.def property int hyprlandMasterSize: Spec.SPEC.hyprlandMasterSize.def property string hyprlandScrollingDirection: Spec.SPEC.hyprlandScrollingDirection.def property int hyprlandScrollingColumnWidth: Spec.SPEC.hyprlandScrollingColumnWidth.def property bool hyprlandScrollingFullscreenOneColumn: Spec.SPEC.hyprlandScrollingFullscreenOneColumn.def property bool hyprlandScrollingFollowFocus: Spec.SPEC.hyprlandScrollingFollowFocus.def property int mangoLayoutGapsOverride: Spec.SPEC.mangoLayoutGapsOverride.def property int mangoLayoutGapsOutOverride: Spec.SPEC.mangoLayoutGapsOutOverride.def property int mangoLayoutRadiusOverride: Spec.SPEC.mangoLayoutRadiusOverride.def property int mangoLayoutBorderSize: Spec.SPEC.mangoLayoutBorderSize.def property int focusFollowsMouse: Spec.SPEC.focusFollowsMouse.def property string mouseAccelProfile: Spec.SPEC.mouseAccelProfile.def property real mouseAccelSpeed: Spec.SPEC.mouseAccelSpeed.def property bool mouseLeftHanded: Spec.SPEC.mouseLeftHanded.def property bool mouseMiddleEmulation: Spec.SPEC.mouseMiddleEmulation.def property bool mouseNaturalScroll: Spec.SPEC.mouseNaturalScroll.def property real mouseScrollFactor: Spec.SPEC.mouseScrollFactor.def property string mouseScrollMethod: Spec.SPEC.mouseScrollMethod.def property string touchpadAccelProfile: Spec.SPEC.touchpadAccelProfile.def property real touchpadAccelSpeed: Spec.SPEC.touchpadAccelSpeed.def property string touchpadClickMethod: Spec.SPEC.touchpadClickMethod.def property bool touchpadDisableOnExternalMouse: Spec.SPEC.touchpadDisableOnExternalMouse.def property bool touchpadDisableWhileTyping: Spec.SPEC.touchpadDisableWhileTyping.def property bool touchpadDragLock: Spec.SPEC.touchpadDragLock.def property bool touchpadMiddleEmulation: Spec.SPEC.touchpadMiddleEmulation.def property bool touchpadNaturalScroll: Spec.SPEC.touchpadNaturalScroll.def property real touchpadScrollFactor: Spec.SPEC.touchpadScrollFactor.def property string touchpadScrollMethod: Spec.SPEC.touchpadScrollMethod.def property bool touchpadTapAndDrag: Spec.SPEC.touchpadTapAndDrag.def property bool touchpadTapToClick: Spec.SPEC.touchpadTapToClick.def property string keyboardLayouts: Spec.SPEC.keyboardLayouts.def property string keyboardVariants: Spec.SPEC.keyboardVariants.def property string keyboardModel: Spec.SPEC.keyboardModel.def property string keyboardOptions: Spec.SPEC.keyboardOptions.def property string keyboardKeymapFile: Spec.SPEC.keyboardKeymapFile.def property string keyboardTrackLayout: Spec.SPEC.keyboardTrackLayout.def property int keyboardRepeatDelay: Spec.SPEC.keyboardRepeatDelay.def property int keyboardRepeatRate: Spec.SPEC.keyboardRepeatRate.def property bool keyboardNumlock: Spec.SPEC.keyboardNumlock.def property int firstDayOfWeek: Spec.SPEC.firstDayOfWeek.def property bool showWeekNumber: Spec.SPEC.showWeekNumber.def property string calendarBackend: Spec.SPEC.calendarBackend.def property string defaultTaskCalendarId: Spec.SPEC.defaultTaskCalendarId.def property bool audioShowStreamDevices: Spec.SPEC.audioShowStreamDevices.def property bool audioMono: Spec.SPEC.audioMono.def property string clockFormat: Spec.SPEC.clockFormat.def readonly property bool localeUses24Hour: { const fmt = Qt.locale().timeFormat(Locale.ShortFormat).replace(/'[^']*'/g, ""); return !/[aA]/.test(fmt); } readonly property bool use24HourClock: clockFormat === "24h" ? true : (clockFormat === "12h" ? false : localeUses24Hour) property bool showSeconds: Spec.SPEC.showSeconds.def property bool padHours12Hour: Spec.SPEC.padHours12Hour.def property bool useFahrenheit: Spec.SPEC.useFahrenheit.def property string windSpeedUnit: Spec.SPEC.windSpeedUnit.def property int animationDuration: Spec.SPEC.animationDuration.def property bool syncComponentAnimationSpeeds: Spec.SPEC.syncComponentAnimationSpeeds.def onSyncComponentAnimationSpeedsChanged: saveSettings() property int popoutAnimationDuration: Spec.SPEC.popoutAnimationDuration.def property int modalAnimationDuration: Spec.SPEC.modalAnimationDuration.def property bool reduceMotion: Spec.SPEC.reduceMotion.def onReduceMotionChanged: saveSettings() property int springBounce: Spec.SPEC.springBounce.def onSpringBounceChanged: saveSettings() property bool enableRippleEffects: Spec.SPEC.enableRippleEffects.def onEnableRippleEffectsChanged: saveSettings() property bool scrollbarsEnabled: Spec.SPEC.scrollbarsEnabled.def onScrollbarsEnabledChanged: saveSettings() property int motionEffect: SettingsData.AnimationEffect.Standard onMotionEffectChanged: saveSettings() property bool m3ElevationEnabled: Spec.SPEC.m3ElevationEnabled.def onM3ElevationEnabledChanged: saveSettings() property int m3ElevationIntensity: Spec.SPEC.m3ElevationIntensity.def onM3ElevationIntensityChanged: saveSettings() property int m3ElevationOpacity: Spec.SPEC.m3ElevationOpacity.def onM3ElevationOpacityChanged: saveSettings() property string m3ElevationColorMode: Spec.SPEC.m3ElevationColorMode.def onM3ElevationColorModeChanged: saveSettings() property string m3ElevationLightDirection: Spec.SPEC.m3ElevationLightDirection.def onM3ElevationLightDirectionChanged: saveSettings() property string m3ElevationCustomColor: Spec.SPEC.m3ElevationCustomColor.def onM3ElevationCustomColorChanged: saveSettings() property bool modalElevationEnabled: Spec.SPEC.modalElevationEnabled.def onModalElevationEnabledChanged: saveSettings() property bool popoutElevationEnabled: Spec.SPEC.popoutElevationEnabled.def onPopoutElevationEnabledChanged: saveSettings() property bool barElevationEnabled: Spec.SPEC.barElevationEnabled.def onBarElevationEnabledChanged: saveSettings() property bool blurEnabled: Spec.SPEC.blurEnabled.def onBlurEnabledChanged: saveSettings() property int blurStrength: Spec.SPEC.blurStrength.def property bool blurBorderSeeded: Spec.SPEC.blurBorderSeeded.def onBlurBorderSeededChanged: saveSettings() property bool blurForegroundLayers: Spec.SPEC.blurForegroundLayers.def onBlurForegroundLayersChanged: saveSettings() property real foregroundLayerTransparency: Spec.SPEC.foregroundLayerTransparency.def property real blurLayerOutlineOpacity: Spec.SPEC.blurLayerOutlineOpacity.def onBlurLayerOutlineOpacityChanged: saveSettings() property bool blurBorderEnabled: Spec.SPEC.blurBorderEnabled.def onBlurBorderEnabledChanged: saveSettings() property string blurBorderColor: Spec.SPEC.blurBorderColor.def onBlurBorderColorChanged: saveSettings() property string blurBorderCustomColor: Spec.SPEC.blurBorderCustomColor.def onBlurBorderCustomColorChanged: saveSettings() property real blurBorderOpacity: Spec.SPEC.blurBorderOpacity.def onBlurBorderOpacityChanged: saveSettings() property bool focusRingEnabled: Spec.SPEC.focusRingEnabled.def property real focusRingWidth: Spec.SPEC.focusRingWidth.def property string focusRingColor: Spec.SPEC.focusRingColor.def property string wallpaperFillMode: Spec.SPEC.wallpaperFillMode.def property bool blurredWallpaperLayer: Spec.SPEC.blurredWallpaperLayer.def property bool blurWallpaperOnOverview: Spec.SPEC.blurWallpaperOnOverview.def property string wallpaperBackgroundColorMode: Spec.SPEC.wallpaperBackgroundColorMode.def property string wallpaperBackgroundCustomColor: Spec.SPEC.wallpaperBackgroundCustomColor.def readonly property color effectiveWallpaperBackgroundColor: wallpaperBackgroundColorFor(wallpaperBackgroundColorMode) property bool nightModeExcludeFullscreen: Spec.SPEC.nightModeExcludeFullscreen.def property var nightModeExcludedApps: Spec.SPEC.nightModeExcludedApps.def function wallpaperBackgroundColorFor(mode) { switch (mode) { case "white": return "#ffffff"; case "primary": return Theme.primary; case "surface": return Theme.surfaceContainer; case "custom": return wallpaperBackgroundCustomColor; default: return "#000000"; } } property bool frameEnabled: Spec.SPEC.frameEnabled.def onFrameEnabledChanged: { saveSettings(); if (!_loading) updateFrameCompositorLayout(); } property real frameThickness: Spec.SPEC.frameThickness.def onFrameThicknessChanged: saveSettings() property int barInsetPaddingShared: Spec.SPEC.barInsetPaddingShared.def onBarInsetPaddingSharedChanged: saveSettings() property bool barInsetPaddingSyncAll: Spec.SPEC.barInsetPaddingSyncAll.def onBarInsetPaddingSyncAllChanged: saveSettings() property int frameBarInsetPadding: Spec.SPEC.frameBarInsetPadding.def onFrameBarInsetPaddingChanged: saveSettings() property real frameRounding: Spec.SPEC.frameRounding.def onFrameRoundingChanged: saveSettings() property var frameScreenPreferences: Spec.SPEC.frameScreenPreferences.def onFrameScreenPreferencesChanged: { saveSettings(); if (!_loading) Qt.callLater(_reconcileConnectedFrameBarStyles); } property real frameBarSize: Spec.SPEC.frameBarSize.def onFrameBarSizeChanged: saveSettings() property bool frameShowOnOverview: Spec.SPEC.frameShowOnOverview.def onFrameShowOnOverviewChanged: saveSettings() property bool frameBlurEnabled: Spec.SPEC.frameBlurEnabled.def onFrameBlurEnabledChanged: saveSettings() property bool frameCloseGaps: Spec.SPEC.frameCloseGaps.def onFrameCloseGapsChanged: saveSettings() property string frameLauncherEmergeSide: Spec.SPEC.frameLauncherEmergeSide.def onFrameLauncherEmergeSideChanged: saveSettings() property bool frameLauncherArcExtender: Spec.SPEC.frameLauncherArcExtender.def onFrameLauncherArcExtenderChanged: saveSettings() property bool frameLauncherEdgeHover: Spec.SPEC.frameLauncherEdgeHover.def onFrameLauncherEdgeHoverChanged: saveSettings() readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" property string frameMode: Spec.SPEC.frameMode.def onFrameModeChanged: { saveSettings(); if (!_loading && frameEnabled) updateFrameCompositorLayout(); } property var connectedFrameBarStyleBackups: Spec.SPEC.connectedFrameBarStyleBackups.def onConnectedFrameBarStyleBackupsChanged: saveSettings() readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected" onConnectedFrameModeActiveChanged: { if (_loading) return; _reconcileConnectedFrameBarStyles(); } readonly property var frameBarConfig: barConfigs.find(bc => bc.enabled !== false && !isIslandBarConfig(bc)) ?? null readonly property real frameSurfaceOpacity: barTransparency(frameBarConfig) readonly property color frameSurfaceBase: barSurfaceColor(frameBarConfig) property string systemTrayIconTintMode: Spec.SPEC.systemTrayIconTintMode.def property int systemTrayIconTintSaturation: Spec.SPEC.systemTrayIconTintSaturation.def property int systemTrayIconTintStrength: Spec.SPEC.systemTrayIconTintStrength.def property int controlCenterColumns: Spec.SPEC.controlCenterColumns.def property real controlCenterIconScale: Spec.SPEC.controlCenterIconScale.def property var controlCenterWidgets: Spec.SPEC.controlCenterWidgets.def property var workspaceNameIcons: Spec.SPEC.workspaceNameIcons.def property bool scrollTitleEnabled: Spec.SPEC.scrollTitleEnabled.def property bool audioVisualizerEnabled: Spec.SPEC.audioVisualizerEnabled.def property int audioWheelScrollAmount: Spec.SPEC.audioWheelScrollAmount.def property bool bluetoothMprisEnabled: Spec.SPEC.bluetoothMprisEnabled.def property var mediaExcludePlayers: Spec.SPEC.mediaExcludePlayers.def property var mediaLyricsProviders: Spec.SPEC.mediaLyricsProviders.def property var appIdSubstitutions: Spec.SPEC.appIdSubstitutions.def property string centeringMode: Spec.SPEC.centeringMode.def property string clockDateFormat: Spec.SPEC.clockDateFormat.def property string lockDateFormat: Spec.SPEC.lockDateFormat.def property bool greeterRememberLastSession: Spec.SPEC.greeterRememberLastSession.def property bool greeterRememberLastUser: Spec.SPEC.greeterRememberLastUser.def property bool greeterAutoLogin: Spec.SPEC.greeterAutoLogin.def property bool greeterEnableFprint: Spec.SPEC.greeterEnableFprint.def property bool greeterEnableU2f: Spec.SPEC.greeterEnableU2f.def property string browserPickerViewMode: Spec.SPEC.browserPickerViewMode.def property string appPickerViewMode: Spec.SPEC.appPickerViewMode.def property bool sortAppsAlphabetically: Spec.SPEC.sortAppsAlphabetically.def property int appLauncherGridColumns: Spec.SPEC.appLauncherGridColumns.def property bool closeNiriOverviewOnWindowFocus: Spec.SPEC.closeNiriOverviewOnWindowFocus.def property bool rememberLastQuery: Spec.SPEC.rememberLastQuery.def property bool launcherHistoryEnabled: Spec.SPEC.launcherHistoryEnabled.def property string launcherEnterAction: Spec.SPEC.launcherEnterAction.def property bool rememberLastMode: Spec.SPEC.rememberLastMode.def property var spotlightSectionViewModes: Spec.SPEC.spotlightSectionViewModes.def onSpotlightSectionViewModesChanged: saveSettings() property var appDrawerSectionViewModes: Spec.SPEC.appDrawerSectionViewModes.def onAppDrawerSectionViewModesChanged: saveSettings() property bool niriOverviewOverlayEnabled: Spec.SPEC.niriOverviewOverlayEnabled.def property string niriOverviewLauncherStyle: Spec.SPEC.niriOverviewLauncherStyle.def property string dankLauncherV2Size: Spec.SPEC.dankLauncherV2Size.def property bool dankLauncherV2ShowSourceBadges: Spec.SPEC.dankLauncherV2ShowSourceBadges.def property bool dankLauncherV2BorderEnabled: Spec.SPEC.dankLauncherV2BorderEnabled.def property int dankLauncherV2BorderThickness: Spec.SPEC.dankLauncherV2BorderThickness.def property string dankLauncherV2BorderColor: Spec.SPEC.dankLauncherV2BorderColor.def property bool dankLauncherV2ShowFooter: Spec.SPEC.dankLauncherV2ShowFooter.def property bool dankLauncherV2UnloadOnClose: Spec.SPEC.dankLauncherV2UnloadOnClose.def property bool dankLauncherV2IncludeFilesInAll: Spec.SPEC.dankLauncherV2IncludeFilesInAll.def property bool dankLauncherV2IncludeFoldersInAll: Spec.SPEC.dankLauncherV2IncludeFoldersInAll.def property bool launcherUseOverlayLayer: Spec.SPEC.launcherUseOverlayLayer.def property string launcherStyle: Spec.SPEC.launcherStyle.def property string avatarRing: Spec.SPEC.avatarRing.def property bool spotlightBarShowModeChips: Spec.SPEC.spotlightBarShowModeChips.def property bool keybindsFloatingWindow: Spec.SPEC.keybindsFloatingWindow.def onKeybindsFloatingWindowChanged: saveSettings() property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyVpnLastConnected: "" readonly property string weatherLocation: SessionData.weatherLocation readonly property string weatherCoordinates: SessionData.weatherCoordinates property bool useAutoLocation: Spec.SPEC.useAutoLocation.def property bool weatherEnabled: Spec.SPEC.weatherEnabled.def readonly property var _dashTabsDefault: [ { "id": "overview", "enabled": true }, { "id": "media", "enabled": true }, { "id": "wallpaper", "enabled": true }, { "id": "weather", "enabled": true }, { "id": "wellbeing", "enabled": true }, { "id": "notifications", "enabled": false } ] property string dashTabPosition: Spec.SPEC.dashTabPosition.def property bool dashTabsEvenlySpaced: Spec.SPEC.dashTabsEvenlySpaced.def property var dashTabs: Spec.SPEC.dashTabs.def onDashTabsChanged: saveSettings() readonly property var _dashCardsDefault: [ { "id": "clock", "w": 2, "h": 1 }, { "id": "weather", "w": 1, "h": 1 }, { "id": "notifications", "w": 3, "h": 5 }, { "id": "calendar", "w": 3, "h": 3 }, { "id": "media", "w": 3, "h": 1 } ] property var dashCards: Spec.SPEC.dashCards.def onDashCardsChanged: saveSettings() property var dashOptions: Spec.SPEC.dashOptions.def onDashOptionsChanged: saveSettings() property bool wellbeingEnabled: Spec.SPEC.wellbeingEnabled.def property int wellbeingDailyLimit: Spec.SPEC.wellbeingDailyLimit.def property var wellbeingAppLimits: Spec.SPEC.wellbeingAppLimits.def function getDashTabs() { const stored = Array.isArray(dashTabs) ? dashTabs : []; const result = []; const seen = {}; for (var i = 0; i < stored.length; i++) { const id = stored[i] && stored[i].id; if (typeof id !== "string" || id === "" || seen[id]) continue; seen[id] = true; result.push({ "id": id, "enabled": stored[i].enabled !== false }); } for (var j = 0; j < _dashTabsDefault.length; j++) { if (seen[_dashTabsDefault[j].id]) continue; result.push({ "id": _dashTabsDefault[j].id, "enabled": _dashTabsDefault[j].enabled }); } return result; } function setDashTabOrder(ids) { const current = getDashTabs(); const ordered = []; for (var i = 0; i < ids.length; i++) { const existing = current.find(t => t.id === ids[i]); if (existing) ordered.push(existing); } for (var j = 0; j < current.length; j++) { if (ids.indexOf(current[j].id) < 0) ordered.push(current[j]); } dashTabs = ordered; } function setDashTabEnabled(id, on) { const current = getDashTabs(); if (!current.some(t => t.id === id)) current.push({ "id": id, "enabled": true }); dashTabs = current.map(t => t.id === id ? { "id": t.id, "enabled": on } : t); } function resetDashTabs() { dashTabs = _dashTabsDefault.map(t => ({ "id": t.id, "enabled": t.enabled })); } function resetDashCards() { dashCards = _dashCardsDefault.map(c => ({ "id": c.id, "w": c.w, "h": c.h })); const options = Object.assign({}, dashOptions); delete options.overview; dashOptions = options; } property string networkPreference: Spec.SPEC.networkPreference.def property string iconThemeDark: Spec.SPEC.iconThemeDark.def property string iconThemeLight: Spec.SPEC.iconThemeLight.def property bool iconThemePerMode: Spec.SPEC.iconThemePerMode.def readonly property string iconTheme: resolveIconTheme() property var availableIconThemes: Spec.SPEC.availableIconThemes.def property string systemDefaultIconTheme: Spec.SPEC.systemDefaultIconTheme.def property var cursorSettings: Spec.SPEC.cursorSettings.def property var availableCursorThemes: Spec.SPEC.availableCursorThemes.def property string systemDefaultCursorTheme: Spec.SPEC.systemDefaultCursorTheme.def property string fontFamily: Spec.SPEC.fontFamily.def property string monoFontFamily: Spec.SPEC.monoFontFamily.def property string displayFontFamily: Spec.SPEC.displayFontFamily.def property int fontWeight: Font.Normal property real fontScale: Spec.SPEC.fontScale.def property int textRenderType: SettingsData.TextRenderType.Qt property int textRenderQuality: SettingsData.TextRenderQuality.Default property bool notepadUseMonospace: Spec.SPEC.notepadUseMonospace.def property string notepadFontFamily: Spec.SPEC.notepadFontFamily.def property real notepadFontSize: Spec.SPEC.notepadFontSize.def property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def property bool notepadShowLineNumbers: Spec.SPEC.notepadShowLineNumbers.def property bool notepadAutoSave: Spec.SPEC.notepadAutoSave.def property string notepadSlideoutSide: Spec.SPEC.notepadSlideoutSide.def property string notepadDefaultMode: Spec.SPEC.notepadDefaultMode.def property real notepadTransparencyOverride: Spec.SPEC.notepadTransparencyOverride.def property real notepadLastCustomTransparency: Spec.SPEC.notepadLastCustomTransparency.def property bool notepadUseCompositorGap: Spec.SPEC.notepadUseCompositorGap.def property int notepadEdgeGap: Spec.SPEC.notepadEdgeGap.def property string activeCompositor: "" // Compositor layout gap when enabled and available, else the manual value. readonly property int notepadEffectiveEdgeGap: { if (notepadUseCompositorGap) { var g = -1; switch (activeCompositor) { case "niri": g = niriLayoutGapsOverride; break; case "hyprland": g = hyprlandLayoutGapsOverride; break; case "mango": g = mangoLayoutGapsOverride; break; } if (g >= 0) return g; } return Math.max(0, notepadEdgeGap); } onNotepadUseMonospaceChanged: saveSettings() onNotepadFontFamilyChanged: saveSettings() onNotepadFontSizeChanged: saveSettings() onNotepadShowLineNumbersChanged: saveSettings() onNotepadAutoSaveChanged: saveSettings() onNotepadSlideoutSideChanged: saveSettings() onNotepadDefaultModeChanged: saveSettings() onNotepadUseCompositorGapChanged: saveSettings() onNotepadEdgeGapChanged: saveSettings() // onCenteringModeChanged: saveSettings() onNotepadTransparencyOverrideChanged: { if (notepadTransparencyOverride > 0) { notepadLastCustomTransparency = notepadTransparencyOverride; } saveSettings(); } onNotepadLastCustomTransparencyChanged: saveSettings() property bool soundsEnabled: Spec.SPEC.soundsEnabled.def property bool useSystemSoundTheme: Spec.SPEC.useSystemSoundTheme.def property bool soundNewNotification: Spec.SPEC.soundNewNotification.def property bool soundVolumeChanged: Spec.SPEC.soundVolumeChanged.def property bool soundPluggedIn: Spec.SPEC.soundPluggedIn.def property bool soundLogin: Spec.SPEC.soundLogin.def property bool muteSoundsWhenMediaPlaying: Spec.SPEC.muteSoundsWhenMediaPlaying.def property int acMonitorTimeout: Spec.SPEC.acMonitorTimeout.def property int acLockTimeout: Spec.SPEC.acLockTimeout.def property int acSuspendTimeout: Spec.SPEC.acSuspendTimeout.def property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend property string acProfileName: Spec.SPEC.acProfileName.def property int acPostLockMonitorTimeout: Spec.SPEC.acPostLockMonitorTimeout.def property int batteryMonitorTimeout: Spec.SPEC.batteryMonitorTimeout.def property int batteryLockTimeout: Spec.SPEC.batteryLockTimeout.def property int batterySuspendTimeout: Spec.SPEC.batterySuspendTimeout.def property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend property string batteryProfileName: Spec.SPEC.batteryProfileName.def property int batteryPostLockMonitorTimeout: Spec.SPEC.batteryPostLockMonitorTimeout.def property int batteryChargeLimit: Spec.SPEC.batteryChargeLimit.def property bool batteryNotifyChargeLimit: Spec.SPEC.batteryNotifyChargeLimit.def property int batteryCriticalThreshold: Spec.SPEC.batteryCriticalThreshold.def property bool batteryCriticalAnimation: Spec.SPEC.batteryCriticalAnimation.def property bool batteryNotifyCritical: Spec.SPEC.batteryNotifyCritical.def property int batteryLowThreshold: Spec.SPEC.batteryLowThreshold.def property bool batteryNotifyLow: Spec.SPEC.batteryNotifyLow.def property int batteryChargeLimitNotificationType: Spec.SPEC.batteryChargeLimitNotificationType.def property int batteryLowNotificationType: Spec.SPEC.batteryLowNotificationType.def property int batteryCriticalNotificationType: Spec.SPEC.batteryCriticalNotificationType.def property bool batteryAutoPowerSaver: Spec.SPEC.batteryAutoPowerSaver.def property bool lowerDisplayRefreshRateOnBattery: Spec.SPEC.lowerDisplayRefreshRateOnBattery.def property bool lockBeforeSuspend: Spec.SPEC.lockBeforeSuspend.def property bool loginctlLockIntegration: Spec.SPEC.loginctlLockIntegration.def property bool fadeToLockEnabled: Spec.SPEC.fadeToLockEnabled.def property int fadeToLockGracePeriod: Spec.SPEC.fadeToLockGracePeriod.def property bool fadeToDpmsEnabled: Spec.SPEC.fadeToDpmsEnabled.def property int fadeToDpmsGracePeriod: Spec.SPEC.fadeToDpmsGracePeriod.def property string launchPrefix: Spec.SPEC.launchPrefix.def property bool syncModeWithPortal: Spec.SPEC.syncModeWithPortal.def property bool terminalsAlwaysDark: Spec.SPEC.terminalsAlwaysDark.def property int terminalOpacity: Spec.SPEC.terminalOpacity.def property string terminalPalette: Spec.SPEC.terminalPalette.def property string muxType: Spec.SPEC.muxType.def property bool muxUseCustomCommand: Spec.SPEC.muxUseCustomCommand.def property string muxCustomCommand: Spec.SPEC.muxCustomCommand.def property string muxSessionFilter: Spec.SPEC.muxSessionFilter.def property bool runDmsMatugenTemplates: Spec.SPEC.runDmsMatugenTemplates.def property bool matugenTemplateGtk: Spec.SPEC.matugenTemplateGtk.def property bool matugenTemplateNiri: Spec.SPEC.matugenTemplateNiri.def property bool matugenTemplateHyprland: Spec.SPEC.matugenTemplateHyprland.def property bool matugenTemplateMangowc: Spec.SPEC.matugenTemplateMangowc.def property bool matugenTemplateQt5ct: Spec.SPEC.matugenTemplateQt5ct.def property bool matugenTemplateQt6ct: Spec.SPEC.matugenTemplateQt6ct.def property bool matugenTemplateFcitx5: Spec.SPEC.matugenTemplateFcitx5.def property bool matugenTemplateQtengine: Spec.SPEC.matugenTemplateQtengine.def property bool matugenTemplateFirefox: Spec.SPEC.matugenTemplateFirefox.def property bool matugenTemplatePywalfox: Spec.SPEC.matugenTemplatePywalfox.def property bool matugenTemplateZenBrowser: Spec.SPEC.matugenTemplateZenBrowser.def property bool matugenTemplateVesktop: Spec.SPEC.matugenTemplateVesktop.def property bool matugenTemplateVencord: Spec.SPEC.matugenTemplateVencord.def property bool matugenTemplateEquibop: Spec.SPEC.matugenTemplateEquibop.def property bool matugenTemplateGhostty: Spec.SPEC.matugenTemplateGhostty.def property bool matugenTemplateKitty: Spec.SPEC.matugenTemplateKitty.def property bool matugenTemplateFoot: Spec.SPEC.matugenTemplateFoot.def property bool matugenTemplateNeovim: Spec.SPEC.matugenTemplateNeovim.def property bool matugenTemplateAlacritty: Spec.SPEC.matugenTemplateAlacritty.def property bool matugenTemplateWezterm: Spec.SPEC.matugenTemplateWezterm.def property bool matugenTemplateDgop: Spec.SPEC.matugenTemplateDgop.def property bool matugenTemplateKcolorscheme: Spec.SPEC.matugenTemplateKcolorscheme.def property bool matugenTemplateVscode: Spec.SPEC.matugenTemplateVscode.def property bool matugenTemplateEmacs: Spec.SPEC.matugenTemplateEmacs.def property bool matugenTemplateZed: Spec.SPEC.matugenTemplateZed.def property var matugenTemplateNeovimSettings: Spec.SPEC.matugenTemplateNeovimSettings.def property bool matugenTemplateNeovimSetBackground: Spec.SPEC.matugenTemplateNeovimSetBackground.def property bool notificationOverlayEnabled: Spec.SPEC.notificationOverlayEnabled.def property bool notificationPopupShadowEnabled: Spec.SPEC.notificationPopupShadowEnabled.def property bool notificationPopupPrivacyMode: Spec.SPEC.notificationPopupPrivacyMode.def property bool notificationPopupBodyInvokesAction: Spec.SPEC.notificationPopupBodyInvokesAction.def property bool notificationForegroundLayers: Spec.SPEC.notificationForegroundLayers.def property int overviewRows: Spec.SPEC.overviewRows.def property int overviewColumns: Spec.SPEC.overviewColumns.def property real overviewScale: Spec.SPEC.overviewScale.def property bool modalDarkenBackground: Spec.SPEC.modalDarkenBackground.def property bool lockScreenShowPowerActions: Spec.SPEC.lockScreenShowPowerActions.def property bool lockScreenShowProfileImage: Spec.SPEC.lockScreenShowProfileImage.def property bool lockScreenShowWeather: Spec.SPEC.lockScreenShowWeather.def property bool lockScreenPowerOffMonitorsOnLock: Spec.SPEC.lockScreenPowerOffMonitorsOnLock.def 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 readonly property bool lockFingerprintReady: Processes.lockFingerprintReady readonly property string lockFingerprintReason: Processes.lockFingerprintReason readonly property bool greeterFingerprintCanEnable: Processes.greeterFingerprintCanEnable readonly property bool greeterFingerprintReady: Processes.greeterFingerprintReady 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 readonly property bool lockU2fReady: Processes.lockU2fReady readonly property string lockU2fReason: Processes.lockU2fReason readonly property bool greeterU2fCanEnable: Processes.greeterU2fCanEnable readonly property bool greeterU2fReady: Processes.greeterU2fReady readonly property string greeterU2fReason: Processes.greeterU2fReason readonly property string greeterU2fSource: Processes.greeterU2fSource property string lockPamPath: Spec.SPEC.lockPamPath.def property bool lockPamInlineFprint: Spec.SPEC.lockPamInlineFprint.def property bool lockPamInlineU2f: Spec.SPEC.lockPamInlineU2f.def property bool lockPamExternallyManaged: Spec.SPEC.lockPamExternallyManaged.def property string lockU2fPamPath: Spec.SPEC.lockU2fPamPath.def property string lockScreenSecurityKeyShortcut: Spec.SPEC.lockScreenSecurityKeyShortcut.def property bool lockScreenSecurityKeyShortcutEnabled: Spec.SPEC.lockScreenSecurityKeyShortcutEnabled.def property bool greeterPamExternallyManaged: Spec.SPEC.greeterPamExternallyManaged.def property string lockScreenInactiveColor: Spec.SPEC.lockScreenInactiveColor.def property bool lockScreenVideoEnabled: Spec.SPEC.lockScreenVideoEnabled.def property string lockScreenVideoPath: Spec.SPEC.lockScreenVideoPath.def property bool lockScreenVideoCycling: Spec.SPEC.lockScreenVideoCycling.def property string lockScreenWallpaperPath: Spec.SPEC.lockScreenWallpaperPath.def property string lockScreenWallpaperFillMode: Spec.SPEC.lockScreenWallpaperFillMode.def property string lockScreenFontFamily: Spec.SPEC.lockScreenFontFamily.def property int notificationTimeoutLow: Spec.SPEC.notificationTimeoutLow.def property int notificationTimeoutNormal: Spec.SPEC.notificationTimeoutNormal.def property int notificationTimeoutCritical: Spec.SPEC.notificationTimeoutCritical.def property bool notificationIgnoreAppTimeout: Spec.SPEC.notificationIgnoreAppTimeout.def property bool notificationCompactMode: Spec.SPEC.notificationCompactMode.def property bool notificationShowTimeoutBar: Spec.SPEC.notificationShowTimeoutBar.def property bool notificationDedupeEnabled: Spec.SPEC.notificationDedupeEnabled.def property int notificationPopupPosition: SettingsData.Position.Top property int notificationAnimationDuration: Spec.SPEC.notificationAnimationDuration.def property bool notificationHistoryEnabled: Spec.SPEC.notificationHistoryEnabled.def property int notificationHistoryMaxCount: Spec.SPEC.notificationHistoryMaxCount.def property int notificationHistoryMaxAgeDays: Spec.SPEC.notificationHistoryMaxAgeDays.def property bool notificationHistorySaveLow: Spec.SPEC.notificationHistorySaveLow.def property bool notificationHistorySaveNormal: Spec.SPEC.notificationHistorySaveNormal.def property bool notificationHistorySaveCritical: Spec.SPEC.notificationHistorySaveCritical.def property var notificationRules: Spec.SPEC.notificationRules.def property bool notificationDndAllowCritical: Spec.SPEC.notificationDndAllowCritical.def property bool notificationDndWhileScreenSharing: Spec.SPEC.notificationDndWhileScreenSharing.def property bool notificationFocusedMonitor: Spec.SPEC.notificationFocusedMonitor.def readonly property var islandBarConfigs: { barConfigs; return (barConfigs || []).filter(cfg => isIslandBarConfig(cfg)); } readonly property bool dankIslandEnabled: (barConfigs || []).some(cfg => (cfg.enabled ?? false) && hostsIsland(cfg)) readonly property var enabledIslandBarConfigs: (barConfigs || []).filter(cfg => cfg?.island === true && (cfg.enabled ?? false)) // Session-only: which bar, island or dot last-used shared shortcuts follow on each screen. property var lastUsedBarByScreen: ({}) // One slot per edge; a dot floats, so it never takes one. readonly property int edgeBarConfigCount: (barConfigs || []).filter(cfg => cfg && !isDotBarConfig(cfg)).length readonly property var dotBarConfig: (barConfigs || []).find(cfg => isDotBarConfig(cfg)) ?? null readonly property var islandDefaults: WidgetDefaults.ISLAND_DEFAULTS readonly property var islandWidgetDefaults: WidgetDefaults.DEFAULTS.island function islandSettings(bc) { if (!bc) return {}; return isIslandBarConfig(bc) ? bc : (islandWidgetEntry(bc) ?? {}); } function islandDefaultsFor(bc) { return isIslandBarConfig(bc) ? islandDefaults : islandWidgetDefaults; } function _isIslandEntry(entry) { return (typeof entry === "string" ? entry : entry?.id) === "island"; } // A widget toggled off in the list is still placed, but it owns no screen. function _islandWidgetLocation(cfg, enabledOnly) { for (const sectionId of ["left", "center", "right"]) { const list = cfg?.[sectionId + "Widgets"] ?? []; const index = list.findIndex(entry => _isIslandEntry(entry)); if (index < 0) continue; if (enabledOnly && list[index]?.enabled === false) return null; return { sectionId, index }; } return null; } function islandWidgetSection(cfg) { return _islandWidgetLocation(cfg, true)?.sectionId ?? ""; } // Mutates cfg in place, so callers pass a clone; a bare string entry is upgraded to carry settings. function _writableIslandSettings(cfg) { if (isIslandBarConfig(cfg)) return cfg; const location = _islandWidgetLocation(cfg); if (!location) return {}; const list = cfg[location.sectionId + "Widgets"]; if (typeof list[location.index] === "string") list[location.index] = { "id": "island", "enabled": true }; return list[location.index]; } function setIslandSettings(barId, patch) { const config = getBarConfig(barId); if (!config) return; if ("islandRouteDash" in patch) { patch = Object.assign({}, patch); const settings = islandSettings(config); for (const activity of islandDashActivities) { const key = islandRouteKey(activity); if (patch[key] === undefined && (settings[key] === undefined || settings[key] === null)) patch[key] = islandSetting(config, key); } } if (isIslandBarConfig(config)) { updateBarConfig(barId, patch); return; } const location = _islandWidgetLocation(config); if (!location) return; updateBarWidget(barId, location.sectionId, location.index, patch); } function islandSetting(bc, key) { const value = islandSettings(bc)[key]; if (value !== undefined && value !== null) return value; switch (key) { case "islandRouteMedia": case "islandRouteWeather": case "islandRouteWallpaper": return islandSetting(bc, "islandRouteDash"); } return islandDefaultsFor(bc)[key]; } function islandSatelliteTransparency(bc) { if (islandSetting(bc, "islandSatelliteFollowInterfaceStyle")) return barTransparency(bc); return islandSetting(bc, "islandSatelliteTransparency"); } function islandLevelDisplay(bc, key) { const value = islandSetting(bc, key); return value === "icon" || value === "percentage" || value === "both" ? value : "both"; } function islandClockDisplay(bc) { const value = islandSetting(bc, "islandHomeClockDisplay"); return value === "time" || value === "date" || value === "both" ? value : "both"; } function islandHomeStatusContent(bc) { const value = islandSetting(bc, "islandHomeStatusContent"); return value === "battery" || value === "connectivity" ? value : "battery"; } function islandEdge(bc) { return positionToSide(bc?.position ?? SettingsData.Position.Top) || "top"; } function islandVertical(bc) { const edge = islandEdge(bc); return edge === "left" || edge === "right"; } function islandStripThickness(bc) { return LayoutResolver.islandThickness(islandSettings(bc), islandDefaultsFor(bc)); } function islandReserveThickness(bc) { return LayoutResolver.islandReserve(islandSettings(bc), islandDefaultsFor(bc)); } readonly property var _islandHomeGroupIds: ["workspaces", "media", "clock", "weather", "status", "volume", "brightness", "notifications", "privacy", "tray"] readonly property var _islandHomeLayoutDefault: [ { "id": "workspaces", "enabled": false }, { "id": "media", "enabled": true }, { "id": "clock", "enabled": true }, { "id": "weather", "enabled": false }, { "id": "status", "enabled": false }, { "id": "volume", "enabled": false }, { "id": "brightness", "enabled": false }, { "id": "notifications", "enabled": true }, { "id": "privacy", "enabled": true }, { "id": "tray", "enabled": false } ] function getIslandHomeLayout(bc) { const layout = islandSettings(bc).islandHomeLayout; const stored = Array.isArray(layout) ? layout : []; const result = []; const seen = {}; for (const entry of stored) { const id = entry && entry.id; if (_islandHomeGroupIds.indexOf(id) < 0 || seen[id]) continue; seen[id] = true; result.push({ "id": id, "enabled": id === "clock" || entry.enabled !== false }); } for (const fallback of _islandHomeLayoutDefault) { if (!seen[fallback.id]) result.push({ "id": fallback.id, "enabled": fallback.enabled }); } return result; } function islandHomeGroupEnabled(bc, id) { const entry = getIslandHomeLayout(bc).find(g => g.id === id); return entry ? entry.enabled : false; } function setIslandHomeLayoutOrder(barId, ids) { const current = getIslandHomeLayout(getBarConfig(barId)); const ordered = ids.map(id => current.find(g => g.id === id)).filter(g => g); for (const entry of current) { if (ids.indexOf(entry.id) < 0) ordered.push(entry); } setIslandSettings(barId, { islandHomeLayout: ordered }); } function setIslandHomeGroupEnabled(barId, id, on) { if (id === "clock") return; setIslandSettings(barId, { islandHomeLayout: getIslandHomeLayout(getBarConfig(barId)).map(g => g.id === id ? { "id": g.id, "enabled": on } : g) }); } property bool osdAlwaysShowValue: Spec.SPEC.osdAlwaysShowValue.def property int osdPosition: SettingsData.Position.BottomCenter property var osdPositionOverrides: Spec.SPEC.osdPositionOverrides.def property bool osdVolumeEnabled: Spec.SPEC.osdVolumeEnabled.def property bool osdMediaVolumeEnabled: Spec.SPEC.osdMediaVolumeEnabled.def property bool osdMediaPlaybackEnabled: Spec.SPEC.osdMediaPlaybackEnabled.def property bool osdBrightnessEnabled: Spec.SPEC.osdBrightnessEnabled.def property bool osdIdleInhibitorEnabled: Spec.SPEC.osdIdleInhibitorEnabled.def property bool osdMicMuteEnabled: Spec.SPEC.osdMicMuteEnabled.def property bool osdMicVolumeEnabled: Spec.SPEC.osdMicVolumeEnabled.def property bool osdCapsLockEnabled: Spec.SPEC.osdCapsLockEnabled.def property bool osdTouchpadEnabled: Spec.SPEC.osdTouchpadEnabled.def property bool osdPowerProfileEnabled: Spec.SPEC.osdPowerProfileEnabled.def property bool osdAudioOutputEnabled: Spec.SPEC.osdAudioOutputEnabled.def property bool osdWorkspaceEnabled: Spec.SPEC.osdWorkspaceEnabled.def property bool powerActionConfirm: Spec.SPEC.powerActionConfirm.def property real powerActionHoldDuration: Spec.SPEC.powerActionHoldDuration.def property var powerMenuActions: Spec.SPEC.powerMenuActions.def property string powerMenuDefaultAction: Spec.SPEC.powerMenuDefaultAction.def property bool powerMenuGridLayout: Spec.SPEC.powerMenuGridLayout.def property string customPowerActionLock: Spec.SPEC.customPowerActionLock.def property string customPowerActionLogout: Spec.SPEC.customPowerActionLogout.def property string customPowerActionSuspend: Spec.SPEC.customPowerActionSuspend.def property string customPowerActionHibernate: Spec.SPEC.customPowerActionHibernate.def property string customPowerActionReboot: Spec.SPEC.customPowerActionReboot.def property string customPowerActionPowerOff: Spec.SPEC.customPowerActionPowerOff.def property var customPowerButtons: Spec.SPEC.customPowerButtons.def property var powerMenuBootEntries: Spec.SPEC.powerMenuBootEntries.def property bool updaterCheckOnStart: Spec.SPEC.updaterCheckOnStart.def property bool updaterUseCustomCommand: Spec.SPEC.updaterUseCustomCommand.def property string updaterCustomCommand: Spec.SPEC.updaterCustomCommand.def property string updaterTerminalAdditionalParams: Spec.SPEC.updaterTerminalAdditionalParams.def property int updaterIntervalSeconds: Spec.SPEC.updaterIntervalSeconds.def property bool updaterNotify: Spec.SPEC.updaterNotify.def property bool updaterPauseOnBattery: Spec.SPEC.updaterPauseOnBattery.def property int updaterNotifyMinSeconds: Spec.SPEC.updaterNotifyMinSeconds.def property bool updaterIncludeFlatpak: Spec.SPEC.updaterIncludeFlatpak.def property bool updaterAllowAUR: Spec.SPEC.updaterAllowAUR.def property bool updaterReopenAfterUpgrade: Spec.SPEC.updaterReopenAfterUpgrade.def property bool updaterUpgradeInWindow: Spec.SPEC.updaterUpgradeInWindow.def property var updaterIgnoredPackages: Spec.SPEC.updaterIgnoredPackages.def property string displayNameMode: Spec.SPEC.displayNameMode.def onDisplayNameModeChanged: { if (!_loading) _reconcileConnectedFrameBarStyles(); } property var screenPreferences: Spec.SPEC.screenPreferences.def property var showOnLastDisplay: Spec.SPEC.showOnLastDisplay.def property var displayProfiles: Spec.SPEC.displayProfiles.def property var displayPreviousRefreshModes: Spec.SPEC.displayPreviousRefreshModes.def property bool displayProfileAutoSelect: Spec.SPEC.displayProfileAutoSelect.def property bool displayShowDisconnected: Spec.SPEC.displayShowDisconnected.def property bool displaySnapToEdge: Spec.SPEC.displaySnapToEdge.def property var barIpcRevealStates: ({}) property var dockConfigs: Spec.SPEC.dockConfigs.def property var barConfigs: Spec.SPEC.barConfigs.def property var desktopWidgetInstances: Spec.SPEC.desktopWidgetInstances.def property var desktopWidgetGroups: Spec.SPEC.desktopWidgetGroups.def property string desktopContextMenu: Spec.SPEC.desktopContextMenu.def property var lockScreenWidgetInstances: Spec.SPEC.lockScreenWidgetInstances.def property var greeterWidgetInstances: Spec.SPEC.greeterWidgetInstances.def property bool greeterFollowLockScreen: Spec.SPEC.greeterFollowLockScreen.def readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances", "greeterWidgetInstances"] // Released greeters still read these three shared keys, so they follow the lock widgets. onLockScreenWidgetInstancesChanged: { const status = widgetInstanceOfType("lockScreenWidgetInstances", "lockStatus"); const auth = widgetInstanceOfType("lockScreenWidgetInstances", "lockAuth"); const power = widgetInstanceOfType("lockScreenWidgetInstances", "lockPower"); const mirror = (key, value) => { if (root[key] !== value) set(key, value); }; mirror("lockScreenShowWeather", !!status && status.enabled !== false && (status.config?.showWeather ?? true)); mirror("lockScreenShowProfileImage", !!auth && (auth.config?.showProfileImage ?? true)); mirror("lockScreenShowPowerActions", !!power && power.enabled !== false); syncGreeterWidgets(); } function widgetInstanceOfType(listKey, widgetType) { return (root[listKey] || []).find(inst => inst.widgetType === widgetType) ?? null; } function lockWidgetInstance(widgetType) { return widgetInstanceOfType("lockScreenWidgetInstances", widgetType); } function syncGreeterWidgets() { if (!greeterFollowLockScreen) return; const next = Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, greeterWidgetInstances); if (JSON.stringify(next) === JSON.stringify(greeterWidgetInstances)) return; set("greeterWidgetInstances", next); } function setGreeterFollowLockScreen(follow) { if (follow === greeterFollowLockScreen) return; if (follow) { for (const inst of greeterWidgetInstances || []) { if (inst.id.startsWith("gw_")) SessionData.removeDesktopWidgetInstancePositions(inst.id); } set("greeterFollowLockScreen", true); syncGreeterWidgets(); return; } const detached = (greeterWidgetInstances || []).map(inst => { if (inst.widgetType === "greeterSession") return inst; const copy = JSON.parse(JSON.stringify(inst)); copy.id = "gw_" + inst.id; SessionData.copyDesktopWidgetInstancePositions(inst.id, copy.id); if (inst.widgetType !== "desktopClock" || inst.config?.autoPosition === false) return copy; copy.config.autoPosition = false; SessionData.pinPublishedLockPosition(inst.id, copy.id, inst.config?.syncPositionAcrossScreens ?? false); return copy; }); set("greeterWidgetInstances", detached); set("greeterFollowLockScreen", false); } function resetLockScreenWidgets() { for (const inst of lockScreenWidgetInstances || []) SessionData.removeDesktopWidgetInstancePositions(inst.id); for (const inst of Spec.SPEC.lockScreenWidgetInstances.def) SessionData.removeDesktopWidgetInstancePositions(inst.id); resetToDefault(["lockScreenWidgetInstances"]); } // Following means the lock layout is the greeter layout, so that is what resets. function resetGreeterWidgets() { for (const inst of greeterWidgetInstances || []) { if (!greeterFollowLockScreen || inst.widgetType === "greeterSession") SessionData.removeDesktopWidgetInstancePositions(inst.id); } if (greeterFollowLockScreen) { resetLockScreenWidgets(); set("greeterWidgetInstances", Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, [])); return; } set("greeterWidgetInstances", Spec.greeterWidgetDefaults().map(inst => inst.widgetType === "greeterSession" ? inst : Object.assign(inst, { id: "gw_" + inst.id }))); } function getDefaultSystemMonitorConfig() { return { showHeader: true, transparency: 0.8, colorMode: "primary", customColor: "#ffffff", showCpu: true, showCpuGraph: true, showCpuTemp: true, showGpuTemp: false, gpuPciId: "", showMemory: true, showMemoryGraph: true, showNetwork: true, showNetworkGraph: true, showDisk: true, showTopProcesses: false, topProcessCount: 3, topProcessSortBy: "cpu", layoutMode: "auto", graphInterval: 60, x: -1, y: -1, width: 320, height: 480, displayPreferences: ["all"] }; } function widgetInstanceListKey(instanceId) { return widgetInstanceListKeys.find(key => (root[key] || []).some(inst => inst.id === instanceId)) ?? "desktopWidgetInstances"; } readonly property var widgetInstanceIdPrefixes: ({ desktopWidgetInstances: "dw_", lockScreenWidgetInstances: "lw_", greeterWidgetInstances: "gw_" }) function createDesktopWidgetInstance(widgetType, name, config, listKey = "desktopWidgetInstances") { const lockScreen = listKey !== "desktopWidgetInstances"; const instance = { id: widgetInstanceIdPrefixes[listKey] + Date.now() + "_" + Math.random().toString(36).substr(2, 9), widgetType: widgetType, name: name || widgetType, enabled: true, config: Object.assign(lockScreen ? { syncPositionAcrossScreens: true } : {}, config || {}) }; const instances = JSON.parse(JSON.stringify(root[listKey] || [])); instances.push(instance); root[listKey] = instances; saveSettings(); return instance; } function updateDesktopWidgetInstance(instanceId, updates) { const listKey = widgetInstanceListKey(instanceId); const instances = JSON.parse(JSON.stringify(root[listKey] || [])); const idx = instances.findIndex(inst => inst.id === instanceId); if (idx === -1) return; Object.assign(instances[idx], updates); root[listKey] = instances; saveSettings(); } function updateDesktopWidgetInstanceConfig(instanceId, configUpdates) { const listKey = widgetInstanceListKey(instanceId); const instances = JSON.parse(JSON.stringify(root[listKey] || [])); const idx = instances.findIndex(inst => inst.id === instanceId); if (idx === -1) return; instances[idx].config = Object.assign({}, instances[idx].config || {}, configUpdates); root[listKey] = instances; saveSettings(); } function removeDesktopWidgetInstance(instanceId) { const listKey = widgetInstanceListKey(instanceId); root[listKey] = (root[listKey] || []).filter(inst => inst.id !== instanceId); SessionData.removeDesktopWidgetInstancePositions(instanceId); saveSettings(); } function duplicateDesktopWidgetInstance(instanceId) { const source = getDesktopWidgetInstance(instanceId); if (!source) return null; const instance = createDesktopWidgetInstance(source.widgetType, source.name + " (Copy)", JSON.parse(JSON.stringify(source.config || {})), widgetInstanceListKey(instanceId)); if (!source.enabled) updateDesktopWidgetInstance(instance.id, { enabled: false }); return instance; } function getDesktopWidgetInstance(instanceId) { for (const key of widgetInstanceListKeys) { const found = (root[key] || []).find(inst => inst.id === instanceId); if (found) return found; } return null; } function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) { const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); const groups = desktopWidgetGroups || []; const idx = instances.findIndex(inst => inst.id === instanceId); if (idx === -1) return false; const [item] = instances.splice(idx, 1); item.group = groupId || null; const groupMatches = inst => { if (!groupId) return !inst.group || !groups.some(g => g.id === inst.group); return inst.group === groupId; }; const groupInstances = instances.filter(groupMatches); const clamped = Math.max(0, Math.min(newIndexInGroup, groupInstances.length)); let targetGlobalIdx; if (clamped >= groupInstances.length) { const last = groupInstances[groupInstances.length - 1]; targetGlobalIdx = last ? instances.findIndex(inst => inst.id === last.id) + 1 : instances.length; } else { const targetInstance = groupInstances[clamped]; targetGlobalIdx = instances.findIndex(inst => inst.id === targetInstance.id); } instances.splice(targetGlobalIdx, 0, item); desktopWidgetInstances = instances; saveSettings(); return true; } function createDesktopWidgetGroup(name) { const id = "dwg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); const group = { id: id, name: name, collapsed: false }; const groups = JSON.parse(JSON.stringify(desktopWidgetGroups || [])); groups.push(group); desktopWidgetGroups = groups; saveSettings(); return group; } function updateDesktopWidgetGroup(groupId, updates) { const groups = JSON.parse(JSON.stringify(desktopWidgetGroups || [])); const idx = groups.findIndex(g => g.id === groupId); if (idx === -1) return; Object.assign(groups[idx], updates); desktopWidgetGroups = groups; saveSettings(); } function removeDesktopWidgetGroup(groupId) { const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); for (let i = 0; i < instances.length; i++) { if (instances[i].group === groupId) instances[i].group = null; } desktopWidgetInstances = instances; const groups = (desktopWidgetGroups || []).filter(g => g.id !== groupId); desktopWidgetGroups = groups; saveSettings(); } signal forceDankBarLayoutRefresh signal forceDockLayoutRefresh signal widgetDataChanged signal workspaceIconsUpdated signal compositorLayoutRefreshNeeded(bool frame) signal blurStrengthRefreshNeeded signal compositorInputRefreshNeeded signal compositorCursorRefreshNeeded signal notificationPopupsInvalidated function refreshAuthAvailability() { if (isGreeterMode) return; Processes.detectAuthCapabilities(); } function _runStartSequence() { Processes.settingsRoot = root; const unsaved = _loadSettings(); initializeListModels(); return unsaved; } function applyStoredTheme() { if (typeof Theme !== "undefined") { Theme.currentThemeCategory = currentThemeCategory; Theme.switchTheme(currentThemeName, false, false); } else { Qt.callLater(function () { if (typeof Theme !== "undefined") { Theme.currentThemeCategory = currentThemeCategory; Theme.switchTheme(currentThemeName, false, false); } }); } } function regenSystemThemes() { if (typeof Theme !== "undefined") { Theme.generateSystemThemesFromCurrentTheme(); } } function updateCompositorLayout() { compositorLayoutRefreshNeeded(false); } function updateCompositorInput() { compositorInputRefreshNeeded(); } function updateBlurStrength() { blurStrengthRefreshNeeded(); } function updateFrameCompositorLayout() { compositorLayoutRefreshNeeded(true); FrameTransitionState.begin(); } function resolveIconTheme() { if (iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode) return iconThemeLight; return iconThemeDark; } function applyStoredIconTheme() { updateGtkIconTheme(); updateQtIconTheme(); updateCosmicIconTheme(); } function setIconThemeUnmanaged() { iconThemePerMode = false; iconThemeDark = "System Default"; iconThemeLight = "System Default"; SessionData.lastAppliedIconTheme = ""; SessionData.saveSettings(); saveSettings(); } function checkIconThemeDrift() { if (isGreeterMode) return; if (resolveIconTheme() === "System Default") return; if (!SessionData.lastAppliedIconTheme) return; const script = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme"); Proc.runCommand("iconThemeDriftCheck", ["sh", "-c", script], (output, exitCode) => { const platform = (output || "").trim(); if (!platform) return; if (platform === SessionData.lastAppliedIconTheme || platform === root.iconThemeDark || platform === root.iconThemeLight) return; root.setIconThemeUnmanaged(); ToastService.showWarning(I18n.tr("Icon theme changed outside DMS; switched to System Default", "shown when an external tool overrides the icon theme DMS applied")); }); } Connections { target: Quickshell function onScreensChanged() { if (!root._loading) Qt.callLater(root._reconcileConnectedFrameBarStyles); } } Connections { target: typeof SessionData !== "undefined" ? SessionData : null function onIsLightModeChanged() { if (!SessionData.isSwitchingMode) return; if (!root.iconThemePerMode) return; if (root.iconThemeLight === root.iconThemeDark) return; root.applyStoredIconTheme(); root.saveSettings(); } } function cosmicIntegrationAvailable() { const desktop = (Quickshell.env("XDG_CURRENT_DESKTOP") || "").toUpperCase(); return desktop.includes("COSMIC"); } function updateCosmicIconTheme() { if (!cosmicIntegrationAvailable()) return; const resolved = resolveIconTheme(); let cosmicThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved; if (!cosmicThemeName || cosmicThemeName === "System Default") { const detectScript = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme"); Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => { if (exitCode !== 0) return; const detected = (output || "").trim(); if (!detected || detected === "System Default") return; const detectedEscaped = detected.replace(/'/g, "'\\''"); const writeScript = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1 printf '"%s"\\n' '${detectedEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`; Quickshell.execDetached(["sh", "-lc", writeScript]); }); return; } const cosmicThemeNameEscaped = cosmicThemeName.replace(/'/g, "'\\''"); const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1 printf '"%s"\\n' '${cosmicThemeNameEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`; Quickshell.execDetached(["sh", "-lc", script]); } function updateCosmicThemeMode(isLightMode) { if (!cosmicIntegrationAvailable()) return; const isDark = isLightMode ? "false" : "true"; const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1 printf '%s\\n' ${isDark} > ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark 2>/dev/null || true`; Quickshell.execDetached(["sh", "-lc", script]); } function updateGtkIconTheme() { const resolved = resolveIconTheme(); const gtkThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved; if (gtkThemeName === "System Default" || gtkThemeName === "") return; SessionData.lastAppliedIconTheme = gtkThemeName; SessionData.saveSettings(); if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") { PortalService.setSystemIconTheme(gtkThemeName); } const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0 for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do settings_file="$config_dir/settings.ini" [ -f "$settings_file" ] && [ ! -w "$settings_file" ] && continue if [ -f "$settings_file" ]; then if grep -q "^gtk-icon-theme-name=" "$settings_file"; then sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file" else if grep -q "\\[Settings\\]" "$settings_file"; then sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file" else echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file" fi fi else echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file" fi done ${GSettings.setCmd("org.gnome.desktop.interface", "icon-theme", gtkThemeName)} || true pkill -HUP -f 'gtk' 2>/dev/null || true`; Quickshell.execDetached(["sh", "-lc", configScript]); } function updateQtIconTheme() { const resolved = resolveIconTheme(); const qtThemeName = (resolved === "System Default") ? "" : resolved; if (!qtThemeName) return; const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''"); const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''"); const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true update_qt_icon_theme() { local config_file="$1" local theme_name="$2" if [ -f "$config_file" ]; then if grep -q "^\\[Appearance\\]" "$config_file"; then if grep -q "^icon_theme=" "$config_file"; then sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file" else sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file" fi else printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file" fi else printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file" fi } update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}' update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'`; Quickshell.execDetached(["sh", "-lc", script]); if (!qtengineActive || !runDmsMatugenTemplates || !matugenTemplateQtengine) return; Proc.runCommand("updateQtengineIconTheme", [Proc.dmsBin, "matugen", "qtengine", "--icon-theme", qtThemeName], () => {}); } function scheduleAuthApply() { if (isGreeterMode) return; Qt.callLater(() => { Processes.settingsRoot = root; Processes.scheduleAuthApply(); }); } function scheduleGreeterAutoLoginSync() { if (isGreeterMode) return; Qt.callLater(() => { Processes.settingsRoot = root; Processes.scheduleGreeterAutoLoginSync(); }); } function syncLauncherHistory(who, key) { if (who[key]) return; AppUsageHistoryData.clear(); SessionData.clearLauncherHistory(); } function markGreeterSyncPending(who, key, oldValue) { if (isGreeterMode) return; if (!(key in SessionData.greeterSyncBaseline)) { var baseline = Object.assign({}, SessionData.greeterSyncBaseline); baseline[key] = oldValue; SessionData.greeterSyncBaseline = baseline; } SessionData.greeterSyncPending = true; SessionData.saveSettings(); } // Older builds flagged keys the linked slot now serves live; drop them so Apply does not nag forever. function pruneGreeterSyncPending() { const baseline = SessionData.greeterSyncBaseline || {}; const keys = Object.keys(baseline); const live = keys.filter(key => Spec.SPEC[key]?.onChange === "markGreeterSyncPending"); if (live.length === keys.length) return; const pruned = {}; for (const key of live) pruned[key] = baseline[key]; SessionData.greeterSyncBaseline = pruned; if (live.length === 0) SessionData.greeterSyncPending = false; SessionData.saveSettings(); } function clearGreeterSyncPending() { SessionData.greeterSyncBaseline = {}; SessionData.greeterSyncPending = false; SessionData.saveSettings(); } function revertGreeterSyncPending() { for (var key in SessionData.greeterSyncBaseline) { if (!(key in Spec.SPEC)) continue; root[key] = SessionData.greeterSyncBaseline[key]; } SessionData.greeterSyncBaseline = {}; SessionData.greeterSyncPending = false; SessionData.saveSettings(); saveSettings(); } readonly property var _hooks: ({ "applyStoredTheme": applyStoredTheme, "regenSystemThemes": regenSystemThemes, "updateCompositorLayout": updateCompositorLayout, "updateCompositorInput": updateCompositorInput, "updateBlurStrength": updateBlurStrength, "applyStoredIconTheme": applyStoredIconTheme, "updateBarConfigs": updateBarConfigs, "updateCompositorCursor": updateCompositorCursor, "scheduleAuthApply": scheduleAuthApply, "scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync, "markGreeterSyncPending": markGreeterSyncPending, "syncLauncherHistory": syncLauncherHistory }) function set(key, value) { if (key === "cornerRadius") { setCornerRadius(value); return; } if (key === "radiusStrength") value = Shape.normalizeStrength(value); Spec.set(root, key, value, saveSettings, _hooks); if (key === "frameEnabled" && value) clearIslandBars(); } function hasSetting(key) { return key in Spec.SPEC; } function specDefault(key) { return SpecUtil.cloneDef(Spec.SPEC[key]?.def); } function isDefault(keys) { return keys.every(key => !(key in Spec.SPEC) || SpecUtil.isDefault(root[key], Spec.SPEC[key].def)); } function resetToDefault(keys) { for (const key of keys) { if (key in Spec.SPEC) Spec.set(root, key, null, saveSettings, _hooks); } } function osdPositionFor(kind) { const override = osdPositionOverrides?.[kind]; return typeof override === "number" ? override : osdPosition; } function hasOsdPositionOverride(kind) { return typeof osdPositionOverrides?.[kind] === "number"; } function setOsdPosition(kind, position) { if (!kind) { set("osdPosition", position); return; } if (position === osdPosition) { resetOsdPosition(kind); return; } set("osdPositionOverrides", Object.assign({}, osdPositionOverrides, { [kind]: position })); } function resetOsdPosition(kind) { if (!kind) { resetToDefault(["osdPosition"]); return; } if (!hasOsdPositionOverride(kind)) return; const next = Object.assign({}, osdPositionOverrides); delete next[kind]; set("osdPositionOverrides", next); } function barConfigDefault(field) { return Spec.SPEC.barConfigs.def[0][field]; } function dockConfigDefaults() { return DockConfig.create("", ""); } // Frame mode hosts bars inside the frame surface, which has nowhere to put an edge island; a dot floats over it. function clearIslandBars() { if (!islandBarConfigs.some(cfg => !isDotBarConfig(cfg))) return; const configs = JSON.parse(JSON.stringify(barConfigs)); for (const cfg of configs) delete cfg.island; _commitBarConfigs(configs); } function _loadSettings() { const isInitial = !_hasLoaded || _settingsStage === SettingsData.Stage.Partial; let unsavedChanges; let obj = _getSettingsObjectFromFiles(); let loadedSettings = JSON.stringify(obj); const oldVersion = obj?.configVersion ?? 0; if (oldVersion > settingsConfigVersion && !isGreeterMode) { newerSettingsBackup.path = `${_configDir}/dms/settings.v${oldVersion}.json`; newerSettingsBackup.setText(JSON.stringify(obj, null, 2)); } const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null; const sessionPayload = oldVersion < 15 ? Store.extractSessionPayload(obj) : null; const cachePayload = oldVersion < 15 ? Store.extractCachePayload(obj) : null; let migrationRan = false; if (oldVersion < settingsConfigVersion) { const migrated = Store.migrateToVersion(obj, settingsConfigVersion); if (migrated) { obj = migrated; migrationRan = true; } } if (legacyPins) { Qt.callLater(() => CacheData.migratePins(legacyPins)); } if (cachePayload) { Qt.callLater(() => CacheData.migrateUsageHistories(cachePayload)); } if (sessionPayload) { Qt.callLater(() => { SessionData.importFromSettings(sessionPayload); _mergeSessionState(); }); } if (obj?.lockScreenActiveMonitor !== undefined) { var oldVal = obj.lockScreenActiveMonitor; if (oldVal && oldVal !== "all") { if (!obj.screenPreferences) { obj.screenPreferences = {}; } if (obj.screenPreferences.lockScreen === undefined) { obj.screenPreferences.lockScreen = [oldVal]; } } delete obj.lockScreenActiveMonitor; } if (obj?.use24HourClock !== undefined && obj?.clockFormat === undefined) { obj.clockFormat = obj.use24HourClock ? "24h" : "12h"; delete obj.use24HourClock; } const prevFrameEnabled = frameEnabled; const prevFrameMode = frameMode; unsavedChanges = loadedSettings !== JSON.stringify(obj); Store.parse(root, obj); // set() enforces this pair, but a hand-edited settings bypass set() entirely. if (frameEnabled) clearIslandBars(); if (obj?.directionalAnimationMode === 3 && frameMode !== "connected") frameMode = "connected"; if (obj?.iconTheme !== undefined && obj?.iconThemeDark === undefined) iconThemeDark = obj.iconTheme; if (obj?.weatherLocation !== undefined) _legacyWeatherLocation = obj.weatherLocation; if (obj?.weatherCoordinates !== undefined) _legacyWeatherCoordinates = obj.weatherCoordinates; if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") { _legacyVpnLastConnected = obj.vpnLastConnected; SessionData.vpnLastConnected = _legacyVpnLastConnected; SessionData.saveSettings(); } _hasLoaded = true; if (!_parseError) { _settingsStage = SettingsData.Stage.Ready; } if (isInitial) { _mergeSessionState(); Qt.callLater(checkIconThemeDrift); } applyStoredTheme(); updateCompositorCursor(); if (!isInitial) { // External edits reload under _loading, which skips the per-property transition triggers const frameChanged = (frameEnabled !== prevFrameEnabled || (frameEnabled && frameMode !== prevFrameMode)); if (!_parseError && frameChanged) { updateFrameCompositorLayout(); } } if (isInitial && _settingsStage === SettingsData.Stage.Ready) { loadPluginSettings(); Qt.callLater(() => _reconcileConnectedFrameBarStyles()); } return unsavedChanges; } function _mergeSessionState() { if (!_hasLoaded || !SessionData._hasLoaded) return; pruneGreeterSyncPending(); const pluginState = SessionData.builtInPluginState || {}; if (Object.keys(pluginState).length > 0) { const updated = JSON.parse(JSON.stringify(builtInPluginSettings)); for (const id in pluginState) { updated[id] = pluginState[id]; } builtInPluginSettings = updated; } } Connections { target: SessionData function onLoaded() { root._mergeSessionState(); } } function _getCurrentSettings() { const setKeys = new Set(); for (const path of _settingsFilesPaths) { const file = _settingsFiles.get(path); for (const key in file.settings) { setKeys.add(key); } } return Store.toJson(root, setKeys); } function getCurrentSettingsJson() { return JSON.stringify(_getCurrentSettings(), null, 2); } function _resetPluginSettings() { _pluginParseError = false; pluginSettings = {}; } function _pluginSettingsErrorCode(error) { if (typeof error === "number") return error; if (error && typeof error === "object") { if (typeof error.code === "number") return error.code; if (typeof error.errno === "number") return error.errno; } const msg = String(error || "").trim(); if (/^\d+$/.test(msg)) return Number(msg); return -1; } function _isMissingPluginSettingsError(error) { if (_pluginSettingsErrorCode(error) === 2) return true; const msg = String(error || "").toLowerCase(); return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1; } function loadPluginSettings() { try { parsePluginSettings(pluginSettingsFile.text()); } catch (e) { const msg = e.message || String(e); if (!_isMissingPluginSettingsError(e)) log.warn("Failed to load plugin_settings.json. Error:", msg); _resetPluginSettings(); } } function parsePluginSettings(content) { _pluginSettingsLoading = true; _pluginParseError = false; try { if (content && content.trim()) { pluginSettings = JSON.parse(content); } else { pluginSettings = {}; } } catch (e) { _pluginParseError = true; const msg = e.message; log.error("Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("plugin_settings.json"), msg)); pluginSettings = {}; } finally { _pluginSettingsLoading = false; } } function _splitSettingsByFile(settings) { const savedSettings = new Set(); const splitSettings = {}; for (let i = _settingsFilesPaths.length - 1; i >= 0; i--) { const path = _settingsFilesPaths[i]; const file = _settingsFiles.get(path); const fileSettings = file.getSettings(); for (const setting in fileSettings) { if (!(setting in settings)) { delete fileSettings[setting]; continue; } if (savedSettings.has(setting)) { continue; } fileSettings[setting] = settings[setting]; savedSettings.add(setting); } splitSettings[path] = fileSettings; } for (const setting in settings) { if (!savedSettings.has(setting)) { splitSettings[defaultSettingsFile.filePath][setting] = settings[setting]; } } return splitSettings; } function saveSettings() { if (isGreeterMode || _loading || !_hasLoaded) return; unsavedUserChanges = true; settingsSaveDebounce.restart(); } function _saveSettings(userChanges) { let reason = null; if (isGreeterMode) { reason = "running in greeter mode."; } else if (_loading) { reason = "some files are being loaded"; } else if (_parseError) { reason = "failed to parse settings."; } if (reason !== null) { log.warn("Refusing to save settings, recent changes may be lost: " + reason); return; } const settings = _getCurrentSettings(); const splitSettings = _splitSettingsByFile(settings); for (const path in splitSettings) { const fileSettings = splitSettings[path]; const file = _settingsFiles.get(path); file.setSettings(fileSettings, userChanges); } unsavedUserChanges = _anySettingsFile(file => file.fileUnsavedUserChanges); } function savePluginSettings() { if (isGreeterMode || _pluginSettingsLoading || _pluginParseError) return; pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2)); } function _connectedFrameBarStyleSnapshot(config) { return { "shadowIntensity": config?.shadowIntensity ?? 0, "squareCorners": config?.squareCorners ?? false, "attachToScreenEdge": config?.attachToScreenEdge ?? false, "gothCornersEnabled": config?.gothCornersEnabled ?? false, "borderEnabled": config?.borderEnabled ?? false }; } function barUsesConnectedFrameStyle(config) { if (!config || !connectedFrameModeActive) return false; return Quickshell.screens.some(screen => barConfigCoversScreen(config, screen) && isScreenInPreferences(screen, frameScreenPreferences)); } function _applyConnectedFrameBarStyles(configs) { if (!Array.isArray(configs)) return { "configs": configs, "changed": false, "backups": null }; const backups = connectedFrameBarStyleBackups || {}; const nextBackups = {}; let backupsChanged = false; let anyChanged = false; const zeroed = _connectedFrameBarStyleSnapshot(null); const out = configs.map(cfg => { if (!cfg?.id) return cfg; const covered = barUsesConnectedFrameStyle(cfg); if (covered) { nextBackups[cfg.id] = backups[cfg.id] ?? _connectedFrameBarStyleSnapshot(cfg); backupsChanged = backupsChanged || backups[cfg.id] === undefined; } const target = covered ? zeroed : backups[cfg.id]; if (!target) return cfg; const current = _connectedFrameBarStyleSnapshot(cfg); if (Object.keys(target).every(key => current[key] === target[key])) return cfg; anyChanged = true; return Object.assign({}, cfg, target); }); const backupsDirty = backupsChanged || Object.keys(nextBackups).length !== Object.keys(backups).length; return { "configs": anyChanged ? out : configs, "changed": anyChanged, "backups": backupsDirty ? nextBackups : null }; } function effectiveBarConfigForRender(config, usesFrameBarChrome) { if (!config || !connectedFrameModeActive || usesFrameBarChrome) return config; const backup = connectedFrameBarStyleBackups[config.id]; if (!backup) return config; return Object.assign({}, config, backup); } function _reconcileConnectedFrameBarStyles() { if (Quickshell.screens.length === 0) return; const result = _applyConnectedFrameBarStyles(barConfigs); if (result.changed) barConfigs = result.configs; if (result.backups) connectedFrameBarStyleBackups = result.backups; if (result.changed) updateBarConfigs(); } function detectAvailableIconThemes() { const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || ""; const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)); const homeDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation)); const dataDirs = xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat([localData]) : ["/usr/share", "/usr/local/share", localData]; const iconPaths = dataDirs.map(d => d + "/icons").concat([homeDir + "/.icons"]); const pathsArg = iconPaths.join(" "); const script = ` echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})" for dir in ${pathsArg}; do [ -d "$dir" ] || continue for theme in "$dir"/*/; do [ -d "$theme" ] || continue basename "$theme" done done | grep -v '^icons$' | grep -v '^default$' | grep -v '^hicolor$' | grep -v '^locolor$' | sort -u `; Proc.runCommand("detectIconThemes", ["sh", "-c", script], (output, exitCode) => { const themes = ["System Default"]; if (output && output.trim()) { const lines = output.trim().split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith("SYSDEFAULT:")) { systemDefaultIconTheme = line.substring(11).trim(); continue; } if (line) themes.push(line); } } availableIconThemes = themes; }); } function detectAvailableCursorThemes() { const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || ""; const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)); const homeDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation)); const dataDirs = xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat([localData]) : ["/usr/share", "/usr/local/share", localData]; const cursorPaths = dataDirs.map(d => d + "/icons").concat([homeDir + "/.icons", homeDir + "/.local/share/icons"]); const pathsArg = cursorPaths.join(" "); const script = ` echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "cursor-theme")})" for dir in ${pathsArg}; do [ -d "$dir" ] || continue for theme in "$dir"/*/; do [ -d "$theme" ] || continue [ -d "$theme/cursors" ] || continue basename "$theme" done done | grep -v '^icons$' | grep -v '^default$' | sort -u `; Proc.runCommand("detectCursorThemes", ["sh", "-c", script], (output, exitCode) => { const themes = ["System Default"]; if (output && output.trim()) { const lines = output.trim().split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith("SYSDEFAULT:")) { systemDefaultCursorTheme = line.substring(11).trim(); continue; } if (line) themes.push(line); } } availableCursorThemes = themes; }); } function getEffectiveTimeFormat() { if (use24HourClock) return showSeconds ? "hh:mm:ss" : "hh:mm"; if (padHours12Hour) return showSeconds ? "hh:mm:ss AP" : "hh:mm AP"; return showSeconds ? "h:mm:ss AP" : "h:mm AP"; } function getEffectiveDateFormat(fallback) { if (clockDateFormat && clockDateFormat.length > 0) return clockDateFormat; return fallback || "ddd d"; } function initializeListModels() { const defaultBar = getPrimaryBarConfig(); if (defaultBar) { Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, defaultBar.leftWidgets, defaultBar.centerWidgets, defaultBar.rightWidgets); } } function updateListModel(listModel, order) { Lists.update(listModel, order); widgetDataChanged(); } function getPopupTriggerPosition(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig) { return ShellLayout.popupTrigger(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig); } function getAdjacentBarInfo(screen, barPosition, barConfig, includeAutoHide) { return ShellLayout.adjacentInfo(screen, barConfig, includeAutoHide); } function getBarBounds(screen, barThickness, barPosition, barConfig) { return ShellLayout.barBounds(screen, barThickness, barPosition, barConfig); } function updateBarConfigs() { barConfigsChanged(); saveSettings(); } // Each screen edge can hold one dock, so resolution and reservation are always edge-scoped. function dockConfigsForScreen(screen) { if (typeof screen === "string") screen = Quickshell.screens.find(s => s.name === screen); if (!screen) return []; return DockConfig.resolveAll(dockConfigs, screen, Quickshell.screens, (config, target) => isScreenInPreferences(target, config.screenPreferences)).filter(config => !_barClaimsEdge(screen, config.position)); } function dockConfigForScreenEdge(screen, side) { return dockConfigsForScreen(screen).find(config => DockConfig.EDGES[config.position] === side) ?? null; } function dockConfigForScreen(screen) { return dockConfigsForScreen(screen)[0] ?? null; } function _barClaimsEdge(screen, position) { return barConfigs.some(bar => bar.enabled && (bar.visible ?? true) && bar.position === position && barConfigCoversScreen(bar, screen)); } function taskbarInsetForEdge(screen, side) { const config = dockConfigForScreenEdge(screen, side); if (!config?.enabled || config.mode !== "taskbar") return 0; const frameInset = !CompositorService.frameWindowVisibleForScreen(screen) ? 0 : !config.useOverlayLayer && CompositorService.usesConnectedFrameChromeForScreen(screen) ? frameEdgeReservation(screen, side) : frameThickness; return DockConfig.effectiveThickness(config) + frameInset; } function dockReservationForEdge(screen, side) { const config = dockConfigForScreenEdge(screen, side); if (!config?.enabled || config.autoHide || config.smartAutoHide) return 0; return Math.max(0, DockConfig.effectiveThickness(config) + (config.mode === "taskbar" || (!config.useOverlayLayer && CompositorService.usesConnectedFrameChromeForScreen(screen)) ? 0 : config.margin + config.bottomGap)); } function _screensCoveredBy(preferences) { return Quickshell.screens.filter(screen => preferences.includes("all") || isScreenInPreferences(screen, preferences)); } // A dock only clashes with something sharing both a screen and an edge; other edges stay free. function dockAssignmentConflict(id, preferences, position) { const edge = position ?? getDockConfig(id)?.position ?? SettingsData.Position.Bottom; const covered = _screensCoveredBy(preferences); for (const other of dockConfigs) { if (other.id === id || other.position !== edge || !other.enabled) continue; if (covered.some(screen => isScreenInPreferences(screen, other.screenPreferences) || other.screenPreferences.includes("all"))) return other.name; } for (const bar of barConfigs) { if (!bar.enabled || !(bar.visible ?? true) || bar.position !== edge) continue; if (covered.some(screen => barConfigCoversScreen(bar, screen))) return bar.name || bar.id; } return ""; } // Edges this configuration could still be moved to, with its current edge always offered. function dockAvailableEdges(id, preferences) { const current = getDockConfig(id)?.position; return [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].filter(edge => edge === current || !dockAssignmentConflict(id, preferences, edge)); } function dockConfigForAction(screen, selector) { if (selector) return getDockConfig(selector) || dockConfigs.find(config => config.name === selector) || null; if (typeof screen === "string") screen = Quickshell.screens.find(item => item.name === screen); const resolved = dockConfigForScreen(screen); if (resolved) return resolved; return dockConfigs.find(config => !config.screenPreferences.includes("all") && screen && isScreenInPreferences(screen, config.screenPreferences)) || dockConfigs.find(config => config.screenPreferences.includes("all")) || dockConfigs[0] || null; } function getDockConfig(id) { return dockConfigs.find(config => config.id === id) ?? null; } function updateDockConfig(id, updates) { const current = getDockConfig(id); if (!current) return; const next = DockConfig.normalize([Object.assign({}, current, updates, { id })])[0]; if (!next) return; dockConfigs = dockConfigs.map(config => config.id === id ? next : config); saveSettings(); } // First edge in `order` that no enabled bar or dock already holds on the covered displays. function firstFreeEdge(id, preferences, order) { return order.find(edge => !dockAssignmentConflict(id, preferences, edge)) ?? -1; } function firstFreeDockEdge(id, preferences) { return firstFreeEdge(id, preferences, [SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right, SettingsData.Position.Top]); } // A new dock shows up right away on every display when an edge is free for it. function createDockConfig() { const id = "dock_" + Date.now(); let name = I18n.tr("Dock"); let number = 2; while (dockConfigs.some(config => config.name === name)) name = I18n.tr("Dock") + " " + number++; const config = DockConfig.create(id, name); const edge = firstFreeDockEdge(id, config.screenPreferences); config.enabled = edge >= 0; config.position = Math.max(SettingsData.Position.Top, edge); dockConfigs = dockConfigs.concat([config]); saveSettings(); return id; } function removeDockConfig(id) { dockConfigs = dockConfigs.filter(config => config.id !== id); SessionData.removeDockPins(id); saveSettings(); } function updateDockWidget(id, instanceId, updates) { const config = getDockConfig(id); if (!config) return; updateDockConfig(id, { widgets: config.widgets.map(item => item.id === instanceId ? Object.assign({}, item, updates) : item) }); } // Pinning an app to a dock whose Apps row was removed would otherwise pin into nothing. function ensureDockApps(id) { const config = getDockConfig(id); if (!config) return; const apps = config.widgets.find(item => item.widgetId === "appsDock"); if (apps && apps.enabled !== false) return; if (apps) { updateDockWidget(id, apps.id, { enabled: true }); return; } updateDockConfig(id, { widgets: config.widgets.concat([ { id: id + "_apps", widgetId: "appsDock", enabled: true } ]) }); } function getBarConfig(barId) { return barConfigs.find(cfg => cfg.id === barId) || null; } function barTransparency(config) { if (config?.followInterfaceStyle !== false) return popupTransparency; return config?.transparency ?? 1.0; } function barWidgetTransparency(config) { if (config?.widgetFollowInterfaceStyle !== false) return Theme.foregroundAlpha; return config?.widgetTransparency ?? 1.0; } function barSurfaceColor(config) { return Theme.surfaceRoleColor(config?.surfaceColor, config?.surfaceCustomColor, Theme.hostSurface); } function widgetOption(widgetType, data, key) { return WidgetDefaults.option(widgetType, data, key); } function widgetDefaults(widgetType) { return WidgetDefaults.DEFAULTS[widgetType] ?? {}; } function barWidgetEntry(barConfig, widgetType) { if (!barConfig) return null; for (const listKey of ["leftWidgets", "centerWidgets", "rightWidgets"]) { for (const entry of barConfig[listKey] ?? []) { const id = typeof entry === "string" ? entry : (entry.id ?? entry.widgetId); if (id !== widgetType) continue; return typeof entry === "string" ? { "id": entry } : entry; } } return null; } function addBarWidget(barId, sectionId, widgetId) { const config = getBarConfig(barId); if (!config) return -1; const entry = { "id": widgetId, "enabled": true }; switch (widgetId) { case "spacer": entry.size = 20; break; case "gpuTemp": entry.selectedGpuIndex = 0; entry.pciId = ""; break; case "diskUsage": entry.mountPath = "/"; break; } const listKey = sectionId + "Widgets"; const list = (config[listKey] ?? []).slice(); if (widgetId === "island") { if (isIslandBarConfig(config) || islandWidgetBlocked(config) || _islandWidgetLocation(config)) return -1; const configs = JSON.parse(JSON.stringify(barConfigs)); const target = configs.find(cfg => cfg.id === barId); list.push(entry); target[listKey] = list; _commitIslandOwner(configs, target); return list.length - 1; } list.push(entry); const patch = {}; patch[listKey] = list; updateBarConfig(barId, patch); return list.length - 1; } function locateBarWidget(widgetId, preferredBarId) { const ordered = barConfigs.slice().sort((a, b) => (b.id === preferredBarId) - (a.id === preferredBarId)); for (const config of ordered) { for (const sectionId of ["left", "center", "right"]) { const list = config[sectionId + "Widgets"] ?? []; for (let i = 0; i < list.length; i++) { const id = typeof list[i] === "string" ? list[i] : list[i].id; if (id === widgetId) return { "barId": config.id, "section": sectionId, "index": i }; } } } return null; } function updateBarWidget(barId, sectionId, index, updates) { const config = getBarConfig(barId); const listKey = sectionId + "Widgets"; const list = (config?.[listKey] ?? []).slice(); if (index < 0 || index >= list.length) return; const entry = typeof list[index] === "string" ? { "id": list[index], "enabled": true } : Object.assign({}, list[index]); Object.assign(entry, updates); list[index] = entry; const patch = {}; patch[listKey] = list; updateBarConfig(barId, patch); } function setBarIsland(barId, on, dot) { const config = getBarConfig(barId); const wantIsland = on === true && dot !== true; const wantDot = on === true && dot === true; if (!config || ((config.island === true) === wantIsland && (config.dot === true) === wantDot)) return; const updates = { island: wantIsland, dot: wantDot }; const configs = JSON.parse(JSON.stringify(barConfigs)); const target = Object.assign(configs.find(cfg => cfg.id === barId), updates); if (on !== true) { _commitBarConfigs(configs); return; } if (!config.enabled) target.enabled = true; // A hidden island would draw but leave routing, with no Visibility card and no `bar reveal` to bring it back. if (config.visible === false) target.visible = true; if ((config.screenPreferences ?? []).length === 0) target.screenPreferences = ["all"]; _stashIslandWidget(target); if (wantIsland) _evictIslandWidgets(configs, target); _commitBarConfigs(configs); } function isBarIpcRevealed(barId) { if (!barId) return false; return !!barIpcRevealStates[barId]; } function setBarIpcReveal(barId, revealed) { if (!barId || isIslandBarConfig(getBarConfig(barId))) return; const nextRevealed = !!revealed; if (!!barIpcRevealStates[barId] === nextRevealed) return; const states = Object.assign({}, barIpcRevealStates); if (nextRevealed) { states[barId] = true; } else { delete states[barId]; } barIpcRevealStates = states; } function toggleBarIpcReveal(barId) { const revealed = !isBarIpcRevealed(barId); setBarIpcReveal(barId, revealed); return revealed; } function addBarConfig(config) { const configs = JSON.parse(JSON.stringify(barConfigs)); configs.push(config); const result = _applyConnectedFrameBarStyles(configs); barConfigs = result.configs; if (result.backups) connectedFrameBarStyleBackups = result.backups; updateBarConfigs(); } function updateBarConfig(barId, updates) { const configs = JSON.parse(JSON.stringify(barConfigs)); const index = configs.findIndex(cfg => cfg.id === barId); if (index === -1) return; const positionChanged = updates.position !== undefined && configs[index].position !== updates.position; if (updates.autoHide === false || updates.visible === false) setBarIpcReveal(barId, false); Object.assign(configs[index], updates); _commitIslandOwner(configs, configs[index]); if (positionChanged) { notificationPopupsInvalidated(); } } function deleteBarConfig(barId) { if (barId === "default") return; const configs = barConfigs.filter(cfg => cfg.id !== barId); barConfigs = configs; if (connectedFrameBarStyleBackups?.[barId] !== undefined) { const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {})); delete nextBackups[barId]; connectedFrameBarStyleBackups = nextBackups; } setBarIpcReveal(barId, false); updateBarConfigs(); } function getBarKindConfigs() { return barConfigs.filter(cfg => !isIslandBarConfig(cfg)); } // Style and popup fallbacks never treat the island as "the bar". function getPrimaryBarConfig() { const bars = getBarKindConfigs(); if (bars.length > 0) return bars[0]; const fallback = getBarConfig("default"); return isIslandBarConfig(fallback) ? null : fallback; } function _sideToPosition(side) { return LayoutResolver.edges.indexOf(side); } function positionToSide(pos) { return LayoutResolver.edgeName(pos); } function barOccupiesSide(screen, side) { const position = _sideToPosition(side); return position >= 0 && (ShellLayout.forScreen(screen)?.bars.some(input => input.config.position === position) ?? false); } // Check if a dock occupies the specified screen edge. function dockOccupiesSide(screen, side) { const config = dockConfigForScreenEdge(screen, side); return !!config?.enabled; } function getScreenModelIndex(screen) { return LayoutResolver.screenModelIndex(screen, Quickshell.screens); } function getScreenDisplayName(screen) { if (!screen) return ""; if (displayNameMode === "model" && screen.model) { const modelIndex = getScreenModelIndex(screen); if (modelIndex >= 0) { return screen.model + "-" + modelIndex; } return screen.model; } return screen.name; } function isScreenInPreferences(screen, prefs) { return LayoutResolver.screenMatches(screen, prefs, Quickshell.screens, displayNameMode); } function getFilteredScreens(componentId) { var prefs = screenPreferences && screenPreferences[componentId] || ["all"]; if (componentId === "wallpaper" && Array.isArray(prefs) && prefs.length === 0) { return []; } if (!prefs || prefs.length === 0 || prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) { return Quickshell.screens; } var filtered = Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs)); if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) { return Quickshell.screens; } return filtered; } function barConfigCoversScreen(bc, screen) { return ShellLayout.coversScreen(bc, screen); } function isIslandBarConfig(bc) { return !!bc && (bc.island === true || bc.dot === true); } function islandWidgetEntry(bc) { const location = isIslandBarConfig(bc) ? null : _islandWidgetLocation(bc); if (!location) return null; const entry = bc[location.sectionId + "Widgets"][location.index]; return typeof entry === "string" ? { "id": entry } : entry; } function hostsIsland(bc) { return isIslandBarConfig(bc) || !!_islandWidgetLocation(bc, true); } function _sharesScreen(a, b) { return Quickshell.screens.some(screen => barConfigCoversScreen(a, screen) && barConfigCoversScreen(b, screen)); } function islandWidgetBlocked(bc) { if (!bc) return true; return (barConfigs || []).some(cfg => cfg.id !== bc.id && cfg.island === true && cfg.enabled !== false && _sharesScreen(cfg, bc)); } // Clones and dots drop the widget for good; an island-layout switch stashes it first. function stripIslandWidget(cfg) { delete cfg.islandWidgetStash; _stripIslandEntries(cfg); } function _stripIslandEntries(cfg) { for (const sectionId of ["left", "center", "right"]) { const key = sectionId + "Widgets"; if (Array.isArray(cfg[key])) cfg[key] = cfg[key].filter(entry => !_isIslandEntry(entry)); } } // An island-layout bar cannot host the widget, but switching back should not cost the user its placement and settings. function _stashIslandWidget(cfg) { const location = _islandWidgetLocation(cfg); if (location) cfg.islandWidgetStash = Object.assign({ "entry": cfg[location.sectionId + "Widgets"][location.index] }, location); _stripIslandEntries(cfg); } // A restore never evicts: while another island holds the screen the stash waits for a later commit to free it. function _restoreIslandWidget(cfg, configs) { const stash = cfg.islandWidgetStash; if (!stash?.entry) { delete cfg.islandWidgetStash; return; } if (isIslandBarConfig(cfg) || _islandWidgetLocation(cfg) || _islandScreenTaken(configs, cfg)) return; delete cfg.islandWidgetStash; const key = stash.sectionId + "Widgets"; const list = Array.isArray(cfg[key]) ? cfg[key].slice() : []; list.splice(Math.min(stash.index, list.length), 0, stash.entry); cfg[key] = list; } function _islandScreenTaken(configs, bc) { return configs.some(cfg => cfg.id !== bc.id && cfg.enabled !== false && (cfg.island === true || _islandWidgetLocation(cfg, true)) && _sharesScreen(cfg, bc)); } function _evictIslandWidgets(configs, owner) { for (const cfg of configs) { if (cfg.id !== owner.id && cfg.enabled !== false && _islandWidgetLocation(cfg, true) && _sharesScreen(cfg, owner)) _stashIslandWidget(cfg); } } // Island-relevant writes must go through here, or a freed screen never gets its stashed widget back. function _commitBarConfigs(configs) { for (const cfg of configs) _restoreIslandWidget(cfg, configs); const result = _applyConnectedFrameBarStyles(configs); barConfigs = result.configs; if (result.backups) connectedFrameBarStyleBackups = result.backups; updateBarConfigs(); } // The bar just written wins its screens: enabling a bar or its island entry evicts the previous host. function _commitIslandOwner(configs, cfg) { if (cfg.enabled !== false && !isIslandBarConfig(cfg) && _islandWidgetLocation(cfg, true)) _evictIslandWidgets(configs, cfg); _commitBarConfigs(configs); } function isDotBarConfig(bc) { return !!bc && bc.dot === true; } // The dot is a companion, not a layout: the first enable clones the base bar so it inherits its look. function setDotEnabled(enabled, baseId) { const existing = dotBarConfig; if (existing) { if ((existing.enabled ?? false) !== enabled) updateBarConfig(existing.id, { enabled }); return; } if (!enabled) return; const base = getBarConfig(baseId); const source = base && !isDotBarConfig(base) ? base : getBarConfig("default"); if (!source) return; const config = Object.assign(JSON.parse(JSON.stringify(source)), { id: "dot" + Date.now(), name: I18n.tr("Dot", "bar layout: free-floating dot that opens island activities"), enabled: true, island: false, dot: true, screenPreferences: ["all"], showOnLastDisplay: true, followInterfaceStyle: false, transparency: barTransparency(source) }); // An inherited "always here" would make the base island and the dot fight by config order. delete config.islandSharedRouting; stripIslandWidget(config); addBarConfig(config); } function islandFreePlacement(bc) { return isDotBarConfig(bc) || (isIslandBarConfig(bc) && islandSetting(bc, "islandFloating") && islandSetting(bc, "islandPlacement") === "free"); } // A hosted island shares its config id with the bar, so last-used cannot tell them apart and would degenerate into always. function islandSharedRoutingMode(bc) { const mode = islandSetting(bc, "islandSharedRouting"); if (mode === "always") return mode; return mode === "last-used" && isIslandBarConfig(bc) ? mode : "normal"; } // Dashboard activities share an IPC close, but choose their destinations independently. readonly property var islandDashActivities: ["home", "media", "weather", "wallpaper"] function islandRouteKey(activity) { switch (activity) { case "controlcenter": return "islandRouteControlCenter"; case "notificationcenter": return "islandRouteNotificationCenter"; case "launcher": return "islandRouteLauncher"; case "home": return "islandRouteDash"; case "media": return "islandRouteMedia"; case "weather": return "islandRouteWeather"; case "wallpaper": return "islandRouteWallpaper"; case "clipboard": return "islandRouteClipboard"; case "powermenu": return "islandRoutePowerMenu"; } return ""; } function islandActivityRoutingMode(bc, activity) { const key = islandRouteKey(activity); const override = key ? islandSetting(bc, key) : "follow"; if (override === "island") return "always"; if (override === "bar") return "never"; return islandSharedRoutingMode(bc); } function sharedShortcutsFollowLastUsed(screen) { return activeIslandConfigsForScreen(screen).some(cfg => islandSharedRoutingMode(cfg) === "last-used"); } function sharedShortcutsOverridden(screen, activity) { return activeIslandConfigsForScreen(screen).some(cfg => { const mode = islandActivityRoutingMode(cfg, activity); return mode === "always" || mode === "last-used"; }); } function recordBarInteraction(screen, barId) { const name = screen?.name; if (!name || !barId || lastUsedBarByScreen[name] === barId || !sharedShortcutsFollowLastUsed(screen)) return; lastUsedBarByScreen = Object.assign({}, lastUsedBarByScreen, { [name]: barId }); } // A satellite carrying the activity's own widget is a standard destination on this screen, same as a bar. function islandSatelliteHosts(cfg, activity) { const widgetId = activity === "controlcenter" ? "controlCenterButton" : activity === "notificationcenter" ? "notificationButton" : ""; if (!widgetId || !isIslandBarConfig(cfg) || isDotBarConfig(cfg) || !islandSetting(cfg, "islandSatellitesEnabled")) return false; return ["leftWidgets", "centerWidgets", "rightWidgets"].some(key => (cfg[key] ?? []).some(entry => (typeof entry === "string" ? entry : entry?.id) === widgetId && (typeof entry === "string" || entry.enabled !== false))); } function sharedTriggerIslandConfig(screen, activity) { const active = activeIslandConfigsForScreen(screen); const configs = active.filter(cfg => islandActivityRoutingMode(cfg, activity) !== "never"); const key = islandRouteKey(activity); const pinned = key ? configs.find(cfg => { // Saved choices outrank defaults, including legacy dashboard-family pins. const settings = islandSettings(cfg); const override = settings[key] ?? (islandDashActivities.indexOf(activity) >= 0 ? settings.islandRouteDash : undefined); return override === "island"; }) : null; if (pinned) return pinned; if (configs.some(cfg => islandActivityRoutingMode(cfg, activity) === "last-used")) { const lastId = lastUsedBarByScreen[screen?.name]; const lastIsland = configs.find(cfg => cfg.id === lastId); if (lastIsland) return lastIsland; } else { const fixed = configs.find(cfg => islandActivityRoutingMode(cfg, activity) === "always"); if (fixed) return fixed; } if (getActiveBarEdgesForScreen(screen).length > 0 || active.some(cfg => islandSatelliteHosts(cfg, activity))) return null; return configs.find(cfg => !isDotBarConfig(cfg)) ?? configs[0] ?? null; } function islandLauncherHostConfig(screen) { const config = sharedTriggerIslandConfig(screen, "launcher"); if (config || launcherStyle !== "island") return config; const configs = activeIslandConfigsForScreen(screen).filter(cfg => islandActivityRoutingMode(cfg, "launcher") !== "never"); return configs.find(cfg => !isDotBarConfig(cfg)) ?? configs[0] ?? null; } // "Always here" is exclusive per screen, or config order would silently pick the winner. function setIslandSharedRouting(barId, mode) { const configs = JSON.parse(JSON.stringify(barConfigs)); const target = configs.find(cfg => cfg.id === barId); if (!target || !(isIslandBarConfig(target) || _islandWidgetLocation(target))) return; _writableIslandSettings(target).islandSharedRouting = mode; if (mode === "always") { for (const cfg of configs) { if (cfg.id !== barId && hostsIsland(cfg) && islandSharedRoutingMode(cfg) === "always" && _sharesScreen(cfg, target)) delete _writableIslandSettings(cfg).islandSharedRouting; } } _commitBarConfigs(configs); } // A hidden bar takes its island with it, so it must not keep swallowing popups and OSDs. function activeIslandConfigsForScreen(screen) { return ShellLayout.islandConfigs(screen).filter(cfg => cfg.visible !== false); } function islandConfigForEdge(screen, edge) { return ShellLayout.edge(screen, edge)?.island ?? null; } function dankIslandHandlesNotifications(screen) { return activeIslandConfigsForScreen(screen).some(cfg => !islandSetting(cfg, "islandNotificationPopups")); } function dankIslandHandlesSystemOsd(screen) { return activeIslandConfigsForScreen(screen).some(cfg => islandSetting(cfg, "islandSystemOsd")); } function dankIslandOwnsEdge(screen, edge) { return islandConfigForEdge(screen, edge) !== null; } function dankIslandEdgeOffset(screen, edge) { return ShellLayout.edge(screen, edge)?.islandThickness ?? 0; } function getActiveBarEdgesForScreen(screen) { return ShellLayout.barEdges(screen, false); } readonly property real frameBarContentGap: frameBarInsetPadding < 0 ? frameThickness : frameBarInsetPadding readonly property real frameBarContentGapExtra: Math.max(0, frameBarContentGap - frameThickness) function frameEdgeReservation(screen, edge) { return ShellLayout.frameReservation(screen, edge); } function frameEdgeInsetForSide(screen, side) { if (!frameEnabled) return 0; return frameEdgeReservation(screen, side); } function setMatugenScheme(scheme) { var normalized = scheme || "scheme-tonal-spot"; if (matugenScheme === normalized) return; set("matugenScheme", normalized); } function setMatugenSmartMode(enabled) { if (matugenSmartMode === enabled) return; set("matugenSmartMode", enabled); } function setMatugenSourceMode(mode) { var normalized = mode || "dominant"; if (matugenSourceMode === normalized) return; set("matugenSourceMode", normalized); } function setMatugenContrast(value) { if (matugenContrast === value) return; set("matugenContrast", value); } function setMatugenSeedColor(hex) { var normalized = /^#[0-9a-f]{6}$/i.test(hex || "") ? hex.toLowerCase() : ""; if (matugenSeedColor === normalized) return; set("matugenSeedColor", normalized); } function setMatugenSpec(spec) { var normalized = spec === "2025" || spec === "dms" ? spec : "2021"; if (matugenSpec === normalized) return; if (normalized !== "2021") { if (matugenContrast < 0) set("matugenContrast", 0); if (typeof Theme !== "undefined" && !Theme.getMatugenScheme(matugenScheme).spec2025) set("matugenScheme", "scheme-tonal-spot"); } set("matugenSpec", normalized); } function setMatugenTargetMonitor(monitorName) { if (matugenTargetMonitor === monitorName) return; set("matugenTargetMonitor", monitorName); } function setCornerRadius(radius) { set("radiusStrength", Shape.strengthFromRadius(radius)); } function setWeatherLocation(displayName, coordinates) { SessionData.setWeatherLocation(displayName, coordinates); } function setIconThemeForMode(themeName, light) { if (light) iconThemeLight = themeName; else iconThemeDark = themeName; applyStoredIconTheme(); saveSettings(); if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme(); } function setIconThemePerMode(enabled) { iconThemePerMode = enabled; applyStoredIconTheme(); saveSettings(); if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme(); } function setCursorTheme(themeName) { const updated = JSON.parse(JSON.stringify(cursorSettings)); if (updated.theme === themeName) return; updated.theme = themeName; cursorSettings = updated; saveSettings(); updateXResources(); updateCompositorCursor(); } function setCursorSize(size) { const updated = JSON.parse(JSON.stringify(cursorSettings)); if (updated.size === size) return; updated.size = size; cursorSettings = updated; saveSettings(); updateXResources(); updateCompositorCursor(); } // This solution for xwayland cursor themes is from the xwls discussion: // https://github.com/Supreeeme/xwayland-satellite/issues/104 // no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher function updateCompositorCursor() { compositorCursorRefreshNeeded(); } function updateXResources() { const homeDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation)); const xresourcesPath = homeDir + "/.Xresources"; const themeName = cursorSettings.theme === "System Default" ? systemDefaultCursorTheme : cursorSettings.theme; const size = cursorSettings.size || 24; if (!themeName) return; const script = ` xresources_file="${xresourcesPath}" [ -f "$xresources_file" ] && [ ! -w "$xresources_file" ] && exit 0 theme_name="${themeName}" cursor_size="${size}" current_theme="" current_size="" if [ -f "$xresources_file" ]; then current_theme=$(grep -E '^[[:space:]]*Xcursor\\.theme:' "$xresources_file" 2>/dev/null | sed 's/.*:[[:space:]]*//' | head -1) current_size=$(grep -E '^[[:space:]]*Xcursor\\.size:' "$xresources_file" 2>/dev/null | sed 's/.*:[[:space:]]*//' | head -1) fi [ "$current_theme" = "$theme_name" ] && [ "$current_size" = "$cursor_size" ] && exit 0 if [ -f "$xresources_file" ]; then cp "$xresources_file" "\${xresources_file}.backup$(date +%s)" fi temp_file="\${xresources_file}.tmp.$$" if [ -f "$xresources_file" ]; then grep -v '^[[:space:]]*Xcursor\\.theme:' "$xresources_file" | grep -v '^[[:space:]]*Xcursor\\.size:' > "$temp_file" 2>/dev/null || true else touch "$temp_file" fi echo "Xcursor.theme: $theme_name" >> "$temp_file" echo "Xcursor.size: $cursor_size" >> "$temp_file" mv "$temp_file" "$xresources_file" xrdb -merge "$xresources_file" 2>/dev/null || true `; Quickshell.execDetached(["sh", "-c", script]); } function getCursorEnvironment() { const isSystemDefault = cursorSettings.theme === "System Default"; const isDefaultSize = !cursorSettings.size || cursorSettings.size === 24; const themeName = isSystemDefault ? "" : cursorSettings.theme; const size = String(cursorSettings.size || 24); const env = {}; // Started from systemd the shell inherits no XCURSOR_SIZE from the compositor // XWayland children would size their cursor from the display instead if (!isDefaultSize || !Quickshell.env("XCURSOR_SIZE")) { env["XCURSOR_SIZE"] = size; env["HYPRCURSOR_SIZE"] = size; } if (themeName) { env["XCURSOR_THEME"] = themeName; env["HYPRCURSOR_THEME"] = themeName; } return env; } function setDankBarLeftWidgets(order) { const defaultBar = getPrimaryBarConfig(); if (defaultBar) { updateBarConfig(defaultBar.id, { "leftWidgets": order }); updateListModel(leftWidgetsModel, order); } } function setDankBarCenterWidgets(order) { const defaultBar = getPrimaryBarConfig(); if (defaultBar) { updateBarConfig(defaultBar.id, { "centerWidgets": order }); updateListModel(centerWidgetsModel, order); } } function setDankBarRightWidgets(order) { const defaultBar = getPrimaryBarConfig(); if (defaultBar) { updateBarConfig(defaultBar.id, { "rightWidgets": order }); updateListModel(rightWidgetsModel, order); } } function setWorkspaceNameIcon(workspaceName, iconData) { var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)); iconMap[workspaceName] = iconData; workspaceNameIcons = iconMap; saveSettings(); workspaceIconsUpdated(); } function removeWorkspaceNameIcon(workspaceName) { var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)); delete iconMap[workspaceName]; workspaceNameIcons = iconMap; saveSettings(); workspaceIconsUpdated(); } function getWorkspaceNameIcon(workspaceName) { return workspaceNameIcons[workspaceName] || null; } function addAppIdSubstitution(pattern, replacement, type) { var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); subs.push({ pattern: pattern, replacement: replacement, type: type }); appIdSubstitutions = subs; saveSettings(); } function updateAppIdSubstitution(index, pattern, replacement, type) { var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); if (index < 0 || index >= subs.length) return; subs[index] = { pattern: pattern, replacement: replacement, type: type }; appIdSubstitutions = subs; saveSettings(); } function removeAppIdSubstitution(index) { var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); if (index < 0 || index >= subs.length) return; subs.splice(index, 1); appIdSubstitutions = subs; saveSettings(); } function addAppIdToList(identity: string, appList: list): list { identity = identity ?? ""; appList = appList ?? []; if (!identity) return appList; var normalizedIdentity = Paths.normalizeAppId(identity); if (!normalizedIdentity) return appList; var cleanList = appList.map(id => id ? Paths.normalizeAppId(id) : "").filter(id => id !== ""); if (cleanList.includes(normalizedIdentity)) return cleanList; cleanList.push(normalizedIdentity); return cleanList; } function removeAppIdFromList(index: int, appList: list): list { var moddedList = appList ? appList.slice() : []; if (index < 0 || index >= moddedList.length) return moddedList; moddedList.splice(index, 1); return moddedList; } function addNightModeExcludedApp(identity: string) { var newList = addAppIdToList(identity, nightModeExcludedApps); nightModeExcludedApps = newList; saveSettings(); } function removeNightModeExcludedApp(index: int) { var newList = removeAppIdFromList(index, nightModeExcludedApps); nightModeExcludedApps = newList; saveSettings(); } function addMediaExcludePlayer(identity) { var newList = addAppIdToList(identity, mediaExcludePlayers); mediaExcludePlayers = newList; saveSettings(); } function removeMediaExcludePlayer(index) { var newList = removeAppIdFromList(index, mediaExcludePlayers); mediaExcludePlayers = newList; saveSettings(); } property var pendingNotificationRule: null function _newNotificationRule(overrides) { return Object.assign({ enabled: true, field: "appName", pattern: "", matchType: "contains", action: "default", urgency: "default", bypassDnd: false }, overrides || {}); } function addNotificationRule(ruleData) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); rules.push(_newNotificationRule(ruleData)); notificationRules = rules; saveSettings(); } function requestNotificationRuleForNotification(appName, desktopEntry) { const pattern = desktopEntry || appName || ""; pendingNotificationRule = _newNotificationRule(pattern ? { field: desktopEntry ? "desktopEntry" : "appName", pattern: pattern, matchType: "exact" } : {}); } function _isMuteRule(rule) { return (rule.action || "").toString().toLowerCase() === "mute"; } function _isDndBypassRule(rule) { return rule.bypassDnd === true; } // Timed mute rules carry an expiresAt timestamp (ms since epoch); 0 or // absent means the mute never expires. Only the mute lapses: a DND bypass // or urgency on the same rule stays in force, and // pruneExpiredNotificationRules() clears the lapsed mute from the // persisted rule. nowMs is optional (defaults to the current time); // reactive callers pass NotificationService.notificationRuleNowMs so // their bindings refresh as rules expire instead of freezing on a stale // Date.now(). function isNotificationRuleExpired(rule, nowMs) { return RuleExpiry.isRuleExpired(rule, nowMs); } // True while any rule carries an expiry timestamp. Gates the expiry // clock and sweeper in NotificationService so they only run when a // timed rule can actually expire. readonly property bool hasTimedNotificationRules: RuleExpiry.hasTimedRule(notificationRules) function _appRuleIndex(rules, appName, desktopEntry, predicate) { const app = (appName || "").toString().toLowerCase(); const desktop = (desktopEntry || "").toString().toLowerCase(); if (!app && !desktop) return -1; return rules.findIndex(rule => { if (!predicate(rule) || (rule.matchType || "contains").toString().toLowerCase() !== "exact" || (rule.conditions || []).length > 0) return false; const pattern = (rule.pattern || "").toString().toLowerCase(); const value = !rule.field || rule.field === "appName" ? app : rule.field === "desktopEntry" ? desktop : ""; return pattern !== "" && pattern === value; }); } function _addAppRule(appName, desktopEntry, overrides) { const pattern = desktopEntry || appName || ""; if (!pattern) return; var rules = JSON.parse(JSON.stringify(notificationRules || [])); const rule = _newNotificationRule(Object.assign({ field: desktopEntry ? "desktopEntry" : "appName", pattern: pattern, matchType: "exact" }, overrides)); if (!rule.expiresAt) delete rule.expiresAt; rules.push(rule); notificationRules = rules; saveSettings(); } function _hasNoAction(rule) { return (rule.action || "default").toString().toLowerCase() === "default"; } function _isNoopRule(rule) { return _hasNoAction(rule) && (rule.urgency || "default").toString().toLowerCase() === "default" && !_isDndBypassRule(rule); } // Edits the first enabled matching rule and drops it once it no longer does anything. function _updateAppRule(appName, desktopEntry, predicate, changes) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); const index = _appRuleIndex(rules, appName, desktopEntry, rule => rule.enabled !== false && predicate(rule)); if (index === -1) return false; const rule = Object.assign(rules[index], changes); if (!rule.expiresAt) delete rule.expiresAt; if (_isNoopRule(rule)) rules.splice(index, 1); notificationRules = rules; saveSettings(); return true; } function addMuteRuleForApp(appName, desktopEntry, expiresAt) { // Re-muting edits the app's existing no-action or mute rule (including an // expired one not yet swept) so duplicates never accumulate. const changes = { action: "mute", expiresAt: expiresAt || 0 }; if (!_updateAppRule(appName, desktopEntry, rule => _hasNoAction(rule) || _isMuteRule(rule), changes)) _addAppRule(appName, desktopEntry, changes); } function isAppMuted(appName, desktopEntry, nowMs) { return _appRuleIndex(notificationRules || [], appName, desktopEntry, rule => rule.enabled !== false && _isMuteRule(rule) && !isNotificationRuleExpired(rule, nowMs)) !== -1; } // ExpiresAt of the active mute rule for an app (ms since epoch), // or 0 when the app is not muted or the mute is permanent. function muteExpiresAt(appName, desktopEntry, nowMs) { const rules = notificationRules || []; const index = _appRuleIndex(rules, appName, desktopEntry, rule => rule.enabled !== false && _isMuteRule(rule) && !isNotificationRuleExpired(rule, nowMs)); return index === -1 ? 0 : (rules[index].expiresAt || 0); } // Clears lapsed timed mutes from the persisted list so the settings UI // stays truthful. Only the mute goes; the rule is dropped once nothing // else is left on it. The idle sweep must stay cheap, so nothing is // copied unless a rule actually expired. function pruneExpiredNotificationRules() { const rules = notificationRules || []; if (!rules.some(rule => isNotificationRuleExpired(rule))) return; const kept = []; for (const source of rules) { if (!isNotificationRuleExpired(source)) { kept.push(source); continue; } const rule = Object.assign({}, source, { action: "default" }); delete rule.expiresAt; if (!_isNoopRule(rule)) kept.push(rule); } notificationRules = kept; saveSettings(); } function removeMuteRuleForApp(appName, desktopEntry) { _updateAppRule(appName, desktopEntry, _isMuteRule, { action: "default", expiresAt: 0 }); } function isAppDndBypassed(appName, desktopEntry) { return _appRuleIndex(notificationRules || [], appName, desktopEntry, rule => rule.enabled !== false && _isDndBypassRule(rule)) !== -1; } function setAppDndBypass(appName, desktopEntry, enabled) { if (!enabled) { _updateAppRule(appName, desktopEntry, _isDndBypassRule, { bypassDnd: false }); return; } if (isAppDndBypassed(appName, desktopEntry)) return; if (!_updateAppRule(appName, desktopEntry, () => true, { bypassDnd: true })) _addAppRule(appName, desktopEntry, { bypassDnd: true }); } function updateNotificationRule(index, ruleData) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); if (index < 0 || index >= rules.length) return; var existing = rules[index] || {}; rules[index] = Object.assign({}, existing, ruleData || {}); notificationRules = rules; saveSettings(); } function updateNotificationRuleField(index, key, value) { if (key === undefined || key === null || key === "") return; var patch = {}; patch[key] = value; updateNotificationRule(index, patch); } function removeNotificationRule(index) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); if (index < 0 || index >= rules.length) return; rules.splice(index, 1); notificationRules = rules; saveSettings(); } function getDefaultNotificationRules() { return Spec.SPEC.notificationRules.def; } function resetNotificationRules() { notificationRules = JSON.parse(JSON.stringify(Spec.SPEC.notificationRules.def)); saveSettings(); } function getDefaultAppIdSubstitutions() { return Spec.SPEC.appIdSubstitutions.def; } function resetAppIdSubstitutions() { appIdSubstitutions = JSON.parse(JSON.stringify(Spec.SPEC.appIdSubstitutions.def)); saveSettings(); } function getRegistryThemeVariant(themeId, defaultVariant) { var stored = registryThemeVariants[themeId]; if (typeof stored === "string") return stored || defaultVariant || ""; return defaultVariant || ""; } function setRegistryThemeVariant(themeId, variantId) { var variants = JSON.parse(JSON.stringify(registryThemeVariants)); variants[themeId] = variantId; registryThemeVariants = variants; saveSettings(); if (typeof Theme !== "undefined") Theme.reloadCustomThemeVariant(); } function getRegistryThemeMultiVariant(themeId, defaults, mode) { var stored = registryThemeVariants[themeId]; if (!stored || typeof stored !== "object") return defaults || {}; if ((stored.dark && typeof stored.dark === "object") || (stored.light && typeof stored.light === "object")) { if (!mode) return stored.dark || stored.light || defaults || {}; var modeData = stored[mode]; if (modeData && typeof modeData === "object") return modeData; return defaults || {}; } return stored; } function setRegistryThemeMultiVariant(themeId, flavor, accent, mode) { var variants = JSON.parse(JSON.stringify(registryThemeVariants)); var existing = variants[themeId]; var perMode = {}; if (existing && typeof existing === "object") { if ((existing.dark && typeof existing.dark === "object") || (existing.light && typeof existing.light === "object")) { perMode = existing; } else if (typeof existing.flavor === "string") { perMode.dark = { flavor: existing.flavor, accent: existing.accent || "" }; } } perMode[mode || "dark"] = { flavor: flavor, accent: accent }; variants[themeId] = perMode; registryThemeVariants = variants; saveSettings(); if (typeof Theme !== "undefined") Theme.reloadCustomThemeVariant(); } function getPluginSetting(pluginId, key, defaultValue) { if (!pluginSettings[pluginId]) { return defaultValue; } return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue; } function setPluginSetting(pluginId, key, value) { const updated = JSON.parse(JSON.stringify(pluginSettings)); if (!updated[pluginId]) { updated[pluginId] = {}; } updated[pluginId][key] = value; pluginSettings = updated; savePluginSettings(); } function getPluginSettingsForPlugin(pluginId) { const settings = pluginSettings[pluginId]; return settings ? JSON.parse(JSON.stringify(settings)) : {}; } function removeDisplayProfile(compositor, profileId) { if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId]) return; const updated = JSON.parse(JSON.stringify(displayProfiles)); delete updated[compositor][profileId]; displayProfiles = updated; saveSettings(); } function setDisplayPreviousRefreshModes(compositor, modes) { if (JSON.stringify(displayPreviousRefreshModes[compositor] || {}) === JSON.stringify(modes || {})) return; const updated = JSON.parse(JSON.stringify(displayPreviousRefreshModes)); if (Object.keys(modes || {}).length > 0) updated[compositor] = modes; else delete updated[compositor]; displayPreviousRefreshModes = updated; saveSettings(); } ListModel { id: leftWidgetsModel } ListModel { id: centerWidgetsModel } ListModel { id: rightWidgetsModel } component SettingsFile: QtObject { id: settingsFile required property string filePath property var settings: ({}) property bool isLoading: false property bool hasLoaded: false property bool hasParseFailed: false property bool fileUnsavedUserChanges: false property bool isFileReadOnly: false property bool selfWrite: false function setSettings(newSettings, userChanges) { const newSettingsJson = JSON.stringify(newSettings, null, 2); if (JSON.stringify(settings, null, 2) === newSettingsJson) { return; } settings = newSettings; fileUnsavedUserChanges = fileUnsavedUserChanges || userChanges; selfWrite = true; settingsFileView.setText(newSettingsJson); } function getSettings() { return Object.assign({}, settings); } property Timer timer: Timer { id: settingsFileReloadDebounce interval: 50 onTriggered: settingsFileView.reload() repeat: false } property FileView fileView: FileView { id: settingsFileView path: isGreeterMode ? "" : filePath blockLoading: false blockWrites: true atomicWrites: true watchChanges: !isGreeterMode onFileChanged: { if (selfWrite) { selfWrite = false; } else { isLoading = true; _loading = true; settingsFileReloadDebounce.restart(); } } onLoaded: { if (isGreeterMode) { return; } isLoading = true; _loading = true; const hadParseFailed = hasParseFailed; hasParseFailed = false; fileUnsavedUserChanges = false; if (unsavedUserChanges) { unsavedUserChanges = _anySettingsFile(file => file.fileUnsavedUserChanges); } const fileName = filePath?.split("/").pop(); try { let txt = settingsFileView.text(); if (!txt || !txt.trim()) { txt = "{}"; } settings = JSON.parse(txt); hasLoaded = true; } catch (error) { hasParseFailed = true; _parseError = true; const msg = error.message; log.error(`Failed to reload ${fileName} - file will not be overwritten. Error:`, msg); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg(fileName), msg)); } finally { isLoading = false; if (hadParseFailed && !hasParseFailed) { _parseError = _anySettingsFile(file => file.hasParseFailed); } _tryCompleteLoading(); _loadSettingsOrStartIfReady(); } } onLoadFailed: error => { if (isGreeterMode) { return; } isLoading = false; _loading = _anySettingsFile(file => file.isLoading); if (error === FileViewError.FileNotFound) { // Fake that the file has been loaded so that it gets created after a save. hasLoaded = true; } applyStoredTheme(); _tryCompleteLoading(); _loadSettingsOrStartIfReady(); } onSaved: { isFileReadOnly = false; isReadOnly = isReadOnly ? _anySettingsFile(file => file.isFileReadOnly) : false; fileUnsavedUserChanges = false; if (unsavedUserChanges) { unsavedUserChanges = _anySettingsFile(file => file.fileUnsavedUserChanges); } } onSaveFailed: error => { selfWrite = false; if (error === FileViewError.PermissionDenied) { isFileReadOnly = true; isReadOnly = true; } if (fileUnsavedUserChanges) { const fileName = filePath?.split("/").pop() || "unknown"; log.warn(`Failed to save ${fileName}`); } } } } enum Stage { Discovering = 0, Loading = 1, Partial = 2, Ready = 3 } property int _settingsStage: SettingsData.Stage.Discovering property var _settingsFiles: new Map() property var _settingsFilesPaths: ([]) function _registerSettingsFile(file) { const filePath = file.filePath; if (_settingsFiles.has(filePath)) { return; } let index; if (file === defaultSettingsFile) { index = 0; } else { index = _settingsFilesPaths.findIndex((path, i) => { return path > filePath && i > 0; }); } _settingsFiles.set(filePath, file); if (index >= 0) { _settingsFilesPaths.splice(index, 0, filePath); } else { _settingsFilesPaths.push(filePath); } _tryCompleteDiscovery(); } function _unregisterSettingsFile(file) { _settingsFiles.delete(file.filePath); _settingsFilesPaths = _settingsFilesPaths.filter(path => path != file.filePath); } function _tryCompleteDiscovery() { if (_settingsStage !== SettingsData.Stage.Discovering || !_settingsFolderChecked) { return; } let expectedCount = 1; if (_settingsFolderExists) { expectedCount += settingsFolderLoader.item.count; } if (_settingsFilesPaths.length == expectedCount) { _settingsStage = SettingsData.Stage.Loading; _tryCompleteLoading(); _loadSettingsOrStartIfReady(); } } function _tryCompleteLoading() { if (_settingsStage !== SettingsData.Stage.Loading) { return; } if (_everySettingsFile(file => file.hasLoaded || file.hasParseFailed)) { _settingsStage = _parseError ? SettingsData.Stage.Partial : SettingsData.Stage.Ready; } } function _loadSettingsOrStartIfReady() { if (_settingsStage < SettingsData.Stage.Partial || _anySettingsFile(file => file.isLoading)) { return; } let unsaved; _loading = true; if (!_hasLoaded || _settingsStage === SettingsData.Stage.Partial) { unsaved = _runStartSequence(); } else { unsaved = _loadSettings(); } _loading = false; if (unsaved) { _saveSettings(false); } } function _getSettingsObjectFromFiles() { const settingsObject = {}; for (const path of _settingsFilesPaths) { const settingsFile = _settingsFiles.get(path); Object.assign(settingsObject, settingsFile.getSettings()); } return settingsObject; } function _anySettingsFile(predicate) { for (const file of _settingsFiles.values()) { if (predicate(file)) { return true; } } return false; } function _everySettingsFile(predicate) { for (const file of _settingsFiles.values()) { if (!predicate(file)) { return false; } } return true; } SettingsFile { id: defaultSettingsFile filePath: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/dms/settings.json" Component.onCompleted: { _registerSettingsFile(defaultSettingsFile); } } property bool _settingsFolderExists: false property bool _settingsFolderChecked: isGreeterMode function _syncSettingsFilesModels() { if (!_settingsFolderChecked) { settingsFilesModelSyncDebounce.restart(); return; } if (!_settingsFolderExists) { return; } const listModel = settingsFilesListModel; const folderModel = settingsFolderLoader.item; const folderPathsSet = new Set(); for (let i = 0; i < folderModel.count; i++) { const filePath = folderModel.get(i, "filePath"); folderPathsSet.add(filePath); if (!_settingsFiles.has(filePath)) { listModel.append({ filePath }); } } for (let i = listModel.count - 1; i >= 0; i--) { const filePath = listModel.get(i).filePath; if (!folderPathsSet.has(filePath)) { listModel.remove(i); } } } Timer { id: settingsFilesModelSyncDebounce interval: 50 repeat: false running: false onTriggered: _syncSettingsFilesModels() } ListModel { id: settingsFilesListModel } Loader { id: settingsFolderLoader active: !isGreeterMode function unloadModel() { active = false; } sourceComponent: FolderListModel { id: settingsFolderModel property url dir: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/dms/config.d" folder: dir showDirs: false nameFilters: ["*.json"] onStatusChanged: { // Folder gets reset to the CWD if the directory doesn't exist. _settingsFolderExists = folder == dir; if (status !== FolderListModel.Ready) { return; } _settingsFolderChecked = true; if (!_settingsFolderExists) { Qt.callLater(() => settingsFolderLoader.unloadModel()); } if (_hasLoaded) { settingsFilesModelSyncDebounce.restart(); } else { _syncSettingsFilesModels(); } _tryCompleteDiscovery(); } } } Instantiator { id: settingsLoader model: settingsFilesListModel onObjectAdded: (_, file) => { _registerSettingsFile(file); } onObjectRemoved: (_, file) => { _unregisterSettingsFile(file); _loadSettingsOrStartIfReady(); } delegate: SettingsFile { id: settingsFile } } readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter" property string greeterSettingsBaseDir: root._greeterCacheDir function setGreeterSettingsBaseDir(dir) { const next = dir || root._greeterCacheDir; if (greeterSettingsBaseDir === next) return; greeterSettingsBaseDir = next; if (isGreeterMode) greeterSettingsFile.reload(); } function resetGreeterSettingsBaseDir() { setGreeterSettingsBaseDir(root._greeterCacheDir); } function loadGreeterSettings(txt) { _loading = true; _hasLoaded = false; try { Store.parse(root, (txt && txt.trim()) ? JSON.parse(txt) : {}); _parseError = false; } catch (e) { _parseError = true; log.error("Failed to parse greeter settings.json:", e.message); Store.parse(root, {}); } finally { _loading = false; } _hasLoaded = true; applyStoredTheme(); } FileView { id: greeterSettingsFile path: isGreeterMode && root.greeterSettingsBaseDir ? (root.greeterSettingsBaseDir + "/settings.json") : "" preload: isGreeterMode blockLoading: false blockWrites: true watchChanges: false printErrors: false onLoaded: { if (isGreeterMode) loadGreeterSettings(greeterSettingsFile.text()); } onLoadFailed: { if (isGreeterMode) loadGreeterSettings(""); } } // Our first save drops the keys a newer shell wrote, so its settings are kept for going back. FileView { id: newerSettingsBackup preload: false blockWrites: true atomicWrites: true printErrors: false } FileView { id: pluginSettingsFile path: isGreeterMode ? "" : pluginSettingsPath blockLoading: true blockWrites: true atomicWrites: true printErrors: false watchChanges: !isGreeterMode onLoaded: { if (isGreeterMode) return; pluginSettingsFileExists = true; parsePluginSettings(pluginSettingsFile.text()); } onLoadFailed: error => { if (isGreeterMode) return; const msg = String(error || ""); const missing = _isMissingPluginSettingsError(error); pluginSettingsFileExists = !missing; if (!missing) log.warn("Failed to load plugin_settings.json. Error:", msg); _resetPluginSettings(); } } Timer { id: settingsSaveDebounce interval: 50 repeat: false running: false onTriggered: _saveSettings(true) } property bool pluginSettingsFileExists: false }