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:
co-authored by
Claude Opus 4.8
parent
39ca9135c4
commit
540e411edf
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user