feat: lock screen

This commit is contained in:
2 * r + 2 * t 2025-06-22 20:50:39 +10:00
parent 7f2b5b4b6f
commit 2a652d549a
8 changed files with 675 additions and 0 deletions

View file

@ -15,6 +15,7 @@ Singleton {
property alias osd: adapter.osd
property alias session: adapter.session
property alias winfo: adapter.winfo
property alias lock: adapter.lock
property alias paths: adapter.paths
FileView {
@ -34,6 +35,7 @@ Singleton {
property JsonObject osd: OsdConfig {}
property JsonObject session: SessionConfig {}
property JsonObject winfo: WInfoConfig {}
property JsonObject lock: LockConfig {}
property JsonObject paths: UserPaths {}
}
}

12
config/LockConfig.qml Normal file
View file

@ -0,0 +1,12 @@
import Quickshell.Io
JsonObject {
property JsonObject sizes: JsonObject {
property real border: 0.1
property int clockWidth: 800
property int clockHeight: 200
property int inputWidth: 600
property int inputHeight: 200
property int faceSize: 100
}
}

View file

@ -0,0 +1,216 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Shapes
import QtQuick.Effects
Item {
id: root
required property bool locked
readonly property real clockBottom: innerMask.anchors.margins + clockPath.height
readonly property real inputTop: innerMask.anchors.margins + inputPath.height
anchors.fill: parent
StyledRect {
id: base
anchors.fill: parent
color: Colours.alpha(Config.border.colour, false)
visible: false
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
id: innerMask
anchors.fill: parent
anchors.margins: root.locked ? root.height * Config.lock.sizes.border : 0
anchors.leftMargin: root.locked ? root.width * Config.lock.sizes.border : 0
anchors.rightMargin: root.locked ? root.width * Config.lock.sizes.border : 0
radius: Appearance.rounding.large * 2
Behavior on anchors.margins {
Anim {}
}
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.rightMargin {
Anim {}
}
}
}
MultiEffect {
anchors.fill: parent
source: base
maskEnabled: true
maskInverted: true
maskSource: mask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Shape {
anchors.fill: parent
anchors.margins: Math.floor(innerMask.anchors.margins)
anchors.leftMargin: innerMask.anchors.leftMargin
anchors.rightMargin: innerMask.anchors.rightMargin
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: clockPath
readonly property int width: Config.lock.sizes.clockWidth
property real height: root.locked ? Config.lock.sizes.clockHeight : 0
readonly property real rounding: Appearance.rounding.large * 4
readonly property bool flatten: height < rounding * 2
readonly property real roundingY: flatten ? height / 2 : rounding
strokeWidth: -1
fillColor: Config.border.colour
startX: (innerMask.width - width) / 2 - rounding
PathArc {
relativeX: clockPath.rounding
relativeY: clockPath.roundingY
radiusX: clockPath.rounding
radiusY: Math.min(clockPath.rounding, clockPath.height)
}
PathLine {
relativeX: 0
relativeY: clockPath.height - clockPath.roundingY * 2
}
PathArc {
relativeX: clockPath.rounding
relativeY: clockPath.roundingY
radiusX: clockPath.rounding
radiusY: Math.min(clockPath.rounding, clockPath.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: clockPath.width - clockPath.rounding * 2
relativeY: 0
}
PathArc {
relativeX: clockPath.rounding
relativeY: -clockPath.roundingY
radiusX: clockPath.rounding
radiusY: Math.min(clockPath.rounding, clockPath.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(clockPath.height - clockPath.roundingY * 2)
}
PathArc {
relativeX: clockPath.rounding
relativeY: -clockPath.roundingY
radiusX: clockPath.rounding
radiusY: Math.min(clockPath.rounding, clockPath.height)
}
Behavior on height {
Anim {}
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
ShapePath {
id: inputPath
readonly property int width: Config.lock.sizes.inputWidth
property real height: root.locked ? Config.lock.sizes.inputHeight : 0
readonly property real rounding: Appearance.rounding.large * 2
readonly property bool flatten: height < rounding * 2
readonly property real roundingY: flatten ? height / 2 : rounding
strokeWidth: -1
fillColor: Config.border.colour
startX: (innerMask.width - width) / 2 - rounding
startY: Math.ceil(innerMask.height)
PathArc {
relativeX: inputPath.rounding
relativeY: -inputPath.roundingY
radiusX: inputPath.rounding
radiusY: Math.min(inputPath.rounding, inputPath.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(inputPath.height - inputPath.roundingY * 2)
}
PathArc {
relativeX: inputPath.rounding
relativeY: -inputPath.roundingY
radiusX: inputPath.rounding
radiusY: Math.min(inputPath.rounding, inputPath.height)
}
PathLine {
relativeX: inputPath.width - inputPath.rounding * 2
relativeY: 0
}
PathArc {
relativeX: inputPath.rounding
relativeY: inputPath.roundingY
radiusX: inputPath.rounding
radiusY: Math.min(inputPath.rounding, inputPath.height)
}
PathLine {
relativeX: 0
relativeY: inputPath.height - inputPath.roundingY * 2
}
PathArc {
relativeX: inputPath.rounding
relativeY: inputPath.roundingY
radiusX: inputPath.rounding
radiusY: Math.min(inputPath.rounding, inputPath.height)
direction: PathArc.Counterclockwise
}
Behavior on height {
Anim {}
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

56
modules/lock/Clock.qml Normal file
View file

@ -0,0 +1,56 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property bool locked
spacing: 0
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Appearance.spacing.small
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.format("HH")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge * 4
font.family: Appearance.font.family.mono
font.weight: 800
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: ":"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge * 4
font.family: Appearance.font.family.mono
font.weight: 800
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.format("mm")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge * 4
font.family: Appearance.font.family.mono
font.weight: 800
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Appearance.padding.large * 3
text: Time.format("dddd, d MMMM yyyy")
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.extraLarge
font.family: Appearance.font.family.mono
font.bold: true
}
}

254
modules/lock/Input.qml Normal file
View file

@ -0,0 +1,254 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pam
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property WlSessionLockSurface lock
property string passwordBuffer
spacing: Appearance.spacing.large * 2
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Appearance.padding.large * 3
Layout.maximumWidth: Config.lock.sizes.inputWidth - Appearance.rounding.large * 2
spacing: Appearance.spacing.large
StyledClippingRect {
Layout.alignment: Qt.AlignVCenter
implicitWidth: Config.lock.sizes.faceSize
implicitHeight: Config.lock.sizes.faceSize
radius: Appearance.rounding.large
color: Colours.palette.m3surfaceContainer
MaterialIcon {
anchors.centerIn: parent
text: "person"
fill: 1
font.pointSize: Config.lock.sizes.faceSize / 2
}
CachingImage {
anchors.fill: parent
path: `${Paths.home}/.face`
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Appearance.spacing.small
StyledText {
Layout.fillWidth: true
text: qsTr("Welcome back, %1").arg(Quickshell.env("USER"))
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: qsTr("Logging in to %1").arg(Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP"))
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.large
elide: Text.ElideRight
}
}
}
StyledRect {
Layout.fillWidth: true
Layout.preferredWidth: charList.implicitWidth + Appearance.padding.large * 2
Layout.preferredHeight: Appearance.font.size.normal + Appearance.padding.large * 2
focus: true
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.small
clip: true
Keys.onPressed: event => {
if (pam.active)
return;
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
placeholder.animate = false;
pam.start();
} else if (event.key === Qt.Key_Backspace) {
if (event.modifiers & Qt.ControlModifier) {
charList.implicitWidth = charList.implicitWidth; // Break binding
root.passwordBuffer = "";
} else {
root.passwordBuffer = root.passwordBuffer.slice(0, -1);
}
} else if ("abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
charList.bindImWidth();
root.passwordBuffer += event.text;
}
}
PamContext {
id: pam
onResponseRequiredChanged: {
if (!responseRequired)
return;
respond(root.passwordBuffer);
charList.implicitWidth = charList.implicitWidth; // Break binding
root.passwordBuffer = "";
placeholder.animate = true;
}
onCompleted: res => {
if (res === PamResult.Success)
return root.lock.unlock();
if (res === PamResult.Error)
placeholder.pamState = "error";
else if (res === PamResult.MaxTries)
placeholder.pamState = "max";
else if (res === PamResult.Failed)
placeholder.pamState = "fail";
placeholderDelay.restart();
}
}
Timer {
id: placeholderDelay
interval: 3000
onTriggered: placeholder.pamState = ""
}
StyledText {
id: placeholder
property string pamState
anchors.centerIn: parent
text: {
if (pam.active)
return qsTr("Loading...");
if (pamState === "error")
return qsTr("An error occured");
if (pamState === "max")
return qsTr("You have reached the maximum number of tries");
if (pamState === "fail")
return qsTr("Incorrect password");
return qsTr("Enter your password");
}
animate: true
color: pam.active ? Colours.palette.m3secondary : pamState ? Colours.palette.m3error : Colours.palette.m3outline
font.pointSize: Appearance.font.size.larger
opacity: root.passwordBuffer ? 0 : 1
Behavior on opacity {
Anim {}
}
}
ListView {
id: charList
function bindImWidth(): void {
imWidthBehavior.enabled = false;
implicitWidth = Qt.binding(() => Math.min(count * (Appearance.font.size.normal + spacing) - spacing, Config.lock.sizes.inputWidth - Appearance.rounding.large * 2 - Appearance.padding.large * 5));
imWidthBehavior.enabled = true;
}
anchors.centerIn: parent
implicitWidth: Math.min(count * (Appearance.font.size.normal + spacing) - spacing, Config.lock.sizes.inputWidth - Appearance.rounding.large * 2 - Appearance.padding.large * 5)
implicitHeight: Appearance.font.size.normal
orientation: Qt.Horizontal
spacing: Appearance.spacing.small / 2
model: ScriptModel {
values: root.passwordBuffer.split("")
}
delegate: StyledRect {
id: ch
implicitWidth: Appearance.font.size.normal
implicitHeight: Appearance.font.size.normal
color: Colours.palette.m3onSurface
radius: Appearance.rounding.full
opacity: 0
scale: 0.5
Component.onCompleted: {
opacity = 1;
scale = 1;
}
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: ch
property: "ListView.delayRemove"
value: true
}
ParallelAnimation {
Anim {
target: ch
property: "opacity"
to: 0
}
Anim {
target: ch
property: "scale"
to: 0.5
}
}
PropertyAction {
target: ch
property: "ListView.delayRemove"
value: false
}
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
}
Behavior on implicitWidth {
id: imWidthBehavior
Anim {}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

36
modules/lock/Lock.qml Normal file
View file

@ -0,0 +1,36 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Scope {
WlSessionLock {
id: lock
LockSurface {
lock: lock
}
}
CustomShortcut {
name: "lock"
description: "Lock the current session"
onPressed: lock.locked = true
}
CustomShortcut {
name: "unlock"
description: "Unlock the current session"
onPressed: lock.locked = false
}
IpcHandler {
target: "lock"
function lock(): void {
lock.locked = true;
}
}
}

View file

@ -0,0 +1,97 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell.Wayland
import QtQuick
import QtQuick.Effects
WlSessionLockSurface {
id: root
required property WlSessionLock lock
property bool locked
function unlock(): void {
locked = false;
background.opacity = 0;
animDelay.start();
}
Component.onCompleted: locked = true
color: "transparent"
Timer {
id: animDelay
interval: Appearance.anim.durations.large
onTriggered: root.lock.locked = false
}
ScreencopyView {
id: screencopy
anchors.fill: parent
captureSource: root.screen
visible: false
}
MultiEffect {
id: background
anchors.fill: parent
source: screencopy
autoPaddingEnabled: false
blurEnabled: true
blur: root.locked ? 1 : 0
blurMax: 64
blurMultiplier: 1
Behavior on opacity {
Anim {}
}
Behavior on blur {
Anim {}
}
}
Backgrounds {
id: backgrounds
locked: root.locked
visible: false
}
MultiEffect {
anchors.fill: source
source: backgrounds
shadowEnabled: true
blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
}
Clock {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: -backgrounds.clockBottom
locked: root.locked
}
Input {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
anchors.topMargin: -backgrounds.inputTop
lock: root
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -4,12 +4,14 @@ import "modules"
import "modules/drawers"
import "modules/background"
import "modules/areapicker"
import "modules/lock"
import Quickshell
ShellRoot {
Background {}
Drawers {}
AreaPicker {}
Lock {}
Shortcuts {}
}