Show fingerprint on lock screen and polkit, gated by lid state

Bring the fingerprint affordance to the Quickshell lock screen and polkit
dialog, matching what hyprlock did on master.

Lock screen: render the md-fingerprint glyph inside the password field's
right edge when a sensor is enrolled, reserving space so long passwords
never run under it.

Polkit dialog: show one method at a time. When a sensor is enrolled and
the reader is reachable, the dialog is just the centered fingerprint icon
(square card); the moment PAM asks for a password it switches to the
password field. Detects pam_fprintd anywhere in the auth stack now that a
gate can precede it.

Lid awareness: a closed lid means the reader is unreachable, so both
surfaces fall back to the password. polkit gets a pam_exec clamshell gate
(auth [success=1 default=ignore] before pam_fprintd) so a shut lid drops
straight to the password prompt instead of blocking on the reader for the
pam_fprintd timeout; the lock screen hides the icon and skips scanning.

The gate points at the fixed /usr/bin path the package always provides so
it survives switching between package installs and dev-link. A migration
adds the gate for existing fingerprint setups.

New helper omarchy-hw-laptop-closed (pure lid state); omarchy-hw-clamshell
now composes it with the external-monitor check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-23 13:51:07 -07:00
co-authored by Claude Opus 4.8
parent 39ca9135c4
commit 540e411edf
12 changed files with 360 additions and 50 deletions
+30 -7
View File
@@ -22,6 +22,9 @@ Item {
property bool fingerprintAuthenticating: false
property bool passwordPamConfigured: false
property bool fingerprintConfigured: false
// Lid shut → the reader is unreachable, so hide the affordance and don't
// bother scanning; fall back to the password like the polkit dialog does.
property bool laptopClosed: false
property bool previewVisible: false
property string enteredPassword: ""
property string pendingPassword: ""
@@ -34,6 +37,8 @@ Item {
readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating
// Fingerprint is only offered when a sensor is enrolled and the lid is open.
readonly property bool fingerprintAvailable: fingerprintConfigured && !laptopClosed
function realScreenCount() {
var screens = Quickshell.screens || []
@@ -82,6 +87,10 @@ Item {
if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true
}
function refreshLaptopClosed() {
if (!laptopClosedProc.running) laptopClosedProc.running = true
}
function logEvent(event) {
lastEvent = event
lastEventAt = new Date().toISOString()
@@ -115,6 +124,7 @@ Item {
Qt.callLater(function() {
root.refreshBackground()
root.refreshFingerprintStatus()
root.refreshLaptopClosed()
})
return true
@@ -182,7 +192,7 @@ Item {
}
function startFingerprint() {
if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
if (!lockRequested || !sessionLock.secure || !fingerprintAvailable) return
if (fingerprintPam.active || fingerprintAuthenticating) return
fingerprintAuthenticating = true
@@ -197,7 +207,7 @@ Item {
if (!lockRequested) return
if (result === PamResult.Success) {
finishUnlock()
} else if (fingerprintConfigured) {
} else if (fingerprintAvailable) {
fingerprintRetryTimer.restart()
}
}
@@ -245,7 +255,7 @@ Item {
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
fingerprintConfigured: root.fingerprintConfigured
fingerprintConfigured: root.fingerprintAvailable
authenticatingPassword: root.authenticatingPassword
failureMessage: root.failureMessage
failedAttempts: root.failedAttempts
@@ -275,7 +285,7 @@ Item {
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
fingerprintConfigured: root.fingerprintConfigured
fingerprintConfigured: root.fingerprintAvailable
authenticatingPassword: false
failureMessage: ""
failedAttempts: 0
@@ -324,7 +334,7 @@ Item {
onError: function(error) {
root.fingerprintAuthenticating = false
if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart()
if (root.lockRequested && root.fingerprintAvailable) fingerprintRetryTimer.restart()
}
}
@@ -356,8 +366,19 @@ Item {
stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true }
onExited: {
root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes"
if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint()
else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort()
if (root.lockRequested && root.fingerprintAvailable) root.startFingerprint()
else if (!root.fingerprintAvailable && fingerprintPam.active) fingerprintPam.abort()
}
}
Process {
id: laptopClosedProc
command: ["bash", "-c", "omarchy-hw-laptop-closed && echo closed || echo open"]
stdout: StdioCollector { id: laptopClosedStdout; waitForEnd: true }
onExited: {
root.laptopClosed = String(laptopClosedStdout.text || "").trim() === "closed"
if (root.lockRequested && root.fingerprintAvailable) root.startFingerprint()
else if (!root.fingerprintAvailable && fingerprintPam.active) fingerprintPam.abort()
}
}
@@ -425,6 +446,7 @@ Item {
Component.onCompleted: {
refreshBackground()
refreshFingerprintStatus()
refreshLaptopClosed()
}
IpcHandler {
@@ -450,6 +472,7 @@ Item {
realScreens: root.realScreenCount(),
passwordPam: root.passwordPamConfigured,
fingerprint: root.fingerprintConfigured,
laptopClosed: root.laptopClosed,
authenticating: root.authenticating,
lastEvent: root.lastEvent,
lastEventAt: root.lastEventAt