├── .clang-format ├── .envrc ├── .github └── workflows │ └── nix.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── VERSION ├── assets ├── hyprpolkitagent-dbus.in ├── hyprpolkitagent-service.in └── screenshot.png ├── flake.lock ├── flake.nix ├── nix ├── default.nix ├── overlays.nix └── shell.nix ├── qml └── main.qml └── src ├── QMLIntegration.cpp ├── QMLIntegration.hpp ├── core ├── Agent.cpp ├── Agent.hpp ├── PolkitListener.cpp └── PolkitListener.hpp └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | 5 | AccessModifierOffset: -2 6 | AlignAfterOpenBracket: Align 7 | AlignConsecutiveMacros: true 8 | AlignConsecutiveAssignments: true 9 | AlignEscapedNewlines: Right 10 | AlignOperands: false 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: true 16 | AllowShortCaseLabelsOnASingleLine: true 17 | AllowShortFunctionsOnASingleLine: Empty 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BreakBeforeBraces: Attach 26 | BreakBeforeTernaryOperators: false 27 | BreakConstructorInitializers: AfterColon 28 | ColumnLimit: 180 29 | CompactNamespaces: false 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 31 | ExperimentalAutoDetectBinPacking: false 32 | FixNamespaceComments: false 33 | IncludeBlocks: Preserve 34 | IndentCaseLabels: true 35 | IndentWidth: 4 36 | PointerAlignment: Left 37 | ReflowComments: false 38 | SortIncludes: false 39 | SortUsingDeclarations: false 40 | SpaceAfterCStyleCast: false 41 | SpaceAfterLogicalNot: false 42 | SpaceAfterTemplateKeyword: true 43 | SpaceBeforeCtorInitializerColon: true 44 | SpaceBeforeInheritanceColon: true 45 | SpaceBeforeParens: ControlStatements 46 | SpaceBeforeRangeBasedForLoopColon: true 47 | SpaceInEmptyParentheses: false 48 | SpacesBeforeTrailingComments: 1 49 | SpacesInAngles: false 50 | SpacesInCStyleCastParentheses: false 51 | SpacesInContainerLiterals: false 52 | SpacesInParentheses: false 53 | SpacesInSquareBrackets: false 54 | Standard: Auto 55 | TabWidth: 4 56 | UseTab: Never 57 | 58 | AllowShortEnumsOnASingleLine: false 59 | 60 | BraceWrapping: 61 | AfterEnum: false 62 | 63 | AlignConsecutiveDeclarations: AcrossEmptyLines 64 | 65 | NamespaceIndentation: All 66 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | jobs: 5 | nix: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | 10 | - uses: DeterminateSystems/nix-installer-action@main 11 | 12 | # not needed (yet) 13 | # - uses: cachix/cachix-action@v12 14 | # with: 15 | # name: hyprland 16 | # authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 17 | 18 | - name: Build 19 | run: nix build --print-build-logs --keep-going 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | .vscode/ 35 | .cache/ 36 | build/ 37 | 38 | compile_commands.json 39 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "subprojects/sdbus-cpp"] 2 | path = subprojects/sdbus-cpp 3 | url = https://github.com/Kistler-Group/sdbus-cpp/ 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | # Get version 4 | file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) 5 | string(STRIP ${VER_RAW} VER) 6 | 7 | project(hpa VERSION ${VER} LANGUAGES CXX) 8 | 9 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 10 | 11 | find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2) 12 | find_package(PkgConfig REQUIRED) 13 | set(CMAKE_CXX_STANDARD 23) 14 | 15 | pkg_check_modules( 16 | deps 17 | REQUIRED 18 | IMPORTED_TARGET 19 | hyprutils 20 | polkit-agent-1 21 | polkit-qt6-1) 22 | 23 | qt_standard_project_setup(REQUIRES 6.5) 24 | 25 | qt_add_executable(hyprpolkitagent 26 | src/main.cpp 27 | src/core/Agent.cpp 28 | src/core/Agent.hpp 29 | src/core/PolkitListener.hpp 30 | src/core/PolkitListener.cpp 31 | src/QMLIntegration.cpp 32 | src/QMLIntegration.hpp 33 | ) 34 | 35 | qt_add_qml_module(hyprpolkitagent 36 | URI hpa 37 | VERSION 1.0 38 | QML_FILES 39 | qml/main.qml 40 | SOURCES 41 | src/QMLIntegration.cpp 42 | src/QMLIntegration.hpp 43 | ) 44 | 45 | target_link_libraries(hyprpolkitagent 46 | PRIVATE Qt6::Widgets Qt6::Quick Qt6::Gui Qt6::QuickControls2 PkgConfig::deps 47 | ) 48 | 49 | include(GNUInstallDirs) 50 | 51 | set(LIBEXECDIR ${CMAKE_INSTALL_FULL_LIBEXECDIR}) 52 | configure_file(assets/hyprpolkitagent-service.in hyprpolkitagent.service @ONLY) 53 | configure_file(assets/hyprpolkitagent-dbus.in hyprpolkitagent-dbus.service @ONLY) 54 | 55 | install(TARGETS hyprpolkitagent 56 | DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) 57 | install(FILES ${CMAKE_BINARY_DIR}/hyprpolkitagent.service 58 | DESTINATION "lib/systemd/user") 59 | install(FILES ${CMAKE_BINARY_DIR}/hyprpolkitagent-dbus.service 60 | DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/services 61 | RENAME hyprpolkitagent.service) 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Hypr Development 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyprpolkitagent 2 | A simple polkit authentication agent for Hyprland, written in QT/QML. 3 | 4 | ![](./assets/screenshot.png) 5 | 6 | ## Usage 7 | 8 | See [the hyprland wiki](https://wiki.hyprland.org/Hypr-Ecosystem/hyprpolkitagent/) -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.2 -------------------------------------------------------------------------------- /assets/hyprpolkitagent-dbus.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.hyprland.hyprpolkitagent 3 | Exec=@LIBEXECDIR@/hyprpolkitagent 4 | SystemdService=hyprpolkitagent.service 5 | -------------------------------------------------------------------------------- /assets/hyprpolkitagent-service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Hyprland Polkit Authentication Agent 3 | PartOf=graphical-session.target 4 | After=graphical-session.target 5 | ConditionEnvironment=WAYLAND_DISPLAY 6 | 7 | [Service] 8 | ExecStart=@LIBEXECDIR@/hyprpolkitagent 9 | Slice=session.slice 10 | TimeoutStopSec=5sec 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=graphical-session.target 15 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprwm/hyprpolkitagent/352638edcd400253dff375c36075ec9548ae1d32/assets/screenshot.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "hyprland-qt-support": { 4 | "inputs": { 5 | "hyprlang": "hyprlang", 6 | "nixpkgs": [ 7 | "nixpkgs" 8 | ], 9 | "systems": [ 10 | "systems" 11 | ] 12 | }, 13 | "locked": { 14 | "lastModified": 1737634706, 15 | "narHash": "sha256-nGCibkfsXz7ARx5R+SnisRtMq21IQIhazp6viBU8I/A=", 16 | "owner": "hyprwm", 17 | "repo": "hyprland-qt-support", 18 | "rev": "8810df502cdee755993cb803eba7b23f189db795", 19 | "type": "github" 20 | }, 21 | "original": { 22 | "owner": "hyprwm", 23 | "repo": "hyprland-qt-support", 24 | "type": "github" 25 | } 26 | }, 27 | "hyprlang": { 28 | "inputs": { 29 | "hyprutils": "hyprutils", 30 | "nixpkgs": [ 31 | "hyprland-qt-support", 32 | "nixpkgs" 33 | ], 34 | "systems": [ 35 | "hyprland-qt-support", 36 | "systems" 37 | ] 38 | }, 39 | "locked": { 40 | "lastModified": 1737634606, 41 | "narHash": "sha256-W7W87Cv6wqZ9PHegI6rH1+ve3zJPiyevMFf0/HwdbCQ=", 42 | "owner": "hyprwm", 43 | "repo": "hyprlang", 44 | "rev": "f41271d35cc0f370d300413d756c2677f386af9d", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "hyprwm", 49 | "repo": "hyprlang", 50 | "type": "github" 51 | } 52 | }, 53 | "hyprutils": { 54 | "inputs": { 55 | "nixpkgs": [ 56 | "hyprland-qt-support", 57 | "hyprlang", 58 | "nixpkgs" 59 | ], 60 | "systems": [ 61 | "hyprland-qt-support", 62 | "hyprlang", 63 | "systems" 64 | ] 65 | }, 66 | "locked": { 67 | "lastModified": 1737632363, 68 | "narHash": "sha256-X9I8POSlHxBVjD0fiX1O2j7U9Zi1+4rIkrsyHP0uHXY=", 69 | "owner": "hyprwm", 70 | "repo": "hyprutils", 71 | "rev": "006620eb29d54ea9086538891404c78563d1bae1", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "owner": "hyprwm", 76 | "repo": "hyprutils", 77 | "type": "github" 78 | } 79 | }, 80 | "hyprutils_2": { 81 | "inputs": { 82 | "nixpkgs": [ 83 | "nixpkgs" 84 | ], 85 | "systems": [ 86 | "systems" 87 | ] 88 | }, 89 | "locked": { 90 | "lastModified": 1737632363, 91 | "narHash": "sha256-X9I8POSlHxBVjD0fiX1O2j7U9Zi1+4rIkrsyHP0uHXY=", 92 | "owner": "hyprwm", 93 | "repo": "hyprutils", 94 | "rev": "006620eb29d54ea9086538891404c78563d1bae1", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "hyprwm", 99 | "repo": "hyprutils", 100 | "type": "github" 101 | } 102 | }, 103 | "nixpkgs": { 104 | "locked": { 105 | "lastModified": 1737469691, 106 | "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=", 107 | "owner": "NixOS", 108 | "repo": "nixpkgs", 109 | "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "NixOS", 114 | "ref": "nixos-unstable", 115 | "repo": "nixpkgs", 116 | "type": "github" 117 | } 118 | }, 119 | "root": { 120 | "inputs": { 121 | "hyprland-qt-support": "hyprland-qt-support", 122 | "hyprutils": "hyprutils_2", 123 | "nixpkgs": "nixpkgs", 124 | "systems": "systems" 125 | } 126 | }, 127 | "systems": { 128 | "locked": { 129 | "lastModified": 1689347949, 130 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 131 | "owner": "nix-systems", 132 | "repo": "default-linux", 133 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 134 | "type": "github" 135 | }, 136 | "original": { 137 | "owner": "nix-systems", 138 | "repo": "default-linux", 139 | "type": "github" 140 | } 141 | } 142 | }, 143 | "root": "root", 144 | "version": 7 145 | } 146 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A polkit authentication agent written in QT/QML"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | systems.url = "github:nix-systems/default-linux"; 7 | 8 | hyprutils = { 9 | url = "github:hyprwm/hyprutils"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | inputs.systems.follows = "systems"; 12 | }; 13 | 14 | hyprland-qt-support = { 15 | url = "github:hyprwm/hyprland-qt-support"; 16 | inputs.nixpkgs.follows = "nixpkgs"; 17 | inputs.systems.follows = "systems"; 18 | }; 19 | }; 20 | 21 | outputs = { 22 | self, 23 | nixpkgs, 24 | systems, 25 | ... 26 | } @ inputs: let 27 | inherit (nixpkgs) lib; 28 | eachSystem = lib.genAttrs (import systems); 29 | pkgsFor = eachSystem ( 30 | system: 31 | import nixpkgs { 32 | localSystem = system; 33 | overlays = [self.overlays.default]; 34 | } 35 | ); 36 | in { 37 | overlays = import ./nix/overlays.nix {inherit inputs self lib;}; 38 | 39 | packages = eachSystem (system: { 40 | default = self.packages.${system}.hyprpolkitagent; 41 | inherit (pkgsFor.${system}) hyprpolkitagent; 42 | }); 43 | 44 | devShells = eachSystem (system: { 45 | default = import ./nix/shell.nix { 46 | pkgs = pkgsFor.${system}; 47 | inherit (pkgsFor.${system}) hyprpolkitagent; 48 | }; 49 | }); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | cmake, 5 | pkg-config, 6 | hyprutils, 7 | hyprland-qt-support, 8 | kdePackages, 9 | polkit, 10 | qt6, 11 | version ? "0", 12 | }: let 13 | inherit (lib.sources) cleanSource cleanSourceWith; 14 | inherit (lib.strings) hasSuffix; 15 | in 16 | stdenv.mkDerivation { 17 | pname = "hyprpolkitagent"; 18 | inherit version; 19 | 20 | src = cleanSourceWith { 21 | filter = name: _type: let 22 | baseName = baseNameOf (toString name); 23 | in 24 | ! (hasSuffix ".nix" baseName); 25 | src = cleanSource ../.; 26 | }; 27 | 28 | nativeBuildInputs = [ 29 | cmake 30 | pkg-config 31 | qt6.wrapQtAppsHook 32 | ]; 33 | 34 | buildInputs = [ 35 | hyprutils 36 | hyprland-qt-support 37 | polkit 38 | kdePackages.polkit-qt-1 39 | qt6.qtbase 40 | qt6.qtsvg 41 | qt6.qtwayland 42 | ]; 43 | 44 | meta = { 45 | description = "A polkit authentication agent written in QT/QML"; 46 | homepage = "https://github.com/hyprwm/hyprpolkitagent"; 47 | license = lib.licenses.bsd3; 48 | maintainers = [lib.maintainers.fufexan]; 49 | mainProgram = "hyprpolkitagent"; 50 | platforms = lib.platforms.linux; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /nix/overlays.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs, 3 | self, 4 | lib, 5 | }: let 6 | mkDate = longDate: (lib.concatStringsSep "-" [ 7 | (builtins.substring 0 4 longDate) 8 | (builtins.substring 4 2 longDate) 9 | (builtins.substring 6 2 longDate) 10 | ]); 11 | date = mkDate (self.lastModifiedDate or "19700101"); 12 | version = lib.removeSuffix "\n" (builtins.readFile ../VERSION); 13 | in { 14 | default = self.overlays.hyprpolkitagent; 15 | 16 | hyprpolkitagent = lib.composeManyExtensions [ 17 | inputs.hyprutils.overlays.default 18 | inputs.hyprland-qt-support.overlays.default 19 | (final: prev: { 20 | hyprpolkitagent = final.callPackage ./. { 21 | stdenv = final.gcc14Stdenv; 22 | version = "${version}+date=${date}_${self.shortRev or "dirty"}"; 23 | }; 24 | }) 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /nix/shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import {}, 3 | hyprpolkitagent ? pkgs.callPackage ./default.nix {}, 4 | ... 5 | }: pkgs.mkShell { 6 | inputsFrom = [ hyprpolkitagent ]; 7 | nativeBuildInputs = [ pkgs.clang-tools ]; 8 | 9 | shellHook = let 10 | inherit (pkgs.lib.strings) concatMapStringsSep; 11 | qtLibPath = f: concatMapStringsSep ":" f (with pkgs.qt6; [ 12 | qtbase 13 | qtdeclarative 14 | qtwayland 15 | pkgs.hyprland-qt-support 16 | ]); 17 | in '' 18 | # Add Qt-related environment variables. 19 | export QT_PLUGIN_PATH=${qtLibPath (p: "${p}/lib/qt-6/plugins")} 20 | export QML2_IMPORT_PATH=${qtLibPath (p: "${p}/lib/qt-6/qml")} 21 | 22 | # Generate compile_commands.json 23 | CMAKE_EXPORT_COMPILE_COMMANDS=1 cmake -S . -B ./build 24 | ln -s build/compile_commands.json . 25 | ''; 26 | } 27 | -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | 5 | ApplicationWindow { 6 | id: window 7 | 8 | property var windowWidth: Math.round(fontMetrics.height * 32.2856) 9 | property var windowHeight: Math.round(fontMetrics.height * 13.9528) 10 | property var heightSafeMargin: 15 11 | 12 | minimumWidth: Math.max(windowWidth, mainLayout.Layout.minimumWidth) + mainLayout.anchors.margins * 2 13 | minimumHeight: Math.max(windowHeight, mainLayout.Layout.minimumHeight) + mainLayout.anchors.margins * 2 + heightSafeMargin 14 | maximumWidth: minimumWidth 15 | maximumHeight: minimumHeight 16 | visible: true 17 | onClosing: { 18 | hpa.setResult("fail"); 19 | } 20 | 21 | FontMetrics { 22 | id: fontMetrics 23 | } 24 | 25 | SystemPalette { 26 | id: system 27 | 28 | colorGroup: SystemPalette.Active 29 | } 30 | 31 | Item { 32 | id: mainLayout 33 | 34 | anchors.fill: parent 35 | Keys.onEscapePressed: (e) => { 36 | hpa.setResult("fail"); 37 | } 38 | Keys.onReturnPressed: (e) => { 39 | hpa.setResult("auth:" + passwordField.text); 40 | } 41 | Keys.onEnterPressed: (e) => { 42 | hpa.setResult("auth:" + passwordField.text); 43 | } 44 | 45 | ColumnLayout { 46 | anchors.fill: parent 47 | anchors.margins: 4 48 | 49 | Label { 50 | color: Qt.darker(system.windowText, 0.8) 51 | font.bold: true 52 | font.pointSize: Math.round(fontMetrics.height * 1.05) 53 | text: "Authenticating for " + hpa.getUser() 54 | Layout.alignment: Qt.AlignHCenter 55 | Layout.maximumWidth: parent.width 56 | elide: Text.ElideRight 57 | wrapMode: Text.WordWrap 58 | } 59 | 60 | HSeparator { 61 | Layout.topMargin: fontMetrics.height / 2 62 | Layout.bottomMargin: fontMetrics.height / 2 63 | } 64 | 65 | Label { 66 | color: system.windowText 67 | text: hpa.getMessage() 68 | Layout.maximumWidth: parent.width 69 | elide: Text.ElideRight 70 | wrapMode: Text.WordWrap 71 | } 72 | 73 | TextField { 74 | id: passwordField 75 | 76 | Layout.topMargin: fontMetrics.height / 2 77 | placeholderText: "Password" 78 | Layout.alignment: Qt.AlignHCenter 79 | hoverEnabled: true 80 | persistentSelection: true 81 | echoMode: TextInput.Password 82 | focus: true 83 | 84 | Connections { 85 | target: hpa 86 | function onFocusField() { 87 | passwordField.focus = true; 88 | } 89 | function onBlockInput(block) { 90 | passwordField.readOnly = block; 91 | if (!block) { 92 | passwordField.focus = true; 93 | passwordField.selectAll(); 94 | } 95 | } 96 | } 97 | 98 | } 99 | 100 | Label { 101 | id: errorLabel 102 | 103 | color: "red" 104 | font.italic: true 105 | Layout.topMargin: 0 106 | text: "" 107 | Layout.alignment: Qt.AlignHCenter 108 | 109 | Connections { 110 | target: hpa 111 | function onSetErrorString(e) { 112 | errorLabel.text = e; 113 | } 114 | } 115 | 116 | } 117 | 118 | Rectangle { 119 | color: "transparent" 120 | Layout.fillHeight: true 121 | } 122 | 123 | HSeparator { 124 | Layout.topMargin: fontMetrics.height / 2 125 | Layout.bottomMargin: fontMetrics.height / 2 126 | } 127 | 128 | RowLayout { 129 | Layout.alignment: Qt.AlignRight 130 | Layout.rightMargin: fontMetrics.height / 2 131 | 132 | Button { 133 | text: "Cancel" 134 | onClicked: (e) => { 135 | hpa.setResult("fail"); 136 | } 137 | } 138 | 139 | Button { 140 | text: "Authenticate" 141 | onClicked: (e) => { 142 | hpa.setResult("auth:" + passwordField.text); 143 | } 144 | } 145 | 146 | } 147 | 148 | } 149 | 150 | } 151 | 152 | component Separator: Rectangle { 153 | color: Qt.darker(window.palette.text, 1.5) 154 | } 155 | 156 | component HSeparator: Separator { 157 | implicitHeight: 1 158 | Layout.fillWidth: true 159 | Layout.leftMargin: fontMetrics.height * 8 160 | Layout.rightMargin: fontMetrics.height * 8 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/QMLIntegration.cpp: -------------------------------------------------------------------------------- 1 | #include "QMLIntegration.hpp" 2 | 3 | #include "core/Agent.hpp" 4 | #include "core/PolkitListener.hpp" 5 | 6 | void CQMLIntegration::onExit() { 7 | g_pAgent->submitResultThreadSafe(result.toStdString()); 8 | } 9 | 10 | void CQMLIntegration::setResult(QString str) { 11 | result = str; 12 | g_pAgent->submitResultThreadSafe(result.toStdString()); 13 | } 14 | 15 | QString CQMLIntegration::getMessage() { 16 | return g_pAgent->listener.session.inProgress ? g_pAgent->listener.session.message : "An application is requesting authentication."; 17 | } 18 | 19 | QString CQMLIntegration::getUser() { 20 | return g_pAgent->listener.session.inProgress ? g_pAgent->listener.session.selectedUser.toString() : "an unknown user"; 21 | } 22 | 23 | void CQMLIntegration::setError(QString str) { 24 | emit setErrorString(str); 25 | } 26 | 27 | void CQMLIntegration::focus() { 28 | emit focusField(); 29 | } 30 | 31 | void CQMLIntegration::setInputBlocked(bool blocked) { 32 | emit blockInput(blocked); 33 | } 34 | -------------------------------------------------------------------------------- /src/QMLIntegration.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class CQMLIntegration : public QObject { 9 | Q_OBJECT; 10 | Q_PROPERTY(QString errorText MEMBER errorText); 11 | 12 | public: 13 | explicit CQMLIntegration(QObject* parent = nullptr) : QObject(parent) { 14 | ; 15 | } 16 | virtual ~CQMLIntegration() { 17 | ; 18 | } 19 | 20 | void setError(QString str); 21 | void focus(); 22 | void setInputBlocked(bool blocked); 23 | 24 | QString result = "fail", errorText = ""; 25 | 26 | Q_INVOKABLE QString getMessage(); 27 | Q_INVOKABLE QString getUser(); 28 | 29 | Q_INVOKABLE void setResult(QString str); 30 | 31 | public slots: 32 | void onExit(); 33 | 34 | signals: 35 | void setErrorString(QString err); 36 | void focusField(); 37 | void blockInput(bool block); 38 | }; 39 | -------------------------------------------------------------------------------- /src/core/Agent.cpp: -------------------------------------------------------------------------------- 1 | #define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE 1 2 | 3 | #include 4 | #include 5 | #include 6 | using namespace Qt::Literals::StringLiterals; 7 | 8 | #include "Agent.hpp" 9 | #include "../QMLIntegration.hpp" 10 | 11 | CAgent::CAgent() { 12 | ; 13 | } 14 | 15 | CAgent::~CAgent() { 16 | ; 17 | } 18 | 19 | bool CAgent::start() { 20 | sessionSubject = makeShared(getpid()); 21 | 22 | listener.registerListener(*sessionSubject, "/org/hyprland/PolicyKit1/AuthenticationAgent"); 23 | 24 | int argc = 1; 25 | char* argv = (char*)"hyprpolkitagent"; 26 | QApplication app(argc, &argv); 27 | 28 | app.setApplicationName("Hyprland Polkit Agent"); 29 | QGuiApplication::setQuitOnLastWindowClosed(false); 30 | 31 | app.exec(); 32 | 33 | return true; 34 | } 35 | 36 | void CAgent::resetAuthState() { 37 | if (authState.authing) { 38 | authState.authing = false; 39 | 40 | if (authState.qmlEngine) 41 | authState.qmlEngine->deleteLater(); 42 | if (authState.qmlIntegration) 43 | authState.qmlIntegration->deleteLater(); 44 | 45 | authState.qmlEngine = nullptr; 46 | authState.qmlIntegration = nullptr; 47 | } 48 | } 49 | 50 | void CAgent::initAuthPrompt() { 51 | resetAuthState(); 52 | 53 | if (!listener.session.inProgress) { 54 | std::print(stderr, "INTERNAL ERROR: Spawning qml prompt but session isn't in progress\n"); 55 | return; 56 | } 57 | 58 | std::print("Spawning qml prompt\n"); 59 | 60 | authState.authing = true; 61 | 62 | authState.qmlIntegration = new CQMLIntegration(); 63 | 64 | if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) 65 | QQuickStyle::setStyle("org.hyprland.style"); 66 | 67 | authState.qmlEngine = new QQmlApplicationEngine(); 68 | authState.qmlEngine->rootContext()->setContextProperty("hpa", authState.qmlIntegration); 69 | authState.qmlEngine->load(QUrl{u"qrc:/qt/qml/hpa/qml/main.qml"_s}); 70 | 71 | authState.qmlIntegration->focusField(); 72 | } 73 | 74 | bool CAgent::resultReady() { 75 | return !lastAuthResult.used; 76 | } 77 | 78 | void CAgent::submitResultThreadSafe(const std::string& result) { 79 | lastAuthResult.used = false; 80 | lastAuthResult.result = result; 81 | 82 | const bool PASS = result.starts_with("auth:"); 83 | 84 | std::print("Got result from qml: {}\n", PASS ? "auth:**PASSWORD**" : result); 85 | 86 | if (PASS) 87 | listener.submitPassword(result.substr(result.find(":") + 1).c_str()); 88 | else 89 | listener.cancelPending(); 90 | } 91 | -------------------------------------------------------------------------------- /src/core/Agent.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "PolkitListener.hpp" 10 | #include 11 | 12 | #include 13 | using namespace Hyprutils::Memory; 14 | #define SP CSharedPointer 15 | #define WP CWeakPointer 16 | 17 | class CQMLIntegration; 18 | 19 | class CAgent { 20 | public: 21 | CAgent(); 22 | ~CAgent(); 23 | 24 | void submitResultThreadSafe(const std::string& result); 25 | void resetAuthState(); 26 | bool start(); 27 | void initAuthPrompt(); 28 | 29 | private: 30 | struct { 31 | bool authing = false; 32 | QQmlApplicationEngine* qmlEngine = nullptr; 33 | CQMLIntegration* qmlIntegration = nullptr; 34 | } authState; 35 | 36 | struct { 37 | std::string result; 38 | bool used = true; 39 | } lastAuthResult; 40 | 41 | CPolkitListener listener; 42 | SP sessionSubject; 43 | 44 | bool resultReady(); 45 | 46 | friend class CQMLIntegration; 47 | friend class CPolkitListener; 48 | }; 49 | 50 | inline std::unique_ptr g_pAgent; -------------------------------------------------------------------------------- /src/core/PolkitListener.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "PolkitListener.hpp" 5 | #include "../QMLIntegration.hpp" 6 | #include "Agent.hpp" 7 | #include 8 | 9 | #include 10 | 11 | using namespace PolkitQt1::Agent; 12 | 13 | CPolkitListener::CPolkitListener(QObject* parent) : Listener(parent) { 14 | ; 15 | } 16 | 17 | void CPolkitListener::initiateAuthentication(const QString& actionId, const QString& message, const QString& iconName, const PolkitQt1::Details& details, const QString& cookie, 18 | const PolkitQt1::Identity::List& identities, AsyncResult* result) { 19 | 20 | std::print("> New authentication session\n"); 21 | 22 | if (session.inProgress) { 23 | result->setError("Authentication in progress"); 24 | result->setCompleted(); 25 | std::print("> REJECTING: Another session present\n"); 26 | return; 27 | } 28 | 29 | if (identities.isEmpty()) { 30 | result->setError("No identities, this is a problem with your system configuration."); 31 | result->setCompleted(); 32 | std::print("> REJECTING: No idents\n"); 33 | return; 34 | } 35 | 36 | session.selectedUser = identities.at(0); 37 | session.cookie = cookie; 38 | session.result = result; 39 | session.actionId = actionId; 40 | session.message = message; 41 | session.iconName = iconName; 42 | session.gainedAuth = false; 43 | session.cancelled = false; 44 | session.inProgress = true; 45 | 46 | g_pAgent->initAuthPrompt(); 47 | 48 | reattempt(); 49 | } 50 | 51 | void CPolkitListener::reattempt() { 52 | session.cancelled = false; 53 | 54 | session.session = new Session(session.selectedUser, session.cookie, session.result); 55 | connect(session.session, SIGNAL(request(QString, bool)), this, SLOT(request(QString, bool))); 56 | connect(session.session, SIGNAL(completed(bool)), this, SLOT(completed(bool))); 57 | connect(session.session, SIGNAL(showError(QString)), this, SLOT(showError(QString))); 58 | connect(session.session, SIGNAL(showInfo(QString)), this, SLOT(showInfo(QString))); 59 | 60 | session.session->initiate(); 61 | } 62 | 63 | bool CPolkitListener::initiateAuthenticationFinish() { 64 | std::print("> initiateAuthenticationFinish()\n"); 65 | return true; 66 | } 67 | 68 | void CPolkitListener::cancelAuthentication() { 69 | std::print("> cancelAuthentication()\n"); 70 | 71 | session.cancelled = true; 72 | 73 | finishAuth(); 74 | } 75 | 76 | void CPolkitListener::request(const QString& request, bool echo) { 77 | std::print("> PKS request: {} echo: {}\n", request.toStdString(), echo); 78 | } 79 | 80 | void CPolkitListener::completed(bool gainedAuthorization) { 81 | std::print("> PKS completed: {}\n", gainedAuthorization ? "Auth successful" : "Auth unsuccessful"); 82 | 83 | session.gainedAuth = gainedAuthorization; 84 | 85 | if (!gainedAuthorization && g_pAgent->authState.qmlIntegration) 86 | g_pAgent->authState.qmlIntegration->setError("Authentication failed"); 87 | 88 | finishAuth(); 89 | } 90 | 91 | void CPolkitListener::showError(const QString& text) { 92 | std::print("> PKS showError: {}\n", text.toStdString()); 93 | 94 | if (g_pAgent->authState.qmlIntegration) 95 | g_pAgent->authState.qmlIntegration->setError(text); 96 | } 97 | 98 | void CPolkitListener::showInfo(const QString& text) { 99 | std::print("> PKS showInfo: {}\n", text.toStdString()); 100 | } 101 | 102 | void CPolkitListener::finishAuth() { 103 | if (!session.inProgress) { 104 | std::print("> finishAuth: ODD. !session.inProgress\n"); 105 | return; 106 | } 107 | 108 | if (!session.gainedAuth && !session.cancelled) { 109 | std::print("> finishAuth: Did not gain auth. Reattempting.\n"); 110 | if (g_pAgent->authState.qmlIntegration) 111 | g_pAgent->authState.qmlIntegration->blockInput(false); 112 | session.session->deleteLater(); 113 | reattempt(); 114 | return; 115 | } 116 | 117 | std::print("> finishAuth: Gained auth, cleaning up.\n"); 118 | 119 | session.inProgress = false; 120 | 121 | if (session.session) { 122 | session.session->result()->setCompleted(); 123 | session.session->deleteLater(); 124 | } else 125 | session.result->setCompleted(); 126 | 127 | g_pAgent->resetAuthState(); 128 | } 129 | 130 | void CPolkitListener::submitPassword(const QString& pass) { 131 | if (!session.session) 132 | return; 133 | 134 | session.session->setResponse(pass); 135 | if (g_pAgent->authState.qmlIntegration) 136 | g_pAgent->authState.qmlIntegration->blockInput(true); 137 | } 138 | 139 | void CPolkitListener::cancelPending() { 140 | if (!session.session) 141 | return; 142 | 143 | session.session->cancel(); 144 | 145 | session.cancelled = true; 146 | 147 | finishAuth(); 148 | } 149 | -------------------------------------------------------------------------------- /src/core/PolkitListener.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class CPolkitListener : public PolkitQt1::Agent::Listener { 12 | Q_OBJECT; 13 | Q_DISABLE_COPY(CPolkitListener); 14 | 15 | public: 16 | CPolkitListener(QObject* parent = nullptr); 17 | ~CPolkitListener() override {}; 18 | 19 | void submitPassword(const QString& pass); 20 | void cancelPending(); 21 | 22 | public Q_SLOTS: 23 | void initiateAuthentication(const QString& actionId, const QString& message, const QString& iconName, const PolkitQt1::Details& details, const QString& cookie, 24 | const PolkitQt1::Identity::List& identities, PolkitQt1::Agent::AsyncResult* result) override; 25 | bool initiateAuthenticationFinish() override; 26 | void cancelAuthentication() override; 27 | 28 | void request(const QString& request, bool echo); 29 | void completed(bool gainedAuthorization); 30 | void showError(const QString& text); 31 | void showInfo(const QString& text); 32 | 33 | private: 34 | struct { 35 | bool inProgress = false, cancelled = false, gainedAuth = false; 36 | QString cookie, message, iconName, actionId; 37 | PolkitQt1::Agent::AsyncResult* result = nullptr; 38 | PolkitQt1::Identity selectedUser; 39 | PolkitQt1::Agent::Session* session = nullptr; 40 | } session; 41 | 42 | void reattempt(); 43 | void finishAuth(); 44 | 45 | friend class CAgent; 46 | friend class CQMLIntegration; 47 | }; 48 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "core/Agent.hpp" 2 | 3 | int main(int argc, char* argv[]) { 4 | g_pAgent = std::make_unique(); 5 | 6 | return g_pAgent->start() == false ? 1 : 0; 7 | } 8 | --------------------------------------------------------------------------------