Add lock screen appearance settings

This commit is contained in:
Gabriel Brown
2026-08-18 15:00:18 -04:00
parent 3520700983
commit a80a6f4dda
8 changed files with 358 additions and 1 deletions
@@ -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
}
});
}
}
}
@@ -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 { SettingsCard {
title: "Colour scheme" title: "Colour scheme"
subtitle: ColorScheme.lastError !== "" subtitle: ColorScheme.lastError !== ""
@@ -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
}
}
@@ -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. | | `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. | | `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 Mirrors must remain the same schema-backed control, never a second preference
or a copied default. Additions to this table require a concrete discoverability or a copied default. Additions to this table require a concrete discoverability
reason and an update to `tests/quickshell/settings-ownership-contract.sh`. 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/panama-home.json` | Home accessory favourites and aliases |
| `$XDG_STATE_HOME/panama/backups/` | Settings snapshots | | `$XDG_STATE_HOME/panama/backups/` | Settings snapshots |
| `$XDG_STATE_HOME/panama/hypridle.conf` | Generated idle config | | `$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 `SystemSettings.restoreDefaults()` spans all of them. A reset that silently
skipped one would be worse than having no reset, because nothing would say so. skipped one would be worse than having no reset, because nothing would say so.
@@ -29,6 +29,7 @@ ChoiceRow 1.0 ChoiceRow.qml
ActionRow 1.0 ActionRow.qml ActionRow 1.0 ActionRow.qml
TextRow 1.0 TextRow.qml TextRow 1.0 TextRow.qml
DesktopPreview 1.0 DesktopPreview.qml DesktopPreview 1.0 DesktopPreview.qml
LockScreenPreview 1.0 LockScreenPreview.qml
PowerPage 1.0 PowerPage.qml PowerPage 1.0 PowerPage.qml
DateTimePage 1.0 DateTimePage.qml DateTimePage 1.0 DateTimePage.qml
AccessibilityPage 1.0 AccessibilityPage.qml AccessibilityPage 1.0 AccessibilityPage.qml
@@ -34,6 +34,7 @@ Singleton {
"windows": "appearance", "windows": "appearance",
"effects": "appearance", "effects": "appearance",
"wallpaper": "appearance", "wallpaper": "appearance",
"lockAppearance": "appearance",
"dock": "desktop", "dock": "desktop",
"focus": "desktop", "focus": "desktop",
"display": "displays", "display": "displays",
@@ -65,7 +66,9 @@ Singleton {
{ label: "Restore defaults", detail: "Return every Panama setting to its shipped value", page: "desktop" }, { 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: "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: "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 { function pageFor(group: string): string {
+92
View File
@@ -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'
@@ -60,6 +60,8 @@ timezone|Timezone|datetime
repeat delay|Repeat delay|shortcuts repeat delay|Repeat delay|shortcuts
system health|System Health|services system health|System Health|services
doctor|Copy health report|services doctor|Copy health report|services
lock screen background|Lock screen background|appearance
password field|Password field|appearance
CASES CASES
! rg -Fq 'Startup & Services' "$repo_dir/config/dot/quickshell/services/SettingsSearch.qml" \ ! rg -Fq 'Startup & Services' "$repo_dir/config/dot/quickshell/services/SettingsSearch.qml" \