pragma ComponentBehavior: Bound import QtQuick import Quickshell.Io import Quickshell.Services.Pam import Quickshell.Wayland Item { id: root property Theme theme property Time timeService property Media mediaService property Notifications notificationService property Power powerService readonly property bool nativeActionRoute: true property bool active: false property bool logicalLocked: false property bool unlocking: false property bool unlocked: false property bool teardownClean: true property string failure: "" property int inputLength: 0 property string pendingSecret: "" property int failureNonce: 0 property real auroraTime: 0 property real angryMix: 0 property bool auroraAvailable: false property var lastAction: ({}) readonly property string pamConfig: "nuguland-lock.conf" readonly property var boundedActions: ["request", "type", "submit", "cancel"] function stateObject() { return { "backend": "Lock.qml", "usesNativeSessionLock": true, "usesPam": true, "pamConfig": pamConfig, "nativeActionRoute": nativeActionRoute, "helperProcesses": [], "serviceOwnedActions": true, "boundedActions": boundedActions, "surface": "native-session-lock-surface", "keyboardFocus": true, "passwordOnly": true, "auroraFullscreen": true, "angryAuroraOnFailure": true, "active": active, "locked": logicalLocked, "nativeLocked": sessionLock.locked, "secure": sessionLock.secure, "unlocking": unlocking, "unlocked": unlocked, "teardownClean": teardownClean, "failure": failure, "inputLength": inputLength, "failureNonce": failureNonce, "lastAction": lastAction }; } function stateJson() { return JSON.stringify(stateObject()); } function request() { active = true; logicalLocked = true; unlocking = false; unlocked = false; teardownClean = false; failure = ""; inputLength = 0; pendingSecret = ""; angryMix = 0; pam.active = true; lastAction = { "kind": "request", "nativeActionRoute": nativeActionRoute }; return stateJson(); } function typeSecret(secret) { inputLength = String(secret).length; failure = ""; lastAction = { "kind": "type", "secretLength": inputLength }; return stateJson(); } function fail(message) { active = true; logicalLocked = true; unlocking = false; unlocked = false; teardownClean = false; inputLength = 0; pendingSecret = ""; failure = String(message || "incorrect password").toLowerCase(); failureNonce += 1; angryMix = 1; lastAction = { "kind": "fail", "message": failure }; return stateJson(); } function finishUnlock(action) { active = false; logicalLocked = false; unlocking = false; unlocked = true; teardownClean = true; failure = ""; inputLength = 0; pendingSecret = ""; angryMix = 0; lastAction = action; } function submit(secret) { if (unlocking) return stateJson(); const value = String(secret); unlocking = true; failure = ""; inputLength = value.length; pendingSecret = value; pam.active = true; pam.start(); lastAction = { "kind": "submit", "secretLength": value.length, "nativeActionRoute": nativeActionRoute }; return stateJson(); } function cancel() { pam.abort(); active = false; logicalLocked = false; unlocking = false; unlocked = false; teardownClean = true; failure = ""; inputLength = 0; pendingSecret = ""; angryMix = 0; lastAction = { "kind": "cancel" }; return stateJson(); } FileView { id: auroraBlob path: Qt.resolvedUrl("shaders/aurora.frag.qsb").toString().replace(/^file:\/\//, "") preload: true blockLoading: true printErrors: false onLoaded: root.auroraAvailable = true onLoadFailed: root.auroraAvailable = false } Timer { interval: 67 repeat: true running: root.logicalLocked onTriggered: { root.auroraTime += 0.04; root.angryMix += (0 - root.angryMix) * 0.018; } } WlSessionLock { id: sessionLock locked: root.logicalLocked WlSessionLockSurface { color: root.theme ? root.theme.ui.backdrop : "black" Rectangle { anchors.fill: parent color: root.theme ? root.theme.ui.backdrop : "black" } Loader { anchors.fill: parent active: root.auroraAvailable sourceComponent: ShaderEffect { property real iTime: root.auroraTime property vector2d iResolution: Qt.vector2d(width, height) property vector2d iMouse: Qt.vector2d(0.5 + root.angryMix * 0.18, 0.48 - root.angryMix * 0.1) property real iFirestormMix: root.angryMix anchors.fill: parent blending: true fragmentShader: Qt.resolvedUrl("shaders/aurora.frag.qsb") } } Item { id: lockUi property real shakeOffset: 0 width: 280 height: 58 x: (parent.width - width) / 2 + shakeOffset y: (parent.height - height) / 2 Rectangle { anchors.fill: parent radius: 16 color: root.theme ? root.theme.withAlpha(root.theme.ui.backdrop, 0.54) : "#66000000" border.width: 2 border.color: root.theme ? root.theme.withAlpha(root.angryMix > 0.08 ? root.theme.error : root.theme.ui.accent, 0.92) : "white" } TextInput { id: secretInput anchors.fill: parent anchors.leftMargin: 22 anchors.rightMargin: 22 echoMode: TextInput.Password passwordCharacter: "•" color: root.theme ? root.theme.content.normal : "white" selectedTextColor: root.theme ? root.theme.content.backdrop : "black" selectionColor: root.theme ? root.theme.ui.accent : "white" font.family: "IBM Plex Sans" font.pixelSize: 24 horizontalAlignment: TextInput.AlignHCenter verticalAlignment: TextInput.AlignVCenter focus: root.logicalLocked activeFocusOnTab: true inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase onTextChanged: { if (root.logicalLocked) root.typeSecret(text); } Keys.onReturnPressed: event => { event.accepted = true; root.submit(secretInput.text); secretInput.text = ""; } Keys.onEnterPressed: event => { event.accepted = true; root.submit(secretInput.text); secretInput.text = ""; } Component.onCompleted: forceActiveFocus() } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton onClicked: secretInput.forceActiveFocus() } Connections { function onLogicalLockedChanged() { if (root.logicalLocked) Qt.callLater(secretInput.forceActiveFocus); } function onFailureNonceChanged() { secretInput.text = ""; Qt.callLater(secretInput.forceActiveFocus); shakeAnimation.restart(); } target: root } SequentialAnimation { id: shakeAnimation NumberAnimation { target: lockUi property: "shakeOffset" to: -18 duration: 36 easing.type: Easing.OutCubic } NumberAnimation { target: lockUi property: "shakeOffset" to: 16 duration: 54 easing.type: Easing.InOutCubic } NumberAnimation { target: lockUi property: "shakeOffset" to: -10 duration: 48 easing.type: Easing.InOutCubic } NumberAnimation { target: lockUi property: "shakeOffset" to: 0 duration: 70 easing.type: Easing.OutCubic } } } } } PamContext { id: pam configDirectory: Qt.resolvedUrl("pam") config: root.pamConfig onPamMessage: { if (pam.responseRequired) { pam.respond(root.pendingSecret); root.pendingSecret = ""; } } onCompleted: result => { if (result === PamResult.Success) root.finishUnlock({ "kind": "pam-unlock" }); else root.fail("incorrect password"); } onError: error => { root.fail(PamError.toString(error)); } } }