feat(lockscreen): add system-themed elegant quickshell lockscreen with PAM authentication and animated letter typing indicator
This commit is contained in:
parent
80d3dad766
commit
5e32e43091
461
quickshell/modules/lockscreen/LockscreenContent.qml
Normal file
461
quickshell/modules/lockscreen/LockscreenContent.qml
Normal file
@ -0,0 +1,461 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import "../../theme"
|
||||
import "../../services"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
focus: true
|
||||
|
||||
// Force focus when lockscreen becomes visible
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.forceActiveFocus()
|
||||
secretInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.forceActiveFocus()
|
||||
secretInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
// Secret hidden TextInput to capture keyboard presses reliably
|
||||
TextInput {
|
||||
id: secretInput
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
echoMode: TextInput.Password
|
||||
|
||||
onTextChanged: {
|
||||
// Synchronize with LockscreenService
|
||||
if (text !== LockscreenService.userPassword) {
|
||||
LockscreenService.userPassword = text
|
||||
LockscreenService.typedCount = text.length
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
event.accepted = true
|
||||
LockscreenService.submitPassword()
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
event.accepted = true
|
||||
secretInput.text = ""
|
||||
LockscreenService.clearPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LockscreenService
|
||||
function onIsLockedChanged() {
|
||||
if (LockscreenService.isLocked) {
|
||||
secretInput.text = ""
|
||||
root.forceActiveFocus()
|
||||
secretInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
function onAuthFailedChanged() {
|
||||
if (LockscreenService.authFailed) {
|
||||
shakeAnim.restart()
|
||||
secretInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shake animation on incorrect password
|
||||
SequentialAnimation {
|
||||
id: shakeAnim
|
||||
running: false
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: -24; duration: 40; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: 24; duration: 50; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: -16; duration: 50; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: 16; duration: 50; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: -8; duration: 40; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: 8; duration: 40; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: cardContainer; property: "anchors.horizontalCenterOffset"; to: 0; duration: 40; easing.type: Easing.OutQuad }
|
||||
}
|
||||
|
||||
// Click anywhere to focus input
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.forceActiveFocus()
|
||||
secretInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Main Centered Content Container
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: 28
|
||||
width: Math.min(520, parent.width - 40)
|
||||
|
||||
// 1. Clock & Date Header
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
id: clockText
|
||||
text: {
|
||||
let date = new Date()
|
||||
let h = date.getHours().toString().padStart(2, '0')
|
||||
let m = date.getMinutes().toString().padStart(2, '0')
|
||||
let s = date.getSeconds().toString().padStart(2, '0')
|
||||
return `${h}:${m}:${s}`
|
||||
}
|
||||
color: Theme.fg
|
||||
font.pixelSize: 64
|
||||
font.weight: Font.Bold
|
||||
font.family: "Sans-Serif"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: clockText.text = {
|
||||
let date = new Date()
|
||||
let h = date.getHours().toString().padStart(2, '0')
|
||||
let m = date.getMinutes().toString().padStart(2, '0')
|
||||
let s = date.getSeconds().toString().padStart(2, '0')
|
||||
return `${h}:${m}:${s}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: dateText
|
||||
text: {
|
||||
let date = new Date()
|
||||
let options = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }
|
||||
return date.toLocaleDateString(Qt.locale(), options)
|
||||
}
|
||||
color: Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.75)
|
||||
font.pixelSize: 15
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 10 }
|
||||
|
||||
// 2. User Avatar & Badge
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 96
|
||||
height: 96
|
||||
radius: 48
|
||||
color: Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.15)
|
||||
border.color: Theme.accent
|
||||
border.width: 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Avatar Icon / Initial Badge
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: LockscreenService.username ? LockscreenService.username.charAt(0).toUpperCase() : "🔒"
|
||||
color: Theme.accent
|
||||
font.pixelSize: 42
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
// Breathing Glow Ring
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
radius: 52
|
||||
color: "transparent"
|
||||
border.color: Theme.accent
|
||||
border.width: 1.5
|
||||
opacity: avatarPulse.opacityVal
|
||||
|
||||
Item {
|
||||
id: avatarPulse
|
||||
property real opacityVal: 0.3
|
||||
SequentialAnimation on opacityVal {
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
NumberAnimation { to: 0.8; duration: 1500; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { to: 0.2; duration: 1500; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: LockscreenService.username || "User"
|
||||
color: Theme.fg
|
||||
font.pixelSize: 20
|
||||
font.weight: Font.Bold
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 4 }
|
||||
|
||||
// 3. Elegant Glass Input Card with Animated Typing Dots
|
||||
Rectangle {
|
||||
id: cardContainer
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: cardLayout.implicitHeight + 36
|
||||
radius: 20
|
||||
color: Theme.isDark ? Qt.rgba(30/255, 30/255, 46/255, 0.65) : Qt.rgba(240/255, 240/255, 245/255, 0.75)
|
||||
border.color: LockscreenService.authFailed ? Theme.red : (secretInput.activeFocus ? Theme.accent : Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.18))
|
||||
border.width: LockscreenService.authFailed ? 2 : (secretInput.activeFocus ? 2 : 1)
|
||||
|
||||
Behavior on border.color { ColorAnimation { duration: 180 } }
|
||||
|
||||
ColumnLayout {
|
||||
id: cardLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 18
|
||||
spacing: 14
|
||||
|
||||
// Password Row: Input Field + Submit Button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
// Lock Icon
|
||||
Text {
|
||||
text: "🔒"
|
||||
font.pixelSize: 18
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Password Visual Placeholder / Input Area
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 44
|
||||
radius: 12
|
||||
color: Qt.rgba(0, 0, 0, 0.25)
|
||||
border.color: Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.1)
|
||||
|
||||
// Empty State Placeholder Text
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
visible: LockscreenService.typedCount === 0 && !LockscreenService.isAuthenticating
|
||||
text: "Type password to unlock..."
|
||||
color: Theme.comment
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
// Authenticating Spinner / Status Text
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: LockscreenService.isAuthenticating
|
||||
text: "Verifying password..."
|
||||
color: Theme.accent
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// ANIMATED TYPING DOTS (Shows WHEN and HOW MANY letters are typed)
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
visible: LockscreenService.typedCount > 0 && !LockscreenService.isAuthenticating
|
||||
|
||||
Repeater {
|
||||
model: LockscreenService.typedCount
|
||||
|
||||
Rectangle {
|
||||
id: dot
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: index === LockscreenService.typedCount - 1 ? Theme.accent : Theme.fg
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
// Spawn Bounce & Pulse Animation for each character typed
|
||||
scale: 1.0
|
||||
SequentialAnimation on scale {
|
||||
running: true
|
||||
NumberAnimation { from: 0.2; to: 1.4; duration: 120; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { from: 1.4; to: 1.0; duration: 100; easing.type: Easing.OutBounce }
|
||||
}
|
||||
|
||||
// Inner Glow Core
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 6
|
||||
height: 6
|
||||
radius: 3
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus Click Area
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.forceActiveFocus()
|
||||
secretInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit / Unlock Arrow Button
|
||||
Rectangle {
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 12
|
||||
color: submitMouse.containsMouse ? Theme.accent : Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.25)
|
||||
border.color: Theme.accent
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "➔"
|
||||
color: submitMouse.containsMouse ? "#ffffff" : Theme.accent
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: submitMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: LockscreenService.submitPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic Typing Status / Letter Counter & Feedback Message
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Typing Letter Counter Badge (Shows EXACT count of letters typed)
|
||||
Rectangle {
|
||||
implicitWidth: counterText.implicitWidth + 16
|
||||
implicitHeight: 24
|
||||
radius: 12
|
||||
color: LockscreenService.typedCount > 0 ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.2) : "transparent"
|
||||
visible: LockscreenService.typedCount > 0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text {
|
||||
id: counterText
|
||||
anchors.centerIn: parent
|
||||
text: `⌨️ ${LockscreenService.typedCount} ${LockscreenService.typedCount === 1 ? 'letter' : 'letters'} typed`
|
||||
color: Theme.accent
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// Auth Error Message Display
|
||||
Text {
|
||||
text: LockscreenService.authErrorMsg
|
||||
color: Theme.red
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
visible: LockscreenService.authFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 14 }
|
||||
|
||||
// 4. Session Action Buttons (Power & Unlock)
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 16
|
||||
|
||||
// Suspend Button
|
||||
Rectangle {
|
||||
implicitWidth: 110
|
||||
implicitHeight: 38
|
||||
radius: 10
|
||||
color: suspendMouse.containsMouse ? Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.15) : Qt.rgba(0, 0, 0, 0.3)
|
||||
border.color: Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.15)
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
Text { text: "🌙"; font.pixelSize: 13 }
|
||||
Text { text: "Sleep"; color: Theme.fg; font.pixelSize: 12; font.weight: Font.Medium }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["systemctl", "suspend"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot Button
|
||||
Rectangle {
|
||||
implicitWidth: 110
|
||||
implicitHeight: 38
|
||||
radius: 10
|
||||
color: rebootMouse.containsMouse ? Qt.rgba(Theme.orange.r, Theme.orange.g, Theme.orange.b, 0.25) : Qt.rgba(0, 0, 0, 0.3)
|
||||
border.color: Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.15)
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
Text { text: "🔄"; font.pixelSize: 13 }
|
||||
Text { text: "Restart"; color: Theme.fg; font.pixelSize: 12; font.weight: Font.Medium }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: SessionService.reboot()
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown Button
|
||||
Rectangle {
|
||||
implicitWidth: 110
|
||||
implicitHeight: 38
|
||||
radius: 10
|
||||
color: powerMouse.containsMouse ? Qt.rgba(Theme.red.r, Theme.red.g, Theme.red.b, 0.25) : Qt.rgba(0, 0, 0, 0.3)
|
||||
border.color: Qt.rgba(Theme.fg.r, Theme.fg.g, Theme.fg.b, 0.15)
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
Text { text: "⚡"; font.pixelSize: 13 }
|
||||
Text { text: "Power Off"; color: Theme.fg; font.pixelSize: 12; font.weight: Font.Medium }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: SessionService.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
quickshell/modules/lockscreen/LockscreenWindow.qml
Normal file
50
quickshell/modules/lockscreen/LockscreenWindow.qml
Normal file
@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../../theme"
|
||||
import "../../services"
|
||||
|
||||
PanelWindow {
|
||||
id: lockscreenWindow
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.keyboardFocus: LockscreenService.isLocked ? WlrLayershell.Exclusive : WlrLayershell.None
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
visible: LockscreenService.isLocked
|
||||
color: "black"
|
||||
|
||||
// Background System Wallpaper
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: WallpaperService.currentWallpaper
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: 0.65
|
||||
|
||||
Behavior on source {
|
||||
PropertyAnimation { duration: 300 }
|
||||
}
|
||||
}
|
||||
|
||||
// Glass Dark Backdrop Overlay with Gradient
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.isDark ? Qt.rgba(15/255, 15/255, 25/255, 0.82) : Qt.rgba(20/255, 20/255, 30/255, 0.70) }
|
||||
GradientStop { position: 0.5; color: Theme.isDark ? Qt.rgba(20/255, 20/255, 32/255, 0.88) : Qt.rgba(25/255, 25/255, 38/255, 0.78) }
|
||||
GradientStop { position: 1.0; color: Theme.isDark ? Qt.rgba(10/255, 10/255, 18/255, 0.92) : Qt.rgba(15/255, 15/255, 25/255, 0.85) }
|
||||
}
|
||||
}
|
||||
|
||||
// Lockscreen Content Layout
|
||||
LockscreenContent {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
110
quickshell/services/LockscreenService.qml
Normal file
110
quickshell/services/LockscreenService.qml
Normal file
@ -0,0 +1,110 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isLocked: false
|
||||
property string userPassword: ""
|
||||
property bool isAuthenticating: false
|
||||
property bool authFailed: false
|
||||
property string authErrorMsg: ""
|
||||
property int typedCount: 0
|
||||
property string username: ""
|
||||
property string realName: ""
|
||||
|
||||
// Signal emitted whenever a key is typed to trigger animated dot entry/bounce
|
||||
signal keyTyped(string char)
|
||||
signal keyDeleted()
|
||||
|
||||
Timer {
|
||||
id: resetErrorTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.authFailed = false
|
||||
root.authErrorMsg = ""
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
usernameProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: usernameProc
|
||||
command: ["whoami"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let u = data.trim()
|
||||
if (u !== "") root.username = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lock() {
|
||||
userPassword = ""
|
||||
typedCount = 0
|
||||
authFailed = false
|
||||
authErrorMsg = ""
|
||||
isLocked = true
|
||||
}
|
||||
|
||||
function unlock() {
|
||||
userPassword = ""
|
||||
typedCount = 0
|
||||
authFailed = false
|
||||
authErrorMsg = ""
|
||||
isLocked = false
|
||||
}
|
||||
|
||||
function appendChar(ch) {
|
||||
if (isAuthenticating) return;
|
||||
authFailed = false
|
||||
userPassword += ch
|
||||
typedCount = userPassword.length
|
||||
root.keyTyped(ch)
|
||||
}
|
||||
|
||||
function backspace() {
|
||||
if (isAuthenticating) return;
|
||||
authFailed = false
|
||||
if (userPassword.length > 0) {
|
||||
userPassword = userPassword.substring(0, userPassword.length - 1)
|
||||
typedCount = userPassword.length
|
||||
root.keyDeleted()
|
||||
}
|
||||
}
|
||||
|
||||
function clearPassword() {
|
||||
userPassword = ""
|
||||
typedCount = 0
|
||||
}
|
||||
|
||||
function submitPassword() {
|
||||
if (isAuthenticating || userPassword === "") return;
|
||||
isAuthenticating = true
|
||||
authFailed = false
|
||||
authErrorMsg = ""
|
||||
|
||||
authProc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/lockscreen_auth_service.py", userPassword]
|
||||
authProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: authProc
|
||||
onExited: (code, exitStatus) => {
|
||||
root.isAuthenticating = false
|
||||
if (code === 0) {
|
||||
root.unlock()
|
||||
} else {
|
||||
root.authFailed = true
|
||||
root.authErrorMsg = "Incorrect password"
|
||||
root.clearPassword()
|
||||
resetErrorTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ Item {
|
||||
id: root
|
||||
|
||||
function lock() {
|
||||
LockscreenService.lock()
|
||||
lockProc.running = true
|
||||
}
|
||||
|
||||
|
||||
75
quickshell/services/python/lockscreen_auth_service.py
Executable file
75
quickshell/services/python/lockscreen_auth_service.py
Executable file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import getpass
|
||||
|
||||
libpam = None
|
||||
try:
|
||||
libpam_path = ctypes.util.find_library('pam') or 'libpam.so.0'
|
||||
libpam = ctypes.CDLL(libpam_path)
|
||||
except Exception:
|
||||
libpam = None
|
||||
|
||||
libc = ctypes.CDLL(None)
|
||||
|
||||
class PamMessage(ctypes.Structure):
|
||||
_fields_ = [('msg_style', ctypes.c_int), ('msg', ctypes.c_char_p)]
|
||||
|
||||
class PamResponse(ctypes.Structure):
|
||||
_fields_ = [('resp', ctypes.c_char_p), ('resp_retcode', ctypes.c_int)]
|
||||
|
||||
CONV_FUNC = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.POINTER(PamMessage)), ctypes.POINTER(ctypes.POINTER(PamResponse)), ctypes.c_void_p)
|
||||
|
||||
class PamConv(ctypes.Structure):
|
||||
_fields_ = [('conv', CONV_FUNC), ('appdata_ptr', ctypes.c_void_p)]
|
||||
|
||||
def verify_password(username, password):
|
||||
if not password:
|
||||
return False
|
||||
if not libpam:
|
||||
return False
|
||||
|
||||
pwd_bytes = password.encode('utf-8')
|
||||
|
||||
def conv_cb(num_msg, msg, resp, appdata_ptr):
|
||||
response_array = (PamResponse * num_msg)()
|
||||
for i in range(num_msg):
|
||||
response_array[i].resp = libc.strdup(pwd_bytes)
|
||||
response_array[i].resp_retcode = 0
|
||||
resp[0] = ctypes.cast(response_array, ctypes.POINTER(PamResponse))
|
||||
return 0
|
||||
|
||||
cb = CONV_FUNC(conv_cb)
|
||||
conv = PamConv(cb, None)
|
||||
|
||||
for service in ['system-auth', 'login', 'passwd', 'kde', 'sddm', 'gdm', 'su', 'shadow']:
|
||||
pamh = ctypes.c_void_p()
|
||||
res = libpam.pam_start(service.encode('utf-8'), username.encode('utf-8'), ctypes.byref(conv), ctypes.byref(pamh))
|
||||
if res == 0:
|
||||
auth_res = libpam.pam_authenticate(pamh, 0)
|
||||
libpam.pam_end(pamh, auth_res)
|
||||
if auth_res == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
# Read from stdin
|
||||
password = sys.stdin.read().strip()
|
||||
else:
|
||||
password = sys.argv[1].strip()
|
||||
|
||||
username = getpass.getuser()
|
||||
if len(sys.argv) >= 3:
|
||||
username = sys.argv[2].strip()
|
||||
|
||||
success = verify_password(username, password)
|
||||
if success:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -7,6 +7,7 @@ import "modules/launcher"
|
||||
import "modules/wallpaper"
|
||||
import "modules/notifications"
|
||||
import "modules/desktop"
|
||||
import "modules/lockscreen"
|
||||
import "services"
|
||||
|
||||
Scope {
|
||||
@ -144,4 +145,7 @@ Scope {
|
||||
|
||||
// Bottom-Right Notification Toast Overlay
|
||||
NotificationToast {}
|
||||
|
||||
// Lockscreen Overlay Window (Layer: Overlay)
|
||||
LockscreenWindow {}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user