diff --git a/core/cmd/dms/commands_backup.go b/core/cmd/dms/commands_backup.go new file mode 100644 index 000000000..cbe5a214b --- /dev/null +++ b/core/cmd/dms/commands_backup.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "os" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/backup" + "github.com/spf13/cobra" +) + +var backupOutputPath string + +var backupCmd = &cobra.Command{ + Use: "backup", + Short: "Backup and restore DMS configuration", +} + +var backupCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a tar.gz backup of ~/.config/DankMaterialShell (settings, plugins, themes)", + Args: cobra.NoArgs, + Run: runBackupCreate, +} + +var backupRestoreCmd = &cobra.Command{ + Use: "restore ", + Short: "Restore a DMS configuration backup; the current configuration is moved aside first", + Args: cobra.ExactArgs(1), + Run: runBackupRestore, +} + +func init() { + backupCreateCmd.Flags().StringVarP(&backupOutputPath, "output", "o", "", "Output archive path (default dms-backup-.tar.gz)") + backupCmd.AddCommand(backupCreateCmd, backupRestoreCmd) + rootCmd.AddCommand(backupCmd) +} + +func runBackupCreate(cmd *cobra.Command, args []string) { + output := backupOutputPath + if output == "" { + output = backup.DefaultArchiveName() + } + + if err := backup.Create(output); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + fmt.Println(output) +} + +func runBackupRestore(cmd *cobra.Command, args []string) { + previous, err := backup.Restore(args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if previous != "" { + fmt.Printf("Previous configuration moved to %s\n", previous) + } + fmt.Println("Restore complete. Restart the shell to apply.") +} diff --git a/core/internal/backup/backup.go b/core/internal/backup/backup.go new file mode 100644 index 000000000..b0559eeef --- /dev/null +++ b/core/internal/backup/backup.go @@ -0,0 +1,198 @@ +package backup + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" +) + +const dirName = "DankMaterialShell" + +func ConfigDir() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(configDir, dirName), nil +} + +func DefaultArchiveName() string { + return fmt.Sprintf("dms-backup-%s.tar.gz", time.Now().Format("20060102-150405")) +} + +func Create(outputPath string) error { + srcDir, err := ConfigDir() + if err != nil { + return err + } + if _, err := os.Stat(srcDir); err != nil { + return fmt.Errorf("no DMS configuration found at %s: %w", srcDir, err) + } + + out, err := os.Create(outputPath) + if err != nil { + return err + } + defer out.Close() + + gz := gzip.NewWriter(out) + defer gz.Close() + tw := tar.NewWriter(gz) + defer tw.Close() + + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + + var link string + if info.Mode()&os.ModeSymlink != 0 { + if link, err = os.Readlink(path); err != nil { + return err + } + } + + header, err := tar.FileInfoHeader(info, link) + if err != nil { + return err + } + header.Name = filepath.ToSlash(filepath.Join(dirName, rel)) + + if err := tw.WriteHeader(header); err != nil { + return err + } + if !info.Mode().IsRegular() { + return nil + } + + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(tw, f) + return err + }) +} + +func Restore(archivePath string) (string, error) { + dstDir, err := ConfigDir() + if err != nil { + return "", err + } + + if err := validateArchive(archivePath); err != nil { + return "", err + } + + previous := "" + if _, err := os.Stat(dstDir); err == nil { + previous = dstDir + ".pre-restore-" + time.Now().Format("20060102-150405") + if err := os.Rename(dstDir, previous); err != nil { + return "", fmt.Errorf("failed to move existing configuration aside: %w", err) + } + } + + if err := extract(archivePath, filepath.Dir(dstDir)); err != nil { + if previous != "" { + os.RemoveAll(dstDir) + os.Rename(previous, dstDir) + } + return "", err + } + return previous, nil +} + +func validateArchive(archivePath string) error { + hasSettings := false + err := walkArchive(archivePath, func(header *tar.Header, _ *tar.Reader) error { + name := filepath.ToSlash(filepath.Clean(header.Name)) + if strings.HasPrefix(name, "..") || filepath.IsAbs(header.Name) { + return fmt.Errorf("unsafe path in archive: %s", header.Name) + } + if !strings.HasPrefix(name, dirName+"/") && name != dirName { + return fmt.Errorf("not a DMS backup: unexpected entry %s", header.Name) + } + if name == dirName+"/settings.json" { + hasSettings = true + } + return nil + }) + if err != nil { + return err + } + if !hasSettings { + return fmt.Errorf("not a DMS backup: settings.json missing from archive") + } + return nil +} + +func extract(archivePath, destParent string) error { + return walkArchive(archivePath, func(header *tar.Header, tr *tar.Reader) error { + target := filepath.Join(destParent, filepath.Clean(header.Name)) + + switch header.Typeflag { + case tar.TypeDir: + return os.MkdirAll(target, os.FileMode(header.Mode)) + case tar.TypeSymlink: + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + return os.Symlink(header.Linkname, target) + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, tr) + return err + default: + return nil + } + }) +} + +func walkArchive(archivePath string, visit func(*tar.Header, *tar.Reader) error) error { + f, err := os.Open(archivePath) + if err != nil { + return err + } + defer f.Close() + + gz, err := gzip.NewReader(f) + if err != nil { + return fmt.Errorf("not a gzip archive: %w", err) + } + defer gz.Close() + + tr := tar.NewReader(gz) + for { + header, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if err := visit(header, tr); err != nil { + return err + } + } +} diff --git a/quickshell/Common/CacheData.qml b/quickshell/Common/CacheData.qml index a0883bb65..9dde50de8 100644 --- a/quickshell/Common/CacheData.qml +++ b/quickshell/Common/CacheData.qml @@ -11,7 +11,7 @@ Singleton { id: root readonly property var log: Log.scoped("CacheData") - readonly property int cacheConfigVersion: 2 + readonly property int cacheConfigVersion: 3 readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation) readonly property string _stateDir: Paths.strip(_stateUrl) @@ -21,11 +21,15 @@ Singleton { property int _loadedCacheVersion: 0 readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"] - readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys) + readonly property var _historyKeys: ["browserUsageHistory", "filePickerUsageHistory"] + readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys, _historyKeys) property string wallpaperLastPath: "" property string profileLastPath: "" + property var browserUsageHistory: ({}) + property var filePickerUsageHistory: ({}) + property var brightnessDevicePins: ({}) property var wifiNetworkPins: ({}) property var bluetoothDevicePins: ({}) @@ -131,6 +135,29 @@ Singleton { saveCache(); } + function migrateUsageHistories(histories) { + if (!histories) + return; + if (!_hasLoaded) + loadCache(); + + let migrated = false; + for (const key of _historyKeys) { + const legacy = histories[key]; + if (!legacy || Object.keys(legacy).length === 0) + continue; + if (Object.keys(root[key] || {}).length > 0) + continue; + root[key] = legacy; + migrated = true; + } + + if (!migrated) + return; + log.info("Migrated usage histories from settings.json"); + saveCache(); + } + function parseCache(content) { _loading = true; try { @@ -172,7 +199,7 @@ Singleton { }; } - for (const key of _pinKeys) { + for (const key of _pinKeys.concat(_historyKeys)) { root[key] = cache[key] !== undefined ? cache[key] : {}; } @@ -198,7 +225,7 @@ Singleton { "fileBrowserSettings": fileBrowserSettings, "configVersion": cacheConfigVersion }; - for (const key of _pinKeys) { + for (const key of _pinKeys.concat(_historyKeys)) { data[key] = root[key]; } cacheFile.setText(JSON.stringify(data, null, 2)); diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 141d72578..2bd1e8021 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -14,7 +14,7 @@ Singleton { id: root readonly property var log: Log.scoped("SessionData") - readonly property int sessionConfigVersion: 3 + readonly property int sessionConfigVersion: 4 signal loaded signal brightnessDisplayHintChanged(string deviceName) @@ -221,6 +221,17 @@ Singleton { property string notepadLastMode: "" + property var niriOutputSettings: ({}) + property var hyprlandOutputSettings: ({}) + property var activeDisplayProfile: ({}) + property var activeDisplayProfileModes: ({}) + property var desktopWidgetGridSettings: ({}) + property var desktopWidgetInstancePositions: ({}) + property var builtInPluginState: ({}) + property bool greeterSyncPending: false + property var greeterSyncBaseline: ({}) + property string lastAppliedIconTheme: "" + property string launcherLastMode: "all" property string launcherLastFileSearchType: "all" property string launcherLastQuery: "" @@ -393,6 +404,161 @@ Singleton { Spec.set(root, key, value, saveSettings, _hooks); } + function importFromSettings(payload) { + if (!payload) + return; + if (!_hasLoaded) + loadSettings(); + if (_parseError) + return; + + let imported = false; + for (const key in payload) { + if (!(key in Spec.SPEC)) + continue; + const current = root[key]; + const isEmpty = current === Spec.SPEC[key].def || (typeof current === "object" && current !== null && Object.keys(current).length === 0); + if (!isEmpty) + continue; + root[key] = payload[key]; + imported = true; + } + + if (imported) { + log.info("Imported machine-specific state from settings.json"); + saveSettings(); + } + } + + function getNiriOutputSetting(outputId, key, defaultValue) { + if (!niriOutputSettings[outputId]) + return defaultValue; + return niriOutputSettings[outputId][key] !== undefined ? niriOutputSettings[outputId][key] : defaultValue; + } + + function setNiriOutputSetting(outputId, key, value) { + const updated = JSON.parse(JSON.stringify(niriOutputSettings)); + if (!updated[outputId]) + updated[outputId] = {}; + updated[outputId][key] = value; + niriOutputSettings = updated; + saveSettings(); + } + + function getNiriOutputSettings(outputId) { + const settings = niriOutputSettings[outputId]; + return settings ? JSON.parse(JSON.stringify(settings)) : {}; + } + + function getHyprlandOutputSetting(outputId, key, defaultValue) { + if (!hyprlandOutputSettings[outputId]) + return defaultValue; + return hyprlandOutputSettings[outputId][key] !== undefined ? hyprlandOutputSettings[outputId][key] : defaultValue; + } + + function setHyprlandOutputSetting(outputId, key, value) { + const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings)); + if (!updated[outputId]) + updated[outputId] = {}; + updated[outputId][key] = value; + hyprlandOutputSettings = updated; + saveSettings(); + } + + function removeHyprlandOutputSetting(outputId, key) { + if (!hyprlandOutputSettings[outputId] || !(key in hyprlandOutputSettings[outputId])) + return; + const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings)); + delete updated[outputId][key]; + hyprlandOutputSettings = updated; + saveSettings(); + } + + function getActiveDisplayProfile(compositor) { + return activeDisplayProfile[compositor] || ""; + } + + function setActiveDisplayProfile(compositor, profileId) { + const updated = JSON.parse(JSON.stringify(activeDisplayProfile)); + updated[compositor] = profileId; + activeDisplayProfile = updated; + saveSettings(); + } + + function setActiveDisplayProfileModes(compositor, modes) { + if (JSON.stringify(activeDisplayProfileModes[compositor] || {}) === JSON.stringify(modes || {})) + return; + const updated = JSON.parse(JSON.stringify(activeDisplayProfileModes)); + updated[compositor] = modes; + activeDisplayProfileModes = updated; + saveSettings(); + } + + function getDesktopWidgetGridSetting(screenKey, property, defaultValue) { + const val = desktopWidgetGridSettings?.[screenKey]?.[property]; + return val !== undefined ? val : defaultValue; + } + + function setDesktopWidgetGridSetting(screenKey, property, value) { + const allSettings = JSON.parse(JSON.stringify(desktopWidgetGridSettings || {})); + if (!allSettings[screenKey]) + allSettings[screenKey] = {}; + allSettings[screenKey][property] = value; + desktopWidgetGridSettings = allSettings; + saveSettings(); + } + + function updateDesktopWidgetInstancePosition(instanceId, screenKey, positionUpdates) { + const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions)); + if (!updated[instanceId]) + updated[instanceId] = {}; + updated[instanceId][screenKey] = Object.assign({}, updated[instanceId][screenKey] || {}, positionUpdates); + desktopWidgetInstancePositions = updated; + saveSettings(); + } + + function syncDesktopWidgetPositionToAllScreens(instanceId) { + const positions = desktopWidgetInstancePositions[instanceId] || {}; + const screenKeys = Object.keys(positions).filter(k => k !== "_synced"); + if (screenKeys.length === 0) + return; + const sourcePos = positions[screenKeys[0]]; + if (!sourcePos) + return; + const screen = Array.from(Quickshell.screens.values()).find(s => SettingsData.getScreenDisplayName(s) === screenKeys[0]); + if (!screen) + return; + const synced = {}; + if (sourcePos.x !== undefined) + synced.x = sourcePos.x / screen.width; + if (sourcePos.y !== undefined) + synced.y = sourcePos.y / screen.height; + if (sourcePos.width !== undefined) + synced.width = sourcePos.width; + if (sourcePos.height !== undefined) + synced.height = sourcePos.height; + const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions)); + updated[instanceId]["_synced"] = synced; + desktopWidgetInstancePositions = updated; + saveSettings(); + } + + function removeDesktopWidgetInstancePositions(instanceId) { + if (!(instanceId in desktopWidgetInstancePositions)) + return; + const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions)); + delete updated[instanceId]; + desktopWidgetInstancePositions = updated; + saveSettings(); + } + + function setBuiltInPluginState(pluginId, state) { + const updated = JSON.parse(JSON.stringify(builtInPluginState)); + updated[pluginId] = state; + builtInPluginState = updated; + saveSettings(); + } + function migrateFromUndefinedToV1(settings) { if (typeof SettingsData !== "undefined") { if (settings.acMonitorTimeout !== undefined) { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 35a24f9de..33376ed63 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -15,7 +15,7 @@ Singleton { id: root readonly property var log: Log.scoped("SettingsData") - readonly property int settingsConfigVersion: 13 + readonly property int settingsConfigVersion: 15 enum Position { Top, @@ -102,6 +102,10 @@ Singleton { updated[pluginId] = {}; updated[pluginId][key] = value; builtInPluginSettings = updated; + if (Store.SESSION_BACKED_PLUGIN_IDS.includes(pluginId)) { + SessionData.setBuiltInPluginState(pluginId, updated[pluginId]); + return; + } saveSettings(); } @@ -536,16 +540,12 @@ Singleton { property string greeterLockDateFormat: "" property string greeterFontFamily: "" property string greeterWallpaperFillMode: "" - property bool greeterSyncPending: false - property var greeterSyncBaseline: ({}) property int mediaSize: 1 property string appLauncherViewMode: "list" property string spotlightModalViewMode: "list" property string browserPickerViewMode: "grid" - property var browserUsageHistory: ({}) property string appPickerViewMode: "grid" - property var filePickerUsageHistory: ({}) property bool sortAppsAlphabetically: false property int appLauncherGridColumns: 4 property bool spotlightCloseNiriOverview: true @@ -673,7 +673,6 @@ Singleton { property string iconThemeDark: "System Default" property string iconThemeLight: "System Default" property bool iconThemePerMode: false - property string lastAppliedIconTheme: "" readonly property string iconTheme: resolveIconTheme() property var availableIconThemes: ["System Default"] property string systemDefaultIconTheme: "" @@ -1006,11 +1005,7 @@ Singleton { property string displayNameMode: "system" property var screenPreferences: ({}) property var showOnLastDisplay: ({}) - property var niriOutputSettings: ({}) - property var hyprlandOutputSettings: ({}) property var displayProfiles: ({}) - property var activeDisplayProfile: ({}) - property var activeDisplayProfileModes: ({}) property var displayPreviousRefreshModes: ({}) property bool displayProfileAutoSelect: false property bool displayShowDisconnected: false @@ -1117,24 +1112,9 @@ Singleton { property var systemMonitorDisplayPreferences: ["all"] property var systemMonitorVariants: [] property var desktopWidgetPositions: ({}) - property var desktopWidgetGridSettings: ({}) property var desktopWidgetInstances: [] property var desktopWidgetGroups: [] - function getDesktopWidgetGridSetting(screenKey, property, defaultValue) { - const val = desktopWidgetGridSettings?.[screenKey]?.[property]; - return val !== undefined ? val : defaultValue; - } - - function setDesktopWidgetGridSetting(screenKey, property, value) { - const allSettings = JSON.parse(JSON.stringify(desktopWidgetGridSettings || {})); - if (!allSettings[screenKey]) - allSettings[screenKey] = {}; - allSettings[screenKey][property] = value; - desktopWidgetGridSettings = allSettings; - saveSettings(); - } - function getDesktopWidgetPosition(pluginId, screenKey, property, defaultValue) { const pos = desktopWidgetPositions?.[pluginId]?.[screenKey]?.[property]; return pos !== undefined ? pos : defaultValue; @@ -1185,8 +1165,7 @@ Singleton { widgetType: widgetType, name: name || widgetType, enabled: true, - config: config || {}, - positions: {} + config: config || {} }; const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); instances.push(instance); @@ -1215,53 +1194,10 @@ Singleton { saveSettings(); } - function updateDesktopWidgetInstancePosition(instanceId, screenKey, positionUpdates) { - const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); - const idx = instances.findIndex(inst => inst.id === instanceId); - if (idx === -1) - return; - if (!instances[idx].positions) - instances[idx].positions = {}; - instances[idx].positions[screenKey] = Object.assign({}, instances[idx].positions[screenKey] || {}, positionUpdates); - desktopWidgetInstances = instances; - saveSettings(); - } - function removeDesktopWidgetInstance(instanceId) { const instances = (desktopWidgetInstances || []).filter(inst => inst.id !== instanceId); desktopWidgetInstances = instances; - saveSettings(); - } - - function syncDesktopWidgetPositionToAllScreens(instanceId) { - const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); - const idx = instances.findIndex(inst => inst.id === instanceId); - if (idx === -1) - return; - const positions = instances[idx].positions || {}; - const screenKeys = Object.keys(positions).filter(k => k !== "_synced"); - if (screenKeys.length === 0) - return; - const sourceKey = screenKeys[0]; - const sourcePos = positions[sourceKey]; - if (!sourcePos) - return; - const screen = Array.from(Quickshell.screens.values()).find(s => getScreenDisplayName(s) === sourceKey); - if (!screen) - return; - const screenW = screen.width; - const screenH = screen.height; - const synced = {}; - if (sourcePos.x !== undefined) - synced.x = sourcePos.x / screenW; - if (sourcePos.y !== undefined) - synced.y = sourcePos.y / screenH; - if (sourcePos.width !== undefined) - synced.width = sourcePos.width; - if (sourcePos.height !== undefined) - synced.height = sourcePos.height; - instances[idx].positions["_synced"] = synced; - desktopWidgetInstances = instances; + SessionData.removeDesktopWidgetInstancePositions(instanceId); saveSettings(); } @@ -1275,8 +1211,7 @@ Singleton { widgetType: source.widgetType, name: source.name + " (Copy)", enabled: source.enabled, - config: JSON.parse(JSON.stringify(source.config || {})), - positions: {} + config: JSON.parse(JSON.stringify(source.config || {})) }; const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); instances.push(instance); @@ -1424,14 +1359,15 @@ Singleton { iconThemePerMode = false; iconThemeDark = "System Default"; iconThemeLight = "System Default"; - lastAppliedIconTheme = ""; + SessionData.lastAppliedIconTheme = ""; + SessionData.saveSettings(); saveSettings(); } function checkIconThemeDrift() { if (resolveIconTheme() === "System Default") return; - if (!lastAppliedIconTheme) + if (!SessionData.lastAppliedIconTheme) return; const script = `if command -v gsettings >/dev/null 2>&1; then gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g" @@ -1443,7 +1379,7 @@ Singleton { const platform = (output || "").trim(); if (!platform) return; - if (platform === root.lastAppliedIconTheme || platform === root.iconThemeDark || platform === root.iconThemeLight) + 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")); @@ -1515,7 +1451,8 @@ Singleton { const gtkThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved; if (gtkThemeName === "System Default" || gtkThemeName === "") return; - lastAppliedIconTheme = gtkThemeName; + SessionData.lastAppliedIconTheme = gtkThemeName; + SessionData.saveSettings(); if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") { PortalService.setSystemIconTheme(gtkThemeName); } @@ -1598,26 +1535,28 @@ Singleton { } function markGreeterSyncPending(who, key, oldValue) { - if (!(key in greeterSyncBaseline)) { - var baseline = greeterSyncBaseline; + if (!(key in SessionData.greeterSyncBaseline)) { + var baseline = Object.assign({}, SessionData.greeterSyncBaseline); baseline[key] = oldValue; - greeterSyncBaseline = baseline; + SessionData.greeterSyncBaseline = baseline; } - greeterSyncPending = true; + SessionData.greeterSyncPending = true; + SessionData.saveSettings(); } function clearGreeterSyncPending() { - greeterSyncBaseline = {}; - greeterSyncPending = false; - saveSettings(); + SessionData.greeterSyncBaseline = {}; + SessionData.greeterSyncPending = false; + SessionData.saveSettings(); } function revertGreeterSyncPending() { - for (var key in greeterSyncBaseline) { - root[key] = greeterSyncBaseline[key]; + for (var key in SessionData.greeterSyncBaseline) { + root[key] = SessionData.greeterSyncBaseline[key]; } - greeterSyncBaseline = {}; - greeterSyncPending = false; + SessionData.greeterSyncBaseline = {}; + SessionData.greeterSyncPending = false; + SessionData.saveSettings(); saveSettings(); } @@ -1650,6 +1589,8 @@ Singleton { 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) { @@ -1659,6 +1600,14 @@ Singleton { } 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; @@ -1697,6 +1646,7 @@ Singleton { _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _hasLoaded = true; + _mergeSessionState(); applyStoredTheme(); updateCompositorCursor(); Qt.callLater(checkIconThemeDrift); @@ -1717,6 +1667,28 @@ Singleton { 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; } @@ -3144,50 +3116,6 @@ Singleton { return settings ? JSON.parse(JSON.stringify(settings)) : {}; } - function getNiriOutputSetting(outputId, key, defaultValue) { - if (!niriOutputSettings[outputId]) - return defaultValue; - return niriOutputSettings[outputId][key] !== undefined ? niriOutputSettings[outputId][key] : defaultValue; - } - - function setNiriOutputSetting(outputId, key, value) { - const updated = JSON.parse(JSON.stringify(niriOutputSettings)); - if (!updated[outputId]) - updated[outputId] = {}; - updated[outputId][key] = value; - niriOutputSettings = updated; - saveSettings(); - } - - function getNiriOutputSettings(outputId) { - const settings = niriOutputSettings[outputId]; - return settings ? JSON.parse(JSON.stringify(settings)) : {}; - } - - function getHyprlandOutputSetting(outputId, key, defaultValue) { - if (!hyprlandOutputSettings[outputId]) - return defaultValue; - return hyprlandOutputSettings[outputId][key] !== undefined ? hyprlandOutputSettings[outputId][key] : defaultValue; - } - - function setHyprlandOutputSetting(outputId, key, value) { - const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings)); - if (!updated[outputId]) - updated[outputId] = {}; - updated[outputId][key] = value; - hyprlandOutputSettings = updated; - saveSettings(); - } - - function removeHyprlandOutputSetting(outputId, key) { - if (!hyprlandOutputSettings[outputId] || !(key in hyprlandOutputSettings[outputId])) - return; - const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings)); - delete updated[outputId][key]; - hyprlandOutputSettings = updated; - saveSettings(); - } - function removeDisplayProfile(compositor, profileId) { if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId]) return; @@ -3197,26 +3125,6 @@ Singleton { saveSettings(); } - function getActiveDisplayProfile(compositor) { - return activeDisplayProfile[compositor] || ""; - } - - function setActiveDisplayProfile(compositor, profileId) { - const updated = JSON.parse(JSON.stringify(activeDisplayProfile)); - updated[compositor] = profileId; - activeDisplayProfile = updated; - saveSettings(); - } - - function setActiveDisplayProfileModes(compositor, modes) { - if (JSON.stringify(activeDisplayProfileModes[compositor] || {}) === JSON.stringify(modes || {})) - return; - const updated = JSON.parse(JSON.stringify(activeDisplayProfileModes)); - updated[compositor] = modes; - activeDisplayProfileModes = updated; - saveSettings(); - } - function setDisplayPreviousRefreshModes(compositor, modes) { if (JSON.stringify(displayPreviousRefreshModes[compositor] || {}) === JSON.stringify(modes || {})) return; diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index 7c8677b7d..0ef9cf35a 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -367,8 +367,7 @@ Singleton { function deferGreeterAutoLoginSyncToPill(details) { toastCategoryDismissed("greeter-autologin-sync"); - if (settingsRoot) - settingsRoot.set("greeterSyncPending", true); + SessionData.set("greeterSyncPending", true); toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync"); finishGreeterAutoLoginSync(); } diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index cdc86a027..00f6d1904 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -91,6 +91,17 @@ var SPEC = { notepadLastMode: { def: "" }, + niriOutputSettings: { def: {} }, + hyprlandOutputSettings: { def: {} }, + activeDisplayProfile: { def: {} }, + activeDisplayProfileModes: { def: {} }, + desktopWidgetGridSettings: { def: {} }, + desktopWidgetInstancePositions: { def: {} }, + builtInPluginState: { def: {} }, + greeterSyncPending: { def: false }, + greeterSyncBaseline: { def: {} }, + lastAppliedIconTheme: { def: "" }, + launcherLastMode: { def: "all" }, launcherLastFileSearchType: { def: "all" }, launcherLastQuery: { def: "" }, diff --git a/quickshell/Common/settings/SessionStore.js b/quickshell/Common/settings/SessionStore.js index 7fcdb65fe..7d8c70f24 100644 --- a/quickshell/Common/settings/SessionStore.js +++ b/quickshell/Common/settings/SessionStore.js @@ -1,6 +1,7 @@ .pragma library .import "./SessionSpec.js" as SpecModule + .import "./SpecUtil.js" as Util function parse(root, jsonObj) { var SPEC = SpecModule.SPEC; @@ -9,7 +10,7 @@ function parse(root, jsonObj) { for (var k in SPEC) { if (!(k in jsonObj)) { - root[k] = SPEC[k].def; + root[k] = Util.cloneDef(SPEC[k].def); } } @@ -27,6 +28,7 @@ function toJson(root) { var out = {}; for (var k in SPEC) { if (SPEC[k].persist === false) continue; + if (Util.isDefault(root[k], SPEC[k].def)) continue; out[k] = root[k]; } out.configVersion = root.sessionConfigVersion; @@ -73,5 +75,13 @@ function migrateToVersion(obj, targetVersion, settingsData) { session.configVersion = 3; } + if (currentVersion < 4) { + console.info("SessionData: Migrating session to version 4"); + console.info("SessionData: Dropping keys that match defaults; session.json now stores only changed values"); + + Util.stripDefaults(session, SpecModule.SPEC); + session.configVersion = 4; + } + return session; } diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index d948a5715..47e9407a2 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -1,5 +1,7 @@ .pragma library + .import "./SpecUtil.js" as Util + function percentToUnit(v) { if (v === undefined || v === null) return undefined; return v > 1 ? v / 100 : v; @@ -270,16 +272,12 @@ var SPEC = { greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" }, greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" }, greeterPamExternallyManaged: { def: false, onChange: "markGreeterSyncPending" }, - greeterSyncPending: { def: false }, - greeterSyncBaseline: { def: {} }, mediaSize: { def: 1 }, appLauncherViewMode: { def: "list" }, spotlightModalViewMode: { def: "list" }, browserPickerViewMode: { def: "grid" }, - browserUsageHistory: { def: {} }, appPickerViewMode: { def: "grid" }, - filePickerUsageHistory: { def: {} }, sortAppsAlphabetically: { def: false }, appLauncherGridColumns: { def: 4 }, spotlightCloseNiriOverview: { def: true }, @@ -312,7 +310,6 @@ var SPEC = { iconThemeDark: { def: "System Default", onChange: "applyStoredIconTheme" }, iconThemeLight: { def: "System Default", onChange: "applyStoredIconTheme" }, iconThemePerMode: { def: false, onChange: "applyStoredIconTheme" }, - lastAppliedIconTheme: { def: "" }, availableIconThemes: { def: ["System Default"], persist: false }, systemDefaultIconTheme: { def: "", persist: false }, @@ -558,11 +555,7 @@ var SPEC = { displayNameMode: { def: "system" }, screenPreferences: { def: {} }, showOnLastDisplay: { def: {} }, - niriOutputSettings: { def: {} }, - hyprlandOutputSettings: { def: {} }, displayProfiles: { def: {} }, - activeDisplayProfile: { def: {} }, - activeDisplayProfileModes: { def: {} }, displayPreviousRefreshModes: { def: {} }, displayProfileAutoSelect: { def: false }, displayShowDisconnected: { def: false }, @@ -668,7 +661,6 @@ var SPEC = { systemMonitorDisplayPreferences: { def: ["all"] }, systemMonitorVariants: { def: [] }, desktopWidgetPositions: { def: {} }, - desktopWidgetGridSettings: { def: {} }, desktopWidgetInstances: { def: [] }, @@ -710,7 +702,7 @@ function getValidKeys() { function set(root, key, value, saveFn, hooks) { if (!(key in SPEC)) return; - if (value === undefined || value === null) value = SPEC[key].def; + if (value === undefined || value === null) value = Util.cloneDef(SPEC[key].def); var oldValue = root[key]; root[key] = value; var hookName = SPEC[key].onChange; diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index e10ad9527..1335d2ac7 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -1,9 +1,78 @@ .pragma library .import "./SettingsSpec.js" as SpecModule + .import "./SpecUtil.js" as Util var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]; +var SESSION_MOVED_KEYS = ["niriOutputSettings", "hyprlandOutputSettings", "activeDisplayProfile", "activeDisplayProfileModes", "desktopWidgetGridSettings", "greeterSyncPending", "greeterSyncBaseline", "lastAppliedIconTheme"]; +var CACHE_MOVED_KEYS = ["browserUsageHistory", "filePickerUsageHistory"]; +var SESSION_BACKED_PLUGIN_IDS = ["dankNotepadModule"]; + +// Superseded by desktopWidgetInstances at v4; nothing has written them since +var STALE_WIDGET_KEYS = ["desktopClockEnabled", "desktopClockStyle", "desktopClockTransparency", "desktopClockColorMode", "desktopClockCustomColor", "desktopClockShowDate", "desktopClockShowAnalogNumbers", "desktopClockShowAnalogSeconds", "desktopClockX", "desktopClockY", "desktopClockWidth", "desktopClockHeight", "desktopClockDisplayPreferences", "systemMonitorEnabled", "systemMonitorShowHeader", "systemMonitorTransparency", "systemMonitorColorMode", "systemMonitorCustomColor", "systemMonitorShowCpu", "systemMonitorShowCpuGraph", "systemMonitorShowCpuTemp", "systemMonitorShowGpuTemp", "systemMonitorGpuPciId", "systemMonitorShowMemory", "systemMonitorShowMemoryGraph", "systemMonitorShowNetwork", "systemMonitorShowNetworkGraph", "systemMonitorShowDisk", "systemMonitorShowTopProcesses", "systemMonitorTopProcessCount", "systemMonitorTopProcessSortBy", "systemMonitorGraphInterval", "systemMonitorLayoutMode", "systemMonitorX", "systemMonitorY", "systemMonitorWidth", "systemMonitorHeight", "systemMonitorDisplayPreferences", "systemMonitorVariants", "desktopWidgetPositions"]; + +function withoutInstancePositions(instances) { + if (!Array.isArray(instances)) return instances; + return instances.map(function (inst) { + if (!inst || !inst.positions) return inst; + var copy = Object.assign({}, inst); + delete copy.positions; + return copy; + }); +} + +function withoutSessionBackedPluginState(pluginSettings) { + if (!pluginSettings) return pluginSettings; + var copy = Object.assign({}, pluginSettings); + for (var i = 0; i < SESSION_BACKED_PLUGIN_IDS.length; i++) { + delete copy[SESSION_BACKED_PLUGIN_IDS[i]]; + } + return copy; +} + +function extractSessionPayload(obj) { + if (!obj) return null; + + var payload = {}; + for (var i = 0; i < SESSION_MOVED_KEYS.length; i++) { + var key = SESSION_MOVED_KEYS[i]; + if (key in obj) payload[key] = obj[key]; + } + + var positions = {}; + var instances = Array.isArray(obj.desktopWidgetInstances) ? obj.desktopWidgetInstances : []; + for (var i = 0; i < instances.length; i++) { + var inst = instances[i]; + if (inst && inst.id && inst.positions && Object.keys(inst.positions).length > 0) { + positions[inst.id] = inst.positions; + } + } + if (Object.keys(positions).length > 0) payload.desktopWidgetInstancePositions = positions; + + var pluginState = {}; + for (var i = 0; i < SESSION_BACKED_PLUGIN_IDS.length; i++) { + var id = SESSION_BACKED_PLUGIN_IDS[i]; + if (obj.builtInPluginSettings && obj.builtInPluginSettings[id]) { + pluginState[id] = obj.builtInPluginSettings[id]; + } + } + if (Object.keys(pluginState).length > 0) payload.builtInPluginState = pluginState; + + return Object.keys(payload).length > 0 ? payload : null; +} + +function extractCachePayload(obj) { + if (!obj) return null; + + var payload = {}; + for (var i = 0; i < CACHE_MOVED_KEYS.length; i++) { + var key = CACHE_MOVED_KEYS[i]; + if (obj[key] && Object.keys(obj[key]).length > 0) payload[key] = obj[key]; + } + return Object.keys(payload).length > 0 ? payload : null; +} + function extractPins(obj) { if (!obj) return null; @@ -28,7 +97,7 @@ function parse(root, jsonObj) { // would wipe values set by detection processes on every reload. if (SPEC[k].persist === false) continue; if (!(k in jsonObj)) { - root[k] = SPEC[k].def; + root[k] = Util.cloneDef(SPEC[k].def); } } @@ -48,7 +117,11 @@ function toJson(root) { for (var k in SPEC) { if (SPEC[k].persist === false) continue; if (k === "pluginSettings") continue; - out[k] = root[k]; + var value = root[k]; + if (k === "desktopWidgetInstances") value = withoutInstancePositions(value); + if (k === "builtInPluginSettings") value = withoutSessionBackedPluginState(value); + if (Util.isDefault(value, SPEC[k].def)) continue; + out[k] = value; } out.configVersion = root.settingsConfigVersion; return out; @@ -289,5 +362,32 @@ function migrateToVersion(obj, targetVersion) { settings.configVersion = 13; } + if (currentVersion < 14) { + console.info("Migrating settings from version", currentVersion, "to version 14"); + console.info("Dropping keys that match defaults; settings.json now stores only changed values"); + + Util.stripDefaults(settings, SpecModule.SPEC); + settings.configVersion = 14; + } + + if (currentVersion < 15) { + console.info("Migrating settings from version", currentVersion, "to version 15"); + console.info("Moving machine-specific state to session.json and usage histories to cache.json"); + + var movedKeys = SESSION_MOVED_KEYS.concat(CACHE_MOVED_KEYS, STALE_WIDGET_KEYS); + for (var i = 0; i < movedKeys.length; i++) { + delete settings[movedKeys[i]]; + } + + if (Array.isArray(settings.desktopWidgetInstances)) { + settings.desktopWidgetInstances = withoutInstancePositions(settings.desktopWidgetInstances); + } + if (settings.builtInPluginSettings) { + settings.builtInPluginSettings = withoutSessionBackedPluginState(settings.builtInPluginSettings); + } + + settings.configVersion = 15; + } + return settings; } diff --git a/quickshell/Common/settings/SpecUtil.js b/quickshell/Common/settings/SpecUtil.js new file mode 100644 index 000000000..6cb914795 --- /dev/null +++ b/quickshell/Common/settings/SpecUtil.js @@ -0,0 +1,30 @@ +.pragma library + +function stableStringify(value) { + if (value === null || typeof value !== "object") + return JSON.stringify(value); + if (Array.isArray(value)) + return "[" + value.map(stableStringify).join(",") + "]"; + return "{" + Object.keys(value).sort().map(function (k) { + return JSON.stringify(k) + ":" + stableStringify(value[k]); + }).join(",") + "}"; +} + +function isDefault(value, def) { + return stableStringify(value) === stableStringify(def); +} + +function cloneDef(def) { + if (def === null || typeof def !== "object") + return def; + return JSON.parse(JSON.stringify(def)); +} + +function stripDefaults(obj, SPEC) { + for (var k in SPEC) { + if (!(k in obj)) + continue; + if (isDefault(obj[k], SPEC[k].def)) + delete obj[k]; + } +} diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index f8c264d1d..8e0ade880 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -1897,7 +1897,7 @@ Item { IpcHandler { function listProfiles(): string { const profiles = DisplayConfigState.validatedProfiles; - const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor); + const activeId = SessionData.getActiveDisplayProfile(CompositorService.compositor); const matchedId = DisplayConfigState.matchedProfile; const lines = []; @@ -1952,7 +1952,7 @@ Item { if (ids.length === 0) return "ERROR: No profiles configured"; - const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor); + const activeId = SessionData.getActiveDisplayProfile(CompositorService.compositor); const idx = ids.indexOf(activeId); const nextId = ids[(idx + 1) % ids.length]; DisplayConfigState.activateProfile(nextId); @@ -1969,7 +1969,7 @@ Item { function status(): string { const auto = SettingsData.displayProfileAutoSelect ? "on" : "off"; - const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor); + const activeId = SessionData.getActiveDisplayProfile(CompositorService.compositor); const matchedId = DisplayConfigState.matchedProfile; const profiles = DisplayConfigState.validatedProfiles; const activeName = profiles[activeId]?.name || "none"; diff --git a/quickshell/Modals/AppPickerModal.qml b/quickshell/Modals/AppPickerModal.qml index 7a49b8f3a..faafe2382 100644 --- a/quickshell/Modals/AppPickerModal.qml +++ b/quickshell/Modals/AppPickerModal.qml @@ -91,7 +91,7 @@ DankModal { function updateApplicationList() { applicationsModel.clear(); const apps = AppSearchService.applications; - const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}; + const usageHistory = usageHistoryKey && CacheData[usageHistoryKey] ? CacheData[usageHistoryKey] : {}; const hasCategoryFilter = categoryFilter.length > 0; const hasMime = mimeType.length > 0; const hasMimeMatches = mimeMatchedAppIds.length > 0; @@ -557,14 +557,14 @@ DankModal { root.applicationSelected(app, root.targetData); if (usageHistoryKey && app.appId) { - const usageHistory = SettingsData[usageHistoryKey] || {}; + const usageHistory = CacheData[usageHistoryKey] || {}; const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0; usageHistory[app.appId] = { count: currentCount + 1, lastUsed: Date.now(), name: app.name }; - SettingsData.set(usageHistoryKey, usageHistory); + CacheData.set(usageHistoryKey, usageHistory); } root.close(); diff --git a/quickshell/Modules/ControlCenter/BuiltinPlugins/DisplayProfilesWidget.qml b/quickshell/Modules/ControlCenter/BuiltinPlugins/DisplayProfilesWidget.qml index fb9b19c66..4b6d77707 100644 --- a/quickshell/Modules/ControlCenter/BuiltinPlugins/DisplayProfilesWidget.qml +++ b/quickshell/Modules/ControlCenter/BuiltinPlugins/DisplayProfilesWidget.qml @@ -21,7 +21,7 @@ PluginComponent { return result; } readonly property bool autoMode: SettingsData.displayProfileAutoSelect - readonly property string activeProfileId: SettingsData.getActiveDisplayProfile(CompositorService.compositor) + readonly property string activeProfileId: SessionData.getActiveDisplayProfile(CompositorService.compositor) readonly property var activeProfile: allProfiles[activeProfileId] || null readonly property string activeProfileName: activeProfile?.name ?? "" readonly property string displayProfileLabel: { @@ -44,7 +44,7 @@ PluginComponent { function setAutoMode(enabled) { SettingsData.displayProfileAutoSelect = enabled; if (!enabled) - SettingsData.setActiveDisplayProfile(CompositorService.compositor, ""); + SessionData.setActiveDisplayProfile(CompositorService.compositor, ""); SettingsData.saveSettings(); if (enabled) DisplayConfigState.applyAutoConfig(); diff --git a/quickshell/Modules/Plugins/DesktopPluginWrapper.qml b/quickshell/Modules/Plugins/DesktopPluginWrapper.qml index 86ce5a7df..708cae56b 100644 --- a/quickshell/Modules/Plugins/DesktopPluginWrapper.qml +++ b/quickshell/Modules/Plugins/DesktopPluginWrapper.qml @@ -108,6 +108,7 @@ Item { } readonly property string screenKey: SettingsData.getScreenDisplayName(screen) readonly property string positionKey: syncPositionAcrossScreens ? "_synced" : screenKey + readonly property var storedPositions: SessionData.desktopWidgetInstancePositions[instanceId] ?? null readonly property int screenWidth: screen?.width ?? 1920 readonly property int screenHeight: screen?.height ?? 1080 @@ -121,7 +122,7 @@ Item { readonly property bool hasSavedPosition: { if (isInstance) - return instanceData?.positions?.[positionKey]?.x !== undefined; + return storedPositions?.[positionKey]?.x !== undefined; if (usePluginService) return pluginService.loadPluginData(pluginId, "desktopX_" + positionKey, null) !== null; return SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "x", null) !== null; @@ -129,7 +130,7 @@ Item { readonly property bool hasSavedSize: { if (isInstance) - return instanceData?.positions?.[positionKey]?.width !== undefined; + return storedPositions?.[positionKey]?.width !== undefined; if (usePluginService) return pluginService.loadPluginData(pluginId, "desktopWidth_" + positionKey, null) !== null; return SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "width", null) !== null; @@ -137,7 +138,7 @@ Item { property real savedX: { if (isInstance) { - const val = instanceData?.positions?.[positionKey]?.x; + const val = storedPositions?.[positionKey]?.x; if (val === undefined) return screenWidth / 2 - savedWidth / 2; return syncPositionAcrossScreens ? val * screenWidth : val; @@ -155,7 +156,7 @@ Item { } property real savedY: { if (isInstance) { - const val = instanceData?.positions?.[positionKey]?.y; + const val = storedPositions?.[positionKey]?.y; if (val === undefined) return screenHeight / 2 - savedHeight / 2; return syncPositionAcrossScreens ? val * screenHeight : val; @@ -173,7 +174,7 @@ Item { } property real savedWidth: { if (isInstance) { - const val = instanceData?.positions?.[positionKey]?.width; + const val = storedPositions?.[positionKey]?.width; if (val === undefined) return 280; return val; @@ -191,7 +192,7 @@ Item { } property real savedHeight: { if (isInstance) { - const val = instanceData?.positions?.[positionKey]?.height; + const val = storedPositions?.[positionKey]?.height; if (val === undefined) return forceSquare ? savedWidth : 180; return forceSquare ? savedWidth : val; @@ -236,14 +237,14 @@ Item { property bool acceptsKeyboardFocus: contentLoader.item?.acceptsKeyboardFocus ?? false property bool isInteracting: dragArea.pressed || resizeArea.pressed - property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings + property var _gridSettingsTrigger: SessionData.desktopWidgetGridSettings readonly property int gridSize: { void _gridSettingsTrigger; - return SettingsData.getDesktopWidgetGridSetting(screenKey, "size", 40); + return SessionData.getDesktopWidgetGridSetting(screenKey, "size", 40); } readonly property bool gridEnabled: { void _gridSettingsTrigger; - return SettingsData.getDesktopWidgetGridSetting(screenKey, "enabled", false); + return SessionData.getDesktopWidgetGridSetting(screenKey, "enabled", false); } function snapToGrid(value) { @@ -254,7 +255,7 @@ Item { const xVal = syncPositionAcrossScreens ? finalX / screenWidth : finalX; const yVal = syncPositionAcrossScreens ? finalY / screenHeight : finalY; if (isInstance && instanceData) { - SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { + SessionData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { x: xVal, y: yVal }); @@ -275,7 +276,7 @@ Item { const sizeVal = forceSquare ? Math.max(finalW, finalH) : finalW; const heightVal = forceSquare ? sizeVal : finalH; if (isInstance && instanceData) { - SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { + SessionData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { width: sizeVal, height: heightVal }); @@ -342,15 +343,15 @@ Item { return; switch (event.key) { case Qt.Key_G: - SettingsData.setDesktopWidgetGridSetting(root.screenKey, "enabled", !root.gridEnabled); + SessionData.setDesktopWidgetGridSetting(root.screenKey, "enabled", !root.gridEnabled); event.accepted = true; break; case Qt.Key_Z: - SettingsData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.max(10, root.gridSize - 10)); + SessionData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.max(10, root.gridSize - 10)); event.accepted = true; break; case Qt.Key_X: - SettingsData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.min(200, root.gridSize + 10)); + SessionData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.min(200, root.gridSize + 10)); event.accepted = true; break; } diff --git a/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml b/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml index 7e6bda8fd..05a69e287 100644 --- a/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml +++ b/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml @@ -362,7 +362,7 @@ SettingsCard { if (!root.instanceId) return; if (isChecked) - SettingsData.syncDesktopWidgetPositionToAllScreens(root.instanceId); + SessionData.syncDesktopWidgetPositionToAllScreens(root.instanceId); SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { syncPositionAcrossScreens: isChecked }); diff --git a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml index b4d41f56f..db7fe4600 100644 --- a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml +++ b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml @@ -234,8 +234,8 @@ Singleton { default: parsed = {}; } - const niriSettings = SettingsData.niriOutputSettings || {}; - const hyprSettings = SettingsData.hyprlandOutputSettings || {}; + const niriSettings = SessionData.niriOutputSettings || {}; + const hyprSettings = SessionData.hyprlandOutputSettings || {}; const profileOutputs = {}; for (const outputName in parsed) { const od = parsed[outputName]; @@ -267,7 +267,7 @@ Singleton { function publishActiveProfileModes() { const compositor = CompositorService.compositor; - const profileId = SettingsData.getActiveDisplayProfile(compositor); + const profileId = SessionData.getActiveDisplayProfile(compositor); const profile = profileId ? validatedProfiles[profileId] : null; const outputs = profile?.outputs || {}; const modes = {}; @@ -280,7 +280,7 @@ Singleton { }; } - SettingsData.setActiveDisplayProfileModes(compositor, modes); + SessionData.setActiveDisplayProfileModes(compositor, modes); } function generateProfileId() { @@ -561,7 +561,7 @@ Singleton { } }; const onWriteSuccess = () => { - SettingsData.setActiveDisplayProfile(CompositorService.compositor, configId); + SessionData.setActiveDisplayProfile(CompositorService.compositor, configId); publishActiveProfileModes(); if (isManual) { profilesLoading = false; @@ -651,7 +651,7 @@ Singleton { validatedProfiles = updated; currentOutputSet = buildCurrentOutputSet(); matchedProfile = findMatchingProfile(); - SettingsData.setActiveDisplayProfile(CompositorService.compositor, id); + SessionData.setActiveDisplayProfile(CompositorService.compositor, id); publishActiveProfileModes(); profileSaved(id, profileName); }); @@ -680,7 +680,7 @@ Singleton { function deleteProfile(profileId) { const compositor = CompositorService.compositor; - const isActive = SettingsData.getActiveDisplayProfile(compositor) === profileId; + const isActive = SessionData.getActiveDisplayProfile(compositor) === profileId; profilesLoading = true; readMonitorsJson(data => { @@ -691,7 +691,7 @@ Singleton { profilesLoading = false; SettingsData.removeDisplayProfile(compositor, profileId); if (isActive) { - SettingsData.setActiveDisplayProfile(compositor, ""); + SessionData.setActiveDisplayProfile(compositor, ""); backendWriteOutputsConfig(allOutputs); } const updated = JSON.parse(JSON.stringify(validatedProfiles)); @@ -774,7 +774,7 @@ Singleton { const match = findConfigEntryByFingerprint(data, currentOutputSet, false); if (match) { if (configEntryMatchesLiveLayout(match.entry)) { - SettingsData.setActiveDisplayProfile(CompositorService.compositor, match.entry.id); + SessionData.setActiveDisplayProfile(CompositorService.compositor, match.entry.id); return; } applyConfigEntry(match.entry, match.entry.id, "", false); @@ -968,7 +968,7 @@ Singleton { } function initHyprlandSettingsFromConfig(parsedOutputs) { - const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + const current = JSON.parse(JSON.stringify(SessionData.hyprlandOutputSettings)); let changed = false; for (const outputName in parsedOutputs) { @@ -997,13 +997,13 @@ Singleton { } if (changed) { - SettingsData.hyprlandOutputSettings = current; - SettingsData.saveSettings(); + SessionData.hyprlandOutputSettings = current; + SessionData.saveSettings(); } } function syncHyprlandVrrFromConfig(parsedOutputs) { - const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + const current = JSON.parse(JSON.stringify(SessionData.hyprlandOutputSettings)); let changed = false; for (const outputName in parsedOutputs) { const settings = parsedOutputs[outputName]?.hyprlandSettings; @@ -1020,28 +1020,24 @@ Singleton { changed = true; } if (changed) { - SettingsData.hyprlandOutputSettings = current; - SettingsData.saveSettings(); + SessionData.hyprlandOutputSettings = current; + SessionData.saveSettings(); } } function syncNiriVrrFromConfig(parsedOutputs) { - let changed = false; for (const outputName in parsedOutputs) { const output = parsedOutputs[outputName]; - const current = SettingsData.getNiriOutputSetting(outputName, "vrrOnDemand", false); + const current = SessionData.getNiriOutputSetting(outputName, "vrrOnDemand", false); const fromConfig = output.vrr_on_demand ?? false; if (current === fromConfig) continue; - SettingsData.setNiriOutputSetting(outputName, "vrrOnDemand", fromConfig || undefined); - changed = true; + SessionData.setNiriOutputSetting(outputName, "vrrOnDemand", fromConfig || undefined); } - if (changed) - SettingsData.saveSettings(); } function syncHyprlandDisabledFromConfig(parsedOutputs) { - const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + const current = JSON.parse(JSON.stringify(SessionData.hyprlandOutputSettings)); let changed = false; for (const outputName in parsedOutputs) { const settings = parsedOutputs[outputName]?.hyprlandSettings; @@ -1058,24 +1054,20 @@ Singleton { changed = true; } if (changed) { - SettingsData.hyprlandOutputSettings = current; - SettingsData.saveSettings(); + SessionData.hyprlandOutputSettings = current; + SessionData.saveSettings(); } } function syncNiriDisabledFromConfig(parsedOutputs) { - let changed = false; for (const outputName in parsedOutputs) { const output = parsedOutputs[outputName]; const fromConfig = output.disabled ?? false; - const current = SettingsData.getNiriOutputSetting(outputName, "disabled", false); + const current = SessionData.getNiriOutputSetting(outputName, "disabled", false); if (current === fromConfig) continue; - SettingsData.setNiriOutputSetting(outputName, "disabled", fromConfig || undefined); - changed = true; + SessionData.setNiriOutputSetting(outputName, "disabled", fromConfig || undefined); } - if (changed) - SettingsData.saveSettings(); } function filterDisconnectedOnly(parsedOutputs) { @@ -1965,7 +1957,7 @@ Singleton { const pending = pendingNiriChanges[identifier]; if (pending && pending[key] !== undefined) return pending[key]; - return SettingsData.getNiriOutputSetting(identifier, key, defaultValue); + return SessionData.getNiriOutputSetting(identifier, key, defaultValue); } function setNiriSetting(output, outputName, key, value) { @@ -1983,7 +1975,7 @@ Singleton { function initOriginalNiriSettings() { if (originalNiriSettings) return; - originalNiriSettings = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings)); + originalNiriSettings = JSON.parse(JSON.stringify(SessionData.niriOutputSettings)); } function getHyprlandOutputIdentifier(output, outputName) { @@ -2001,7 +1993,7 @@ Singleton { const val = pending[key]; return (val !== null && val !== undefined) ? val : defaultValue; } - return SettingsData.getHyprlandOutputSetting(identifier, key, defaultValue); + return SessionData.getHyprlandOutputSetting(identifier, key, defaultValue); } function setHyprlandSetting(output, outputName, key, value) { @@ -2019,7 +2011,7 @@ Singleton { function initOriginalHyprlandSettings() { if (originalHyprlandSettings) return; - originalHyprlandSettings = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + originalHyprlandSettings = JSON.parse(JSON.stringify(SessionData.hyprlandOutputSettings)); } function initOriginalOutputs() { @@ -2259,7 +2251,7 @@ Singleton { } function buildMergedNiriSettings() { - const merged = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings)); + const merged = JSON.parse(JSON.stringify(SessionData.niriOutputSettings)); for (const outputId in pendingNiriChanges) { if (!merged[outputId]) merged[outputId] = {}; @@ -2278,20 +2270,20 @@ Singleton { function commitNiriSettingsChanges() { for (const outputId in pendingNiriChanges) { for (const key in pendingNiriChanges[outputId]) { - SettingsData.setNiriOutputSetting(outputId, key, pendingNiriChanges[outputId][key]); + SessionData.setNiriOutputSetting(outputId, key, pendingNiriChanges[outputId][key]); } } // Clear stale disabled from SettingsData so NiriService reads clean state if (Object.keys(outputs).length <= 1) { - for (const id in SettingsData.niriOutputSettings) { - if (SettingsData.niriOutputSettings[id]?.disabled) - SettingsData.setNiriOutputSetting(id, "disabled", null); + for (const id in SessionData.niriOutputSettings) { + if (SessionData.niriOutputSettings[id]?.disabled) + SessionData.setNiriOutputSetting(id, "disabled", null); } } } function buildMergedHyprlandSettings() { - const merged = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + const merged = JSON.parse(JSON.stringify(SessionData.hyprlandOutputSettings)); for (const outputId in pendingHyprlandChanges) { if (!merged[outputId]) merged[outputId] = {}; @@ -2316,16 +2308,16 @@ Singleton { for (const key in pendingHyprlandChanges[outputId]) { const val = pendingHyprlandChanges[outputId][key]; if (val === null || val === undefined) - SettingsData.removeHyprlandOutputSetting(outputId, key); + SessionData.removeHyprlandOutputSetting(outputId, key); else - SettingsData.setHyprlandOutputSetting(outputId, key, val); + SessionData.setHyprlandOutputSetting(outputId, key, val); } } // Clear stale disabled from SettingsData so HyprlandService reads clean state if (Object.keys(outputs).length <= 1) { - for (const id in SettingsData.hyprlandOutputSettings) { - if (SettingsData.hyprlandOutputSettings[id]?.disabled) - SettingsData.removeHyprlandOutputSetting(id, "disabled"); + for (const id in SessionData.hyprlandOutputSettings) { + if (SessionData.hyprlandOutputSettings[id]?.disabled) + SessionData.removeHyprlandOutputSetting(id, "disabled"); } } } @@ -2372,13 +2364,13 @@ Singleton { } if (hadNiriChanges) { - SettingsData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings)); - SettingsData.saveSettings(); + SessionData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings)); + SessionData.saveSettings(); } if (hadHyprlandChanges) { - SettingsData.hyprlandOutputSettings = JSON.parse(JSON.stringify(originalHyprlandSettings)); - SettingsData.saveSettings(); + SessionData.hyprlandOutputSettings = JSON.parse(JSON.stringify(originalHyprlandSettings)); + SessionData.saveSettings(); } pendingHyprlandChanges = {}; diff --git a/quickshell/Modules/Settings/DisplayConfigTab.qml b/quickshell/Modules/Settings/DisplayConfigTab.qml index ad548476d..261e26d0e 100644 --- a/quickshell/Modules/Settings/DisplayConfigTab.qml +++ b/quickshell/Modules/Settings/DisplayConfigTab.qml @@ -12,7 +12,7 @@ Item { LayoutMirroring.childrenInherit: true property string selectedProfileId: { - const id = SettingsData.activeDisplayProfile[CompositorService.compositor] || ""; + const id = SessionData.activeDisplayProfile[CompositorService.compositor] || ""; if (!SettingsData.displayProfileAutoSelect) { const profile = DisplayConfigState.validatedProfiles[id]; if (profile && profile.name === "") @@ -169,7 +169,7 @@ Item { onToggled: checked => { SettingsData.displayProfileAutoSelect = checked; if (!checked) - SettingsData.setActiveDisplayProfile(CompositorService.compositor, ""); + SessionData.setActiveDisplayProfile(CompositorService.compositor, ""); SettingsData.saveSettings(); if (checked) DisplayConfigState.applyAutoConfig(); diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index 9ed0173ea..45e72ac5e 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -714,7 +714,7 @@ Item { Rectangle { id: syncPendingPill - readonly property bool shown: SettingsData.greeterSyncPending && root.greeterInstalled + readonly property bool shown: SessionData.greeterSyncPending && root.greeterInstalled anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index 565a284cc..74db71fc9 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -377,7 +377,7 @@ Singleton { function applyWlrModeOverrides(modeOverrides, callback) { if (CompositorService.isHyprland) { - HyprlandService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), SettingsData.hyprlandOutputSettings, callback); + HyprlandService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), SessionData.hyprlandOutputSettings, callback); return; } @@ -519,7 +519,7 @@ Singleton { function findActiveProfileMode(outputName, output) { const compositor = CompositorService.compositor; - const profileModes = SettingsData.activeDisplayProfileModes?.[compositor] || {}; + const profileModes = SessionData.activeDisplayProfileModes?.[compositor] || {}; if (Object.keys(profileModes).length === 0) return ""; diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index 61e29dab0..519fc92eb 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -180,7 +180,7 @@ Singleton { return; } - const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings; + const settings = hyprlandSettings || SessionData.hyprlandOutputSettings; let lines = ["-- Auto-generated by DMS — do not edit manually", ""]; for (const outputName in outputsData) { diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 7f625c3d7..13a04da30 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1446,7 +1446,7 @@ window-rule { const identifier = getOutputIdentifier(output, outputName); if (niriSettings) return niriSettings[identifier] || niriSettings[outputName] || {}; - return SettingsData.getNiriOutputSettings(identifier); + return SessionData.getNiriOutputSettings(identifier); } function transformToNiri(transform) {