import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Polkit import Quickshell.Wayland import qs.Commons import qs.Ui import "PolkitModel.js" as PolkitModel Item { id: root property string fontFamily: Style.font.menuFamily // Bound to the central [polkit] section in shell.toml via Color.qml. property color accent: Color.polkit.accent property color background: Color.polkit.background property color foreground: Color.polkit.text property color border: Color.polkit.border property color borderError: Color.polkit.borderError property var borderSpec: Border.surfaceSpec("polkit", errorFlash ? "border-error" : "border", errorFlash ? borderError : border, Math.max(1, Style.space(2)), "border-alpha") property color scrim: Color.polkit.scrim readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding property int fieldHeight: Math.max(Style.space(42), Style.spacing.controlHeight) property bool closing: false property bool submitted: false property string currentMessage: "" property string currentPrompt: "" property string currentSupplementary: "" property bool responseRequired: false property bool responseVisible: false property bool failed: false property bool errorFlash: 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 // 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 // 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) { fingerprintConfigured = PolkitModel.fingerprintConfiguredFromPamConfig(raw) } function refreshLidState() { if (!laptopClosedProc.running) laptopClosedProc.running = true } function resetSnapshot() { currentMessage = "" currentPrompt = "" currentSupplementary = "" responseRequired = false responseVisible = false failed = false errorFlash = false submitted = false passwordInput.text = "" } function syncFromFlow() { var flow = polkitAgent.flow if (!flow) return currentMessage = String(flow.message || "Authentication is needed...") currentPrompt = String(flow.inputPrompt || "") currentSupplementary = String(flow.supplementaryMessage || "") responseRequired = !!flow.isResponseRequired responseVisible = !!flow.responseVisible failed = !!flow.failed if (responseRequired) submitted = false } function beginFlow() { closeTimer.stop() closing = false submitted = false passwordInput.text = "" refreshLidState() syncFromFlow() Qt.callLater(refocus) } function refocus() { if (!dialogVisible) return // 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() } function submitResponse() { var flow = polkitAgent.flow if (!flow || !flow.isResponseRequired) return submitted = true errorFlash = false flow.submit(passwordInput.text) passwordInput.text = "" keyCatcher.forceActiveFocus() } function cancelRequest() { var flow = polkitAgent.flow passwordInput.text = "" submitted = false closing = true closeTimer.restart() if (flow) flow.cancelAuthenticationRequest() } function triggerFailureFeedback() { submitted = false errorFlash = true passwordInput.text = "" errorTimer.restart() shakeAnimation.restart() Qt.callLater(refocus) } Timer { id: closeTimer interval: 300 repeat: false onTriggered: { closing = false resetSnapshot() } } Timer { id: errorTimer interval: 1200 repeat: false onTriggered: root.errorFlash = false } SequentialAnimation { id: shakeAnimation NumberAnimation { target: root; property: "shakeOffset"; to: -8; duration: 35; easing.type: Easing.OutQuad } NumberAnimation { target: root; property: "shakeOffset"; to: 8; duration: 50; easing.type: Easing.InOutQuad } NumberAnimation { target: root; property: "shakeOffset"; to: 0; duration: 55; easing.type: Easing.OutQuad } } FileView { path: "/etc/pam.d/polkit-1" watchChanges: true printErrors: false onLoaded: root.loadPamConfig(text()) 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" onAuthenticationRequestStarted: root.beginFlow() onIsActiveChanged: { if (isActive) root.syncFromFlow() else if (!root.closing) root.resetSnapshot() } onIsRegisteredChanged: { if (isRegistered) console.log("omarchy polkit agent registered") else console.warn("omarchy polkit agent is not registered; another agent may be running") } } Connections { target: polkitAgent.flow function onIsResponseRequiredChanged() { root.syncFromFlow() if (!polkitAgent.flow || !polkitAgent.flow.isResponseRequired) passwordInput.text = "" Qt.callLater(root.refocus) } function onInputPromptChanged() { root.syncFromFlow() } function onResponseVisibleChanged() { root.syncFromFlow() } function onSupplementaryMessageChanged() { root.syncFromFlow() } function onFailedChanged() { root.syncFromFlow() } function onAuthenticationFailed() { root.syncFromFlow() root.triggerFailureFeedback() } function onAuthenticationSucceeded() { root.closing = true closeTimer.restart() } function onAuthenticationRequestCancelled() { root.closing = true closeTimer.restart() } } PanelWindow { id: panel visible: root.dialogVisible anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.namespace: "omarchy-polkit" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore Rectangle { anchors.fill: parent color: root.scrim } MouseArea { anchors.fill: parent onClicked: root.refocus() } BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent anchors.horizontalCenterOffset: root.shakeOffset color: root.background borderSpec: root.borderSpec padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: root.refocus() } Item { id: keyCatcher anchors.fill: parent focus: true Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { root.cancelRequest() event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (root.responseRequired) root.submitResponse() event.accepted = true } } } // 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 anchors.bottomMargin: card.contentBottomInset anchors.leftMargin: card.contentLeftInset spacing: Style.space(14) Text { text: "\uf023" color: root.errorFlash ? Color.polkit.textError : root.accent font.family: root.fontFamily font.pixelSize: Style.font.iconLarge width: Style.space(26) height: root.fieldHeight horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } Item { width: parent.width - Style.space(40) height: root.fieldHeight TextInput { id: passwordInput anchors.fill: parent verticalAlignment: TextInput.AlignVCenter activeFocusOnPress: true clip: true selectionColor: Util.alpha(root.accent, 0.45) selectedTextColor: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.iconLarge echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password passwordCharacter: "\u2022" color: root.errorFlash ? Color.polkit.textError : root.foreground cursorVisible: activeFocus && !root.submitted && !root.errorFlash readOnly: root.submitted || root.errorFlash enabled: root.dialogVisible onAccepted: root.submitResponse() Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { root.cancelRequest() event.accepted = true } } } Text { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: root.errorFlash ? "Wrong" : (root.submitted ? "Checking..." : "Enter password") color: root.errorFlash ? Color.polkit.textError : root.foreground opacity: root.errorFlash ? 1 : 0.36 font.family: root.fontFamily font.pixelSize: Style.font.iconLarge elide: Text.ElideRight visible: passwordInput.text.length === 0 } Rectangle { width: Math.max(1, Style.space(2)) height: Style.space(24) anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter color: root.errorFlash ? Color.polkit.textError : root.foreground visible: passwordInput.visible && passwordInput.activeFocus && passwordInput.text.length === 0 && !root.submitted && !root.errorFlash } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton enabled: passwordInput.visible onClicked: passwordInput.forceActiveFocus() } } } } Rectangle { width: Math.min(justificationText.implicitWidth + Style.space(24), panel.width - Style.gapsOut * 2) height: Style.space(28) anchors.horizontalCenter: card.horizontalCenter anchors.bottom: card.top anchors.bottomMargin: Style.space(10) radius: root.cornerRadius color: root.background Text { id: justificationText anchors.fill: parent anchors.leftMargin: Style.space(12) anchors.rightMargin: Style.space(12) text: root.authorizationLabel(root.currentMessage) color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideMiddle } } } }