260 lines
9.6 KiB
QML
260 lines
9.6 KiB
QML
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import QtQuick.Effects
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Polkit
|
|
import Quickshell.Wayland
|
|
import qs.Commons
|
|
import qs.Widgets
|
|
import qs.Services.UI
|
|
|
|
PanelWindow {
|
|
id: polkitWindow
|
|
|
|
property AuthFlow flow
|
|
property var pluginApi
|
|
property string resolvedMessage: ""
|
|
property var transientMatch: null
|
|
|
|
Connections {
|
|
target: flow
|
|
function onFailedChanged() {
|
|
if (flow && flow.failed) {
|
|
ToastService.showError(
|
|
pluginApi ? pluginApi.tr("error.failed.title") : "Authentication Failed",
|
|
pluginApi ? pluginApi.tr("error.failed.message") : "The password you entered was incorrect."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve transient service names when flow changes
|
|
onFlowChanged: {
|
|
resolveTransientServiceName(flow.message);
|
|
}
|
|
|
|
// Resolve transient service names (run-PID-random.service) to actual command
|
|
Process {
|
|
id: cmdLineProcess
|
|
command: ["cat", "/proc/0/cmdline"]
|
|
running: false
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: function() {
|
|
var args = this.text.split(String.fromCharCode(0)).filter(function(s) { return s.length > 0; });
|
|
Logger.i("polkit-agent: Process output length=" + this.text.length + ", args=" + args.length);
|
|
if (args.length > 0 && polkitWindow.transientMatch) {
|
|
var resolvedCmd = args.join(' ');
|
|
var isCommand = args.length > 1 || args[0].includes('/');
|
|
polkitWindow.resolvedMessage = polkitWindow.resolvedMessage.replace(polkitWindow.transientMatch[0], resolvedCmd);
|
|
if (isCommand) {
|
|
polkitWindow.resolvedMessage = polkitWindow.resolvedMessage.replace(/transient unit/i, 'run command');
|
|
}
|
|
Logger.i("polkit-agent: Resolved message: " + polkitWindow.resolvedMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveTransientServiceName(message) {
|
|
if (!message) return;
|
|
var match = message.match(/run-p?(\d+)-[^.]+\.service/);
|
|
if (!match) {
|
|
Logger.i("polkit-agent: No transient service pattern in message: " + message);
|
|
polkitWindow.resolvedMessage = message;
|
|
polkitWindow.transientMatch = null;
|
|
return;
|
|
}
|
|
var pid = match[1];
|
|
Logger.i("polkit-agent: Found transient service pattern, PID=" + pid + ", message=" + message);
|
|
polkitWindow.resolvedMessage = message;
|
|
polkitWindow.transientMatch = match;
|
|
cmdLineProcess.command = ["cat", "/proc/" + pid + "/cmdline"];
|
|
cmdLineProcess.running = true;
|
|
}
|
|
|
|
// Layer above everything else (critical system prompt)
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
readonly property real shadowPadding: Style.shadowBlurMax + Style.marginL
|
|
|
|
// Explicit size - include shadowPadding so the shadow isn't clipped at window corners
|
|
implicitWidth: 400 * Style.uiScaleRatio + shadowPadding * 2
|
|
implicitHeight: contentLayout.implicitHeight + (Style.marginL * 2) + shadowPadding * 2
|
|
|
|
color: "transparent"
|
|
|
|
Item {
|
|
id: contentContainer
|
|
anchors.fill: parent
|
|
anchors.margins: shadowPadding
|
|
focus: true
|
|
|
|
Keys.onPressed: function(event) {
|
|
if (!flow) return;
|
|
|
|
if (Keybinds.checkKey(event, "escape", Settings)) {
|
|
flow.cancelAuthenticationRequest();
|
|
event.accepted = true;
|
|
} else if (Keybinds.checkKey(event, "enter", Settings)) {
|
|
if (passwordInput.text !== "") {
|
|
flow.submit(passwordInput.text);
|
|
passwordInput.text = "";
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
|
|
transform: Translate {
|
|
id: shakeTranslate
|
|
x: 0
|
|
}
|
|
|
|
// Error animation
|
|
SequentialAnimation {
|
|
id: errorShake
|
|
running: flow && flow.failed
|
|
loops: 1
|
|
|
|
NumberAnimation { target: shakeTranslate; property: "x"; from: 0; to: -10; duration: 50; easing.type: Easing.InOutQuad }
|
|
NumberAnimation { target: shakeTranslate; property: "x"; from: -10; to: 10; duration: 50; easing.type: Easing.InOutQuad }
|
|
NumberAnimation { target: shakeTranslate; property: "x"; from: 10; to: -10; duration: 50; easing.type: Easing.InOutQuad }
|
|
NumberAnimation { target: shakeTranslate; property: "x"; from: -10; to: 10; duration: 50; easing.type: Easing.InOutQuad }
|
|
NumberAnimation { target: shakeTranslate; property: "x"; from: 10; to: 0; duration: 50; easing.type: Easing.InOutQuad }
|
|
}
|
|
|
|
// Shadow effect (behind background)
|
|
NDropShadow {
|
|
anchors.fill: customBackground
|
|
source: customBackground
|
|
autoPaddingEnabled: true
|
|
z: -1
|
|
}
|
|
|
|
Rectangle {
|
|
id: customBackground
|
|
anchors.fill: parent
|
|
radius: Style.radiusL
|
|
color: Qt.alpha(Color.mSurface, 0.95)
|
|
border.color: (flow && (flow.failed || flow.supplementaryIsError)) ? Color.mError : Color.mOutline
|
|
border.width: Style.borderS
|
|
|
|
Behavior on border.color {
|
|
ColorAnimation { duration: Style.animationFast }
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: contentLayout
|
|
anchors.centerIn: parent
|
|
width: parent.width - (Style.marginL * 2)
|
|
spacing: Style.marginM
|
|
|
|
// Header with Icon
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginM
|
|
|
|
NImageRounded {
|
|
Layout.preferredWidth: Style.fontSizeXXL * 2
|
|
Layout.preferredHeight: Style.fontSizeXXL * 2
|
|
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) || ((flow && flow.iconName) ? Quickshell.iconPath(flow.iconName) : "")
|
|
fallbackIcon: "lock"
|
|
borderWidth: 0
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginXS
|
|
|
|
NText {
|
|
text: polkitWindow.resolvedMessage || (flow ? flow.message : (pluginApi ? pluginApi.tr("window.title") : "Authentication Required"))
|
|
pointSize: Style.fontSizeL
|
|
font.weight: Style.fontWeightBold
|
|
color: Color.mOnSurface
|
|
wrapMode: Text.Wrap
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NText {
|
|
text: flow ? flow.actionId : ""
|
|
pointSize: Style.fontSizeXS
|
|
color: Color.mOnSurfaceVariant
|
|
wrapMode: Text.Wrap
|
|
Layout.fillWidth: true
|
|
visible: text !== ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Supplementary Message (Error or prompt)
|
|
NText {
|
|
visible: flow && flow.supplementaryMessage !== ""
|
|
text: flow ? flow.supplementaryMessage : ""
|
|
pointSize: Style.fontSizeS
|
|
color: (flow && flow.supplementaryIsError) ? Color.mError : Color.mOnSurfaceVariant
|
|
wrapMode: Text.Wrap
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Input Field
|
|
NTextInput {
|
|
id: passwordInput
|
|
Layout.fillWidth: true
|
|
placeholderText: flow ? flow.inputPrompt : (pluginApi ? pluginApi.tr("prompt.password") : "Password")
|
|
label: (flow && flow.isResponseRequired) ? (pluginApi ? pluginApi.tr("prompt.password") : "Password") : ""
|
|
inputItem.echoMode: (flow && !flow.responseVisible) ? TextInput.Password : TextInput.Normal
|
|
visible: flow && flow.isResponseRequired
|
|
|
|
onAccepted: {
|
|
if (flow) {
|
|
flow.submit(passwordInput.text)
|
|
passwordInput.text = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actions
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.marginS
|
|
spacing: Style.marginM
|
|
|
|
Item { Layout.fillWidth: true } // Spacer
|
|
|
|
NButton {
|
|
text: pluginApi ? pluginApi.tr("action.cancel") : "Cancel"
|
|
backgroundColor: Color.mSurfaceVariant
|
|
textColor: Color.mOnSurfaceVariant
|
|
outlined: false
|
|
onClicked: {
|
|
if (flow) flow.cancelAuthenticationRequest()
|
|
}
|
|
}
|
|
|
|
NButton {
|
|
text: pluginApi ? pluginApi.tr("action.authenticate") : "Authenticate"
|
|
backgroundColor: Color.mPrimary
|
|
textColor: Color.mOnPrimary
|
|
enabled: flow && flow.isResponseRequired
|
|
onClicked: {
|
|
if (flow) {
|
|
flow.submit(passwordInput.text)
|
|
passwordInput.text = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Focus handling
|
|
Component.onCompleted: {
|
|
passwordInput.inputItem.forceActiveFocus()
|
|
}
|
|
}
|