pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import qs.HyprChrome.Theme import qs.HyprChrome.Widgets.Polkit // GUI password prompt for `sudo -A`, reusing the polkit dialog. // // sudo does NOT speak polkit — it is setuid + PAM reading your tty, and no // sudoers option bridges the two. What it does support is an ASKPASS helper: a // program it runs to obtain the password, which prints it on stdout. So this is // not the polkit agent serving sudo; it is a second, separate path that happens // to render the same dialog. // // Flow, driven by the helper in home.nix (`qs-askpass`): // // sudo -A // -> qs-askpass makes a 0600 fifo under XDG_RUNTIME_DIR // -> qs ipc call askpass prompt "" "" (returns at once) // -> this dialog opens, user types // -> a one-line writer is started here, secret written to its STDIN // -> qs-askpass reads the fifo and prints the secret on stdout // -> sudo reads it // // The secret travels on a pipe the whole way. It is never an argument and never // an environment variable, so it does not appear in /proc for any process — the // fifo PATH is in argv, which is not secret. It does cross more process // boundaries than the polkit path, where the password stays inside the PAM // conversation; that is the inherent cost of askpass, not of this design. // // Cancelling answers with an empty line, so the helper reads nothing, exits // non-zero, and sudo aborts rather than burning a retry on a blank password. Scope { id: root // Which output to appear on; the shell puts it on the focused monitor. property var screen: null // The fifo the helper is blocked reading. Non-empty means a request is in // flight, which is exactly what "a prompt is open" means here. property string fifoPath: "" property string promptText: "" property bool failed: false readonly property bool active: root.fifoPath !== "" // Held only between submit and the writer process actually starting: a // Process cannot be written to before it is running. property string pendingSecret: "" IpcHandler { target: "askpass" // Called by qs-askpass. Returns immediately — the helper blocks on the // fifo, not on this call, because an IpcHandler function runs on the // QML thread and blocking here would freeze the whole shell. function prompt(message: string, fifo: string): string { if (root.active) return "busy"; root.promptText = message === "" ? "Password:" : message; root.fifoPath = fifo; root.failed = false; return "ok"; } // So a helper that times out can take the dialog down with it rather // than leaving it on screen with nothing listening. function cancel(): string { root.dismiss(); return "ok"; } } // Cancelling answers with an EMPTY line rather than by closing silently: // the helper then reads zero bytes and exits non-zero, so sudo aborts // instead of spending a retry on a blank password. function dismiss() { root.respond(""); } function submit(secret) { root.respond(secret); } // The writer reads ONE LINE and exits; it does not wait for EOF. // // The obvious version — `cat > fifo`, write the secret, then close stdin by // setting stdinEnabled false — does not terminate. Measured: the secret // arrives intact but `cat` never sees EOF, so the fifo is never closed and // the helper blocks until its timeout. sudo would hang after you typed. // // A single `read` needs no EOF at all: the trailing newline ends it, the // shell writes what it got and exits, and THAT close is what gives the // helper its EOF. `IFS=` keeps leading and trailing whitespace, `-r` keeps // backslashes, and the secret still travels on stdin rather than in argv. function respond(secret) { if (!root.active) return; root.pendingSecret = secret + "\n"; writer.command = ["sh", "-c", "IFS= read -r line; printf %s \"$line\" > \"$1\"", "sh", root.fifoPath]; writer.running = true; root.fifoPath = ""; } // Opening a fifo for writing BLOCKS until a reader attaches, which is why // this is a subprocess rather than a FileView: the helper's `cat` is that // reader, and blocking the QML thread on it would freeze the shell. Process { id: writer stdinEnabled: true // Written on `started`, not at respond() time: a Process has no stdin // to write to until it is actually running. onStarted: { writer.write(root.pendingSecret); root.pendingSecret = ""; } } PanelWindow { id: win screen: root.screen visible: root.active WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore color: Theme.textAlpha(0) anchors { top: true left: true right: true bottom: true } // No click-off dismissal, for the same reason the polkit dialog has // none: something is blocked waiting on the answer, and losing it to a // stray click would leave sudo hanging with no visible cause. PolkitPromptContent { id: content anchors.horizontalCenter: parent.horizontalCenter y: Math.max(32, Math.round(parent.height / 3 - height / 2)) width: 520 // Deliberately the polkit dialog's own content component: this is a // password prompt with the same shape, and keeping one means a // restyle of PolkitPanel covers both. `identities` stays empty — // sudo offers no choice of who authenticates — which hides the // picker and the "AS" line on its own. message: "Authentication is required to run a command as another user" actionId: "sudo" iconName: "" showIcon: false identities: [] responseRequired: true inputPrompt: root.promptText responseVisible: false failed: root.failed onSubmitted: value => root.submit(value) onCancelled: root.dismiss() } onVisibleChanged: { if (win.visible) { content.clearResponse(); content.focusInput(); } } } }