From 1ca571458c82b5a6995e5beacda7d37c6d451a94 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 01:32:13 -0400 Subject: [PATCH] Complete Panama settings controls --- config/dot/quickshell/config/Settings.qml | 20 +- .../quickshell/modules/settings/AboutPage.qml | 68 +++-- .../modules/settings/ConnectivityPage.qml | 126 ++++----- .../modules/settings/DisplaysPage.qml | 100 +++---- .../quickshell/modules/settings/HomePage.qml | 259 +++++++++--------- .../modules/settings/NotificationsPage.qml | 145 +++++----- .../settings/ScreenIntelligencePage.qml | 161 +++++------ .../modules/settings/ServicesPage.qml | 185 ++++++++----- .../quickshell/modules/settings/SoundPage.qml | 89 +++--- .../settings-hardcoded-values-contract.sh | 43 +++ tests/quickshell/settings-pages-contract.sh | 84 +++++- 11 files changed, 725 insertions(+), 555 deletions(-) create mode 100755 tests/quickshell/settings-hardcoded-values-contract.sh diff --git a/config/dot/quickshell/config/Settings.qml b/config/dot/quickshell/config/Settings.qml index 7d09c2f..31f40e7 100644 --- a/config/dot/quickshell/config/Settings.qml +++ b/config/dot/quickshell/config/Settings.qml @@ -28,13 +28,13 @@ Singleton { // label deliberately general rather than exposing precise coordinates in // the UI or guessing at a city from them. readonly property string weatherLocation: "Local weather" - readonly property string temperatureUnit: "fahrenheit" - readonly property int weatherRefreshMinutes: 20 + readonly property string temperatureUnit: DesktopPreferences.get("temperatureUnit") + readonly property int weatherRefreshMinutes: DesktopPreferences.get("weatherRefreshMinutes") // ── Vitals ────────────────────────────────────────────────────────────── // The GNOME Vitals extension showed processor usage, memory usage and GPU // usage, in that order. Same here. - readonly property int vitalsIntervalMs: 2000 + readonly property int vitalsIntervalMs: DesktopPreferences.get("vitalsIntervalMs") readonly property bool showCpu: DesktopPreferences.get("showCpu") readonly property bool showMemory: DesktopPreferences.get("showMemory") readonly property bool showGpu: DesktopPreferences.get("showGpu") @@ -51,10 +51,10 @@ Singleton { readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled") // ── Notifications ─────────────────────────────────────────────────────── - readonly property int notificationTimeoutMs: 5000 - readonly property int notificationTimeoutCriticalMs: 0 // 0 = never auto-expire - readonly property int notificationHistoryLimit: 100 - readonly property int maxVisibleToasts: 4 + readonly property int notificationTimeoutMs: DesktopPreferences.get("notificationTimeoutMs") + readonly property int notificationTimeoutCriticalMs: DesktopPreferences.get("notificationTimeoutCriticalMs") // 0 = never auto-expire + readonly property int notificationHistoryLimit: DesktopPreferences.get("notificationHistoryLimit") + readonly property int maxVisibleToasts: DesktopPreferences.get("maxVisibleToasts") // ── Focus ────────────────────────────────────────────────────────────── // One deliberate default rather than a preset picker: quick settings and @@ -79,9 +79,9 @@ Singleton { readonly property int dockHideDelayMs: DesktopPreferences.get("dockHideDelayMs") // ── Capture ───────────────────────────────────────────────────────────── - readonly property string screenshotDir: "Pictures/Screenshots" - readonly property string recordingDir: "Videos/Recordings" + readonly property string screenshotDir: DesktopPreferences.get("screenshotDir") + readonly property string recordingDir: DesktopPreferences.get("recordingDir") // Passed to wf-recorder. Uses VAAPI on the AMD card so recording does not // cost CPU while gaming. - readonly property string recorderArgs: "-c h264_vaapi -d /dev/dri/renderD128" + readonly property string recorderArgs: DesktopPreferences.get("recorderArgs") } diff --git a/config/dot/quickshell/modules/settings/AboutPage.qml b/config/dot/quickshell/modules/settings/AboutPage.qml index 6c4871c..d648c56 100644 --- a/config/dot/quickshell/modules/settings/AboutPage.qml +++ b/config/dot/quickshell/modules/settings/AboutPage.qml @@ -3,39 +3,49 @@ import Quickshell import qs.config import qs.services -Item { - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds +SettingsPage { + title: "About Panama" + lede: "A curated Hyprland desktop built around focus, speed, and good taste." - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingsCard { + title: "Panama Desktop" + subtitle: "Tokyo Night Moon · Prism glass · native tiling" - Text { text: "About Panama"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - Text { text: "A curated Hyprland desktop built around focus, speed, and good taste."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } + TextRow { + label: "Hyprland" + value: SystemSettings.hyprlandVersion || "Detecting…" + } + TextRow { + label: "Quickshell" + value: SystemSettings.quickshellVersion + } + TextRow { + label: "Display" + value: SystemSettings.monitorName || "Detecting…" + } + TextRow { + label: "Configuration" + detail: Quickshell.shellDir + value: "Local" + divider: false + } + } - SettingsCard { - title: "Panama Desktop" - subtitle: "Tokyo Night Moon · Prism glass · native tiling" - SettingRow { label: "Hyprland"; value: SystemSettings.hyprlandVersion || "Detecting…" } - SettingRow { label: "Quickshell"; value: SystemSettings.quickshellVersion } - SettingRow { label: "Display"; value: SystemSettings.monitorName || "Detecting…" } - SettingRow { label: "Configuration"; detail: Quickshell.shellDir; value: "Local"; divider: false } - } + SettingsCard { + title: "Design principles" - SettingsCard { - title: "Design principles" - SettingRow { label: "Curated by default"; detail: "Strong choices instead of an incoherent matrix of switches" } - SettingRow { label: "Quiet while idle"; detail: "No continuous decorative repaint loops" } - SettingRow { label: "Real system boundaries"; detail: "Every control either works or clearly hands off to its owner"; divider: false } - } + TextRow { + label: "Curated by default" + detail: "Strong choices instead of an incoherent matrix of switches" + } + TextRow { + label: "Quiet while idle" + detail: "No continuous decorative repaint loops" + } + TextRow { + label: "Real system boundaries" + detail: "Every control either works or clearly hands off to its owner" + divider: false } } } diff --git a/config/dot/quickshell/modules/settings/ConnectivityPage.qml b/config/dot/quickshell/modules/settings/ConnectivityPage.qml index 9e4c549..182be9f 100644 --- a/config/dot/quickshell/modules/settings/ConnectivityPage.qml +++ b/config/dot/quickshell/modules/settings/ConnectivityPage.qml @@ -5,9 +5,12 @@ import qs.config import qs.services import qs.modules.quicksettings -Item { +SettingsPage { id: root + title: "Network & Devices" + lede: "Connect graphically—no terminal workflow required." + readonly property var wifiDevice: { for (const device of Networking.devices.values) { if (device.type === DeviceType.Wifi) @@ -17,72 +20,73 @@ Item { } readonly property var bluetoothAdapter: Bluetooth.defaultAdapter - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds + SettingsCard { + title: "Wi‑Fi" + subtitle: Networking.wifiEnabled ? "Available networks" : "Wireless networking is off" - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingRow { + label: "Wi‑Fi" + detail: root.wifiDevice ? "Managed by NetworkManager" : "No wireless adapter found" + controlWidth: 48 - Text { text: "Network & Devices"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - Text { text: "Connect graphically—no terminal workflow required."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } - - SettingsCard { - title: "Wi‑Fi" - subtitle: Networking.wifiEnabled ? "Available networks" : "Wireless networking is off" - SettingRow { - label: "Wi‑Fi" - detail: root.wifiDevice ? "Managed by NetworkManager" : "No wireless adapter found" - controlWidth: 48 - SettingsToggle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - checked: Networking.wifiEnabled - enabled: Networking.wifiHardwareEnabled - onToggled: value => Networking.wifiEnabled = value - } - } - WifiList { width: parent.width; device: root.wifiDevice; active: true; maxHeight: 240 } - SettingRow { - label: "Advanced network settings" - detail: "VPN, wired profiles, DNS, and connection details" - divider: false - controlWidth: 104 - SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open panel"; onClicked: SystemSettings.openGnomePanel("network") } - } + SettingsToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: Networking.wifiEnabled + enabled: Networking.wifiHardwareEnabled + onToggled: value => Networking.wifiEnabled = value } + } - SettingsCard { - title: "Bluetooth" - subtitle: root.bluetoothAdapter?.enabled ? "Nearby and paired devices" : "Bluetooth is off" - SettingRow { - label: "Bluetooth" - detail: root.bluetoothAdapter ? "Pair and reconnect without leaving Settings" : "No Bluetooth adapter found" - controlWidth: 48 - SettingsToggle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - checked: root.bluetoothAdapter?.enabled ?? false - enabled: root.bluetoothAdapter !== null - onToggled: value => { if (root.bluetoothAdapter) root.bluetoothAdapter.enabled = value; } - } - } - BluetoothList { width: parent.width; active: true; maxHeight: 220 } - SettingRow { - label: "Advanced Bluetooth settings" - detail: "Device details and system-level options" - divider: false - controlWidth: 104 - SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open panel"; onClicked: SystemSettings.openGnomePanel("bluetooth") } + WifiList { + width: parent.width + device: root.wifiDevice + active: true + maxHeight: 240 + } + + ActionRow { + label: "Advanced network settings" + detail: "VPN, wired profiles, DNS, and connection details" + divider: false + action: "Open panel" + onTriggered: SystemSettings.openGnomePanel("network") + } + } + + SettingsCard { + title: "Bluetooth" + subtitle: root.bluetoothAdapter?.enabled ? "Nearby and paired devices" : "Bluetooth is off" + + SettingRow { + label: "Bluetooth" + detail: root.bluetoothAdapter ? "Pair and reconnect without leaving Settings" : "No Bluetooth adapter found" + controlWidth: 48 + + SettingsToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: root.bluetoothAdapter?.enabled ?? false + enabled: root.bluetoothAdapter !== null + onToggled: value => { + if (root.bluetoothAdapter) + root.bluetoothAdapter.enabled = value; } } } + + BluetoothList { + width: parent.width + active: true + maxHeight: 220 + } + + ActionRow { + label: "Advanced Bluetooth settings" + detail: "Device details and system-level options" + divider: false + action: "Open panel" + onTriggered: SystemSettings.openGnomePanel("bluetooth") + } } } diff --git a/config/dot/quickshell/modules/settings/DisplaysPage.qml b/config/dot/quickshell/modules/settings/DisplaysPage.qml index d3b38ab..07230c7 100644 --- a/config/dot/quickshell/modules/settings/DisplaysPage.qml +++ b/config/dot/quickshell/modules/settings/DisplaysPage.qml @@ -3,64 +3,50 @@ import qs.config import qs.services import qs.widgets -Item { - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds +SettingsPage { + title: "Displays" + lede: SystemSettings.monitorDescription || "Reading the active display…" - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingsCard { + title: SystemSettings.monitorName || "Active display" + subtitle: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight} at ${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale · ${SystemSettings.monitorFormat}` + TextRow { label: "Color mode"; detail: "Wide-gamut SDR desktop at 10-bit"; value: SystemSettings.colorPreset || "wide" } + TextRow { label: "Variable refresh"; detail: SystemSettings.monitorVrrActive ? "Active for current fullscreen content" : "Ready when game or video content requests it"; value: SystemSettings.monitorVrrActive ? "Active" : "Standby"; divider: false } + } - Text { text: "Displays"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - Text { text: SystemSettings.monitorDescription || "Reading the active display…"; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } - - SettingsCard { - title: SystemSettings.monitorName || "Active display" - subtitle: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight} at ${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale · ${SystemSettings.monitorFormat}` - SettingRow { label: "Color mode"; detail: "Wide-gamut SDR desktop at 10-bit"; value: SystemSettings.colorPreset || "wide" } - SettingRow { label: "Variable refresh"; detail: SystemSettings.monitorVrrActive ? "Active for current fullscreen content" : "Ready when game or video content requests it"; value: SystemSettings.monitorVrrActive ? "Active" : "Standby"; divider: false } - } - - SettingsCard { - title: "Gaming display policy" - subtitle: "These values apply immediately and are restored when Panama starts." - SettingRow { + SettingsCard { + title: "Gaming display policy" + subtitle: "These values apply immediately and are restored when Panama starts." + SettingRow { label: "Game-aware HDR" detail: "Enter HDR only for fullscreen content that requests it" controlWidth: 48 SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.autoHdr; onToggled: value => SystemSettings.setAutoHdr(value) } - } - SettingRow { + } + SettingRow { label: "Content-aware VRR" detail: "Enable variable refresh only for game and video content" controlWidth: 48 SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.vrrPolicy === 3; onToggled: value => SystemSettings.setVrrPolicy(value ? 3 : 0) } - } - SettingRow { + } + SettingRow { label: "Direct scanout for games" detail: "Bypass compositing only for windows classified as games" divider: false controlWidth: 48 SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.directScanoutPolicy === 2; onToggled: value => SystemSettings.setDirectScanoutPolicy(value ? 2 : 0) } - } - } + } + } - SettingsCard { - title: "Night Light" - SettingRow { + SettingsCard { + title: "Night Light" + SettingRow { label: "Warm display colors" detail: NightLight.automatic ? "Following the evening schedule" : "Manual control" controlWidth: 48 SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: NightLight.active; onToggled: NightLight.toggle() } - } - SettingRow { + } + SettingRow { label: "Color temperature" detail: `${NightLight.temperature} K` divider: false @@ -71,27 +57,25 @@ Item { icon: "weather-clear-night-symbolic" onMoved: value => NightLight.temperature = Math.round((6500 - value * 4000) / 50) * 50 } - } - } + } + } - Rectangle { - width: parent.width - height: warningText.implicitHeight + 30 - radius: Theme.cardRadius - color: Theme.alpha(Theme.warn, 0.085) - border.width: 1 - border.color: Theme.alpha(Theme.warn, 0.22) - Text { - id: warningText - anchors.fill: parent - anchors.margins: 15 - text: "Full-time desktop HDR stays unavailable here because the current compositor path can break screenshots, OBS, Sunshine, and lock-screen capture. Game-aware HDR keeps the desktop dependable without giving up HDR games." - color: Theme.mix(Theme.fg, Theme.warn, 0.25) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.WordWrap - } - } + Rectangle { + width: parent.width + height: warningText.implicitHeight + 30 + radius: Theme.cardRadius + color: Theme.alpha(Theme.warn, 0.085) + border.width: 1 + border.color: Theme.alpha(Theme.warn, 0.22) + Text { + id: warningText + anchors.fill: parent + anchors.margins: 15 + text: "Full-time desktop HDR stays unavailable here because the current compositor path can break screenshots, OBS, Sunshine, and lock-screen capture. Game-aware HDR keeps the desktop dependable without giving up HDR games." + color: Theme.mix(Theme.fg, Theme.warn, 0.25) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap } } } diff --git a/config/dot/quickshell/modules/settings/HomePage.qml b/config/dot/quickshell/modules/settings/HomePage.qml index caac2fb..ef9777e 100644 --- a/config/dot/quickshell/modules/settings/HomePage.qml +++ b/config/dot/quickshell/modules/settings/HomePage.qml @@ -2,160 +2,159 @@ import QtQuick import qs.config import qs.services -Item { +SettingsPage { id: root readonly property int openedHour: new Date().getHours() readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening") - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds + title: `${root.greeting}, Gabriel` + lede: "Your Panama desktop is configured and ready." - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingsCard { + title: SystemSettings.monitorDescription || "Active display" + subtitle: SystemSettings.monitorName || "Detecting your display…" - Text { - text: `${root.greeting}, Gabriel` - color: Theme.fg - font.family: Theme.fontFamily - font.pixelSize: 27 - font.weight: Font.DemiBold - } + Grid { + id: monitorLayout - Text { - text: "Your Panama desktop is configured and ready." - color: Theme.fgDim - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - bottomPadding: 6 - } + width: parent.width + columns: width >= 620 ? 2 : 1 + columnSpacing: 28 + rowSpacing: 16 - SettingsCard { - title: SystemSettings.monitorDescription || "Active display" - subtitle: SystemSettings.monitorName || "Detecting your display…" + Item { + width: monitorLayout.columns === 2 + ? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47 + : monitorLayout.width + height: 164 - Row { - width: parent.width - height: 164 - spacing: 28 + Rectangle { + width: Math.min(parent.width - 24, 260) + height: width * 0.64 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 5 + radius: 12 + color: Theme.alpha(Theme.bgDark, 0.94) + border.width: 1 + border.color: Theme.alpha(Theme.accent, 0.34) - Item { - width: parent.width * 0.47 - height: parent.height - - Rectangle { - width: Math.min(parent.width - 24, 260) - height: width * 0.64 - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 5 - radius: 12 - color: Theme.alpha(Theme.bgDark, 0.94) - border.width: 1 - border.color: Theme.alpha(Theme.accent, 0.34) - - Rectangle { - anchors.fill: parent - anchors.margins: 10 - radius: 7 - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0; color: Theme.mix(Theme.bg, Theme.accent, 0.08) } - GradientStop { position: 1; color: Theme.mix(Theme.bg, Theme.accentSecondary, 0.08) } - } - - Text { - anchors.centerIn: parent - text: SystemSettings.monitorName || "DISPLAY" - color: Theme.fgMuted - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - font.letterSpacing: 1.5 - } - } + Rectangle { + anchors.fill: parent + anchors.margins: 10 + radius: 7 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0; color: Theme.mix(Theme.bg, Theme.accent, 0.08) } + GradientStop { position: 1; color: Theme.mix(Theme.bg, Theme.accentSecondary, 0.08) } } - } - - Column { - width: parent.width * 0.47 - anchors.verticalCenter: parent.verticalCenter - spacing: 13 Text { - text: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight}` - color: Theme.fg + anchors.centerIn: parent + text: SystemSettings.monitorName || "DISPLAY" + color: Theme.fgMuted font.family: Theme.fontFamily - font.features: Theme.tabularFigures - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.DemiBold - } - Text { - text: `${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale` - color: Theme.fgDim - font.family: Theme.fontFamily - font.features: Theme.tabularFigures - font.pixelSize: Theme.fontSize - } - Text { - text: `${SystemSettings.monitorFormat || "Detecting format"} · ${SystemSettings.colorPreset || "standard color"}` - color: Theme.fgDim - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - } - Text { - text: SystemSettings.autoHdr ? "Game-aware HDR is ready" : "Game-aware HDR is off" - color: SystemSettings.autoHdr ? Theme.ok : Theme.warn - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize + font.pixelSize: Theme.fontSizeSmall + font.letterSpacing: 1.5 } } } } - Row { - width: parent.width - spacing: 16 + Column { + width: monitorLayout.columns === 2 + ? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47 + : monitorLayout.width + height: monitorLayout.columns === 2 ? 164 : implicitHeight + spacing: 13 - SettingsCard { - width: (parent.width - parent.spacing) / 2 - title: "Quiet focus" - subtitle: "Notifications and focused work" - - SettingRow { - label: "Do Not Disturb" - detail: Notifs.doNotDisturb ? "Banners are currently quiet" : "Notification banners are visible" - divider: false - controlWidth: 48 - SettingsToggle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - checked: Notifs.doNotDisturb - onToggled: value => Notifs.doNotDisturb = value - } - } + Text { + text: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight}` + color: Theme.fg + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold } - - SettingsCard { - width: (parent.width - parent.spacing) / 2 - title: "Desktop services" - subtitle: "The essentials are running" - - SettingRow { - label: "Sync & remote access" - detail: `${SystemSettings.nextcloudActive ? "Nextcloud ready" : "Nextcloud stopped"} · ${SystemSettings.rustdeskActive ? "RustDesk ready" : "RustDesk stopped"}` - divider: false - value: SystemSettings.nextcloudActive && SystemSettings.rustdeskActive ? "Healthy" : "Review" - } + Text { + text: `${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale` + color: Theme.fgDim + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSize + } + Text { + text: `${SystemSettings.monitorFormat || "Detecting format"} · ${SystemSettings.colorPreset || "standard color"}` + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + Text { + text: SystemSettings.autoHdr ? "Game-aware HDR is ready" : "Game-aware HDR is off" + color: SystemSettings.autoHdr ? Theme.ok : Theme.warn + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize } } } } + + SettingsCard { + title: "Weather" + subtitle: "Local conditions in the date menu" + ChoiceRow { setting: "temperatureUnit" } + SliderRow { setting: "weatherRefreshMinutes"; divider: false } + } + + SettingsCard { + title: "System vitals" + subtitle: "Processor, memory, and graphics activity in the bar" + SliderRow { setting: "vitalsIntervalMs"; divider: false } + } + + Grid { + id: summaryCards + + width: parent.width + columns: width >= 720 ? 2 : 1 + columnSpacing: 16 + rowSpacing: 16 + + SettingsCard { + width: summaryCards.columns === 2 + ? (summaryCards.width - summaryCards.columnSpacing) / 2 + : summaryCards.width + title: "Quiet focus" + subtitle: "Notifications and focused work" + + SettingRow { + label: "Do Not Disturb" + detail: Notifs.doNotDisturb ? "Banners are currently quiet" : "Notification banners are visible" + divider: false + controlWidth: 48 + SettingsToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: Notifs.doNotDisturb + onToggled: value => Notifs.doNotDisturb = value + } + } + } + + SettingsCard { + width: summaryCards.columns === 2 + ? (summaryCards.width - summaryCards.columnSpacing) / 2 + : summaryCards.width + title: "Desktop services" + subtitle: "The essentials are running" + + TextRow { + label: "Sync & remote access" + detail: `${SystemSettings.nextcloudActive ? "Nextcloud ready" : "Nextcloud stopped"} · ${SystemSettings.rustdeskActive ? "RustDesk ready" : "RustDesk stopped"}` + divider: false + value: SystemSettings.nextcloudActive && SystemSettings.rustdeskActive ? "Healthy" : "Review" + } + } + } } diff --git a/config/dot/quickshell/modules/settings/NotificationsPage.qml b/config/dot/quickshell/modules/settings/NotificationsPage.qml index 82fc817..43163d4 100644 --- a/config/dot/quickshell/modules/settings/NotificationsPage.qml +++ b/config/dot/quickshell/modules/settings/NotificationsPage.qml @@ -2,78 +2,97 @@ import QtQuick import qs.config import qs.services -Item { - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds +SettingsPage { + title: "Notifications & Focus" + lede: "Control interruptions without losing useful history." - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingsCard { + title: "Notifications" - Text { text: "Notifications & Focus"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - Text { text: "Control interruptions without losing useful history."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } + SettingRow { + label: "Do Not Disturb" + detail: "Keep notifications in the center but suppress banners" + controlWidth: 48 - SettingsCard { - title: "Notifications" - SettingRow { - label: "Do Not Disturb" - detail: "Keep notifications in the center but suppress banners" - controlWidth: 48 - SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: Notifs.doNotDisturb; onToggled: value => Notifs.doNotDisturb = value } - } - SettingRow { label: "Notification history"; detail: "Live notifications retained by Panama"; value: `${Notifs.history.length} items` } - SettingRow { - label: "Clear notification history" - detail: "Dismiss every item currently in the notification center" - divider: false - controlWidth: 94 - SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Clear all"; enabled: Notifs.history.length > 0; onClicked: Notifs.dismissAll() } - } + SettingsToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: Notifs.doNotDisturb + onToggled: value => Notifs.doNotDisturb = value } + } + + TextRow { + label: "Notification history" + detail: "Live notifications retained by Panama" + value: `${Notifs.history.length} items` + } + + ActionRow { + label: "Clear notification history" + detail: "Dismiss every item currently in the notification center" + divider: false + action: "Clear all" + enabled: Notifs.history.length > 0 + onTriggered: Notifs.dismissAll() + } + } + + SettingsCard { + title: "Banner behavior" + + SliderRow { setting: "notificationTimeoutMs" } + SliderRow { + setting: "notificationTimeoutCriticalMs" + zeroLabel: "Never" + } + SliderRow { setting: "notificationHistoryLimit" } + SliderRow { setting: "maxVisibleToasts"; divider: false } + } + + SettingsCard { + title: "Focus sessions" + subtitle: "A focus session binds quiet mode and Caffeine to the current workspace." + + SettingRow { + id: durationRow + + label: "Default duration" + detail: "Used by Super+Shift+F and Quick Settings" + controlWidth: 264 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 6 + + Repeater { + model: [25, 45, 60, 90] - SettingsCard { - title: "Focus sessions" - subtitle: "A focus session binds quiet mode and Caffeine to the current workspace." - SettingRow { - label: "Default duration" - detail: "Used by Super+Shift+F and Quick Settings" - controlWidth: 264 - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 6 - Repeater { - model: [25, 45, 60, 90] - SettingsButton { - required property int modelData - text: `${modelData}m` - tone: DesktopPreferences.get("focusDurationMinutes") === modelData ? "accent" : "normal" - onClicked: DesktopPreferences.set("focusDurationMinutes", modelData) - } - } - } - } - SettingRow { - label: FocusSession.active ? `Active on ${FocusSession.workspaceLabel}` : "No active focus session" - detail: FocusSession.active ? `${FocusSession.remainingText} remaining` : "Start one without leaving Settings" - divider: false - controlWidth: 120 SettingsButton { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: FocusSession.active ? "Show controls" : "Start focus" - tone: FocusSession.active ? "normal" : "accent" - onClicked: FocusSession.reveal() + required property int modelData + + text: `${modelData}m` + tone: DesktopPreferences.get("focusDurationMinutes") === modelData ? "accent" : "normal" + onClicked: SystemSettings.commitPreference("focusDurationMinutes", modelData) } } } } + + SettingRow { + label: FocusSession.active ? `Active on ${FocusSession.workspaceLabel}` : "No active focus session" + detail: FocusSession.active ? `${FocusSession.remainingText} remaining` : "Start one without leaving Settings" + divider: false + controlWidth: 120 + + SettingsButton { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: FocusSession.active ? "Show controls" : "Start focus" + tone: FocusSession.active ? "normal" : "accent" + onClicked: FocusSession.reveal() + } + } } } diff --git a/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml b/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml index dd7a84d..82a7785 100644 --- a/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml +++ b/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml @@ -2,9 +2,12 @@ import QtQuick import qs.config import qs.services -Item { +SettingsPage { id: root + title: "Screen Intelligence" + lede: "Turn text and codes on screen into content you can use." + Component.onCompleted: ScreenIntelligence.refresh() Timer { @@ -13,98 +16,78 @@ Item { onTriggered: Capture.openIntelligence() } - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds + SettingsCard { + title: "Read anything on screen" + subtitle: "Select a region, window, or display. Panama recognizes it locally and gives you clean follow-up actions." - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingRow { + icon: "󰗊" + label: "Read screen text" + detail: "Copy, search, translate, or open detected links" + controlWidth: 160 + divider: false - Text { - text: "Screen Intelligence" - color: Theme.fg - font.family: Theme.fontFamily - font.pixelSize: 27 - font.weight: Font.DemiBold - } - Text { - text: "Turn text and codes on screen into content you can use." - color: Theme.fgDim - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - bottomPadding: 6 - } - - SettingsCard { - title: "Read anything on screen" - subtitle: "Select a region, window, or display. Panama recognizes it locally and gives you clean follow-up actions." - - SettingRow { - icon: "󰗊" - label: "Read screen text" - detail: "Copy, search, translate, or open detected links" - controlWidth: 160 - divider: false - SettingsButton { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: "Start reading" - tone: "accent" - enabled: ScreenIntelligence.ocrReady && ScreenIntelligence.englishReady - onClicked: { - ShellState.closeSettings(); - launchDelay.restart(); - } - } - } - } - - SettingsCard { - title: "Local recognition" - subtitle: "The screen image stays in Panama's cache and is deleted when you dismiss the result." - - SettingRow { - label: "Text recognition" - detail: "Tesseract with the English language model" - value: ScreenIntelligence.ocrReady && ScreenIntelligence.englishReady ? "Ready" : "Needs install" - } - SettingRow { - label: "QR & barcodes" - detail: "ZBar recognizes codes alongside ordinary text" - value: ScreenIntelligence.codeReady ? "Ready" : "Needs install" - } - SettingRow { - label: "Privacy" - detail: "Only Search, Translate, and Open send the selected result to another application or service" - value: "Local first" - divider: false - } - } - - SettingsCard { - title: "Shortcut" - SettingRow { - label: "Read a screen selection" - detail: "Also available as Read in the Print-screen picker" - value: "Super + Shift + S" - controlWidth: 190 - divider: ScreenIntelligence.ocrReady && ScreenIntelligence.codeReady && ScreenIntelligence.englishReady - } - SettingRow { - visible: !ScreenIntelligence.ocrReady || !ScreenIntelligence.codeReady || !ScreenIntelligence.englishReady - label: "Install recognition engines" - detail: "sudo dnf install -y tesseract zbar" - value: "Required once" - divider: false + SettingsButton { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: "Start reading" + tone: "accent" + enabled: ScreenIntelligence.ocrReady && ScreenIntelligence.englishReady + onClicked: { + ShellState.closeSettings(); + launchDelay.restart(); } } } } + + SettingsCard { + title: "Capture preferences" + subtitle: "Choose where captures go and how recordings are encoded." + + ChoiceRow { setting: "screenshotDir" } + ChoiceRow { setting: "recordingDir" } + ChoiceRow { setting: "recorderArgs"; divider: false } + } + + SettingsCard { + title: "Local recognition" + subtitle: "The screen image stays in Panama's cache and is deleted when you dismiss the result." + + TextRow { + label: "Text recognition" + detail: "Tesseract with the English language model" + value: ScreenIntelligence.ocrReady && ScreenIntelligence.englishReady ? "Ready" : "Needs install" + } + TextRow { + label: "QR & barcodes" + detail: "ZBar recognizes codes alongside ordinary text" + value: ScreenIntelligence.codeReady ? "Ready" : "Needs install" + } + TextRow { + label: "Privacy" + detail: "Only Search, Translate, and Open send the selected result to another application or service" + value: "Local first" + divider: false + } + } + + SettingsCard { + title: "Shortcut" + + TextRow { + label: "Read a screen selection" + detail: "Also available as Read in the Print-screen picker" + value: "Super + Shift + S" + controlWidth: 190 + divider: ScreenIntelligence.ocrReady && ScreenIntelligence.codeReady && ScreenIntelligence.englishReady + } + TextRow { + visible: !ScreenIntelligence.ocrReady || !ScreenIntelligence.codeReady || !ScreenIntelligence.englishReady + label: "Install recognition engines" + detail: "sudo dnf install -y tesseract zbar" + value: "Required once" + divider: false + } + } } diff --git a/config/dot/quickshell/modules/settings/ServicesPage.qml b/config/dot/quickshell/modules/settings/ServicesPage.qml index cb5bd60..c6e9bd4 100644 --- a/config/dot/quickshell/modules/settings/ServicesPage.qml +++ b/config/dot/quickshell/modules/settings/ServicesPage.qml @@ -2,89 +2,132 @@ import QtQuick import qs.config import qs.services -Item { - function status(active: bool): string { return active ? "Running" : "Stopped"; } +SettingsPage { + title: "Startup & Services" + lede: "A clear view of the background tools that make the desktop feel complete." - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds + function status(active: bool): string { + return active ? "Running" : "Stopped"; + } - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + Item { + width: parent.width + implicitHeight: refresh.implicitHeight + + SettingsButton { + id: refresh + anchors.right: parent.right + text: SystemSettings.busy ? "Refreshing…" : "Refresh" + enabled: !SystemSettings.busy + onClicked: SystemSettings.refresh() + } + } + + SettingsCard { + title: "Your services" + + SettingRow { + label: "Nextcloud" + detail: "File synchronization and tray status" + controlWidth: 190 Row { - width: parent.width - Text { width: parent.width - refresh.width; text: "Startup & Services"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - SettingsButton { id: refresh; text: SystemSettings.busy ? "Refreshing…" : "Refresh"; enabled: !SystemSettings.busy; onClicked: SystemSettings.refresh() } - } - Text { text: "A clear view of the background tools that make the desktop feel complete."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } - - SettingsCard { - title: "Your services" - SettingRow { - label: "Nextcloud" - detail: "File synchronization and tray status" - controlWidth: 190 - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 10 - Text { anchors.verticalCenter: parent.verticalCenter; text: status(SystemSettings.nextcloudActive); color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize } - SettingsButton { text: "Open"; onClicked: SystemSettings.openApplication("nextcloud") } - } + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 10 + Text { + anchors.verticalCenter: parent.verticalCenter + text: status(SystemSettings.nextcloudActive) + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize } - SettingRow { - label: "RustDesk" - detail: "Remote access through the enabled system service" - controlWidth: 190 - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 10 - Text { anchors.verticalCenter: parent.verticalCenter; text: status(SystemSettings.rustdeskActive); color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize } - SettingsButton { text: "Open"; onClicked: SystemSettings.openApplication("rustdesk") } - } - } - SettingRow { - label: "KDE Connect" - detail: "Phone pairing, clipboard, files, and remote controls" - divider: false - controlWidth: 190 - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 10 - Text { anchors.verticalCenter: parent.verticalCenter; text: status(SystemSettings.kdeconnectActive); color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize } - SettingsButton { text: "Open"; onClicked: SystemSettings.openApplication("kdeconnect") } - } + SettingsButton { + text: "Open" + onClicked: SystemSettings.openApplication("nextcloud") } } + } - SettingsCard { - title: "Desktop foundation" - SettingRow { label: "Hyprpaper"; detail: "Wallpaper service"; value: status(SystemSettings.hyprpaperActive) } - SettingRow { label: "Hypridle"; detail: "Idle and lock policy"; value: status(SystemSettings.hypridleActive) } - SettingRow { label: "Vicinae"; detail: "Spotlight-style launcher daemon"; value: status(SystemSettings.vicinaeActive); divider: false } + SettingRow { + label: "RustDesk" + detail: "Remote access through the enabled system service" + controlWidth: 190 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 10 + Text { + anchors.verticalCenter: parent.verticalCenter + text: status(SystemSettings.rustdeskActive) + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + SettingsButton { + text: "Open" + onClicked: SystemSettings.openApplication("rustdesk") + } } + } - SettingsCard { - title: "Fedora system settings" - subtitle: "These remain owned by trusted system services and GNOME's mature panels." - SettingRow { - label: "Network, Bluetooth, printers, users, and accounts" - detail: "GNOME Settings remains searchable from the launcher too" - divider: false - controlWidth: 122 - SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open network"; onClicked: SystemSettings.openGnomePanel("network") } + SettingRow { + label: "KDE Connect" + detail: "Phone pairing, clipboard, files, and remote controls" + divider: false + controlWidth: 190 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 10 + Text { + anchors.verticalCenter: parent.verticalCenter + text: status(SystemSettings.kdeconnectActive) + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + SettingsButton { + text: "Open" + onClicked: SystemSettings.openApplication("kdeconnect") } } } } + + SettingsCard { + title: "Desktop foundation" + + TextRow { + label: "Hyprpaper" + detail: "Wallpaper service" + value: status(SystemSettings.hyprpaperActive) + } + TextRow { + label: "Hypridle" + detail: "Idle and lock policy" + value: status(SystemSettings.hypridleActive) + } + TextRow { + label: "Vicinae" + detail: "Spotlight-style launcher daemon" + value: status(SystemSettings.vicinaeActive) + divider: false + } + } + + SettingsCard { + title: "Fedora system settings" + subtitle: "These remain owned by trusted system services and GNOME's mature panels." + + ActionRow { + label: "Network, Bluetooth, printers, users, and accounts" + detail: "GNOME Settings remains searchable from the launcher too" + divider: false + action: "Open network" + onTriggered: SystemSettings.openGnomePanel("network") + } + } } diff --git a/config/dot/quickshell/modules/settings/SoundPage.qml b/config/dot/quickshell/modules/settings/SoundPage.qml index b8c956e..a850abe 100644 --- a/config/dot/quickshell/modules/settings/SoundPage.qml +++ b/config/dot/quickshell/modules/settings/SoundPage.qml @@ -4,50 +4,61 @@ import qs.config import qs.services import qs.modules.quicksettings -Item { - Flickable { - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: content.implicitHeight + 64 - boundsBehavior: Flickable.StopAtBounds +SettingsPage { + title: "Sound" + lede: "Live PipeWire output, input, and device selection." - Column { - id: content - width: parent.width - 68 - x: 34 - y: 30 - spacing: 16 + SettingsCard { + title: "Output" + subtitle: Pipewire.defaultAudioSink?.description ?? "No output device" - Text { text: "Sound"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } - Text { text: "Live PipeWire output, input, and device selection."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } + AudioSlider { + width: parent.width + node: Pipewire.defaultAudioSink + output: true + } + Rectangle { + width: parent.width + height: 1 + color: Theme.alpha(Theme.fg, 0.06) + } + AudioDeviceList { + width: parent.width + output: true + maxHeight: 190 + } + } - SettingsCard { - title: "Output" - subtitle: Pipewire.defaultAudioSink?.description ?? "No output device" - AudioSlider { width: parent.width; node: Pipewire.defaultAudioSink; output: true } - Rectangle { width: parent.width; height: 1; color: Theme.alpha(Theme.fg, 0.06) } - AudioDeviceList { width: parent.width; output: true; maxHeight: 190 } - } + SettingsCard { + title: "Input" + subtitle: Pipewire.defaultAudioSource?.description ?? "No input device" - SettingsCard { - title: "Input" - subtitle: Pipewire.defaultAudioSource?.description ?? "No input device" - AudioSlider { width: parent.width; node: Pipewire.defaultAudioSource; output: false } - Rectangle { width: parent.width; height: 1; color: Theme.alpha(Theme.fg, 0.06) } - AudioDeviceList { width: parent.width; output: false; maxHeight: 160 } - } + AudioSlider { + width: parent.width + node: Pipewire.defaultAudioSource + output: false + } + Rectangle { + width: parent.width + height: 1 + color: Theme.alpha(Theme.fg, 0.06) + } + AudioDeviceList { + width: parent.width + output: false + maxHeight: 160 + } + } - SettingsCard { - title: "Advanced sound" - SettingRow { - label: "Application volumes and profiles" - detail: "Open Fedora's complete sound panel" - divider: false - controlWidth: 104 - SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open panel"; onClicked: SystemSettings.openGnomePanel("sound") } - } - } + SettingsCard { + title: "Advanced sound" + + ActionRow { + label: "Application volumes and profiles" + detail: "Open Fedora's complete sound panel" + divider: false + action: "Open panel" + onTriggered: SystemSettings.openGnomePanel("sound") } } } diff --git a/tests/quickshell/settings-hardcoded-values-contract.sh b/tests/quickshell/settings-hardcoded-values-contract.sh new file mode 100755 index 0000000..5fe0d0f --- /dev/null +++ b/tests/quickshell/settings-hardcoded-values-contract.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Public Settings.qml values are the compatibility surface consumed throughout +# the shell. Once a value becomes user-configurable, this file must read it from +# DesktopPreferences rather than keeping a second hardcoded source of truth. + +set -euo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +settings="$repo_dir/config/dot/quickshell/config/Settings.qml" + +fail() { + printf 'settings hardcoded values contract: %s\n' "$1" >&2 + exit 1 +} + +properties=( + temperatureUnit + weatherRefreshMinutes + vitalsIntervalMs + notificationTimeoutMs + notificationTimeoutCriticalMs + notificationHistoryLimit + maxVisibleToasts + screenshotDir + recordingDir + recorderArgs +) + +for property in "${properties[@]}"; do + count="$(rg -c \ + "^[[:space:]]*readonly property [A-Za-z]+ ${property}: DesktopPreferences\\.get\\(\"${property}\"\\)[[:space:]]*(//.*)?$" \ + "$settings" || true)" + [[ "$count" == "1" ]] \ + || fail "$property must use DesktopPreferences.get(\"$property\") exactly once" +done + +# dockPinned was already migrated on the shared branch. Pinning it here keeps a +# later bulk edit from accidentally restoring the old hardcoded app list. +rg -q '^[[:space:]]*readonly property var dockPinned: DesktopPreferences\.get\("dockPinned"\)[[:space:]]*$' "$settings" \ + || fail 'dockPinned no longer reads DesktopPreferences exactly' + +printf 'settings hardcoded values contract: PASS\n' diff --git a/tests/quickshell/settings-pages-contract.sh b/tests/quickshell/settings-pages-contract.sh index 8e217cf..bdb3fcb 100755 --- a/tests/quickshell/settings-pages-contract.sh +++ b/tests/quickshell/settings-pages-contract.sh @@ -3,6 +3,85 @@ set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +fail() { + printf 'settings pages contract: %s\n' "$1" >&2 + exit 1 +} + +pages=(Home Displays Connectivity Sound Notifications ScreenIntelligence Services About) +for page in "${pages[@]}"; do + page_file="$repo_dir/config/dot/quickshell/modules/settings/${page}Page.qml" + [[ -f "$page_file" ]] || fail "${page}Page.qml is missing" + + root_type="$(awk ' + /^import / { next } + /^[[:space:]]*\/\// { next } + /^[[:space:]]*$/ { next } + match($0, /^[[:space:]]*([A-Za-z][A-Za-z0-9]*)[[:space:]]*\{/, found) { + print found[1] + exit + } + ' "$page_file")" + [[ "$root_type" == "SettingsPage" ]] \ + || fail "${page}Page.qml root is ${root_type:-unknown}, expected SettingsPage" + ! rg -q '^[[:space:]]*Flickable[[:space:]]*\{' "$page_file" \ + || fail "${page}Page.qml still copies the page Flickable scaffold" +done + +require_row() { + local file="$1" + local row_type="$2" + local setting="$3" + + python3 - "$file" "$row_type" "$setting" <<'PY' || \ + fail "$(basename "$file") is missing $row_type for $setting" +import re +import sys + +text = open(sys.argv[1], encoding="utf-8").read() +row_type = re.escape(sys.argv[2]) +setting = re.escape(sys.argv[3]) +pattern = rf"{row_type}\s*\{{(?:(?!\n\s*[A-Z][A-Za-z0-9]*\s*\{{).)*?setting\s*:\s*\"{setting}\"" +raise SystemExit(0 if re.search(pattern, text, re.S) else 1) +PY +} + +home_page="$repo_dir/config/dot/quickshell/modules/settings/HomePage.qml" +require_row "$home_page" ChoiceRow temperatureUnit +require_row "$home_page" SliderRow weatherRefreshMinutes +require_row "$home_page" SliderRow vitalsIntervalMs + +notifications_page="$repo_dir/config/dot/quickshell/modules/settings/NotificationsPage.qml" +for setting in notificationTimeoutMs notificationTimeoutCriticalMs notificationHistoryLimit maxVisibleToasts; do + require_row "$notifications_page" SliderRow "$setting" +done +python3 - "$notifications_page" <<'PY' || fail 'critical notification timeout does not render zero as Never' +import re +import sys + +text = open(sys.argv[1], encoding="utf-8").read() +block = re.search( + r'SliderRow\s*\{(?:(?!\n\s*[A-Z][A-Za-z0-9]*\s*\{).)*?' + r'setting\s*:\s*"notificationTimeoutCriticalMs"(?P.*?)\n\s*\}', + text, + re.S, +) +raise SystemExit(0 if block and re.search(r'zeroLabel\s*:\s*"Never"', block.group(0)) else 1) +PY + +intelligence_page="$repo_dir/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml" +require_row "$intelligence_page" ChoiceRow screenshotDir +require_row "$intelligence_page" ChoiceRow recordingDir +require_row "$intelligence_page" ChoiceRow recorderArgs + +# The source-only contract is safe during a shared Quickshell quiet window. +# The existing compositor integration checks remain available explicitly. +if [[ "${PANAMA_SETTINGS_STATIC_ONLY:-0}" == "1" ]]; then + printf 'settings pages contract: PASS (static)\n' + exit 0 +fi + state_home="$(mktemp -d /tmp/panama-settings-pages-state.XXXXXX)" source_config_path="$repo_dir/config/dot/quickshell" config_path="$state_home/quickshell" @@ -58,11 +137,6 @@ exit 97 EOF chmod +x "$test_bin/flatpak" -fail() { - printf 'settings pages contract: %s\n' "$1" >&2 - exit 1 -} - qs_for_test() { PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \ qs -p "$config_path" "$@"