Complete Panama settings controls

This commit is contained in:
Gabriel Brown
2026-08-18 01:36:07 -04:00
parent 768801dbe4
commit 1ca571458c
11 changed files with 725 additions and 555 deletions
+10 -10
View File
@@ -28,13 +28,13 @@ Singleton {
// label deliberately general rather than exposing precise coordinates in // label deliberately general rather than exposing precise coordinates in
// the UI or guessing at a city from them. // the UI or guessing at a city from them.
readonly property string weatherLocation: "Local weather" readonly property string weatherLocation: "Local weather"
readonly property string temperatureUnit: "fahrenheit" readonly property string temperatureUnit: DesktopPreferences.get("temperatureUnit")
readonly property int weatherRefreshMinutes: 20 readonly property int weatherRefreshMinutes: DesktopPreferences.get("weatherRefreshMinutes")
// ── Vitals ────────────────────────────────────────────────────────────── // ── Vitals ──────────────────────────────────────────────────────────────
// The GNOME Vitals extension showed processor usage, memory usage and GPU // The GNOME Vitals extension showed processor usage, memory usage and GPU
// usage, in that order. Same here. // 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 showCpu: DesktopPreferences.get("showCpu")
readonly property bool showMemory: DesktopPreferences.get("showMemory") readonly property bool showMemory: DesktopPreferences.get("showMemory")
readonly property bool showGpu: DesktopPreferences.get("showGpu") readonly property bool showGpu: DesktopPreferences.get("showGpu")
@@ -51,10 +51,10 @@ Singleton {
readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled") readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled")
// ── Notifications ─────────────────────────────────────────────────────── // ── Notifications ───────────────────────────────────────────────────────
readonly property int notificationTimeoutMs: 5000 readonly property int notificationTimeoutMs: DesktopPreferences.get("notificationTimeoutMs")
readonly property int notificationTimeoutCriticalMs: 0 // 0 = never auto-expire readonly property int notificationTimeoutCriticalMs: DesktopPreferences.get("notificationTimeoutCriticalMs") // 0 = never auto-expire
readonly property int notificationHistoryLimit: 100 readonly property int notificationHistoryLimit: DesktopPreferences.get("notificationHistoryLimit")
readonly property int maxVisibleToasts: 4 readonly property int maxVisibleToasts: DesktopPreferences.get("maxVisibleToasts")
// ── Focus ────────────────────────────────────────────────────────────── // ── Focus ──────────────────────────────────────────────────────────────
// One deliberate default rather than a preset picker: quick settings and // One deliberate default rather than a preset picker: quick settings and
@@ -79,9 +79,9 @@ Singleton {
readonly property int dockHideDelayMs: DesktopPreferences.get("dockHideDelayMs") readonly property int dockHideDelayMs: DesktopPreferences.get("dockHideDelayMs")
// ── Capture ───────────────────────────────────────────────────────────── // ── Capture ─────────────────────────────────────────────────────────────
readonly property string screenshotDir: "Pictures/Screenshots" readonly property string screenshotDir: DesktopPreferences.get("screenshotDir")
readonly property string recordingDir: "Videos/Recordings" readonly property string recordingDir: DesktopPreferences.get("recordingDir")
// Passed to wf-recorder. Uses VAAPI on the AMD card so recording does not // Passed to wf-recorder. Uses VAAPI on the AMD card so recording does not
// cost CPU while gaming. // cost CPU while gaming.
readonly property string recorderArgs: "-c h264_vaapi -d /dev/dri/renderD128" readonly property string recorderArgs: DesktopPreferences.get("recorderArgs")
} }
@@ -3,39 +3,49 @@ import Quickshell
import qs.config import qs.config
import qs.services import qs.services
Item { SettingsPage {
Flickable { title: "About Panama"
anchors.fill: parent lede: "A curated Hyprland desktop built around focus, speed, and good taste."
clip: true
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingsCard {
id: content title: "Panama Desktop"
width: parent.width - 68 subtitle: "Tokyo Night Moon · Prism glass · native tiling"
x: 34
y: 30
spacing: 16
Text { text: "About Panama"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } TextRow {
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 } 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 { SettingsCard {
title: "Panama Desktop" title: "Design principles"
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 { TextRow {
title: "Design principles" label: "Curated by default"
SettingRow { label: "Curated by default"; detail: "Strong choices instead of an incoherent matrix of switches" } 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: "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
} }
} }
} }
@@ -5,9 +5,12 @@ import qs.config
import qs.services import qs.services
import qs.modules.quicksettings import qs.modules.quicksettings
Item { SettingsPage {
id: root id: root
title: "Network & Devices"
lede: "Connect graphically—no terminal workflow required."
readonly property var wifiDevice: { readonly property var wifiDevice: {
for (const device of Networking.devices.values) { for (const device of Networking.devices.values) {
if (device.type === DeviceType.Wifi) if (device.type === DeviceType.Wifi)
@@ -17,72 +20,73 @@ Item {
} }
readonly property var bluetoothAdapter: Bluetooth.defaultAdapter readonly property var bluetoothAdapter: Bluetooth.defaultAdapter
Flickable { SettingsCard {
anchors.fill: parent title: "WiFi"
clip: true subtitle: Networking.wifiEnabled ? "Available networks" : "Wireless networking is off"
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingRow {
id: content label: "WiFi"
width: parent.width - 68 detail: root.wifiDevice ? "Managed by NetworkManager" : "No wireless adapter found"
x: 34 controlWidth: 48
y: 30
spacing: 16
Text { text: "Network & Devices"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } SettingsToggle {
Text { text: "Connect graphically—no terminal workflow required."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
SettingsCard { checked: Networking.wifiEnabled
title: "WiFi" enabled: Networking.wifiHardwareEnabled
subtitle: Networking.wifiEnabled ? "Available networks" : "Wireless networking is off" onToggled: value => Networking.wifiEnabled = value
SettingRow {
label: "WiFi"
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") }
}
} }
}
SettingsCard { WifiList {
title: "Bluetooth" width: parent.width
subtitle: root.bluetoothAdapter?.enabled ? "Nearby and paired devices" : "Bluetooth is off" device: root.wifiDevice
SettingRow { active: true
label: "Bluetooth" maxHeight: 240
detail: root.bluetoothAdapter ? "Pair and reconnect without leaving Settings" : "No Bluetooth adapter found" }
controlWidth: 48
SettingsToggle { ActionRow {
anchors.right: parent.right label: "Advanced network settings"
anchors.verticalCenter: parent.verticalCenter detail: "VPN, wired profiles, DNS, and connection details"
checked: root.bluetoothAdapter?.enabled ?? false divider: false
enabled: root.bluetoothAdapter !== null action: "Open panel"
onToggled: value => { if (root.bluetoothAdapter) root.bluetoothAdapter.enabled = value; } onTriggered: SystemSettings.openGnomePanel("network")
} }
} }
BluetoothList { width: parent.width; active: true; maxHeight: 220 }
SettingRow { SettingsCard {
label: "Advanced Bluetooth settings" title: "Bluetooth"
detail: "Device details and system-level options" subtitle: root.bluetoothAdapter?.enabled ? "Nearby and paired devices" : "Bluetooth is off"
divider: false
controlWidth: 104 SettingRow {
SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open panel"; onClicked: SystemSettings.openGnomePanel("bluetooth") } 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")
}
} }
} }
@@ -3,64 +3,50 @@ import qs.config
import qs.services import qs.services
import qs.widgets import qs.widgets
Item { SettingsPage {
Flickable { title: "Displays"
anchors.fill: parent lede: SystemSettings.monitorDescription || "Reading the active display…"
clip: true
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingsCard {
id: content title: SystemSettings.monitorName || "Active display"
width: parent.width - 68 subtitle: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight} at ${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale · ${SystemSettings.monitorFormat}`
x: 34 TextRow { label: "Color mode"; detail: "Wide-gamut SDR desktop at 10-bit"; value: SystemSettings.colorPreset || "wide" }
y: 30 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 }
spacing: 16 }
Text { text: "Displays"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } SettingsCard {
Text { text: SystemSettings.monitorDescription || "Reading the active display…"; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } title: "Gaming display policy"
subtitle: "These values apply immediately and are restored when Panama starts."
SettingsCard { SettingRow {
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 {
label: "Game-aware HDR" label: "Game-aware HDR"
detail: "Enter HDR only for fullscreen content that requests it" detail: "Enter HDR only for fullscreen content that requests it"
controlWidth: 48 controlWidth: 48
SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.autoHdr; onToggled: value => SystemSettings.setAutoHdr(value) } SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.autoHdr; onToggled: value => SystemSettings.setAutoHdr(value) }
} }
SettingRow { SettingRow {
label: "Content-aware VRR" label: "Content-aware VRR"
detail: "Enable variable refresh only for game and video content" detail: "Enable variable refresh only for game and video content"
controlWidth: 48 controlWidth: 48
SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.vrrPolicy === 3; onToggled: value => SystemSettings.setVrrPolicy(value ? 3 : 0) } 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" label: "Direct scanout for games"
detail: "Bypass compositing only for windows classified as games" detail: "Bypass compositing only for windows classified as games"
divider: false divider: false
controlWidth: 48 controlWidth: 48
SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.directScanoutPolicy === 2; onToggled: value => SystemSettings.setDirectScanoutPolicy(value ? 2 : 0) } SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: SystemSettings.directScanoutPolicy === 2; onToggled: value => SystemSettings.setDirectScanoutPolicy(value ? 2 : 0) }
} }
} }
SettingsCard { SettingsCard {
title: "Night Light" title: "Night Light"
SettingRow { SettingRow {
label: "Warm display colors" label: "Warm display colors"
detail: NightLight.automatic ? "Following the evening schedule" : "Manual control" detail: NightLight.automatic ? "Following the evening schedule" : "Manual control"
controlWidth: 48 controlWidth: 48
SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: NightLight.active; onToggled: NightLight.toggle() } SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; checked: NightLight.active; onToggled: NightLight.toggle() }
} }
SettingRow { SettingRow {
label: "Color temperature" label: "Color temperature"
detail: `${NightLight.temperature} K` detail: `${NightLight.temperature} K`
divider: false divider: false
@@ -71,27 +57,25 @@ Item {
icon: "weather-clear-night-symbolic" icon: "weather-clear-night-symbolic"
onMoved: value => NightLight.temperature = Math.round((6500 - value * 4000) / 50) * 50 onMoved: value => NightLight.temperature = Math.round((6500 - value * 4000) / 50) * 50
} }
} }
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: warningText.implicitHeight + 30 height: warningText.implicitHeight + 30
radius: Theme.cardRadius radius: Theme.cardRadius
color: Theme.alpha(Theme.warn, 0.085) color: Theme.alpha(Theme.warn, 0.085)
border.width: 1 border.width: 1
border.color: Theme.alpha(Theme.warn, 0.22) border.color: Theme.alpha(Theme.warn, 0.22)
Text { Text {
id: warningText id: warningText
anchors.fill: parent anchors.fill: parent
anchors.margins: 15 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." 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) color: Theme.mix(Theme.fg, Theme.warn, 0.25)
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
}
}
} }
} }
} }
@@ -2,160 +2,159 @@ import QtQuick
import qs.config import qs.config
import qs.services import qs.services
Item { SettingsPage {
id: root id: root
readonly property int openedHour: new Date().getHours() readonly property int openedHour: new Date().getHours()
readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening") readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening")
Flickable { title: `${root.greeting}, Gabriel`
anchors.fill: parent lede: "Your Panama desktop is configured and ready."
clip: true
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingsCard {
id: content title: SystemSettings.monitorDescription || "Active display"
width: parent.width - 68 subtitle: SystemSettings.monitorName || "Detecting your display…"
x: 34
y: 30
spacing: 16
Text { Grid {
text: `${root.greeting}, Gabriel` id: monitorLayout
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: 27
font.weight: Font.DemiBold
}
Text { width: parent.width
text: "Your Panama desktop is configured and ready." columns: width >= 620 ? 2 : 1
color: Theme.fgDim columnSpacing: 28
font.family: Theme.fontFamily rowSpacing: 16
font.pixelSize: Theme.fontSize
bottomPadding: 6
}
SettingsCard { Item {
title: SystemSettings.monitorDescription || "Active display" width: monitorLayout.columns === 2
subtitle: SystemSettings.monitorName || "Detecting your display…" ? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47
: monitorLayout.width
height: 164
Row { Rectangle {
width: parent.width width: Math.min(parent.width - 24, 260)
height: 164 height: width * 0.64
spacing: 28 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 { Rectangle {
width: parent.width * 0.47 anchors.fill: parent
height: parent.height anchors.margins: 10
radius: 7
Rectangle { gradient: Gradient {
width: Math.min(parent.width - 24, 260) orientation: Gradient.Horizontal
height: width * 0.64 GradientStop { position: 0; color: Theme.mix(Theme.bg, Theme.accent, 0.08) }
anchors.horizontalCenter: parent.horizontalCenter GradientStop { position: 1; color: Theme.mix(Theme.bg, Theme.accentSecondary, 0.08) }
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
}
}
} }
}
Column {
width: parent.width * 0.47
anchors.verticalCenter: parent.verticalCenter
spacing: 13
Text { Text {
text: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight}` anchors.centerIn: parent
color: Theme.fg text: SystemSettings.monitorName || "DISPLAY"
color: Theme.fgMuted
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.features: Theme.tabularFigures font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeLarge font.letterSpacing: 1.5
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
} }
} }
} }
} }
Row { Column {
width: parent.width width: monitorLayout.columns === 2
spacing: 16 ? (monitorLayout.width - monitorLayout.columnSpacing) * 0.47
: monitorLayout.width
height: monitorLayout.columns === 2 ? 164 : implicitHeight
spacing: 13
SettingsCard { Text {
width: (parent.width - parent.spacing) / 2 text: `${SystemSettings.monitorWidth} × ${SystemSettings.monitorHeight}`
title: "Quiet focus" color: Theme.fg
subtitle: "Notifications and focused work" font.family: Theme.fontFamily
font.features: Theme.tabularFigures
SettingRow { font.pixelSize: Theme.fontSizeLarge
label: "Do Not Disturb" font.weight: Font.DemiBold
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 {
SettingsCard { text: `${SystemSettings.monitorRefreshRate.toFixed(0)} Hz · ${SystemSettings.monitorScale.toFixed(1)}× scale`
width: (parent.width - parent.spacing) / 2 color: Theme.fgDim
title: "Desktop services" font.family: Theme.fontFamily
subtitle: "The essentials are running" font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSize
SettingRow { }
label: "Sync & remote access" Text {
detail: `${SystemSettings.nextcloudActive ? "Nextcloud ready" : "Nextcloud stopped"} · ${SystemSettings.rustdeskActive ? "RustDesk ready" : "RustDesk stopped"}` text: `${SystemSettings.monitorFormat || "Detecting format"} · ${SystemSettings.colorPreset || "standard color"}`
divider: false color: Theme.fgDim
value: SystemSettings.nextcloudActive && SystemSettings.rustdeskActive ? "Healthy" : "Review" 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"
}
}
}
} }
@@ -2,78 +2,97 @@ import QtQuick
import qs.config import qs.config
import qs.services import qs.services
Item { SettingsPage {
Flickable { title: "Notifications & Focus"
anchors.fill: parent lede: "Control interruptions without losing useful history."
clip: true
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingsCard {
id: content title: "Notifications"
width: parent.width - 68
x: 34
y: 30
spacing: 16
Text { text: "Notifications & Focus"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } SettingRow {
Text { text: "Control interruptions without losing useful history."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } label: "Do Not Disturb"
detail: "Keep notifications in the center but suppress banners"
controlWidth: 48
SettingsCard { SettingsToggle {
title: "Notifications" anchors.right: parent.right
SettingRow { anchors.verticalCenter: parent.verticalCenter
label: "Do Not Disturb" checked: Notifs.doNotDisturb
detail: "Keep notifications in the center but suppress banners" onToggled: value => Notifs.doNotDisturb = value
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() }
}
} }
}
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 { SettingsButton {
anchors.right: parent.right required property int modelData
anchors.verticalCenter: parent.verticalCenter
text: FocusSession.active ? "Show controls" : "Start focus" text: `${modelData}m`
tone: FocusSession.active ? "normal" : "accent" tone: DesktopPreferences.get("focusDurationMinutes") === modelData ? "accent" : "normal"
onClicked: FocusSession.reveal() 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()
}
}
} }
} }
@@ -2,9 +2,12 @@ import QtQuick
import qs.config import qs.config
import qs.services import qs.services
Item { SettingsPage {
id: root id: root
title: "Screen Intelligence"
lede: "Turn text and codes on screen into content you can use."
Component.onCompleted: ScreenIntelligence.refresh() Component.onCompleted: ScreenIntelligence.refresh()
Timer { Timer {
@@ -13,98 +16,78 @@ Item {
onTriggered: Capture.openIntelligence() onTriggered: Capture.openIntelligence()
} }
Flickable { SettingsCard {
anchors.fill: parent title: "Read anything on screen"
clip: true subtitle: "Select a region, window, or display. Panama recognizes it locally and gives you clean follow-up actions."
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingRow {
id: content icon: "󰗊"
width: parent.width - 68 label: "Read screen text"
x: 34 detail: "Copy, search, translate, or open detected links"
y: 30 controlWidth: 160
spacing: 16 divider: false
Text { SettingsButton {
text: "Screen Intelligence" anchors.right: parent.right
color: Theme.fg anchors.verticalCenter: parent.verticalCenter
font.family: Theme.fontFamily text: "Start reading"
font.pixelSize: 27 tone: "accent"
font.weight: Font.DemiBold enabled: ScreenIntelligence.ocrReady && ScreenIntelligence.englishReady
} onClicked: {
Text { ShellState.closeSettings();
text: "Turn text and codes on screen into content you can use." launchDelay.restart();
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
} }
} }
} }
} }
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
}
}
} }
@@ -2,89 +2,132 @@ import QtQuick
import qs.config import qs.config
import qs.services import qs.services
Item { SettingsPage {
function status(active: bool): string { return active ? "Running" : "Stopped"; } title: "Startup & Services"
lede: "A clear view of the background tools that make the desktop feel complete."
Flickable { function status(active: bool): string {
anchors.fill: parent return active ? "Running" : "Stopped";
clip: true }
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { Item {
id: content width: parent.width
width: parent.width - 68 implicitHeight: refresh.implicitHeight
x: 34
y: 30 SettingsButton {
spacing: 16 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 { Row {
width: parent.width anchors.right: parent.right
Text { width: parent.width - refresh.width; text: "Startup & Services"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } anchors.verticalCenter: parent.verticalCenter
SettingsButton { id: refresh; text: SystemSettings.busy ? "Refreshing…" : "Refresh"; enabled: !SystemSettings.busy; onClicked: SystemSettings.refresh() } spacing: 10
} Text {
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 } anchors.verticalCenter: parent.verticalCenter
text: status(SystemSettings.nextcloudActive)
SettingsCard { color: Theme.fgDim
title: "Your services" font.family: Theme.fontFamily
SettingRow { font.pixelSize: Theme.fontSize
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") }
}
} }
SettingRow { SettingsButton {
label: "RustDesk" text: "Open"
detail: "Remote access through the enabled system service" onClicked: SystemSettings.openApplication("nextcloud")
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") }
}
} }
} }
}
SettingsCard { SettingRow {
title: "Desktop foundation" label: "RustDesk"
SettingRow { label: "Hyprpaper"; detail: "Wallpaper service"; value: status(SystemSettings.hyprpaperActive) } detail: "Remote access through the enabled system service"
SettingRow { label: "Hypridle"; detail: "Idle and lock policy"; value: status(SystemSettings.hypridleActive) } controlWidth: 190
SettingRow { label: "Vicinae"; detail: "Spotlight-style launcher daemon"; value: status(SystemSettings.vicinaeActive); divider: false }
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 { SettingRow {
title: "Fedora system settings" label: "KDE Connect"
subtitle: "These remain owned by trusted system services and GNOME's mature panels." detail: "Phone pairing, clipboard, files, and remote controls"
SettingRow { divider: false
label: "Network, Bluetooth, printers, users, and accounts" controlWidth: 190
detail: "GNOME Settings remains searchable from the launcher too"
divider: false Row {
controlWidth: 122 anchors.right: parent.right
SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open network"; onClicked: SystemSettings.openGnomePanel("network") } 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")
}
}
} }
@@ -4,50 +4,61 @@ import qs.config
import qs.services import qs.services
import qs.modules.quicksettings import qs.modules.quicksettings
Item { SettingsPage {
Flickable { title: "Sound"
anchors.fill: parent lede: "Live PipeWire output, input, and device selection."
clip: true
contentWidth: width
contentHeight: content.implicitHeight + 64
boundsBehavior: Flickable.StopAtBounds
Column { SettingsCard {
id: content title: "Output"
width: parent.width - 68 subtitle: Pipewire.defaultAudioSink?.description ?? "No output device"
x: 34
y: 30
spacing: 16
Text { text: "Sound"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: 27; font.weight: Font.DemiBold } AudioSlider {
Text { text: "Live PipeWire output, input, and device selection."; color: Theme.fgDim; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize; bottomPadding: 6 } 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 { SettingsCard {
title: "Output" title: "Input"
subtitle: Pipewire.defaultAudioSink?.description ?? "No output device" subtitle: Pipewire.defaultAudioSource?.description ?? "No input 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 { AudioSlider {
title: "Input" width: parent.width
subtitle: Pipewire.defaultAudioSource?.description ?? "No input device" node: Pipewire.defaultAudioSource
AudioSlider { width: parent.width; node: Pipewire.defaultAudioSource; output: false } output: false
Rectangle { width: parent.width; height: 1; color: Theme.alpha(Theme.fg, 0.06) } }
AudioDeviceList { width: parent.width; output: false; maxHeight: 160 } Rectangle {
} width: parent.width
height: 1
color: Theme.alpha(Theme.fg, 0.06)
}
AudioDeviceList {
width: parent.width
output: false
maxHeight: 160
}
}
SettingsCard { SettingsCard {
title: "Advanced sound" title: "Advanced sound"
SettingRow {
label: "Application volumes and profiles" ActionRow {
detail: "Open Fedora's complete sound panel" label: "Application volumes and profiles"
divider: false detail: "Open Fedora's complete sound panel"
controlWidth: 104 divider: false
SettingsButton { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: "Open panel"; onClicked: SystemSettings.openGnomePanel("sound") } action: "Open panel"
} onTriggered: SystemSettings.openGnomePanel("sound")
}
} }
} }
} }
+43
View File
@@ -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'
+79 -5
View File
@@ -3,6 +3,85 @@
set -euo pipefail set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" 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<tail>.*?)\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)" state_home="$(mktemp -d /tmp/panama-settings-pages-state.XXXXXX)"
source_config_path="$repo_dir/config/dot/quickshell" source_config_path="$repo_dir/config/dot/quickshell"
config_path="$state_home/quickshell" config_path="$state_home/quickshell"
@@ -58,11 +137,6 @@ exit 97
EOF EOF
chmod +x "$test_bin/flatpak" chmod +x "$test_bin/flatpak"
fail() {
printf 'settings pages contract: %s\n' "$1" >&2
exit 1
}
qs_for_test() { qs_for_test() {
PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \ PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \
qs -p "$config_path" "$@" qs -p "$config_path" "$@"