Merge branch 'fingerprint-lid-aware' into quattro
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user