aurora38c75f1
aurora: native polkit prompt and bluetooth pairing agent
+453 −3
7 files changed
- M .config/hypr/hyprland.lua +1 −1
- A .config/quickshell/aurora/Agents.qml +71 −0
- A .config/quickshell/aurora/AuthPrompt.qml +231 −0
- A .config/quickshell/aurora/btagent.py +145 −0
- M .config/quickshell/aurora/shell.qml +3 −0
- M packages/desktop.txt +2 −0
- M packages/utils.txt +0 −2
| @@ -27,7 +27,7 @@ hl.on("hyprland.start", function() | |||
| 27 | 27 | hl.exec_cmd("hypridle") | |
| 28 | 28 | -- hl.exec_cmd("blueman-applet") | |
| 29 | 29 | hl.exec_cmd("hyprpaper") -- wallpaper is set by aurora (Wall.qml), it reads hyprpaper.conf | |
| 30 | − | hl.exec_cmd("systemctl --user start hyprpolkitagent") | |
| 30 | + | -- polkit agent: aurora (Agents.qml); it starts hyprpolkitagent itself if it can't register | |
| 31 | 31 | end) | |
| 32 | 32 | ||
| 33 | 33 | -------------------------------------------------- | |
| @@ -0,0 +1,71 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Io | |
| 5 | + | import Quickshell.Services.Polkit | |
| 6 | + | ||
| 7 | + | // Authentication agents, shown by AuthPrompt. | |
| 8 | + | // polkit Quickshell's native PolkitAgent (replaced hyprpolkitagent). Polkit allows one agent per | |
| 9 | + | // session: if registering fails (another agent running), hyprpolkitagent is started as a fallback. | |
| 10 | + | // bluez btagent.py (python-gobject), the default pairing agent: passkey confirmation, PIN/passkey | |
| 11 | + | // entry and display. JSON lines over its stdout/stdin, see the script. Restarted with backoff. | |
| 12 | + | Singleton { | |
| 13 | + | id: root | |
| 14 | + | ||
| 15 | + | readonly property var flow: agent.flow | |
| 16 | + | readonly property bool polkitActive: agent.isActive && agent.flow !== null | |
| 17 | + | readonly property bool registered: agent.isRegistered | |
| 18 | + | ||
| 19 | + | PolkitAgent { id: agent } | |
| 20 | + | ||
| 21 | + | // ---- bluetooth: the request being shown, {id, method, alias, passkey|pincode}; id 0 = display only | |
| 22 | + | property var bt: null | |
| 23 | + | readonly property bool btActive: bt !== null | |
| 24 | + | property bool btRegistered: false | |
| 25 | + | function btReply(ok, value) { | |
| 26 | + | if (bt && bt.id) btProc.write(JSON.stringify({ id: bt.id, ok: ok, value: value || "" }) + "\n") | |
| 27 | + | bt = null | |
| 28 | + | } | |
| 29 | + | function btLine(l) { | |
| 30 | + | var m | |
| 31 | + | try { m = JSON.parse(l) } catch (e) { return } | |
| 32 | + | if (m.event === "registered") { btRegistered = true; backoff = 2000 } | |
| 33 | + | else if (m.event === "cancel") bt = null | |
| 34 | + | else if (m.event === "error") console.warn("btagent:", m.msg) | |
| 35 | + | else if (m.method) { | |
| 36 | + | if (bt && bt.id) btReply(false) // a new request replaces an unanswered one | |
| 37 | + | bt = m | |
| 38 | + | if (!m.id) displayTimeout.restart() | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | property int backoff: 2000 | |
| 42 | + | Process { | |
| 43 | + | id: btProc | |
| 44 | + | command: ["python3", "-I", Quickshell.shellPath("btagent.py")] | |
| 45 | + | running: true | |
| 46 | + | stdinEnabled: true | |
| 47 | + | stdout: SplitParser { onRead: l => root.btLine(l) } | |
| 48 | + | stderr: SplitParser { onRead: l => console.warn("btagent:", l) } | |
| 49 | + | onExited: { | |
| 50 | + | root.bt = null | |
| 51 | + | root.btRegistered = false | |
| 52 | + | restartBt.interval = root.backoff | |
| 53 | + | root.backoff = Math.min(root.backoff * 2, 60000) | |
| 54 | + | restartBt.start() | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | Timer { id: restartBt; onTriggered: btProc.running = true } | |
| 58 | + | // a shown passkey/PIN has no answer: drop it after a while (pairing is done or failed by then) | |
| 59 | + | Timer { id: displayTimeout; interval: 30000; onTriggered: if (root.bt && !root.bt.id) root.bt = null } | |
| 60 | + | ||
| 61 | + | Timer { | |
| 62 | + | interval: 5000 | |
| 63 | + | running: true | |
| 64 | + | onTriggered: { | |
| 65 | + | if (agent.isRegistered) return | |
| 66 | + | Quickshell.execDetached(["notify-send", "-a", "aurora", "-i", "dialog-password", "polkit agent", | |
| 67 | + | "aurora couldn't register as the polkit agent, starting hyprpolkitagent instead"]) | |
| 68 | + | Quickshell.execDetached(["systemctl", "--user", "start", "hyprpolkitagent"]) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | } | |
| @@ -0,0 +1,231 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Wayland | |
| 5 | + | ||
| 6 | + | // Prompt for the auth agents (Agents): a polkit password (Agents.flow) or a bluetooth pairing request | |
| 7 | + | // (Agents.bt: confirm a passkey, type a PIN/passkey, or show one to type on the device). Polkit wins | |
| 8 | + | // if both are pending. Overlay layer with exclusive keyboard on the focused monitor; Enter accepts, | |
| 9 | + | // Esc cancels. Typed text is cleared after every attempt, never logged. | |
| 10 | + | PanelWindow { | |
| 11 | + | id: win | |
| 12 | + | readonly property var flow: Agents.flow | |
| 13 | + | readonly property var bt: Agents.bt | |
| 14 | + | readonly property string mode: Agents.polkitActive ? "polkit" : (Agents.btActive ? "bt" : "") | |
| 15 | + | readonly property bool opened: mode !== "" | |
| 16 | + | readonly property string btMethod: bt ? bt.method : "" | |
| 17 | + | // bluetooth: what the field / big code shows | |
| 18 | + | readonly property bool btInput: btMethod === "RequestPinCode" || btMethod === "RequestPasskey" | |
| 19 | + | readonly property string btCode: !bt ? "" : (bt.passkey ? bt.passkey.slice(0, 3) + " " + bt.passkey.slice(3) : (bt.pincode || "")) | |
| 20 | + | readonly property bool btDisplay: btMethod === "DisplayPasskey" || btMethod === "DisplayPinCode" | |
| 21 | + | readonly property bool needsInput: mode === "polkit" || btInput | |
| 22 | + | ||
| 23 | + | visible: opened | |
| 24 | + | implicitWidth: 420 | |
| 25 | + | implicitHeight: col.implicitHeight + 32 | |
| 26 | + | exclusiveZone: 0 | |
| 27 | + | color: "transparent" | |
| 28 | + | WlrLayershell.layer: WlrLayer.Overlay | |
| 29 | + | WlrLayershell.namespace: "aurora-auth" | |
| 30 | + | WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None | |
| 31 | + | ||
| 32 | + | property bool waiting: false // submitted, polkit is checking | |
| 33 | + | property string error: "" // Quickshell doesn't put failures in supplementaryMessage | |
| 34 | + | function reset() { pass.text = ""; waiting = false; if (needsInput) pass.forceActiveFocus(); else keys.forceActiveFocus() } | |
| 35 | + | function accept() { | |
| 36 | + | if (mode === "polkit") { | |
| 37 | + | if (!flow || !flow.isResponseRequired || waiting) return | |
| 38 | + | waiting = true | |
| 39 | + | error = "" | |
| 40 | + | flow.submit(pass.text) | |
| 41 | + | pass.text = "" | |
| 42 | + | } else if (mode === "bt") { | |
| 43 | + | if (btInput && pass.text === "") return | |
| 44 | + | Agents.btReply(true, pass.text) | |
| 45 | + | pass.text = "" | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | function cancel() { | |
| 49 | + | if (mode === "polkit") { if (flow) flow.cancelAuthenticationRequest() } | |
| 50 | + | else if (mode === "bt") Agents.btReply(false) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | onModeChanged: if (opened) { error = ""; reset() } | |
| 54 | + | onBtChanged: if (bt) reset() | |
| 55 | + | Connections { | |
| 56 | + | target: win.flow | |
| 57 | + | function onIsResponseRequiredChanged() { if (win.flow.isResponseRequired) win.reset() } | |
| 58 | + | // polkit asks again right after a failure (isResponseRequired goes true with failed set) | |
| 59 | + | function onAuthenticationFailed() { win.error = "wrong password, try again"; win.reset() } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // keyboard for the button-only bluetooth prompts | |
| 63 | + | Item { | |
| 64 | + | id: keys | |
| 65 | + | Keys.onReturnPressed: win.accept() | |
| 66 | + | Keys.onEnterPressed: win.accept() | |
| 67 | + | Keys.onEscapePressed: win.cancel() | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | Rectangle { | |
| 71 | + | anchors.fill: parent | |
| 72 | + | color: Theme.bgA | |
| 73 | + | border.width: 2 | |
| 74 | + | border.color: win.error !== "" || (win.mode === "polkit" && win.flow && win.flow.supplementaryIsError) ? Theme.err : Theme.accent | |
| 75 | + | ||
| 76 | + | ColumnLayout { | |
| 77 | + | id: col | |
| 78 | + | anchors.left: parent.left | |
| 79 | + | anchors.right: parent.right | |
| 80 | + | anchors.top: parent.top | |
| 81 | + | anchors.margins: 16 | |
| 82 | + | spacing: 10 | |
| 83 | + | ||
| 84 | + | RowLayout { | |
| 85 | + | Layout.fillWidth: true | |
| 86 | + | spacing: 10 | |
| 87 | + | Text { | |
| 88 | + | text: win.mode === "bt" ? Theme.iBt : Theme.iLock | |
| 89 | + | color: Theme.accent | |
| 90 | + | font.family: Theme.font | |
| 91 | + | font.pixelSize: Theme.fontSize + 8 | |
| 92 | + | } | |
| 93 | + | SectionLabel { | |
| 94 | + | Layout.fillWidth: true | |
| 95 | + | text: win.mode === "bt" ? "bluetooth pairing" : "authentication required" | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | Text { | |
| 99 | + | Layout.fillWidth: true | |
| 100 | + | color: Theme.fg | |
| 101 | + | wrapMode: Text.Wrap | |
| 102 | + | font.family: Theme.font | |
| 103 | + | font.pixelSize: Theme.fontSize | |
| 104 | + | text: { | |
| 105 | + | if (win.mode === "polkit") return win.flow ? win.flow.message : "" | |
| 106 | + | if (!win.bt) return "" | |
| 107 | + | var a = win.bt.alias | |
| 108 | + | switch (win.btMethod) { | |
| 109 | + | case "RequestConfirmation": return "pair with " + a + "? check that it shows the same code:" | |
| 110 | + | case "RequestPinCode": return "enter the PIN for " + a + " (often 0000 or 1234)" | |
| 111 | + | case "RequestPasskey": return "enter the 6-digit passkey shown on " + a | |
| 112 | + | case "DisplayPasskey": | |
| 113 | + | case "DisplayPinCode": return "type this code on " + a + ", then press Enter there:" | |
| 114 | + | case "RequestAuthorization": return "allow " + a + " to pair with this computer?" | |
| 115 | + | case "AuthorizeService": return "allow " + a + " to use a service?" | |
| 116 | + | } | |
| 117 | + | return a | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | Text { | |
| 121 | + | Layout.fillWidth: true | |
| 122 | + | visible: text !== "" | |
| 123 | + | text: win.mode === "polkit" ? (win.flow ? win.flow.actionId : "") | |
| 124 | + | : (win.btMethod === "AuthorizeService" ? win.bt.uuid : "") | |
| 125 | + | color: Theme.dim | |
| 126 | + | elide: Text.ElideMiddle | |
| 127 | + | font.family: Theme.font | |
| 128 | + | font.pixelSize: Theme.fontSize - 2 | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // bluetooth passkey / PIN to compare or type on the device | |
| 132 | + | Text { | |
| 133 | + | Layout.fillWidth: true | |
| 134 | + | visible: win.mode === "bt" && win.btCode !== "" && !win.btInput | |
| 135 | + | horizontalAlignment: Text.AlignHCenter | |
| 136 | + | text: win.btCode | |
| 137 | + | color: Theme.accent | |
| 138 | + | font.family: Theme.font | |
| 139 | + | font.pixelSize: Theme.fontSize * 3 | |
| 140 | + | font.bold: true | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // more than one admin: pick who authenticates | |
| 144 | + | Flow { | |
| 145 | + | Layout.fillWidth: true | |
| 146 | + | visible: win.mode === "polkit" && win.flow !== null && win.flow.identities.length > 1 | |
| 147 | + | spacing: 4 | |
| 148 | + | Repeater { | |
| 149 | + | model: win.mode === "polkit" && win.flow ? win.flow.identities : [] | |
| 150 | + | delegate: IconBtn { | |
| 151 | + | required property var modelData | |
| 152 | + | implicitHeight: 24 | |
| 153 | + | label: modelData.displayName || String(modelData) | |
| 154 | + | active: win.flow && win.flow.selectedIdentity === modelData | |
| 155 | + | onClicked: win.flow.selectedIdentity = modelData | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | Rectangle { | |
| 161 | + | Layout.fillWidth: true | |
| 162 | + | visible: win.needsInput | |
| 163 | + | implicitHeight: 32 | |
| 164 | + | color: Theme.surface | |
| 165 | + | border.width: 1 | |
| 166 | + | border.color: pass.activeFocus ? Theme.accent : Theme.line | |
| 167 | + | Text { | |
| 168 | + | id: promptTxt | |
| 169 | + | anchors.left: parent.left | |
| 170 | + | anchors.leftMargin: 10 | |
| 171 | + | anchors.verticalCenter: parent.verticalCenter | |
| 172 | + | text: win.mode === "bt" ? (win.btMethod === "RequestPinCode" ? "PIN" : "passkey") | |
| 173 | + | : (win.flow && win.flow.inputPrompt !== "" ? win.flow.inputPrompt.replace(/:\s*$/, "") : "password") | |
| 174 | + | color: Theme.dim | |
| 175 | + | font.family: Theme.font | |
| 176 | + | font.pixelSize: Theme.fontSize | |
| 177 | + | } | |
| 178 | + | TextInput { | |
| 179 | + | id: pass | |
| 180 | + | anchors.fill: parent | |
| 181 | + | anchors.leftMargin: promptTxt.implicitWidth + 22 | |
| 182 | + | anchors.rightMargin: 10 | |
| 183 | + | verticalAlignment: TextInput.AlignVCenter | |
| 184 | + | echoMode: win.mode === "bt" || (win.flow && win.flow.responseVisible) ? TextInput.Normal : TextInput.Password | |
| 185 | + | passwordCharacter: "•" | |
| 186 | + | maximumLength: win.btMethod === "RequestPasskey" ? 6 : 256 | |
| 187 | + | validator: RegularExpressionValidator { | |
| 188 | + | regularExpression: win.btMethod === "RequestPasskey" ? /[0-9]*/ : /.*/ | |
| 189 | + | } | |
| 190 | + | enabled: !win.waiting | |
| 191 | + | clip: true | |
| 192 | + | color: Theme.fg | |
| 193 | + | selectionColor: Theme.accent | |
| 194 | + | selectedTextColor: Theme.bg | |
| 195 | + | font.family: Theme.font | |
| 196 | + | font.pixelSize: Theme.fontSize | |
| 197 | + | Keys.onReturnPressed: win.accept() | |
| 198 | + | Keys.onEnterPressed: win.accept() | |
| 199 | + | Keys.onEscapePressed: win.cancel() | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | Text { | |
| 203 | + | Layout.fillWidth: true | |
| 204 | + | visible: win.mode === "polkit" && text !== "" | |
| 205 | + | text: win.waiting ? "checking…" : (win.error !== "" ? win.error : (win.flow ? win.flow.supplementaryMessage : "")) | |
| 206 | + | color: !win.waiting && (win.error !== "" || (win.flow && win.flow.supplementaryIsError)) ? Theme.err : Theme.dim | |
| 207 | + | wrapMode: Text.Wrap | |
| 208 | + | font.family: Theme.font | |
| 209 | + | font.pixelSize: Theme.fontSize - 1 | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | RowLayout { | |
| 213 | + | Layout.fillWidth: true | |
| 214 | + | spacing: 6 | |
| 215 | + | Item { Layout.fillWidth: true } | |
| 216 | + | IconBtn { | |
| 217 | + | visible: !win.btDisplay | |
| 218 | + | label: win.mode === "bt" ? "reject" : "cancel" | |
| 219 | + | implicitHeight: 26 | |
| 220 | + | onClicked: win.cancel() | |
| 221 | + | } | |
| 222 | + | IconBtn { | |
| 223 | + | label: win.btDisplay ? "close" : (win.mode === "bt" ? (win.btInput ? "pair" : "allow") : "authenticate") | |
| 224 | + | implicitHeight: 26 | |
| 225 | + | active: true | |
| 226 | + | onClicked: win.accept() | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | } | |
| @@ -0,0 +1,145 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | """BlueZ pairing agent for aurora (Agents.qml runs it; Quickshell's Bluetooth module has none). | |
| 3 | + | ||
| 4 | + | Exports org.bluez.Agent1 at /aurora/agent (capability KeyboardDisplay) and makes it the default | |
| 5 | + | agent, again whenever bluetoothd restarts. Talks JSON lines: | |
| 6 | + | stdout {"id": n, "method": "RequestConfirmation", "device": path, "alias": name, "passkey": "012345"} | |
| 7 | + | id 0 = display only (DisplayPasskey/DisplayPinCode), no reply expected | |
| 8 | + | {"event": "registered" | "cancel" | "error", "msg": ...} | |
| 9 | + | stdin {"id": n, "ok": true|false, "value": "1234"} (value for RequestPinCode/RequestPasskey) | |
| 10 | + | Python stdlib + python-gobject only. | |
| 11 | + | """ | |
| 12 | + | import json | |
| 13 | + | import sys | |
| 14 | + | ||
| 15 | + | from gi.repository import Gio, GLib | |
| 16 | + | ||
| 17 | + | PATH = "/aurora/agent" | |
| 18 | + | XML = """<node><interface name="org.bluez.Agent1"> | |
| 19 | + | <method name="Release"/> | |
| 20 | + | <method name="RequestPinCode"><arg type="o" direction="in"/><arg type="s" direction="out"/></method> | |
| 21 | + | <method name="DisplayPinCode"><arg type="o" direction="in"/><arg type="s" direction="in"/></method> | |
| 22 | + | <method name="RequestPasskey"><arg type="o" direction="in"/><arg type="u" direction="out"/></method> | |
| 23 | + | <method name="DisplayPasskey"><arg type="o" direction="in"/><arg type="u" direction="in"/><arg type="q" direction="in"/></method> | |
| 24 | + | <method name="RequestConfirmation"><arg type="o" direction="in"/><arg type="u" direction="in"/></method> | |
| 25 | + | <method name="RequestAuthorization"><arg type="o" direction="in"/></method> | |
| 26 | + | <method name="AuthorizeService"><arg type="o" direction="in"/><arg type="s" direction="in"/></method> | |
| 27 | + | <method name="Cancel"/> | |
| 28 | + | </interface></node>""" | |
| 29 | + | ||
| 30 | + | bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) | |
| 31 | + | loop = GLib.MainLoop() | |
| 32 | + | pending = {} # id -> (invocation, method) | |
| 33 | + | last_id = 0 | |
| 34 | + | ||
| 35 | + | ||
| 36 | + | def emit(**kw): | |
| 37 | + | print(json.dumps(kw), flush=True) | |
| 38 | + | ||
| 39 | + | ||
| 40 | + | def dev_prop(path, name): | |
| 41 | + | try: | |
| 42 | + | v = bus.call_sync("org.bluez", path, "org.freedesktop.DBus.Properties", "Get", | |
| 43 | + | GLib.Variant("(ss)", ("org.bluez.Device1", name)), GLib.VariantType("(v)"), | |
| 44 | + | Gio.DBusCallFlags.NONE, 2000, None) | |
| 45 | + | return v.unpack()[0] | |
| 46 | + | except GLib.Error: | |
| 47 | + | return None | |
| 48 | + | ||
| 49 | + | ||
| 50 | + | def alias(path): | |
| 51 | + | return dev_prop(path, "Alias") or path.rsplit("/", 1)[-1] | |
| 52 | + | ||
| 53 | + | ||
| 54 | + | def ask(inv, method, dev, **extra): | |
| 55 | + | global last_id | |
| 56 | + | last_id += 1 | |
| 57 | + | pending[last_id] = (inv, method) | |
| 58 | + | emit(id=last_id, method=method, device=dev, alias=alias(dev), **extra) | |
| 59 | + | ||
| 60 | + | ||
| 61 | + | def on_call(conn, sender, path, iface, method, params, inv): | |
| 62 | + | a = params.unpack() | |
| 63 | + | if method == "Release": | |
| 64 | + | inv.return_value(None) | |
| 65 | + | elif method == "Cancel": | |
| 66 | + | for i, _ in pending.values(): | |
| 67 | + | i.return_dbus_error("org.bluez.Error.Canceled", "cancelled") | |
| 68 | + | pending.clear() | |
| 69 | + | inv.return_value(None) | |
| 70 | + | emit(event="cancel") | |
| 71 | + | elif method == "DisplayPinCode": | |
| 72 | + | inv.return_value(None) | |
| 73 | + | emit(id=0, method=method, device=a[0], alias=alias(a[0]), pincode=a[1]) | |
| 74 | + | elif method == "DisplayPasskey": | |
| 75 | + | inv.return_value(None) | |
| 76 | + | emit(id=0, method=method, device=a[0], alias=alias(a[0]), passkey="%06d" % a[1], entered=a[2]) | |
| 77 | + | elif method == "AuthorizeService": | |
| 78 | + | # a device we already paired with / trust may use its services; anything else asks | |
| 79 | + | if dev_prop(a[0], "Paired") or dev_prop(a[0], "Trusted"): | |
| 80 | + | inv.return_value(None) | |
| 81 | + | else: | |
| 82 | + | ask(inv, method, a[0], uuid=a[1]) | |
| 83 | + | elif method == "RequestConfirmation": | |
| 84 | + | ask(inv, method, a[0], passkey="%06d" % a[1]) | |
| 85 | + | else: # RequestPinCode, RequestPasskey, RequestAuthorization | |
| 86 | + | ask(inv, method, a[0]) | |
| 87 | + | ||
| 88 | + | ||
| 89 | + | def on_stdin(ch, cond): | |
| 90 | + | if cond & (GLib.IO_HUP | GLib.IO_ERR): | |
| 91 | + | loop.quit() | |
| 92 | + | return False | |
| 93 | + | status, line, _, _ = ch.read_line() | |
| 94 | + | if status == GLib.IOStatus.EOF: | |
| 95 | + | loop.quit() | |
| 96 | + | return False | |
| 97 | + | try: | |
| 98 | + | r = json.loads(line) | |
| 99 | + | except (TypeError, ValueError): | |
| 100 | + | return True | |
| 101 | + | item = pending.pop(r.get("id"), None) | |
| 102 | + | if not item: | |
| 103 | + | return True | |
| 104 | + | inv, method = item | |
| 105 | + | if not r.get("ok"): | |
| 106 | + | inv.return_dbus_error("org.bluez.Error.Rejected", "rejected") | |
| 107 | + | return True | |
| 108 | + | v = r.get("value", "") | |
| 109 | + | try: | |
| 110 | + | if method == "RequestPinCode": | |
| 111 | + | inv.return_value(GLib.Variant("(s)", (str(v),))) | |
| 112 | + | elif method == "RequestPasskey": | |
| 113 | + | n = int(v) | |
| 114 | + | if not 0 <= n <= 999999: | |
| 115 | + | raise ValueError | |
| 116 | + | inv.return_value(GLib.Variant("(u)", (n,))) | |
| 117 | + | else: | |
| 118 | + | inv.return_value(None) | |
| 119 | + | except ValueError: | |
| 120 | + | inv.return_dbus_error("org.bluez.Error.Rejected", "bad value") | |
| 121 | + | return True | |
| 122 | + | ||
| 123 | + | ||
| 124 | + | def register(*_): | |
| 125 | + | try: | |
| 126 | + | for m, args in (("RegisterAgent", GLib.Variant("(os)", (PATH, "KeyboardDisplay"))), | |
| 127 | + | ("RequestDefaultAgent", GLib.Variant("(o)", (PATH,)))): | |
| 128 | + | bus.call_sync("org.bluez", "/org/bluez", "org.bluez.AgentManager1", m, args, None, | |
| 129 | + | Gio.DBusCallFlags.NONE, 5000, None) | |
| 130 | + | emit(event="registered") | |
| 131 | + | except GLib.Error as e: | |
| 132 | + | emit(event="error", msg=e.message) | |
| 133 | + | ||
| 134 | + | ||
| 135 | + | def main(): | |
| 136 | + | node = Gio.DBusNodeInfo.new_for_xml(XML) | |
| 137 | + | bus.register_object_with_closures2(PATH, node.interfaces[0], on_call, None, None) | |
| 138 | + | Gio.bus_watch_name_on_connection(bus, "org.bluez", Gio.BusNameWatcherFlags.NONE, register, None) | |
| 139 | + | ch = GLib.IOChannel.unix_new(sys.stdin.fileno()) | |
| 140 | + | GLib.io_add_watch(ch, GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP | GLib.IO_ERR, on_stdin) | |
| 141 | + | loop.run() | |
| 142 | + | ||
| 143 | + | ||
| 144 | + | if __name__ == "__main__": | |
| 145 | + | main() | |
| @@ -148,6 +148,9 @@ ShellRoot { | |||
| 148 | 148 | onCloseRequested: root.closeOverlay() | |
| 149 | 149 | } | |
| 150 | 150 | ||
| 151 | + | // polkit password prompt (Agents), on the focused monitor | |
| 152 | + | AuthPrompt { screen: root.focusedScr } | |
| 153 | + | ||
| 151 | 154 | PowerMenu { | |
| 152 | 155 | screen: root.overlayScreen | |
| 153 | 156 | opened: root.overlay === "power" | |
| @@ -24,6 +24,8 @@ wl-clipboard | |||
| 24 | 24 | cliphist | |
| 25 | 25 | playerctl | |
| 26 | 26 | gnome-keyring | |
| 27 | + | libnotify | |
| 28 | + | python-gobject | |
| 27 | 29 | ||
| 28 | 30 | # Audio | |
| 29 | 31 | pipewire | |
| @@ -79,7 +79,6 @@ qpdf | |||
| 79 | 79 | # Desktop bits | |
| 80 | 80 | alacritty | |
| 81 | 81 | gammastep | |
| 82 | − | libnotify | |
| 83 | 82 | wtype | |
| 84 | 83 | nwg-look | |
| 85 | 84 | fastfetch | |
| @@ -92,7 +91,6 @@ cmake | |||
| 92 | 91 | meson | |
| 93 | 92 | lua | |
| 94 | 93 | bc | |
| 95 | − | python-gobject | |
| 96 | 94 | gobject-introspection | |
| 97 | 95 | python-pip | |
| 98 | 96 | python-virtualenv | |