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
+2 -11
View File
@@ -3,14 +3,5 @@
# omarchy:summary=Returns true when clamshell mode is active
# omarchy:hidden=true
lid_closed=false
for state in /proc/acpi/button/lid/*/state; do
[[ -r $state ]] || continue
if [[ $(< "$state") == *"closed"* ]]; then
lid_closed=true
break
fi
done
[[ $lid_closed == "true" ]] && omarchy-hw-external-monitors
# Clamshell = lid closed while driving one or more external monitors.
omarchy-hw-laptop-closed && omarchy-hw-external-monitors
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# omarchy:summary=Returns true when the laptop lid is closed
# omarchy:hidden=true
for state in /proc/acpi/button/lid/*/state; do
[[ -r $state ]] || continue
[[ $(< "$state") == *"closed"* ]] && exit 0
done
exit 1
+3 -3
View File
@@ -13,10 +13,10 @@ remove_pam_config() {
sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/sudo
fi
# Remove from polkit
if [[ -f /etc/pam.d/polkit-1 ]] && grep -Fq 'pam_fprintd.so' /etc/pam.d/polkit-1; then
# Remove from polkit (both the fingerprint module and its clamshell gate)
if [[ -f /etc/pam.d/polkit-1 ]] && grep -Eq 'pam_fprintd\.so|omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then
echo "Removing fingerprint authentication from polkit..."
sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/polkit-1
sudo sed -i -e '/pam_fprintd\.so/d' -e '/omarchy-hw-laptop-closed/d' /etc/pam.d/polkit-1
fi
}
+24 -6
View File
@@ -25,13 +25,31 @@ setup_pam_config() {
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/sudo
fi
# Configure polkit
if [[ -f /etc/pam.d/polkit-1 ]] && ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then
echo "Configuring polkit for fingerprint authentication..."
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1
elif [[ ! -f /etc/pam.d/polkit-1 ]]; then
# Configure polkit. A clamshell gate runs before pam_fprintd: when the lid
# is shut the reader is unreachable, so it skips fingerprint (success=1) and
# PAM drops straight to the password prompt. Lid open → fingerprint, then
# password as the fallback.
#
# pam_exec needs a literal absolute path (no env expansion). Point at the
# fixed /usr/bin path the omarchy package always provides, so the gate keeps
# working across package installs and dev-link — the latter overlays
# $OMARCHY_PATH trees but leaves /usr/bin untouched.
local polkit_gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed"
if [[ -f /etc/pam.d/polkit-1 ]]; then
if ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then
echo "Configuring polkit for fingerprint authentication..."
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1
fi
if ! grep -q 'omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then
echo "Adding clamshell gate to polkit..."
# Insert immediately before pam_fprintd so success=1 skips exactly it.
sudo sed -i "/pam_fprintd\.so/i $polkit_gate" /etc/pam.d/polkit-1
fi
else
echo "Creating polkit configuration with fingerprint authentication..."
sudo tee /etc/pam.d/polkit-1 >/dev/null <<'EOF'
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
$polkit_gate
auth sufficient pam_fprintd.so
auth required pam_unix.so
+21
View File
@@ -0,0 +1,21 @@
echo "Gate polkit fingerprint auth behind the lid state (password when the lid is shut)"
# Existing fingerprint setups have pam_fprintd first in /etc/pam.d/polkit-1 but
# no lid gate, so a closed-lid pkexec would block on the unreachable reader for
# the full pam_fprintd timeout before offering the password. Insert a pam_exec
# gate before pam_fprintd that skips fingerprint while the lid is closed. New
# setups already get this from omarchy-setup-security-fingerprint.
#
# The gate points at the fixed /usr/bin path the omarchy package always
# provides, so it keeps working across package installs and dev-link (which
# overlays $OMARCHY_PATH but leaves /usr/bin untouched). pam_exec needs a
# literal absolute path — it does not expand env vars.
polkit_pam="/etc/pam.d/polkit-1"
gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed"
if [[ -f $polkit_pam ]] &&
grep -q 'pam_fprintd\.so' "$polkit_pam" &&
! grep -q 'omarchy-hw-laptop-closed' "$polkit_pam"; then
sudo sed -i "/pam_fprintd\.so/i $gate" "$polkit_pam"
fi
+25 -2
View File
@@ -24,6 +24,9 @@ Item {
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19)
// Space to keep clear on each side of the field for the fingerprint icon
// (icon width plus a gap) so the centered dots never run under it.
readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
// Shrink the dots to fit once the password outgrows the field, so every
// keystroke stays visible — otherwise long passwords clip with no feedback.
readonly property real passwordDotScale: dotMetrics.advanceWidth > 0
@@ -130,9 +133,11 @@ Item {
id: passwordInput
anchors.fill: parent
anchors.topMargin: inputField.borderTop
anchors.rightMargin: inputField.borderRight + 18
// Reserve the fingerprint icon's width on both sides so the centered
// dots stay symmetric and never slide under the icon as they grow.
anchors.rightMargin: inputField.borderRight + 18 + root.fingerprintReserve
anchors.bottomMargin: inputField.borderBottom
anchors.leftMargin: inputField.borderLeft + 18
anchors.leftMargin: inputField.borderLeft + 18 + root.fingerprintReserve
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
activeFocusOnPress: true
@@ -190,6 +195,24 @@ Item {
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Fingerprint hint pinned inside the field's right edge when a sensor is
// enrolled, so the user knows they can touch to unlock instead of typing.
// Matches hyprlock, which draws its fingerprint icon in the same spot.
Text {
id: fingerprintIcon
objectName: "fingerprintIndicator"
anchors.right: parent.right
anchors.rightMargin: inputField.borderRight + 18
anchors.verticalCenter: parent.verticalCenter
visible: root.fingerprintConfigured
text: "󰈷"
color: Color.lock.placeholder
font.family: Style.font.family
font.pixelSize: Math.round(root.fieldFontSize * 1.1)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
+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
+46 -13
View File
@@ -32,24 +32,34 @@ Item {
property bool responseVisible: false
property bool failed: false
property bool errorFlash: false
property bool fingerprintFirst: false
// pam_fprintd appears in the polkit PAM stack (a sensor is enrolled).
property bool fingerprintConfigured: false
// Lid shut right now — the reader is physically unreachable, so we fall back
// to the password even when a sensor is enrolled. Refreshed per request.
property bool laptopClosed: false
property int shakeOffset: 0
readonly property bool dialogVisible: polkitAgent.isActive || closing
readonly property bool fingerprintWaiting: dialogVisible && !responseRequired && !submitted && (fingerprintFirst || promptLooksFingerprint(currentPrompt + " " + currentSupplementary))
readonly property int cardWidth: Math.min(Style.space(312), Math.max(Style.space(260), panel.width - Style.gapsOut * 2))
// We show one method at a time. Fingerprint owns the dialog while PAM is
// waiting on the reader (lid open, sensor enrolled); the moment PAM asks for
// a password — including immediately when the lid is shut and the clamshell
// gate skips pam_fprintd — we switch to the password field instead.
readonly property bool fingerprintMode: fingerprintConfigured && !laptopClosed && dialogVisible && !responseRequired && !submitted && !errorFlash
readonly property int cardHeight: panel.height > 0 ? Math.min(fieldHeight + contentMargin * 2, panel.height - Style.gapsOut * 2) : fieldHeight + contentMargin * 2
function promptLooksFingerprint(text) {
return PolkitModel.promptLooksFingerprint(text)
}
// Password mode is a wide field; fingerprint mode collapses to a square that
// just frames the centered sensor icon.
readonly property int cardWidth: fingerprintMode ? cardHeight : Math.min(Style.space(312), Math.max(Style.space(260), panel.width - Style.gapsOut * 2))
function authorizationLabel(message) {
return PolkitModel.authorizationLabel(message)
}
function loadPamConfig(raw) {
fingerprintFirst = PolkitModel.fingerprintFirstFromPamConfig(raw)
fingerprintConfigured = PolkitModel.fingerprintConfiguredFromPamConfig(raw)
}
function refreshLidState() {
if (!laptopClosedProc.running) laptopClosedProc.running = true
}
function resetSnapshot() {
@@ -83,13 +93,16 @@ Item {
closing = false
submitted = false
passwordInput.text = ""
refreshLidState()
syncFromFlow()
Qt.callLater(refocus)
}
function refocus() {
if (!dialogVisible) return
if (fingerprintWaiting) keyCatcher.forceActiveFocus()
// In fingerprint mode there is no field to type into — park focus on the
// key catcher so Escape still cancels; otherwise focus the password field.
if (fingerprintMode) keyCatcher.forceActiveFocus()
else passwordInput.forceActiveFocus()
}
@@ -149,10 +162,17 @@ Item {
watchChanges: true
printErrors: false
onLoaded: root.loadPamConfig(text())
onLoadFailed: root.fingerprintFirst = false
onLoadFailed: root.fingerprintConfigured = false
onFileChanged: reload()
}
Process {
id: laptopClosedProc
command: ["bash", "-c", "omarchy-hw-laptop-closed && echo closed || echo open"]
stdout: StdioCollector { id: laptopClosedOut; waitForEnd: true }
onExited: root.laptopClosed = String(laptopClosedOut.text || "").trim() === "closed"
}
PolkitAgent {
id: polkitAgent
path: "/org/omarchy/PolkitAgent"
@@ -248,8 +268,22 @@ Item {
}
}
// Fingerprint mode shows just the sensor icon, centered and alone \u2014 no
// padlock, no field, no prompt text.
OpticalGlyph {
anchors.centerIn: parent
width: Math.round(root.fieldHeight * 0.7)
height: width
visible: root.fingerprintMode
text: "\udb80\ude37"
fontFamily: root.fontFamily
fontSize: Math.round(root.fieldHeight * 0.7)
color: root.errorFlash ? Color.polkit.textError : root.accent
}
Row {
id: cardRow
visible: !root.fingerprintMode
anchors.fill: parent
anchors.topMargin: card.contentTopInset
anchors.rightMargin: card.contentRightInset
@@ -287,8 +321,7 @@ Item {
color: root.errorFlash ? Color.polkit.textError : root.foreground
cursorVisible: activeFocus && !root.submitted && !root.errorFlash
readOnly: root.submitted || root.errorFlash
enabled: root.dialogVisible && !root.fingerprintWaiting
visible: !root.fingerprintWaiting
enabled: root.dialogVisible
onAccepted: root.submitResponse()
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
@@ -308,7 +341,7 @@ Item {
font.family: root.fontFamily
font.pixelSize: Style.font.iconLarge
elide: Text.ElideRight
visible: passwordInput.visible && passwordInput.text.length === 0
visible: passwordInput.text.length === 0
}
Rectangle {
+6 -3
View File
@@ -3,13 +3,16 @@ function promptLooksFingerprint(text) {
return s.indexOf("finger") !== -1 || s.indexOf("fprint") !== -1 || s.indexOf("swipe") !== -1
}
function fingerprintFirstFromPamConfig(raw) {
function fingerprintConfiguredFromPamConfig(raw) {
// Fingerprint is available whenever pam_fprintd appears anywhere in the auth
// stack — it need not be the first module. A clamshell gate (pam_exec) may
// legitimately precede it to skip fingerprint while the lid is closed.
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/^\s+|\s+$/g, "")
if (!line || line.charAt(0) === "#") continue
if (!line.match(/^auth\s+/)) continue
return line.indexOf("pam_fprintd.so") !== -1
if (line.indexOf("pam_fprintd.so") !== -1) return true
}
return false
}
@@ -23,7 +26,7 @@ function authorizationLabel(message) {
if (typeof module !== "undefined") {
module.exports = {
promptLooksFingerprint: promptLooksFingerprint,
fingerprintFirstFromPamConfig: fingerprintFirstFromPamConfig,
fingerprintConfiguredFromPamConfig: fingerprintConfiguredFromPamConfig,
authorizationLabel: authorizationLabel
}
}
@@ -0,0 +1,108 @@
import QtQuick
import Quickshell
import qs.Commons
ShellRoot {
id: root
readonly property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
readonly property string rootPath: Quickshell.env("OMARCHY_PATH")
property var failures: []
function fail(message) {
failures.push(String(message))
}
function assertTrue(condition, message) {
if (!condition) fail(message)
}
function shellQuote(value) {
return "'" + String(value).replace(/'/g, "'\\''") + "'"
}
function writeResult() {
var payload = JSON.stringify({
ok: failures.length === 0,
failures: failures
})
if (resultPath) {
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
}
}
Item { id: host; width: 800; height: 600 }
TextMetrics {
id: probe
font.family: Style.font.family
}
Timer {
interval: 1
running: true
repeat: false
onTriggered: {
try {
var component = Qt.createComponent("file://" + root.rootPath + "/shell/plugins/lock/LockView.qml", Component.PreferSynchronous)
if (component.status !== Component.Ready) {
root.fail("LockView failed to load: " + component.errorString())
return
}
var view = component.createObject(host, { width: 800, height: 600, loadBackground: false })
if (!view) {
root.fail("LockView failed to instantiate: " + component.errorString())
return
}
var indicator = view.children ? findByObjectName(view, "fingerprintIndicator") : null
root.assertTrue(indicator !== null, "fingerprint indicator exists in the lock view")
if (indicator) {
view.fingerprintConfigured = false
root.assertTrue(!indicator.visible, "fingerprint indicator is hidden when no sensor is configured")
view.fingerprintConfigured = true
root.assertTrue(indicator.visible, "fingerprint indicator is shown when a sensor is configured")
// The field reserves space for the icon so a long password can never
// slide underneath it. The reserve must exceed the icon's own width
// (leaving a gap), and the shrunk dots must fit the reserved-clear
// area even at extreme lengths.
root.assertTrue(view.fingerprintReserve > indicator.width,
"reserved space exceeds the icon width, got reserve " + view.fingerprintReserve + " vs icon " + indicator.width)
view.passwordText = "x".repeat(80)
var clearWidth = view.fieldWidth - 2 * view.fingerprintReserve
probe.font.pixelSize = Math.max(1, Math.floor(view.passwordDotFontSize * view.passwordDotScale))
probe.font.letterSpacing = view.passwordDotLetterSpacing * view.passwordDotScale
probe.text = "●".repeat(80)
root.assertTrue(probe.advanceWidth <= clearWidth,
"80 dots stay clear of the fingerprint icon, need " + probe.advanceWidth + "px of " + clearWidth)
view.fingerprintConfigured = false
root.assertTrue(view.fingerprintReserve === 0, "no space is reserved when no sensor is configured")
}
view.destroy()
} catch (error) {
root.fail("lock fingerprint indicator fixture threw: " + error)
} finally {
root.writeResult()
}
}
}
function findByObjectName(node, name) {
if (!node) return null
if (node.objectName === name) return node
var kids = node.children || []
for (var i = 0; i < kids.length; i++) {
var found = findByObjectName(kids[i], name)
if (found) return found
}
return null
}
}
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
QS_PID=""
cleanup() {
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
kill "$QS_PID" 2>/dev/null || true
wait "$QS_PID" 2>/dev/null || true
fi
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
pass "no Wayland compositor; skipping lock fingerprint indicator test"
exit 0
fi
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping lock fingerprint indicator test"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/lock-fingerprint-indicator"
mkdir -p "$config_dir" "$TMPDIR/home"
cp "$SHELL_TEST_DIR/fixtures/lock-fingerprint-indicator/shell.qml" "$config_dir/shell.qml"
ln -s "$ROOT/shell/Ui" "$config_dir/Ui"
ln -s "$ROOT/shell/Commons" "$config_dir/Commons"
OMARCHY_PATH="$ROOT" \
OMARCHY_QML_TEST_RESULT="$result" \
HOME="$TMPDIR/home" \
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
PATH="$ROOT/bin:$PATH" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..80}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,220p' "$log" >&2
fail "lock fingerprint indicator quickshell exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,220p' "$log" >&2
fail "lock fingerprint indicator test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
printf 'Lock fingerprint indicator result:\n' >&2
jq . "$result" >&2
printf 'Lock fingerprint indicator log:\n' >&2
sed -n '1,220p' "$log" >&2
fail "fingerprint indicator tracks the configured sensor"
fi
pass "fingerprint indicator tracks the configured sensor"
+13 -5
View File
@@ -23,19 +23,27 @@ assertEqual(
)
assert(
polkit.fingerprintFirstFromPamConfig(`
polkit.fingerprintConfiguredFromPamConfig(`
# comment
auth sufficient pam_fprintd.so
auth include system-auth
`),
'polkit detects fingerprint-first PAM config'
'polkit detects fingerprint in a PAM config'
)
assert(
!polkit.fingerprintFirstFromPamConfig(`
polkit.fingerprintConfiguredFromPamConfig(`
auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed
auth sufficient pam_fprintd.so
auth required pam_unix.so
`),
'polkit detects fingerprint even behind a clamshell gate'
)
assert(
!polkit.fingerprintConfiguredFromPamConfig(`
account include system-auth
auth include system-auth
auth sufficient pam_fprintd.so
auth required pam_unix.so
`),
'polkit detects password-first PAM config'
'polkit reports no fingerprint when pam_fprintd is absent'
)
JS