Merge branch 'fingerprint-lid-aware' into quattro
This commit is contained in:
@@ -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
|
||||
|
||||
Executable
+11
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <<EOF
|
||||
$polkit_gate
|
||||
auth sufficient pam_fprintd.so
|
||||
auth required pam_unix.so
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
echo "Gate polkit fingerprint auth behind the lid state (password when the lid is shut)"
|
||||
|
||||
# Existing fingerprint setups have pam_fprintd first in /etc/pam.d/polkit-1 but
|
||||
# no lid gate, so a closed-lid pkexec would block on the unreachable reader for
|
||||
# the full pam_fprintd timeout before offering the password. Insert a pam_exec
|
||||
# gate before pam_fprintd that skips fingerprint while the lid is closed. New
|
||||
# setups already get this from omarchy-setup-security-fingerprint.
|
||||
#
|
||||
# The gate points at the fixed /usr/bin path the omarchy package always
|
||||
# provides, so it keeps working across package installs and dev-link (which
|
||||
# overlays $OMARCHY_PATH but leaves /usr/bin untouched). pam_exec needs a
|
||||
# literal absolute path — it does not expand env vars.
|
||||
|
||||
polkit_pam="/etc/pam.d/polkit-1"
|
||||
gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed"
|
||||
|
||||
if [[ -f $polkit_pam ]] &&
|
||||
grep -q 'pam_fprintd\.so' "$polkit_pam" &&
|
||||
! grep -q 'omarchy-hw-laptop-closed' "$polkit_pam"; then
|
||||
sudo sed -i "/pam_fprintd\.so/i $gate" "$polkit_pam"
|
||||
fi
|
||||
@@ -24,6 +24,9 @@ Item {
|
||||
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
|
||||
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
|
||||
readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19)
|
||||
// Space to keep clear on each side of the field for the fingerprint icon
|
||||
// (icon width plus a gap) so the centered dots never run under it.
|
||||
readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
|
||||
// Shrink the dots to fit once the password outgrows the field, so every
|
||||
// keystroke stays visible — otherwise long passwords clip with no feedback.
|
||||
readonly property real passwordDotScale: dotMetrics.advanceWidth > 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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