pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets import qs.HyprChrome.Theme import qs.HyprChrome.Widgets.Bar.Panels // Headlessly renderable visual core of the polkit authentication prompt. // // Nothing here imports Quickshell.Services.Polkit: every field an AuthFlow // exposes arrives as a plain property and every action leaves as a signal, so // the whole dialog can be rendered offscreen (tools/quickshell-preview) and // staged in DebugWindow without a real authorization request. PolkitPrompt.qml // owns the agent and does the mapping. // // `identities` is read structurally — each entry only needs `displayName` — so // the adapter can hand over the flow's QList unchanged while the // headless test passes plain JS objects. Item { id: root // ---- flow state, mirrored ---- property string message: "" property string actionId: "" property string iconName: "" property bool showIcon: true // Who may authenticate. One entry is the common case and renders as a // plain line; the picker only appears when polkit actually offers a // choice (a user in several admin groups, or root plus wheel). property var identities: [] property int selectedIdentity: 0 // PAM conversation. `responseVisible` is polkit's echo flag — it is NOT // always false: a smartcard PIN prompt or a security-question stack asks // for echoed input, and masking those makes the prompt unusable. property bool responseRequired: false property string inputPrompt: "" property bool responseVisible: false // pam_info / pam_error text, and whether the last attempt was rejected. property string supplementaryMessage: "" property bool supplementaryIsError: false property bool failed: false property alias response: responseInput.text signal submitted(string value) signal cancelled signal identityRequested(int index) function focusInput() { responseInput.forceActiveFocus(); } function clearResponse() { responseInput.text = ""; } implicitWidth: 520 implicitHeight: panel.implicitHeight // Escape reaches here by propagating up the focus chain from the TextInput, // which does not consume it — so cancelling works whether or not the input // currently has focus. Keys.onEscapePressed: event => { root.cancelled(); event.accepted = true; } component MicroText: Text { color: Theme.muted font.family: Theme.microFont font.pixelSize: 8 font.letterSpacing: 0.9 elide: Text.ElideRight } BarPanel { id: panel width: root.width panelId: "PKT" title: "AUTHORIZATION REQUIRED" // The action id is the one piece that says WHAT is being authorized // independently of the (localizable, often vague) message. meta: root.actionId chamfer: 16 // A modal, not a rail panel: clicking the body must reach the input // rather than collapse the dialog out from under it. expanded: true toggleOnClick: false // Collapsed rendering is never shown here, but BarPanel keeps both // slots instantiated, so the summary stays bound to the same truth. summary: MicroText { text: root.inputPrompt color: Theme.text } // Sized like every other BarPanel body: the slot decides the width and // the layout's implicitHeight becomes its height, so a wrapped message // or an extra pam_info line grows the panel instead of being clipped. ColumnLayout { width: parent.width spacing: 10 // ---- what is being asked ---- RowLayout { Layout.fillWidth: true spacing: 10 IconImage { visible: root.showIcon && root.iconName !== "" implicitSize: 32 source: root.showIcon && root.iconName !== "" ? Quickshell.iconPath(root.iconName, "dialog-password") : "" } Text { Layout.fillWidth: true wrapMode: Text.Wrap text: root.message color: Theme.text font.family: Theme.displayFont font.pixelSize: 13 font.letterSpacing: 0.6 } } // ---- identity ---- // Single identity: stated, not offered. Several: chips, because a // combo box would be the only QtQuick.Controls widget in the rail. MicroText { Layout.fillWidth: true visible: root.identities.length === 1 text: "AS " + (root.identities.length === 1 ? root.identities[0].displayName : "") } Flow { Layout.fillWidth: true visible: root.identities.length > 1 spacing: 6 Repeater { model: root.identities Rectangle { id: chip required property int index required property var modelData readonly property bool current: chip.index === root.selectedIdentity width: chipLabel.implicitWidth + 14 height: chipLabel.implicitHeight + 8 color: chip.current ? Theme.accent : "transparent" border.width: 1 border.color: chip.current ? Theme.accent : Theme.disabled Text { id: chipLabel anchors.centerIn: parent text: chip.modelData.displayName color: chip.current ? Theme.surface : Theme.muted font.family: Theme.microFont font.pixelSize: 9 font.letterSpacing: 0.8 } MouseArea { anchors.fill: parent onClicked: root.identityRequested(chip.index) } } } } // ---- the conversation ---- Rectangle { Layout.fillWidth: true implicitHeight: 34 color: Theme.selection border.width: 1 border.color: root.failed ? Theme.hot : Theme.hair RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 10 Text { text: ">_" color: root.responseRequired ? Theme.accent : Theme.disabled font.family: Theme.displayFont font.pixelSize: 15 font.bold: true } TextInput { id: responseInput Layout.fillWidth: true Layout.fillHeight: true verticalAlignment: TextInput.AlignVCenter enabled: root.responseRequired color: Theme.text selectionColor: Theme.accent selectedTextColor: Theme.surface font.family: Theme.displayFont font.pixelSize: 14 font.letterSpacing: 1 clip: true echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password passwordCharacter: "▪" // Qt reveals the last typed character for a moment by // default. On a screen-visible layer-shell overlay // that is a shoulder-surfing hole, so: never. passwordMaskDelay: 0 onAccepted: { if (root.responseRequired) root.submitted(responseInput.text); } // Placeholder: TextInput has none of its own, and the // PAM prompt ("Password:", "PIN:") is the only label // this field gets. Text { anchors.verticalCenter: parent.verticalCenter visible: responseInput.text.length === 0 text: root.inputPrompt color: Theme.disabled font: responseInput.font } } } } // ---- pam_info / pam_error ---- Text { Layout.fillWidth: true visible: root.supplementaryMessage !== "" wrapMode: Text.Wrap text: root.supplementaryMessage color: root.supplementaryIsError ? Theme.hot : Theme.muted font.family: Theme.microFont font.pixelSize: 9 font.letterSpacing: 0.7 } // ---- key hints ---- RowLayout { Layout.fillWidth: true spacing: 14 MicroText { text: "ENTER AUTHENTICATE" } MicroText { text: "ESC CANCEL" } Item { Layout.fillWidth: true } MicroText { text: root.responseVisible ? "ECHO ON" : "" color: Theme.hot } } } } }