1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

lock: fprintd support

This commit is contained in:
bbedward
2025-10-14 09:18:57 -04:00
parent 1019eb925a
commit ee755b8bd6
7 changed files with 288 additions and 69 deletions

View File

@@ -173,6 +173,9 @@ Singleton {
property int dankBarPosition: SettingsData.Position.Top
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
property bool lockScreenShowPowerActions: true
property bool enableFprint: false
property int maxFprintTries: 3
property bool fprintdAvailable: false
property bool hideBrightnessSlider: false
property string widgetBackgroundColor: "sch"
property string surfaceBase: "s"
@@ -440,6 +443,8 @@ Singleton {
popupGapsManual = settings.popupGapsManual !== undefined ? settings.popupGapsManual : 4
dankBarPosition = settings.dankBarPosition !== undefined ? settings.dankBarPosition : (settings.dankBarAtBottom !== undefined ? (settings.dankBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : (settings.topBarAtBottom !== undefined ? (settings.topBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : SettingsData.Position.Top))
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
enableFprint = settings.enableFprint !== undefined ? settings.enableFprint : false
maxFprintTries = settings.maxFprintTries !== undefined ? settings.maxFprintTries : 3
hideBrightnessSlider = settings.hideBrightnessSlider !== undefined ? settings.hideBrightnessSlider : false
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
@@ -576,6 +581,8 @@ Singleton {
"popupGapsManual": popupGapsManual,
"dankBarPosition": dankBarPosition,
"lockScreenShowPowerActions": lockScreenShowPowerActions,
"enableFprint": enableFprint,
"maxFprintTries": maxFprintTries,
"hideBrightnessSlider": hideBrightnessSlider,
"widgetBackgroundColor": widgetBackgroundColor,
"surfaceBase": surfaceBase,
@@ -1441,6 +1448,16 @@ Singleton {
saveSettings()
}
function setEnableFprint(enabled) {
enableFprint = enabled
saveSettings()
}
function setMaxFprintTries(tries) {
maxFprintTries = tries
saveSettings()
}
function setHideBrightnessSlider(enabled) {
hideBrightnessSlider = enabled
saveSettings()
@@ -1513,6 +1530,7 @@ Singleton {
loadSettings()
fontCheckTimer.start()
initializeListModels()
fprintdDetectionProcess.running = true
}
}
@@ -1672,6 +1690,16 @@ Singleton {
}
}
Process {
id: fprintdDetectionProcess
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
running: false
onExited: exitCode => {
fprintdAvailable = (exitCode === 0)
}
}
IpcHandler {
function reveal(): string {
root.setDankBarVisible(true)

View File

@@ -97,6 +97,15 @@ Item {
visible: SessionService.loginctlAvailable && SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Enable fingerprint authentication")
description: "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)"
checked: SettingsData.enableFprint
visible: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.setEnableFprint(checked)
}
}
}

View File

@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common

View File

@@ -6,7 +6,6 @@ import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
@@ -253,22 +252,50 @@ Item {
border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: passwordField.activeFocus ? 2 : 1
DankIcon {
id: lockIcon
Item {
id: lockIconContainer
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: "lock"
width: 20
height: 20
DankIcon {
id: lockIcon
anchors.centerIn: parent
name: {
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off";
if (pam.fprint.active)
return "fingerprint";
return "lock";
}
size: 20
color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
opacity: pam.passwd.active ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
TextInput {
id: passwordField
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
anchors.rightMargin: {
let margin = Theme.spacingM
if (loadingSpinner.visible) {
@@ -296,9 +323,9 @@ Item {
}
}
onAccepted: {
if (!demoMode && !pam.active) {
if (!demoMode && !pam.passwd.active) {
console.log("Enter pressed, starting PAM authentication")
pam.start()
pam.passwd.start()
}
}
Keys.onPressed: event => {
@@ -306,7 +333,7 @@ Item {
return
}
if (pam.active) {
if (pam.passwd.active) {
console.log("PAM is active, ignoring input")
event.accepted = true
return
@@ -355,7 +382,7 @@ Item {
StyledText {
id: placeholder
anchors.left: lockIcon.right
anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2
@@ -367,12 +394,12 @@ Item {
if (root.unlocking) {
return "Unlocking..."
}
if (pam.active) {
if (pam.passwd.active) {
return "Authenticating..."
}
return "Password..."
}
color: root.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline)
color: root.unlocking ? Theme.primary : (pam.passwd.active ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium
opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
@@ -392,7 +419,7 @@ Item {
}
StyledText {
anchors.left: lockIcon.right
anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2
@@ -427,7 +454,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !root.unlocking
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.passwd.active && !root.unlocking
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
@@ -439,7 +466,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: !demoMode && !pam.active && !root.unlocking
visible: !demoMode && !pam.passwd.active && !root.unlocking
enabled: visible
onClicked: {
if (keyboardController.isKeyboardActive) {
@@ -460,7 +487,7 @@ Item {
height: 24
radius: 12
color: "transparent"
visible: !demoMode && (pam.active || root.unlocking)
visible: !demoMode && (pam.passwd.active || root.unlocking)
DankIcon {
anchors.centerIn: parent
@@ -492,7 +519,7 @@ Item {
Item {
anchors.fill: parent
visible: pam.active && !root.unlocking
visible: pam.passwd.active && !root.unlocking
Rectangle {
width: 20
@@ -522,7 +549,7 @@ Item {
}
RotationAnimation on rotation {
running: pam.active && !root.unlocking
running: pam.passwd.active && !root.unlocking
loops: Animation.Infinite
duration: Anims.durLong
from: 0
@@ -540,12 +567,12 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: 36
visible: (demoMode || (!pam.active && !root.unlocking))
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
enabled: !demoMode
onClicked: {
if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication")
pam.start()
pam.passwd.start()
}
}
@@ -1091,53 +1118,30 @@ Item {
}
}
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
PamContext {
Pam {
id: pam
config: pamConfigWatcher.loaded ? "dankshell" : "login"
onResponseRequiredChanged: {
if (demoMode)
return
console.log("PAM response required:", responseRequired)
if (!responseRequired)
return
console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length)
respond(root.passwordBuffer)
}
onCompleted: res => {
if (demoMode)
return
console.log("PAM authentication completed with result:", res)
if (res === PamResult.Success) {
console.log("Authentication successful, unlocking")
lockSecured: !demoMode
onUnlockRequested: {
root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
return
}
console.log("Authentication failed:", res)
onStateChanged: {
root.pamState = state
if (state !== "") {
placeholderDelay.restart()
passwordField.text = ""
root.passwordBuffer = ""
if (res === PamResult.Error)
root.pamState = "error"
else if (res === PamResult.MaxTries)
root.pamState = "max"
else if (res === PamResult.Failed)
root.pamState = "fail"
placeholderDelay.restart()
}
}
}
Binding {
target: pam
property: "buffer"
value: root.passwordBuffer
}
Timer {
id: placeholderDelay

177
Modules/Lock/Pam.qml Normal file
View File

@@ -0,0 +1,177 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pam
import qs.Common
Scope {
id: root
property bool lockSecured: false
readonly property alias passwd: passwd
readonly property alias fprint: fprint
property string lockMessage
property string state
property string fprintState
property string buffer
signal flashMsg
signal unlockRequested
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
PamContext {
id: passwd
config: pamConfigWatcher.loaded ? "dankshell" : "login"
onMessageChanged: {
if (message.startsWith("The account is locked"))
root.lockMessage = message;
else if (root.lockMessage && message.endsWith(" left to unlock)"))
root.lockMessage += "\n" + message;
}
onResponseRequiredChanged: {
if (!responseRequired)
return;
respond(root.buffer);
}
onCompleted: res => {
if (res === PamResult.Success) {
root.unlockRequested();
return;
}
if (res === PamResult.Error)
root.state = "error";
else if (res === PamResult.MaxTries)
root.state = "max";
else if (res === PamResult.Failed)
root.state = "fail";
root.flashMsg();
stateReset.restart();
}
}
PamContext {
id: fprint
property bool available
property int tries
property int errorTries
function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured) {
abort();
return;
}
tries = 0;
errorTries = 0;
start();
}
config: "fprint"
configDirectory: Quickshell.shellDir + "/assets/pam"
onCompleted: res => {
if (!available)
return;
if (res === PamResult.Success) {
root.unlockRequested();
return;
}
if (res === PamResult.Error) {
root.fprintState = "error";
errorTries++;
if (errorTries < 5) {
abort();
errorRetry.restart();
}
} else if (res === PamResult.MaxTries) {
tries++;
if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail";
start();
} else {
root.fprintState = "max";
abort();
}
}
root.flashMsg();
fprintStateReset.start();
}
}
Process {
id: availProc
command: ["sh", "-c", "fprintd-list $USER"]
onExited: code => {
fprint.available = code === 0;
fprint.checkAvail();
}
}
Timer {
id: errorRetry
interval: 800
onTriggered: fprint.start()
}
Timer {
id: stateReset
interval: 4000
onTriggered: {
if (root.state !== "max")
root.state = "";
}
}
Timer {
id: fprintStateReset
interval: 4000
onTriggered: {
root.fprintState = "";
fprint.errorTries = 0;
}
}
onLockSecuredChanged: {
if (lockSecured) {
availProc.running = true;
root.state = "";
root.fprintState = "";
root.lockMessage = "";
} else {
fprint.abort();
}
}
Connections {
target: SettingsData
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
}
}

View File

@@ -291,7 +291,7 @@ sudo pacman -S cava wl-clipboard cliphist brightnessctl qt6-multimedia
paru -S matugen-bin dgop
# Fedora
sudo dnf install cava wl-clipboard brightnessctl qt6-multimedia
sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
```

3
assets/pam/fprint Normal file
View File

@@ -0,0 +1,3 @@
#%PAM-1.0
auth required pam_fprintd.so max-tries=1