From 540e411edfcda70acfd478fc3c2918600a344ab5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 23 Jul 2026 13:51:07 -0700 Subject: [PATCH] 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) --- bin/omarchy-hw-clamshell | 13 +-- bin/omarchy-hw-laptop-closed | 11 ++ bin/omarchy-remove-security-fingerprint | 6 +- bin/omarchy-setup-security-fingerprint | 30 ++++- migrations/1784818437.sh | 21 ++++ shell/plugins/lock/LockView.qml | 27 ++++- shell/plugins/lock/Service.qml | 37 ++++-- shell/plugins/polkit/PolkitAgent.qml | 59 +++++++--- shell/plugins/polkit/PolkitModel.js | 9 +- .../lock-fingerprint-indicator/shell.qml | 108 ++++++++++++++++++ .../lock-fingerprint-indicator-test.sh | 71 ++++++++++++ test/shell.d/polkit-test.sh | 18 ++- 12 files changed, 360 insertions(+), 50 deletions(-) create mode 100755 bin/omarchy-hw-laptop-closed create mode 100644 migrations/1784818437.sh create mode 100644 test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml create mode 100755 test/shell.d/lock-fingerprint-indicator-test.sh diff --git a/bin/omarchy-hw-clamshell b/bin/omarchy-hw-clamshell index 0792e6ec..238f6627 100755 --- a/bin/omarchy-hw-clamshell +++ b/bin/omarchy-hw-clamshell @@ -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 diff --git a/bin/omarchy-hw-laptop-closed b/bin/omarchy-hw-laptop-closed new file mode 100755 index 00000000..8e03f965 --- /dev/null +++ b/bin/omarchy-hw-laptop-closed @@ -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 diff --git a/bin/omarchy-remove-security-fingerprint b/bin/omarchy-remove-security-fingerprint index cdb54387..9bff3782 100755 --- a/bin/omarchy-remove-security-fingerprint +++ b/bin/omarchy-remove-security-fingerprint @@ -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 } diff --git a/bin/omarchy-setup-security-fingerprint b/bin/omarchy-setup-security-fingerprint index b4977c59..f56bf805 100755 --- a/bin/omarchy-setup-security-fingerprint +++ b/bin/omarchy-setup-security-fingerprint @@ -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 < 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 + } } } } diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index b15a0821..e2c9f20e 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -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 diff --git a/shell/plugins/polkit/PolkitAgent.qml b/shell/plugins/polkit/PolkitAgent.qml index a27ab019..8ce95973 100644 --- a/shell/plugins/polkit/PolkitAgent.qml +++ b/shell/plugins/polkit/PolkitAgent.qml @@ -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 { diff --git a/shell/plugins/polkit/PolkitModel.js b/shell/plugins/polkit/PolkitModel.js index ca2ea142..7ce71c78 100644 --- a/shell/plugins/polkit/PolkitModel.js +++ b/shell/plugins/polkit/PolkitModel.js @@ -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 } } diff --git a/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml b/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml new file mode 100644 index 00000000..1a5f92fe --- /dev/null +++ b/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml @@ -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 + } +} diff --git a/test/shell.d/lock-fingerprint-indicator-test.sh b/test/shell.d/lock-fingerprint-indicator-test.sh new file mode 100755 index 00000000..70ad7d54 --- /dev/null +++ b/test/shell.d/lock-fingerprint-indicator-test.sh @@ -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" diff --git a/test/shell.d/polkit-test.sh b/test/shell.d/polkit-test.sh index 2683e2b5..5283589d 100644 --- a/test/shell.d/polkit-test.sh +++ b/test/shell.d/polkit-test.sh @@ -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