Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Lock.qml

Raw
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));
        }
    }
}