Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions shell/plugins/lock/LockView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion shell/plugins/lock/Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down Expand Up @@ -128,6 +130,7 @@ Item {
pendingPassword = ""
failureMessage = ""
failedAttempts = 0
fingerprintMessage = ""
authenticatingPassword = false
fingerprintAuthenticating = false
fingerprintRetryTimer.stop()
Expand All @@ -143,6 +146,7 @@ Item {

resetAuthenticationState()
lockRequested = true
fingerprintWakeUsed = false
armBlankTimer()
logEvent("lock-requested")
queueSessionLock()
Expand Down Expand Up @@ -248,6 +252,7 @@ Item {
if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
if (fingerprintPam.active || fingerprintAuthenticating) return

fingerprintMessage = ""
fingerprintAuthenticating = true
if (!fingerprintPam.start()) {
fingerprintAuthenticating = false
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -341,6 +347,7 @@ Item {
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
fingerprintConfigured: root.fingerprintConfigured
fingerprintMessage: root.fingerprintMessage
authenticatingPassword: false
failureMessage: ""
failedAttempts: 0
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down
25 changes: 24 additions & 1 deletion shell/plugins/polkit/PolkitAgent.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions test/shell.d/lock-fingerprint-message-test.sh
Original file line number Diff line number Diff line change
@@ -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