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>
109 lines
3.6 KiB
QML
109 lines
3.6 KiB
QML
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
|
|
}
|
|
}
|