Files
homelab/dotfiles/quickshell/HyprChrome/Widgets/Polkit/PolkitPromptContent.qml
T
darmanandClaude Opus 5 b3c3cc38f0 feat(quickshell): give the prompt its own panel, ESC handling and placement
PolkitPanel replaces BarPanel as the dialog's chrome. Deliberately not a
subclass or a fork: most of BarPanel is density machinery — summary slot,
animated height, state pair, transitions — that a modal never uses, and
inheriting it would tie the dialog's look to a component whose real job is the
rail, so every restyle here would have to be justified against the panels up
there. It keeps the shell's silhouette (cut corners with detached accent caps,
the accent rules, the header strip) and drops the rail's tick decoration.

ESC closes the rail. The bar had no keyboard focus at all, so this adds it,
gated by the shell rather than left to the compositor to arbitrate between two
exclusive surfaces — that resolves by stacking and would invert silently the
day the layers change:

    grabsKeyboard: shell.expanded && !polkit.prompting

so ESC dismisses the prompt while one is open and closes the rail afterwards.
Verified by instrumenting the handoff: expanded -> true, prompt open -> false,
prompt dismissed -> true, collapsed -> false. Note the rail now takes EXCLUSIVE
keyboard focus while expanded, which is the cost of answering a keypress the
user has not aimed at anything.

The dialog sits a third of the way down rather than centred, panel centre on
the third so it grows symmetrically as the message wraps, floored at a margin
so a tall prompt on a short output cannot be pushed off the top.

Also carries the backdrop tuning: dim 0.75 -> 0.65, gridOpacity 0.15 -> 0.10,
crossOpacity 0.45 -> 0.15, now that the scrim is used by the prompt as well as
the rail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAq2kKCLazZmrKvkd3akud
2026-09-01 23:38:23 +02:00

258 lines
9.3 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.HyprChrome.Theme
// 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<Identity*> 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
}
PolkitPanel {
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
// The body 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
}
}
}
}
}