import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Services.Polkit import "../config" as Cfg Item { id: root readonly property var t: Cfg.Config.theme readonly property var cfg: Cfg.Config property bool interactionAvailable: false property bool windowShouldExist: false PolkitAgent { id: agent onAuthenticationRequestStarted: { root.windowShouldExist = true root.interactionAvailable = true resetInput() } } readonly property var flow: agent.flow ?? null Connections { target: flow function onAuthenticationFailed() { root.interactionAvailable = true resetInput() } } function submitAuth() { if (!root.interactionAvailable) return if (flow?.isResponseRequired) { root.interactionAvailable = false flow.submit(windowLoader.item?.passwordText ?? "") windowLoader.item?.clearPassword() } } function resetInput() { windowLoader.item?.resetInput() } Loader { id: windowLoader active: root.windowShouldExist sourceComponent: Component { PanelWindow { id: overlay property bool exiting: false property bool authFailed: false property var displayFlow: null function captureFlowSnapshot(flow) { if (!flow) return null return { message: flow.message, actionId: flow.actionId, supplementaryMessage: flow.supplementaryMessage, supplementaryIsError: flow.supplementaryIsError, identities: flow.identities?.slice() ?? [], selectedIdentity: flow.selectedIdentity, isResponseRequired: flow.isResponseRequired, inputPrompt: flow.inputPrompt, responseVisible: flow.responseVisible } } readonly property string passwordText: passwordInput.text function clearPassword() { passwordInput.clear() } function resetInput() { passwordInput.clear() passwordInput.forceActiveFocus() } visible: true anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.namespace: "main-shell-polkit" Timer { id: exitDebounce interval: 250 onTriggered: { if (!agent.isActive && !overlay.exiting) { overlay.exiting = true exitAnim.start() } } } function cancelAndClose() { if (overlay.exiting) return overlay.exiting = true exitAnim.start() root.flow?.cancelAuthenticationRequest() } Rectangle { anchors.fill: parent color: t.polkitOverlayBg } component DialogButton: Rectangle { property string label property int labelWeight: Font.Normal property color bgColor property color bgHoverColor property color borderColor property color borderHoverColor property color labelColor signal tapped() width: (parent.width - 12) / 2 height: 44 radius: 10 HoverHandler { id: hover } color: hover.hovered ? bgHoverColor : bgColor border.color: hover.hovered ? borderHoverColor : borderColor border.width: 1 Behavior on color { ColorAnimation { duration: 200 } } Behavior on border.color { ColorAnimation { duration: 200 } } scale: pressHandler.pressed ? 0.97 : 1.0 Behavior on scale { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } } TapHandler { id: pressHandler cursorShape: Qt.PointingHandCursor onTapped: parent.tapped() } Text { anchors.centerIn: parent text: label color: labelColor font.pixelSize: 13 font.weight: labelWeight font.letterSpacing: 0.2 } } Rectangle { id: card property real shakeX: 0 x: Math.round((parent.width - width) / 2) + shakeX y: Math.round((parent.height - height) / 2) width: 440 height: content.implicitHeight + 64 radius: cfg.popupRadius color: t.polkitCardBg border.color: t.polkitCardBorder border.width: 1 opacity: 0 scale: 0.86 Component.onCompleted: { overlay.displayFlow = agent.flow enterAnim.start() } ParallelAnimation { id: enterAnim NumberAnimation { target: card; property: "opacity" from: 0; to: 1 duration: 260; easing.type: Easing.OutCubic } NumberAnimation { target: card; property: "scale" from: 0.86; to: 1.0 duration: 360; easing.type: Easing.OutBack; easing.overshoot: 0.6 } } ParallelAnimation { id: exitAnim NumberAnimation { target: card; property: "opacity" from: 1; to: 0 duration: 200; easing.type: Easing.InCubic } NumberAnimation { target: card; property: "scale" from: 1.0; to: 0.88 duration: 260; easing.type: Easing.InBack } onFinished: { overlay.exiting = false root.windowShouldExist = false } } Connections { target: agent function onIsActiveChanged() { if (agent.isActive) { overlay.authFailed = false overlay.displayFlow = agent.flow exitDebounce.stop() if (overlay.exiting) { exitAnim.stop() overlay.exiting = false } card.opacity = 0 card.scale = 0.86 enterAnim.restart() } else { if (overlay.displayFlow) overlay.displayFlow = captureFlowSnapshot(overlay.displayFlow) exitDebounce.restart() } } } Connections { target: flow function onAuthenticationFailed() { overlay.authFailed = true shakeAnim.restart() } } SequentialAnimation { id: shakeAnim NumberAnimation { target: card; property: "shakeX"; to: 12; duration: 45; easing.type: Easing.InOutQuad } NumberAnimation { target: card; property: "shakeX"; to: -10; duration: 45 } NumberAnimation { target: card; property: "shakeX"; to: 8; duration: 45 } NumberAnimation { target: card; property: "shakeX"; to: -5; duration: 45 } NumberAnimation { target: card; property: "shakeX"; to: 3; duration: 45 } NumberAnimation { target: card; property: "shakeX"; to: 0; duration: 45 } } Column { id: content anchors { top: parent.top left: parent.left right: parent.right topMargin: 32 leftMargin: 32 rightMargin: 32 } spacing: 16 Item { width: parent.width height: 72 Rectangle { anchors.centerIn: parent width: 66 height: 66 radius: 33 color: t.polkitIconBg border.color: t.polkitIconBorder border.width: 1 Text { anchors.centerIn: parent text: "󰌆" font.pixelSize: 28 color: t.polkitIconColor Component.onCompleted: { if (implicitWidth < 4) text = "🔒" } } } } Text { width: parent.width horizontalAlignment: Text.AlignHCenter text: "Authentication Required" color: t.polkitTitle font.pixelSize: 17 font.weight: Font.DemiBold font.letterSpacing: 0.3 } Text { visible: !!displayFlow?.message width: parent.width wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: displayFlow?.message ?? "" color: t.polkitMessage font.pixelSize: 13 lineHeight: 1.50 } Item { visible: !!displayFlow?.actionId width: parent.width height: pillBg.height Rectangle { id: pillBg anchors.horizontalCenter: parent.horizontalCenter width: Math.min(pillText.implicitWidth + 22, parent.width) height: pillText.implicitHeight + 10 radius: 6 color: t.polkitPillBg Text { id: pillText anchors.centerIn: parent width: parent.width - 22 elide: Text.ElideMiddle text: displayFlow?.actionId ?? "" color: t.polkitPillText font.pixelSize: 11 font.family: "monospace" } } } Rectangle { width: parent.width height: 1 color: t.polkitDivider } Rectangle { visible: overlay.authFailed || !!displayFlow?.supplementaryMessage width: parent.width height: suppRow.implicitHeight + 20 radius: 10 color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrBg : t.polkitSuppOkBg border.color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrBorder : t.polkitSuppOkBorder border.width: 1 Row { id: suppRow anchors { top: parent.top; topMargin: 10 bottom: parent.bottom; bottomMargin: 10 left: parent.left; leftMargin: 14 right: parent.right; rightMargin: 14 } spacing: 9 Text { anchors.top: parent.top text: "✕" font.pixelSize: 12 font.weight: Font.Medium lineHeight: 1.8 color: t.polkitSuppErrText } Text { width: parent.width - 22 wrapMode: Text.WordWrap text: overlay.authFailed ? "Authentication failed, please try again." : (displayFlow?.supplementaryMessage ?? "") font.pixelSize: 12 lineHeight: 1.45 color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrText : t.polkitSuppOkText } } } Text { visible: true width: parent.width text: displayFlow?.inputPrompt ?? "Password" color: t.polkitInputPrompt font.pixelSize: 12 } Rectangle { visible: true width: parent.width height: 46 radius: 10 color: t.polkitFieldBg border.color: passwordInput.activeFocus ? t.polkitFieldBorderFocus : t.polkitFieldBorder border.width: passwordInput.activeFocus ? 1.5 : 1 Behavior on border.color { ColorAnimation { duration: 120 } } Behavior on border.width { NumberAnimation { duration: 120 } } TextInput { id: passwordInput enabled: root.interactionAvailable anchors { left: parent.left; leftMargin: 16 right: eyeBtn.left; rightMargin: 8 verticalCenter: parent.verticalCenter } property bool showPassword: false echoMode: (displayFlow?.responseVisible || showPassword) ? TextInput.Normal : TextInput.Password color: t.polkitInputText selectionColor: t.polkitInputSelection selectedTextColor: t.polkitInputSelectedText font.pixelSize: 14 passwordCharacter: "•" clip: true Keys.onReturnPressed: root.submitAuth() Keys.onEnterPressed: root.submitAuth() Keys.onEscapePressed: overlay.cancelAndClose() } Text { id: eyeBtn anchors { right: parent.right; rightMargin: 14 verticalCenter: parent.verticalCenter } text: passwordInput.showPassword ? "🙈" : "👁" font.pixelSize: 20 color: eyeHover.hovered ? t.polkitEyeIconHover : t.polkitEyeIcon Behavior on color { ColorAnimation { duration: 120 } } HoverHandler { id: eyeHover } MouseArea { anchors.fill: parent anchors.margins: -8 cursorShape: Qt.PointingHandCursor onClicked: passwordInput.showPassword = !passwordInput.showPassword } } } Item { width: 1; height: 2 } Row { width: parent.width spacing: 12 layoutDirection: Qt.RightToLeft DialogButton { label: "Authenticate" labelWeight: Font.Medium labelColor: t.polkitAuthText bgColor: t.polkitAuthBg bgHoverColor: t.polkitAuthBgHover borderColor: t.polkitAuthBg borderHoverColor: t.polkitAuthBgHover onTapped: root.submitAuth() } DialogButton { label: "Cancel" labelColor: t.polkitCancelText bgColor: t.polkitCancelBg bgHoverColor: t.polkitCancelBgHover borderColor: t.polkitCancelBorder borderHoverColor: t.polkitCancelBgHover onTapped: overlay.cancelAndClose() } } } Rectangle { anchors.fill: parent radius: cfg.popupRadius color: Qt.rgba( t.polkitCardBg.r, t.polkitCardBg.g, t.polkitCardBg.b, 0.78) opacity: root.interactionAvailable ? 0 : 1 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: 180; easing.type: Easing.InOutSine } } Canvas { id: spinnerCanvas anchors.centerIn: parent width: 40 height: 40 property real angle: 0 NumberAnimation on angle { running: !root.interactionAvailable from: 0 to: 360 duration: 700 loops: Animation.Infinite } onAngleChanged: requestPaint() onPaint: { const ctx = getContext("2d") ctx.clearRect(0, 0, width, height) const cx = width / 2, cy = height / 2 const r = width / 2 - 3 ctx.strokeStyle = Qt.rgba( t.polkitTitle.r, t.polkitTitle.g, t.polkitTitle.b, 0.12) ctx.lineWidth = 3.5 ctx.beginPath() ctx.arc(cx, cy, r, 0, 2 * Math.PI) ctx.stroke() const start = (angle - 90) * Math.PI / 180 ctx.strokeStyle = Qt.rgba( t.polkitTitle.r, t.polkitTitle.g, t.polkitTitle.b, 0.88) ctx.lineCap = "round" ctx.beginPath() ctx.arc(cx, cy, r, start, start + 1.3 * Math.PI) ctx.stroke() } } } } } } } }