feat(nosignal): redesign lock screen to match handoff

Rewrite the WlSessionLockSurface visuals to the NoSignal lock design while
keeping the secure session-lock + PAM machinery (Pam buffer/handleKey/state,
WlSessionLock unlock):

- Blurred screencopy wallpaper + dark scrim; top status row (lock+Locked /
  wifi/bt/battery); centred 108px ExtraLight clock + date; gradient avatar +
  username; password field (key glyph, masked dots, peach enter glyph, error
  border) fed to pam.handleKey; bottom now-playing strip + "Press Enter to
  unlock". JetBrains Mono / Nerd Font (NsIcon) / Theme tokens.
- Escape does not dismiss; correct password (PAM) unlocks; wrong password shows
  inline error. Verified in VM: renders, masks, unlocks.
- Glyphs: add login (enter arrow) + key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-06-14 11:30:20 +01:00
parent 83d5de1ba3
commit 7c06b808ae
2 changed files with 264 additions and 169 deletions

View file

@ -1,11 +1,22 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
// NoSignal lock screen surface (interface redesign 2026-06-14).
// Secure ext-session-lock content: blurred wallpaper + scrim, top status row,
// centred clock/date/avatar/username/password, bottom now-playing strip.
// Reuses caelestia's Pam (buffer + handleKey + state) for PAM auth and the
// WlSessionLock unlock signal input is fed to pam.handleKey, never compared
// in QML. JetBrains Mono / Nerd Font / NoSignal Theme tokens.
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Effects import QtQuick.Effects
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
import qs.services import qs.services
import qs.utils
WlSessionLockSurface { WlSessionLockSurface {
id: root id: root
@ -13,7 +24,7 @@ WlSessionLockSurface {
required property WlSessionLock lock required property WlSessionLock lock
required property Pam pam required property Pam pam
readonly property alias unlocking: unlockAnim.running readonly property bool errored: root.pam.state === "error" || root.pam.state === "fail"
contentItem.Config.screen: screen.name contentItem.Config.screen: screen.name
contentItem.Tokens.screen: screen.name contentItem.Tokens.screen: screen.name
@ -22,144 +33,17 @@ WlSessionLockSurface {
Connections { Connections {
function onUnlock(): void { function onUnlock(): void {
unlockAnim.start(); root.lock.locked = false;
} }
target: root.lock target: root.lock
} }
SequentialAnimation { // Blurred wallpaper + scrim
id: unlockAnim
ParallelAnimation {
Anim {
target: lockContent
properties: "implicitWidth,implicitHeight"
to: lockContent.size
}
Anim {
target: lockBg
property: "radius"
to: lockContent.radius
}
Anim {
target: content
property: "scale"
to: 0
}
Anim {
target: content
property: "opacity"
to: 0
type: Anim.StandardSmall
}
Anim {
target: lockIcon
property: "opacity"
to: 1
type: Anim.StandardLarge
}
Anim {
target: background
property: "opacity"
to: 0
type: Anim.StandardLarge
}
SequentialAnimation {
PauseAnimation {
duration: Tokens.anim.durations.small
}
Anim {
type: Anim.Standard
target: lockContent
property: "opacity"
to: 0
}
}
}
PropertyAction {
target: root.lock
property: "locked"
value: false
}
}
ParallelAnimation {
id: initAnim
running: true
Anim {
target: background
property: "opacity"
to: 1
type: Anim.StandardLarge
}
SequentialAnimation {
ParallelAnimation {
Anim {
target: lockContent
property: "scale"
to: 1
type: Anim.FastSpatial
}
Anim {
target: lockContent
property: "rotation"
to: 360
duration: Tokens.anim.durations.expressiveFastSpatial
easing: Tokens.anim.standardAccel
}
}
ParallelAnimation {
Anim {
target: lockIcon
property: "rotation"
to: 360
easing: Tokens.anim.standardDecel
}
Anim {
type: Anim.DefaultEffects
target: lockIcon
property: "opacity"
to: 0
}
Anim {
type: Anim.DefaultEffects
target: content
property: "opacity"
to: 1
}
Anim {
target: content
property: "scale"
to: 1
}
Anim {
target: lockBg
property: "radius"
to: lockContent.Tokens.rounding.extraLarge * 1.5
}
Anim {
target: lockContent
property: "implicitWidth"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio
}
Anim {
target: lockContent
property: "implicitHeight"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult
}
}
}
}
ScreencopyView { ScreencopyView {
id: background id: background
anchors.fill: parent anchors.fill: parent
captureSource: root.screen captureSource: root.screen
opacity: 0
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
@ -168,57 +52,265 @@ WlSessionLockSurface {
blur: 1 blur: 1
blurMax: 64 blurMax: 64
blurMultiplier: 1 blurMultiplier: 1
brightness: -0.38
saturation: 0.1
} }
} }
Rectangle {
anchors.fill: parent
color: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.5)
}
// Keyboard capture (feed PAM; Escape does nothing)
Item { Item {
id: lockContent anchors.fill: parent
focus: true
Component.onCompleted: forceActiveFocus()
Keys.onPressed: event => root.pam.handleKey(event)
}
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 // Top status row
readonly property int radius: size / 4 * Tokens.rounding.scale RowLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 22
anchors.leftMargin: 28
anchors.rightMargin: 28
anchors.centerIn: parent RowLayout {
implicitWidth: size spacing: 7
implicitHeight: size NsIcon {
icon: "lock"
rotation: 180 color: Theme.textMuted
scale: 0 size: 15
}
StyledRect { StyledText {
id: lockBg text: "Locked"
color: Theme.textMuted
anchors.fill: parent font.family: Theme.font.family
color: Colours.palette.m3surface font.pixelSize: Theme.font.bar
radius: parent.radius
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
} }
} }
MaterialIcon { Item {
id: lockIcon Layout.fillWidth: true
anchors.centerIn: parent
text: "lock"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build()
rotation: 180
} }
Content { RowLayout {
id: content spacing: 14
anchors.centerIn: parent NsIcon {
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased color: Theme.textMuted
size: 15
}
NsIcon {
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth_connected" : "bluetooth_disabled"
color: Theme.textMuted
size: 15
}
RowLayout {
spacing: 6
visible: UPower.displayDevice.isLaptopBattery
NsIcon {
icon: Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged].includes(UPower.displayDevice.state))
color: Theme.textMuted
size: 15
}
StyledText {
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
}
}
lock: root // Centre block
opacity: 0 ColumnLayout {
scale: 0 anchors.centerIn: parent
spacing: 0
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("hh:mm")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 108
font.weight: Font.ExtraLight
font.letterSpacing: 3
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("ddd, d MMMM yyyy")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: 15
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 64
implicitWidth: 74
implicitHeight: 74
radius: 37
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent
icon: "person"
color: Theme.onAccent
size: 34
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
text: SysInfo.user || "user"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 14
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
implicitWidth: 312
implicitHeight: 44
radius: Theme.radius.panel
color: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.55)
border.width: 1
border.color: root.errored ? Qt.rgba(224 / 255, 116 / 255, 106 / 255, 0.6) : Qt.rgba(1, 1, 1, 0.1)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: 12
spacing: 10
NsIcon {
icon: "key"
color: Theme.textMuted
size: 15
}
StyledText {
Layout.fillWidth: true
text: root.pam.buffer.length > 0 ? "•".repeat(root.pam.buffer.length) : "Enter password"
color: root.pam.buffer.length > 0 ? Theme.text : Theme.textFaint
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.body
font.letterSpacing: root.pam.buffer.length > 0 ? 2 : 0
}
NsIcon {
icon: "login"
color: root.pam.buffer.length > 0 ? Theme.accent : Theme.textFaint
size: 16
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onClicked: if (root.pam.buffer.length > 0)
root.pam.passwd.start()
}
}
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.preferredHeight: 14
text: root.errored ? "Incorrect — try again" : (root.pam.lockMessage || "")
color: Theme.danger
font.family: Theme.font.family
font.pixelSize: 12
}
}
// Bottom strip
RowLayout {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 26
anchors.leftMargin: 28
anchors.rightMargin: 28
RowLayout {
spacing: 10
visible: Players.active !== null
Rectangle {
implicitWidth: 42
implicitHeight: 42
radius: Theme.radius.panel
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent
icon: "music_note"
color: Theme.onAccent
size: 18
}
}
ColumnLayout {
spacing: 0
StyledText {
text: Players.active?.trackTitle || ""
color: Theme.text
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
text: Players.active?.trackArtist || ""
color: Theme.textMuted
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
Item {
Layout.fillWidth: true
}
StyledText {
text: "Press Enter to unlock"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: 11
} }
} }
} }

View file

@ -197,7 +197,10 @@ Singleton {
"update": root.cp(0xf021), "update": root.cp(0xf021),
"wallpaper": root.cp(0xf03e), "wallpaper": root.cp(0xf03e),
"workspaces": root.cp(0xf009), "workspaces": root.cp(0xf009),
"dock": root.cp(0xf2d1) "dock": root.cp(0xf2d1),
// lock screen
"login": root.cp(0xf090),
"key": root.cp(0xf084)
} }
function get(name: string): string { function get(name: string): string {