From 6d88ae46fe0ac3d27ea2d66a0dfaea7fb066400b Mon Sep 17 00:00:00 2001 From: Toni Bergholm Date: Tue, 15 Sep 2026 17:59:50 +0300 Subject: [PATCH] Show fingerprint reader status in authentication dialogs --- shell/plugins/lock/LockView.qml | 16 +++++++++ shell/plugins/lock/Service.qml | 19 +++++++++- shell/plugins/polkit/PolkitAgent.qml | 25 ++++++++++++- test/shell.d/lock-fingerprint-message-test.sh | 36 +++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 test/shell.d/lock-fingerprint-message-test.sh diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index e7a430ac67f..0575cf827eb 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -8,6 +8,7 @@ Item { property string backgroundPath: "" property int backgroundVersion: 0 + property string fingerprintMessage: "" property bool fingerprintConfigured: false property bool authenticatingPassword: false property string failureMessage: "" @@ -121,6 +122,21 @@ Item { onPositionChanged: root.wakeRequested() } + Text { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.verticalCenter + anchors.topMargin: root.fieldHeight / 2 + 16 + width: Math.min(parent.width - 32, root.fieldWidth + 100) + visible: root.fingerprintConfigured + text: root.fingerprintMessage || "Preparing fingerprint reader…" + textFormat: Text.PlainText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Color.lock.text + font.family: Style.font.family + font.pixelSize: Math.round(root.fieldFontSize * 0.65) + } + BorderSurface { id: inputField width: root.fieldWidth diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index 94d43b68ebd..aa379a4440a 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -21,6 +21,8 @@ Item { property bool authenticatingPassword: false property bool fingerprintAuthenticating: false property bool passwordPamConfigured: false + property string fingerprintMessage: "" + property bool fingerprintWakeUsed: false property bool fingerprintConfigured: false property bool previewVisible: false property string enteredPassword: "" @@ -128,6 +130,7 @@ Item { pendingPassword = "" failureMessage = "" failedAttempts = 0 + fingerprintMessage = "" authenticatingPassword = false fingerprintAuthenticating = false fingerprintRetryTimer.stop() @@ -143,6 +146,7 @@ Item { resetAuthenticationState() lockRequested = true + fingerprintWakeUsed = false armBlankTimer() logEvent("lock-requested") queueSessionLock() @@ -248,6 +252,7 @@ Item { if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return if (fingerprintPam.active || fingerprintAuthenticating) return + fingerprintMessage = "" fingerprintAuthenticating = true if (!fingerprintPam.start()) { fingerprintAuthenticating = false @@ -309,6 +314,7 @@ Item { backgroundPath: root.backgroundPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured + fingerprintMessage: root.fingerprintMessage authenticatingPassword: root.authenticatingPassword failureMessage: root.failureMessage failedAttempts: root.failedAttempts @@ -341,6 +347,7 @@ Item { backgroundPath: root.backgroundPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured + fingerprintMessage: root.fingerprintMessage authenticatingPassword: false failureMessage: "" failedAttempts: 0 @@ -381,6 +388,16 @@ Item { PamContext { id: fingerprintPam + onPamMessage: { + root.fingerprintMessage = message + // Wake once for the first informational message. Later retries must not + // repeatedly wake an unattended laptop. No translated text matching. + if (root.lockRequested && !root.fingerprintWakeUsed && !messageIsError + && !responseRequired && message.length > 0) { + root.fingerprintWakeUsed = true + root.runWake() + } + } config: "omarchy-lock-fingerprint" user: root.userName @@ -418,7 +435,7 @@ Item { Process { id: fingerprintCheckProc - command: ["bash", "-c", "if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]] && command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"$USER\" 2>/dev/null | grep -qi finger; then echo yes; else echo no; fi"] + command: ["bash", "-c", "if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]] && command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"$USER\" 2>/dev/null | grep -Eq '^[[:space:]]*-[[:space:]]*#[0-9]+:'; then echo yes; else echo no; fi"] stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true } onExited: { root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes" diff --git a/shell/plugins/polkit/PolkitAgent.qml b/shell/plugins/polkit/PolkitAgent.qml index 8786eeebc1b..a121cf97a64 100644 --- a/shell/plugins/polkit/PolkitAgent.qml +++ b/shell/plugins/polkit/PolkitAgent.qml @@ -269,7 +269,7 @@ Item { } // Fingerprint mode shows just the sensor icon, centered and alone \u2014 no - // padlock, no field, no prompt text. + // padlock or field. Reader status appears below the card. OpticalGlyph { anchors.centerIn: parent width: Math.round(root.fieldHeight * 0.7) @@ -364,6 +364,29 @@ Item { } } + Rectangle { + visible: root.fingerprintMode + width: Math.min(Style.space(360), panel.width - Style.gapsOut * 2) + height: readinessText.implicitHeight + Style.space(16) + anchors.horizontalCenter: card.horizontalCenter + anchors.top: card.bottom + anchors.topMargin: Style.space(10) + radius: root.cornerRadius + color: root.background + Text { + id: readinessText + anchors.centerIn: parent + width: parent.width - Style.space(24) + text: root.currentSupplementary || "Preparing fingerprint reader…" + textFormat: Text.PlainText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + Rectangle { width: Math.min(justificationText.implicitWidth + Style.space(24), panel.width - Style.gapsOut * 2) height: Style.space(28) diff --git a/test/shell.d/lock-fingerprint-message-test.sh b/test/shell.d/lock-fingerprint-message-test.sh new file mode 100644 index 00000000000..38979968fd9 --- /dev/null +++ b/test/shell.d/lock-fingerprint-message-test.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') +const vm = require('vm') +const source = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8') +const handler = source.match(/id: fingerprintPam\s+onPamMessage: \{([\s\S]*?)\n \}\n config:/) +assert(handler, 'fingerprint PAM exposes a message handler') +let wakes = 0 +const state = { lockRequested: true, fingerprintWakeUsed: false, fingerprintMessage: '', runWake() { wakes++ } } +function message(text, error = false, response = false) { + vm.runInNewContext(handler[1], { root: state, message: text, messageIsError: error, responseRequired: response }) +} +message('') +message('Reader unavailable', true) +message('Password:', false, true) +assertEqual(wakes, 0, 'empty, error, and response prompts do not wake the screen') +message('Place your finger') +assertEqual(state.fingerprintMessage, 'Place your finger', 'PAM placement message reaches the lock view state') +assertEqual(wakes, 1, 'first informational prompt wakes the screen') +message('Try again', true) +message('Place your finger') +assertEqual(wakes, 1, 'retry prompts do not repeatedly wake an unattended screen') +state.lockRequested = false +state.fingerprintWakeUsed = false +message('Reader ready') +assertEqual(wakes, 1, 'late prompts after unlock do not wake the screen') +const reset = source.match(/function resetAuthenticationState\(\) \{([\s\S]*?)\n \}/) +const context = { fingerprintMessage: 'Previous attempt', fingerprintRetryTimer: { stop() {} }, passwordPam: { active: false }, fingerprintPam: { active: false } } +vm.runInNewContext(reset[1], context) +assertEqual(context.fingerprintMessage, '', 'authentication reset clears the previous reader message') +JS