├── VERSION ├── src ├── helpers │ ├── Log.cpp │ ├── MiscFunctions.hpp │ ├── Timer.hpp │ ├── Timer.cpp │ ├── MiscFunctions.cpp │ └── Log.hpp ├── dbusDefines.hpp ├── includes.hpp ├── meson.build ├── shared │ ├── Session.hpp │ ├── ToplevelMappingManager.hpp │ ├── ToplevelManager.hpp │ ├── Session.cpp │ ├── ToplevelMappingManager.cpp │ ├── ScreencopyShared.hpp │ ├── ToplevelManager.cpp │ └── ScreencopyShared.cpp ├── portals │ ├── Screenshot.hpp │ ├── GlobalShortcuts.hpp │ ├── Screencopy.hpp │ ├── Screenshot.cpp │ └── GlobalShortcuts.cpp ├── main.cpp └── core │ ├── PortalManager.hpp │ └── PortalManager.cpp ├── meson_options.txt ├── contrib ├── config.sample └── systemd │ └── xdg-desktop-portal-hyprland.service.in ├── hyprland-share-picker ├── Makefile ├── mainpicker.cpp ├── elidedbutton.h ├── meson.build ├── mainpicker.h ├── elidedbutton.cpp ├── CMakeLists.txt ├── main.cpp └── mainpicker.ui ├── org.freedesktop.impl.portal.desktop.hyprland.service.in ├── hyprland.portal ├── .editorconfig ├── .gitmodules ├── .builds ├── alpine.yml ├── freebsd.yml └── archlinux.yml ├── .gitignore ├── .github └── workflows │ └── nix-build.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── flake.nix ├── nix ├── default.nix └── overlays.nix ├── .clang-format ├── protocols ├── meson.build ├── wlr-screencopy-unstable-v1.xml └── wlr-foreign-toplevel-management-unstable-v1.xml ├── meson.build ├── flake.lock └── CMakeLists.txt /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.11 2 | -------------------------------------------------------------------------------- /src/helpers/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.hpp" 2 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') 2 | -------------------------------------------------------------------------------- /contrib/config.sample: -------------------------------------------------------------------------------- 1 | [screencast] 2 | output_name= 3 | max_fps=30 4 | chooser_cmd=slurp -f %o -or 5 | chooser_type=simple 6 | -------------------------------------------------------------------------------- /src/dbusDefines.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef std::tuple> dbUasv; -------------------------------------------------------------------------------- /src/includes.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | using namespace Hyprutils::Memory; 5 | #define SP CSharedPointer 6 | #define WP CWeakPointer -------------------------------------------------------------------------------- /hyprland-share-picker/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | mkdir -p build && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja 4 | cmake --build ./build --config Release --target all -j$(shell nproc) -------------------------------------------------------------------------------- /org.freedesktop.impl.portal.desktop.hyprland.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.freedesktop.impl.portal.desktop.hyprland 3 | Exec=@LIBEXECDIR@/xdg-desktop-portal-hyprland 4 | SystemdService=xdg-desktop-portal-hyprland.service 5 | -------------------------------------------------------------------------------- /hyprland.portal: -------------------------------------------------------------------------------- 1 | [portal] 2 | DBusName=org.freedesktop.impl.portal.desktop.hyprland 3 | Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; 4 | UseIn=wlroots;Hyprland;sway;Wayfire;river; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [*.{xml,yml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hyprland-protocols"] 2 | path = subprojects/hyprland-protocols 3 | url = https://github.com/hyprwm/hyprland-protocols 4 | [submodule "subprojects/sdbus-cpp"] 5 | path = subprojects/sdbus-cpp 6 | url = https://github.com/Kistler-Group/sdbus-cpp 7 | -------------------------------------------------------------------------------- /src/helpers/MiscFunctions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | std::string execAndGet(const char* cmd); 6 | void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message); 7 | bool inShellPath(const std::string& exec); 8 | void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode); -------------------------------------------------------------------------------- /contrib/systemd/xdg-desktop-portal-hyprland.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Portal service (Hyprland implementation) 3 | PartOf=graphical-session.target 4 | After=graphical-session.target 5 | ConditionEnvironment=WAYLAND_DISPLAY 6 | 7 | [Service] 8 | Type=dbus 9 | BusName=org.freedesktop.impl.portal.desktop.hyprland 10 | ExecStart=@LIBEXECDIR@/xdg-desktop-portal-hyprland 11 | Restart=on-failure 12 | Slice=session.slice 13 | -------------------------------------------------------------------------------- /hyprland-share-picker/mainpicker.cpp: -------------------------------------------------------------------------------- 1 | #include "mainpicker.h" 2 | #include "./ui_mainpicker.h" 3 | #include 4 | 5 | MainPicker::MainPicker(QWidget *parent) 6 | : QMainWindow(parent) 7 | , ui(new Ui::MainPicker) 8 | { 9 | ui->setupUi(this); 10 | } 11 | 12 | MainPicker::~MainPicker() 13 | { 14 | delete ui; 15 | } 16 | 17 | void MainPicker::onMonitorButtonClicked(QObject* target, QEvent* event) { 18 | qDebug() << "click"; 19 | } 20 | -------------------------------------------------------------------------------- /hyprland-share-picker/elidedbutton.h: -------------------------------------------------------------------------------- 1 | #ifndef ELIDEDBUTTON_H 2 | #define ELIDEDBUTTON_H 3 | 4 | #include 5 | 6 | class ElidedButton : public QPushButton 7 | { 8 | public: 9 | explicit ElidedButton(QWidget *parent = nullptr); 10 | explicit ElidedButton(const QString &text, QWidget *parent = nullptr); 11 | void setText(QString); 12 | 13 | protected: 14 | void resizeEvent(QResizeEvent *); 15 | 16 | private: 17 | void updateText(); 18 | QString og_text; 19 | }; 20 | 21 | #endif // ELIDEDBUTTON_H 22 | -------------------------------------------------------------------------------- /src/helpers/Timer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class CTimer { 7 | public: 8 | CTimer(float ms, std::function callback); 9 | 10 | bool passed() const; 11 | float passedMs() const; 12 | float duration() const; 13 | 14 | std::function m_fnCallback; 15 | 16 | private: 17 | std::chrono::high_resolution_clock::time_point m_tStart; 18 | float m_fDuration; 19 | }; -------------------------------------------------------------------------------- /.builds/alpine.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - elogind-dev 4 | - gcc 5 | - meson 6 | - pipewire-dev 7 | - wayland-dev 8 | - wayland-protocols 9 | - inih-dev 10 | - scdoc 11 | - libdrm 12 | - mesa-dev 13 | sources: 14 | - https://github.com/emersion/xdg-desktop-portal-wlr 15 | tasks: 16 | - setup: | 17 | cd xdg-desktop-portal-wlr 18 | meson -Dauto_features=enabled -Dsystemd=disabled -Dsd-bus-provider=libelogind build/ 19 | - build: | 20 | cd xdg-desktop-portal-wlr 21 | ninja -C build/ 22 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | image: freebsd/latest 2 | packages: 3 | - basu 4 | - libepoll-shim 5 | - meson 6 | - pipewire 7 | - pkgconf 8 | - wayland 9 | - wayland-protocols 10 | - inih 11 | - scdoc 12 | - graphics/libdrm 13 | - graphics/mesa-libs 14 | sources: 15 | - https://github.com/emersion/xdg-desktop-portal-wlr 16 | tasks: 17 | - setup: | 18 | cd xdg-desktop-portal-wlr 19 | meson -Dauto_features=enabled -Dsystemd=disabled -Dsd-bus-provider=basu build/ 20 | - build: | 21 | cd xdg-desktop-portal-wlr 22 | ninja -C build/ 23 | -------------------------------------------------------------------------------- /hyprland-share-picker/meson.build: -------------------------------------------------------------------------------- 1 | # select either qt6 or qt5 2 | qtdep = dependency('qt6', 'qt5', modules: ['Widgets']) 3 | qtver = qtdep.version() 4 | qt = import('qt' + qtver[0]) 5 | 6 | ui_files = qt.compile_ui(sources: 'mainpicker.ui') 7 | moc = qt.compile_moc(headers: 'mainpicker.h') 8 | 9 | sources = files([ 10 | 'main.cpp', 11 | 'mainpicker.cpp', 12 | 'mainpicker.h', 13 | 'elidedbutton.h', 14 | 'elidedbutton.cpp', 15 | ]) 16 | 17 | executable('hyprland-share-picker', 18 | sources, 19 | ui_files, 20 | moc, 21 | dependencies: qtdep, 22 | install: true 23 | ) 24 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | globber = run_command('find', '.', '-name', '*.cpp', check: true) 2 | src = globber.stdout().strip().split('\n') 3 | 4 | executable('xdg-desktop-portal-hyprland', 5 | [src], 6 | dependencies: [ 7 | client_protos, 8 | dependency('gbm'), 9 | dependency('hyprlang'), 10 | dependency('hyprutils'), 11 | dependency('libdrm'), 12 | dependency('libpipewire-0.3'), 13 | dependency('sdbus-c++'), 14 | dependency('threads'), 15 | dependency('wayland-client'), 16 | ], 17 | include_directories: inc, 18 | install: true, 19 | install_dir: get_option('libexecdir') 20 | ) 21 | -------------------------------------------------------------------------------- /src/shared/Session.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../includes.hpp" 4 | 5 | #include 6 | 7 | struct SDBusSession { 8 | std::unique_ptr object; 9 | sdbus::ObjectPath handle; 10 | std::function onDestroy; 11 | }; 12 | 13 | struct SDBusRequest { 14 | std::unique_ptr object; 15 | sdbus::ObjectPath handle; 16 | std::function onDestroy; 17 | }; 18 | 19 | std::unique_ptr createDBusSession(sdbus::ObjectPath handle); 20 | std::unique_ptr createDBusRequest(sdbus::ObjectPath handle); -------------------------------------------------------------------------------- /src/helpers/Timer.cpp: -------------------------------------------------------------------------------- 1 | #include "Timer.hpp" 2 | 3 | CTimer::CTimer(float ms, std::function callback) { 4 | m_fDuration = ms; 5 | m_tStart = std::chrono::high_resolution_clock::now(); 6 | m_fnCallback = callback; 7 | } 8 | 9 | bool CTimer::passed() const { 10 | return std::chrono::high_resolution_clock::now() > (m_tStart + std::chrono::milliseconds((uint64_t)m_fDuration)); 11 | } 12 | 13 | float CTimer::passedMs() const { 14 | return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_tStart).count(); 15 | } 16 | 17 | float CTimer::duration() const { 18 | return m_fDuration; 19 | } -------------------------------------------------------------------------------- /hyprland-share-picker/mainpicker.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINPICKER_H 2 | #define MAINPICKER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | QT_BEGIN_NAMESPACE 10 | namespace Ui { class MainPicker; } 11 | QT_END_NAMESPACE 12 | 13 | class MainPicker : public QMainWindow 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | MainPicker(QWidget *parent = nullptr); 19 | ~MainPicker(); 20 | 21 | void onMonitorButtonClicked(QObject* target, QEvent* event); 22 | 23 | std::unordered_map windowIDs; // button -> id 24 | 25 | private: 26 | Ui::MainPicker *ui; 27 | }; 28 | #endif // MAINPICKER_H 29 | -------------------------------------------------------------------------------- /.builds/archlinux.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - gcc 4 | - clang 5 | - meson 6 | - wayland 7 | - wayland-protocols 8 | - pipewire 9 | - libinih 10 | - scdoc 11 | - mesa 12 | sources: 13 | - https://github.com/emersion/xdg-desktop-portal-wlr 14 | tasks: 15 | - setup: | 16 | cd xdg-desktop-portal-wlr 17 | CC=gcc meson -Dauto_features=enabled -Dsd-bus-provider=libsystemd build-gcc/ 18 | CC=clang meson -Dauto_features=enabled -Dsd-bus-provider=libsystemd build-clang/ 19 | - build-gcc: | 20 | cd xdg-desktop-portal-wlr 21 | ninja -C build-gcc/ 22 | - build-clang: | 23 | cd xdg-desktop-portal-wlr 24 | ninja -C build-clang/ 25 | -------------------------------------------------------------------------------- /hyprland-share-picker/elidedbutton.cpp: -------------------------------------------------------------------------------- 1 | #include "elidedbutton.h" 2 | 3 | ElidedButton::ElidedButton(QWidget *parent) 4 | : QPushButton(parent) 5 | { 6 | } 7 | 8 | ElidedButton::ElidedButton( const QString& text, QWidget* parent ) 9 | : ElidedButton( parent ) 10 | { 11 | setText(text); 12 | } 13 | 14 | void ElidedButton::setText(QString text) 15 | { 16 | og_text = text; 17 | updateText(); 18 | } 19 | 20 | void ElidedButton::resizeEvent(QResizeEvent *event) 21 | { 22 | QPushButton::resizeEvent(event); 23 | updateText(); 24 | } 25 | 26 | void ElidedButton::updateText() 27 | { 28 | QFontMetrics metrics(font()); 29 | QString elided = metrics.elidedText(og_text, Qt::ElideRight, width() - 15); 30 | QPushButton::setText(elided); 31 | } -------------------------------------------------------------------------------- /src/portals/Screenshot.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../dbusDefines.hpp" 5 | 6 | class CScreenshotPortal { 7 | public: 8 | CScreenshotPortal(); 9 | 10 | dbUasv onScreenshot(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options); 11 | dbUasv onPickColor(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options); 12 | 13 | private: 14 | std::unique_ptr m_pObject; 15 | 16 | const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.Screenshot"}; 17 | const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Build folders 55 | build/ 56 | build-*/ 57 | hyprland-share-picker/build/ 58 | 59 | # Generated code files 60 | protocols/*.c* 61 | protocols/*.h* 62 | 63 | # Nix build results 64 | result 65 | result-man 66 | 67 | # Code editors 68 | .vscode/ 69 | 70 | # MISCELLANEOUS 71 | .cache 72 | -------------------------------------------------------------------------------- /src/shared/ToplevelMappingManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "wayland.hpp" 4 | #include "hyprland-toplevel-mapping-v1.hpp" 5 | #include "wlr-foreign-toplevel-management-unstable-v1.hpp" 6 | #include 7 | #include "../includes.hpp" 8 | 9 | class CToplevelMappingManager { 10 | public: 11 | CToplevelMappingManager(SP mgr); 12 | 13 | uint64_t getWindowForToplevel(SP handle); 14 | 15 | private: 16 | SP m_pManager = nullptr; 17 | 18 | std::unordered_map, uint64_t> m_muAddresses; 19 | std::vector> m_vHandles; 20 | void fetchWindowForToplevel(SP handle); 21 | 22 | friend struct SToplevelHandle; 23 | friend class CToplevelManager; 24 | }; -------------------------------------------------------------------------------- /.github/workflows/nix-build.yaml: -------------------------------------------------------------------------------- 1 | name: Build xdph (Nix) 2 | 'on': 3 | - push 4 | - pull_request 5 | - workflow_dispatch 6 | jobs: 7 | nix: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Clone repository 12 | uses: actions/checkout@v3 13 | - name: Install Nix 14 | uses: nixbuild/nix-quick-install-action@v31 15 | with: 16 | nix_conf: | 17 | keep-env-derivations = true 18 | keep-outputs = true 19 | - name: Restore and save Nix store 20 | uses: nix-community/cache-nix-action@v6 21 | with: 22 | primary-key: 'nix-${{ runner.os }}-${{ hashFiles(''**/*.nix'', ''**/flake.lock'') }}' 23 | restore-prefixes-first-match: 'nix-${{ runner.os }}-' 24 | gc-max-store-size-linux: 1G 25 | purge: true 26 | purge-prefixes: 'nix-${{ runner.os }}-' 27 | purge-created: 0 28 | purge-last-accessed: 0 29 | purge-primary-key: never 30 | - name: Build xdg-desktop-portal-hyprland 31 | run: >- 32 | nix build --print-build-logs --extra-substituters 33 | "https://hyprland.cachix.org" 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We closely follow the wlroots [contributing] guidelines where possible. Please 4 | see that document for more information. 5 | 6 | ## Tooling 7 | 8 | Useful tools include `dbus-monitor` to watch requests being made, 9 | and `dbus-send` and the similar `busctl call` for manual dbus calls. 10 | 11 | You can test the integration with the [portal-test] Flatpak app. 12 | 13 | Alternatively you can trigger it with [trigger-screen-shot.py] and 14 | [xdp-screen-cast.py]. 15 | 16 | [contributing]: https://github.com/swaywm/wlroots/blob/master/CONTRIBUTING.md 17 | [portal-test]: https://github.com/matthiasclasen/portal-test 18 | [trigger-screen-shot.py]: https://gist.github.com/danshick/3446dac24c64ce6172eced4ac255ac3d 19 | [xdp-screen-cast.py]: https://gitlab.gnome.org/snippets/19 20 | 21 | ## Alternate *.portal Location 22 | 23 | xdg-desktop-portal will read the XDG_DESKTOP_PORTAL_DIR environment variable for an 24 | alternate path for *.portal files. This can be useful when testing changes to that 25 | portal file, or for testing xdpw without installing it. This feature is undocumented 26 | and shouldn't be relied on, but may be helpful in some circumstances. 27 | 28 | https://github.com/flatpak/xdg-desktop-portal/blob/e7f78640e35debb68fef891fc233c449006d9724/src/portal-impl.c#L124 29 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "helpers/Log.hpp" 4 | #include "core/PortalManager.hpp" 5 | 6 | void printHelp() { 7 | std::cout << R"#(┃ xdg-desktop-portal-hyprland 8 | ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9 | ┃ -v (--verbose) → enable trace logging 10 | ┃ -q (--quiet) → disable logging 11 | ┃ -h (--help) → print this menu 12 | ┃ -V (--version) → print xdph's version 13 | )#"; 14 | } 15 | 16 | int main(int argc, char** argv, char** envp) { 17 | g_pPortalManager = std::make_unique(); 18 | 19 | for (int i = 1; i < argc; ++i) { 20 | std::string arg = argv[i]; 21 | 22 | if (arg == "--verbose" || arg == "-v") 23 | Debug::verbose = true; 24 | 25 | else if (arg == "--quiet" || arg == "-q") 26 | Debug::quiet = true; 27 | 28 | else if (arg == "--help" || arg == "-h") { 29 | printHelp(); 30 | return 0; 31 | } else if (arg == "--version" || arg == "-V") { 32 | std::cout << "xdg-desktop-portal-hyprland v" << XDPH_VERSION << "\n"; 33 | return 0; 34 | } else { 35 | printHelp(); 36 | return 1; 37 | } 38 | } 39 | 40 | Debug::log(LOG, "Initializing xdph..."); 41 | 42 | g_pPortalManager->init(); 43 | 44 | return 0; 45 | } -------------------------------------------------------------------------------- /src/shared/ToplevelManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "wayland.hpp" 4 | #include "wlr-foreign-toplevel-management-unstable-v1.hpp" 5 | #include 6 | #include 7 | #include 8 | #include "../includes.hpp" 9 | 10 | class CToplevelManager; 11 | 12 | struct SToplevelHandle { 13 | SToplevelHandle(SP handle); 14 | std::string windowClass; 15 | std::string windowTitle; 16 | SP handle = nullptr; 17 | CToplevelManager* mgr = nullptr; 18 | }; 19 | 20 | class CToplevelManager { 21 | public: 22 | CToplevelManager(uint32_t name, uint32_t version); 23 | 24 | void activate(); 25 | void deactivate(); 26 | SP handleFromClass(const std::string& windowClass); 27 | SP handleFromHandleLower(uint32_t handle); 28 | SP handleFromHandleFull(uint64_t handle); 29 | 30 | std::vector> m_vToplevels; 31 | 32 | private: 33 | SP m_pManager = nullptr; 34 | 35 | int64_t m_iActivateLocks = 0; 36 | 37 | struct { 38 | uint32_t name = 0; 39 | uint32_t version = 0; 40 | } m_sWaylandConnection; 41 | 42 | friend struct SToplevelHandle; 43 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, vaxerski 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xdg-desktop-portal-hyprland 2 | 3 | An [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal) backend 4 | for Hyprland. 5 | 6 | ## Installing 7 | 8 | First, make sure to install the required dependencies: 9 | 10 | ``` 11 | gbm 12 | hyprland-protocols 13 | hyprlang 14 | hyprutils 15 | hyprwayland-scanner 16 | libdrm 17 | libpipewire-0.3 18 | libspa-0.2 19 | sdbus-cpp 20 | wayland-client 21 | wayland-protocols 22 | ``` 23 | 24 | Then run the build and install command: 25 | 26 | ```sh 27 | git clone --recursive https://github.com/hyprwm/xdg-desktop-portal-hyprland 28 | cd xdg-desktop-portal-hyprland/ 29 | cmake -DCMAKE_INSTALL_LIBEXECDIR=/usr/lib -DCMAKE_INSTALL_PREFIX=/usr -B build 30 | cmake --build build 31 | sudo cmake --install build 32 | ``` 33 | 34 | ## Nix 35 | 36 | > [!CAUTION] 37 | > XDPH should not be used from this flake directly! 38 | > 39 | > Instead, use it from the [Hyprland flake](https://github.com/hyprwm/Hyprland). 40 | 41 | There are two reasons for the above: 42 | 43 | 1. Hyprland depends on XDPH, but XDPH also depends on Hyprland. This results in 44 | a cyclic dependency, which is a nightmare. To counter this, we use the 45 | Nixpkgs Hyprland package in this flake, so that it can be later consumed by 46 | the Hyprland flake while overriding the Hyprland package. 47 | 2. Even if you manually do all the overriding, you may still get it wrong and 48 | lose out on the Cachix cache (which has XDPH as exposed by the Hyprland 49 | flake). 50 | 51 | ## Running, FAQs, etc. 52 | 53 | See 54 | [the Hyprland wiki](https://wiki.hyprland.org/Hypr-Ecosystem/xdg-desktop-portal-hyprland/) 55 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "xdg-desktop-portal-hyprland"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | # 8 | systems.url = "github:nix-systems/default-linux"; 9 | 10 | hyprland-protocols = { 11 | url = "github:hyprwm/hyprland-protocols"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | inputs.systems.follows = "systems"; 14 | }; 15 | 16 | hyprlang = { 17 | url = "github:hyprwm/hyprlang"; 18 | inputs.hyprutils.follows = "hyprutils"; 19 | inputs.nixpkgs.follows = "nixpkgs"; 20 | inputs.systems.follows = "systems"; 21 | }; 22 | 23 | hyprutils = { 24 | url = "github:hyprwm/hyprutils"; 25 | inputs.nixpkgs.follows = "nixpkgs"; 26 | inputs.systems.follows = "systems"; 27 | }; 28 | 29 | hyprwayland-scanner = { 30 | url = "github:hyprwm/hyprwayland-scanner"; 31 | inputs.nixpkgs.follows = "nixpkgs"; 32 | inputs.systems.follows = "systems"; 33 | }; 34 | }; 35 | 36 | outputs = { 37 | self, 38 | nixpkgs, 39 | systems, 40 | ... 41 | } @ inputs: let 42 | inherit (nixpkgs) lib; 43 | eachSystem = lib.genAttrs (import systems); 44 | pkgsFor = eachSystem (system: 45 | import nixpkgs { 46 | localSystem = system; 47 | overlays = [self.overlays.default]; 48 | }); 49 | in { 50 | overlays = import ./nix/overlays.nix {inherit self inputs lib;}; 51 | 52 | packages = eachSystem (system: { 53 | inherit (pkgsFor.${system}) xdg-desktop-portal-hyprland sdbus-cpp_2; 54 | default = self.packages.${system}.xdg-desktop-portal-hyprland; 55 | }); 56 | 57 | formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/shared/Session.cpp: -------------------------------------------------------------------------------- 1 | #include "Session.hpp" 2 | #include "../core/PortalManager.hpp" 3 | #include "../helpers/Log.hpp" 4 | 5 | static int onCloseRequest(SDBusRequest* req) { 6 | Debug::log(TRACE, "[internal] Close Request {}", (void*)req); 7 | 8 | if (!req) 9 | return 0; 10 | 11 | req->onDestroy(); 12 | req->object.release(); 13 | 14 | return 0; 15 | } 16 | 17 | static int onCloseSession(SDBusSession* sess) { 18 | Debug::log(TRACE, "[internal] Close Session {}", (void*)sess); 19 | 20 | if (!sess) 21 | return 0; 22 | 23 | sess->onDestroy(); 24 | sess->object.release(); 25 | 26 | return 0; 27 | } 28 | 29 | std::unique_ptr createDBusSession(sdbus::ObjectPath handle) { 30 | Debug::log(TRACE, "[internal] Create Session {}", handle.c_str()); 31 | 32 | std::unique_ptr pSession = std::make_unique(); 33 | const auto PSESSION = pSession.get(); 34 | 35 | pSession->object = sdbus::createObject(*g_pPortalManager->getConnection(), handle); 36 | 37 | pSession->object->addVTable(sdbus::registerMethod("Close").implementedAs([PSESSION]() { onCloseSession(PSESSION); })).forInterface("org.freedesktop.impl.portal.Session"); 38 | 39 | return pSession; 40 | } 41 | 42 | std::unique_ptr createDBusRequest(sdbus::ObjectPath handle) { 43 | Debug::log(TRACE, "[internal] Create Request {}", handle.c_str()); 44 | 45 | std::unique_ptr pRequest = std::make_unique(); 46 | const auto PREQUEST = pRequest.get(); 47 | 48 | pRequest->object = sdbus::createObject(*g_pPortalManager->getConnection(), handle); 49 | 50 | pRequest->object->addVTable(sdbus::registerMethod("Close").implementedAs([PREQUEST]() { onCloseRequest(PREQUEST); })).forInterface("org.freedesktop.impl.portal.Request"); 51 | 52 | return pRequest; 53 | } -------------------------------------------------------------------------------- /src/shared/ToplevelMappingManager.cpp: -------------------------------------------------------------------------------- 1 | #include "ToplevelMappingManager.hpp" 2 | #include "../helpers/Log.hpp" 3 | #include 4 | 5 | CToplevelMappingManager::CToplevelMappingManager(SP mgr) : m_pManager(mgr) { 6 | Debug::log(LOG, "[toplevel mapping] registered manager"); 7 | } 8 | 9 | void CToplevelMappingManager::fetchWindowForToplevel(SP handle) { 10 | if (!handle) 11 | return; 12 | 13 | Debug::log(TRACE, "[toplevel mapping] fetching window for toplevel at {}", (void*)handle.get()); 14 | auto const HANDLE = makeShared(m_pManager->sendGetWindowForToplevelWlr(handle->resource())); 15 | 16 | m_vHandles.push_back(HANDLE); 17 | 18 | HANDLE->setWindowAddress([this, handle](CCHyprlandToplevelWindowMappingHandleV1* h, uint32_t address_hi, uint32_t address) { 19 | const auto ADDRESS = (uint64_t)address_hi << 32 | address; 20 | m_muAddresses.insert_or_assign(handle, ADDRESS); 21 | Debug::log(TRACE, "[toplevel mapping] mapped toplevel at {} to window {}", (void*)handle.get(), ADDRESS); 22 | std::erase_if(m_vHandles, [&](const auto& other) { return other.get() == h; }); 23 | }); 24 | 25 | HANDLE->setFailed([this, handle](CCHyprlandToplevelWindowMappingHandleV1* h) { 26 | Debug::log(TRACE, "[toplevel mapping] failed to map toplevel at {} to window", (void*)handle.get()); 27 | std::erase_if(m_vHandles, [&](const auto& other) { return other.get() == h; }); 28 | }); 29 | } 30 | 31 | uint64_t CToplevelMappingManager::getWindowForToplevel(CSharedPointer handle) { 32 | auto iter = m_muAddresses.find(handle); 33 | if (iter != m_muAddresses.end()) 34 | return iter->second; 35 | 36 | if (handle) 37 | Debug::log(TRACE, "[toplevel mapping] did not find window address for toplevel at {}", (void*)handle.get()); 38 | 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/helpers/MiscFunctions.cpp: -------------------------------------------------------------------------------- 1 | #include "MiscFunctions.hpp" 2 | #include "../helpers/Log.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | using namespace Hyprutils::OS; 13 | 14 | std::string execAndGet(const char* cmd) { 15 | std::string command = cmd + std::string{" 2>&1"}; 16 | CProcess proc("/bin/sh", {"-c", cmd}); 17 | 18 | if (!proc.runSync()) 19 | return "error"; 20 | 21 | return proc.stdOut(); 22 | } 23 | 24 | void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message) { 25 | const std::string CMD = std::format("hyprctl notify {} {} {} \"{}\"", icon, timeMs, color, message); 26 | Debug::log(LOG, "addHyprlandNotification: {}", CMD); 27 | if (fork() == 0) 28 | execl("/bin/sh", "/bin/sh", "-c", CMD.c_str(), nullptr); 29 | } 30 | 31 | bool inShellPath(const std::string& exec) { 32 | 33 | if (exec.starts_with("/") || exec.starts_with("./") || exec.starts_with("../")) 34 | return std::filesystem::exists(exec); 35 | 36 | // we are relative to our PATH 37 | const char* path = std::getenv("PATH"); 38 | 39 | if (!path) 40 | return false; 41 | 42 | // collect paths 43 | std::string pathString = path; 44 | std::vector paths; 45 | uint32_t nextBegin = 0; 46 | for (uint32_t i = 0; i < pathString.size(); i++) { 47 | if (path[i] == ':') { 48 | paths.push_back(pathString.substr(nextBegin, i - nextBegin)); 49 | nextBegin = i + 1; 50 | } 51 | } 52 | 53 | if (nextBegin < pathString.size()) 54 | paths.push_back(pathString.substr(nextBegin, pathString.size() - nextBegin)); 55 | 56 | return std::ranges::any_of(paths, [&exec](std::string& path) { return access((path + "/" + exec).c_str(), X_OK) == 0; }); 57 | } 58 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | cmake, 5 | makeWrapper, 6 | pkg-config, 7 | wrapQtAppsHook, 8 | hyprland, 9 | hyprland-protocols, 10 | hyprlang, 11 | hyprutils, 12 | hyprwayland-scanner, 13 | libdrm, 14 | libgbm, 15 | pipewire, 16 | qtbase, 17 | qttools, 18 | qtwayland, 19 | sdbus-cpp_2, 20 | slurp, 21 | systemd, 22 | wayland, 23 | wayland-protocols, 24 | wayland-scanner, 25 | debug ? false, 26 | version ? "git", 27 | src, 28 | }: 29 | stdenv.mkDerivation { 30 | pname = "xdg-desktop-portal-hyprland" + lib.optionalString debug "-debug"; 31 | inherit version; 32 | 33 | inherit src; 34 | 35 | depsBuildBuild = [ 36 | pkg-config 37 | ]; 38 | 39 | nativeBuildInputs = [ 40 | cmake 41 | makeWrapper 42 | pkg-config 43 | wrapQtAppsHook 44 | hyprwayland-scanner 45 | ]; 46 | 47 | buildInputs = [ 48 | hyprland-protocols 49 | hyprlang 50 | hyprutils 51 | libdrm 52 | libgbm 53 | pipewire 54 | qtbase 55 | qttools 56 | qtwayland 57 | sdbus-cpp_2 58 | systemd 59 | wayland 60 | wayland-protocols 61 | wayland-scanner 62 | ]; 63 | 64 | cmakeBuildType = 65 | if debug 66 | then "Debug" 67 | else "RelWithDebInfo"; 68 | 69 | dontStrip = true; 70 | 71 | dontWrapQtApps = true; 72 | 73 | postInstall = '' 74 | wrapProgramShell $out/bin/hyprland-share-picker \ 75 | "''${qtWrapperArgs[@]}" \ 76 | --prefix PATH ":" ${lib.makeBinPath [slurp hyprland]} 77 | 78 | wrapProgramShell $out/libexec/xdg-desktop-portal-hyprland \ 79 | --prefix PATH ":" ${lib.makeBinPath [(placeholder "out")]} 80 | ''; 81 | 82 | meta = with lib; { 83 | mainProgram = "xdg-desktop-portal-hyprland"; 84 | homepage = "https://github.com/hyprwm/xdg-desktop-portal-hyprland"; 85 | description = "xdg-desktop-portal backend for Hyprland"; 86 | license = licenses.bsd3; 87 | maintainers = with maintainers; [fufexan]; 88 | platforms = platforms.linux; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /protocols/meson.build: -------------------------------------------------------------------------------- 1 | wayland_protos = dependency('wayland-protocols', 2 | version: '>=1.31', 3 | default_options: ['tests=false'], 4 | ) 5 | 6 | hyprland_protos = dependency('hyprland-protocols', 7 | version: '>=0.6.4', 8 | fallback: 'hyprland-protocols', 9 | ) 10 | 11 | wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') 12 | hl_protocol_dir = hyprland_protos.get_variable('pkgdatadir') 13 | 14 | hyprwayland_scanner_dep = dependency('hyprwayland-scanner', required: true, native: true, version: '>=0.4.2') 15 | hyprwayland_scanner = find_program( 16 | hyprwayland_scanner_dep.get_variable(pkgconfig: 'hyprwayland_scanner'), 17 | native: true, 18 | ) 19 | 20 | client_protocols = [ 21 | 'wlr-screencopy-unstable-v1.xml', 22 | 'wlr-foreign-toplevel-management-unstable-v1.xml', 23 | hl_protocol_dir / 'protocols/hyprland-toplevel-export-v1.xml', 24 | hl_protocol_dir / 'protocols/hyprland-toplevel-mapping-v1.xml', 25 | hl_protocol_dir / 'protocols/hyprland-global-shortcuts-v1.xml', 26 | wl_protocol_dir / 'stable/linux-dmabuf/linux-dmabuf-v1.xml', 27 | wl_protocol_dir / 'staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml', 28 | ] 29 | 30 | wl_proto_files = [] 31 | 32 | foreach xml: client_protocols 33 | wl_proto_files += custom_target( 34 | xml.underscorify() + '_c', 35 | input: xml, 36 | output: ['@BASENAME@.cpp', '@BASENAME@.hpp'], 37 | command: [hyprwayland_scanner, '--client', '@INPUT@', '@OUTDIR@'], 38 | ) 39 | endforeach 40 | 41 | wayland_scanner = dependency('wayland-scanner') 42 | wayland_scanner_dir = wayland_scanner.get_variable('pkgdatadir') 43 | 44 | wayland_xml = wayland_scanner_dir / 'wayland.xml' 45 | wayland_protocol = custom_target( 46 | wayland_xml.underscorify(), 47 | input: wayland_xml, 48 | output: ['@BASENAME@.cpp', '@BASENAME@.hpp'], 49 | command: [hyprwayland_scanner, '--wayland-enums', '--client', '@INPUT@', '@OUTDIR@'], 50 | ) 51 | 52 | lib_client_protos = static_library( 53 | 'client_protos', 54 | wl_proto_files + wayland_protocol, 55 | ) 56 | 57 | client_protos = declare_dependency( 58 | link_with: lib_client_protos, 59 | sources: wl_proto_files + wayland_protocol 60 | ) 61 | -------------------------------------------------------------------------------- /src/shared/ScreencopyShared.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | extern "C" { 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | } 15 | #include "wayland.hpp" 16 | #include "wlr-foreign-toplevel-management-unstable-v1.hpp" 17 | #include "../includes.hpp" 18 | 19 | #define XDPH_PWR_BUFFERS 4 20 | #define XDPH_PWR_BUFFERS_MIN 2 21 | #define XDPH_PWR_ALIGN 16 22 | 23 | enum eSelectionType { 24 | TYPE_INVALID = -1, 25 | TYPE_OUTPUT = 0, 26 | TYPE_WINDOW, 27 | TYPE_GEOMETRY, 28 | TYPE_WORKSPACE, 29 | }; 30 | 31 | struct zwlr_foreign_toplevel_handle_v1; 32 | 33 | struct SSelectionData { 34 | eSelectionType type = TYPE_INVALID; 35 | std::string output; 36 | SP windowHandle = nullptr; 37 | uint32_t x = 0, y = 0, w = 0, h = 0; // for TYPE_GEOMETRY 38 | bool allowToken = false; 39 | 40 | // for restoring 41 | std::string windowClass; 42 | }; 43 | 44 | struct wl_buffer; 45 | 46 | SSelectionData promptForScreencopySelection(); 47 | uint32_t drmFourccFromSHM(wl_shm_format format); 48 | spa_video_format pwFromDrmFourcc(uint32_t format); 49 | wl_shm_format wlSHMFromDrmFourcc(uint32_t format); 50 | spa_video_format pwStripAlpha(spa_video_format format); 51 | std::string getRandName(std::string prefix); 52 | spa_pod* build_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifiers, int modifier_count); 53 | spa_pod* fixate_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifier); 54 | spa_pod* build_buffer(spa_pod_builder* b, uint32_t blocks, uint32_t size, uint32_t stride, uint32_t datatype); 55 | int anonymous_shm_open(); 56 | SP import_wl_shm_buffer(int fd, wl_shm_format fmt, int width, int height, int stride); -------------------------------------------------------------------------------- /hyprland-share-picker/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project( 4 | hyprland-share-picker 5 | VERSION 0.1 6 | LANGUAGES CXX) 7 | 8 | set(QT_VERSION_MAJOR 6) 9 | 10 | set(CMAKE_AUTOUIC ON) 11 | set(CMAKE_AUTOMOC ON) 12 | set(CMAKE_AUTORCC ON) 13 | 14 | set(CMAKE_CXX_STANDARD 17) 15 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 16 | 17 | find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets) 18 | find_package(Qt6 REQUIRED COMPONENTS Widgets) 19 | 20 | find_package(PkgConfig REQUIRED) 21 | pkg_check_modules( 22 | deps 23 | REQUIRED 24 | IMPORTED_TARGET 25 | hyprutils>=0.2.6) 26 | 27 | set(PROJECT_SOURCES main.cpp mainpicker.cpp mainpicker.h mainpicker.ui 28 | elidedbutton.h elidedbutton.cpp) 29 | 30 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 31 | qt_add_executable(hyprland-share-picker MANUAL_FINALIZATION 32 | ${PROJECT_SOURCES}) 33 | # Define target properties for Android with Qt 6 as: set_property(TARGET 34 | # hyprland-share-picker APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR 35 | # ${CMAKE_CURRENT_SOURCE_DIR}/android) For more information, see 36 | # https://doc.qt.io/qt-6/qt-add-executable.html#target-creation 37 | else() 38 | if(ANDROID) 39 | add_library(hyprland-share-picker SHARED ${PROJECT_SOURCES}) 40 | # Define properties for Android with Qt 5 after find_package() calls as: 41 | # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") 42 | else() 43 | add_executable(hyprland-share-picker ${PROJECT_SOURCES}) 44 | endif() 45 | endif() 46 | 47 | target_link_libraries(hyprland-share-picker 48 | PRIVATE Qt${QT_VERSION_MAJOR}::Widgets PkgConfig::deps) 49 | 50 | set_target_properties( 51 | hyprland-share-picker 52 | PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com 53 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 54 | MACOSX_BUNDLE_SHORT_VERSION_STRING 55 | ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 56 | MACOSX_BUNDLE TRUE 57 | WIN32_EXECUTABLE TRUE) 58 | 59 | install( 60 | TARGETS hyprland-share-picker 61 | BUNDLE DESTINATION . 62 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) 63 | 64 | if(QT_VERSION_MAJOR EQUAL 6) 65 | qt_finalize_executable(hyprland-share-picker) 66 | endif() 67 | -------------------------------------------------------------------------------- /nix/overlays.nix: -------------------------------------------------------------------------------- 1 | { 2 | self, 3 | inputs, 4 | lib, 5 | }: let 6 | ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION); 7 | 8 | mkDate = longDate: (lib.concatStringsSep "-" [ 9 | (builtins.substring 0 4 longDate) 10 | (builtins.substring 4 2 longDate) 11 | (builtins.substring 6 2 longDate) 12 | ]); 13 | 14 | version = ver + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); 15 | in { 16 | # List dependencies in ascending order with respect to usage (`foldr`). 17 | default = lib.composeManyExtensions [ 18 | self.overlays.xdg-desktop-portal-hyprland 19 | self.overlays.sdbus-cpp_2 20 | inputs.hyprland-protocols.overlays.default 21 | inputs.hyprwayland-scanner.overlays.default 22 | inputs.hyprlang.overlays.default 23 | inputs.hyprutils.overlays.default 24 | ]; 25 | 26 | xdg-desktop-portal-hyprland = lib.composeManyExtensions [ 27 | (final: prev: { 28 | xdg-desktop-portal-hyprland = final.callPackage ./default.nix { 29 | stdenv = prev.gcc15Stdenv; 30 | inherit (final.qt6) qtbase qttools wrapQtAppsHook qtwayland; 31 | inherit version; 32 | src = self; 33 | }; 34 | }) 35 | ]; 36 | 37 | # If `prev` already contains `sdbus-cpp_2`, do not modify the package set. 38 | # If the previous fixpoint does not contain the attribute, 39 | # create a new package attribute, `sdbus-cpp_2` by overriding `sdbus-cpp` 40 | # from `final` with the new version of `src`. 41 | # 42 | # This matches the naming/versioning scheme used in `nixos-unstable` as of writing (10-27-2024). 43 | # 44 | # This overlay can be applied to either a stable release of Nixpkgs, or any of the unstable branches. 45 | # If you're using an unstable branch (or a release one) which already has `sdbus-cpp_2`, 46 | # this overlay is effectively a wrapper of an identity function. 47 | # 48 | # TODO: Remove this overlay after the next stable Nixpkgs release. 49 | sdbus-cpp_2 = final: prev: { 50 | sdbus-cpp_2 = 51 | prev.sdbus-cpp_2 52 | or (final.sdbus-cpp.overrideAttrs (self: _: { 53 | version = "2.0.0"; 54 | 55 | src = final.fetchFromGitHub { 56 | owner = "Kistler-group"; 57 | repo = "sdbus-cpp"; 58 | rev = "v${self.version}"; 59 | hash = "sha256-W8V5FRhV3jtERMFrZ4gf30OpIQLYoj2yYGpnYOmH2+g="; 60 | }; 61 | })); 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/helpers/Log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | enum eLogLevel { 7 | TRACE = 0, 8 | INFO, 9 | LOG, 10 | WARN, 11 | ERR, 12 | CRIT 13 | }; 14 | 15 | #define RASSERT(expr, reason, ...) \ 16 | if (!(expr)) { \ 17 | Debug::log(CRIT, "\n==========================================================================================\nASSERTION FAILED! \n\n{}\n\nat: line {} in {}", \ 18 | std::format(reason, ##__VA_ARGS__), __LINE__, \ 19 | ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find_last_of('/') + 1); })().c_str()); \ 20 | printf("Assertion failed! See the log in /tmp/hypr/hyprland.log for more info."); \ 21 | abort(); /* so that we crash and get a coredump */ \ 22 | } 23 | 24 | #define ASSERT(expr) RASSERT(expr, "?") 25 | 26 | namespace Debug { 27 | inline bool quiet = false; 28 | inline bool verbose = false; 29 | 30 | template 31 | void log(eLogLevel level, const std::string& fmt, Args&&... args) { 32 | 33 | if (!verbose && level == TRACE) 34 | return; 35 | 36 | if (quiet) 37 | return; 38 | 39 | std::cout << '['; 40 | 41 | switch (level) { 42 | case TRACE: std::cout << "TRACE"; break; 43 | case INFO: std::cout << "INFO"; break; 44 | case LOG: std::cout << "LOG"; break; 45 | case WARN: std::cout << "WARN"; break; 46 | case ERR: std::cout << "ERR"; break; 47 | case CRIT: std::cout << "CRITICAL"; break; 48 | } 49 | 50 | std::cout << "] "; 51 | 52 | std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n"; 53 | } 54 | }; -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('xdg-desktop-portal-hyprland', 'cpp', 'c', 2 | version: run_command('cat', files('VERSION'), check: true).stdout().strip(), 3 | license: 'BSD-3-Clause', 4 | meson_version: '>=0.63.0', 5 | default_options: [ 6 | 'warning_level=2', 7 | 'optimization=3', 8 | 'buildtype=release', 9 | 'debug=false', 10 | # 'cpp_std=c++23' # not yet supported by meson, as of version 0.63.0 11 | ], 12 | ) 13 | 14 | # clang v14.0.6 uses C++2b instead of C++23, so we've gotta account for that 15 | # replace the following with a project default option once meson gets support for C++23 16 | cpp_compiler = meson.get_compiler('cpp') 17 | if cpp_compiler.has_argument('-std=c++23') 18 | add_global_arguments('-std=c++23', language: 'cpp') 19 | elif cpp_compiler.has_argument('-std=c++2b') 20 | add_global_arguments('-std=c++2b', language: 'cpp') 21 | else 22 | error('Could not configure current C++ compiler (' + cpp_compiler.get_id() + ' ' + cpp_compiler.version() + ') with required C++ standard (C++23)') 23 | endif 24 | 25 | add_project_arguments(cpp_compiler.get_supported_arguments([ 26 | '-Wno-missing-field-initializers', 27 | '-Wno-narrowing', 28 | '-Wno-pointer-arith', 29 | '-Wno-unused-parameter', 30 | '-Wno-unused-value', 31 | '-fpermissive', 32 | '-Wno-address-of-temporary' 33 | ]), language: 'cpp') 34 | 35 | conf_data = configuration_data() 36 | conf_data.set('LIBEXECDIR', join_paths(get_option('prefix'), get_option('libexecdir'))) 37 | 38 | systemd = dependency('systemd', required: get_option('systemd')) 39 | 40 | if systemd.found() 41 | systemd_service_file = 'xdg-desktop-portal-hyprland.service' 42 | user_unit_dir = systemd.get_variable(pkgconfig: 'systemduserunitdir', 43 | pkgconfig_define: ['prefix', get_option('prefix')]) 44 | 45 | configure_file( 46 | configuration: conf_data, 47 | input: 'contrib/systemd/' + systemd_service_file + '.in', 48 | output: '@BASENAME@', 49 | install_dir: user_unit_dir, 50 | ) 51 | endif 52 | 53 | configure_file( 54 | configuration: conf_data, 55 | input: 'org.freedesktop.impl.portal.desktop.hyprland.service.in', 56 | output: '@BASENAME@', 57 | install_dir: join_paths(get_option('datadir'), 'dbus-1', 'services'), 58 | ) 59 | 60 | install_data( 61 | 'hyprland.portal', 62 | install_dir: join_paths(get_option('datadir'), 'xdg-desktop-portal', 'portals'), 63 | ) 64 | 65 | inc = include_directories('.', 'protocols') 66 | 67 | subdir('protocols') 68 | subdir('src') 69 | subdir('hyprland-share-picker') 70 | -------------------------------------------------------------------------------- /src/portals/GlobalShortcuts.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "hyprland-global-shortcuts-v1.hpp" 5 | #include "../shared/Session.hpp" 6 | #include "../dbusDefines.hpp" 7 | 8 | struct SKeybind { 9 | SKeybind(SP shortcut); 10 | std::string id, description, preferredTrigger; 11 | SP shortcut = nullptr; 12 | void* session = nullptr; 13 | }; 14 | 15 | class CGlobalShortcutsPortal { 16 | public: 17 | CGlobalShortcutsPortal(SP mgr); 18 | 19 | using DBusShortcut = sdbus::Struct>; 20 | 21 | dbUasv onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); 22 | dbUasv onBindShortcuts(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::vector shortcuts, std::string appID, 23 | std::unordered_map opts); 24 | dbUasv onListShortcuts(sdbus::ObjectPath sessionHandle, sdbus::ObjectPath requestHandle); 25 | 26 | void onActivated(SKeybind* pKeybind, uint64_t time); 27 | void onDeactivated(SKeybind* pKeybind, uint64_t time); 28 | 29 | struct SSession { 30 | std::string appid; 31 | sdbus::ObjectPath requestHandle, sessionHandle; 32 | std::unique_ptr request; 33 | std::unique_ptr session; 34 | 35 | bool registered = false; 36 | 37 | std::vector> keybinds; 38 | }; 39 | 40 | std::vector> m_vSessions; 41 | 42 | private: 43 | struct { 44 | SP manager; 45 | } m_sState; 46 | 47 | std::unique_ptr m_pObject; 48 | 49 | SSession* getSession(sdbus::ObjectPath& path); 50 | SKeybind* getShortcutById(const std::string& appID, const std::string& shortcutId); 51 | SKeybind* registerShortcut(SSession* session, const DBusShortcut& shortcut); 52 | 53 | const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.GlobalShortcuts"}; 54 | const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; 55 | }; -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "hyprland-protocols": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "systems": [ 9 | "systems" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1749046714, 14 | "narHash": "sha256-kymV5FMnddYGI+UjwIw8ceDjdeg7ToDVjbHCvUlhn14=", 15 | "owner": "hyprwm", 16 | "repo": "hyprland-protocols", 17 | "rev": "613878cb6f459c5e323aaafe1e6f388ac8a36330", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "hyprwm", 22 | "repo": "hyprland-protocols", 23 | "type": "github" 24 | } 25 | }, 26 | "hyprlang": { 27 | "inputs": { 28 | "hyprutils": [ 29 | "hyprutils" 30 | ], 31 | "nixpkgs": [ 32 | "nixpkgs" 33 | ], 34 | "systems": [ 35 | "systems" 36 | ] 37 | }, 38 | "locked": { 39 | "lastModified": 1749145882, 40 | "narHash": "sha256-qr0KXeczF8Sma3Ae7+dR2NHhvG7YeLBJv19W4oMu6ZE=", 41 | "owner": "hyprwm", 42 | "repo": "hyprlang", 43 | "rev": "1bfb84f54d50c7ae6558c794d3cfd5f6a7e6e676", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "hyprwm", 48 | "repo": "hyprlang", 49 | "type": "github" 50 | } 51 | }, 52 | "hyprutils": { 53 | "inputs": { 54 | "nixpkgs": [ 55 | "nixpkgs" 56 | ], 57 | "systems": [ 58 | "systems" 59 | ] 60 | }, 61 | "locked": { 62 | "lastModified": 1749135356, 63 | "narHash": "sha256-Q8mAKMDsFbCEuq7zoSlcTuxgbIBVhfIYpX0RjE32PS0=", 64 | "owner": "hyprwm", 65 | "repo": "hyprutils", 66 | "rev": "e36db00dfb3a3d3fdcc4069cb292ff60d2699ccb", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "hyprwm", 71 | "repo": "hyprutils", 72 | "type": "github" 73 | } 74 | }, 75 | "hyprwayland-scanner": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "nixpkgs" 79 | ], 80 | "systems": [ 81 | "systems" 82 | ] 83 | }, 84 | "locked": { 85 | "lastModified": 1749145760, 86 | "narHash": "sha256-IHaGWpGrv7seFWdw/1A+wHtTsPlOGIKMrk1TUIYJEFI=", 87 | "owner": "hyprwm", 88 | "repo": "hyprwayland-scanner", 89 | "rev": "817918315ea016cc2d94004bfb3223b5fd9dfcc6", 90 | "type": "github" 91 | }, 92 | "original": { 93 | "owner": "hyprwm", 94 | "repo": "hyprwayland-scanner", 95 | "type": "github" 96 | } 97 | }, 98 | "nixpkgs": { 99 | "locked": { 100 | "lastModified": 1748929857, 101 | "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", 102 | "owner": "NixOS", 103 | "repo": "nixpkgs", 104 | "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "NixOS", 109 | "ref": "nixos-unstable", 110 | "repo": "nixpkgs", 111 | "type": "github" 112 | } 113 | }, 114 | "root": { 115 | "inputs": { 116 | "hyprland-protocols": "hyprland-protocols", 117 | "hyprlang": "hyprlang", 118 | "hyprutils": "hyprutils", 119 | "hyprwayland-scanner": "hyprwayland-scanner", 120 | "nixpkgs": "nixpkgs", 121 | "systems": "systems" 122 | } 123 | }, 124 | "systems": { 125 | "locked": { 126 | "lastModified": 1689347949, 127 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 128 | "owner": "nix-systems", 129 | "repo": "default-linux", 130 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 131 | "type": "github" 132 | }, 133 | "original": { 134 | "owner": "nix-systems", 135 | "repo": "default-linux", 136 | "type": "github" 137 | } 138 | } 139 | }, 140 | "root": "root", 141 | "version": 7 142 | } 143 | -------------------------------------------------------------------------------- /src/shared/ToplevelManager.cpp: -------------------------------------------------------------------------------- 1 | #include "ToplevelManager.hpp" 2 | #include "../helpers/Log.hpp" 3 | #include "../core/PortalManager.hpp" 4 | 5 | SToplevelHandle::SToplevelHandle(SP handle_) : handle(handle_) { 6 | handle->setTitle([this](CCZwlrForeignToplevelHandleV1* r, const char* title) { 7 | if (title) 8 | windowTitle = title; 9 | 10 | Debug::log(TRACE, "[toplevel] toplevel at {} set title to {}", (void*)this, windowTitle); 11 | }); 12 | handle->setAppId([this](CCZwlrForeignToplevelHandleV1* r, const char* class_) { 13 | if (class_) 14 | windowClass = class_; 15 | 16 | Debug::log(TRACE, "[toplevel] toplevel at {} set class to {}", (void*)this, windowClass); 17 | }); 18 | handle->setClosed([this](CCZwlrForeignToplevelHandleV1* r) { 19 | Debug::log(TRACE, "[toplevel] toplevel at {} closed", (void*)this); 20 | 21 | std::erase_if(g_pPortalManager->m_sHelpers.toplevel->m_vToplevels, [&](const auto& e) { return e.get() == this; }); 22 | if (g_pPortalManager->m_sHelpers.toplevelMapping) 23 | g_pPortalManager->m_sHelpers.toplevelMapping->m_muAddresses.erase(this->handle); 24 | }); 25 | } 26 | 27 | CToplevelManager::CToplevelManager(uint32_t name, uint32_t version) { 28 | m_sWaylandConnection = {name, version}; 29 | } 30 | 31 | void CToplevelManager::activate() { 32 | m_iActivateLocks++; 33 | 34 | Debug::log(LOG, "[toplevel] (activate) locks: {}", m_iActivateLocks); 35 | 36 | if (m_pManager || m_iActivateLocks < 1) 37 | return; 38 | 39 | m_pManager = 40 | makeShared((wl_proxy*)wl_registry_bind((wl_registry*)g_pPortalManager->m_sWaylandConnection.registry->resource(), m_sWaylandConnection.name, 41 | &zwlr_foreign_toplevel_manager_v1_interface, m_sWaylandConnection.version)); 42 | 43 | m_pManager->setToplevel([this](CCZwlrForeignToplevelManagerV1* r, wl_proxy* newHandle) { 44 | Debug::log(TRACE, "[toplevel] New toplevel at {}", (void*)newHandle); 45 | 46 | const auto HANDLE = m_vToplevels.emplace_back(makeShared(makeShared(newHandle))); 47 | if (g_pPortalManager->m_sHelpers.toplevelMapping) 48 | g_pPortalManager->m_sHelpers.toplevelMapping->fetchWindowForToplevel(HANDLE->handle); 49 | }); 50 | m_pManager->setFinished([this](CCZwlrForeignToplevelManagerV1* r) { 51 | m_vToplevels.clear(); 52 | if (g_pPortalManager->m_sHelpers.toplevelMapping) 53 | g_pPortalManager->m_sHelpers.toplevelMapping->m_muAddresses.clear(); 54 | }); 55 | 56 | wl_display_roundtrip(g_pPortalManager->m_sWaylandConnection.display); 57 | 58 | Debug::log(LOG, "[toplevel] Activated, bound to {:x}, toplevels: {}", (uintptr_t)m_pManager, m_vToplevels.size()); 59 | } 60 | 61 | void CToplevelManager::deactivate() { 62 | m_iActivateLocks--; 63 | 64 | Debug::log(LOG, "[toplevel] (deactivate) locks: {}", m_iActivateLocks); 65 | 66 | if (!m_pManager || m_iActivateLocks > 0) 67 | return; 68 | 69 | m_pManager.reset(); 70 | m_vToplevels.clear(); 71 | if (g_pPortalManager->m_sHelpers.toplevelMapping) 72 | g_pPortalManager->m_sHelpers.toplevelMapping->m_muAddresses.clear(); 73 | 74 | Debug::log(LOG, "[toplevel] unbound manager"); 75 | } 76 | 77 | SP CToplevelManager::handleFromClass(const std::string& windowClass) { 78 | for (auto& tl : m_vToplevels) { 79 | if (tl->windowClass == windowClass) 80 | return tl; 81 | } 82 | 83 | return nullptr; 84 | } 85 | 86 | SP CToplevelManager::handleFromHandleLower(uint32_t handle) { 87 | for (auto& tl : m_vToplevels) { 88 | if (((uint64_t)tl->handle->resource() & 0xFFFFFFFF) == handle) 89 | return tl; 90 | } 91 | 92 | return nullptr; 93 | } 94 | 95 | SP CToplevelManager::handleFromHandleFull(uint64_t handle) { 96 | for (auto& tl : m_vToplevels) { 97 | if ((uint64_t)tl->handle->resource() == handle) 98 | return tl; 99 | } 100 | 101 | return nullptr; 102 | } 103 | -------------------------------------------------------------------------------- /src/core/PortalManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "wayland.hpp" 8 | #include "../portals/Screencopy.hpp" 9 | #include "../portals/Screenshot.hpp" 10 | #include "../portals/GlobalShortcuts.hpp" 11 | #include "../helpers/Timer.hpp" 12 | #include "../shared/ToplevelManager.hpp" 13 | #include "../shared/ToplevelMappingManager.hpp" 14 | #include 15 | #include 16 | 17 | #include "hyprland-toplevel-export-v1.hpp" 18 | #include "hyprland-global-shortcuts-v1.hpp" 19 | #include "linux-dmabuf-v1.hpp" 20 | #include "wlr-foreign-toplevel-management-unstable-v1.hpp" 21 | #include "wlr-screencopy-unstable-v1.hpp" 22 | 23 | #include "../includes.hpp" 24 | #include "../dbusDefines.hpp" 25 | 26 | #include 27 | 28 | struct pw_loop; 29 | 30 | struct SOutput { 31 | SOutput(SP); 32 | std::string name; 33 | SP output = nullptr; 34 | uint32_t id = 0; 35 | float refreshRate = 60.0; 36 | wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; 37 | }; 38 | 39 | struct SDMABUFModifier { 40 | uint32_t fourcc = 0; 41 | uint64_t mod = 0; 42 | }; 43 | 44 | class CPortalManager { 45 | public: 46 | CPortalManager(); 47 | 48 | void init(); 49 | 50 | void onGlobal(uint32_t name, const char* interface, uint32_t version); 51 | void onGlobalRemoved(uint32_t name); 52 | 53 | sdbus::IConnection* getConnection(); 54 | SOutput* getOutputFromName(const std::string& name); 55 | 56 | struct { 57 | pw_loop* loop = nullptr; 58 | } m_sPipewire; 59 | 60 | struct { 61 | std::unique_ptr screencopy; 62 | std::unique_ptr screenshot; 63 | std::unique_ptr globalShortcuts; 64 | } m_sPortals; 65 | 66 | struct { 67 | std::unique_ptr toplevel; 68 | std::unique_ptr toplevelMapping; 69 | } m_sHelpers; 70 | 71 | struct { 72 | wl_display* display = nullptr; 73 | SP registry; 74 | SP hyprlandToplevelMgr; 75 | SP linuxDmabuf; 76 | SP linuxDmabufFeedback; 77 | SP shm; 78 | gbm_bo* gbm = nullptr; 79 | gbm_device* gbmDevice = nullptr; 80 | struct { 81 | void* formatTable = nullptr; 82 | size_t formatTableSize = 0; 83 | bool deviceUsed = false; 84 | bool done = false; 85 | } dma; 86 | } m_sWaylandConnection; 87 | 88 | struct { 89 | std::unique_ptr config; 90 | } m_sConfig; 91 | 92 | std::vector m_vDMABUFMods; 93 | 94 | void addTimer(const CTimer& timer); 95 | 96 | gbm_device* createGBMDevice(drmDevice* dev); 97 | 98 | // terminate after the event loop has been created. Before we can exit() 99 | void terminate(); 100 | 101 | private: 102 | void startEventLoop(); 103 | 104 | bool m_bTerminate = false; 105 | pid_t m_iPID = 0; 106 | 107 | struct { 108 | std::condition_variable loopSignal; 109 | std::mutex loopMutex; 110 | std::atomic shouldProcess = false; 111 | std::mutex loopRequestMutex; 112 | } m_sEventLoopInternals; 113 | 114 | struct { 115 | std::condition_variable loopSignal; 116 | std::mutex loopMutex; 117 | bool shouldProcess = false; 118 | std::vector> timers; 119 | std::unique_ptr thread; 120 | } m_sTimersThread; 121 | 122 | std::unique_ptr m_pConnection; 123 | std::vector> m_vOutputs; 124 | 125 | std::mutex m_mEventLock; 126 | }; 127 | 128 | inline std::unique_ptr g_pPortalManager; 129 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19) 2 | 3 | file(READ ${CMAKE_CURRENT_SOURCE_DIR}/VERSION VER) 4 | string(STRIP ${VER} VER) 5 | 6 | project( 7 | xdg-desktop-portal-hyprland 8 | DESCRIPTION "An XDG-Destop-Portal backend for Hyprland (and wlroots)" 9 | VERSION ${VER}) 10 | 11 | set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") 12 | set(SYSTEMD_SERVICES 13 | ON 14 | CACHE BOOL "Install systemd service file") 15 | 16 | if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) 17 | message(STATUS "Configuring XDPH in Debug with CMake") 18 | add_compile_definitions(HYPRLAND_DEBUG) 19 | else() 20 | add_compile_options(-O3) 21 | message(STATUS "Configuring XDPH in Release with CMake") 22 | endif() 23 | 24 | add_compile_definitions(XDPH_VERSION="${VER}") 25 | 26 | include_directories(. "protocols/") 27 | 28 | # configure 29 | include(GNUInstallDirs) 30 | set(LIBEXECDIR ${CMAKE_INSTALL_FULL_LIBEXECDIR}) 31 | configure_file(org.freedesktop.impl.portal.desktop.hyprland.service.in 32 | org.freedesktop.impl.portal.desktop.hyprland.service @ONLY) 33 | if(SYSTEMD_SERVICES) 34 | configure_file(contrib/systemd/xdg-desktop-portal-hyprland.service.in 35 | contrib/systemd/xdg-desktop-portal-hyprland.service @ONLY) 36 | endif() 37 | 38 | set(CMAKE_CXX_STANDARD 23) 39 | add_compile_options( 40 | -Wall 41 | -Wextra 42 | -Wno-unused-parameter 43 | -Wno-unused-value 44 | -Wno-missing-field-initializers 45 | -Wno-narrowing 46 | -Wno-pointer-arith 47 | $<$:-fpermissive> 48 | -Wno-address-of-temporary) 49 | 50 | # dependencies 51 | message(STATUS "Checking deps...") 52 | add_subdirectory(hyprland-share-picker) 53 | 54 | find_package(Threads REQUIRED) 55 | find_package(PkgConfig REQUIRED) 56 | pkg_check_modules( 57 | deps 58 | REQUIRED 59 | IMPORTED_TARGET 60 | wayland-client 61 | wayland-protocols 62 | libpipewire-0.3>=1.1.82 63 | libspa-0.2 64 | libdrm 65 | gbm 66 | hyprlang>=0.2.0 67 | hyprutils>=0.2.6 68 | hyprwayland-scanner>=0.4.2) 69 | 70 | # check whether we can find sdbus-c++ through pkg-config 71 | pkg_check_modules(SDBUS IMPORTED_TARGET sdbus-c++>=2.0.0) 72 | if(NOT SDBUS_FOUND) 73 | include_directories("subprojects/sdbus-cpp/include/") 74 | add_subdirectory(subprojects/sdbus-cpp EXCLUDE_FROM_ALL) 75 | add_library(PkgConfig::SDBUS ALIAS sdbus-c++) 76 | endif() 77 | 78 | # same for hyprland-protocols 79 | pkg_check_modules(HYPRLAND_PROTOS IMPORTED_TARGET hyprland-protocols) 80 | if(HYPRLAND_PROTOS_FOUND) 81 | set(HYPRLAND_PROTOCOLS "${HYPRLAND_PROTOS_PREFIX}/share/hyprland-protocols") 82 | else() 83 | set(HYPRLAND_PROTOCOLS "${CMAKE_SOURCE_DIR}/subprojects/hyprland-protocols") 84 | endif() 85 | 86 | file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") 87 | add_executable(xdg-desktop-portal-hyprland ${SRCFILES}) 88 | target_link_libraries( 89 | xdg-desktop-portal-hyprland PRIVATE rt PkgConfig::SDBUS Threads::Threads 90 | PkgConfig::deps) 91 | 92 | # protocols 93 | pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) 94 | message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") 95 | pkg_get_variable(WAYLAND_SCANNER_DIR wayland-scanner pkgdatadir) 96 | message(STATUS "Found wayland-scanner at ${WAYLAND_SCANNER_DIR}") 97 | 98 | function(protocolnew protoPath protoName external) 99 | if(external) 100 | set(path ${protoPath}) 101 | else() 102 | set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath}) 103 | endif() 104 | add_custom_command( 105 | OUTPUT ${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp 106 | ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp 107 | COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml 108 | ${CMAKE_SOURCE_DIR}/protocols/ 109 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 110 | target_sources(xdg-desktop-portal-hyprland PRIVATE protocols/${protoName}.cpp 111 | protocols/${protoName}.hpp) 112 | endfunction() 113 | function(protocolWayland) 114 | add_custom_command( 115 | OUTPUT ${CMAKE_SOURCE_DIR}/protocols/wayland.cpp 116 | ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp 117 | COMMAND hyprwayland-scanner --wayland-enums --client 118 | ${WAYLAND_SCANNER_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ 119 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 120 | target_sources(xdg-desktop-portal-hyprland PRIVATE protocols/wayland.cpp 121 | protocols/wayland.hpp) 122 | endfunction() 123 | 124 | protocolwayland() 125 | 126 | protocolnew("${CMAKE_SOURCE_DIR}/protocols" 127 | "wlr-foreign-toplevel-management-unstable-v1" true) 128 | protocolnew("${CMAKE_SOURCE_DIR}/protocols" "wlr-screencopy-unstable-v1" true) 129 | protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-global-shortcuts-v1" 130 | true) 131 | protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-toplevel-export-v1" 132 | true) 133 | protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-toplevel-mapping-v1" 134 | true) 135 | protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) 136 | protocolnew("staging/ext-foreign-toplevel-list" "ext-foreign-toplevel-list-v1" false) 137 | 138 | # Installation 139 | install(TARGETS hyprland-share-picker) 140 | install(TARGETS xdg-desktop-portal-hyprland 141 | DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) 142 | 143 | install(FILES hyprland.portal 144 | DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/xdg-desktop-portal/portals") 145 | install( 146 | FILES ${CMAKE_BINARY_DIR}/org.freedesktop.impl.portal.desktop.hyprland.service 147 | DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services") 148 | if(SYSTEMD_SERVICES) 149 | install( 150 | FILES 151 | ${CMAKE_BINARY_DIR}/contrib/systemd/xdg-desktop-portal-hyprland.service 152 | DESTINATION "lib/systemd/user") 153 | endif() 154 | -------------------------------------------------------------------------------- /src/portals/Screencopy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "wlr-screencopy-unstable-v1.hpp" 4 | #include "hyprland-toplevel-export-v1.hpp" 5 | #include 6 | #include 7 | #include 8 | #include "../shared/ScreencopyShared.hpp" 9 | #include 10 | #include "../shared/Session.hpp" 11 | #include "../dbusDefines.hpp" 12 | #include 13 | 14 | enum cursorModes { 15 | HIDDEN = 1, 16 | EMBEDDED = 2, 17 | METADATA = 4, 18 | }; 19 | 20 | enum sourceTypes { 21 | MONITOR = 1, 22 | WINDOW = 2, 23 | VIRTUAL = 4, 24 | }; 25 | 26 | enum frameStatus { 27 | FRAME_NONE = 0, 28 | FRAME_QUEUED, 29 | FRAME_READY, 30 | FRAME_FAILED, 31 | FRAME_RENEG, 32 | }; 33 | 34 | struct pw_context; 35 | struct pw_core; 36 | struct pw_stream; 37 | struct pw_buffer; 38 | 39 | struct SBuffer { 40 | bool isDMABUF = false; 41 | uint32_t w = 0, h = 0, fmt = 0; 42 | int planeCount = 0; 43 | 44 | int fd[4]; 45 | uint32_t size[4], stride[4], offset[4]; 46 | 47 | gbm_bo* bo = nullptr; 48 | 49 | SP wlBuffer = nullptr; 50 | pw_buffer* pwBuffer = nullptr; 51 | }; 52 | 53 | class CPipewireConnection; 54 | 55 | class CScreencopyPortal { 56 | public: 57 | CScreencopyPortal(SP); 58 | 59 | void appendToplevelExport(SP); 60 | 61 | dbUasv onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); 62 | dbUasv onSelectSources(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); 63 | dbUasv onStart(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::string parentWindow, 64 | std::unordered_map opts); 65 | 66 | struct SSession { 67 | std::string appid; 68 | sdbus::ObjectPath requestHandle, sessionHandle; 69 | uint32_t cursorMode = HIDDEN; 70 | uint32_t persistMode = 0; 71 | 72 | std::unique_ptr request; 73 | std::unique_ptr session; 74 | SSelectionData selection; 75 | Hyprutils::Memory::CWeakPointer self; 76 | 77 | void startCopy(); 78 | void initCallbacks(); 79 | 80 | struct { 81 | bool active = false; 82 | SP frameCallback = nullptr; 83 | SP windowFrameCallback = nullptr; 84 | frameStatus status = FRAME_NONE; 85 | uint64_t tvSec = 0; 86 | uint32_t tvNsec = 0; 87 | uint64_t tvTimestampNs = 0; 88 | uint32_t nodeID = 0; 89 | uint32_t framerate = 60; 90 | wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; 91 | std::chrono::system_clock::time_point begunFrame = std::chrono::system_clock::now(); 92 | uint32_t copyRetries = 0; 93 | 94 | struct { 95 | uint32_t w = 0, h = 0, size = 0, stride = 0, fmt = 0; 96 | } frameInfoSHM; 97 | 98 | struct { 99 | uint32_t w = 0, h = 0, fmt = 0; 100 | } frameInfoDMA; 101 | 102 | struct { 103 | uint32_t x = 0, y = 0, w = 0, h = 0; 104 | } damage[4]; 105 | uint32_t damageCount = 0; 106 | } sharingData; 107 | 108 | void onCloseRequest(sdbus::MethodCall&); 109 | void onCloseSession(sdbus::MethodCall&); 110 | }; 111 | 112 | void startFrameCopy(SSession* pSession); 113 | void queueNextShareFrame(SSession* pSession); 114 | bool hasToplevelCapabilities(); 115 | 116 | std::unique_ptr m_pPipewire; 117 | 118 | private: 119 | std::unique_ptr m_pObject; 120 | 121 | std::vector> m_vSessions; 122 | 123 | SSession* getSession(sdbus::ObjectPath& path); 124 | void startSharing(SSession* pSession); 125 | 126 | struct { 127 | SP screencopy = nullptr; 128 | SP toplevel = nullptr; 129 | } m_sState; 130 | 131 | const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.ScreenCast"}; 132 | const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; 133 | 134 | friend struct SSession; 135 | }; 136 | 137 | class CPipewireConnection { 138 | public: 139 | CPipewireConnection(); 140 | ~CPipewireConnection(); 141 | 142 | bool good(); 143 | 144 | void createStream(CScreencopyPortal::SSession* pSession); 145 | void destroyStream(CScreencopyPortal::SSession* pSession); 146 | 147 | void enqueue(CScreencopyPortal::SSession* pSession); 148 | void dequeue(CScreencopyPortal::SSession* pSession); 149 | 150 | struct SPWStream { 151 | CScreencopyPortal::SSession* pSession = nullptr; 152 | pw_stream* stream = nullptr; 153 | bool streamState = false; 154 | spa_hook streamListener; 155 | SBuffer* currentPWBuffer = nullptr; 156 | spa_video_info_raw pwVideoInfo; 157 | uint32_t seq = 0; 158 | bool isDMA = false; 159 | 160 | std::vector> buffers; 161 | }; 162 | 163 | std::unique_ptr createBuffer(SPWStream* pStream, bool dmabuf); 164 | SPWStream* streamFromSession(CScreencopyPortal::SSession* pSession); 165 | void removeSessionFrameCallbacks(CScreencopyPortal::SSession* pSession); 166 | uint32_t buildFormatsFor(spa_pod_builder* b[2], const spa_pod* params[2], SPWStream* stream); 167 | void updateStreamParam(SPWStream* pStream); 168 | 169 | private: 170 | std::vector> m_vStreams; 171 | 172 | bool buildModListFor(SPWStream* stream, uint32_t drmFmt, uint64_t** mods, uint32_t* modCount); 173 | 174 | pw_context* m_pContext = nullptr; 175 | pw_core* m_pCore = nullptr; 176 | }; -------------------------------------------------------------------------------- /src/portals/Screenshot.cpp: -------------------------------------------------------------------------------- 1 | #include "Screenshot.hpp" 2 | #include "../core/PortalManager.hpp" 3 | #include "../helpers/Log.hpp" 4 | #include "../helpers/MiscFunctions.hpp" 5 | 6 | #include 7 | #include 8 | 9 | std::string lastScreenshot; 10 | 11 | // 12 | static dbUasv pickHyprPicker(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { 13 | const std::string HYPRPICKER_CMD = "hyprpicker --format=rgb --no-fancy"; 14 | std::string rgbColor = execAndGet(HYPRPICKER_CMD.c_str()); 15 | 16 | if (rgbColor.size() > 12) { 17 | Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); 18 | return {1, {}}; 19 | } 20 | 21 | std::array colors{0, 0, 0}; 22 | 23 | try { 24 | for (uint8_t i = 0; i < 2; i++) { 25 | uint64_t next = rgbColor.find(' '); 26 | 27 | if (next == std::string::npos) { 28 | Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); 29 | return {1, {}}; 30 | } 31 | 32 | colors[i] = std::stoi(rgbColor.substr(0, next)); 33 | rgbColor = rgbColor.substr(next + 1, rgbColor.size() - next); 34 | } 35 | colors[2] = std::stoi(rgbColor); 36 | } catch (...) { 37 | Debug::log(ERR, "Reading RGB values from hyprpicker failed. This is likely a string to integer error."); 38 | return {1, {}}; 39 | } 40 | 41 | auto [r, g, b] = colors; 42 | std::unordered_map results; 43 | results["color"] = sdbus::Variant{sdbus::Struct(r / 255.0, g / 255.0, b / 255.0)}; 44 | 45 | return {0, results}; 46 | } 47 | 48 | static dbUasv pickSlurp(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { 49 | const std::string PICK_COLOR_CMD = "grim -g \"$(slurp -p)\" -t ppm -"; 50 | std::string ppmColor = execAndGet(PICK_COLOR_CMD.c_str()); 51 | 52 | // unify whitespace 53 | ppmColor = std::regex_replace(ppmColor, std::regex("\\s+"), std::string(" ")); 54 | 55 | // check if we got a 1x1 PPM Image 56 | if (!ppmColor.starts_with("P6 1 1 ")) { 57 | Debug::log(ERR, "grim did not return a PPM Image for us."); 58 | return {1, {}}; 59 | } 60 | 61 | // convert it to a rgb value 62 | try { 63 | std::string maxValString = ppmColor.substr(7, ppmColor.size()); 64 | maxValString = maxValString.substr(0, maxValString.find(' ')); 65 | uint32_t maxVal = std::stoi(maxValString); 66 | 67 | double r, g, b; 68 | 69 | // 1 byte per triplet 70 | if (maxVal < 256) { 71 | std::string byteString = ppmColor.substr(11, 14); 72 | 73 | r = (uint8_t)byteString[0] / (maxVal * 1.0); 74 | g = (uint8_t)byteString[1] / (maxVal * 1.0); 75 | b = (uint8_t)byteString[2] / (maxVal * 1.0); 76 | } else { 77 | // 2 byte per triplet (MSB first) 78 | std::string byteString = ppmColor.substr(11, 17); 79 | 80 | r = ((byteString[0] << 8) | byteString[1]) / (maxVal * 1.0); 81 | g = ((byteString[2] << 8) | byteString[3]) / (maxVal * 1.0); 82 | b = ((byteString[4] << 8) | byteString[5]) / (maxVal * 1.0); 83 | } 84 | 85 | std::unordered_map results; 86 | results["color"] = sdbus::Variant{sdbus::Struct(r, g, b)}; 87 | 88 | return {0, results}; 89 | } catch (...) { Debug::log(ERR, "Converting PPM to RGB failed. This is likely a string to integer error."); } 90 | 91 | return {1, {}}; 92 | } 93 | 94 | CScreenshotPortal::CScreenshotPortal() { 95 | m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); 96 | 97 | m_pObject 98 | ->addVTable( 99 | sdbus::registerMethod("Screenshot").implementedAs([this](sdbus::ObjectPath o, std::string s1, std::string s2, std::unordered_map m) { 100 | return onScreenshot(o, s1, s2, m); 101 | }), 102 | sdbus::registerMethod("PickColor").implementedAs([this](sdbus::ObjectPath o, std::string s1, std::string s2, std::unordered_map m) { 103 | return onPickColor(o, s1, s2, m); 104 | }), 105 | sdbus::registerProperty("version").withGetter([]() { return uint32_t{2}; })) 106 | .forInterface(INTERFACE_NAME); 107 | 108 | Debug::log(LOG, "[screenshot] init successful"); 109 | } 110 | 111 | dbUasv CScreenshotPortal::onScreenshot(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { 112 | 113 | Debug::log(LOG, "[screenshot] New screenshot request:"); 114 | Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); 115 | Debug::log(LOG, "[screenshot] | appid: {}", appID); 116 | 117 | bool isInteractive = options.count("interactive") && options["interactive"].get() && inShellPath("slurp"); 118 | 119 | // make screenshot 120 | 121 | const auto RUNTIME_DIR = getenv("XDG_RUNTIME_DIR"); 122 | srand(time(nullptr)); 123 | 124 | const std::string HYPR_DIR = RUNTIME_DIR ? std::string{RUNTIME_DIR} + "/hypr/" : "/tmp/hypr/"; 125 | const std::string SNAP_FILE = std::format("xdph_screenshot_{:x}.png", rand()); // rand() is good enough 126 | const std::string FILE_PATH = HYPR_DIR + SNAP_FILE; 127 | const std::string SNAP_CMD = "grim '" + FILE_PATH + "'"; 128 | const std::string SNAP_INTERACTIVE_CMD = "grim -g \"$(slurp)\" '" + FILE_PATH + "'"; 129 | 130 | std::unordered_map results; 131 | results["uri"] = sdbus::Variant{"file://" + FILE_PATH}; 132 | 133 | std::filesystem::remove(FILE_PATH); 134 | std::filesystem::create_directory(HYPR_DIR); 135 | 136 | // remove last screenshot. This could cause issues if the app hasn't read the screenshot back yet, but oh well. 137 | if (!lastScreenshot.empty()) 138 | std::filesystem::remove(lastScreenshot); 139 | lastScreenshot = FILE_PATH; 140 | 141 | if (isInteractive) 142 | execAndGet(SNAP_INTERACTIVE_CMD.c_str()); 143 | else 144 | execAndGet(SNAP_CMD.c_str()); 145 | 146 | uint32_t responseCode = std::filesystem::exists(FILE_PATH) ? 0 : 1; 147 | 148 | return {responseCode, results}; 149 | } 150 | 151 | dbUasv CScreenshotPortal::onPickColor(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { 152 | 153 | Debug::log(LOG, "[screenshot] New PickColor request:"); 154 | Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); 155 | Debug::log(LOG, "[screenshot] | appid: {}", appID); 156 | 157 | bool hyprPickerInstalled = inShellPath("hyprpicker"); 158 | bool slurpInstalled = inShellPath("slurp"); 159 | 160 | if (!slurpInstalled && !hyprPickerInstalled) { 161 | Debug::log(ERR, "Neither slurp nor hyprpicker found. We can't pick colors."); 162 | return {1, {}}; 163 | } 164 | 165 | // use hyprpicker if installed, slurp as fallback 166 | if (hyprPickerInstalled) 167 | return pickHyprPicker(requestHandle, appID, parentWindow, options); 168 | else 169 | return pickSlurp(requestHandle, appID, parentWindow, options); 170 | } 171 | -------------------------------------------------------------------------------- /src/portals/GlobalShortcuts.cpp: -------------------------------------------------------------------------------- 1 | #include "GlobalShortcuts.hpp" 2 | #include "../core/PortalManager.hpp" 3 | #include "../helpers/Log.hpp" 4 | 5 | SKeybind::SKeybind(SP shortcut_) : shortcut(shortcut_) { 6 | shortcut->setPressed([this](CCHyprlandGlobalShortcutV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { 7 | g_pPortalManager->m_sPortals.globalShortcuts->onActivated(this, ((uint64_t)tv_sec_hi << 32) | (uint64_t)(tv_sec_lo)); 8 | }); 9 | shortcut->setReleased([this](CCHyprlandGlobalShortcutV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { 10 | g_pPortalManager->m_sPortals.globalShortcuts->onDeactivated(this, ((uint64_t)tv_sec_hi << 32) | (uint64_t)(tv_sec_lo)); 11 | }); 12 | } 13 | 14 | // 15 | 16 | CGlobalShortcutsPortal::SSession* CGlobalShortcutsPortal::getSession(sdbus::ObjectPath& path) { 17 | for (auto& s : m_vSessions) { 18 | if (s->sessionHandle == path) 19 | return s.get(); 20 | } 21 | 22 | return nullptr; 23 | } 24 | 25 | SKeybind* CGlobalShortcutsPortal::getShortcutById(const std::string& appID, const std::string& shortcutId) { 26 | for (auto& s : m_vSessions) { 27 | if (s->appid != appID) 28 | continue; 29 | 30 | for (auto& keybind : s->keybinds) { 31 | if (keybind->id == shortcutId) 32 | return keybind.get(); 33 | } 34 | } 35 | 36 | return nullptr; 37 | } 38 | 39 | SKeybind* CGlobalShortcutsPortal::registerShortcut(SSession* session, const DBusShortcut& shortcut) { 40 | std::string id = shortcut.get<0>(); 41 | std::unordered_map data = shortcut.get<1>(); 42 | std::string description; 43 | 44 | for (auto& [k, v] : data) { 45 | if (k == "description") 46 | description = v.get(); 47 | else 48 | Debug::log(LOG, "[globalshortcuts] unknown shortcut data type {}", k); 49 | } 50 | 51 | auto* PSHORTCUT = getShortcutById(session->appid, id); 52 | if (PSHORTCUT) 53 | Debug::log(WARN, "[globalshortcuts] shortcut {} already registered for appid {}", id, session->appid); 54 | else { 55 | PSHORTCUT = session->keybinds 56 | .emplace_back(std::make_unique( 57 | makeShared(m_sState.manager->sendRegisterShortcut(id.c_str(), session->appid.c_str(), description.c_str(), "")))) 58 | .get(); 59 | } 60 | 61 | PSHORTCUT->id = std::move(id); 62 | PSHORTCUT->description = std::move(description); 63 | PSHORTCUT->session = session; 64 | 65 | return PSHORTCUT; 66 | } 67 | 68 | dbUasv CGlobalShortcutsPortal::onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, 69 | std::unordered_map opts) { 70 | Debug::log(LOG, "[globalshortcuts] New session:"); 71 | Debug::log(LOG, "[globalshortcuts] | {}", requestHandle.c_str()); 72 | Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); 73 | Debug::log(LOG, "[globalshortcuts] | appid: {}", appID); 74 | 75 | const auto PSESSION = m_vSessions.emplace_back(std::make_unique(appID, requestHandle, sessionHandle)).get(); 76 | 77 | // create objects 78 | PSESSION->session = createDBusSession(sessionHandle); 79 | PSESSION->session->onDestroy = [PSESSION]() { PSESSION->session.release(); }; 80 | PSESSION->request = createDBusRequest(requestHandle); 81 | PSESSION->request->onDestroy = [PSESSION]() { PSESSION->request.release(); }; 82 | 83 | for (auto& [k, v] : opts) { 84 | if (k == "shortcuts") { 85 | PSESSION->registered = true; 86 | 87 | std::vector shortcuts = v.get>(); 88 | 89 | for (auto& s : shortcuts) { 90 | registerShortcut(PSESSION, s); 91 | } 92 | 93 | Debug::log(LOG, "[globalshortcuts] registered {} shortcuts", shortcuts.size()); 94 | } 95 | } 96 | 97 | return {0, {}}; 98 | } 99 | 100 | dbUasv CGlobalShortcutsPortal::onBindShortcuts(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::vector shortcuts, std::string appID, 101 | std::unordered_map opts) { 102 | Debug::log(LOG, "[globalshortcuts] Bind keys:"); 103 | Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); 104 | 105 | const auto PSESSION = getSession(sessionHandle); 106 | 107 | if (!PSESSION) { 108 | Debug::log(ERR, "[globalshortcuts] No session?"); 109 | return {1, {}}; 110 | } 111 | 112 | std::vector shortcutsToReturn; 113 | 114 | PSESSION->registered = true; 115 | 116 | for (auto& s : shortcuts) { 117 | const auto* PSHORTCUT = registerShortcut(PSESSION, s); 118 | 119 | std::unordered_map shortcutData; 120 | shortcutData["description"] = sdbus::Variant{PSHORTCUT->description}; 121 | shortcutData["trigger_description"] = sdbus::Variant{""}; 122 | shortcutsToReturn.push_back({PSHORTCUT->id, shortcutData}); 123 | } 124 | 125 | Debug::log(LOG, "[globalshortcuts] registered {} shortcuts", shortcuts.size()); 126 | 127 | std::unordered_map data; 128 | data["shortcuts"] = sdbus::Variant{shortcutsToReturn}; 129 | 130 | return {0, data}; 131 | } 132 | 133 | dbUasv CGlobalShortcutsPortal::onListShortcuts(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle) { 134 | Debug::log(LOG, "[globalshortcuts] List keys:"); 135 | Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); 136 | 137 | const auto PSESSION = getSession(sessionHandle); 138 | 139 | if (!PSESSION) { 140 | Debug::log(ERR, "[globalshortcuts] No session?"); 141 | return {1, {}}; 142 | } 143 | 144 | std::vector shortcuts; 145 | 146 | for (auto& s : PSESSION->keybinds) { 147 | std::unordered_map opts; 148 | opts["description"] = sdbus::Variant{s->description}; 149 | opts["trigger_description"] = sdbus::Variant{""}; 150 | shortcuts.push_back({s->id, opts}); 151 | } 152 | 153 | std::unordered_map data; 154 | data["shortcuts"] = sdbus::Variant{shortcuts}; 155 | 156 | return {0, data}; 157 | } 158 | 159 | CGlobalShortcutsPortal::CGlobalShortcutsPortal(SP mgr) { 160 | m_sState.manager = mgr; 161 | 162 | m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); 163 | 164 | m_pObject 165 | ->addVTable(sdbus::registerMethod("CreateSession") 166 | .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s, std::unordered_map m) { 167 | return onCreateSession(o1, o2, s, m); 168 | }), 169 | sdbus::registerMethod("BindShortcuts") 170 | .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::vector v1, std::string s1, 171 | std::unordered_map m2) { return onBindShortcuts(o1, o2, v1, s1, m2); }), 172 | sdbus::registerMethod("ListShortcuts").implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2) { return onListShortcuts(o1, o2); }), 173 | sdbus::registerSignal("Activated").withParameters>(), 174 | sdbus::registerSignal("Deactivated").withParameters>(), 175 | sdbus::registerSignal("ShortcutsChanged").withParameters>>()) 176 | .forInterface(INTERFACE_NAME); 177 | 178 | Debug::log(LOG, "[globalshortcuts] registered"); 179 | } 180 | 181 | void CGlobalShortcutsPortal::onActivated(SKeybind* pKeybind, uint64_t time) { 182 | const auto PSESSION = (CGlobalShortcutsPortal::SSession*)pKeybind->session; 183 | 184 | Debug::log(TRACE, "[gs] Session {} called activated on {}", PSESSION->sessionHandle.c_str(), pKeybind->id); 185 | 186 | m_pObject->emitSignal("Activated").onInterface(INTERFACE_NAME).withArguments(PSESSION->sessionHandle, pKeybind->id, time, std::unordered_map{}); 187 | } 188 | 189 | void CGlobalShortcutsPortal::onDeactivated(SKeybind* pKeybind, uint64_t time) { 190 | const auto PSESSION = (CGlobalShortcutsPortal::SSession*)pKeybind->session; 191 | 192 | Debug::log(TRACE, "[gs] Session {} called deactivated on {}", PSESSION->sessionHandle.c_str(), pKeybind->id); 193 | 194 | m_pObject->emitSignal("Deactivated").onInterface(INTERFACE_NAME).withArguments(PSESSION->sessionHandle, pKeybind->id, time, std::unordered_map{}); 195 | } 196 | -------------------------------------------------------------------------------- /hyprland-share-picker/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | using namespace Hyprutils::OS; 20 | 21 | #include "mainpicker.h" 22 | #include "elidedbutton.h" 23 | 24 | std::string execAndGet(const char* cmd) { 25 | std::string command = cmd + std::string{" 2>&1"}; 26 | CProcess proc("/bin/sh", {"-c", cmd}); 27 | 28 | if (!proc.runSync()) 29 | return "error"; 30 | 31 | return proc.stdOut(); 32 | } 33 | 34 | QApplication* pickerPtr = nullptr; 35 | MainPicker* mainPickerPtr = nullptr; 36 | 37 | struct SWindowEntry { 38 | std::string name; 39 | std::string clazz; 40 | unsigned long long id = 0; 41 | }; 42 | 43 | std::vector getWindows(const char* env) { 44 | std::vector result; 45 | 46 | if (!env) 47 | return result; 48 | 49 | std::string rolling = env; 50 | 51 | while (!rolling.empty()) { 52 | // ID 53 | const auto IDSEPPOS = rolling.find("[HC>]"); 54 | const auto IDSTR = rolling.substr(0, IDSEPPOS); 55 | 56 | // class 57 | const auto CLASSSEPPOS = rolling.find("[HT>]"); 58 | const auto CLASSSTR = rolling.substr(IDSEPPOS + 5, CLASSSEPPOS - IDSEPPOS - 5); 59 | 60 | // title 61 | const auto TITLESEPPOS = rolling.find("[HE>]"); 62 | const auto TITLESTR = rolling.substr(CLASSSEPPOS + 5, TITLESEPPOS - 5 - CLASSSEPPOS); 63 | 64 | // window address 65 | const auto WINDOWSEPPOS = rolling.find("[HA>]"); 66 | const auto WINDOWADDR = rolling.substr(TITLESEPPOS + 5, WINDOWSEPPOS - 5 - TITLESEPPOS); 67 | 68 | try { 69 | result.push_back({TITLESTR, CLASSSTR, std::stoull(IDSTR)}); 70 | } catch (std::exception& e) { 71 | // silent err 72 | } 73 | 74 | rolling = rolling.substr(WINDOWSEPPOS + 5); 75 | } 76 | 77 | return result; 78 | } 79 | 80 | int main(int argc, char* argv[]) { 81 | qputenv("QT_LOGGING_RULES", "qml=false"); 82 | 83 | bool allowTokenByDefault = false; 84 | for (int i = 1; i < argc; ++i) { 85 | if (argv[i] == std::string{"--allow-token"}) 86 | allowTokenByDefault = true; 87 | } 88 | 89 | const char* WINDOWLISTSTR = getenv("XDPH_WINDOW_SHARING_LIST"); 90 | const auto WINDOWLIST = getWindows(WINDOWLISTSTR); 91 | 92 | QApplication picker(argc, argv); 93 | pickerPtr = &picker; 94 | MainPicker w; 95 | mainPickerPtr = &w; 96 | 97 | QSettings* settings = new QSettings("/tmp/hypr/hyprland-share-picker.conf", QSettings::IniFormat); 98 | w.setGeometry(0, 0, settings->value("width").toInt(), settings->value("height").toInt()); 99 | 100 | QCoreApplication::setApplicationName("org.hyprland.xdg-desktop-portal-hyprland"); 101 | 102 | // get the tabwidget 103 | const auto TABWIDGET = w.findChild("tabWidget"); 104 | const auto ALLOWTOKENBUTTON = w.findChild("checkBox"); 105 | 106 | if (allowTokenByDefault) 107 | ALLOWTOKENBUTTON->setCheckState(Qt::CheckState::Checked); 108 | 109 | const auto TAB1 = (QWidget*)TABWIDGET->children()[0]; 110 | 111 | const auto SCREENS_SCROLL_AREA_CONTENTS = 112 | (QWidget*)TAB1->findChild("screens")->findChild("scrollArea")->findChild("scrollAreaWidgetContents"); 113 | 114 | const auto SCREENS_SCROLL_AREA_CONTENTS_LAYOUT = SCREENS_SCROLL_AREA_CONTENTS->layout(); 115 | 116 | // add all screens 117 | const auto SCREENS = picker.screens(); 118 | 119 | constexpr int BUTTON_HEIGHT = 41; 120 | 121 | for (int i = 0; i < SCREENS.size(); ++i) { 122 | const auto GEOMETRY = SCREENS[i]->geometry(); 123 | 124 | QString text = QString::fromStdString(std::string("Screen " + std::to_string(i) + " at " + std::to_string(GEOMETRY.x()) + ", " + std::to_string(GEOMETRY.y()) + " (" + 125 | std::to_string(GEOMETRY.width()) + "x" + std::to_string(GEOMETRY.height()) + ") (") + 126 | SCREENS[i]->name().toStdString() + ")"); 127 | QString outputName = SCREENS[i]->name(); 128 | ElidedButton* button = new ElidedButton(text); 129 | button->setMinimumSize(0, BUTTON_HEIGHT); 130 | SCREENS_SCROLL_AREA_CONTENTS_LAYOUT->addWidget(button); 131 | 132 | QObject::connect(button, &QPushButton::clicked, [=]() { 133 | std::cout << "[SELECTION]"; 134 | std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); 135 | std::cout << "/"; 136 | 137 | std::cout << "screen:" << outputName.toStdString() << "\n"; 138 | 139 | settings->setValue("width", mainPickerPtr->width()); 140 | settings->setValue("height", mainPickerPtr->height()); 141 | settings->sync(); 142 | 143 | pickerPtr->quit(); 144 | return 0; 145 | }); 146 | } 147 | 148 | QSpacerItem* SCREENS_SPACER = new QSpacerItem(0, 10000, QSizePolicy::Expanding, QSizePolicy::Expanding); 149 | SCREENS_SCROLL_AREA_CONTENTS_LAYOUT->addItem(SCREENS_SPACER); 150 | 151 | // windows 152 | const auto WINDOWS_SCROLL_AREA_CONTENTS = 153 | (QWidget*)TAB1->findChild("windows")->findChild("scrollArea_2")->findChild("scrollAreaWidgetContents_2"); 154 | 155 | const auto WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT = WINDOWS_SCROLL_AREA_CONTENTS->layout(); 156 | 157 | // loop over them 158 | int windowIterator = 0; 159 | for (auto& window : WINDOWLIST) { 160 | QString text = QString::fromStdString(window.clazz + ": " + window.name); 161 | 162 | ElidedButton* button = new ElidedButton(text); 163 | button->setMinimumSize(0, BUTTON_HEIGHT); 164 | WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT->addWidget(button); 165 | 166 | mainPickerPtr->windowIDs[button] = window.id; 167 | 168 | QObject::connect(button, &QPushButton::clicked, [=]() { 169 | std::cout << "[SELECTION]"; 170 | std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); 171 | std::cout << "/"; 172 | 173 | std::cout << "window:" << mainPickerPtr->windowIDs[button] << "\n"; 174 | 175 | settings->setValue("width", mainPickerPtr->width()); 176 | settings->setValue("height", mainPickerPtr->height()); 177 | settings->sync(); 178 | 179 | pickerPtr->quit(); 180 | return 0; 181 | }); 182 | 183 | windowIterator++; 184 | } 185 | 186 | QSpacerItem* WINDOWS_SPACER = new QSpacerItem(0, 10000, QSizePolicy::Expanding, QSizePolicy::Expanding); 187 | WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT->addItem(WINDOWS_SPACER); 188 | 189 | // lastly, region 190 | const auto REGION_OBJECT = (QWidget*)TAB1->findChild("region"); 191 | const auto REGION_LAYOUT = REGION_OBJECT->layout(); 192 | 193 | QString text = "Select region..."; 194 | 195 | ElidedButton* button = new ElidedButton(text); 196 | button->setMaximumSize(400, BUTTON_HEIGHT); 197 | REGION_LAYOUT->addWidget(button); 198 | 199 | QObject::connect(button, &QPushButton::clicked, [=]() { 200 | auto REGION = execAndGet("slurp -f \"%o %x %y %w %h\""); 201 | REGION = REGION.substr(0, REGION.length()); 202 | 203 | // now, get the screen 204 | QScreen* pScreen = nullptr; 205 | if (REGION.find_first_of(' ') == std::string::npos) { 206 | std::cout << "error1\n"; 207 | pickerPtr->quit(); 208 | return 1; 209 | } 210 | const auto SCREEN_NAME = REGION.substr(0, REGION.find_first_of(' ')); 211 | 212 | for (auto& screen : SCREENS) { 213 | if (screen->name().toStdString() == SCREEN_NAME) { 214 | pScreen = screen; 215 | break; 216 | } 217 | } 218 | 219 | if (!pScreen) { 220 | std::cout << "error2\n"; 221 | pickerPtr->quit(); 222 | return 1; 223 | } 224 | 225 | // get all the coords 226 | try { 227 | REGION = REGION.substr(REGION.find_first_of(' ') + 1); 228 | const auto X = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); 229 | REGION = REGION.substr(REGION.find_first_of(' ') + 1); 230 | const auto Y = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); 231 | REGION = REGION.substr(REGION.find_first_of(' ') + 1); 232 | const auto W = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); 233 | REGION = REGION.substr(REGION.find_first_of(' ') + 1); 234 | const auto H = std::stoi(REGION); 235 | 236 | std::cout << "[SELECTION]"; 237 | std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); 238 | std::cout << "/"; 239 | 240 | std::cout << "region:" << SCREEN_NAME << "@" << X - pScreen->geometry().x() << "," << Y - pScreen->geometry().y() << "," << W << "," << H << "\n"; 241 | 242 | settings->setValue("width", mainPickerPtr->width()); 243 | settings->setValue("height", mainPickerPtr->height()); 244 | settings->sync(); 245 | 246 | pickerPtr->quit(); 247 | return 0; 248 | } catch (...) { 249 | std::cout << "error3\n"; 250 | pickerPtr->quit(); 251 | return 1; 252 | } 253 | 254 | std::cout << "error4\n"; 255 | pickerPtr->quit(); 256 | return 1; 257 | }); 258 | 259 | w.show(); 260 | return picker.exec(); 261 | } 262 | -------------------------------------------------------------------------------- /hyprland-share-picker/mainpicker.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainPicker 4 | 5 | 6 | 7 | 0 8 | 0 9 | 500 10 | 300 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 500 22 | 290 23 | 24 | 25 | 26 | 27 | 1280 28 | 800 29 | 30 | 31 | 32 | Select what to share 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | 49 | 1280 50 | 800 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 0 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 69 | 70 | 71 | 72 | 16777215 73 | 16777215 74 | 75 | 76 | 77 | Qt::StrongFocus 78 | 79 | 80 | 81 | 82 | 83 | QTabWidget::North 84 | 85 | 86 | 0 87 | 88 | 89 | 90 | 91 | 0 92 | 0 93 | 94 | 95 | 96 | Qt::LeftToRight 97 | 98 | 99 | Screen 100 | 101 | 102 | 103 | 104 | 105 | 106 | 0 107 | 0 108 | 109 | 110 | 111 | Qt::NoFocus 112 | 113 | 114 | Qt::ScrollBarAlwaysOff 115 | 116 | 117 | QAbstractScrollArea::AdjustIgnored 118 | 119 | 120 | true 121 | 122 | 123 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 124 | 125 | 126 | 127 | true 128 | 129 | 130 | 131 | 0 132 | 0 133 | 410 134 | 18 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 142 | 143 | 144 | 145 | 0 146 | 0 147 | 148 | 149 | 150 | 151 | 16777215 152 | 16777215 153 | 154 | 155 | 156 | Qt::NoFocus 157 | 158 | 159 | 160 | 6 161 | 162 | 163 | QLayout::SetDefaultConstraint 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 0 175 | 0 176 | 177 | 178 | 179 | Window 180 | 181 | 182 | 183 | 184 | 185 | Qt::NoFocus 186 | 187 | 188 | Qt::ScrollBarAlwaysOff 189 | 190 | 191 | true 192 | 193 | 194 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 195 | 196 | 197 | 198 | 199 | 0 200 | 0 201 | 410 202 | 18 203 | 204 | 205 | 206 | 207 | 0 208 | 0 209 | 210 | 211 | 212 | 213 | 0 214 | 0 215 | 216 | 217 | 218 | Qt::NoFocus 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 0 230 | 0 231 | 232 | 233 | 234 | 235 | 16777215 236 | 16777215 237 | 238 | 239 | 240 | Qt::LeftToRight 241 | 242 | 243 | false 244 | 245 | 246 | Region 247 | 248 | 249 | 250 | QLayout::SetDefaultConstraint 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | Qt::ClickFocus 260 | 261 | 262 | By selecting this, the application will be given a restore token that it can use to skip prompting you next time. 263 | Only select if you trust the application. 264 | 265 | 266 | Qt::LeftToRight 267 | 268 | 269 | Allow a restore token 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | tabWidget 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /protocols/wlr-screencopy-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Simon Ser 5 | Copyright © 2019 Andri Yngvason 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a 8 | copy of this software and associated documentation files (the "Software"), 9 | to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | and/or sell copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice (including the next 15 | paragraph) shall be included in all copies or substantial portions of the 16 | Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | 26 | 27 | 28 | This protocol allows clients to ask the compositor to copy part of the 29 | screen content to a client buffer. 30 | 31 | Warning! The protocol described in this file is experimental and 32 | backward incompatible changes may be made. Backward compatible changes 33 | may be added together with the corresponding interface version bump. 34 | Backward incompatible changes are done by bumping the version number in 35 | the protocol and interface names and resetting the interface version. 36 | Once the protocol is to be declared stable, the 'z' prefix and the 37 | version number in the protocol and interface names are removed and the 38 | interface version number is reset. 39 | 40 | 41 | 42 | 43 | This object is a manager which offers requests to start capturing from a 44 | source. 45 | 46 | 47 | 48 | 49 | Capture the next frame of an entire output. 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | Capture the next frame of an output's region. 60 | 61 | The region is given in output logical coordinates, see 62 | xdg_output.logical_size. The region will be clipped to the output's 63 | extents. 64 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | All objects created by the manager will still remain valid, until their 78 | appropriate destroy request has been called. 79 | 80 | 81 | 82 | 83 | 84 | 85 | This object represents a single frame. 86 | 87 | When created, a series of buffer events will be sent, each representing a 88 | supported buffer type. The "buffer_done" event is sent afterwards to 89 | indicate that all supported buffer types have been enumerated. The client 90 | will then be able to send a "copy" request. If the capture is successful, 91 | the compositor will send a "flags" followed by a "ready" event. 92 | 93 | For objects version 2 or lower, wl_shm buffers are always supported, ie. 94 | the "buffer" event is guaranteed to be sent. 95 | 96 | If the capture failed, the "failed" event is sent. This can happen anytime 97 | before the "ready" event. 98 | 99 | Once either a "ready" or a "failed" event is received, the client should 100 | destroy the frame. 101 | 102 | 103 | 104 | 105 | Provides information about wl_shm buffer parameters that need to be 106 | used for this frame. This event is sent once after the frame is created 107 | if wl_shm buffers are supported. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Copy the frame to the supplied buffer. The buffer must have a the 118 | correct size, see zwlr_screencopy_frame_v1.buffer and 119 | zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a 120 | supported format. 121 | 122 | If the frame is successfully copied, a "flags" and a "ready" events are 123 | sent. Otherwise, a "failed" event is sent. 124 | 125 | 126 | 127 | 128 | 129 | 131 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | Provides flags about the frame. This event is sent once before the 142 | "ready" event. 143 | 144 | 145 | 146 | 147 | 148 | 149 | Called as soon as the frame is copied, indicating it is available 150 | for reading. This event includes the time at which presentation happened 151 | at. 152 | 153 | The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, 154 | each component being an unsigned 32-bit value. Whole seconds are in 155 | tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, 156 | and the additional fractional part in tv_nsec as nanoseconds. Hence, 157 | for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part 158 | may have an arbitrary offset at start. 159 | 160 | After receiving this event, the client should destroy the object. 161 | 162 | 164 | 166 | 168 | 169 | 170 | 171 | 172 | This event indicates that the attempted frame copy has failed. 173 | 174 | After receiving this event, the client should destroy the object. 175 | 176 | 177 | 178 | 179 | 180 | Destroys the frame. This request can be sent at any time by the client. 181 | 182 | 183 | 184 | 185 | 186 | 187 | Same as copy, except it waits until there is damage to copy. 188 | 189 | 190 | 191 | 192 | 193 | 194 | This event is sent right before the ready event when copy_with_damage is 195 | requested. It may be generated multiple times for each copy_with_damage 196 | request. 197 | 198 | The arguments describe a box around an area that has changed since the 199 | last copy request that was derived from the current screencopy manager 200 | instance. 201 | 202 | The union of all regions received between the call to copy_with_damage 203 | and a ready event is the total damage since the prior ready event. 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | Provides information about linux-dmabuf buffer parameters that need to 215 | be used for this frame. This event is sent once after the frame is 216 | created if linux-dmabuf buffers are supported. 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | This event is sent once after all buffer events have been sent. 226 | 227 | The client should proceed to create a buffer of one of the supported 228 | types, and send a "copy" request. 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /protocols/wlr-foreign-toplevel-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Ilia Bozhinov 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | The purpose of this protocol is to enable the creation of taskbars 31 | and docks by providing them with a list of opened applications and 32 | letting them request certain actions on them, like maximizing, etc. 33 | 34 | After a client binds the zwlr_foreign_toplevel_manager_v1, each opened 35 | toplevel window will be sent via the toplevel event 36 | 37 | 38 | 39 | 40 | This event is emitted whenever a new toplevel window is created. It 41 | is emitted for all toplevels, regardless of the app that has created 42 | them. 43 | 44 | All initial details of the toplevel(title, app_id, states, etc.) will 45 | be sent immediately after this event via the corresponding events in 46 | zwlr_foreign_toplevel_handle_v1. 47 | 48 | 49 | 50 | 51 | 52 | 53 | Indicates the client no longer wishes to receive events for new toplevels. 54 | However the compositor may emit further toplevel_created events, until 55 | the finished event is emitted. 56 | 57 | The client must not send any more requests after this one. 58 | 59 | 60 | 61 | 62 | 63 | This event indicates that the compositor is done sending events to the 64 | zwlr_foreign_toplevel_manager_v1. The server will destroy the object 65 | immediately after sending this request, so it will become invalid and 66 | the client should free any resources associated with it. 67 | 68 | 69 | 70 | 71 | 72 | 73 | A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel 74 | window. Each app may have multiple opened toplevels. 75 | 76 | Each toplevel has a list of outputs it is visible on, conveyed to the 77 | client with the output_enter and output_leave events. 78 | 79 | 80 | 81 | 82 | This event is emitted whenever the title of the toplevel changes. 83 | 84 | 85 | 86 | 87 | 88 | 89 | This event is emitted whenever the app-id of the toplevel changes. 90 | 91 | 92 | 93 | 94 | 95 | 96 | This event is emitted whenever the toplevel becomes visible on 97 | the given output. A toplevel may be visible on multiple outputs. 98 | 99 | 100 | 101 | 102 | 103 | 104 | This event is emitted whenever the toplevel stops being visible on 105 | the given output. It is guaranteed that an entered-output event 106 | with the same output has been emitted before this event. 107 | 108 | 109 | 110 | 111 | 112 | 113 | Requests that the toplevel be maximized. If the maximized state actually 114 | changes, this will be indicated by the state event. 115 | 116 | 117 | 118 | 119 | 120 | Requests that the toplevel be unmaximized. If the maximized state actually 121 | changes, this will be indicated by the state event. 122 | 123 | 124 | 125 | 126 | 127 | Requests that the toplevel be minimized. If the minimized state actually 128 | changes, this will be indicated by the state event. 129 | 130 | 131 | 132 | 133 | 134 | Requests that the toplevel be unminimized. If the minimized state actually 135 | changes, this will be indicated by the state event. 136 | 137 | 138 | 139 | 140 | 141 | Request that this toplevel be activated on the given seat. 142 | There is no guarantee the toplevel will be actually activated. 143 | 144 | 145 | 146 | 147 | 148 | 149 | The different states that a toplevel can have. These have the same meaning 150 | as the states with the same names defined in xdg-toplevel 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 162 | is created and each time the toplevel state changes, either because of a 163 | compositor action or because of a request in this protocol. 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event is sent after all changes in the toplevel state have been 172 | sent. 173 | 174 | This allows changes to the zwlr_foreign_toplevel_handle_v1 properties 175 | to be seen as atomic, even if they happen via multiple events. 176 | 177 | 178 | 179 | 180 | 181 | Send a request to the toplevel to close itself. The compositor would 182 | typically use a shell-specific method to carry out this request, for 183 | example by sending the xdg_toplevel.close event. However, this gives 184 | no guarantees the toplevel will actually be destroyed. If and when 185 | this happens, the zwlr_foreign_toplevel_handle_v1.closed event will 186 | be emitted. 187 | 188 | 189 | 190 | 191 | 192 | The rectangle of the surface specified in this request corresponds to 193 | the place where the app using this protocol represents the given toplevel. 194 | It can be used by the compositor as a hint for some operations, e.g 195 | minimizing. The client is however not required to set this, in which 196 | case the compositor is free to decide some default value. 197 | 198 | If the client specifies more than one rectangle, only the last one is 199 | considered. 200 | 201 | The dimensions are given in surface-local coordinates. 202 | Setting width=height=0 removes the already-set rectangle. 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 215 | 216 | 217 | 218 | 219 | This event means the toplevel has been destroyed. It is guaranteed there 220 | won't be any more events for this zwlr_foreign_toplevel_handle_v1. The 221 | toplevel itself becomes inert so any requests will be ignored except the 222 | destroy request. 223 | 224 | 225 | 226 | 227 | 228 | Destroys the zwlr_foreign_toplevel_handle_v1 object. 229 | 230 | This request should be called either when the client does not want to 231 | use the toplevel anymore or after the closed event to finalize the 232 | destruction of the object. 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | Requests that the toplevel be fullscreened on the given output. If the 241 | fullscreen state and/or the outputs the toplevel is visible on actually 242 | change, this will be indicated by the state and output_enter/leave 243 | events. 244 | 245 | The output parameter is only a hint to the compositor. Also, if output 246 | is NULL, the compositor should decide which output the toplevel will be 247 | fullscreened on, if at all. 248 | 249 | 250 | 251 | 252 | 253 | 254 | Requests that the toplevel be unfullscreened. If the fullscreen state 255 | actually changes, this will be indicated by the state event. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | This event is emitted whenever the parent of the toplevel changes. 264 | 265 | No event is emitted when the parent handle is destroyed by the client. 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /src/shared/ScreencopyShared.cpp: -------------------------------------------------------------------------------- 1 | #include "ScreencopyShared.hpp" 2 | #include "../helpers/MiscFunctions.hpp" 3 | #include 4 | #include "../helpers/Log.hpp" 5 | #include 6 | #include 7 | #include "../core/PortalManager.hpp" 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | using namespace Hyprutils::OS; 14 | 15 | std::string sanitizeNameForWindowList(const std::string& name) { 16 | std::string result = name; 17 | std::replace(result.begin(), result.end(), '\'', ' '); 18 | std::replace(result.begin(), result.end(), '\"', ' '); 19 | std::replace(result.begin(), result.end(), '$', ' '); 20 | std::replace(result.begin(), result.end(), '`', ' '); 21 | for (size_t i = 1; i < result.size(); ++i) { 22 | if (result[i - 1] == '>' && result[i] == ']') 23 | result[i] = ' '; 24 | } 25 | return result; 26 | } 27 | 28 | std::string buildWindowList() { 29 | std::string result = ""; 30 | if (!g_pPortalManager->m_sPortals.screencopy->hasToplevelCapabilities()) 31 | return result; 32 | 33 | for (auto& e : g_pPortalManager->m_sHelpers.toplevel->m_vToplevels) { 34 | result += std::format("{}[HC>]{}[HT>]{}[HE>]{}[HA>]", (uint32_t)(((uint64_t)e->handle->resource()) & 0xFFFFFFFF), sanitizeNameForWindowList(e->windowClass), 35 | sanitizeNameForWindowList(e->windowTitle), 36 | g_pPortalManager->m_sHelpers.toplevelMapping ? g_pPortalManager->m_sHelpers.toplevelMapping->getWindowForToplevel(e->handle) : 0); 37 | } 38 | 39 | return result; 40 | } 41 | 42 | SSelectionData promptForScreencopySelection() { 43 | SSelectionData data; 44 | 45 | const char* WAYLAND_DISPLAY = getenv("WAYLAND_DISPLAY"); 46 | const char* XCURSOR_SIZE = getenv("XCURSOR_SIZE"); 47 | const char* HYPRLAND_INSTANCE_SIGNATURE = getenv("HYPRLAND_INSTANCE_SIGNATURE"); 48 | 49 | static auto* const* PALLOWTOKENBYDEFAULT = 50 | (Hyprlang::INT* const*)g_pPortalManager->m_sConfig.config->getConfigValuePtr("screencopy:allow_token_by_default")->getDataStaticPtr(); 51 | static auto* const* PCUSTOMPICKER = (Hyprlang::STRING* const)g_pPortalManager->m_sConfig.config->getConfigValuePtr("screencopy:custom_picker_binary")->getDataStaticPtr(); 52 | 53 | std::vector args; 54 | if (**PALLOWTOKENBYDEFAULT) 55 | args.emplace_back("--allow-token"); 56 | 57 | CProcess proc(std::string{*PCUSTOMPICKER}.empty() ? "hyprland-share-picker" : *PCUSTOMPICKER, args); 58 | proc.addEnv("WAYLAND_DISPLAY", WAYLAND_DISPLAY ? WAYLAND_DISPLAY : ""); 59 | proc.addEnv("QT_QPA_PLATFORM", "wayland"); 60 | proc.addEnv("XCURSOR_SIZE", XCURSOR_SIZE ? XCURSOR_SIZE : "24"); 61 | proc.addEnv("HYPRLAND_INSTANCE_SIGNATURE", HYPRLAND_INSTANCE_SIGNATURE ? HYPRLAND_INSTANCE_SIGNATURE : "0"); 62 | proc.addEnv("XDPH_WINDOW_SHARING_LIST", buildWindowList()); // buildWindowList will sanitize any shell stuff in case the picker (qt) does something funky? It shouldn't. 63 | 64 | if (!proc.runSync()) 65 | return data; 66 | 67 | const auto RETVAL = proc.stdOut(); 68 | const auto RETVALERR = proc.stdErr(); 69 | 70 | if (!RETVAL.contains("[SELECTION]")) { 71 | // failed 72 | constexpr const char* QPA_ERR = "qt.qpa.plugin: Could not find the Qt platform plugin"; 73 | 74 | if (RETVAL.contains(QPA_ERR) || RETVALERR.contains(QPA_ERR)) { 75 | // prompt the user to install qt5-wayland and qt6-wayland 76 | addHyprlandNotification("3", 7000, "0", "[xdph] Could not open the picker: qt5-wayland or qt6-wayland doesn't seem to be installed."); 77 | } 78 | 79 | return data; 80 | } 81 | 82 | const auto SELECTION = RETVAL.substr(RETVAL.find("[SELECTION]") + 11); 83 | 84 | Debug::log(LOG, "[sc] Selection: {}", SELECTION); 85 | 86 | const auto FLAGS = SELECTION.substr(0, SELECTION.find_first_of('/')); 87 | const auto SEL = SELECTION.substr(SELECTION.find_first_of('/') + 1); 88 | 89 | for (auto& flag : FLAGS) { 90 | if (flag == 'r') 91 | data.allowToken = true; 92 | else 93 | Debug::log(LOG, "[screencopy] unknown flag from share-picker: {}", flag); 94 | } 95 | 96 | if (SEL.find("screen:") == 0) { 97 | data.type = TYPE_OUTPUT; 98 | data.output = SEL.substr(7); 99 | 100 | data.output.pop_back(); 101 | } else if (SEL.find("window:") == 0) { 102 | data.type = TYPE_WINDOW; 103 | uint32_t handleLo = std::stoull(SEL.substr(7)); 104 | data.windowHandle = nullptr; 105 | 106 | const auto HANDLE = g_pPortalManager->m_sHelpers.toplevel->handleFromHandleLower(handleLo); 107 | if (HANDLE) { 108 | data.windowHandle = HANDLE->handle; 109 | data.windowClass = HANDLE->windowClass; 110 | } 111 | } else if (SEL.find("region:") == 0) { 112 | std::string running = SEL; 113 | running = running.substr(7); 114 | data.type = TYPE_GEOMETRY; 115 | data.output = running.substr(0, running.find_first_of('@')); 116 | running = running.substr(running.find_first_of('@') + 1); 117 | 118 | data.x = std::stoi(running.substr(0, running.find_first_of(','))); 119 | running = running.substr(running.find_first_of(',') + 1); 120 | data.y = std::stoi(running.substr(0, running.find_first_of(','))); 121 | running = running.substr(running.find_first_of(',') + 1); 122 | data.w = std::stoi(running.substr(0, running.find_first_of(','))); 123 | running = running.substr(running.find_first_of(',') + 1); 124 | data.h = std::stoi(running); 125 | } 126 | 127 | return data; 128 | } 129 | 130 | wl_shm_format wlSHMFromDrmFourcc(uint32_t format) { 131 | switch (format) { 132 | case DRM_FORMAT_ARGB8888: return WL_SHM_FORMAT_ARGB8888; 133 | case DRM_FORMAT_XRGB8888: return WL_SHM_FORMAT_XRGB8888; 134 | case DRM_FORMAT_RGBA8888: 135 | case DRM_FORMAT_RGBX8888: 136 | case DRM_FORMAT_ABGR8888: 137 | case DRM_FORMAT_XBGR8888: 138 | case DRM_FORMAT_BGRA8888: 139 | case DRM_FORMAT_BGRX8888: 140 | case DRM_FORMAT_NV12: 141 | case DRM_FORMAT_XRGB2101010: 142 | case DRM_FORMAT_XBGR2101010: 143 | case DRM_FORMAT_RGBX1010102: 144 | case DRM_FORMAT_BGRX1010102: 145 | case DRM_FORMAT_ARGB2101010: 146 | case DRM_FORMAT_ABGR2101010: 147 | case DRM_FORMAT_RGBA1010102: 148 | case DRM_FORMAT_BGRA1010102: return (wl_shm_format)format; 149 | default: Debug::log(ERR, "[screencopy] Unknown format {}", format); abort(); 150 | } 151 | } 152 | 153 | uint32_t drmFourccFromSHM(wl_shm_format format) { 154 | switch (format) { 155 | case WL_SHM_FORMAT_ARGB8888: return DRM_FORMAT_ARGB8888; 156 | case WL_SHM_FORMAT_XRGB8888: return DRM_FORMAT_XRGB8888; 157 | case WL_SHM_FORMAT_RGBA8888: 158 | case WL_SHM_FORMAT_RGBX8888: 159 | case WL_SHM_FORMAT_ABGR8888: 160 | case WL_SHM_FORMAT_XBGR8888: 161 | case WL_SHM_FORMAT_BGRA8888: 162 | case WL_SHM_FORMAT_BGRX8888: 163 | case WL_SHM_FORMAT_NV12: 164 | case WL_SHM_FORMAT_XRGB2101010: 165 | case WL_SHM_FORMAT_XBGR2101010: 166 | case WL_SHM_FORMAT_RGBX1010102: 167 | case WL_SHM_FORMAT_BGRX1010102: 168 | case WL_SHM_FORMAT_ARGB2101010: 169 | case WL_SHM_FORMAT_ABGR2101010: 170 | case WL_SHM_FORMAT_RGBA1010102: 171 | case WL_SHM_FORMAT_BGRA1010102: 172 | case WL_SHM_FORMAT_BGR888: return (uint32_t)format; 173 | default: Debug::log(ERR, "[screencopy] Unknown format {}", (int)format); abort(); 174 | } 175 | } 176 | 177 | spa_video_format pwFromDrmFourcc(uint32_t format) { 178 | switch (format) { 179 | case DRM_FORMAT_ARGB8888: return SPA_VIDEO_FORMAT_BGRA; 180 | case DRM_FORMAT_XRGB8888: return SPA_VIDEO_FORMAT_BGRx; 181 | case DRM_FORMAT_RGBA8888: return SPA_VIDEO_FORMAT_ABGR; 182 | case DRM_FORMAT_RGBX8888: return SPA_VIDEO_FORMAT_xBGR; 183 | case DRM_FORMAT_ABGR8888: return SPA_VIDEO_FORMAT_RGBA; 184 | case DRM_FORMAT_XBGR8888: return SPA_VIDEO_FORMAT_RGBx; 185 | case DRM_FORMAT_BGRA8888: return SPA_VIDEO_FORMAT_ARGB; 186 | case DRM_FORMAT_BGRX8888: return SPA_VIDEO_FORMAT_xRGB; 187 | case DRM_FORMAT_NV12: return SPA_VIDEO_FORMAT_NV12; 188 | case DRM_FORMAT_XRGB2101010: return SPA_VIDEO_FORMAT_xRGB_210LE; 189 | case DRM_FORMAT_XBGR2101010: return SPA_VIDEO_FORMAT_xBGR_210LE; 190 | case DRM_FORMAT_RGBX1010102: return SPA_VIDEO_FORMAT_RGBx_102LE; 191 | case DRM_FORMAT_BGRX1010102: return SPA_VIDEO_FORMAT_BGRx_102LE; 192 | case DRM_FORMAT_ARGB2101010: return SPA_VIDEO_FORMAT_ARGB_210LE; 193 | case DRM_FORMAT_ABGR2101010: return SPA_VIDEO_FORMAT_ABGR_210LE; 194 | case DRM_FORMAT_RGBA1010102: return SPA_VIDEO_FORMAT_RGBA_102LE; 195 | case DRM_FORMAT_BGRA1010102: return SPA_VIDEO_FORMAT_BGRA_102LE; 196 | case DRM_FORMAT_BGR888: return SPA_VIDEO_FORMAT_BGR; 197 | default: Debug::log(ERR, "[screencopy] Unknown format {}", (int)format); abort(); 198 | } 199 | } 200 | 201 | std::string getRandName(std::string prefix) { 202 | std::srand(time(NULL)); 203 | return prefix + 204 | std::format("{}{}{}{}{}{}", (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), 205 | (int)(std::rand() % 10)); 206 | } 207 | 208 | spa_video_format pwStripAlpha(spa_video_format format) { 209 | switch (format) { 210 | case SPA_VIDEO_FORMAT_BGRA: return SPA_VIDEO_FORMAT_BGRx; 211 | case SPA_VIDEO_FORMAT_ABGR: return SPA_VIDEO_FORMAT_xBGR; 212 | case SPA_VIDEO_FORMAT_RGBA: return SPA_VIDEO_FORMAT_RGBx; 213 | case SPA_VIDEO_FORMAT_ARGB: return SPA_VIDEO_FORMAT_xRGB; 214 | case SPA_VIDEO_FORMAT_ARGB_210LE: return SPA_VIDEO_FORMAT_xRGB_210LE; 215 | case SPA_VIDEO_FORMAT_ABGR_210LE: return SPA_VIDEO_FORMAT_xBGR_210LE; 216 | case SPA_VIDEO_FORMAT_RGBA_102LE: return SPA_VIDEO_FORMAT_RGBx_102LE; 217 | case SPA_VIDEO_FORMAT_BGRA_102LE: return SPA_VIDEO_FORMAT_BGRx_102LE; 218 | default: return SPA_VIDEO_FORMAT_UNKNOWN; 219 | } 220 | } 221 | 222 | spa_pod* build_buffer(spa_pod_builder* b, uint32_t blocks, uint32_t size, uint32_t stride, uint32_t datatype) { 223 | assert(blocks > 0); 224 | assert(datatype > 0); 225 | spa_pod_frame f[1]; 226 | 227 | spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); 228 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(XDPH_PWR_BUFFERS, XDPH_PWR_BUFFERS_MIN, 32), 0); 229 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), 0); 230 | if (size > 0) { 231 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), 0); 232 | } 233 | if (stride > 0) { 234 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), 0); 235 | } 236 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_align, SPA_POD_Int(XDPH_PWR_ALIGN), 0); 237 | spa_pod_builder_add(b, SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(datatype), 0); 238 | return (spa_pod*)spa_pod_builder_pop(b, &f[0]); 239 | } 240 | 241 | spa_pod* fixate_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifier) { 242 | spa_pod_frame f[1]; 243 | 244 | spa_video_format format_without_alpha = pwStripAlpha(format); 245 | 246 | spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); 247 | spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); 248 | spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); 249 | /* format */ 250 | if (modifier || format_without_alpha == SPA_VIDEO_FORMAT_UNKNOWN) { 251 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); 252 | } else { 253 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, format_without_alpha), 0); 254 | } 255 | /* modifiers */ 256 | if (modifier) { 257 | // implicit modifier 258 | spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); 259 | spa_pod_builder_long(b, *modifier); 260 | } 261 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), 0); 262 | // variable framerate 263 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(0, 1)), 0); 264 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&SPA_FRACTION(framerate, 1), &SPA_FRACTION(1, 1), &SPA_FRACTION(framerate, 1)), 0); 265 | return (spa_pod*)spa_pod_builder_pop(b, &f[0]); 266 | } 267 | 268 | spa_pod* build_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifiers, int modifier_count) { 269 | spa_pod_frame f[2]; 270 | int i, c; 271 | 272 | spa_video_format format_without_alpha = pwStripAlpha(format); 273 | 274 | spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); 275 | spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); 276 | spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); 277 | /* format */ 278 | if (modifier_count > 0 || format_without_alpha == SPA_VIDEO_FORMAT_UNKNOWN) { 279 | // modifiers are defined only in combinations with their format 280 | // we should not announce the format without alpha 281 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); 282 | } else { 283 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, format_without_alpha), 0); 284 | } 285 | /* modifiers */ 286 | if (modifier_count > 0) { 287 | // build an enumeration of modifiers 288 | spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); 289 | spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); 290 | // modifiers from the array 291 | for (i = 0, c = 0; i < modifier_count; i++) { 292 | spa_pod_builder_long(b, modifiers[i]); 293 | if (c++ == 0) 294 | spa_pod_builder_long(b, modifiers[i]); 295 | } 296 | spa_pod_builder_pop(b, &f[1]); 297 | } 298 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), 0); 299 | // variable framerate 300 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(0, 1)), 0); 301 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&SPA_FRACTION(framerate, 1), &SPA_FRACTION(1, 1), &SPA_FRACTION(framerate, 1)), 0); 302 | return (spa_pod*)spa_pod_builder_pop(b, &f[0]); 303 | } 304 | 305 | void randname(char* buf) { 306 | struct timespec ts; 307 | clock_gettime(CLOCK_REALTIME, &ts); 308 | long r = ts.tv_nsec; 309 | for (int i = 0; i < 6; ++i) { 310 | assert(buf[i] == 'X'); 311 | buf[i] = 'A' + (r & 15) + (r & 16) * 2; 312 | r >>= 5; 313 | } 314 | } 315 | 316 | int anonymous_shm_open() { 317 | char name[] = "/xdph-shm-XXXXXX"; 318 | int retries = 100; 319 | 320 | do { 321 | randname(name + strlen(name) - 6); 322 | 323 | --retries; 324 | // shm_open guarantees that O_CLOEXEC is set 325 | int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); 326 | if (fd >= 0) { 327 | shm_unlink(name); 328 | return fd; 329 | } 330 | } while (retries > 0 && errno == EEXIST); 331 | 332 | return -1; 333 | } 334 | 335 | SP import_wl_shm_buffer(int fd, wl_shm_format fmt, int width, int height, int stride) { 336 | int size = stride * height; 337 | 338 | if (fd < 0) 339 | return nullptr; 340 | 341 | auto pool = makeShared(g_pPortalManager->m_sWaylandConnection.shm->sendCreatePool(fd, size)); 342 | auto buf = makeShared(pool->sendCreateBuffer(0, width, height, stride, fmt)); 343 | 344 | return buf; 345 | } 346 | -------------------------------------------------------------------------------- /src/core/PortalManager.cpp: -------------------------------------------------------------------------------- 1 | #include "PortalManager.hpp" 2 | #include "../helpers/Log.hpp" 3 | #include "../helpers/MiscFunctions.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | SOutput::SOutput(SP output_) : output(output_) { 14 | output->setName([this](CCWlOutput* o, const char* name_) { 15 | if (!name_) 16 | return; 17 | 18 | name = name_; 19 | 20 | Debug::log(LOG, "Found output name {}", name); 21 | }); 22 | output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { // 23 | refreshRate = refresh; 24 | }); 25 | output->setGeometry([this](CCWlOutput* r, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, 26 | int32_t transform_) { // 27 | transform = (wl_output_transform)transform_; 28 | }); 29 | } 30 | 31 | CPortalManager::CPortalManager() { 32 | const auto XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME"); 33 | const auto HOME = getenv("HOME"); 34 | 35 | if (!HOME && !XDG_CONFIG_HOME) 36 | Debug::log(WARN, "neither $HOME nor $XDG_CONFIG_HOME is present in env"); 37 | 38 | std::string path = 39 | (!XDG_CONFIG_HOME && !HOME) ? "/tmp/xdph.conf" : (XDG_CONFIG_HOME ? std::string{XDG_CONFIG_HOME} + "/hypr/xdph.conf" : std::string{HOME} + "/.config/hypr/xdph.conf"); 40 | 41 | m_sConfig.config = std::make_unique(path.c_str(), Hyprlang::SConfigOptions{.allowMissingConfig = true}); 42 | 43 | m_sConfig.config->addConfigValue("general:toplevel_dynamic_bind", Hyprlang::INT{0L}); 44 | m_sConfig.config->addConfigValue("screencopy:max_fps", Hyprlang::INT{120L}); 45 | m_sConfig.config->addConfigValue("screencopy:allow_token_by_default", Hyprlang::INT{0L}); 46 | m_sConfig.config->addConfigValue("screencopy:custom_picker_binary", Hyprlang::STRING{""}); 47 | 48 | m_sConfig.config->commence(); 49 | m_sConfig.config->parse(); 50 | } 51 | 52 | void CPortalManager::onGlobal(uint32_t name, const char* interface, uint32_t version) { 53 | const std::string INTERFACE = interface; 54 | 55 | Debug::log(LOG, " | Got interface: {} (ver {})", INTERFACE, version); 56 | 57 | if (INTERFACE == zwlr_screencopy_manager_v1_interface.name && m_sPipewire.loop) { 58 | m_sPortals.screencopy = std::make_unique(makeShared( 59 | (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &zwlr_screencopy_manager_v1_interface, version))); 60 | } 61 | 62 | if (INTERFACE == hyprland_global_shortcuts_manager_v1_interface.name) { 63 | m_sPortals.globalShortcuts = std::make_unique(makeShared( 64 | (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_global_shortcuts_manager_v1_interface, version))); 65 | } 66 | 67 | else if (INTERFACE == hyprland_toplevel_export_manager_v1_interface.name) { 68 | m_sWaylandConnection.hyprlandToplevelMgr = makeShared( 69 | (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_toplevel_export_manager_v1_interface, version)); 70 | } 71 | 72 | else if (INTERFACE == wl_output_interface.name) { 73 | const auto POUTPUT = m_vOutputs 74 | .emplace_back(std::make_unique(makeShared( 75 | (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &wl_output_interface, version)))) 76 | .get(); 77 | POUTPUT->id = name; 78 | } 79 | 80 | else if (INTERFACE == zwp_linux_dmabuf_v1_interface.name) { 81 | if (version < 4) { 82 | Debug::log(ERR, "cannot use linux_dmabuf with ver < 4"); 83 | return; 84 | } 85 | 86 | m_sWaylandConnection.linuxDmabuf = 87 | makeShared((wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &zwp_linux_dmabuf_v1_interface, version)); 88 | m_sWaylandConnection.linuxDmabufFeedback = makeShared(m_sWaylandConnection.linuxDmabuf->sendGetDefaultFeedback()); 89 | 90 | m_sWaylandConnection.linuxDmabufFeedback->setMainDevice([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* device_arr) { 91 | Debug::log(LOG, "[core] dmabufFeedbackMainDevice"); 92 | 93 | if (m_sWaylandConnection.dma.done) 94 | return; 95 | 96 | RASSERT(!m_sWaylandConnection.gbm, "double dmabuf feedback"); 97 | 98 | dev_t device; 99 | assert(device_arr->size == sizeof(device)); 100 | memcpy(&device, device_arr->data, sizeof(device)); 101 | 102 | drmDevice* drmDev; 103 | if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0) { 104 | Debug::log(WARN, "[dmabuf] unable to open main device?"); 105 | exit(1); 106 | } 107 | 108 | m_sWaylandConnection.gbmDevice = createGBMDevice(drmDev); 109 | }); 110 | m_sWaylandConnection.linuxDmabufFeedback->setFormatTable([this](CCZwpLinuxDmabufFeedbackV1* r, int fd, uint32_t size) { 111 | Debug::log(TRACE, "[core] dmabufFeedbackFormatTable"); 112 | 113 | if (m_sWaylandConnection.dma.done) 114 | return; 115 | 116 | m_vDMABUFMods.clear(); 117 | 118 | m_sWaylandConnection.dma.formatTable = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); 119 | 120 | if (m_sWaylandConnection.dma.formatTable == MAP_FAILED) { 121 | Debug::log(ERR, "[core] format table failed to mmap"); 122 | m_sWaylandConnection.dma.formatTable = nullptr; 123 | m_sWaylandConnection.dma.formatTableSize = 0; 124 | return; 125 | } 126 | 127 | m_sWaylandConnection.dma.formatTableSize = size; 128 | }); 129 | m_sWaylandConnection.linuxDmabufFeedback->setDone([this](CCZwpLinuxDmabufFeedbackV1* r) { 130 | Debug::log(TRACE, "[core] dmabufFeedbackDone"); 131 | 132 | if (m_sWaylandConnection.dma.done) 133 | return; 134 | 135 | if (m_sWaylandConnection.dma.formatTable) 136 | munmap(m_sWaylandConnection.dma.formatTable, m_sWaylandConnection.dma.formatTableSize); 137 | 138 | m_sWaylandConnection.dma.formatTable = nullptr; 139 | m_sWaylandConnection.dma.formatTableSize = 0; 140 | m_sWaylandConnection.dma.done = true; 141 | }); 142 | m_sWaylandConnection.linuxDmabufFeedback->setTrancheTargetDevice([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* device_arr) { 143 | Debug::log(TRACE, "[core] dmabufFeedbackTrancheTargetDevice"); 144 | 145 | if (m_sWaylandConnection.dma.done) 146 | return; 147 | 148 | dev_t device; 149 | assert(device_arr->size == sizeof(device)); 150 | memcpy(&device, device_arr->data, sizeof(device)); 151 | 152 | drmDevice* drmDev; 153 | if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0) 154 | return; 155 | 156 | if (m_sWaylandConnection.gbmDevice) { 157 | drmDevice* drmDevRenderer = NULL; 158 | drmGetDevice2(gbm_device_get_fd(m_sWaylandConnection.gbmDevice), /* flags */ 0, &drmDevRenderer); 159 | m_sWaylandConnection.dma.deviceUsed = drmDevicesEqual(drmDevRenderer, drmDev); 160 | } else { 161 | m_sWaylandConnection.gbmDevice = createGBMDevice(drmDev); 162 | m_sWaylandConnection.dma.deviceUsed = m_sWaylandConnection.gbm; 163 | } 164 | }); 165 | m_sWaylandConnection.linuxDmabufFeedback->setTrancheFormats([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* indices) { 166 | Debug::log(TRACE, "[core] dmabufFeedbackTrancheFormats"); 167 | 168 | if (m_sWaylandConnection.dma.done) 169 | return; 170 | 171 | if (!m_sWaylandConnection.dma.deviceUsed || !m_sWaylandConnection.dma.formatTable) 172 | return; 173 | 174 | struct fm_entry { 175 | uint32_t format; 176 | uint32_t padding; 177 | uint64_t modifier; 178 | }; 179 | // An entry in the table has to be 16 bytes long 180 | assert(sizeof(struct fm_entry) == 16); 181 | 182 | uint32_t n_modifiers = m_sWaylandConnection.dma.formatTableSize / sizeof(struct fm_entry); 183 | fm_entry* fm_entry = (struct fm_entry*)m_sWaylandConnection.dma.formatTable; 184 | uint16_t* idx; 185 | 186 | for (idx = (uint16_t*)indices->data; (const char*)idx < (const char*)indices->data + indices->size; idx++) { 187 | if (*idx >= n_modifiers) 188 | continue; 189 | 190 | m_vDMABUFMods.push_back({(fm_entry + *idx)->format, (fm_entry + *idx)->modifier}); 191 | } 192 | }); 193 | m_sWaylandConnection.linuxDmabufFeedback->setTrancheDone([this](CCZwpLinuxDmabufFeedbackV1* r) { 194 | Debug::log(TRACE, "[core] dmabufFeedbackTrancheDone"); 195 | 196 | if (m_sWaylandConnection.dma.done) 197 | return; 198 | 199 | m_sWaylandConnection.dma.deviceUsed = false; 200 | }); 201 | 202 | } 203 | 204 | else if (INTERFACE == wl_shm_interface.name) 205 | m_sWaylandConnection.shm = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &wl_shm_interface, version)); 206 | 207 | else if (INTERFACE == zwlr_foreign_toplevel_manager_v1_interface.name) { 208 | m_sHelpers.toplevel = std::make_unique(name, version); 209 | 210 | // remove when another fix is found for https://github.com/hyprwm/xdg-desktop-portal-hyprland/issues/147 211 | if (!std::any_cast(m_sConfig.config->getConfigValue("general:toplevel_dynamic_bind"))) 212 | m_sHelpers.toplevel->activate(); 213 | } 214 | 215 | else if (INTERFACE == hyprland_toplevel_mapping_manager_v1_interface.name) { 216 | m_sHelpers.toplevelMapping = std::make_unique(makeShared( 217 | (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_toplevel_mapping_manager_v1_interface, version))); 218 | } 219 | } 220 | 221 | void CPortalManager::onGlobalRemoved(uint32_t name) { 222 | std::erase_if(m_vOutputs, [&](const auto& other) { return other->id == name; }); 223 | } 224 | 225 | void CPortalManager::init() { 226 | m_iPID = getpid(); 227 | 228 | try { 229 | m_pConnection = sdbus::createSessionBusConnection(sdbus::ServiceName{"org.freedesktop.impl.portal.desktop.hyprland"}); 230 | } catch (std::exception& e) { 231 | Debug::log(CRIT, "Couldn't create the dbus connection ({})", e.what()); 232 | exit(1); 233 | } 234 | 235 | if (!m_pConnection) { 236 | Debug::log(CRIT, "Couldn't connect to dbus"); 237 | exit(1); 238 | } 239 | 240 | // init wayland connection 241 | m_sWaylandConnection.display = wl_display_connect(nullptr); 242 | 243 | if (!m_sWaylandConnection.display) { 244 | Debug::log(CRIT, "Couldn't connect to a wayland compositor"); 245 | exit(1); 246 | } 247 | 248 | if (const auto ENV = getenv("XDG_CURRENT_DESKTOP"); ENV) { 249 | Debug::log(LOG, "XDG_CURRENT_DESKTOP set to {}", ENV); 250 | 251 | if (std::string(ENV) != "Hyprland") 252 | Debug::log(WARN, "Not running on hyprland, some features might be unavailable"); 253 | } else { 254 | Debug::log(WARN, "XDG_CURRENT_DESKTOP unset, running on an unknown desktop"); 255 | } 256 | 257 | m_sWaylandConnection.registry = makeShared((wl_proxy*)wl_display_get_registry(m_sWaylandConnection.display)); 258 | m_sWaylandConnection.registry->setGlobal([this](CCWlRegistry* r, uint32_t name, const char* iface, uint32_t ver) { onGlobal(name, iface, ver); }); 259 | m_sWaylandConnection.registry->setGlobalRemove([this](CCWlRegistry* r, uint32_t name) { onGlobalRemoved(name); }); 260 | 261 | pw_init(nullptr, nullptr); 262 | m_sPipewire.loop = pw_loop_new(nullptr); 263 | 264 | if (!m_sPipewire.loop) 265 | Debug::log(ERR, "Pipewire: refused to create a loop. Screensharing will not work."); 266 | 267 | Debug::log(LOG, "Gathering exported interfaces"); 268 | 269 | wl_display_roundtrip(m_sWaylandConnection.display); 270 | 271 | if (!m_sPortals.screencopy) 272 | Debug::log(WARN, "Screencopy not started: compositor doesn't support zwlr_screencopy_v1 or pw refused a loop"); 273 | else if (m_sWaylandConnection.hyprlandToplevelMgr) 274 | m_sPortals.screencopy->appendToplevelExport(m_sWaylandConnection.hyprlandToplevelMgr); 275 | 276 | if (!inShellPath("grim")) 277 | Debug::log(WARN, "grim not found. Screenshots will not work."); 278 | else { 279 | m_sPortals.screenshot = std::make_unique(); 280 | 281 | if (!inShellPath("slurp")) 282 | Debug::log(WARN, "slurp not found. You won't be able to select a region when screenshotting."); 283 | 284 | if (!inShellPath("slurp") && !inShellPath("hyprpicker")) 285 | Debug::log(WARN, "Neither slurp nor hyprpicker found. You won't be able to pick colors."); 286 | else if (!inShellPath("hyprpicker")) 287 | Debug::log(INFO, "hyprpicker not found. We suggest to use hyprpicker for color picking to be less meh."); 288 | } 289 | 290 | wl_display_roundtrip(m_sWaylandConnection.display); 291 | 292 | startEventLoop(); 293 | } 294 | 295 | void CPortalManager::startEventLoop() { 296 | 297 | pollfd pollfds[] = { 298 | { 299 | .fd = m_pConnection->getEventLoopPollData().fd, 300 | .events = POLLIN, 301 | }, 302 | { 303 | .fd = wl_display_get_fd(m_sWaylandConnection.display), 304 | .events = POLLIN, 305 | }, 306 | { 307 | .fd = pw_loop_get_fd(m_sPipewire.loop), 308 | .events = POLLIN, 309 | }, 310 | }; 311 | 312 | std::thread pollThr([this, &pollfds]() { 313 | while (1) { 314 | int ret = poll(pollfds, 3, 5000 /* 5 seconds, reasonable. It's because we might need to terminate */); 315 | if (ret < 0) { 316 | Debug::log(CRIT, "[core] Polling fds failed with {}", strerror(errno)); 317 | g_pPortalManager->terminate(); 318 | } 319 | 320 | for (size_t i = 0; i < 3; ++i) { 321 | if (pollfds[i].revents & POLLHUP) { 322 | Debug::log(CRIT, "[core] Disconnected from pollfd id {}", i); 323 | g_pPortalManager->terminate(); 324 | } 325 | } 326 | 327 | if (m_bTerminate) 328 | break; 329 | 330 | if (ret != 0) { 331 | Debug::log(TRACE, "[core] got poll event"); 332 | std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); 333 | m_sEventLoopInternals.shouldProcess = true; 334 | m_sEventLoopInternals.loopSignal.notify_all(); 335 | } 336 | } 337 | }); 338 | 339 | m_sTimersThread.thread = std::make_unique([this] { 340 | while (1) { 341 | std::unique_lock lk(m_sTimersThread.loopMutex); 342 | 343 | // find nearest timer ms 344 | m_mEventLock.lock(); 345 | float nearest = 60000; /* reasonable timeout */ 346 | for (auto& t : m_sTimersThread.timers) { 347 | float until = t->duration() - t->passedMs(); 348 | if (until < nearest) 349 | nearest = until; 350 | } 351 | m_mEventLock.unlock(); 352 | 353 | m_sTimersThread.loopSignal.wait_for(lk, std::chrono::milliseconds((int)nearest), [this] { return m_sTimersThread.shouldProcess; }); 354 | m_sTimersThread.shouldProcess = false; 355 | 356 | if (m_bTerminate) 357 | break; 358 | 359 | // awakened. Check if any timers passed 360 | m_mEventLock.lock(); 361 | bool notify = false; 362 | for (auto& t : m_sTimersThread.timers) { 363 | if (t->passed()) { 364 | Debug::log(TRACE, "[core] got timer event"); 365 | notify = true; 366 | break; 367 | } 368 | } 369 | m_mEventLock.unlock(); 370 | 371 | if (notify) { 372 | std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); 373 | m_sEventLoopInternals.shouldProcess = true; 374 | m_sEventLoopInternals.loopSignal.notify_all(); 375 | } 376 | } 377 | }); 378 | 379 | while (1) { // dbus events 380 | // wait for being awakened 381 | std::unique_lock lk(m_sEventLoopInternals.loopMutex); 382 | if (m_sEventLoopInternals.shouldProcess == false) // avoid a lock if a thread managed to request something already since we .unlock()ed 383 | m_sEventLoopInternals.loopSignal.wait_for(lk, std::chrono::seconds(5), [this] { return m_sEventLoopInternals.shouldProcess == true; }); // wait for events 384 | 385 | std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); 386 | 387 | if (m_bTerminate) 388 | break; 389 | 390 | m_sEventLoopInternals.shouldProcess = false; 391 | 392 | m_mEventLock.lock(); 393 | 394 | if (pollfds[0].revents & POLLIN /* dbus */) { 395 | while (m_pConnection->processPendingEvent()) { 396 | ; 397 | } 398 | } 399 | 400 | if (pollfds[1].revents & POLLIN /* wl */) { 401 | wl_display_flush(m_sWaylandConnection.display); 402 | if (wl_display_prepare_read(m_sWaylandConnection.display) == 0) { 403 | wl_display_read_events(m_sWaylandConnection.display); 404 | wl_display_dispatch_pending(m_sWaylandConnection.display); 405 | } else { 406 | wl_display_dispatch(m_sWaylandConnection.display); 407 | } 408 | } 409 | 410 | if (pollfds[2].revents & POLLIN /* pw */) { 411 | while (pw_loop_iterate(m_sPipewire.loop, 0) != 0) { 412 | ; 413 | } 414 | } 415 | 416 | std::vector toRemove; 417 | for (auto& t : m_sTimersThread.timers) { 418 | if (t->passed()) { 419 | t->m_fnCallback(); 420 | toRemove.emplace_back(t.get()); 421 | Debug::log(TRACE, "[core] calling timer {}", (void*)t.get()); 422 | } 423 | } 424 | 425 | int ret = 0; 426 | do { 427 | ret = wl_display_dispatch_pending(m_sWaylandConnection.display); 428 | wl_display_flush(m_sWaylandConnection.display); 429 | } while (ret > 0); 430 | 431 | if (!toRemove.empty()) 432 | std::erase_if(m_sTimersThread.timers, 433 | [&](const auto& t) { return std::find_if(toRemove.begin(), toRemove.end(), [&](const auto& other) { return other == t.get(); }) != toRemove.end(); }); 434 | 435 | m_mEventLock.unlock(); 436 | } 437 | 438 | Debug::log(ERR, "[core] Terminated"); 439 | 440 | m_sPortals.globalShortcuts.reset(); 441 | m_sPortals.screencopy.reset(); 442 | m_sPortals.screenshot.reset(); 443 | m_sHelpers.toplevel.reset(); 444 | 445 | m_pConnection.reset(); 446 | pw_loop_destroy(m_sPipewire.loop); 447 | wl_display_disconnect(m_sWaylandConnection.display); 448 | 449 | m_sTimersThread.thread.release(); 450 | pollThr.join(); // wait for poll to exit 451 | } 452 | 453 | sdbus::IConnection* CPortalManager::getConnection() { 454 | return m_pConnection.get(); 455 | } 456 | 457 | SOutput* CPortalManager::getOutputFromName(const std::string& name) { 458 | for (auto& o : m_vOutputs) { 459 | if (o->name == name) 460 | return o.get(); 461 | } 462 | return nullptr; 463 | } 464 | 465 | static char* gbm_find_render_node(drmDevice* device) { 466 | drmDevice* devices[64]; 467 | char* render_node = NULL; 468 | 469 | int n = drmGetDevices2(0, devices, sizeof(devices) / sizeof(devices[0])); 470 | for (int i = 0; i < n; ++i) { 471 | drmDevice* dev = devices[i]; 472 | if (device && !drmDevicesEqual(device, dev)) { 473 | continue; 474 | } 475 | if (!(dev->available_nodes & (1 << DRM_NODE_RENDER))) 476 | continue; 477 | 478 | render_node = strdup(dev->nodes[DRM_NODE_RENDER]); 479 | break; 480 | } 481 | 482 | drmFreeDevices(devices, n); 483 | return render_node; 484 | } 485 | 486 | gbm_device* CPortalManager::createGBMDevice(drmDevice* dev) { 487 | char* renderNode = gbm_find_render_node(dev); 488 | 489 | if (!renderNode) { 490 | Debug::log(ERR, "[core] Couldn't find a render node"); 491 | return nullptr; 492 | } 493 | 494 | Debug::log(TRACE, "[core] createGBMDevice: render node {}", renderNode); 495 | 496 | int fd = open(renderNode, O_RDWR | O_CLOEXEC); 497 | if (fd < 0) { 498 | Debug::log(ERR, "[core] couldn't open render node"); 499 | free(renderNode); 500 | return NULL; 501 | } 502 | 503 | free(renderNode); 504 | return gbm_create_device(fd); 505 | } 506 | 507 | void CPortalManager::addTimer(const CTimer& timer) { 508 | Debug::log(TRACE, "[core] adding timer for {}ms", timer.duration()); 509 | m_sTimersThread.timers.emplace_back(std::make_unique(timer)); 510 | m_sTimersThread.shouldProcess = true; 511 | m_sTimersThread.loopSignal.notify_all(); 512 | } 513 | 514 | void CPortalManager::terminate() { 515 | m_bTerminate = true; 516 | 517 | // if we don't exit in 5s, we'll kill by force. Nuclear option. PIDs are not reused in linux until a wrap-around, 518 | // and I doubt anyone will make 4.2M PIDs within 5s. 519 | if (fork() == 0) 520 | execl("/bin/sh", "/bin/sh", "-c", std::format("sleep 5 && kill -9 {}", m_iPID).c_str(), nullptr); 521 | 522 | { 523 | m_sEventLoopInternals.shouldProcess = true; 524 | m_sEventLoopInternals.loopSignal.notify_all(); 525 | } 526 | 527 | m_sTimersThread.shouldProcess = true; 528 | m_sTimersThread.loopSignal.notify_all(); 529 | } 530 | --------------------------------------------------------------------------------