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 {
anchors.fill: parent
focus: true
Component.onCompleted: forceActiveFocus()
Keys.onPressed: event => root.pam.handleKey(event)
}
// Top status row
RowLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 22
anchors.leftMargin: 28
anchors.rightMargin: 28
RowLayout {
spacing: 7
NsIcon {
icon: "lock"
color: Theme.textMuted
size: 15
}
StyledText {
text: "Locked"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
} }
} }
Item { Item {
id: lockContent Layout.fillWidth: true
}
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 RowLayout {
readonly property int radius: size / 4 * Tokens.rounding.scale spacing: 14
NsIcon {
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
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
}
}
}
}
// Centre block
ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: size spacing: 0
implicitHeight: size
rotation: 180 StyledText {
scale: 0 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
}
StyledRect { StyledText {
id: lockBg 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.fill: parent
color: Colours.palette.m3surface anchors.leftMargin: 14
radius: parent.radius anchors.rightMargin: 12
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 spacing: 10
layer.enabled: true NsIcon {
layer.effect: MultiEffect { icon: "key"
shadowEnabled: true color: Theme.textMuted
blurMax: 15 size: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) }
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()
}
}
} }
} }
MaterialIcon { StyledText {
id: lockIcon 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 anchors.centerIn: parent
text: "lock" icon: "music_note"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build() color: Theme.onAccent
rotation: 180 size: 18
}
} }
Content { ColumnLayout {
id: content 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
}
}
}
anchors.centerIn: parent Item {
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased Layout.fillWidth: true
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased }
lock: root StyledText {
opacity: 0 text: "Press Enter to unlock"
scale: 0 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 {