diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index e5094a06..8016d7fb 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -7,16 +7,20 @@ import Quickshell import Quickshell.Io import qs.Common import qs.Services +import "settings/SessionSpec.js" as Spec +import "settings/SessionStore.js" as Store Singleton { id: root - readonly property int sessionConfigVersion: 1 + readonly property int sessionConfigVersion: 2 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" - property bool hasTriedDefaultSession: false property bool _parseError: false property bool _hasLoaded: false + property bool _isReadOnly: false + property bool _hasUnsavedChanges: false + property var _loadedSessionSnapshot: null readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) readonly property string _stateDir: Paths.strip(_stateUrl) @@ -95,18 +99,52 @@ Singleton { property string wifiDeviceOverride: "" property bool weatherHourlyDetailed: true + property string weatherLocation: "New York, NY" + property string weatherCoordinates: "40.7128,-74.0060" + Component.onCompleted: { if (!isGreeterMode) { loadSettings(); } } + property var _pendingMigration: null + function loadSettings() { + _hasUnsavedChanges = false; + _pendingMigration = null; + if (isGreeterMode) { parseSettings(greeterSessionFile.text()); - } else { - parseSettings(settingsFile.text()); + return; } + parseSettings(settingsFile.text()); + _checkSessionWritable(); + } + + function _checkSessionWritable() { + sessionWritableCheckProcess.running = true; + } + + function _onWritableCheckComplete(writable) { + _isReadOnly = !writable; + if (_isReadOnly) { + console.info("SessionData: session.json is read-only (NixOS home-manager mode)"); + } else if (_pendingMigration) { + settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); + } + _pendingMigration = null; + } + + function _checkForUnsavedChanges() { + if (!_hasLoaded || !_loadedSessionSnapshot) + return false; + const current = getCurrentSessionJson(); + return current !== _loadedSessionSnapshot; + } + + function getCurrentSessionJson() { + return JSON.stringify(Store.toJson(root), null, 2); } function parseSettings(content) { @@ -116,79 +154,48 @@ Singleton { _parseError = true; return; } - var settings = JSON.parse(content); - isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false; - if (settings.wallpaperPath && settings.wallpaperPath.startsWith("we:")) { + let obj = JSON.parse(content); + + if (obj.brightnessLogarithmicDevices && !obj.brightnessExponentialDevices) { + obj.brightnessExponentialDevices = obj.brightnessLogarithmicDevices; + } + + if (obj.nightModeStartTime !== undefined) { + const parts = obj.nightModeStartTime.split(":"); + obj.nightModeStartHour = parseInt(parts[0]) || 18; + obj.nightModeStartMinute = parseInt(parts[1]) || 0; + } + if (obj.nightModeEndTime !== undefined) { + const parts = obj.nightModeEndTime.split(":"); + obj.nightModeEndHour = parseInt(parts[0]) || 6; + obj.nightModeEndMinute = parseInt(parts[1]) || 0; + } + + const oldVersion = obj.configVersion ?? 0; + if (oldVersion === 0) { + migrateFromUndefinedToV1(obj); + } + + if (oldVersion < sessionConfigVersion) { + const settingsDataRef = (typeof SettingsData !== "undefined") ? SettingsData : null; + const migrated = Store.migrateToVersion(obj, sessionConfigVersion, settingsDataRef); + if (migrated) { + _pendingMigration = migrated; + obj = migrated; + } + } + + Store.parse(root, obj); + + if (wallpaperPath && wallpaperPath.startsWith("we:")) { console.warn("WallpaperEngine wallpaper detected, resetting wallpaper"); wallpaperPath = ""; Quickshell.execDetached(["notify-send", "-u", "critical", "-a", "DMS", "-i", "dialog-warning", "WallpaperEngine Support Moved", "WallpaperEngine support has been moved to a plugin. Please enable the Linux Wallpaper Engine plugin in Settings → Plugins to continue using WallpaperEngine."]); - } else { - wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""; } - perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false; - monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}; - perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false; - wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""; - wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""; - monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}; - monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}; - brightnessExponentialDevices = settings.brightnessExponentialDevices !== undefined ? settings.brightnessExponentialDevices : (settings.brightnessLogarithmicDevices || {}); - brightnessUserSetValues = settings.brightnessUserSetValues !== undefined ? settings.brightnessUserSetValues : {}; - brightnessExponentValues = settings.brightnessExponentValues !== undefined ? settings.brightnessExponentValues : {}; - doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false; - nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false; - nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500; - nightModeHighTemperature = settings.nightModeHighTemperature !== undefined ? settings.nightModeHighTemperature : 6500; - nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false; - nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"; - if (settings.nightModeStartTime !== undefined) { - const parts = settings.nightModeStartTime.split(":"); - nightModeStartHour = parseInt(parts[0]) || 18; - nightModeStartMinute = parseInt(parts[1]) || 0; - } else { - nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18; - nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0; - } - if (settings.nightModeEndTime !== undefined) { - const parts = settings.nightModeEndTime.split(":"); - nightModeEndHour = parseInt(parts[0]) || 6; - nightModeEndMinute = parseInt(parts[1]) || 0; - } else { - nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6; - nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0; - } - latitude = settings.latitude !== undefined ? settings.latitude : 0.0; - longitude = settings.longitude !== undefined ? settings.longitude : 0.0; - nightModeUseIPLocation = settings.nightModeUseIPLocation !== undefined ? settings.nightModeUseIPLocation : false; - nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""; - pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []; - hiddenTrayIds = settings.hiddenTrayIds !== undefined ? settings.hiddenTrayIds : []; - selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0; - nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false; - nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false; - enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []; - wifiDeviceOverride = settings.wifiDeviceOverride !== undefined ? settings.wifiDeviceOverride : ""; - weatherHourlyDetailed = settings.weatherHourlyDetailed !== undefined ? settings.weatherHourlyDetailed : true; - wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false; - wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"; - wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300; - wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"; - monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}; - lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""; - launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""; - wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"; - includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none"); - recentColors = settings.recentColors !== undefined ? settings.recentColors : []; - showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false; - _hasLoaded = true; - if (settings.configVersion === undefined) { - migrateFromUndefinedToV1(settings); - saveSettings(); - } else if (settings.configVersion === sessionConfigVersion) { - cleanupUnusedKeys(); - } + _hasLoaded = true; + _loadedSessionSnapshot = getCurrentSessionJson(); if (!isGreeterMode && typeof Theme !== "undefined") { Theme.generateSystemThemesFromCurrentTheme(); @@ -208,54 +215,11 @@ Singleton { function saveSettings() { if (isGreeterMode || _parseError || !_hasLoaded) return; - settingsFile.setText(JSON.stringify({ - "isLightMode": isLightMode, - "wallpaperPath": wallpaperPath, - "perMonitorWallpaper": perMonitorWallpaper, - "monitorWallpapers": monitorWallpapers, - "perModeWallpaper": perModeWallpaper, - "wallpaperPathLight": wallpaperPathLight, - "wallpaperPathDark": wallpaperPathDark, - "monitorWallpapersLight": monitorWallpapersLight, - "monitorWallpapersDark": monitorWallpapersDark, - "brightnessExponentialDevices": brightnessExponentialDevices, - "brightnessUserSetValues": brightnessUserSetValues, - "brightnessExponentValues": brightnessExponentValues, - "doNotDisturb": doNotDisturb, - "nightModeEnabled": nightModeEnabled, - "nightModeTemperature": nightModeTemperature, - "nightModeHighTemperature": nightModeHighTemperature, - "nightModeAutoEnabled": nightModeAutoEnabled, - "nightModeAutoMode": nightModeAutoMode, - "nightModeStartHour": nightModeStartHour, - "nightModeStartMinute": nightModeStartMinute, - "nightModeEndHour": nightModeEndHour, - "nightModeEndMinute": nightModeEndMinute, - "latitude": latitude, - "longitude": longitude, - "nightModeUseIPLocation": nightModeUseIPLocation, - "nightModeLocationProvider": nightModeLocationProvider, - "pinnedApps": pinnedApps, - "hiddenTrayIds": hiddenTrayIds, - "selectedGpuIndex": selectedGpuIndex, - "nvidiaGpuTempEnabled": nvidiaGpuTempEnabled, - "nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled, - "enabledGpuPciIds": enabledGpuPciIds, - "wifiDeviceOverride": wifiDeviceOverride, - "weatherHourlyDetailed": weatherHourlyDetailed, - "wallpaperCyclingEnabled": wallpaperCyclingEnabled, - "wallpaperCyclingMode": wallpaperCyclingMode, - "wallpaperCyclingInterval": wallpaperCyclingInterval, - "wallpaperCyclingTime": wallpaperCyclingTime, - "monitorCyclingSettings": monitorCyclingSettings, - "lastBrightnessDevice": lastBrightnessDevice, - "launchPrefix": launchPrefix, - "wallpaperTransition": wallpaperTransition, - "includedTransitions": includedTransitions, - "recentColors": recentColors, - "showThirdPartyPlugins": showThirdPartyPlugins, - "configVersion": sessionConfigVersion - }, null, 2)); + if (_isReadOnly) { + _hasUnsavedChanges = _checkForUnsavedChanges(); + return; + } + settingsFile.setText(getCurrentSessionJson()); } function migrateFromUndefinedToV1(settings) { @@ -306,32 +270,6 @@ Singleton { } } - function cleanupUnusedKeys() { - const validKeys = ["isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper", "wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight", "monitorWallpapersDark", "doNotDisturb", "nightModeEnabled", "nightModeTemperature", "nightModeHighTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "hiddenTrayIds", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wifiDeviceOverride", "weatherHourlyDetailed", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "brightnessExponentialDevices", "brightnessUserSetValues", "brightnessExponentValues", "launchPrefix", "wallpaperTransition", "includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"]; - - try { - const content = settingsFile.text(); - if (!content || !content.trim()) - return; - const settings = JSON.parse(content); - let needsSave = false; - - for (const key in settings) { - if (!validKeys.includes(key)) { - console.log("SessionData: Removing unused key:", key); - delete settings[key]; - needsSave = true; - } - } - - if (needsSave) { - settingsFile.setText(JSON.stringify(settings, null, 2)); - } - } catch (e) { - console.warn("SessionData: Failed to cleanup unused keys:", e.message); - } - } - function setLightMode(lightMode) { isSwitchingMode = true; isLightMode = lightMode; @@ -923,6 +861,12 @@ Singleton { saveSettings(); } + function setWeatherLocation(displayName, coordinates) { + weatherLocation = displayName; + weatherCoordinates = coordinates; + saveSettings(); + } + function syncWallpaperForCurrentMode() { if (!perModeWallpaper) return; @@ -1005,14 +949,8 @@ Singleton { watchChanges: !isGreeterMode onLoaded: { if (!isGreeterMode) { + _hasUnsavedChanges = false; parseSettings(settingsFile.text()); - hasTriedDefaultSession = false; - } - } - onLoadFailed: error => { - if (!isGreeterMode && !hasTriedDefaultSession) { - hasTriedDefaultSession = true; - defaultSessionCheckProcess.running = true; } } } @@ -1037,14 +975,17 @@ Singleton { } Process { - id: defaultSessionCheckProcess + id: sessionWritableCheckProcess - command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir + "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"] + property string sessionPath: Paths.strip(settingsFile.path) + + command: ["sh", "-c", "[ -w \"" + sessionPath + "\" ] && echo 'writable' || echo 'readonly'"] running: false - onExited: exitCode => { - if (exitCode === 0) { - console.info("Copied default-session.json to session.json"); - settingsFile.reload(); + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim(); + root._onWritableCheckComplete(result === "writable"); } } } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 609f0903..8e747f5b 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store Singleton { id: root - readonly property int settingsConfigVersion: 4 + readonly property int settingsConfigVersion: 5 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" @@ -58,7 +58,9 @@ Singleton { property bool _parseError: false property bool _pluginParseError: false property bool _hasLoaded: false - property bool hasTriedDefaultSettings: false + property bool _isReadOnly: false + property bool _hasUnsavedChanges: false + property var _loadedSettingsSnapshot: null property var pluginSettings: ({}) property alias dankBarLeftWidgetsModel: leftWidgetsModel @@ -202,8 +204,10 @@ Singleton { property bool spotlightCloseNiriOverview: true property bool niriOverviewOverlayEnabled: true - property string weatherLocation: "New York, NY" - property string weatherCoordinates: "40.7128,-74.0060" + property string _legacyWeatherLocation: "New York, NY" + property string _legacyWeatherCoordinates: "40.7128,-74.0060" + readonly property string weatherLocation: SessionData.weatherLocation + readonly property string weatherCoordinates: SessionData.weatherCoordinates property bool useAutoLocation: false property bool weatherEnabled: true @@ -776,6 +780,9 @@ Singleton { function loadSettings() { _loading = true; _parseError = false; + _hasUnsavedChanges = false; + _pendingMigration = null; + try { const txt = settingsFile.text(); let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; @@ -784,16 +791,25 @@ Singleton { if (oldVersion < settingsConfigVersion) { const migrated = Store.migrateToVersion(obj, settingsConfigVersion); if (migrated) { - settingsFile.setText(JSON.stringify(migrated, null, 2)); + _pendingMigration = migrated; obj = migrated; } } Store.parse(root, obj); + + if (obj.weatherLocation !== undefined) + _legacyWeatherLocation = obj.weatherLocation; + if (obj.weatherCoordinates !== undefined) + _legacyWeatherCoordinates = obj.weatherCoordinates; + + _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasLoaded = true; applyStoredTheme(); applyStoredIconTheme(); Processes.detectQtTools(); + + _checkSettingsWritable(); } catch (e) { _parseError = true; const msg = e.message; @@ -807,6 +823,33 @@ Singleton { loadPluginSettings(); } + property var _pendingMigration: null + + function _checkSettingsWritable() { + settingsWritableCheckProcess.running = true; + } + + function _onWritableCheckComplete(writable) { + _isReadOnly = !writable; + if (_isReadOnly) { + console.info("SettingsData: settings.json is read-only (NixOS home-manager mode)"); + } else 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 loadPluginSettings() { _pluginSettingsLoading = true; parsePluginSettings(pluginSettingsFile.text()); @@ -836,6 +879,10 @@ Singleton { function saveSettings() { if (_loading || _parseError || !_hasLoaded) return; + if (_isReadOnly) { + _hasUnsavedChanges = _checkForUnsavedChanges(); + return; + } settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2)); } @@ -1400,9 +1447,7 @@ Singleton { } function setWeatherLocation(displayName, coordinates) { - weatherLocation = displayName; - weatherCoordinates = coordinates; - saveSettings(); + SessionData.setWeatherLocation(displayName, coordinates); } function setIconTheme(themeName) { @@ -1800,6 +1845,7 @@ Singleton { if (isGreeterMode) return; _loading = true; + _hasUnsavedChanges = false; try { const txt = settingsFile.text(); if (!txt || !txt.trim()) { @@ -1809,6 +1855,13 @@ Singleton { 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; + + _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasLoaded = true; applyStoredTheme(); applyStoredIconTheme(); @@ -1820,13 +1873,9 @@ Singleton { } finally { _loading = false; } - hasTriedDefaultSettings = false; } onLoadFailed: error => { - if (!isGreeterMode && !hasTriedDefaultSettings) { - hasTriedDefaultSettings = true; - Processes.checkDefaultSettings(); - } else if (!isGreeterMode) { + if (!isGreeterMode) { applyStoredTheme(); } } @@ -1853,4 +1902,20 @@ Singleton { } property bool pluginSettingsFileExists: false + + Process { + id: settingsWritableCheckProcess + + property string settingsPath: Paths.strip(settingsFile.path) + + command: ["sh", "-c", "[ -w \"" + settingsPath + "\" ] && echo 'writable' || echo 'readonly'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim(); + root._onWritableCheckComplete(result === "writable"); + } + } + } } diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index dc8cb48e..d14cf6fa 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -22,10 +22,6 @@ Singleton { pluginSettingsCheckProcess.running = true; } - function checkDefaultSettings() { - defaultSettingsCheckProcess.running = true; - } - property var qtToolsDetectionProcess: Process { command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] running: false @@ -51,25 +47,6 @@ Singleton { } } - property var defaultSettingsCheckProcess: Process { - command: ["sh", "-c", "CONFIG_DIR=\"" + (settingsRoot?._configDir || "") + "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-settings.json\" ] && [ ! -f \"$CONFIG_DIR/settings.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-settings.json\" \"$CONFIG_DIR/settings.json\" && echo 'copied'; else echo 'not_found'; fi"] - running: false - onExited: function (exitCode) { - if (!settingsRoot) - return; - if (exitCode === 0) { - console.info("Copied default-settings.json to settings.json"); - if (settingsRoot.settingsFile) { - settingsRoot.settingsFile.reload(); - } - } else { - if (typeof ThemeApplier !== "undefined") { - ThemeApplier.applyStoredTheme(settingsRoot); - } - } - } - } - property var fprintdDetectionProcess: Process { command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"] running: false diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js new file mode 100644 index 00000000..96cd5fc1 --- /dev/null +++ b/quickshell/Common/settings/SessionSpec.js @@ -0,0 +1,63 @@ +.pragma library + +var SPEC = { + isLightMode: { def: false }, + doNotDisturb: { def: false }, + + wallpaperPath: { def: "" }, + perMonitorWallpaper: { def: false }, + monitorWallpapers: { def: {} }, + perModeWallpaper: { def: false }, + wallpaperPathLight: { def: "" }, + wallpaperPathDark: { def: "" }, + monitorWallpapersLight: { def: {} }, + monitorWallpapersDark: { def: {} }, + wallpaperTransition: { def: "fade" }, + includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] }, + + wallpaperCyclingEnabled: { def: false }, + wallpaperCyclingMode: { def: "interval" }, + wallpaperCyclingInterval: { def: 300 }, + wallpaperCyclingTime: { def: "06:00" }, + monitorCyclingSettings: { def: {} }, + + nightModeEnabled: { def: false }, + nightModeTemperature: { def: 4500 }, + nightModeHighTemperature: { def: 6500 }, + nightModeAutoEnabled: { def: false }, + nightModeAutoMode: { def: "time" }, + nightModeStartHour: { def: 18 }, + nightModeStartMinute: { def: 0 }, + nightModeEndHour: { def: 6 }, + nightModeEndMinute: { def: 0 }, + latitude: { def: 0.0 }, + longitude: { def: 0.0 }, + nightModeUseIPLocation: { def: false }, + nightModeLocationProvider: { def: "" }, + + weatherLocation: { def: "New York, NY" }, + weatherCoordinates: { def: "40.7128,-74.0060" }, + + pinnedApps: { def: [] }, + hiddenTrayIds: { def: [] }, + recentColors: { def: [] }, + showThirdPartyPlugins: { def: false }, + launchPrefix: { def: "" }, + lastBrightnessDevice: { def: "" }, + + brightnessExponentialDevices: { def: {} }, + brightnessUserSetValues: { def: {} }, + brightnessExponentValues: { def: {} }, + + selectedGpuIndex: { def: 0 }, + nvidiaGpuTempEnabled: { def: false }, + nonNvidiaGpuTempEnabled: { def: false }, + enabledGpuPciIds: { def: [] }, + + wifiDeviceOverride: { def: "" }, + weatherHourlyDetailed: { def: true } +}; + +function getValidKeys() { + return Object.keys(SPEC).concat(["configVersion"]); +} diff --git a/quickshell/Common/settings/SessionStore.js b/quickshell/Common/settings/SessionStore.js new file mode 100644 index 00000000..cd2da06b --- /dev/null +++ b/quickshell/Common/settings/SessionStore.js @@ -0,0 +1,92 @@ +.pragma library + +.import "./SessionSpec.js" as SpecModule + +function parse(root, jsonObj) { + var SPEC = SpecModule.SPEC; + for (var k in SPEC) { + root[k] = SPEC[k].def; + } + + if (!jsonObj) return; + + for (var k in jsonObj) { + if (!SPEC[k]) continue; + var raw = jsonObj[k]; + var spec = SPEC[k]; + var coerce = spec.coerce; + root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw; + } +} + +function toJson(root) { + var SPEC = SpecModule.SPEC; + var out = {}; + for (var k in SPEC) { + if (SPEC[k].persist === false) continue; + out[k] = root[k]; + } + out.configVersion = root.sessionConfigVersion; + return out; +} + +function migrateToVersion(obj, targetVersion, settingsData) { + if (!obj) return null; + + var session = JSON.parse(JSON.stringify(obj)); + var currentVersion = session.configVersion || 0; + + if (currentVersion >= targetVersion) { + return null; + } + + if (currentVersion < 2) { + console.info("SessionData: Migrating session from version", currentVersion, "to version 2"); + console.info("SessionData: Importing weather location and coordinates from settings"); + + if (settingsData && typeof settingsData !== "undefined") { + if (session.weatherLocation === undefined || session.weatherLocation === "New York, NY") { + var settingsWeatherLocation = settingsData._legacyWeatherLocation; + if (settingsWeatherLocation && settingsWeatherLocation !== "New York, NY") { + session.weatherLocation = settingsWeatherLocation; + console.info("SessionData: Migrated weatherLocation:", settingsWeatherLocation); + } + } + + if (session.weatherCoordinates === undefined || session.weatherCoordinates === "40.7128,-74.0060") { + var settingsWeatherCoordinates = settingsData._legacyWeatherCoordinates; + if (settingsWeatherCoordinates && settingsWeatherCoordinates !== "40.7128,-74.0060") { + session.weatherCoordinates = settingsWeatherCoordinates; + console.info("SessionData: Migrated weatherCoordinates:", settingsWeatherCoordinates); + } + } + } + + session.configVersion = 2; + } + + return session; +} + +function cleanup(fileText) { + var getValidKeys = SpecModule.getValidKeys; + if (!fileText || !fileText.trim()) return null; + + try { + var session = JSON.parse(fileText); + var validKeys = getValidKeys(); + var needsSave = false; + + for (var key in session) { + if (validKeys.indexOf(key) < 0) { + delete session[key]; + needsSave = true; + } + } + + return needsSave ? JSON.stringify(session, null, 2) : null; + } catch (e) { + console.warn("SessionData: Failed to cleanup unused keys:", e.message); + return null; + } +} diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 61045fcd..060a681e 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -112,8 +112,6 @@ var SPEC = { spotlightCloseNiriOverview: { def: true }, niriOverviewOverlayEnabled: { def: true }, - weatherLocation: { def: "New York, NY" }, - weatherCoordinates: { def: "40.7128,-74.0060" }, useAutoLocation: { def: false }, weatherEnabled: { def: true }, diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index f79db2f9..7e51e7d8 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -214,6 +214,16 @@ function migrateToVersion(obj, targetVersion) { settings.configVersion = 4; } + if (currentVersion < 5) { + console.info("Migrating settings from version", currentVersion, "to version 5"); + console.info("Moving sensitive data (weather location, coordinates) to session.json"); + + delete settings.weatherLocation; + delete settings.weatherCoordinates; + + settings.configVersion = 5; + } + return settings; } diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 15af4de9..ba6baf82 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -220,9 +220,91 @@ FloatingWindow { } } + Rectangle { + id: readOnlyBanner + + property bool showBanner: (SettingsData._isReadOnly && SettingsData._hasUnsavedChanges) || (SessionData._isReadOnly && SessionData._hasUnsavedChanges) + + width: parent.width + height: showBanner ? bannerContent.implicitHeight + Theme.spacingM * 2 : 0 + color: Theme.surfaceContainerHigh + visible: showBanner + clip: true + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Row { + id: bannerContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSize + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: bannerText + + text: I18n.tr("Settings are read-only. Changes will not persist.", "read-only settings warning for NixOS home-manager users") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: Math.max(100, parent.width - (copySettingsButton.visible ? copySettingsButton.width + Theme.spacingM : 0) - (copySessionButton.visible ? copySessionButton.width + Theme.spacingM : 0) - Theme.spacingM * 2 - Theme.iconSize) + wrapMode: Text.WordWrap + } + + DankButton { + id: copySettingsButton + + visible: SettingsData._isReadOnly && SettingsData._hasUnsavedChanges + text: "settings.json" + iconName: "content_copy" + backgroundColor: Theme.primary + textColor: Theme.primaryText + buttonHeight: 32 + horizontalPadding: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + onClicked: { + Quickshell.execDetached(["dms", "cl", "copy", SettingsData.getCurrentSettingsJson()]); + ToastService.showInfo(I18n.tr("Copied to clipboard")); + } + } + + DankButton { + id: copySessionButton + + visible: SessionData._isReadOnly && SessionData._hasUnsavedChanges + text: "session.json" + iconName: "content_copy" + backgroundColor: Theme.primary + textColor: Theme.primaryText + buttonHeight: 32 + horizontalPadding: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + onClicked: { + Quickshell.execDetached(["dms", "cl", "copy", SessionData.getCurrentSessionJson()]); + ToastService.showInfo(I18n.tr("Copied to clipboard")); + } + } + } + } + Item { width: parent.width - height: parent.height - 48 + height: parent.height - 48 - readOnlyBanner.height clip: true SettingsSidebar { diff --git a/quickshell/Modules/Settings/TimeWeatherTab.qml b/quickshell/Modules/Settings/TimeWeatherTab.qml index 0787b32f..26064fa1 100644 --- a/quickshell/Modules/Settings/TimeWeatherTab.qml +++ b/quickshell/Modules/Settings/TimeWeatherTab.qml @@ -455,8 +455,8 @@ Item { onTextEdited: { if (text && longitudeInput.text) { const coords = text + "," + longitudeInput.text; - SettingsData.weatherCoordinates = coords; - SettingsData.saveSettings(); + SessionData.weatherCoordinates = coords; + SessionData.saveSettings(); } } } @@ -505,8 +505,8 @@ Item { onTextEdited: { if (text && latitudeInput.text) { const coords = latitudeInput.text + "," + text; - SettingsData.weatherCoordinates = coords; - SettingsData.saveSettings(); + SessionData.weatherCoordinates = coords; + SessionData.saveSettings(); } } } diff --git a/quickshell/Widgets/DankButton.qml b/quickshell/Widgets/DankButton.qml index d83ed4e2..6b62ddae 100644 --- a/quickshell/Widgets/DankButton.qml +++ b/quickshell/Widgets/DankButton.qml @@ -30,9 +30,9 @@ Rectangle { radius: parent.radius color: { if (pressed) - return Theme.primaryPressed; + return Theme.withAlpha(root.textColor, 0.20); if (hovered) - return Theme.primaryHover; + return Theme.withAlpha(root.textColor, 0.12); return "transparent"; }