From d4a46e53611854f305599092f4d5a6b9418b8e82 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 21 Sep 2025 16:31:42 -0400 Subject: [PATCH] Default settings generation completed! --- Assets/settings-default.json | 182 +++++++++++++++++++++++++++++++++++ Commons/Settings.qml | 57 ++++++----- Helpers/QtObj2JS.js | 114 ++++++++++++++++++++++ 3 files changed, 330 insertions(+), 23 deletions(-) create mode 100644 Assets/settings-default.json create mode 100644 Helpers/QtObj2JS.js diff --git a/Assets/settings-default.json b/Assets/settings-default.json new file mode 100644 index 000000000..b14d05c7e --- /dev/null +++ b/Assets/settings-default.json @@ -0,0 +1,182 @@ +{ + "settingsVersion": 3, + "bar": { + "position": "top", + "backgroundOpacity": 1, + "monitors": [], + "density": "default", + "showCapsule": true, + "floating": false, + "marginVertical": 0.25, + "marginHorizontal": 0.25, + "widgets": { + "left": [ + { + "id": "SystemMonitor" + }, + { + "id": "ActiveWindow" + }, + { + "id": "MediaMini" + } + ], + "center": [ + { + "id": "Workspace" + } + ], + "right": [ + { + "id": "ScreenRecorderIndicator" + }, + { + "id": "Tray" + }, + { + "id": "NotificationHistory" + }, + { + "id": "WiFi" + }, + { + "id": "Bluetooth" + }, + { + "id": "Battery" + }, + { + "id": "Volume" + }, + { + "id": "Brightness" + }, + { + "id": "NightLight" + }, + { + "id": "Clock" + }, + { + "id": "SidePanelToggle" + } + ] + } + }, + "general": { + "avatarImage": "", + "dimDesktop": true, + "showScreenCorners": false, + "forceBlackScreenCorners": false, + "radiusRatio": 1, + "screenRadiusRatio": 1, + "animationSpeed": 1 + }, + "location": { + "name": "Tokyo", + "useFahrenheit": false, + "use12hourFormat": false, + "showWeekNumberInCalendar": false + }, + "screenRecorder": { + "directory": "", + "frameRate": 60, + "audioCodec": "opus", + "videoCodec": "h264", + "quality": "very_high", + "colorRange": "limited", + "showCursor": true, + "audioSource": "default_output", + "videoSource": "portal" + }, + "wallpaper": { + "enabled": true, + "directory": "", + "enableMultiMonitorDirectories": false, + "setWallpaperOnAllMonitors": true, + "fillMode": "crop", + "fillColor": "#000000", + "randomEnabled": false, + "randomIntervalSec": 300, + "transitionDuration": 1500, + "transitionType": "random", + "transitionEdgeSmoothness": 0.05, + "monitors": [] + }, + "appLauncher": { + "enableClipboardHistory": false, + "position": "center", + "backgroundOpacity": 1, + "pinnedExecs": [], + "useApp2Unit": false, + "sortByMostUsed": true + }, + "dock": { + "autoHide": false, + "exclusive": false, + "backgroundOpacity": 1, + "floatingRatio": 1, + "monitors": [] + }, + "network": { + "wifiEnabled": true, + "bluetoothEnabled": true + }, + "notifications": { + "doNotDisturb": false, + "monitors": [], + "lastSeenTs": 0, + "lowUrgencyDuration": 3, + "normalUrgencyDuration": 8, + "criticalUrgencyDuration": 15 + }, + "audio": { + "volumeStep": 5, + "cavaFrameRate": 60, + "visualizerType": "linear", + "mprisBlacklist": [], + "preferredPlayer": "" + }, + "ui": { + "fontDefault": "Roboto", + "fontFixed": "DejaVu Sans Mono", + "fontBillboard": "Inter", + "monitorsScaling": [], + "idleInhibitorEnabled": false + }, + "brightness": { + "brightnessStep": 5 + }, + "colorSchemes": { + "useWallpaperColors": false, + "predefinedScheme": "", + "darkMode": true + }, + "matugen": { + "gtk4": false, + "gtk3": false, + "qt6": false, + "qt5": false, + "kitty": false, + "ghostty": false, + "foot": false, + "fuzzel": false, + "vesktop": false, + "pywalfox": false, + "enableUserTemplates": false + }, + "nightLight": { + "enabled": false, + "forced": false, + "autoSchedule": true, + "nightTemp": "4000", + "dayTemp": "6500", + "manualSunrise": "06:30", + "manualSunset": "18:30" + }, + "hooks": { + "enabled": false, + "wallpaperChange": "", + "darkModeChange": "" + } +} \ No newline at end of file diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 436630861..f4c463bb8 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -5,10 +5,16 @@ import Quickshell import Quickshell.Io import qs.Commons import qs.Services +import "../Helpers/QtObj2JS.js" as QtObj2JS Singleton { id: root + // Used to access via Settings.data.xxx.yyy + readonly property alias data: adapter + property bool isLoaded: false + property bool directoriesCreated: false + // Define our app directories // Default config directory: ~/.config/noctalia // Default cache directory: ~/.cache/noctalia @@ -18,20 +24,14 @@ Singleton { property string cacheDirImages: cacheDir + "images/" property string cacheDirImagesWallpapers: cacheDir + "images/wallpapers/" property string cacheDirImagesNotifications: cacheDir + "images/notifications/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + property string defaultLocation: "Tokyo" + property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png" + property string defaultAvatar: Quickshell.env("HOME") + "/.face" property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos" - property string defaultLocation: "Tokyo" property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers" - property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png" - - // Used to access via Settings.data.xxx.yyy - readonly property alias data: adapter - - property bool isLoaded: false - property bool directoriesCreated: false // Signal emitted when settings are loaded after startupcale changes signal settingsLoaded @@ -50,11 +50,18 @@ Singleton { // Mark directories as created and trigger file loading directoriesCreated = true - generateDefaultSettings(); - - settingsFileView.adapter = adapter; + // This should only be activated once when the settings structure has changed + // Then it should be commented out again, regular users don't need to generate + // default settings on every start + //generateDefaultSettings() + // Patch-in the local default, resolved to user's home + adapter.general.avatarImage = defaultAvatar + adapter.screenRecorder.directory = defaultVideosDirectory + adapter.wallpaper.directory = defaultWallpapersDirectory + // Set the adapter to the settingsFileView to trigger the real settings load + settingsFileView.adapter = adapter } // Don't write settings to disk immediately @@ -162,7 +169,7 @@ Singleton { // general property JsonObject general: JsonObject { - property string avatarImage: defaultAvatar + property string avatarImage: "" property bool dimDesktop: true property bool showScreenCorners: false property bool forceBlackScreenCorners: false @@ -181,7 +188,7 @@ Singleton { // screen recorder property JsonObject screenRecorder: JsonObject { - property string directory: defaultVideosDirectory + property string directory: "" property int frameRate: 60 property string audioCodec: "opus" property string videoCodec: "h264" @@ -195,7 +202,7 @@ Singleton { // wallpaper property JsonObject wallpaper: JsonObject { property bool enabled: true - property string directory: defaultWallpapersDirectory + property string directory: "" property bool enableMultiMonitorDirectories: false property bool setWallpaperOnAllMonitors: true property string fillMode: "crop" @@ -271,7 +278,7 @@ Singleton { property JsonObject colorSchemes: JsonObject { property bool useWallpaperColors: false - property string predefinedScheme: "" + property string predefinedScheme: "Noctalia (default)" property bool darkMode: true } @@ -310,18 +317,21 @@ Singleton { } } + // ----------------------------------------------------- + // Generate default settings at the root of the repo function generateDefaultSettings() { try { - Logger.log("Settings", "Generating default settings file...") + Logger.log("Settings", "Generating settings-default.json") - var defaultPath = configDir + "settings-default.json" - var jsonData = JSON.stringify(adapter, null, 2) - var tempFile = "/tmp/noctalia-default-settings.json" + // Prepare a clean JSON + var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter) + var jsonData = JSON.stringify(plainAdapter, null, 2) - // Write to temp file first, then copy to final location - Quickshell.execDetached(["sh", "-c", `echo '${jsonData}' > "${tempFile}" && cp "${tempFile}" "${defaultPath}" && rm "${tempFile}"`]) + var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json" - Logger.log("Settings", "Generated settings-default.json with default values") + // Encode transfer it has base64 to avoid any escaping issue + var base64Data = Qt.btoa(jsonData) + Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`]) } catch (error) { Logger.error("Settings", "Failed to generate default settings file: " + error) } @@ -467,6 +477,7 @@ Singleton { const widgetAfter = JSON.stringify(widget) return (widgetAfter !== widgetBefore) } + // ----------------------------------------------------- // Kickoff essential services function kickOffServices() { diff --git a/Helpers/QtObj2JS.js b/Helpers/QtObj2JS.js new file mode 100644 index 000000000..75a248faf --- /dev/null +++ b/Helpers/QtObj2JS.js @@ -0,0 +1,114 @@ +// ----------------------------------------------------- +// Helper function to convert Qt objects to plain JavaScript objects +// Only used when generating settings-default.json +function qtObjectToPlainObject(obj) { + if (obj === null || obj === undefined) { + return obj; + } + + // Handle primitive types + if (typeof obj !== "object") { + return obj; + } + + // Handle native JavaScript arrays + if (Array.isArray(obj)) { + return obj.map((item) => qtObjectToPlainObject(item)); + } + + // Detect QML arrays FIRST (before color detection) + // QML arrays have a numeric length property and indexed properties + if (typeof obj.length === "number" && obj.length >= 0) { + // Check if it has indexed properties - be more flexible about detection + var hasIndexedProps = true; + var hasNumericKeys = false; + + // Check if we have at least some numeric properties + for (var i = 0; i < obj.length; i++) { + if (obj.hasOwnProperty(i) || obj[i] !== undefined) { + hasNumericKeys = true; + break; + } + } + + // If we have length > 0 and some numeric keys, treat as array + if (obj.length > 0 && hasNumericKeys) { + var arr = []; + for (var i = 0; i < obj.length; i++) { + // Use direct property access, handle undefined gracefully + var item = obj[i]; + if (item !== undefined) { + arr.push(qtObjectToPlainObject(item)); + } + } + return arr; // Return here to avoid processing as object + } + + // Handle empty arrays (length = 0) + if (obj.length === 0) { + return []; + } + } + + // Detect and convert QML color objects to hex strings + if ( + typeof obj.r === "number" && + typeof obj.g === "number" && + typeof obj.b === "number" && + typeof obj.a === "number" && + typeof obj.valid === "boolean" + ) { + // This looks like a QML color object + try { + // Try to get the string representation (should be hex like "#000000") + if (typeof obj.toString === "function") { + return obj.toString(); + } else { + // Fallback: convert RGBA to hex manually + var r = Math.round(obj.r * 255); + var g = Math.round(obj.g * 255); + var b = Math.round(obj.b * 255); + var hex = + "#" + + r.toString(16).padStart(2, "0") + + g.toString(16).padStart(2, "0") + + b.toString(16).padStart(2, "0"); + return hex; + } + } catch (e) { + // If conversion fails, fall through to regular object handling + } + } + + // Handle regular objects + var plainObj = {}; + + // Get all property names, but filter out Qt-specific ones + var propertyNames = Object.getOwnPropertyNames(obj); + + for (var i = 0; i < propertyNames.length; i++) { + var propName = propertyNames[i]; + + // Skip Qt-specific properties, functions, and array-like properties + if ( + propName === "objectName" || + propName === "objectNameChanged" || + propName === "length" || // Skip length property + /^\d+$/.test(propName) || // Skip numeric keys (0, 1, 2, etc.) + propName.endsWith("Changed") || + typeof obj[propName] === "function" + ) { + continue; + } + + try { + var value = obj[propName]; + plainObj[propName] = qtObjectToPlainObject(value); + } catch (e) { + // Skip properties that can't be accessed + continue; + } + } + + return plainObj; +}