pragma Singleton pragma ComponentBehavior: Bound import QtCore import QtQuick import Quickshell import Quickshell.Io import qs.Common import qs.Common.settings import qs.Services import "GSettings.js" as GSettings import "settings/SettingsSpec.js" as Spec import "settings/SettingsStore.js" as Store Singleton { id: root readonly property var log: Log.scoped("SettingsData") readonly property int settingsConfigVersion: 18 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 AnimationVariant { Material, Fluent, Dynamic } 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 } 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 + "/DankMaterialShell/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 _hasUnsavedChanges: false property bool _selfWrite: false property var _loadedSettingsSnapshot: null property var pluginSettings: ({}) property var builtInPluginSettings: ({}) 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: false property bool clipboardEnterToPaste: false property bool clipboardRememberTypeFilter: false property bool clipboardUseOverlayLayer: false property string clipboardTypeFilter: "all" property var clipboardVisibleEntryActions: ["pin", "edit", "delete"] property var launcherPluginVisibility: ({}) 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 launcherPluginOrder: [] 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 alias dankBarLeftWidgetsModel: leftWidgetsModel property alias dankBarCenterWidgetsModel: centerWidgetsModel property alias dankBarRightWidgetsModel: rightWidgetsModel property string currentThemeName: "purple" property string currentThemeCategory: "generic" property string customThemeFile: "" property var registryThemeVariants: ({}) property string matugenScheme: "scheme-tonal-spot" property bool matugenSmartMode: false property string matugenSourceMode: "dominant" property real matugenContrast: 0 property bool runUserMatugenTemplates: true property string matugenTargetMonitor: "" property real popupTransparency: 1.0 property real dockTransparency: 1 property bool floatingWindowSyncGlobal: true property real floatingWindowTransparency: 1.0 property bool floatingWindowForegroundLayers: true property real floatingWindowForegroundTransparency: 1.0 property bool dmsWindowsFloating: true property string widgetBackgroundColor: "sch" property string widgetBackgroundCustomColor: "#6750A4" property real widgetBackgroundCustomStrength: 0.50 property string widgetColorMode: "default" property string controlCenterTileColorMode: "primary" property string buttonColorMode: "primary" property real cornerRadius: 12 property int niriLayoutGapsOverride: -1 property int niriLayoutRadiusOverride: -1 property int niriLayoutBorderSize: -1 property int hyprlandLayoutGapsOverride: -1 property int hyprlandLayoutGapsOutOverride: -1 property int hyprlandLayoutRadiusOverride: -1 property int hyprlandLayoutBorderSize: -1 property bool hyprlandResizeOnBorder: false property int mangoLayoutGapsOverride: -1 property int mangoLayoutGapsOutOverride: -1 property int mangoLayoutRadiusOverride: -1 property int mangoLayoutBorderSize: -1 property bool mangoTrackpadNaturalScrolling: true property string mouseAccelProfile: "default" property real mouseAccelSpeed: 0.0 property bool mouseLeftHanded: false property bool mouseMiddleEmulation: false property bool mouseNaturalScroll: false property real mouseScrollFactor: 1.0 property string mouseScrollMethod: "default" property string touchpadAccelProfile: "default" property real touchpadAccelSpeed: 0.0 property bool touchpadDisableOnExternalMouse: false property bool touchpadDisableWhileTyping: true property bool touchpadDragLock: false property bool touchpadMiddleEmulation: false property bool touchpadNaturalScroll: true property real touchpadScrollFactor: 1.0 property string touchpadScrollMethod: "default" property bool touchpadTapAndDrag: true property bool touchpadTapToClick: true property string keyboardLayouts: "" property string keyboardVariants: "" property string keyboardModel: "" property string keyboardOptions: "" property string keyboardKeymapFile: "" property string keyboardTrackLayout: "" property int keyboardRepeatDelay: 0 property int keyboardRepeatRate: 0 property bool keyboardNumlock: false property int firstDayOfWeek: -1 property bool showWeekNumber: false property string calendarBackend: "auto" property string defaultTaskCalendarId: "" property bool audioShowStreamDevices: false property string clockFormat: "auto" 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: false property bool padHours12Hour: false property bool useFahrenheit: false property string windSpeedUnit: "kmh" property int animationSpeed: SettingsData.AnimationSpeed.Short property int customAnimationDuration: 500 property bool syncComponentAnimationSpeeds: true onSyncComponentAnimationSpeedsChanged: saveSettings() property int popoutAnimationSpeed: SettingsData.AnimationSpeed.Short property int popoutCustomAnimationDuration: 150 property int modalAnimationSpeed: SettingsData.AnimationSpeed.Short property int modalCustomAnimationDuration: 150 property bool reduceMotion: false onReduceMotionChanged: saveSettings() property int springBounce: 1 onSpringBounceChanged: saveSettings() property bool enableRippleEffects: true onEnableRippleEffectsChanged: saveSettings() property int animationVariant: SettingsData.AnimationVariant.Material onAnimationVariantChanged: saveSettings() property int motionEffect: SettingsData.AnimationEffect.Standard onMotionEffectChanged: saveSettings() property bool m3ElevationEnabled: true onM3ElevationEnabledChanged: saveSettings() property int m3ElevationIntensity: 12 onM3ElevationIntensityChanged: saveSettings() property int m3ElevationOpacity: 30 onM3ElevationOpacityChanged: saveSettings() property string m3ElevationColorMode: "default" onM3ElevationColorModeChanged: saveSettings() property string m3ElevationLightDirection: "top" onM3ElevationLightDirectionChanged: saveSettings() property string m3ElevationCustomColor: "#000000" onM3ElevationCustomColorChanged: saveSettings() property bool modalElevationEnabled: true onModalElevationEnabledChanged: saveSettings() property bool popoutElevationEnabled: true onPopoutElevationEnabledChanged: saveSettings() property bool barElevationEnabled: true onBarElevationEnabledChanged: saveSettings() property bool blurEnabled: false onBlurEnabledChanged: saveSettings() property bool blurForegroundLayers: true onBlurForegroundLayersChanged: saveSettings() property real foregroundLayerTransparency: 1.0 property real blurLayerOutlineOpacity: 0.12 onBlurLayerOutlineOpacityChanged: saveSettings() property bool blurBorderEnabled: true onBlurBorderEnabledChanged: saveSettings() property string blurBorderColor: "outline" onBlurBorderColorChanged: saveSettings() property string blurBorderCustomColor: "#ffffff" onBlurBorderCustomColorChanged: saveSettings() property real blurBorderOpacity: 0.35 onBlurBorderOpacityChanged: saveSettings() property string wallpaperFillMode: "Fill" property bool blurredWallpaperLayer: false property bool blurWallpaperOnOverview: false property string wallpaperBackgroundColorMode: "black" property string wallpaperBackgroundCustomColor: "#000000" readonly property color effectiveWallpaperBackgroundColor: { switch (wallpaperBackgroundColorMode) { case "black": return "#000000"; case "white": return "#ffffff"; case "primary": return Theme.primary; case "surface": return Theme.surfaceContainer; case "custom": return wallpaperBackgroundCustomColor; default: return "#000000"; } } property bool frameEnabled: false onFrameEnabledChanged: { saveSettings(); if (!_loading) updateFrameCompositorLayout(); } property real frameThickness: 16 onFrameThicknessChanged: saveSettings() property int barInsetPaddingShared: -1 onBarInsetPaddingSharedChanged: saveSettings() property bool barInsetPaddingSyncAll: false onBarInsetPaddingSyncAllChanged: saveSettings() property int frameBarInsetPadding: -1 onFrameBarInsetPaddingChanged: saveSettings() property real frameRounding: 23 onFrameRoundingChanged: saveSettings() property string frameColor: "" onFrameColorChanged: saveSettings() property real frameOpacity: 1.0 onFrameOpacityChanged: saveSettings() property var frameScreenPreferences: ["all"] onFrameScreenPreferencesChanged: saveSettings() property real frameBarSize: 40 onFrameBarSizeChanged: saveSettings() property bool frameShowOnOverview: false onFrameShowOnOverviewChanged: saveSettings() property bool frameBlurEnabled: true onFrameBlurEnabledChanged: saveSettings() property bool frameCloseGaps: true onFrameCloseGapsChanged: saveSettings() property string frameLauncherEmergeSide: "bottom" onFrameLauncherEmergeSideChanged: saveSettings() property bool frameLauncherArcExtender: false onFrameLauncherArcExtenderChanged: saveSettings() property bool frameLauncherEdgeHover: false onFrameLauncherEdgeHoverChanged: saveSettings() readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" property string frameMode: "connected" onFrameModeChanged: { saveSettings(); if (!_loading && frameEnabled) updateFrameCompositorLayout(); } property var connectedFrameBarStyleBackups: ({}) onConnectedFrameBarStyleBackupsChanged: saveSettings() readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected" onConnectedFrameModeActiveChanged: { if (_loading) return; _reconcileConnectedFrameBarStyles(); } readonly property color effectiveFrameColor: { const fc = frameColor; if (!fc || fc === "default") return Theme.surfaceContainer; if (fc === "primary") return Theme.primary; if (fc === "surface") return Theme.surface; return fc; } property bool showLauncherButton: true property bool showWorkspaceSwitcher: true property bool showFocusedWindow: true property bool showWeather: true property bool showMusic: true property bool showClipboard: true property bool showCpuUsage: true property bool showMemUsage: true property bool showCpuTemp: true property bool showGpuTemp: true property int selectedGpuIndex: 0 property var enabledGpuPciIds: [] property bool showSystemTray: true property string systemTrayIconTintMode: "none" property int systemTrayIconTintSaturation: 50 property int systemTrayIconTintStrength: 135 property bool showClock: true property bool showNotificationButton: true property bool showBattery: true property bool showControlCenterButton: true property bool showCapsLockIndicator: true property bool controlCenterShowNetworkIcon: true property bool controlCenterShowBluetoothIcon: true property bool controlCenterShowAudioIcon: true property bool controlCenterShowAudioPercent: false property bool controlCenterShowVpnIcon: true property bool controlCenterShowBrightnessIcon: false property bool controlCenterShowBrightnessPercent: false property bool controlCenterShowMicIcon: false property bool controlCenterShowMicPercent: true property bool controlCenterShowBatteryIcon: false property bool controlCenterShowPrinterIcon: false property bool controlCenterShowScreenSharingIcon: true property bool controlCenterShowIdleInhibitorIcon: false property bool controlCenterShowDoNotDisturbIcon: false property bool showPrivacyButton: true property bool privacyShowMicIcon: false property bool privacyShowCameraIcon: false property bool privacyShowScreenShareIcon: false property var controlCenterWidgets: [ { "id": "volumeSlider", "enabled": true, "width": 50 }, { "id": "brightnessSlider", "enabled": true, "width": 50 }, { "id": "wifi", "enabled": true, "width": 50 }, { "id": "bluetooth", "enabled": true, "width": 50 }, { "id": "audioOutput", "enabled": true, "width": 50 }, { "id": "audioInput", "enabled": true, "width": 50 }, { "id": "nightMode", "enabled": true, "width": 50 }, { "id": "darkMode", "enabled": true, "width": 50 } ] property bool showWorkspaceIndex: false property bool showWorkspaceName: false property bool showWorkspacePadding: false property bool workspaceScrolling: false property bool showWorkspaceApps: false property bool workspaceDragReorder: true property bool groupWorkspaceApps: true property bool groupActiveWorkspaceApps: false property int maxWorkspaceIcons: 3 property int workspaceAppIconSizeOffset: 0 property bool workspaceFollowFocus: false property bool showOccupiedWorkspacesOnly: false property bool reverseScrolling: false property bool dwlShowAllTags: false property bool workspaceActiveAppHighlightEnabled: false property string workspaceColorMode: "default" property string workspaceFocusedCustomColor: "#6750A4" property string workspaceOccupiedColorMode: "none" property string workspaceOccupiedCustomColor: "#625B71" property string workspaceUnfocusedColorMode: "default" property string workspaceUnfocusedCustomColor: "#49454E" property string workspaceUrgentColorMode: "default" property string workspaceUrgentCustomColor: "#B3261E" property bool workspaceFocusedBorderEnabled: false property string workspaceFocusedBorderColor: "primary" property string workspaceFocusedBorderCustomColor: "#6750A4" property int workspaceFocusedBorderThickness: 2 property bool workspaceUnfocusedMonitorSeparateAppearance: false property string workspaceUnfocusedMonitorColorMode: "default" property string workspaceUnfocusedMonitorFocusedCustomColor: "#6750A4" property string workspaceUnfocusedMonitorOccupiedColorMode: "none" property string workspaceUnfocusedMonitorOccupiedCustomColor: "#625B71" property string workspaceUnfocusedMonitorUnfocusedColorMode: "default" property string workspaceUnfocusedMonitorUnfocusedCustomColor: "#49454E" property string workspaceUnfocusedMonitorUrgentColorMode: "default" property string workspaceUnfocusedMonitorUrgentCustomColor: "#B3261E" property bool workspaceUnfocusedMonitorBorderEnabled: false property string workspaceUnfocusedMonitorBorderColor: "primary" property string workspaceUnfocusedMonitorBorderCustomColor: "#6750A4" property int workspaceUnfocusedMonitorBorderThickness: 2 property var workspaceNameIcons: ({}) property bool waveProgressEnabled: true property bool scrollTitleEnabled: true property bool mediaAdaptiveWidthEnabled: true property bool audioVisualizerEnabled: true property bool mediaUseAlbumArtAccent: false property bool mediaWallpaperEnabled: true property bool appleMusicAnimatedArtEnabled: false property string audioScrollMode: "volume" property int audioWheelScrollAmount: 5 property bool audioDeviceScrollVolumeEnabled: false property var mediaExcludePlayers: [] property bool clockCompactMode: false property int focusedWindowSize: 1 property bool focusedWindowCompactMode: false property bool focusedWindowShowIcon: true property bool runningAppsCompactMode: true property int barMaxVisibleApps: 0 property int barMaxVisibleRunningApps: 0 property bool barShowOverflowBadge: true property bool trayAutoOverflow: true property bool trayPopupSingleLine: true property int trayMaxVisibleItems: 0 property real trayIconSpacing: 0 property bool appsDockHideIndicators: false property bool appsDockColorizeActive: false property string appsDockActiveColorMode: "primary" property bool appsDockEnlargeOnHover: false property int appsDockEnlargePercentage: 125 property int appsDockIconSizePercentage: 100 property bool keyboardLayoutNameCompactMode: false property bool keyboardLayoutNameShowIcon: false property bool runningAppsCurrentWorkspace: true property bool runningAppsGroupByApp: false property bool runningAppsCurrentMonitor: false property var appIdSubstitutions: [] property string centeringMode: "index" property string clockDateFormat: "" property string lockDateFormat: "" property bool greeterRememberLastSession: true property bool greeterRememberLastUser: true property bool greeterAutoLogin: false property bool greeterEnableFprint: false property bool greeterEnableU2f: false property bool greeterShowWeather: true property string greeterWallpaperPath: "" property string greeterLockDateFormat: "" property string greeterFontFamily: "" property string greeterWallpaperFillMode: "" property int mediaSize: 1 property string appLauncherViewMode: "list" property string spotlightModalViewMode: "list" property string browserPickerViewMode: "grid" property string appPickerViewMode: "grid" property bool sortAppsAlphabetically: false property int appLauncherGridColumns: 4 property bool closeNiriOverviewOnWindowFocus: true property bool rememberLastQuery: false property bool rememberLastMode: true property var spotlightSectionViewModes: ({}) onSpotlightSectionViewModesChanged: saveSettings() property var appDrawerSectionViewModes: ({}) onAppDrawerSectionViewModesChanged: saveSettings() property bool niriOverviewOverlayEnabled: true property string niriOverviewLauncherStyle: "full" property string dankLauncherV2Size: "compact" property bool dankLauncherV2ShowSourceBadges: true property bool dankLauncherV2BorderEnabled: false property int dankLauncherV2BorderThickness: 2 property string dankLauncherV2BorderColor: "primary" property bool dankLauncherV2ShowFooter: true property bool dankLauncherV2UnloadOnClose: false property bool dankLauncherV2IncludeFilesInAll: false property bool dankLauncherV2IncludeFoldersInAll: false property bool launcherUseOverlayLayer: false property string launcherStyle: "full" property bool spotlightBarShowModeChips: false property bool keybindsFloatingWindow: false 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: false property bool weatherEnabled: true readonly property var _dashTabIds: ["overview", "media", "wallpaper", "weather", "settings"] readonly property var _dashTabsDefault: [ { "id": "overview", "enabled": true }, { "id": "media", "enabled": true }, { "id": "wallpaper", "enabled": true }, { "id": "weather", "enabled": true }, { "id": "settings", "enabled": true } ] property var dashTabs: _dashTabsDefault onDashTabsChanged: saveSettings() 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 (_dashTabIds.indexOf(id) < 0 || seen[id]) continue; seen[id] = true; result.push({ "id": id, "enabled": stored[i].enabled !== false }); } for (var j = 0; j < _dashTabIds.length; j++) { if (!seen[_dashTabIds[j]]) result.push({ "id": _dashTabIds[j], "enabled": true }); } return result; } function visibleDashTabIds() { return getDashTabs().filter(t => t.enabled && (t.id !== "weather" || weatherEnabled)).map(t => t.id); } function dashTabIndexForId(id) { const idx = visibleDashTabIds().indexOf(id); return idx < 0 ? 0 : idx; } 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) { dashTabs = getDashTabs().map(t => t.id === id ? { "id": t.id, "enabled": on } : t); } function resetDashTabs() { dashTabs = _dashTabsDefault.map(t => ({ "id": t.id, "enabled": t.enabled })); } property string networkPreference: "auto" property string iconThemeDark: "System Default" property string iconThemeLight: "System Default" property bool iconThemePerMode: false readonly property string iconTheme: resolveIconTheme() property var availableIconThemes: ["System Default"] property string systemDefaultIconTheme: "" property var cursorSettings: ({ "theme": "System Default", "size": 24, "niri": { "hideWhenTyping": false, "hideAfterInactiveMs": 0 }, "hyprland": { "hideOnKeyPress": false, "hideOnTouch": false, "inactiveTimeout": 0 }, "mango": { "cursorHideTimeout": 0 } }) property var availableCursorThemes: ["System Default"] property string systemDefaultCursorTheme: "" property string launcherLogoMode: "apps" property string launcherLogoCustomPath: "" property string launcherLogoColorOverride: "" property bool launcherLogoColorInvertOnMode: false property real launcherLogoBrightness: 0.5 property real launcherLogoContrast: 1 property int launcherLogoSizeOffset: 0 property string fontFamily: "Inter Variable" property string monoFontFamily: "Fira Code" property int fontWeight: Font.Normal property real fontScale: 1.0 property real dankBarFontScale: 1.0 property int textRenderType: SettingsData.TextRenderType.Qt property int textRenderQuality: SettingsData.TextRenderQuality.Default property bool notepadUseMonospace: true property string notepadFontFamily: "" property real notepadFontSize: 14 property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def property bool notepadShowLineNumbers: false property bool notepadAutoSave: false property string notepadSlideoutSide: "right" property string notepadDefaultMode: "slideout" property real notepadTransparencyOverride: -1 property real notepadLastCustomTransparency: 0.7 property bool notepadUseCompositorGap: false property int notepadEdgeGap: 0 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: true property bool useSystemSoundTheme: false property bool soundNewNotification: true property bool soundVolumeChanged: true property bool soundPluggedIn: true property bool soundLogin: false property bool muteSoundsWhenMediaPlaying: true property int acMonitorTimeout: 0 property int acLockTimeout: 0 property int acSuspendTimeout: 0 property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend property string acProfileName: "" property int acPostLockMonitorTimeout: 0 property int batteryMonitorTimeout: 0 property int batteryLockTimeout: 0 property int batterySuspendTimeout: 0 property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend property string batteryProfileName: "" property int batteryPostLockMonitorTimeout: 0 property int batteryChargeLimit: 100 property bool batteryNotifyChargeLimit: false property int batteryCriticalThreshold: 10 property bool batteryNotifyCritical: true property int batteryLowThreshold: 20 property bool batteryNotifyLow: false property int batteryChargeLimitNotificationType: 0 property int batteryLowNotificationType: 0 property int batteryCriticalNotificationType: 1 property bool batteryAutoPowerSaver: false property bool lowerDisplayRefreshRateOnBattery: false property bool showBatteryPercent: true property bool showBatteryPercentOnlyOnBattery: false property bool showBatteryTime: false property bool showBatteryTimeOnlyOnBattery: false property bool showBatteryPowerCharging: false property bool showBatteryPowerDischarging: false property string batteryStyle: "icon" property bool lockBeforeSuspend: false property bool loginctlLockIntegration: true property bool fadeToLockEnabled: true property int fadeToLockGracePeriod: 5 property bool fadeToDpmsEnabled: true property int fadeToDpmsGracePeriod: 5 property string launchPrefix: "" property bool syncModeWithPortal: true property bool terminalsAlwaysDark: false property string muxType: "tmux" property bool muxUseCustomCommand: false property string muxCustomCommand: "" property string muxSessionFilter: "" property bool runDmsMatugenTemplates: true property bool matugenTemplateGtk: true property bool matugenTemplateNiri: true property bool matugenTemplateHyprland: true property bool matugenTemplateMangowc: true property bool matugenTemplateQt5ct: true property bool matugenTemplateQt6ct: true property bool matugenTemplateFcitx5: true property bool matugenTemplateQtengine: true property bool matugenTemplateFirefox: true property bool matugenTemplatePywalfox: true property bool matugenTemplateZenBrowser: true property bool matugenTemplateVesktop: true property bool matugenTemplateVencord: true property bool matugenTemplateEquibop: true property bool matugenTemplateGhostty: true property bool matugenTemplateKitty: true property bool matugenTemplateFoot: true property bool matugenTemplateNeovim: false property bool matugenTemplateAlacritty: true property bool matugenTemplateWezterm: true property bool matugenTemplateDgop: true property bool matugenTemplateKcolorscheme: true property bool matugenTemplateVscode: true property bool matugenTemplateEmacs: true property bool matugenTemplateZed: true property var matugenTemplateNeovimSettings: ({ "dark": { "baseTheme": "github_dark", "harmony": 0.5 }, "light": { "baseTheme": "github_light", "harmony": 0.5 } }) property bool matugenTemplateNeovimSetBackground: true property bool showDock: false property bool dockAutoHide: false property bool dockSmartAutoHide: false property bool dockUseOverlayLayer: false property bool dockShowOnFullscreen: false property bool dockGroupByApp: false property bool dockSeparatePinnedAndRunningApps: false property bool dockRestoreSpecialWorkspaceOnClick: false property bool dockOpenOnOverview: false property int dockPosition: SettingsData.Position.Bottom property real dockSpacing: 4 property real dockBottomGap: 0 property real dockMargin: 0 property real dockIconSize: 40 property string dockIndicatorStyle: "circle" property bool dockBorderEnabled: false property string dockBorderColor: "surfaceText" property real dockBorderOpacity: 1.0 property int dockBorderThickness: 1 property bool dockIsolateDisplays: false property bool dockLauncherEnabled: false property string dockLauncherLogoMode: "apps" property string dockLauncherLogoCustomPath: "" property string dockLauncherLogoColorOverride: "" property int dockLauncherLogoSizeOffset: 0 property real dockLauncherLogoBrightness: 0.5 property real dockLauncherLogoContrast: 1 property int dockMaxVisibleApps: 0 property int dockMaxVisibleRunningApps: 0 property bool dockShowOverflowBadge: true property bool dockShowTrash: false property string dockTrashFileManager: "default" property string dockTrashCustomCommand: "" property bool notificationOverlayEnabled: false property bool notificationPopupShadowEnabled: true property bool notificationPopupPrivacyMode: false property bool notificationPopupBodyInvokesAction: false property bool notificationForegroundLayers: true property int overviewRows: 2 property int overviewColumns: 5 property real overviewScale: 0.16 property bool modalDarkenBackground: true property bool lockScreenShowPowerActions: true property bool lockScreenShowSystemIcons: true property bool lockScreenShowTime: true property string lockScreenClockStyle: "horizontal" property bool lockScreenShowDate: true property bool lockScreenShowProfileImage: true property bool lockScreenShowPasswordField: true property bool lockScreenShowMediaPlayer: true property bool lockScreenShowWeather: true property bool lockScreenPowerOffMonitorsOnLock: false property bool lockAtStartup: false property bool enableFprint: false property int maxFprintTries: 15 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: false property string u2fMode: "or" 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: "" property bool lockPamInlineFprint: false property bool lockPamInlineU2f: false property bool lockPamExternallyManaged: false property string lockU2fPamPath: "" property string lockScreenSecurityKeyShortcut: "Ctrl+Q" property bool lockScreenSecurityKeyShortcutEnabled: false property bool greeterPamExternallyManaged: false property string lockScreenInactiveColor: "#000000" property int lockScreenNotificationMode: 0 property bool lockScreenVideoEnabled: false property string lockScreenVideoPath: "" property bool lockScreenVideoCycling: false property string lockScreenWallpaperPath: "" property string lockScreenWallpaperFillMode: "" property string lockScreenFontFamily: "" property bool hideBrightnessSlider: false property int notificationTimeoutLow: 5000 property int notificationTimeoutNormal: 5000 property int notificationTimeoutCritical: 0 property bool notificationIgnoreAppTimeout: false property bool notificationCompactMode: false property bool notificationShowTimeoutBar: false property bool notificationDedupeEnabled: true property int notificationPopupPosition: SettingsData.Position.Top property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short property int notificationCustomAnimationDuration: 400 property bool notificationHistoryEnabled: true property int notificationHistoryMaxCount: 50 property int notificationHistoryMaxAgeDays: 7 property bool notificationHistorySaveLow: true property bool notificationHistorySaveNormal: true property bool notificationHistorySaveCritical: true property var notificationRules: [] property bool notificationDndAllowCritical: true property bool notificationFocusedMonitor: false // Island is a per-instance render mode: any bar config with island:true draws as an island // instead of a DankBar, and carries its own island* look settings. readonly property var islandBarConfigs: { barConfigs; return (barConfigs || []).filter(cfg => cfg && cfg.island === true); } readonly property bool dankIslandEnabled: islandBarConfigs.some(cfg => cfg.enabled ?? false) readonly property var islandDefaults: ({ "islandFloating": false, "islandUseOverlayLayer": false, "islandReserveThickness": 40, "islandCompactThickness": 38, "islandOuterGap": 4, "islandAlongOffset": 0, "islandInteractionMode": "hybrid", "islandHoverOpenDelay": 150, "islandHoverCloseDelay": 150, "islandPalette": "default", "islandTransparency": 1, "islandCornerRadius": 34, "islandHighContrast": false, "islandMediaClockVisible": true, "islandNotificationBadgeClearOnOpen": false, "islandNotificationExpand": false, "islandHomeCompactTight": false, "islandHomeClockDisplay": "both", "islandHomeVolumeDisplay": "both", "islandHomeBrightnessDisplay": "both", "islandBatteryStyle": "solid", "islandSatellitesEnabled": true, "islandSatellitePosition": "edges", "islandSatelliteGap": 12, "islandSatelliteBackground": false, "islandSatelliteGothCorners": true, "islandSatelliteTransparency": 1, "islandSatelliteSwoopRadius": 24, "islandReducedMotion": false, "islandSpringStiffness": 560, "islandSpringDamping": 37, "islandSpringMass": 1 }) function islandSetting(bc, key) { const value = bc?.[key]; return value === undefined || value === null ? islandDefaults[key] : value; } 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 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) { const reserve = Math.max(24, Math.min(128, islandSetting(bc, "islandReserveThickness"))); const compact = Math.max(24, Math.min(72, islandSetting(bc, "islandCompactThickness"))); const gap = Math.max(0, Math.min(48, islandSetting(bc, "islandOuterGap"))); return Math.max(reserve, gap + compact); } readonly property var _islandHomeGroupIds: ["media", "clock", "weather", "status", "volume", "brightness", "notifications"] readonly property var _islandHomeLayoutDefault: [ { "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 } ] function getIslandHomeLayout(bc) { const stored = Array.isArray(bc?.islandHomeLayout) ? bc.islandHomeLayout : []; 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); } updateBarConfig(barId, { islandHomeLayout: ordered }); } function setIslandHomeGroupEnabled(barId, id, on) { if (id === "clock") return; updateBarConfig(barId, { islandHomeLayout: getIslandHomeLayout(getBarConfig(barId)).map(g => g.id === id ? { "id": g.id, "enabled": on } : g) }); } property bool osdAlwaysShowValue: false property int osdPosition: SettingsData.Position.BottomCenter property bool osdVolumeEnabled: true property bool osdMediaVolumeEnabled: true property bool osdMediaPlaybackEnabled: false property bool osdBrightnessEnabled: true property bool osdIdleInhibitorEnabled: true property bool osdMicMuteEnabled: true property bool osdMicVolumeEnabled: true property bool osdCapsLockEnabled: true property bool osdPowerProfileEnabled: true property bool osdAudioOutputEnabled: true property bool powerActionConfirm: true property real powerActionHoldDuration: 0.5 property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property string powerMenuDefaultAction: "logout" property bool powerMenuGridLayout: false property string customPowerActionLock: "" property string customPowerActionLogout: "" property string customPowerActionSuspend: "" property string customPowerActionHibernate: "" property string customPowerActionReboot: "" property string customPowerActionPowerOff: "" property var customPowerButtons: [] property bool updaterHideWidget: false property bool updaterCheckOnStart: false property bool updaterUseCustomCommand: false property string updaterCustomCommand: "" property string updaterTerminalAdditionalParams: "" property int updaterIntervalSeconds: 1800 property bool updaterIncludeFlatpak: true property bool updaterAllowAUR: true property var updaterIgnoredPackages: [] property string displayNameMode: "system" property var screenPreferences: ({}) property var showOnLastDisplay: ({}) property var displayProfiles: ({}) property var displayPreviousRefreshModes: ({}) property bool displayProfileAutoSelect: false property bool displayShowDisconnected: false property bool displaySnapToEdge: true property var barIpcRevealStates: ({}) property var barConfigs: [ { "id": "default", "name": "Main Bar", "enabled": true, "position": 0, "screenPreferences": ["all"], "showOnLastDisplay": true, "leftWidgets": ["launcherButton", "workspaceSwitcher", "focusedWindow"], "centerWidgets": ["music", "clock", "weather"], "rightWidgets": ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"], "spacing": 4, "innerPadding": 4, "barInsetPadding": -1, "barLengthPadding": 0, "bottomGap": 0, "attachToScreenEdge": false, "transparency": 1.0, "widgetTransparency": 1.0, "squareCorners": false, "noBackground": false, "maximizeWidgetIcons": false, "maximizeWidgetText": false, "removeWidgetPadding": false, "widgetPadding": 8, "batteryColorMode": "theme", "gothCornersEnabled": false, "gothCornerRadiusOverride": false, "gothCornerRadiusValue": 12, "borderEnabled": false, "borderColor": "surfaceText", "borderOpacity": 1.0, "borderThickness": 1, "widgetOutlineEnabled": false, "widgetOutlineColor": "primary", "widgetOutlineOpacity": 1.0, "widgetOutlineThickness": 1, "fontScale": 1.0, "iconScale": 1.0, "autoHide": false, "autoHideStrict": false, "autoHideDelay": 250, "showOnWindowsOpen": false, "openOnOverview": false, "visible": true, "popupGapsAuto": true, "popupGapsManual": 4, "maximizeDetection": true, "useOverlayLayer": false, "scrollEnabled": true, "scrollXBehavior": "column", "scrollYBehavior": "workspace", "shadowIntensity": 0, "shadowOpacity": 60, "shadowColorMode": "default", "shadowCustomColor": "#000000", "clickThrough": false, "hoverPopouts": false, "hoverPopoutDelay": 150 } ] property bool desktopClockEnabled: false property string desktopClockStyle: "analog" property real desktopClockTransparency: 0.8 property string desktopClockColorMode: "primary" property color desktopClockCustomColor: "#ffffff" property bool desktopClockShowDate: true property bool desktopClockShowAnalogNumbers: false property bool desktopClockShowAnalogSeconds: true property real desktopClockX: -1 property real desktopClockY: -1 property real desktopClockWidth: 280 property real desktopClockHeight: 180 property var desktopClockDisplayPreferences: ["all"] property bool systemMonitorEnabled: false property bool systemMonitorShowHeader: true property real systemMonitorTransparency: 0.8 property string systemMonitorColorMode: "primary" property color systemMonitorCustomColor: "#ffffff" property bool systemMonitorShowCpu: true property bool systemMonitorShowCpuGraph: true property bool systemMonitorShowCpuTemp: true property bool systemMonitorShowGpuTemp: false property string systemMonitorGpuPciId: "" property bool systemMonitorShowMemory: true property bool systemMonitorShowMemoryGraph: true property bool systemMonitorShowNetwork: true property bool systemMonitorShowNetworkGraph: true property bool systemMonitorShowDisk: true property bool systemMonitorShowTopProcesses: false property int systemMonitorTopProcessCount: 3 property string systemMonitorTopProcessSortBy: "cpu" property string systemMonitorLayoutMode: "auto" property int systemMonitorGraphInterval: 60 property real systemMonitorX: -1 property real systemMonitorY: -1 property real systemMonitorWidth: 320 property real systemMonitorHeight: 480 property var systemMonitorDisplayPreferences: ["all"] property var systemMonitorVariants: [] property var desktopWidgetPositions: ({}) property var desktopWidgetInstances: [] property var desktopWidgetGroups: [] function getDesktopWidgetPosition(pluginId, screenKey, property, defaultValue) { const pos = desktopWidgetPositions?.[pluginId]?.[screenKey]?.[property]; return pos !== undefined ? pos : defaultValue; } function updateDesktopWidgetPosition(pluginId, screenKey, updates) { const allPositions = JSON.parse(JSON.stringify(desktopWidgetPositions || {})); if (!allPositions[pluginId]) allPositions[pluginId] = {}; allPositions[pluginId][screenKey] = Object.assign({}, allPositions[pluginId][screenKey] || {}, updates); desktopWidgetPositions = allPositions; saveSettings(); } 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 createDesktopWidgetInstance(widgetType, name, config) { const id = "dw_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); const instance = { id: id, widgetType: widgetType, name: name || widgetType, enabled: true, config: config || {} }; const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); instances.push(instance); desktopWidgetInstances = instances; saveSettings(); return instance; } function updateDesktopWidgetInstance(instanceId, updates) { const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); const idx = instances.findIndex(inst => inst.id === instanceId); if (idx === -1) return; Object.assign(instances[idx], updates); desktopWidgetInstances = instances; saveSettings(); } function updateDesktopWidgetInstanceConfig(instanceId, configUpdates) { const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); const idx = instances.findIndex(inst => inst.id === instanceId); if (idx === -1) return; instances[idx].config = Object.assign({}, instances[idx].config || {}, configUpdates); desktopWidgetInstances = instances; saveSettings(); } function removeDesktopWidgetInstance(instanceId) { const instances = (desktopWidgetInstances || []).filter(inst => inst.id !== instanceId); desktopWidgetInstances = instances; SessionData.removeDesktopWidgetInstancePositions(instanceId); saveSettings(); } function duplicateDesktopWidgetInstance(instanceId) { const source = getDesktopWidgetInstance(instanceId); if (!source) return null; const newId = "dw_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); const instance = { id: newId, widgetType: source.widgetType, name: source.name + " (Copy)", enabled: source.enabled, config: JSON.parse(JSON.stringify(source.config || {})) }; const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); instances.push(instance); desktopWidgetInstances = instances; saveSettings(); return instance; } function getDesktopWidgetInstance(instanceId) { return (desktopWidgetInstances || []).find(inst => inst.id === instanceId) || 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 compositorInputRefreshNeeded signal compositorCursorRefreshNeeded signal notificationPopupsInvalidated function refreshAuthAvailability() { if (isGreeterMode) return; Processes.detectAuthCapabilities(); } Component.onCompleted: { if (isGreeterMode) return; Processes.settingsRoot = root; loadSettings(); initializeListModels(); refreshAuthAvailability(); Processes.checkPluginSettings(); } 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 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: 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 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(); } function clearGreeterSyncPending() { SessionData.greeterSyncBaseline = {}; SessionData.greeterSyncPending = false; SessionData.saveSettings(); } function revertGreeterSyncPending() { for (var key in SessionData.greeterSyncBaseline) { 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, "applyStoredIconTheme": applyStoredIconTheme, "updateBarConfigs": updateBarConfigs, "updateCompositorCursor": updateCompositorCursor, "scheduleAuthApply": scheduleAuthApply, "scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync, "markGreeterSyncPending": markGreeterSyncPending }) function set(key, value) { Spec.set(root, key, value, saveSettings, _hooks); if (key === "frameEnabled" && value) clearIslandBars(); } // Frame mode hosts bars inside the frame surface, which has nowhere to put an island. function clearIslandBars() { const islands = islandBarConfigs; if (islands.length === 0) return; const configs = JSON.parse(JSON.stringify(barConfigs)); for (const cfg of configs) delete cfg.island; barConfigs = configs; updateBarConfigs(); } function loadSettings() { _loading = true; _parseError = false; _hasUnsavedChanges = false; _pendingMigration = null; try { const txt = settingsFile.text(); let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; const oldVersion = obj?.configVersion ?? 0; const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null; const sessionPayload = oldVersion < 15 ? Store.extractSessionPayload(obj) : null; const cachePayload = oldVersion < 15 ? Store.extractCachePayload(obj) : null; if (oldVersion < settingsConfigVersion) { const migrated = Store.migrateToVersion(obj, settingsConfigVersion); if (migrated) { _pendingMigration = migrated; obj = migrated; } } 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; } Store.parse(root, obj); // set() enforces this pair, but a hand-edited settings.json bypasses set() entirely. if (frameEnabled && islandBarConfigs.length > 0) 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(); } _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasLoaded = true; _mergeSessionState(); applyStoredTheme(); updateCompositorCursor(); Qt.callLater(checkIconThemeDrift); _checkSettingsWritable(); } catch (e) { _parseError = true; const msg = e.message; log.error("Failed to parse settings.json - file will not be overwritten. Error:", msg); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("settings.json"), msg)); applyStoredTheme(); } finally { _loading = false; } loadPluginSettings(); Qt.callLater(() => _reconcileConnectedFrameBarStyles()); } property var _pendingMigration: null function _mergeSessionState() { if (!_hasLoaded || !SessionData._hasLoaded) return; 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 _checkSettingsWritable() { settingsWritableCheckProcess.running = true; } function _onWritableCheckComplete(writable) { const wasReadOnly = _isReadOnly; _isReadOnly = !writable; if (_isReadOnly) { _hasUnsavedChanges = _checkForUnsavedChanges(); if (!wasReadOnly) log.info("settings.json is now read-only"); } else { _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasUnsavedChanges = false; if (wasReadOnly) log.info("settings.json is now writable"); if (_pendingMigration) settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); } _pendingMigration = null; } function _checkForUnsavedChanges() { if (!_hasLoaded || !_loadedSettingsSnapshot) return false; const current = JSON.stringify(Store.toJson(root)); return current !== _loadedSettingsSnapshot; } function getCurrentSettingsJson() { return JSON.stringify(Store.toJson(root), 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 saveSettings() { if (_loading || _parseError || !_hasLoaded) return; _selfWrite = true; settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2)); if (_isReadOnly) _checkSettingsWritable(); } function savePluginSettings() { if (_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 _hasConnectedFrameBarStyleBackups() { return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0; } function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) { if (!Array.isArray(configs)) return; const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {})); const validIds = {}; let changed = false; for (let i = 0; i < configs.length; i++) { const config = configs[i]; if (!config?.id) continue; validIds[config.id] = true; if (!overwriteExisting && nextBackups[config.id] !== undefined) continue; const snapshot = _connectedFrameBarStyleSnapshot(config); if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) { nextBackups[config.id] = snapshot; changed = true; } } if (overwriteExisting) { for (const barId in nextBackups) { if (validIds[barId]) continue; delete nextBackups[barId]; changed = true; } } if (changed) connectedFrameBarStyleBackups = nextBackups; } function _restoreConnectedFrameBarStyleBackups() { if (!_hasConnectedFrameBarStyleBackups()) return; const backups = connectedFrameBarStyleBackups || {}; const configs = JSON.parse(JSON.stringify(barConfigs)); let changed = false; for (let i = 0; i < configs.length; i++) { const backup = backups[configs[i].id]; if (!backup) continue; for (const key in backup) { if (configs[i][key] === backup[key]) continue; configs[i][key] = backup[key]; changed = true; } } if (changed) barConfigs = configs; connectedFrameBarStyleBackups = ({}); if (changed) updateBarConfigs(); } // Zeroes out connected-mode-hostile fields (shadow, square/goth corners, edge attach, border). // Returns { configs, changed } — `configs` is the same ref when no change. function _sanitizeBarConfigsForConnectedFrame(configs) { if (!connectedFrameModeActive || !Array.isArray(configs)) return { "configs": configs, "changed": false }; let anyChanged = false; const out = configs.map(cfg => { if (!cfg) return cfg; let dirty = false; const s = Object.assign({}, cfg); if ((s.shadowIntensity ?? 0) !== 0) { s.shadowIntensity = 0; dirty = true; } if (s.squareCorners ?? false) { s.squareCorners = false; dirty = true; } if (s.attachToScreenEdge ?? false) { s.attachToScreenEdge = false; dirty = true; } if (s.gothCornersEnabled ?? false) { s.gothCornersEnabled = false; dirty = true; } if (s.borderEnabled ?? false) { s.borderEnabled = false; dirty = true; } if (dirty) anyChanged = true; return dirty ? s : cfg; }); return { "configs": anyChanged ? out : configs, "changed": anyChanged }; } function effectiveBarConfigForRender(config, usesFrameBarChrome) { if (!config || !connectedFrameModeActive || usesFrameBarChrome) return config; const backup = connectedFrameBarStyleBackups[config.id]; if (!backup) return config; return Object.assign({}, config, backup); } // Single entry point for connected-mode settings state. // !active → restore backups function _reconcileConnectedFrameBarStyles() { if (!connectedFrameModeActive) { _restoreConnectedFrameBarStyleBackups(); return; } if (!_hasConnectedFrameBarStyleBackups()) _captureConnectedFrameBarStyleBackups(barConfigs, true); const result = _sanitizeBarConfigsForConnectedFrame(barConfigs); if (result.changed) { barConfigs = result.configs; 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) { const relativeX = pos.x; const relativeY = pos.y; const defaultBar = getPrimaryBarConfig(); const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4); const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0); const isConnected = connectedFrameModeActive; const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap); const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true); const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4); const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue); const edgeSpacing = isConnected ? 0 : spacing; switch (position) { case SettingsData.Position.Left: return { "x": barThickness + edgeSpacing + popupGap, "y": relativeY, "width": widgetWidth }; case SettingsData.Position.Right: return { "x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap), "y": relativeY, "width": widgetWidth }; case SettingsData.Position.Bottom: return { "x": relativeX, "y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap), "width": widgetWidth }; default: return { "x": relativeX, "y": barThickness + edgeSpacing + bottomGap + popupGap, "width": widgetWidth }; } } function getAdjacentBarInfo(screen, barPosition, barConfig) { if (!screen || !barConfig) { return { "topBar": 0, "bottomBar": 0, "leftBar": 0, "rightBar": 0 }; } if (barConfig.autoHide) { return { "topBar": 0, "bottomBar": 0, "leftBar": 0, "rightBar": 0 }; } const enabledBars = getEnabledBarConfigs(); const defaultBar = getPrimaryBarConfig(); const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); let topBar = 0; let bottomBar = 0; let leftBar = 0; let rightBar = 0; for (var i = 0; i < enabledBars.length; i++) { const other = enabledBars[i]; if (other.id === barConfig.id) continue; if (other.autoHide) continue; if (!barConfigCoversScreen(other, screen)) continue; const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4); const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4); const otherThickness = Theme.barThickness(otherPadding, CompositorService.getScreenScale(screen)) + otherSpacing; const useAutoGaps = other.popupGapsAuto !== undefined ? other.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true); const manualGap = other.popupGapsManual !== undefined ? other.popupGapsManual : (defaultBar?.popupGapsManual ?? 4); const popupGap = useAutoGaps ? Math.max(4, otherSpacing) : manualGap; switch (other.position) { case SettingsData.Position.Top: topBar = Math.max(topBar, otherThickness + popupGap); break; case SettingsData.Position.Bottom: bottomBar = Math.max(bottomBar, otherThickness + popupGap); break; case SettingsData.Position.Left: leftBar = Math.max(leftBar, otherThickness + popupGap); break; case SettingsData.Position.Right: rightBar = Math.max(rightBar, otherThickness + popupGap); break; } } // The island is not a bar, but it is chrome: popouts still have to clear its strip. if (!isIslandBarConfig(barConfig)) { topBar = Math.max(topBar, dankIslandEdgeOffset(screen, "top")); bottomBar = Math.max(bottomBar, dankIslandEdgeOffset(screen, "bottom")); leftBar = Math.max(leftBar, dankIslandEdgeOffset(screen, "left")); rightBar = Math.max(rightBar, dankIslandEdgeOffset(screen, "right")); } return { "topBar": topBar, "bottomBar": bottomBar, "leftBar": leftBar, "rightBar": rightBar }; } function getBarBounds(screen, barThickness, barPosition, barConfig) { if (!screen) { return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }; } const defaultBar = getPrimaryBarConfig(); const wingRadius = (defaultBar?.gothCornerRadiusOverride ?? false) ? (defaultBar?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius; const wingSize = (defaultBar?.gothCornersEnabled ?? false) ? Math.max(0, wingRadius) : 0; const screenWidth = screen.width; const screenHeight = screen.height; const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); const isConnected = connectedFrameModeActive; const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0); const bottomGap = isConnected ? 0 : rawBottomGap; let topOffset = 0; let bottomOffset = 0; let leftOffset = 0; let rightOffset = 0; if (barConfig) { const enabledBars = getEnabledBarConfigs(); for (var i = 0; i < enabledBars.length; i++) { const other = enabledBars[i]; if (other.id === barConfig.id) continue; if (!barConfigCoversScreen(other, screen)) continue; const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4); const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4); const otherThickness = Theme.barThickness(otherPadding, CompositorService.getScreenScale(screen)) + otherSpacing + wingSize; const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0)); switch (other.position) { case SettingsData.Position.Top: if (position === SettingsData.Position.Top && other.id < barConfig.id) { topOffset += otherThickness; // Simple stacking for same pos } else if (position === SettingsData.Position.Left || position === SettingsData.Position.Right) { topOffset = Math.max(topOffset, otherThickness); } break; case SettingsData.Position.Bottom: if (position === SettingsData.Position.Bottom && other.id < barConfig.id) { bottomOffset += (otherThickness + otherBottomGap); } else if (position === SettingsData.Position.Left || position === SettingsData.Position.Right) { bottomOffset = Math.max(bottomOffset, otherThickness + otherBottomGap); } break; case SettingsData.Position.Left: if (position === SettingsData.Position.Top || position === SettingsData.Position.Bottom) { leftOffset = Math.max(leftOffset, otherThickness); } else if (position === SettingsData.Position.Left && other.id < barConfig.id) { leftOffset += otherThickness; } break; case SettingsData.Position.Right: if (position === SettingsData.Position.Top || position === SettingsData.Position.Bottom) { rightOffset = Math.max(rightOffset, otherThickness); } else if (position === SettingsData.Position.Right && other.id < barConfig.id) { rightOffset += otherThickness; } break; } } } switch (position) { case SettingsData.Position.Top: return { "x": leftOffset, "y": topOffset + bottomGap, "width": screenWidth - leftOffset - rightOffset, "height": barThickness + wingSize, "wingSize": wingSize }; case SettingsData.Position.Bottom: return { "x": leftOffset, "y": screenHeight - barThickness - wingSize - bottomGap - bottomOffset, "width": screenWidth - leftOffset - rightOffset, "height": barThickness + wingSize, "wingSize": wingSize }; case SettingsData.Position.Left: return { "x": 0, "y": topOffset, "width": barThickness + wingSize, "height": screenHeight - topOffset - bottomOffset, "wingSize": wingSize }; case SettingsData.Position.Right: return { "x": screenWidth - barThickness - wingSize, "y": topOffset, "width": barThickness + wingSize, "height": screenHeight - topOffset - bottomOffset, "wingSize": wingSize }; } return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }; } function updateBarConfigs() { barConfigsChanged(); saveSettings(); } function getBarConfig(barId) { return barConfigs.find(cfg => cfg.id === barId) || null; } function setBarIsland(barId, on) { const config = getBarConfig(barId); if (!config || (config.island === true) === (on === true)) return; const updates = { island: on === true }; if (on === true) { if (!config.enabled) updates.enabled = true; if ((config.screenPreferences ?? []).length === 0) updates.screenPreferences = ["all"]; } updateBarConfig(barId, updates); } 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); if (connectedFrameModeActive) _captureConnectedFrameBarStyleBackups(configs, false); barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs; 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); barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs; updateBarConfigs(); 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(); } // Bar-kind instances only. Islands reserve their own edges via dankIslandOwnsEdge. function getEnabledBarConfigs() { return barConfigs.filter(cfg => cfg.enabled && !isIslandBarConfig(cfg)); } 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) { switch (side) { case "top": return SettingsData.Position.Top; case "bottom": return SettingsData.Position.Bottom; case "left": return SettingsData.Position.Left; case "right": return SettingsData.Position.Right; } return -1; } function positionToSide(pos) { switch (pos) { case SettingsData.Position.Top: return "top"; case SettingsData.Position.Bottom: return "bottom"; case SettingsData.Position.Left: return "left"; case SettingsData.Position.Right: return "right"; } return ""; } // Check if a bar occupies the specified screen edge function barOccupiesSide(screen, side) { if (!screen) return false; const sidePos = _sideToPosition(side); if (sidePos < 0) return false; const bars = getEnabledBarConfigs(); for (var i = 0; i < bars.length; i++) { const bc = bars[i]; if (bc.position !== sidePos) continue; if (barConfigCoversScreen(bc, screen)) return true; } return false; } // Check if the dock occupies the specified screen edge. function dockOccupiesSide(side) { if (!showDock) return false; return dockPosition === _sideToPosition(side); } function getScreensSortedByPosition() { const screens = []; for (var i = 0; i < Quickshell.screens.length; i++) { screens.push(Quickshell.screens[i]); } screens.sort((a, b) => { if (a.x !== b.x) return a.x - b.x; return a.y - b.y; }); return screens; } function getScreenModelIndex(screen) { if (!screen || !screen.model) return -1; const sorted = getScreensSortedByPosition(); let modelCount = 0; let screenIndex = -1; for (var i = 0; i < sorted.length; i++) { if (sorted[i].model === screen.model) { if (sorted[i].name === screen.name) { screenIndex = modelCount; } modelCount++; } } if (modelCount <= 1) return -1; return screenIndex; } 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) { if (!screen) return false; const screenDisplayName = getScreenDisplayName(screen); return prefs.some(pref => { if (typeof pref === "string") { if (pref === "all" || pref === screen.name) return true; if (displayNameMode === "model") { return pref === screenDisplayName; } return pref === screen.model; } if (displayNameMode === "model") { if (pref.model && screen.model) { if (pref.modelIndex !== undefined) { const screenModelIndex = getScreenModelIndex(screen); return pref.model === screen.model && pref.modelIndex === screenModelIndex; } return pref.model === screen.model; } return false; } return pref.name === screen.name; }); } 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) { var prefs = bc?.screenPreferences || ["all"]; if (prefs.includes("all") || isScreenInPreferences(screen, prefs)) return true; return (bc?.showOnLastDisplay ?? false) && Quickshell.screens.length === 1; } function isIslandBarConfig(bc) { return !!bc && bc.island === true; } function activeIslandConfigsForScreen(screen) { if (!screen) return []; return islandBarConfigs.filter(cfg => (cfg.enabled ?? false) && barConfigCoversScreen(cfg, screen)); } // Only the first island claiming an edge renders there; the rest would stack on top of it. function islandConfigForEdge(screen, edge) { return activeIslandConfigsForScreen(screen).find(cfg => islandEdge(cfg) === edge) ?? null; } function dankIslandCoversScreen(screen) { return activeIslandConfigsForScreen(screen).length > 0; } function dankIslandOwnsEdge(screen, edge) { return islandConfigForEdge(screen, edge) !== null; } // Painted strip thickness on `edge`, including when floating drops the exclusive zone. function dankIslandEdgeOffset(screen, edge) { const cfg = islandConfigForEdge(screen, edge); return cfg ? islandStripThickness(cfg) : 0; } function dankIslandIsSoleBarForScreen(screen) { return dankIslandCoversScreen(screen) && getActiveBarEdgesForScreen(screen).length === 0; } // Edges an island already holds on every screen this config covers, so a second island // instance cannot be dropped on top of it. function islandEdgeTakenFor(bc, screen, edge) { const owner = islandConfigForEdge(screen, edge); return !!owner && owner.id !== bc?.id; } function getActiveBarEdgesForScreen(screen) { return ["top", "bottom", "left", "right"].filter(edge => getActiveBarConfigsForEdge(screen, edge).length > 0); } function getOverlayBarEdgesForScreen(screen) { return ["top", "bottom", "left", "right"].filter(edge => getActiveBarConfigsForEdge(screen, edge).some(bc => bc.useOverlayLayer ?? false)); } readonly property real frameBarContentGap: frameBarInsetPadding < 0 ? frameThickness : frameBarInsetPadding readonly property real frameBarContentGapExtra: Math.max(0, frameBarContentGap - frameThickness) function getActiveBarConfigsForEdge(screen, edge) { if (!screen) return []; const sidePos = _sideToPosition(edge); if (sidePos < 0 || dankIslandOwnsEdge(screen, edge)) return []; return barConfigs.filter(bc => bc.enabled && (bc.position ?? SettingsData.Position.Top) === sidePos && !isIslandBarConfig(bc) && barConfigCoversScreen(bc, screen)); } function getFrameHostedBarConfigsForEdge(screen, edge) { return getActiveBarConfigsForEdge(screen, edge).filter(bc => !(bc.useOverlayLayer ?? false)); } // Connected mode hosts one row per non-overlay bar; an edge holding only overlay bars keeps one band. function frameEdgeReservation(screen, edge) { if (!screen) return 0; const active = getActiveBarConfigsForEdge(screen, edge); if (active.length === 0) return frameThickness; const rows = FrameTransitionState.effectiveConnectedFrameModeActive ? active.filter(bc => !(bc.useOverlayLayer ?? false)) : active; return frameBarSize * Math.max(1, rows.length); } 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); if (typeof Theme !== "undefined") { Theme.generateSystemThemesFromCurrentTheme(); } } function setMatugenSmartMode(enabled) { if (matugenSmartMode === enabled) return; set("matugenSmartMode", enabled); } function setMatugenSourceMode(mode) { var normalized = mode || "dominant"; if (matugenSourceMode === normalized) return; // Regeneration comes from the regenSystemThemes onChange hook in // SettingsSpec.js, which set() dispatches. matugenScheme above also // calls Theme.generateSystemThemesFromCurrentTheme() directly, which is // redundant with its own hook. set("matugenSourceMode", normalized); } function setMatugenContrast(value) { if (matugenContrast === value) return; set("matugenContrast", value); } function setMatugenTargetMonitor(monitorName) { if (matugenTargetMonitor === monitorName) return; set("matugenTargetMonitor", monitorName); if (typeof Theme !== "undefined") { Theme.generateSystemThemesFromCurrentTheme(); } } function setCornerRadius(radius) { set("cornerRadius", radius); updateCompositorLayout(); } 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 setShowDock(enabled) { showDock = enabled; const defaultBar = getPrimaryBarConfig(); // -1 matches no Position, so island-only setups have no dock conflict. const barPos = defaultBar ? (defaultBar.position ?? SettingsData.Position.Top) : -1; if (enabled && dockPosition === barPos) { if (barPos === SettingsData.Position.Top) { setDockPosition(SettingsData.Position.Bottom); return; } if (barPos === SettingsData.Position.Bottom) { setDockPosition(SettingsData.Position.Top); return; } if (barPos === SettingsData.Position.Left) { setDockPosition(SettingsData.Position.Right); return; } if (barPos === SettingsData.Position.Right) { setDockPosition(SettingsData.Position.Left); return; } } saveSettings(); } function setDockPosition(position) { dockPosition = position; const defaultBar = getPrimaryBarConfig(); // -1 matches no Position, so island-only setups have no dock conflict. const barPos = defaultBar ? (defaultBar.position ?? SettingsData.Position.Top) : -1; if (position === SettingsData.Position.Bottom && barPos === SettingsData.Position.Bottom && showDock) { setDankBarPosition(SettingsData.Position.Top); } if (position === SettingsData.Position.Top && barPos === SettingsData.Position.Top && showDock) { setDankBarPosition(SettingsData.Position.Bottom); } if (position === SettingsData.Position.Left && barPos === SettingsData.Position.Left && showDock) { setDankBarPosition(SettingsData.Position.Right); } if (position === SettingsData.Position.Right && barPos === SettingsData.Position.Right && showDock) { setDankBarPosition(SettingsData.Position.Left); } saveSettings(); Qt.callLater(() => forceDockLayoutRefresh()); } function setDankBarPosition(position) { const defaultBar = getPrimaryBarConfig(); if (!defaultBar) return; if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) { setDockPosition(SettingsData.Position.Top); return; } if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) { setDockPosition(SettingsData.Position.Bottom); return; } if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) { setDockPosition(SettingsData.Position.Right); return; } if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) { setDockPosition(SettingsData.Position.Left); return; } updateBarConfig(defaultBar.id, { "position": position }); } 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 addMediaExcludePlayer(identity) { if (identity === undefined || identity === null) return; var normalizedIdentity = identity.toString().trim().toLowerCase(); if (!normalizedIdentity) return; var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; var normalizedList = list.map(function (id) { return id ? id.toString().trim().toLowerCase() : ""; }); if (normalizedList.indexOf(normalizedIdentity) >= 0) return; list.push(normalizedIdentity); mediaExcludePlayers = list; saveSettings(); } function removeMediaExcludePlayer(index) { var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; if (index < 0 || index >= list.length) return; list.splice(index, 1); mediaExcludePlayers = list; saveSettings(); } property bool _pendingExpandNotificationRules: false property int _pendingNotificationRuleIndex: -1 function _newNotificationRule(overrides) { return Object.assign({ enabled: true, field: "appName", pattern: "", matchType: "contains", action: "default", urgency: "default", bypassDnd: false }, overrides || {}); } function addNotificationRule() { var rules = JSON.parse(JSON.stringify(notificationRules || [])); rules.push(_newNotificationRule()); notificationRules = rules; saveSettings(); } function addNotificationRuleForNotification(appName, desktopEntry) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); var pattern = desktopEntry || appName || ""; rules.push(_newNotificationRule(pattern ? { field: desktopEntry ? "desktopEntry" : "appName", pattern: pattern, matchType: "exact" } : {})); notificationRules = rules; saveSettings(); var index = rules.length - 1; _pendingExpandNotificationRules = true; _pendingNotificationRuleIndex = index; return index; } function _isMuteRule(rule) { return (rule.action || "").toString().toLowerCase() === "mute"; } function _isDndBypassRule(rule) { return rule.bypassDnd === true; } 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)) return false; const pattern = (rule.pattern || "").toString().toLowerCase(); return pattern !== "" && (pattern === app || pattern === desktop); }); } function _addAppRule(appName, desktopEntry, overrides) { const pattern = desktopEntry || appName || ""; if (!pattern) return; var rules = JSON.parse(JSON.stringify(notificationRules || [])); rules.push(_newNotificationRule(Object.assign({ field: desktopEntry ? "desktopEntry" : "appName", pattern: pattern, matchType: "exact" }, overrides))); notificationRules = rules; saveSettings(); } function _removeAppRule(appName, desktopEntry, predicate) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); const index = _appRuleIndex(rules, appName, desktopEntry, predicate); if (index === -1) return; rules.splice(index, 1); notificationRules = rules; saveSettings(); } function addMuteRuleForApp(appName, desktopEntry) { _addAppRule(appName, desktopEntry, { action: "mute" }); } function isAppMuted(appName, desktopEntry) { return _appRuleIndex(notificationRules || [], appName, desktopEntry, rule => rule.enabled !== false && _isMuteRule(rule)) !== -1; } function removeMuteRuleForApp(appName, desktopEntry) { _removeAppRule(appName, desktopEntry, _isMuteRule); } function isAppDndBypassed(appName, desktopEntry) { return _appRuleIndex(notificationRules || [], appName, desktopEntry, rule => rule.enabled !== false && _isDndBypassRule(rule)) !== -1; } function setAppDndBypass(appName, desktopEntry, enabled) { if (!enabled) { _removeAppRule(appName, desktopEntry, _isDndBypassRule); return; } if (isAppDndBypassed(appName, desktopEntry)) return; _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 toggleShowDock() { setShowDock(!showDock); } 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 } property alias settingsFile: settingsFile Timer { id: settingsFileReloadDebounce interval: 50 onTriggered: settingsFile.reload() repeat: false } FileView { id: settingsFile path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" blockLoading: true blockWrites: true atomicWrites: true watchChanges: !isGreeterMode onFileChanged: { if (_selfWrite) { _selfWrite = false; return; } settingsFileReloadDebounce.restart(); } onLoaded: { if (isGreeterMode) return; const wasLoaded = _hasLoaded; const prevFrameEnabled = frameEnabled; const prevFrameMode = frameMode; _loading = true; _hasUnsavedChanges = false; try { const txt = settingsFile.text(); if (!txt || !txt.trim()) { _parseError = true; return; } const obj = JSON.parse(txt); _parseError = false; Store.parse(root, obj); 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(); } _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasLoaded = true; applyStoredTheme(); updateCompositorCursor(); } catch (e) { _parseError = true; const msg = e.message; log.error("Failed to reload settings.json - file will not be overwritten. Error:", msg); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("settings.json"), msg)); } finally { _loading = false; } // External edits reload under _loading, which skips the per-property transition triggers if (wasLoaded && !_parseError && (frameEnabled !== prevFrameEnabled || (frameEnabled && frameMode !== prevFrameMode))) updateFrameCompositorLayout(); } onLoadFailed: error => { if (isGreeterMode) return; applyStoredTheme(); } onSaveFailed: error => { root._isReadOnly = true; root._hasUnsavedChanges = root._checkForUnsavedChanges(); } } FileView { id: pluginSettingsFile path: isGreeterMode ? "" : pluginSettingsPath blockLoading: true blockWrites: true atomicWrites: true printErrors: false watchChanges: !isGreeterMode onLoaded: { if (isGreeterMode) return; parsePluginSettings(pluginSettingsFile.text()); } onLoadFailed: error => { if (isGreeterMode) return; const msg = String(error || ""); if (!_isMissingPluginSettingsError(error)) log.warn("Failed to load plugin_settings.json. Error:", msg); _resetPluginSettings(); } } property bool pluginSettingsFileExists: false Process { id: settingsWritableCheckProcess property string settingsPath: Paths.strip(settingsFile.path) command: ["sh", "-c", "[ ! -f \"" + settingsPath + "\" ] || [ -w \"" + settingsPath + "\" ] && echo 'writable' || echo 'readonly'"] running: false stdout: StdioCollector { onStreamFinished: { const result = text.trim(); root._onWritableCheckComplete(result === "writable"); } } } }