From a80a6f4dda5bd41db5cecfc58fa71ed480957893 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 14:12:30 -0400 Subject: [PATCH] Add lock screen appearance settings --- .../lock-screen-settings-harness.qml | 83 ++++++++++ .../modules/settings/AppearancePage.qml | 18 +++ .../modules/settings/LockScreenPreview.qml | 152 ++++++++++++++++++ .../dot/quickshell/modules/settings/README.md | 6 + config/dot/quickshell/modules/settings/qmldir | 1 + .../quickshell/services/SettingsSearch.qml | 5 +- .../lock-screen-settings-contract.sh | 92 +++++++++++ tests/quickshell/settings-search-contract.sh | 2 + 8 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 config/dot/quickshell/lock-screen-settings-harness.qml create mode 100644 config/dot/quickshell/modules/settings/LockScreenPreview.qml create mode 100755 tests/quickshell/lock-screen-settings-contract.sh diff --git a/config/dot/quickshell/lock-screen-settings-harness.qml b/config/dot/quickshell/lock-screen-settings-harness.qml new file mode 100644 index 0000000..3e6ba53 --- /dev/null +++ b/config/dot/quickshell/lock-screen-settings-harness.qml @@ -0,0 +1,83 @@ +import Quickshell +import Quickshell.Io +import QtQuick + +import qs.modules.settings + +ShellRoot { + id: root + + readonly property string fixtureWallpaper: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg` + + LockScreenPreview { + id: screenshotPreview + width: 620 + backgroundMode: "screenshot" + blurLevel: 4 + showClock: true + showDate: true + showUser: true + fadeOnEmpty: false + wallpaperPath: root.fixtureWallpaper + } + + LockScreenPreview { + id: wallpaperPreview + width: 620 + backgroundMode: "wallpaper" + blurLevel: 2 + showClock: false + showDate: true + showUser: false + fadeOnEmpty: true + wallpaperPath: root.fixtureWallpaper + } + + LockScreenPreview { + id: solidPreview + width: 620 + backgroundMode: "solid" + blurLevel: 0 + showClock: true + showDate: false + showUser: true + fadeOnEmpty: false + wallpaperPath: root.fixtureWallpaper + } + + IpcHandler { + target: "lock-screen-settings-test" + + function status(): string { + return JSON.stringify({ + screenshot: { + mode: screenshotPreview.previewMode, + wallpaperVisible: screenshotPreview.wallpaperVisible, + blurStrength: screenshotPreview.blurStrength, + clockVisible: screenshotPreview.clockVisible, + dateVisible: screenshotPreview.dateVisible, + userVisible: screenshotPreview.userVisible, + passwordVisible: screenshotPreview.passwordVisible + }, + wallpaper: { + mode: wallpaperPreview.previewMode, + wallpaperVisible: wallpaperPreview.wallpaperVisible, + blurStrength: wallpaperPreview.blurStrength, + clockVisible: wallpaperPreview.clockVisible, + dateVisible: wallpaperPreview.dateVisible, + userVisible: wallpaperPreview.userVisible, + passwordVisible: wallpaperPreview.passwordVisible + }, + solid: { + mode: solidPreview.previewMode, + wallpaperVisible: solidPreview.wallpaperVisible, + blurStrength: solidPreview.blurStrength, + clockVisible: solidPreview.clockVisible, + dateVisible: solidPreview.dateVisible, + userVisible: solidPreview.userVisible, + passwordVisible: solidPreview.passwordVisible + } + }); + } + } +} diff --git a/config/dot/quickshell/modules/settings/AppearancePage.qml b/config/dot/quickshell/modules/settings/AppearancePage.qml index 50dc3aa..edac189 100644 --- a/config/dot/quickshell/modules/settings/AppearancePage.qml +++ b/config/dot/quickshell/modules/settings/AppearancePage.qml @@ -75,6 +75,24 @@ SettingsPage { } } + SettingsCard { + title: "Lock screen" + subtitle: LockScreen.lastError !== "" + ? LockScreen.lastError + : "A representative preview of the screen shown before authentication." + + LockScreenPreview { + width: parent.width + } + + ChoiceRow { setting: "lockBackgroundMode" } + SliderRow { setting: "lockBlurLevel"; zeroLabel: "Off" } + ToggleRow { setting: "lockShowClock" } + ToggleRow { setting: "lockShowDate" } + ToggleRow { setting: "lockShowUser" } + ToggleRow { setting: "lockFadeOnEmpty"; divider: false } + } + SettingsCard { title: "Colour scheme" subtitle: ColorScheme.lastError !== "" diff --git a/config/dot/quickshell/modules/settings/LockScreenPreview.qml b/config/dot/quickshell/modules/settings/LockScreenPreview.qml new file mode 100644 index 0000000..d8cc720 --- /dev/null +++ b/config/dot/quickshell/modules/settings/LockScreenPreview.qml @@ -0,0 +1,152 @@ +import Quickshell +import QtQuick + +import qs.config +import qs.services + +Rectangle { + id: root + + property string backgroundMode: DesktopPreferences.get("lockBackgroundMode") + property int blurLevel: DesktopPreferences.get("lockBlurLevel") + property bool showClock: DesktopPreferences.get("lockShowClock") + property bool showDate: DesktopPreferences.get("lockShowDate") + property bool showUser: DesktopPreferences.get("lockShowUser") + property bool fadeOnEmpty: DesktopPreferences.get("lockFadeOnEmpty") + property bool use24Hour: DesktopPreferences.get("use24Hour") + property string wallpaperPath: Wallpaper.active !== "" + ? Wallpaper.active + : (Wallpaper.configured !== "" ? Wallpaper.configured : Wallpaper.shippedPath) + + readonly property string previewMode: root.backgroundMode + readonly property real blurStrength: Math.max(0, Math.min(1, root.blurLevel / 5)) + readonly property bool wallpaperVisible: wallpaper.visible + readonly property bool clockVisible: clockLabel.visible + readonly property bool dateVisible: dateLabel.visible + readonly property bool userVisible: userLabel.visible + readonly property bool passwordVisible: passwordField.visible + + width: parent ? parent.width : 620 + implicitHeight: 230 + radius: Theme.cardRadius + clip: true + color: Theme.bg + border.width: 1 + border.color: Theme.alpha(Theme.fg, 0.10) + + Image { + id: wallpaper + anchors.fill: parent + visible: root.backgroundMode === "wallpaper" + source: visible && root.wallpaperPath !== "" ? root.wallpaperPath : "" + asynchronous: true + cache: true + fillMode: Image.PreserveAspectCrop + sourceSize.width: 960 + sourceSize.height: 540 + } + + // Screenshot mode stays representative instead of taking a real desktop + // capture inside Settings. The layered window silhouettes communicate the + // selected softness without a live ShaderEffect or GPU repaint loop. + Rectangle { + anchors.fill: parent + visible: root.backgroundMode === "screenshot" + color: Theme.bgDark + + Rectangle { + x: parent.width * 0.10 + y: parent.height * 0.12 + width: parent.width * 0.45 + height: parent.height * 0.66 + radius: 14 + root.blurStrength * 10 + color: Theme.alpha(Theme.bgHighlight, 0.52 - root.blurStrength * 0.18) + border.width: 1 + border.color: Theme.alpha(Theme.accent, 0.14) + } + + Rectangle { + x: parent.width * 0.48 + y: parent.height * 0.24 + width: parent.width * 0.40 + height: parent.height * 0.57 + radius: 14 + root.blurStrength * 10 + color: Theme.alpha(Theme.bgPanel, 0.64 - root.blurStrength * 0.20) + border.width: 1 + border.color: Theme.alpha(Theme.accentSecondary, 0.13) + } + } + + Rectangle { + anchors.fill: parent + color: root.backgroundMode === "wallpaper" + ? Theme.alpha(Theme.bg, 0.42) + : Theme.alpha(Theme.bg, 0.12 + root.blurStrength * 0.16) + } + + Text { + id: clockLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 26 + visible: root.showClock + text: Qt.formatDateTime(clock.date, root.use24Hour ? "HH:mm" : "h:mm") + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: 42 + font.weight: Font.Light + } + + Text { + id: dateLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: clockLabel.visible ? clockLabel.bottom : parent.top + anchors.topMargin: clockLabel.visible ? 1 : 35 + visible: root.showDate + text: Qt.formatDateTime(clock.date, "dddd, MMMM d") + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + + Rectangle { + id: passwordField + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: root.showUser ? 48 : 30 + visible: !root.fadeOnEmpty + width: Math.min(270, parent.width * 0.48) + height: 36 + radius: height / 2 + color: Theme.alpha(Theme.bgPanel, 0.84) + border.width: 1 + border.color: Theme.alpha(Theme.accent, 0.78) + + Text { + anchors.centerIn: parent + text: "Password" + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.italic: true + } + } + + Text { + id: userLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 19 + visible: root.showUser + text: Quickshell.env("USER") || "User" + color: Theme.alpha(Theme.fg, 0.90) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + SystemClock { + id: clock + precision: SystemClock.Minutes + } +} diff --git a/config/dot/quickshell/modules/settings/README.md b/config/dot/quickshell/modules/settings/README.md index b16936e..34c48bc 100644 --- a/config/dot/quickshell/modules/settings/README.md +++ b/config/dot/quickshell/modules/settings/README.md @@ -78,6 +78,11 @@ the owner instead of duplicating it. | `lockMinutes` | Power | Privacy | Idle timing owns the mechanism; privacy owns the expectation that the unattended desktop locks. | | `lockOnSleep` | Power | Privacy | Suspend owns the transition; privacy owns whether waking requires authentication. | +Lock-screen visuals belong only to **Appearance**: background source, blur, +clock, date, user name, and password-field presentation. **Power** owns when +the session locks, while **Privacy** keeps only the established timing mirrors +above. Visual controls must not be copied onto either page. + Mirrors must remain the same schema-backed control, never a second preference or a copied default. Additions to this table require a concrete discoverability reason and an update to `tests/quickshell/settings-ownership-contract.sh`. @@ -155,6 +160,7 @@ file with `qs -p`, and discover its PID from the exact Config path in | `$XDG_STATE_HOME/panama/panama-home.json` | Home accessory favourites and aliases | | `$XDG_STATE_HOME/panama/backups/` | Settings snapshots | | `$XDG_STATE_HOME/panama/hypridle.conf` | Generated idle config | +| `$XDG_STATE_HOME/panama/hyprlock.conf` | Generated lock-screen appearance | `SystemSettings.restoreDefaults()` spans all of them. A reset that silently skipped one would be worse than having no reset, because nothing would say so. diff --git a/config/dot/quickshell/modules/settings/qmldir b/config/dot/quickshell/modules/settings/qmldir index ed91864..c3c5833 100644 --- a/config/dot/quickshell/modules/settings/qmldir +++ b/config/dot/quickshell/modules/settings/qmldir @@ -29,6 +29,7 @@ ChoiceRow 1.0 ChoiceRow.qml ActionRow 1.0 ActionRow.qml TextRow 1.0 TextRow.qml DesktopPreview 1.0 DesktopPreview.qml +LockScreenPreview 1.0 LockScreenPreview.qml PowerPage 1.0 PowerPage.qml DateTimePage 1.0 DateTimePage.qml AccessibilityPage 1.0 AccessibilityPage.qml diff --git a/config/dot/quickshell/services/SettingsSearch.qml b/config/dot/quickshell/services/SettingsSearch.qml index 408abcf..fd6d73c 100644 --- a/config/dot/quickshell/services/SettingsSearch.qml +++ b/config/dot/quickshell/services/SettingsSearch.qml @@ -34,6 +34,7 @@ Singleton { "windows": "appearance", "effects": "appearance", "wallpaper": "appearance", + "lockAppearance": "appearance", "dock": "desktop", "focus": "desktop", "display": "displays", @@ -65,7 +66,9 @@ Singleton { { label: "Restore defaults", detail: "Return every Panama setting to its shipped value", page: "desktop" }, { label: "Keyboard shortcuts", detail: "Every shortcut the compositor has bound", page: "shortcuts" }, { label: "System Health", detail: "Check Panama services, integrations, tools, and recovery actions", page: "services" }, - { label: "Copy health report", detail: "Copy a redacted Panama doctor report", page: "services" } + { label: "Copy health report", detail: "Copy a redacted Panama doctor report", page: "services" }, + { label: "Lock screen background", detail: "Choose a blurred desktop, wallpaper, or solid colour", page: "appearance" }, + { label: "Password field", detail: "Choose whether the empty lock-screen field stays visible", page: "appearance" } ] function pageFor(group: string): string { diff --git a/tests/quickshell/lock-screen-settings-contract.sh b/tests/quickshell/lock-screen-settings-contract.sh new file mode 100755 index 0000000..4998193 --- /dev/null +++ b/tests/quickshell/lock-screen-settings-contract.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +preview="$repo_dir/config/dot/quickshell/modules/settings/LockScreenPreview.qml" +appearance="$repo_dir/config/dot/quickshell/modules/settings/AppearancePage.qml" +power="$repo_dir/config/dot/quickshell/modules/settings/PowerPage.qml" +privacy="$repo_dir/config/dot/quickshell/modules/settings/PrivacyPage.qml" +harness="$repo_dir/config/dot/quickshell/lock-screen-settings-harness.qml" +state_home="$(mktemp -d /tmp/panama-lock-settings-state.XXXXXX)" +shell_log="$state_home/quickshell.log" +harness_pid="" + +fail() { + printf 'lock screen settings contract: %s\n' "$1" >&2 + [[ -s "$shell_log" ]] && sed -n '1,160p' "$shell_log" >&2 + exit 1 +} + +instances_for_harness() { + qs list --all 2>/dev/null | awk -v expected="$harness" ' + /^Instance / { pid = "" } + /^[[:space:]]*Process ID:/ { pid = $3 } + /^[[:space:]]*Config path:/ { + path = $0 + sub(/^[[:space:]]*Config path: /, "", path) + if (path == expected && pid ~ /^[0-9]+$/) print pid + } + ' +} + +cleanup() { + if [[ "$harness_pid" =~ ^[0-9]+$ ]] && kill -0 "$harness_pid" 2>/dev/null; then + kill "$harness_pid" 2>/dev/null || true + fi + rm -rf "$state_home" +} +trap cleanup EXIT + +[[ -f "$preview" ]] || fail 'LockScreenPreview.qml is missing' +rg -Fq 'title: "Lock screen"' "$appearance" || fail 'Appearance has no Lock screen card' +for key in lockBackgroundMode lockBlurLevel lockShowClock lockShowDate lockShowUser lockFadeOnEmpty; do + rg -Fq "setting: \"$key\"" "$appearance" || fail "Appearance does not expose $key" + ! rg -Fq "setting: \"$key\"" "$power" "$privacy" \ + || fail "$key was duplicated onto Power or Privacy" +done + +python3 - "$appearance" <<'PY' || fail 'Lock screen card is not between Background and Shell typography' +import sys + +text = open(sys.argv[1], encoding="utf-8").read() +background = text.index('title: "Background"') +lock = text.index('title: "Lock screen"') +typography = text.index('title: "Shell typography"') +raise SystemExit(0 if background < lock < typography else 1) +PY + +qs_for_harness() { + if [[ "$harness_pid" =~ ^[0-9]+$ && "${1:-}" == "ipc" ]]; then + XDG_STATE_HOME="$state_home" qs -p "$harness" ipc --pid "$harness_pid" "${@:2}" + else + XDG_STATE_HOME="$state_home" qs -p "$harness" "$@" + fi +} + +before_lockers="$(pgrep -x hyprlock | sort -n || true)" +qs_for_harness --daemonize >"$shell_log" 2>&1 || fail 'isolated preview harness did not launch' +for _ in $(seq 1 60); do + harness_pid="$(instances_for_harness | head -1)" + if [[ "$harness_pid" =~ ^[0-9]+$ ]] \ + && qs_for_harness ipc show 2>/dev/null | rg -q '^target lock-screen-settings-test$'; then + break + fi + sleep 0.1 +done +[[ "$harness_pid" =~ ^[0-9]+$ ]] || fail 'isolated preview harness process did not start' + +status="$(qs_for_harness ipc call lock-screen-settings-test status)" +jq -e ' + .screenshot == {"mode":"screenshot","wallpaperVisible":false,"blurStrength":0.8,"clockVisible":true,"dateVisible":true,"userVisible":true,"passwordVisible":true} + and .wallpaper == {"mode":"wallpaper","wallpaperVisible":true,"blurStrength":0.4,"clockVisible":false,"dateVisible":true,"userVisible":false,"passwordVisible":false} + and .solid == {"mode":"solid","wallpaperVisible":false,"blurStrength":0,"clockVisible":true,"dateVisible":false,"userVisible":true,"passwordVisible":true} +' <<<"$status" >/dev/null || fail "preview roles did not follow their settings: $status" + +after_lockers="$(pgrep -x hyprlock | sort -n || true)" +[[ "$after_lockers" == "$before_lockers" ]] || fail 'preview started a real hyprlock process' +if rg -n 'ReferenceError|TypeError|Binding loop|Unable to assign|Cannot assign|Failed to load' "$shell_log"; then + fail 'preview harness emitted a QML runtime warning' +fi + +printf 'lock screen settings contract: PASS\n' diff --git a/tests/quickshell/settings-search-contract.sh b/tests/quickshell/settings-search-contract.sh index 2973d8f..6d7fc4b 100755 --- a/tests/quickshell/settings-search-contract.sh +++ b/tests/quickshell/settings-search-contract.sh @@ -60,6 +60,8 @@ timezone|Timezone|datetime repeat delay|Repeat delay|shortcuts system health|System Health|services doctor|Copy health report|services +lock screen background|Lock screen background|appearance +password field|Password field|appearance CASES ! rg -Fq 'Startup & Services' "$repo_dir/config/dot/quickshell/services/SettingsSearch.qml" \