From 8f643282b93cbf9eb3d89aa792df28c4d4197018 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 16 May 2026 22:20:25 +0200 Subject: [PATCH] Switch from gnome polkit to QS --- default/hypr/autostart.lua | 1 - default/omarchy-skill/SKILL.md | 2 +- default/quickshell/omarchy-shell/README.md | 16 +- .../omarchy-shell/plugins/README.md | 2 +- .../omarchy-shell/plugins/bar/README.md | 4 +- .../plugins/polkit/PolkitAgent.qml | 358 ++++++++++++++++++ .../plugins/polkit/manifest.json | 14 + install/omarchy-base.packages | 1 - migrations/1778956449.sh | 13 + 9 files changed, 398 insertions(+), 13 deletions(-) create mode 100644 default/quickshell/omarchy-shell/plugins/polkit/PolkitAgent.qml create mode 100644 default/quickshell/omarchy-shell/plugins/polkit/manifest.json create mode 100644 migrations/1778956449.sh diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 5d241cd7..b1053cad 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -2,7 +2,6 @@ hl.on("hyprland.start", function() hl.exec_cmd("omarchy-restart-shell") hl.exec_cmd("uwsm-app -- hypridle") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") - hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") hl.exec_cmd("uwsm-app -- omarchy-hyprland-monitor-watch") diff --git a/default/omarchy-skill/SKILL.md b/default/omarchy-skill/SKILL.md index 422871c0..6f2d96a6 100644 --- a/default/omarchy-skill/SKILL.md +++ b/default/omarchy-skill/SKILL.md @@ -160,7 +160,7 @@ inside a single long-running Quickshell process (`omarchy-shell`). The shell hot-reloads `shell.json` on save — no restart needed for layout changes. For more invasive changes (new plugin, packaged update): -**Commands:** `omarchy-restart-shell`, `omarchy refresh shell`, `omarchy launch settings` +**Commands:** `omarchy-restart-shell`, `omarchy refresh shell`, `omarchy launch bar settings` ### Terminals diff --git a/default/quickshell/omarchy-shell/README.md b/default/quickshell/omarchy-shell/README.md index 6dcfc446..02d9e690 100644 --- a/default/quickshell/omarchy-shell/README.md +++ b/default/quickshell/omarchy-shell/README.md @@ -31,6 +31,9 @@ default/quickshell/omarchy-shell/ settings/ image-picker/ menu/ + notifications/ + osd/ + polkit/ ``` The plugin discovery path is documented in [plugins/README.md](plugins/README.md). @@ -87,10 +90,8 @@ The full schema lives in `services/PluginRegistry.qml`. 1. Drop the plugin into `~/.config/omarchy/plugins//`. The directory must contain a `manifest.json` plus the QML files referenced from its `entryPoints`. -2. `omarchy-shell-ipc shell rescanPlugins` — or open the Plugin Manager - tab in `omarchy launch settings` and click **Rescan**. -3. Enable the plugin (Plugin Manager **Enable** toggle, or - `omarchy-shell-ipc shell setPluginEnabled true`). +2. `omarchy-shell-ipc shell rescanPlugins`. +3. Enable the plugin with `omarchy-shell-ipc shell setPluginEnabled true`. 4. If it's a `bar-widget`, add it to a layout section from the bar editor. First-party plugins under `default/quickshell/omarchy-shell/plugins/` @@ -153,8 +154,8 @@ The `shell-defaults.json` bundled with the shell describes the fresh-install state. When the user has no `shell.json`, the shell uses the defaults verbatim. Once the user customizes anything, `shell.json` becomes the authoritative file — we do **not** deep-merge defaults back -in. Pressing **Reset to defaults** in `omarchy launch settings` -rewrites `shell.json` from the current `shell-defaults.json`. +in. Pressing **Reset bar to defaults** in `omarchy launch bar settings` +rewrites the `bar` subtree from the current `shell-defaults.json`. ### shell.json shape @@ -189,7 +190,8 @@ rewrites `shell.json` from the current `shell-defaults.json`. separate per-plugin settings file, no merge layers. The fields on each entry are the values the plugin sees. 3. **Enabled ⇔ present.** A plugin is enabled iff its id appears somewhere - in shell.json. To disable, remove it. (The bar settings UI does both.) + in shell.json. For bar widgets, the bar settings UI adds/removes layout + entries; other plugin kinds are enabled with the shell IPC. 4. **Multiple instances** are allowed when a manifest sets `allowMultiple: true`. Each instance is independent — e.g. two clocks in different timezones are just two `{"id":"calendar", "timezone": ...}` diff --git a/default/quickshell/omarchy-shell/plugins/README.md b/default/quickshell/omarchy-shell/plugins/README.md index eb2e8515..e5e07f61 100644 --- a/default/quickshell/omarchy-shell/plugins/README.md +++ b/default/quickshell/omarchy-shell/plugins/README.md @@ -31,7 +31,7 @@ and customization schema. Visual editor for the bar layout. Summoned by `omarchy-shell-ipc shell summon omarchy.settings "{}"` (which is what -`omarchy launch settings` ultimately calls). Provides: +`omarchy launch bar settings` ultimately calls). Provides: - bar position and center-anchor controls - per-section add/move/remove/edit of bar widget entries diff --git a/default/quickshell/omarchy-shell/plugins/bar/README.md b/default/quickshell/omarchy-shell/plugins/bar/README.md index a082c94a..390b7664 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/README.md +++ b/default/quickshell/omarchy-shell/plugins/bar/README.md @@ -14,9 +14,9 @@ the shell for its whole session. ## Customizing -The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`shell-defaults.json`](../../shell-defaults.json). Once you customize anything via `omarchy launch settings` or by editing shell.json directly, your file is canonical — there is no deep-merge. +The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`shell-defaults.json`](../../shell-defaults.json). Once you customize anything via `omarchy launch bar settings` or by editing shell.json directly, your file is canonical — there is no deep-merge. -Launch the visual editor with `omarchy launch settings` (or run `omarchy-launch-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. +Launch the visual editor with `omarchy launch bar settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. Example `shell.json` (bar subtree only shown): diff --git a/default/quickshell/omarchy-shell/plugins/polkit/PolkitAgent.qml b/default/quickshell/omarchy-shell/plugins/polkit/PolkitAgent.qml new file mode 100644 index 00000000..5ad4f063 --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/polkit/PolkitAgent.qml @@ -0,0 +1,358 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Polkit +import Quickshell.Wayland +import qs.Commons + +Item { + id: root + + property string omarchyPath: "" + property var shell: null + property var manifest: null + + property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace" + property string styleFile: Quickshell.env("OMARCHY_MENU_STYLE_FILE") || (Quickshell.env("HOME") + "/.local/state/omarchy/toggles/quickshell-menu.json") + property color accent: Color.accent + property color background: Color.menu.background + property color foreground: Color.menu.text + property color border: foreground + property int cornerRadius: 0 + property int contentMargin: 18 + property int contentSpacing: 12 + property int fieldHeight: 42 + + property bool closing: false + property bool submitted: false + property string currentMessage: "" + property string currentPrompt: "" + property string currentSupplementary: "" + property bool responseRequired: false + property bool responseVisible: false + property bool failed: false + property bool errorFlash: false + property bool fingerprintFirst: false + property int shakeOffset: 0 + + readonly property bool dialogVisible: polkitAgent.isActive || closing + readonly property bool fingerprintWaiting: dialogVisible && !responseRequired && !submitted && (fingerprintFirst || promptLooksFingerprint(currentPrompt + " " + currentSupplementary)) + readonly property int cardWidth: Math.min(312, Math.max(260, panel.width - 48)) + readonly property int cardHeight: panel.height > 0 ? Math.min(fieldHeight + contentMargin * 2, panel.height - 48) : fieldHeight + contentMargin * 2 + + function withAlpha(color, alpha) { + return Qt.rgba(color.r, color.g, color.b, alpha) + } + + function loadStyle(raw) { + try { + var style = JSON.parse(raw || "{}") + root.cornerRadius = Number(style.radius || 0) + } catch (e) {} + } + + function messageText() { + return "Authentication is needed..." + } + + function promptLooksFingerprint(text) { + var s = String(text || "").toLowerCase() + return s.indexOf("finger") !== -1 || s.indexOf("fprint") !== -1 || s.indexOf("swipe") !== -1 + } + + function loadPamConfig(raw) { + fingerprintFirst = false + var lines = String(raw || "").split("\n") + for (var i = 0; i < lines.length; i++) { + var line = lines[i].replace(/^\s+|\s+$/g, "") + if (!line || line.charAt(0) === "#") continue + if (!line.match(/^auth\s+/)) continue + fingerprintFirst = line.indexOf("pam_fprintd.so") !== -1 + return + } + } + + function resetSnapshot() { + currentMessage = "" + currentPrompt = "" + currentSupplementary = "" + responseRequired = false + responseVisible = false + failed = false + errorFlash = false + submitted = false + passwordInput.text = "" + } + + function syncFromFlow() { + var flow = polkitAgent.flow + if (!flow) return + + currentMessage = String(flow.message || "Authentication is needed...") + currentPrompt = String(flow.inputPrompt || "") + currentSupplementary = String(flow.supplementaryMessage || "") + responseRequired = !!flow.isResponseRequired + responseVisible = !!flow.responseVisible + failed = !!flow.failed + + if (responseRequired) submitted = false + } + + function beginFlow() { + closeTimer.stop() + closing = false + submitted = false + passwordInput.text = "" + syncFromFlow() + Qt.callLater(refocus) + } + + function refocus() { + if (!dialogVisible) return + if (fingerprintWaiting) keyCatcher.forceActiveFocus() + else passwordInput.forceActiveFocus() + } + + function submitResponse() { + var flow = polkitAgent.flow + if (!flow || !flow.isResponseRequired) return + submitted = true + errorFlash = false + flow.submit(passwordInput.text) + passwordInput.text = "" + keyCatcher.forceActiveFocus() + } + + function cancelRequest() { + var flow = polkitAgent.flow + passwordInput.text = "" + submitted = false + closing = true + closeTimer.restart() + if (flow) flow.cancelAuthenticationRequest() + } + + function triggerFailureFeedback() { + submitted = false + errorFlash = true + passwordInput.text = "" + errorTimer.restart() + shakeAnimation.restart() + Qt.callLater(refocus) + } + + Timer { + id: closeTimer + interval: 300 + repeat: false + onTriggered: { + closing = false + resetSnapshot() + } + } + + Timer { + id: errorTimer + interval: 1200 + repeat: false + onTriggered: root.errorFlash = false + } + + SequentialAnimation { + id: shakeAnimation + NumberAnimation { target: root; property: "shakeOffset"; to: -8; duration: 35; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "shakeOffset"; to: 8; duration: 50; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; property: "shakeOffset"; to: 0; duration: 55; easing.type: Easing.OutQuad } + } + + FileView { + path: root.styleFile + watchChanges: true + onLoaded: root.loadStyle(text()) + onFileChanged: { reload(); root.loadStyle(text()) } + } + + FileView { + path: "/etc/pam.d/polkit-1" + watchChanges: true + printErrors: false + onLoaded: root.loadPamConfig(text()) + onLoadFailed: root.fingerprintFirst = false + onFileChanged: reload() + } + + PolkitAgent { + id: polkitAgent + path: "/org/omarchy/PolkitAgent" + + onAuthenticationRequestStarted: root.beginFlow() + onIsActiveChanged: { + if (isActive) root.syncFromFlow() + else if (!root.closing) root.resetSnapshot() + } + onIsRegisteredChanged: { + if (isRegistered) console.log("omarchy polkit agent registered") + else console.warn("omarchy polkit agent is not registered; another agent may be running") + } + } + + Connections { + target: polkitAgent.flow + + function onIsResponseRequiredChanged() { + root.syncFromFlow() + if (!polkitAgent.flow || !polkitAgent.flow.isResponseRequired) passwordInput.text = "" + Qt.callLater(root.refocus) + } + + function onInputPromptChanged() { root.syncFromFlow() } + function onResponseVisibleChanged() { root.syncFromFlow() } + function onSupplementaryMessageChanged() { root.syncFromFlow() } + function onFailedChanged() { root.syncFromFlow() } + + function onAuthenticationFailed() { + root.syncFromFlow() + root.triggerFailureFeedback() + } + + function onAuthenticationSucceeded() { + root.closing = true + closeTimer.restart() + } + + function onAuthenticationRequestCancelled() { + root.closing = true + closeTimer.restart() + } + } + + PanelWindow { + id: panel + visible: root.dialogVisible + anchors { top: true; bottom: true; left: true; right: true } + color: "transparent" + WlrLayershell.namespace: "omarchy-polkit" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + + Rectangle { + anchors.fill: parent + color: root.withAlpha(root.background, 0.5) + } + + MouseArea { + anchors.fill: parent + onClicked: root.refocus() + } + + Rectangle { + id: card + width: root.cardWidth + height: root.cardHeight + radius: root.cornerRadius + anchors.centerIn: parent + anchors.horizontalCenterOffset: root.shakeOffset + color: root.background + border.color: root.accent + border.width: 2 + + MouseArea { anchors.fill: parent; onClicked: root.refocus() } + + Item { + id: keyCatcher + anchors.fill: parent + focus: true + + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + root.cancelRequest() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (root.responseRequired) root.submitResponse() + event.accepted = true + } + } + } + + Row { + id: cardRow + anchors.fill: parent + anchors.margins: root.contentMargin + spacing: 14 + + Text { + text: "\uf023" + color: root.errorFlash ? Color.urgent : root.accent + font.family: root.fontFamily + font.pixelSize: 20 + width: 26 + height: root.fieldHeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Item { + width: parent.width - 40 + height: root.fieldHeight + + TextInput { + id: passwordInput + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + activeFocusOnPress: true + clip: true + selectionColor: root.withAlpha(root.accent, 0.45) + selectedTextColor: root.foreground + font.family: root.fontFamily + font.pixelSize: 19 + echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password + passwordCharacter: "\u2022" + color: root.errorFlash ? Color.urgent : root.foreground + cursorVisible: activeFocus && !root.submitted && !root.errorFlash + readOnly: root.submitted || root.errorFlash + enabled: root.dialogVisible && !root.fingerprintWaiting + visible: !root.fingerprintWaiting + onAccepted: root.submitResponse() + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + root.cancelRequest() + event.accepted = true + } + } + } + + Text { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: root.errorFlash ? "Wrong" : (root.submitted ? "Checking..." : "Enter password") + color: root.errorFlash ? Color.urgent : root.foreground + opacity: root.errorFlash ? 1 : 0.36 + font.family: root.fontFamily + font.pixelSize: 19 + elide: Text.ElideRight + visible: passwordInput.visible && passwordInput.text.length === 0 + } + + Rectangle { + width: 2 + height: 24 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.errorFlash ? Color.urgent : root.foreground + visible: passwordInput.visible && passwordInput.activeFocus && passwordInput.text.length === 0 && !root.submitted && !root.errorFlash + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + enabled: passwordInput.visible + onClicked: passwordInput.forceActiveFocus() + } + } + } + } + } +} diff --git a/default/quickshell/omarchy-shell/plugins/polkit/manifest.json b/default/quickshell/omarchy-shell/plugins/polkit/manifest.json new file mode 100644 index 00000000..fc690431 --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/polkit/manifest.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "id": "omarchy.polkit", + "name": "Polkit Agent", + "version": "1.0.0", + "author": "Omarchy", + "description": "Theme-aware authentication dialog for privileged actions.", + "kinds": ["service"], + "activation": "persistent", + "keepLoaded": true, + "entryPoints": { + "service": "PolkitAgent.qml" + } +} diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 63e4bac8..8cc80eec 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -98,7 +98,6 @@ pinta playerctl plocate plymouth -polkit-gnome postgresql-libs power-profiles-daemon python-gobject diff --git a/migrations/1778956449.sh b/migrations/1778956449.sh new file mode 100644 index 00000000..443d8893 --- /dev/null +++ b/migrations/1778956449.sh @@ -0,0 +1,13 @@ +echo "Switch polkit authentication to omarchy-shell" + +# The default Hyprland autostart now uses omarchy-shell's native Quickshell +# Polkit agent. Remove any stale user override that starts the old GTK agent. +if [[ -f ~/.config/hypr/autostart.lua ]]; then + sed -i '/polkit-gnome-authentication-agent-1/d' ~/.config/hypr/autostart.lua +fi + +pkill -u "$USER" -f '^/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1$' 2>/dev/null || true + +omarchy-pkg-drop polkit-gnome + +omarchy-restart-shell