pax_global_header00006660000000000000000000000064147075033570014525gustar00rootroot0000000000000052 comment=fb1ce162029e9d207c51b468e6ab7ac7886c7419 hyprwm-xdg-desktop-portal-hyprland-fb1ce16/000077500000000000000000000000001470750335700211115ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.builds/000077500000000000000000000000001470750335700224515ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.builds/alpine.yml000066400000000000000000000006671470750335700244550ustar00rootroot00000000000000image: alpine/edge packages: - elogind-dev - gcc - meson - pipewire-dev - wayland-dev - wayland-protocols - inih-dev - scdoc - libdrm - mesa-dev sources: - https://github.com/emersion/xdg-desktop-portal-wlr tasks: - setup: | cd xdg-desktop-portal-wlr meson -Dauto_features=enabled -Dsystemd=disabled -Dsd-bus-provider=libelogind build/ - build: | cd xdg-desktop-portal-wlr ninja -C build/ hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.builds/archlinux.yml000066400000000000000000000010751470750335700251740ustar00rootroot00000000000000image: archlinux packages: - gcc - clang - meson - wayland - wayland-protocols - pipewire - libinih - scdoc - mesa sources: - https://github.com/emersion/xdg-desktop-portal-wlr tasks: - setup: | cd xdg-desktop-portal-wlr CC=gcc meson -Dauto_features=enabled -Dsd-bus-provider=libsystemd build-gcc/ CC=clang meson -Dauto_features=enabled -Dsd-bus-provider=libsystemd build-clang/ - build-gcc: | cd xdg-desktop-portal-wlr ninja -C build-gcc/ - build-clang: | cd xdg-desktop-portal-wlr ninja -C build-clang/ hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.builds/freebsd.yml000066400000000000000000000007121470750335700246060ustar00rootroot00000000000000image: freebsd/latest packages: - basu - libepoll-shim - meson - pipewire - pkgconf - wayland - wayland-protocols - inih - scdoc - graphics/libdrm - graphics/mesa-libs sources: - https://github.com/emersion/xdg-desktop-portal-wlr tasks: - setup: | cd xdg-desktop-portal-wlr meson -Dauto_features=enabled -Dsystemd=disabled -Dsd-bus-provider=basu build/ - build: | cd xdg-desktop-portal-wlr ninja -C build/ hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.clang-format000066400000000000000000000034161470750335700234700ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveMacros: true AlignConsecutiveAssignments: true AlignEscapedNewlines: Right AlignOperands: false AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllConstructorInitializersOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon ColumnLimit: 180 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false IncludeBlocks: Preserve IndentCaseLabels: true IndentWidth: 4 PointerAlignment: Left ReflowComments: false SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: true SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 4 UseTab: Never AllowShortEnumsOnASingleLine: false BraceWrapping: AfterEnum: false AlignConsecutiveDeclarations: AcrossEmptyLines NamespaceIndentation: All hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.editorconfig000066400000000000000000000003051470750335700235640ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = tab indent_size = 4 [*.{xml,yml}] indent_style = space indent_size = 2 hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.github/000077500000000000000000000000001470750335700224515ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.github/workflows/000077500000000000000000000000001470750335700245065ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.github/workflows/nix-build.yaml000066400000000000000000000006661470750335700272750ustar00rootroot00000000000000name: Build xdph (Nix) on: [push, pull_request, workflow_dispatch] jobs: nix: name: "Build" runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v3 - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build xdg-desktop-portal-hyprland run: nix build --print-build-logs --extra-substituters "https://hyprland.cachix.org" hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.gitignore000066400000000000000000000010331470750335700230760ustar00rootroot00000000000000# Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf # build folder build/ build-*/ .cache .vscode/ hyprland-share-picker/build/ protocols/*.c* protocols/*.h*hyprwm-xdg-desktop-portal-hyprland-fb1ce16/.gitmodules000066400000000000000000000003601470750335700232650ustar00rootroot00000000000000[submodule "hyprland-protocols"] path = subprojects/hyprland-protocols url = https://github.com/hyprwm/hyprland-protocols [submodule "subprojects/sdbus-cpp"] path = subprojects/sdbus-cpp url = https://github.com/Kistler-Group/sdbus-cpp hyprwm-xdg-desktop-portal-hyprland-fb1ce16/CMakeLists.txt000066400000000000000000000117161470750335700236570ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.19) file(READ ${CMAKE_CURRENT_SOURCE_DIR}/VERSION VER) string(STRIP ${VER} VER) project( xdg-desktop-portal-hyprland DESCRIPTION "An XDG-Destop-Portal backend for Hyprland (and wlroots)" VERSION ${VER}) set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") set(SYSTEMD_SERVICES ON CACHE BOOL "Install systemd service file") if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) message(STATUS "Configuring XDPH in Debug with CMake") add_compile_definitions(HYPRLAND_DEBUG) else() add_compile_options(-O3) message(STATUS "Configuring XDPH in Release with CMake") endif() add_compile_definitions(XDPH_VERSION="${VER}") include_directories(. "protocols/") # configure include(GNUInstallDirs) set(LIBEXECDIR ${CMAKE_INSTALL_FULL_LIBEXECDIR}) configure_file(org.freedesktop.impl.portal.desktop.hyprland.service.in org.freedesktop.impl.portal.desktop.hyprland.service @ONLY) if(SYSTEMD_SERVICES) configure_file(contrib/systemd/xdg-desktop-portal-hyprland.service.in contrib/systemd/xdg-desktop-portal-hyprland.service @ONLY) endif() set(CMAKE_CXX_STANDARD 23) add_compile_options( -Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wno-narrowing -Wno-pointer-arith $<$:-fpermissive> -Wno-address-of-temporary) # dependencies message(STATUS "Checking deps...") add_subdirectory(hyprland-share-picker) find_package(Threads REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules( deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols libpipewire-0.3>=1.1.82 libspa-0.2 libdrm gbm hyprlang>=0.2.0 hyprutils hyprwayland-scanner>=0.4.2) # check whether we can find sdbus-c++ through pkg-config pkg_check_modules(SDBUS IMPORTED_TARGET sdbus-c++>=2.0.0) if(NOT SDBUS_FOUND) include_directories("subprojects/sdbus-cpp/include/") add_subdirectory(subprojects/sdbus-cpp EXCLUDE_FROM_ALL) add_library(PkgConfig::SDBUS ALIAS sdbus-c++) endif() # same for hyprland-protocols pkg_check_modules(HYPRLAND_PROTOS IMPORTED_TARGET hyprland-protocols) if(HYPRLAND_PROTOS_FOUND) set(HYPRLAND_PROTOCOLS "${HYPRLAND_PROTOS_PREFIX}/share/hyprland-protocols") else() set(HYPRLAND_PROTOCOLS "${CMAKE_SOURCE_DIR}/subprojects/hyprland-protocols") endif() file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") add_executable(xdg-desktop-portal-hyprland ${SRCFILES}) target_link_libraries( xdg-desktop-portal-hyprland PRIVATE rt PkgConfig::SDBUS Threads::Threads PkgConfig::deps) # protocols pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") pkg_get_variable(WAYLAND_SCANNER_DIR wayland-scanner pkgdatadir) message(STATUS "Found wayland-scanner at ${WAYLAND_SCANNER_DIR}") function(protocolnew protoPath protoName external) if(external) set(path ${protoPath}) else() set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath}) endif() add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml ${CMAKE_SOURCE_DIR}/protocols/ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) target_sources(xdg-desktop-portal-hyprland PRIVATE protocols/${protoName}.cpp protocols/${protoName}.hpp) endfunction() function(protocolWayland) add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/protocols/wayland.cpp ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp COMMAND hyprwayland-scanner --wayland-enums --client ${WAYLAND_SCANNER_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) target_sources(xdg-desktop-portal-hyprland PRIVATE protocols/wayland.cpp protocols/wayland.hpp) endfunction() protocolwayland() protocolnew("${CMAKE_SOURCE_DIR}/protocols" "wlr-foreign-toplevel-management-unstable-v1" true) protocolnew("${CMAKE_SOURCE_DIR}/protocols" "wlr-screencopy-unstable-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-global-shortcuts-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-toplevel-export-v1" true) protocolnew("unstable/linux-dmabuf" "linux-dmabuf-unstable-v1" false) # Installation install(TARGETS hyprland-share-picker) install(TARGETS xdg-desktop-portal-hyprland DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) install(FILES hyprland.portal DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/xdg-desktop-portal/portals") install( FILES ${CMAKE_BINARY_DIR}/org.freedesktop.impl.portal.desktop.hyprland.service DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services") if(SYSTEMD_SERVICES) install( FILES ${CMAKE_BINARY_DIR}/contrib/systemd/xdg-desktop-portal-hyprland.service DESTINATION "lib/systemd/user") endif() hyprwm-xdg-desktop-portal-hyprland-fb1ce16/CONTRIBUTING.md000066400000000000000000000022611470750335700233430ustar00rootroot00000000000000# Contributing We closely follow the wlroots [contributing] guidelines where possible. Please see that document for more information. ## Tooling Useful tools include `dbus-monitor` to watch requests being made, and `dbus-send` and the similar `busctl call` for manual dbus calls. You can test the integration with the [portal-test] Flatpak app. Alternatively you can trigger it with [trigger-screen-shot.py] and [xdp-screen-cast.py]. [contributing]: https://github.com/swaywm/wlroots/blob/master/CONTRIBUTING.md [portal-test]: https://github.com/matthiasclasen/portal-test [trigger-screen-shot.py]: https://gist.github.com/danshick/3446dac24c64ce6172eced4ac255ac3d [xdp-screen-cast.py]: https://gitlab.gnome.org/snippets/19 ## Alternate *.portal Location xdg-desktop-portal will read the XDG_DESKTOP_PORTAL_DIR environment variable for an alternate path for *.portal files. This can be useful when testing changes to that portal file, or for testing xdpw without installing it. This feature is undocumented and shouldn't be relied on, but may be helpful in some circumstances. https://github.com/flatpak/xdg-desktop-portal/blob/e7f78640e35debb68fef891fc233c449006d9724/src/portal-impl.c#L124 hyprwm-xdg-desktop-portal-hyprland-fb1ce16/LICENSE000066400000000000000000000027531470750335700221250ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2023, vaxerski All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.hyprwm-xdg-desktop-portal-hyprland-fb1ce16/README.md000066400000000000000000000025741470750335700224000ustar00rootroot00000000000000# xdg-desktop-portal-hyprland An [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal) backend for Hyprland. ## Installing First, make sure to install the required dependencies: ``` gbm hyprland-protocols hyprlang hyprutils hyprwayland-scanner libdrm libpipewire-0.3 libspa-0.2 sdbus-cpp wayland-client wayland-protocols ``` Then run the build and install command: ```sh git clone --recursive https://github.com/hyprwm/xdg-desktop-portal-hyprland cd xdg-desktop-portal-hyprland/ cmake -DCMAKE_INSTALL_LIBEXECDIR=/usr/lib -DCMAKE_INSTALL_PREFIX=/usr -B build cmake --build build sudo cmake --install build ``` ## Nix > [!CAUTION] > XDPH should not be used from this flake directly! > > Instead, use it from the [Hyprland flake](https://github.com/hyprwm/Hyprland). There are two reasons for the above: 1. Hyprland depends on XDPH, but XDPH also depends on Hyprland. This results in a cyclic dependency, which is a nightmare. To counter this, we use the Nixpkgs Hyprland package in this flake, so that it can be later consumed by the Hyprland flake while overriding the Hyprland package. 2. Even if you manually do all the overriding, you may still get it wrong and lose out on the Cachix cache (which has XDPH as exposed by the Hyprland flake). ## Running, FAQs, etc. See [the Hyprland wiki](https://wiki.hyprland.org/Hypr-Ecosystem/xdg-desktop-portal-hyprland/) hyprwm-xdg-desktop-portal-hyprland-fb1ce16/VERSION000066400000000000000000000000061470750335700221550ustar00rootroot000000000000001.3.8 hyprwm-xdg-desktop-portal-hyprland-fb1ce16/contrib/000077500000000000000000000000001470750335700225515ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/contrib/config.sample000066400000000000000000000001251470750335700252170ustar00rootroot00000000000000[screencast] output_name= max_fps=30 chooser_cmd=slurp -f %o -or chooser_type=simple hyprwm-xdg-desktop-portal-hyprland-fb1ce16/contrib/systemd/000077500000000000000000000000001470750335700242415ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/contrib/systemd/xdg-desktop-portal-hyprland.service.in000066400000000000000000000005041470750335700335760ustar00rootroot00000000000000[Unit] Description=Portal service (Hyprland implementation) PartOf=graphical-session.target After=graphical-session.target ConditionEnvironment=WAYLAND_DISPLAY [Service] Type=dbus BusName=org.freedesktop.impl.portal.desktop.hyprland ExecStart=@LIBEXECDIR@/xdg-desktop-portal-hyprland Restart=on-failure Slice=session.slice hyprwm-xdg-desktop-portal-hyprland-fb1ce16/flake.lock000066400000000000000000000077131470750335700230550ustar00rootroot00000000000000{ "nodes": { "hyprland-protocols": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1721326555, "narHash": "sha256-zCu4R0CSHEactW9JqYki26gy8h9f6rHmSwj4XJmlHgg=", "owner": "hyprwm", "repo": "hyprland-protocols", "rev": "5a11232266bf1a1f5952d5b179c3f4b2facaaa84", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprland-protocols", "type": "github" } }, "hyprlang": { "inputs": { "hyprutils": "hyprutils", "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1725997860, "narHash": "sha256-d/rZ/fHR5l1n7PeyLw0StWMNLXVU9c4HFyfskw568so=", "owner": "hyprwm", "repo": "hyprlang", "rev": "dfeb5811dd6485490cce18d6cc1e38a055eea876", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprlang", "type": "github" } }, "hyprutils": { "inputs": { "nixpkgs": [ "hyprlang", "nixpkgs" ], "systems": [ "hyprlang", "systems" ] }, "locked": { "lastModified": 1721324102, "narHash": "sha256-WAZ0X6yJW1hFG6otkHBfyJDKRpNP5stsRqdEuHrFRpk=", "owner": "hyprwm", "repo": "hyprutils", "rev": "962582a090bc233c4de9d9897f46794280288989", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprutils", "type": "github" } }, "hyprutils_2": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1724966483, "narHash": "sha256-WXDgKIbzjYKczxSZOsJplCS1i1yrTUpsDPuJV/xpYLo=", "owner": "hyprwm", "repo": "hyprutils", "rev": "8976e3f6a5357da953a09511d0c7f6a890fb6ec2", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprutils", "type": "github" } }, "hyprwayland-scanner": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1726840673, "narHash": "sha256-HIPEXyRRVZoqD6U+lFS1B0tsIU7p83FaB9m7KT/x6mQ=", "owner": "hyprwm", "repo": "hyprwayland-scanner", "rev": "b68dab23fc922eae99306988133ee80a40b39ca5", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprwayland-scanner", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1726463316, "narHash": "sha256-gI9kkaH0ZjakJOKrdjaI/VbaMEo9qBbSUl93DnU7f4c=", "owner": "NixOS", "repo": "nixpkgs", "rev": "99dc8785f6a0adac95f5e2ab05cc2e1bf666d172", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "hyprland-protocols": "hyprland-protocols", "hyprlang": "hyprlang", "hyprutils": "hyprutils_2", "hyprwayland-scanner": "hyprwayland-scanner", "nixpkgs": "nixpkgs", "systems": "systems" } }, "systems": { "locked": { "lastModified": 1689347949, "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", "owner": "nix-systems", "repo": "default-linux", "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default-linux", "type": "github" } } }, "root": "root", "version": 7 } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/flake.nix000066400000000000000000000027331470750335700227200ustar00rootroot00000000000000{ description = "xdg-desktop-portal-hyprland"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # systems.url = "github:nix-systems/default-linux"; hyprland-protocols = { url = "github:hyprwm/hyprland-protocols"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; hyprlang = { url = "github:hyprwm/hyprlang"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; hyprutils = { url = "github:hyprwm/hyprutils"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; hyprwayland-scanner = { url = "github:hyprwm/hyprwayland-scanner"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; }; outputs = { self, nixpkgs, systems, ... } @ inputs: let inherit (nixpkgs) lib; eachSystem = lib.genAttrs (import systems); pkgsFor = eachSystem (system: import nixpkgs { localSystem = system; overlays = [self.overlays.default]; }); in { overlays = import ./nix/overlays.nix {inherit self inputs lib;}; packages = eachSystem (system: { inherit (pkgsFor.${system}) xdg-desktop-portal-hyprland; default = self.packages.${system}.xdg-desktop-portal-hyprland; }); formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); }; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/000077500000000000000000000000001470750335700253055ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/CMakeLists.txt000066400000000000000000000035341470750335700300520ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.5) project( hyprland-share-picker VERSION 0.1 LANGUAGES CXX) set(QT_VERSION_MAJOR 6) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets) find_package(Qt6 REQUIRED COMPONENTS Widgets) set(PROJECT_SOURCES main.cpp mainpicker.cpp mainpicker.h mainpicker.ui elidedbutton.h elidedbutton.cpp) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(hyprland-share-picker MANUAL_FINALIZATION ${PROJECT_SOURCES}) # Define target properties for Android with Qt 6 as: set_property(TARGET # hyprland-share-picker APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR # ${CMAKE_CURRENT_SOURCE_DIR}/android) For more information, see # https://doc.qt.io/qt-6/qt-add-executable.html#target-creation else() if(ANDROID) add_library(hyprland-share-picker SHARED ${PROJECT_SOURCES}) # Define properties for Android with Qt 5 after find_package() calls as: # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") else() add_executable(hyprland-share-picker ${PROJECT_SOURCES}) endif() endif() target_link_libraries(hyprland-share-picker PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties( hyprland-share-picker PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} MACOSX_BUNDLE TRUE WIN32_EXECUTABLE TRUE) install( TARGETS hyprland-share-picker BUNDLE DESTINATION . LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) if(QT_VERSION_MAJOR EQUAL 6) qt_finalize_executable(hyprland-share-picker) endif() hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/Makefile000066400000000000000000000002631470750335700267460ustar00rootroot00000000000000 all: mkdir -p build && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja cmake --build ./build --config Release --target all -j$(shell nproc)hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/elidedbutton.cpp000066400000000000000000000011311470750335700304670ustar00rootroot00000000000000#include "elidedbutton.h" ElidedButton::ElidedButton(QWidget *parent) : QPushButton(parent) { } ElidedButton::ElidedButton( const QString& text, QWidget* parent ) : ElidedButton( parent ) { setText(text); } void ElidedButton::setText(QString text) { og_text = text; updateText(); } void ElidedButton::resizeEvent(QResizeEvent *event) { QPushButton::resizeEvent(event); updateText(); } void ElidedButton::updateText() { QFontMetrics metrics(font()); QString elided = metrics.elidedText(og_text, Qt::ElideRight, width() - 15); QPushButton::setText(elided); }hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/elidedbutton.h000066400000000000000000000006321470750335700301410ustar00rootroot00000000000000#ifndef ELIDEDBUTTON_H #define ELIDEDBUTTON_H #include class ElidedButton : public QPushButton { public: explicit ElidedButton(QWidget *parent = nullptr); explicit ElidedButton(const QString &text, QWidget *parent = nullptr); void setText(QString); protected: void resizeEvent(QResizeEvent *); private: void updateText(); QString og_text; }; #endif // ELIDEDBUTTON_H hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/main.cpp000066400000000000000000000210551470750335700267400ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mainpicker.h" #include "elidedbutton.h" std::string execAndGet(const char* cmd) { std::array buffer; std::string result; std::unique_ptr pipe(popen(cmd, "r"), pclose); if (!pipe) { throw std::runtime_error("popen() failed!"); } while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { result += buffer.data(); } return result; } QApplication* pickerPtr = nullptr; MainPicker* mainPickerPtr = nullptr; struct SWindowEntry { std::string name; std::string clazz; unsigned long long id = 0; }; std::vector getWindows(const char* env) { std::vector result; if (!env) return result; std::string rolling = env; while (!rolling.empty()) { // ID const auto IDSEPPOS = rolling.find("[HC>]"); const auto IDSTR = rolling.substr(0, IDSEPPOS); // class const auto CLASSSEPPOS = rolling.find("[HT>]"); const auto CLASSSTR = rolling.substr(IDSEPPOS + 5, CLASSSEPPOS - IDSEPPOS - 5); // title const auto TITLESEPPOS = rolling.find("[HE>]"); const auto TITLESTR = rolling.substr(CLASSSEPPOS + 5, TITLESEPPOS - 5 - CLASSSEPPOS); try { result.push_back({TITLESTR, CLASSSTR, std::stoull(IDSTR)}); } catch (std::exception& e) { // silent err } rolling = rolling.substr(TITLESEPPOS + 5); } return result; } int main(int argc, char* argv[]) { qputenv("QT_LOGGING_RULES", "qml=false"); bool allowTokenByDefault = false; for (int i = 1; i < argc; ++i) { if (argv[i] == std::string{"--allow-token"}) allowTokenByDefault = true; } const char* WINDOWLISTSTR = getenv("XDPH_WINDOW_SHARING_LIST"); const auto WINDOWLIST = getWindows(WINDOWLISTSTR); QApplication picker(argc, argv); pickerPtr = &picker; MainPicker w; mainPickerPtr = &w; QSettings* settings = new QSettings("/tmp/hypr/hyprland-share-picker.conf", QSettings::IniFormat); w.setGeometry(0, 0, settings->value("width").toInt(), settings->value("height").toInt()); // get the tabwidget const auto TABWIDGET = w.findChild("tabWidget"); const auto ALLOWTOKENBUTTON = w.findChild("checkBox"); if (allowTokenByDefault) ALLOWTOKENBUTTON->setCheckState(Qt::CheckState::Checked); const auto TAB1 = (QWidget*)TABWIDGET->children()[0]; const auto SCREENS_SCROLL_AREA_CONTENTS = (QWidget*)TAB1->findChild("screens")->findChild("scrollArea")->findChild("scrollAreaWidgetContents"); const auto SCREENS_SCROLL_AREA_CONTENTS_LAYOUT = SCREENS_SCROLL_AREA_CONTENTS->layout(); // add all screens const auto SCREENS = picker.screens(); constexpr int BUTTON_HEIGHT = 41; for (int i = 0; i < SCREENS.size(); ++i) { const auto GEOMETRY = SCREENS[i]->geometry(); QString text = QString::fromStdString(std::string("Screen " + std::to_string(i) + " at " + std::to_string(GEOMETRY.x()) + ", " + std::to_string(GEOMETRY.y()) + " (" + std::to_string(GEOMETRY.width()) + "x" + std::to_string(GEOMETRY.height()) + ") (") + SCREENS[i]->name().toStdString() + ")"); QString outputName = SCREENS[i]->name(); ElidedButton* button = new ElidedButton(text); button->setMinimumSize(0, BUTTON_HEIGHT); SCREENS_SCROLL_AREA_CONTENTS_LAYOUT->addWidget(button); QObject::connect(button, &QPushButton::clicked, [=]() { std::cout << "[SELECTION]"; std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); std::cout << "/"; std::cout << "screen:" << outputName.toStdString() << "\n"; settings->setValue("width", mainPickerPtr->width()); settings->setValue("height", mainPickerPtr->height()); settings->sync(); pickerPtr->quit(); return 0; }); } QSpacerItem* SCREENS_SPACER = new QSpacerItem(0, 10000, QSizePolicy::Expanding, QSizePolicy::Expanding); SCREENS_SCROLL_AREA_CONTENTS_LAYOUT->addItem(SCREENS_SPACER); // windows const auto WINDOWS_SCROLL_AREA_CONTENTS = (QWidget*)TAB1->findChild("windows")->findChild("scrollArea_2")->findChild("scrollAreaWidgetContents_2"); const auto WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT = WINDOWS_SCROLL_AREA_CONTENTS->layout(); // loop over them int windowIterator = 0; for (auto& window : WINDOWLIST) { QString text = QString::fromStdString(window.clazz + ": " + window.name); ElidedButton* button = new ElidedButton(text); button->setMinimumSize(0, BUTTON_HEIGHT); WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT->addWidget(button); mainPickerPtr->windowIDs[button] = window.id; QObject::connect(button, &QPushButton::clicked, [=]() { std::cout << "[SELECTION]"; std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); std::cout << "/"; std::cout << "window:" << mainPickerPtr->windowIDs[button] << "\n"; settings->setValue("width", mainPickerPtr->width()); settings->setValue("height", mainPickerPtr->height()); settings->sync(); pickerPtr->quit(); return 0; }); windowIterator++; } QSpacerItem* WINDOWS_SPACER = new QSpacerItem(0, 10000, QSizePolicy::Expanding, QSizePolicy::Expanding); WINDOWS_SCROLL_AREA_CONTENTS_LAYOUT->addItem(WINDOWS_SPACER); // lastly, region const auto REGION_OBJECT = (QWidget*)TAB1->findChild("region"); const auto REGION_LAYOUT = REGION_OBJECT->layout(); QString text = "Select region..."; ElidedButton* button = new ElidedButton(text); button->setMaximumSize(400, BUTTON_HEIGHT); REGION_LAYOUT->addWidget(button); QObject::connect(button, &QPushButton::clicked, [=]() { auto REGION = execAndGet("slurp -f \"%o %x %y %w %h\""); REGION = REGION.substr(0, REGION.length()); // now, get the screen QScreen* pScreen = nullptr; if (REGION.find_first_of(' ') == std::string::npos) { std::cout << "error1\n"; pickerPtr->quit(); return 1; } const auto SCREEN_NAME = REGION.substr(0, REGION.find_first_of(' ')); for (auto& screen : SCREENS) { if (screen->name().toStdString() == SCREEN_NAME) { pScreen = screen; break; } } if (!pScreen) { std::cout << "error2\n"; pickerPtr->quit(); return 1; } // get all the coords try { REGION = REGION.substr(REGION.find_first_of(' ') + 1); const auto X = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); REGION = REGION.substr(REGION.find_first_of(' ') + 1); const auto Y = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); REGION = REGION.substr(REGION.find_first_of(' ') + 1); const auto W = std::stoi(REGION.substr(0, REGION.find_first_of(' '))); REGION = REGION.substr(REGION.find_first_of(' ') + 1); const auto H = std::stoi(REGION); std::cout << "[SELECTION]"; std::cout << (ALLOWTOKENBUTTON->isChecked() ? "r" : ""); std::cout << "/"; std::cout << "region:" << SCREEN_NAME << "@" << X - pScreen->geometry().x() << "," << Y - pScreen->geometry().y() << "," << W << "," << H << "\n"; settings->setValue("width", mainPickerPtr->width()); settings->setValue("height", mainPickerPtr->height()); settings->sync(); pickerPtr->quit(); return 0; } catch (...) { std::cout << "error3\n"; pickerPtr->quit(); return 1; } std::cout << "error4\n"; pickerPtr->quit(); return 1; }); w.show(); return picker.exec(); } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/mainpicker.cpp000066400000000000000000000005261470750335700301360ustar00rootroot00000000000000#include "mainpicker.h" #include "./ui_mainpicker.h" #include MainPicker::MainPicker(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainPicker) { ui->setupUi(this); } MainPicker::~MainPicker() { delete ui; } void MainPicker::onMonitorButtonClicked(QObject* target, QEvent* event) { qDebug() << "click"; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/mainpicker.h000066400000000000000000000010021470750335700275710ustar00rootroot00000000000000#ifndef MAINPICKER_H #define MAINPICKER_H #include #include #include #include QT_BEGIN_NAMESPACE namespace Ui { class MainPicker; } QT_END_NAMESPACE class MainPicker : public QMainWindow { Q_OBJECT public: MainPicker(QWidget *parent = nullptr); ~MainPicker(); void onMonitorButtonClicked(QObject* target, QEvent* event); std::unordered_map windowIDs; // button -> id private: Ui::MainPicker *ui; }; #endif // MAINPICKER_H hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/mainpicker.ui000066400000000000000000000215211470750335700277670ustar00rootroot00000000000000 MainPicker 0 0 500 300 0 0 500 290 1280 800 MainPicker 0 0 0 0 1280 800 0 0 0 0 16777215 16777215 Qt::StrongFocus QTabWidget::North 0 0 0 Qt::LeftToRight Screen 0 0 Qt::NoFocus Qt::ScrollBarAlwaysOff QAbstractScrollArea::AdjustIgnored true Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true 0 0 410 18 0 0 0 0 16777215 16777215 Qt::NoFocus 6 QLayout::SetDefaultConstraint 0 0 Window Qt::NoFocus Qt::ScrollBarAlwaysOff true Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 0 0 410 18 0 0 0 0 Qt::NoFocus 0 0 16777215 16777215 Qt::LeftToRight false Region QLayout::SetDefaultConstraint Qt::ClickFocus By selecting this, the application will be given a restore token that it can use to skip prompting you next time. Only select if you trust the application. Qt::LeftToRight Allow a restore token tabWidget hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland-share-picker/meson.build000066400000000000000000000007111470750335700274460ustar00rootroot00000000000000# select either qt6 or qt5 qtdep = dependency('qt6', 'qt5', modules: ['Widgets']) qtver = qtdep.version() qt = import('qt' + qtver[0]) ui_files = qt.compile_ui(sources: 'mainpicker.ui') moc = qt.compile_moc(headers: 'mainpicker.h') sources = files([ 'main.cpp', 'mainpicker.cpp', 'mainpicker.h', 'elidedbutton.h', 'elidedbutton.cpp', ]) executable('hyprland-share-picker', sources, ui_files, moc, dependencies: qtdep, install: true ) hyprwm-xdg-desktop-portal-hyprland-fb1ce16/hyprland.portal000066400000000000000000000003601470750335700241540ustar00rootroot00000000000000[portal] DBusName=org.freedesktop.impl.portal.desktop.hyprland Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; UseIn=wlroots;Hyprland;sway;Wayfire;river; hyprwm-xdg-desktop-portal-hyprland-fb1ce16/meson.build000066400000000000000000000042171470750335700232570ustar00rootroot00000000000000project('xdg-desktop-portal-hyprland', 'cpp', 'c', version: run_command('cat', files('VERSION'), check: true).stdout().strip(), license: 'BSD-3-Clause', meson_version: '>=0.63.0', default_options: [ 'warning_level=2', 'optimization=3', 'buildtype=release', 'debug=false', # 'cpp_std=c++23' # not yet supported by meson, as of version 0.63.0 ], ) # clang v14.0.6 uses C++2b instead of C++23, so we've gotta account for that # replace the following with a project default option once meson gets support for C++23 cpp_compiler = meson.get_compiler('cpp') if cpp_compiler.has_argument('-std=c++23') add_global_arguments('-std=c++23', language: 'cpp') elif cpp_compiler.has_argument('-std=c++2b') add_global_arguments('-std=c++2b', language: 'cpp') else error('Could not configure current C++ compiler (' + cpp_compiler.get_id() + ' ' + cpp_compiler.version() + ') with required C++ standard (C++23)') endif add_project_arguments(cpp_compiler.get_supported_arguments([ '-Wno-missing-field-initializers', '-Wno-narrowing', '-Wno-pointer-arith', '-Wno-unused-parameter', '-Wno-unused-value', '-fpermissive', '-Wno-address-of-temporary' ]), language: 'cpp') conf_data = configuration_data() conf_data.set('LIBEXECDIR', join_paths(get_option('prefix'), get_option('libexecdir'))) systemd = dependency('systemd', required: get_option('systemd')) if systemd.found() systemd_service_file = 'xdg-desktop-portal-hyprland.service' user_unit_dir = systemd.get_variable(pkgconfig: 'systemduserunitdir', pkgconfig_define: ['prefix', get_option('prefix')]) configure_file( configuration: conf_data, input: 'contrib/systemd/' + systemd_service_file + '.in', output: '@BASENAME@', install_dir: user_unit_dir, ) endif configure_file( configuration: conf_data, input: 'org.freedesktop.impl.portal.desktop.hyprland.service.in', output: '@BASENAME@', install_dir: join_paths(get_option('datadir'), 'dbus-1', 'services'), ) install_data( 'hyprland.portal', install_dir: join_paths(get_option('datadir'), 'xdg-desktop-portal', 'portals'), ) inc = include_directories('.', 'protocols') subdir('protocols') subdir('src') subdir('hyprland-share-picker') hyprwm-xdg-desktop-portal-hyprland-fb1ce16/meson_options.txt000066400000000000000000000001441470750335700245450ustar00rootroot00000000000000option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') hyprwm-xdg-desktop-portal-hyprland-fb1ce16/nix/000077500000000000000000000000001470750335700217075ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/nix/default.nix000066400000000000000000000030221470750335700240500ustar00rootroot00000000000000{ lib, stdenv, cmake, makeWrapper, pkg-config, wrapQtAppsHook, hyprland, hyprland-protocols, hyprlang, hyprutils, hyprwayland-scanner, libdrm, mesa, pipewire, qtbase, qttools, qtwayland, sdbus-cpp, slurp, systemd, wayland, wayland-protocols, wayland-scanner, debug ? false, version ? "git", }: stdenv.mkDerivation { pname = "xdg-desktop-portal-hyprland" + lib.optionalString debug "-debug"; inherit version; src = ../.; depsBuildBuild = [ pkg-config ]; nativeBuildInputs = [ cmake makeWrapper pkg-config wrapQtAppsHook hyprwayland-scanner ]; buildInputs = [ hyprland-protocols hyprlang hyprutils libdrm mesa pipewire qtbase qttools qtwayland sdbus-cpp systemd wayland wayland-protocols wayland-scanner ]; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; dontStrip = true; dontWrapQtApps = true; postInstall = '' wrapProgramShell $out/bin/hyprland-share-picker \ "''${qtWrapperArgs[@]}" \ --prefix PATH ":" ${lib.makeBinPath [slurp hyprland]} wrapProgramShell $out/libexec/xdg-desktop-portal-hyprland \ --prefix PATH ":" ${lib.makeBinPath [(placeholder "out")]} ''; meta = with lib; { homepage = "https://github.com/hyprwm/xdg-desktop-portal-hyprland"; description = "xdg-desktop-portal backend for Hyprland"; license = licenses.bsd3; maintainers = with maintainers; [fufexan]; platforms = platforms.linux; }; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/nix/overlays.nix000066400000000000000000000026431470750335700243000ustar00rootroot00000000000000{ self, inputs, lib, }: let ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION); mkJoinedOverlays = overlays: final: prev: lib.foldl' (attrs: overlay: attrs // (overlay final prev)) {} overlays; mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) (builtins.substring 4 2 longDate) (builtins.substring 6 2 longDate) ]); version = ver + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); in { default = mkJoinedOverlays (with self.overlays; [ xdg-desktop-portal-hyprland inputs.hyprlang.overlays.default inputs.hyprland-protocols.overlays.default inputs.hyprutils.overlays.default inputs.hyprwayland-scanner.overlays.default self.overlays.sdbuscpp ]); xdg-desktop-portal-hyprland = lib.composeManyExtensions [ (final: prev: { xdg-desktop-portal-hyprland = final.callPackage ./default.nix { stdenv = prev.gcc13Stdenv; inherit (final.qt6) qtbase qttools wrapQtAppsHook qtwayland; inherit version; }; }) ]; sdbuscpp = final: prev: { sdbus-cpp = prev.sdbus-cpp.overrideAttrs (self: super: { version = "2.0.0"; src = final.fetchFromGitHub { owner = "Kistler-group"; repo = "sdbus-cpp"; rev = "refs/tags/v${self.version}"; hash = "sha256-W8V5FRhV3jtERMFrZ4gf30OpIQLYoj2yYGpnYOmH2+g="; }; }); }; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/org.freedesktop.impl.portal.desktop.hyprland.service.in000066400000000000000000000002431470750335700337300ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.impl.portal.desktop.hyprland Exec=@LIBEXECDIR@/xdg-desktop-portal-hyprland SystemdService=xdg-desktop-portal-hyprland.service hyprwm-xdg-desktop-portal-hyprland-fb1ce16/protocols/000077500000000000000000000000001470750335700231355ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/protocols/meson.build000066400000000000000000000033101470750335700252740ustar00rootroot00000000000000wayland_protos = dependency('wayland-protocols', version: '>=1.31', default_options: ['tests=false'], ) hyprland_protos = dependency('hyprland-protocols', version: '>=0.2', fallback: 'hyprland-protocols', ) wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') hl_protocol_dir = hyprland_protos.get_variable('pkgdatadir') hyprwayland_scanner_dep = dependency('hyprwayland-scanner', required: true, native: true, version: '>=0.4.2') hyprwayland_scanner = find_program( hyprwayland_scanner_dep.get_variable(pkgconfig: 'hyprwayland_scanner'), native: true, ) client_protocols = [ 'wlr-screencopy-unstable-v1.xml', 'wlr-foreign-toplevel-management-unstable-v1.xml', hl_protocol_dir / 'protocols/hyprland-toplevel-export-v1.xml', hl_protocol_dir / 'protocols/hyprland-global-shortcuts-v1.xml', wl_protocol_dir / 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml', ] wl_proto_files = [] foreach xml: client_protocols wl_proto_files += custom_target( xml.underscorify() + '_c', input: xml, output: ['@BASENAME@.cpp', '@BASENAME@.hpp'], command: [hyprwayland_scanner, '--client', '@INPUT@', '@OUTDIR@'], ) endforeach wayland_scanner = dependency('wayland-scanner') wayland_scanner_dir = wayland_scanner.get_variable('pkgdatadir') wayland_xml = wayland_scanner_dir / 'wayland.xml' wayland_protocol = custom_target( wayland_xml.underscorify(), input: wayland_xml, output: ['@BASENAME@.cpp', '@BASENAME@.hpp'], command: [hyprwayland_scanner, '--wayland-enums', '--client', '@INPUT@', '@OUTDIR@'], ) lib_client_protos = static_library( 'client_protos', wl_proto_files + wayland_protocol, ) client_protos = declare_dependency( link_with: lib_client_protos, sources: wl_proto_files + wayland_protocol ) hyprwm-xdg-desktop-portal-hyprland-fb1ce16/protocols/wlr-foreign-toplevel-management-unstable-v1.xml000066400000000000000000000264441470750335700342250ustar00rootroot00000000000000 Copyright © 2018 Ilia Bozhinov Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. The purpose of this protocol is to enable the creation of taskbars and docks by providing them with a list of opened applications and letting them request certain actions on them, like maximizing, etc. After a client binds the zwlr_foreign_toplevel_manager_v1, each opened toplevel window will be sent via the toplevel event This event is emitted whenever a new toplevel window is created. It is emitted for all toplevels, regardless of the app that has created them. All initial details of the toplevel(title, app_id, states, etc.) will be sent immediately after this event via the corresponding events in zwlr_foreign_toplevel_handle_v1. Indicates the client no longer wishes to receive events for new toplevels. However the compositor may emit further toplevel_created events, until the finished event is emitted. The client must not send any more requests after this one. This event indicates that the compositor is done sending events to the zwlr_foreign_toplevel_manager_v1. The server will destroy the object immediately after sending this request, so it will become invalid and the client should free any resources associated with it. A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel window. Each app may have multiple opened toplevels. Each toplevel has a list of outputs it is visible on, conveyed to the client with the output_enter and output_leave events. This event is emitted whenever the title of the toplevel changes. This event is emitted whenever the app-id of the toplevel changes. This event is emitted whenever the toplevel becomes visible on the given output. A toplevel may be visible on multiple outputs. This event is emitted whenever the toplevel stops being visible on the given output. It is guaranteed that an entered-output event with the same output has been emitted before this event. Requests that the toplevel be maximized. If the maximized state actually changes, this will be indicated by the state event. Requests that the toplevel be unmaximized. If the maximized state actually changes, this will be indicated by the state event. Requests that the toplevel be minimized. If the minimized state actually changes, this will be indicated by the state event. Requests that the toplevel be unminimized. If the minimized state actually changes, this will be indicated by the state event. Request that this toplevel be activated on the given seat. There is no guarantee the toplevel will be actually activated. The different states that a toplevel can have. These have the same meaning as the states with the same names defined in xdg-toplevel This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 is created and each time the toplevel state changes, either because of a compositor action or because of a request in this protocol. This event is sent after all changes in the toplevel state have been sent. This allows changes to the zwlr_foreign_toplevel_handle_v1 properties to be seen as atomic, even if they happen via multiple events. Send a request to the toplevel to close itself. The compositor would typically use a shell-specific method to carry out this request, for example by sending the xdg_toplevel.close event. However, this gives no guarantees the toplevel will actually be destroyed. If and when this happens, the zwlr_foreign_toplevel_handle_v1.closed event will be emitted. The rectangle of the surface specified in this request corresponds to the place where the app using this protocol represents the given toplevel. It can be used by the compositor as a hint for some operations, e.g minimizing. The client is however not required to set this, in which case the compositor is free to decide some default value. If the client specifies more than one rectangle, only the last one is considered. The dimensions are given in surface-local coordinates. Setting width=height=0 removes the already-set rectangle. This event means the toplevel has been destroyed. It is guaranteed there won't be any more events for this zwlr_foreign_toplevel_handle_v1. The toplevel itself becomes inert so any requests will be ignored except the destroy request. Destroys the zwlr_foreign_toplevel_handle_v1 object. This request should be called either when the client does not want to use the toplevel anymore or after the closed event to finalize the destruction of the object. Requests that the toplevel be fullscreened on the given output. If the fullscreen state and/or the outputs the toplevel is visible on actually change, this will be indicated by the state and output_enter/leave events. The output parameter is only a hint to the compositor. Also, if output is NULL, the compositor should decide which output the toplevel will be fullscreened on, if at all. Requests that the toplevel be unfullscreened. If the fullscreen state actually changes, this will be indicated by the state event. This event is emitted whenever the parent of the toplevel changes. No event is emitted when the parent handle is destroyed by the client. hyprwm-xdg-desktop-portal-hyprland-fb1ce16/protocols/wlr-screencopy-unstable-v1.xml000066400000000000000000000226051470750335700307770ustar00rootroot00000000000000 Copyright © 2018 Simon Ser Copyright © 2019 Andri Yngvason Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This protocol allows clients to ask the compositor to copy part of the screen content to a client buffer. Warning! The protocol described in this file is experimental and backward incompatible changes may be made. Backward compatible changes may be added together with the corresponding interface version bump. Backward incompatible changes are done by bumping the version number in the protocol and interface names and resetting the interface version. Once the protocol is to be declared stable, the 'z' prefix and the version number in the protocol and interface names are removed and the interface version number is reset. This object is a manager which offers requests to start capturing from a source. Capture the next frame of an entire output. Capture the next frame of an output's region. The region is given in output logical coordinates, see xdg_output.logical_size. The region will be clipped to the output's extents. All objects created by the manager will still remain valid, until their appropriate destroy request has been called. This object represents a single frame. When created, a series of buffer events will be sent, each representing a supported buffer type. The "buffer_done" event is sent afterwards to indicate that all supported buffer types have been enumerated. The client will then be able to send a "copy" request. If the capture is successful, the compositor will send a "flags" followed by a "ready" event. For objects version 2 or lower, wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. If the capture failed, the "failed" event is sent. This can happen anytime before the "ready" event. Once either a "ready" or a "failed" event is received, the client should destroy the frame. Provides information about wl_shm buffer parameters that need to be used for this frame. This event is sent once after the frame is created if wl_shm buffers are supported. Copy the frame to the supplied buffer. The buffer must have a the correct size, see zwlr_screencopy_frame_v1.buffer and zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a supported format. If the frame is successfully copied, a "flags" and a "ready" events are sent. Otherwise, a "failed" event is sent. Provides flags about the frame. This event is sent once before the "ready" event. Called as soon as the frame is copied, indicating it is available for reading. This event includes the time at which presentation happened at. The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, each component being an unsigned 32-bit value. Whole seconds are in tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, and the additional fractional part in tv_nsec as nanoseconds. Hence, for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part may have an arbitrary offset at start. After receiving this event, the client should destroy the object. This event indicates that the attempted frame copy has failed. After receiving this event, the client should destroy the object. Destroys the frame. This request can be sent at any time by the client. Same as copy, except it waits until there is damage to copy. This event is sent right before the ready event when copy_with_damage is requested. It may be generated multiple times for each copy_with_damage request. The arguments describe a box around an area that has changed since the last copy request that was derived from the current screencopy manager instance. The union of all regions received between the call to copy_with_damage and a ready event is the total damage since the prior ready event. Provides information about linux-dmabuf buffer parameters that need to be used for this frame. This event is sent once after the frame is created if linux-dmabuf buffers are supported. This event is sent once after all buffer events have been sent. The client should proceed to create a buffer of one of the supported types, and send a "copy" request. hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/000077500000000000000000000000001470750335700217005ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/core/000077500000000000000000000000001470750335700226305ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/core/PortalManager.cpp000066400000000000000000000464261470750335700261040ustar00rootroot00000000000000#include "PortalManager.hpp" #include "../helpers/Log.hpp" #include "../helpers/MiscFunctions.hpp" #include #include #include #include #include #include SOutput::SOutput(SP output_) : output(output_) { output->setName([this](CCWlOutput* o, const char* name_) { if (!name_) return; name = name_; Debug::log(LOG, "Found output name {}", name); }); output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { // refreshRate = refresh; }); 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, int32_t transform_) { // transform = (wl_output_transform)transform_; }); } CPortalManager::CPortalManager() { const auto XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME"); const auto HOME = getenv("HOME"); if (!HOME && !XDG_CONFIG_HOME) Debug::log(WARN, "neither $HOME nor $XDG_CONFIG_HOME is present in env"); std::string path = (!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"); m_sConfig.config = std::make_unique(path.c_str(), Hyprlang::SConfigOptions{.allowMissingConfig = true}); m_sConfig.config->addConfigValue("general:toplevel_dynamic_bind", Hyprlang::INT{0L}); m_sConfig.config->addConfigValue("screencopy:max_fps", Hyprlang::INT{120L}); m_sConfig.config->addConfigValue("screencopy:allow_token_by_default", Hyprlang::INT{0L}); m_sConfig.config->commence(); m_sConfig.config->parse(); } void CPortalManager::onGlobal(uint32_t name, const char* interface, uint32_t version) { const std::string INTERFACE = interface; Debug::log(LOG, " | Got interface: {} (ver {})", INTERFACE, version); if (INTERFACE == zwlr_screencopy_manager_v1_interface.name && m_sPipewire.loop) { m_sPortals.screencopy = std::make_unique(makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &zwlr_screencopy_manager_v1_interface, version))); } if (INTERFACE == hyprland_global_shortcuts_manager_v1_interface.name) { m_sPortals.globalShortcuts = std::make_unique(makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_global_shortcuts_manager_v1_interface, version))); } else if (INTERFACE == hyprland_toplevel_export_manager_v1_interface.name) { m_sWaylandConnection.hyprlandToplevelMgr = makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_toplevel_export_manager_v1_interface, version)); } else if (INTERFACE == wl_output_interface.name) { const auto POUTPUT = m_vOutputs .emplace_back(std::make_unique(makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &wl_output_interface, version)))) .get(); POUTPUT->id = name; } else if (INTERFACE == zwp_linux_dmabuf_v1_interface.name) { if (version < 4) { Debug::log(ERR, "cannot use linux_dmabuf with ver < 4"); return; } m_sWaylandConnection.linuxDmabuf = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &zwp_linux_dmabuf_v1_interface, version)); m_sWaylandConnection.linuxDmabufFeedback = makeShared(m_sWaylandConnection.linuxDmabuf->sendGetDefaultFeedback()); m_sWaylandConnection.linuxDmabufFeedback->setMainDevice([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* device_arr) { Debug::log(LOG, "[core] dmabufFeedbackMainDevice"); RASSERT(!m_sWaylandConnection.gbm, "double dmabuf feedback"); dev_t device; assert(device_arr->size == sizeof(device)); memcpy(&device, device_arr->data, sizeof(device)); drmDevice* drmDev; if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0) { Debug::log(WARN, "[dmabuf] unable to open main device?"); exit(1); } m_sWaylandConnection.gbmDevice = createGBMDevice(drmDev); }); m_sWaylandConnection.linuxDmabufFeedback->setFormatTable([this](CCZwpLinuxDmabufFeedbackV1* r, int fd, uint32_t size) { Debug::log(TRACE, "[core] dmabufFeedbackFormatTable"); m_vDMABUFMods.clear(); m_sWaylandConnection.dma.formatTable = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (m_sWaylandConnection.dma.formatTable == MAP_FAILED) { Debug::log(ERR, "[core] format table failed to mmap"); m_sWaylandConnection.dma.formatTable = nullptr; m_sWaylandConnection.dma.formatTableSize = 0; return; } m_sWaylandConnection.dma.formatTableSize = size; }); m_sWaylandConnection.linuxDmabufFeedback->setDone([this](CCZwpLinuxDmabufFeedbackV1* r) { Debug::log(TRACE, "[core] dmabufFeedbackDone"); if (m_sWaylandConnection.dma.formatTable) munmap(m_sWaylandConnection.dma.formatTable, m_sWaylandConnection.dma.formatTableSize); m_sWaylandConnection.dma.formatTable = nullptr; m_sWaylandConnection.dma.formatTableSize = 0; }); m_sWaylandConnection.linuxDmabufFeedback->setTrancheTargetDevice([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* device_arr) { Debug::log(TRACE, "[core] dmabufFeedbackTrancheTargetDevice"); dev_t device; assert(device_arr->size == sizeof(device)); memcpy(&device, device_arr->data, sizeof(device)); drmDevice* drmDev; if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0) return; if (m_sWaylandConnection.gbmDevice) { drmDevice* drmDevRenderer = NULL; drmGetDevice2(gbm_device_get_fd(m_sWaylandConnection.gbmDevice), /* flags */ 0, &drmDevRenderer); m_sWaylandConnection.dma.deviceUsed = drmDevicesEqual(drmDevRenderer, drmDev); } else { m_sWaylandConnection.gbmDevice = createGBMDevice(drmDev); m_sWaylandConnection.dma.deviceUsed = m_sWaylandConnection.gbm; } }); m_sWaylandConnection.linuxDmabufFeedback->setTrancheFormats([this](CCZwpLinuxDmabufFeedbackV1* r, wl_array* indices) { Debug::log(TRACE, "[core] dmabufFeedbackTrancheFormats"); if (!m_sWaylandConnection.dma.deviceUsed || !m_sWaylandConnection.dma.formatTable) return; struct fm_entry { uint32_t format; uint32_t padding; uint64_t modifier; }; // An entry in the table has to be 16 bytes long assert(sizeof(struct fm_entry) == 16); uint32_t n_modifiers = m_sWaylandConnection.dma.formatTableSize / sizeof(struct fm_entry); fm_entry* fm_entry = (struct fm_entry*)m_sWaylandConnection.dma.formatTable; uint16_t* idx; for (idx = (uint16_t*)indices->data; (const char*)idx < (const char*)indices->data + indices->size; idx++) { if (*idx >= n_modifiers) continue; m_vDMABUFMods.push_back({(fm_entry + *idx)->format, (fm_entry + *idx)->modifier}); } }); m_sWaylandConnection.linuxDmabufFeedback->setTrancheDone([this](CCZwpLinuxDmabufFeedbackV1* r) { Debug::log(TRACE, "[core] dmabufFeedbackTrancheDone"); m_sWaylandConnection.dma.deviceUsed = false; }); } else if (INTERFACE == wl_shm_interface.name) m_sWaylandConnection.shm = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &wl_shm_interface, version)); else if (INTERFACE == zwlr_foreign_toplevel_manager_v1_interface.name) { m_sHelpers.toplevel = std::make_unique(name, version); // remove when another fix is found for https://github.com/hyprwm/xdg-desktop-portal-hyprland/issues/147 if (!std::any_cast(m_sConfig.config->getConfigValue("general:toplevel_dynamic_bind"))) m_sHelpers.toplevel->activate(); } } void CPortalManager::onGlobalRemoved(uint32_t name) { std::erase_if(m_vOutputs, [&](const auto& other) { return other->id == name; }); } void CPortalManager::init() { m_iPID = getpid(); try { m_pConnection = sdbus::createSessionBusConnection(sdbus::ServiceName{"org.freedesktop.impl.portal.desktop.hyprland"}); } catch (std::exception& e) { Debug::log(CRIT, "Couldn't create the dbus connection ({})", e.what()); exit(1); } if (!m_pConnection) { Debug::log(CRIT, "Couldn't connect to dbus"); exit(1); } // init wayland connection m_sWaylandConnection.display = wl_display_connect(nullptr); if (!m_sWaylandConnection.display) { Debug::log(CRIT, "Couldn't connect to a wayland compositor"); exit(1); } if (const auto ENV = getenv("XDG_CURRENT_DESKTOP"); ENV) { Debug::log(LOG, "XDG_CURRENT_DESKTOP set to {}", ENV); if (std::string(ENV) != "Hyprland") Debug::log(WARN, "Not running on hyprland, some features might be unavailable"); } else { Debug::log(WARN, "XDG_CURRENT_DESKTOP unset, running on an unknown desktop"); } m_sWaylandConnection.registry = makeShared((wl_proxy*)wl_display_get_registry(m_sWaylandConnection.display)); m_sWaylandConnection.registry->setGlobal([this](CCWlRegistry* r, uint32_t name, const char* iface, uint32_t ver) { onGlobal(name, iface, ver); }); m_sWaylandConnection.registry->setGlobalRemove([this](CCWlRegistry* r, uint32_t name) { onGlobalRemoved(name); }); pw_init(nullptr, nullptr); m_sPipewire.loop = pw_loop_new(nullptr); if (!m_sPipewire.loop) Debug::log(ERR, "Pipewire: refused to create a loop. Screensharing will not work."); Debug::log(LOG, "Gathering exported interfaces"); wl_display_roundtrip(m_sWaylandConnection.display); if (!m_sPortals.screencopy) Debug::log(WARN, "Screencopy not started: compositor doesn't support zwlr_screencopy_v1 or pw refused a loop"); else if (m_sWaylandConnection.hyprlandToplevelMgr) m_sPortals.screencopy->appendToplevelExport(m_sWaylandConnection.hyprlandToplevelMgr); if (!inShellPath("grim")) Debug::log(WARN, "grim not found. Screenshots will not work."); else { m_sPortals.screenshot = std::make_unique(); if (!inShellPath("slurp")) Debug::log(WARN, "slurp not found. You won't be able to select a region when screenshotting."); if (!inShellPath("slurp") && !inShellPath("hyprpicker")) Debug::log(WARN, "Neither slurp nor hyprpicker found. You won't be able to pick colors."); else if (!inShellPath("hyprpicker")) Debug::log(INFO, "hyprpicker not found. We suggest to use hyprpicker for color picking to be less meh."); } wl_display_roundtrip(m_sWaylandConnection.display); startEventLoop(); } void CPortalManager::startEventLoop() { pollfd pollfds[] = { { .fd = m_pConnection->getEventLoopPollData().fd, .events = POLLIN, }, { .fd = wl_display_get_fd(m_sWaylandConnection.display), .events = POLLIN, }, { .fd = pw_loop_get_fd(m_sPipewire.loop), .events = POLLIN, }, }; std::thread pollThr([this, &pollfds]() { while (1) { int ret = poll(pollfds, 3, 5000 /* 5 seconds, reasonable. It's because we might need to terminate */); if (ret < 0) { Debug::log(CRIT, "[core] Polling fds failed with {}", strerror(errno)); g_pPortalManager->terminate(); } for (size_t i = 0; i < 3; ++i) { if (pollfds[i].revents & POLLHUP) { Debug::log(CRIT, "[core] Disconnected from pollfd id {}", i); g_pPortalManager->terminate(); } } if (m_bTerminate) break; if (ret != 0) { Debug::log(TRACE, "[core] got poll event"); std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); m_sEventLoopInternals.shouldProcess = true; m_sEventLoopInternals.loopSignal.notify_all(); } } }); m_sTimersThread.thread = std::make_unique([this] { while (1) { std::unique_lock lk(m_sTimersThread.loopMutex); // find nearest timer ms m_mEventLock.lock(); float nearest = 60000; /* reasonable timeout */ for (auto& t : m_sTimersThread.timers) { float until = t->duration() - t->passedMs(); if (until < nearest) nearest = until; } m_mEventLock.unlock(); m_sTimersThread.loopSignal.wait_for(lk, std::chrono::milliseconds((int)nearest), [this] { return m_sTimersThread.shouldProcess; }); m_sTimersThread.shouldProcess = false; if (m_bTerminate) break; // awakened. Check if any timers passed m_mEventLock.lock(); bool notify = false; for (auto& t : m_sTimersThread.timers) { if (t->passed()) { Debug::log(TRACE, "[core] got timer event"); notify = true; break; } } m_mEventLock.unlock(); if (notify) { std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); m_sEventLoopInternals.shouldProcess = true; m_sEventLoopInternals.loopSignal.notify_all(); } } }); while (1) { // dbus events // wait for being awakened std::unique_lock lk(m_sEventLoopInternals.loopMutex); if (m_sEventLoopInternals.shouldProcess == false) // avoid a lock if a thread managed to request something already since we .unlock()ed m_sEventLoopInternals.loopSignal.wait_for(lk, std::chrono::seconds(5), [this] { return m_sEventLoopInternals.shouldProcess == true; }); // wait for events std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); if (m_bTerminate) break; m_sEventLoopInternals.shouldProcess = false; m_mEventLock.lock(); if (pollfds[0].revents & POLLIN /* dbus */) { while (m_pConnection->processPendingEvent()) { ; } } if (pollfds[1].revents & POLLIN /* wl */) { wl_display_flush(m_sWaylandConnection.display); if (wl_display_prepare_read(m_sWaylandConnection.display) == 0) { wl_display_read_events(m_sWaylandConnection.display); wl_display_dispatch_pending(m_sWaylandConnection.display); } else { wl_display_dispatch(m_sWaylandConnection.display); } } if (pollfds[2].revents & POLLIN /* pw */) { while (pw_loop_iterate(m_sPipewire.loop, 0) != 0) { ; } } std::vector toRemove; for (auto& t : m_sTimersThread.timers) { if (t->passed()) { t->m_fnCallback(); toRemove.emplace_back(t.get()); Debug::log(TRACE, "[core] calling timer {}", (void*)t.get()); } } int ret = 0; do { ret = wl_display_dispatch_pending(m_sWaylandConnection.display); wl_display_flush(m_sWaylandConnection.display); } while (ret > 0); if (!toRemove.empty()) std::erase_if(m_sTimersThread.timers, [&](const auto& t) { return std::find_if(toRemove.begin(), toRemove.end(), [&](const auto& other) { return other == t.get(); }) != toRemove.end(); }); m_mEventLock.unlock(); } Debug::log(ERR, "[core] Terminated"); m_sPortals.globalShortcuts.reset(); m_sPortals.screencopy.reset(); m_sPortals.screenshot.reset(); m_sHelpers.toplevel.reset(); m_pConnection.reset(); pw_loop_destroy(m_sPipewire.loop); wl_display_disconnect(m_sWaylandConnection.display); m_sTimersThread.thread.release(); pollThr.join(); // wait for poll to exit } sdbus::IConnection* CPortalManager::getConnection() { return m_pConnection.get(); } SOutput* CPortalManager::getOutputFromName(const std::string& name) { for (auto& o : m_vOutputs) { if (o->name == name) return o.get(); } return nullptr; } static char* gbm_find_render_node(drmDevice* device) { drmDevice* devices[64]; char* render_node = NULL; int n = drmGetDevices2(0, devices, sizeof(devices) / sizeof(devices[0])); for (int i = 0; i < n; ++i) { drmDevice* dev = devices[i]; if (device && !drmDevicesEqual(device, dev)) { continue; } if (!(dev->available_nodes & (1 << DRM_NODE_RENDER))) continue; render_node = strdup(dev->nodes[DRM_NODE_RENDER]); break; } drmFreeDevices(devices, n); return render_node; } gbm_device* CPortalManager::createGBMDevice(drmDevice* dev) { char* renderNode = gbm_find_render_node(dev); if (!renderNode) { Debug::log(ERR, "[core] Couldn't find a render node"); return nullptr; } Debug::log(TRACE, "[core] createGBMDevice: render node {}", renderNode); int fd = open(renderNode, O_RDWR | O_CLOEXEC); if (fd < 0) { Debug::log(ERR, "[core] couldn't open render node"); free(renderNode); return NULL; } free(renderNode); return gbm_create_device(fd); } void CPortalManager::addTimer(const CTimer& timer) { Debug::log(TRACE, "[core] adding timer for {}ms", timer.duration()); m_sTimersThread.timers.emplace_back(std::make_unique(timer)); m_sTimersThread.shouldProcess = true; m_sTimersThread.loopSignal.notify_all(); } void CPortalManager::terminate() { m_bTerminate = true; // if we don't exit in 5s, we'll kill by force. Nuclear option. PIDs are not reused in linux until a wrap-around, // and I doubt anyone will make 4.2M PIDs within 5s. if (fork() == 0) execl("/bin/sh", "/bin/sh", "-c", std::format("sleep 5 && kill -9 {}", m_iPID).c_str(), nullptr); { m_sEventLoopInternals.shouldProcess = true; m_sEventLoopInternals.loopSignal.notify_all(); } m_sTimersThread.shouldProcess = true; m_sTimersThread.loopSignal.notify_all(); } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/core/PortalManager.hpp000066400000000000000000000071471470750335700261060ustar00rootroot00000000000000#pragma once #include #include #include #include "wayland.hpp" #include "../portals/Screencopy.hpp" #include "../portals/Screenshot.hpp" #include "../portals/GlobalShortcuts.hpp" #include "../helpers/Timer.hpp" #include "../shared/ToplevelManager.hpp" #include #include #include "hyprland-toplevel-export-v1.hpp" #include "hyprland-global-shortcuts-v1.hpp" #include "linux-dmabuf-unstable-v1.hpp" #include "wlr-foreign-toplevel-management-unstable-v1.hpp" #include "wlr-screencopy-unstable-v1.hpp" #include "../includes.hpp" #include "../dbusDefines.hpp" #include struct pw_loop; struct SOutput { SOutput(SP); std::string name; SP output = nullptr; uint32_t id = 0; float refreshRate = 60.0; wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; }; struct SDMABUFModifier { uint32_t fourcc = 0; uint64_t mod = 0; }; class CPortalManager { public: CPortalManager(); void init(); void onGlobal(uint32_t name, const char* interface, uint32_t version); void onGlobalRemoved(uint32_t name); sdbus::IConnection* getConnection(); SOutput* getOutputFromName(const std::string& name); struct { pw_loop* loop = nullptr; } m_sPipewire; struct { std::unique_ptr screencopy; std::unique_ptr screenshot; std::unique_ptr globalShortcuts; } m_sPortals; struct { std::unique_ptr toplevel; } m_sHelpers; struct { wl_display* display = nullptr; SP registry; SP hyprlandToplevelMgr; SP linuxDmabuf; SP linuxDmabufFeedback; SP shm; gbm_bo* gbm = nullptr; gbm_device* gbmDevice = nullptr; struct { void* formatTable = nullptr; size_t formatTableSize = 0; bool deviceUsed = false; } dma; } m_sWaylandConnection; struct { std::unique_ptr config; } m_sConfig; std::vector m_vDMABUFMods; void addTimer(const CTimer& timer); gbm_device* createGBMDevice(drmDevice* dev); // terminate after the event loop has been created. Before we can exit() void terminate(); private: void startEventLoop(); bool m_bTerminate = false; pid_t m_iPID = 0; struct { std::condition_variable loopSignal; std::mutex loopMutex; std::atomic shouldProcess = false; std::mutex loopRequestMutex; } m_sEventLoopInternals; struct { std::condition_variable loopSignal; std::mutex loopMutex; bool shouldProcess = false; std::vector> timers; std::unique_ptr thread; } m_sTimersThread; std::unique_ptr m_pConnection; std::vector> m_vOutputs; std::mutex m_mEventLock; }; inline std::unique_ptr g_pPortalManager;hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/dbusDefines.hpp000066400000000000000000000002051470750335700246410ustar00rootroot00000000000000#pragma once #include typedef std::tuple> dbUasv;hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/000077500000000000000000000000001470750335700233425ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/Log.cpp000066400000000000000000000000231470750335700245620ustar00rootroot00000000000000#include "Log.hpp" hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/Log.hpp000066400000000000000000000043061470750335700245770ustar00rootroot00000000000000#pragma once #include #include #include enum eLogLevel { TRACE = 0, INFO, LOG, WARN, ERR, CRIT }; #define RASSERT(expr, reason, ...) \ if (!(expr)) { \ Debug::log(CRIT, "\n==========================================================================================\nASSERTION FAILED! \n\n{}\n\nat: line {} in {}", \ std::format(reason, ##__VA_ARGS__), __LINE__, \ ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find_last_of('/') + 1); })().c_str()); \ printf("Assertion failed! See the log in /tmp/hypr/hyprland.log for more info."); \ abort(); /* so that we crash and get a coredump */ \ } #define ASSERT(expr) RASSERT(expr, "?") namespace Debug { inline bool quiet = false; inline bool verbose = false; template void log(eLogLevel level, const std::string& fmt, Args&&... args) { if (!verbose && level == TRACE) return; if (quiet) return; std::cout << '['; switch (level) { case TRACE: std::cout << "TRACE"; break; case INFO: std::cout << "INFO"; break; case LOG: std::cout << "LOG"; break; case WARN: std::cout << "WARN"; break; case ERR: std::cout << "ERR"; break; case CRIT: std::cout << "CRITICAL"; break; } std::cout << "] "; std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n"; } };hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/MiscFunctions.cpp000066400000000000000000000036701470750335700266400ustar00rootroot00000000000000#include "MiscFunctions.hpp" #include "../helpers/Log.hpp" #include #include #include #include #include #include std::string execAndGet(const char* cmd) { Debug::log(LOG, "execAndGet: {}", cmd); std::array buffer; std::string result; const std::unique_ptr pipe(popen(cmd, "r"), pclose); if (!pipe) { Debug::log(ERR, "execAndGet: failed in pipe"); return ""; } while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { result += buffer.data(); } return result; } void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message) { const std::string CMD = std::format("hyprctl notify {} {} {} \"{}\"", icon, timeMs, color, message); Debug::log(LOG, "addHyprlandNotification: {}", CMD); if (fork() == 0) execl("/bin/sh", "/bin/sh", "-c", CMD.c_str(), nullptr); } bool inShellPath(const std::string& exec) { if (exec.starts_with("/") || exec.starts_with("./") || exec.starts_with("../")) return std::filesystem::exists(exec); // we are relative to our PATH const char* path = std::getenv("PATH"); if (!path) return false; // collect paths std::string pathString = path; std::vector paths; uint32_t nextBegin = 0; for (uint32_t i = 0; i < pathString.size(); i++) { if (path[i] == ':') { paths.push_back(pathString.substr(nextBegin, i - nextBegin)); nextBegin = i + 1; } } if (nextBegin < pathString.size()) paths.push_back(pathString.substr(nextBegin, pathString.size() - nextBegin)); return std::ranges::any_of(paths, [&exec](std::string& path) { return access((path + "/" + exec).c_str(), X_OK) == 0; }); } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/MiscFunctions.hpp000066400000000000000000000005621470750335700266420ustar00rootroot00000000000000#pragma once #include #include std::string execAndGet(const char* cmd); void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message); bool inShellPath(const std::string& exec); void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode);hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/Timer.cpp000066400000000000000000000010731470750335700251270ustar00rootroot00000000000000#include "Timer.hpp" CTimer::CTimer(float ms, std::function callback) { m_fDuration = ms; m_tStart = std::chrono::high_resolution_clock::now(); m_fnCallback = callback; } bool CTimer::passed() const { return std::chrono::high_resolution_clock::now() > (m_tStart + std::chrono::milliseconds((uint64_t)m_fDuration)); } float CTimer::passedMs() const { return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_tStart).count(); } float CTimer::duration() const { return m_fDuration; }hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/helpers/Timer.hpp000066400000000000000000000006751470750335700251430ustar00rootroot00000000000000#pragma once #include #include class CTimer { public: CTimer(float ms, std::function callback); bool passed() const; float passedMs() const; float duration() const; std::function m_fnCallback; private: std::chrono::high_resolution_clock::time_point m_tStart; float m_fDuration; };hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/includes.hpp000066400000000000000000000002121470750335700242120ustar00rootroot00000000000000#pragma once #include using namespace Hyprutils::Memory; #define SP CSharedPointer #define WP CWeakPointerhyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/main.cpp000066400000000000000000000023561470750335700233360ustar00rootroot00000000000000#include #include "helpers/Log.hpp" #include "core/PortalManager.hpp" void printHelp() { std::cout << R"#(┃ xdg-desktop-portal-hyprland ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┃ -v (--verbose) → enable trace logging ┃ -q (--quiet) → disable logging ┃ -h (--help) → print this menu ┃ -V (--version) → print xdph's version )#"; } int main(int argc, char** argv, char** envp) { g_pPortalManager = std::make_unique(); for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--verbose" || arg == "-v") Debug::verbose = true; else if (arg == "--quiet" || arg == "-q") Debug::quiet = true; else if (arg == "--help" || arg == "-h") { printHelp(); return 0; } else if (arg == "--version" || arg == "-V") { std::cout << "xdg-desktop-portal-hyprland v" << XDPH_VERSION << "\n"; return 0; } else { printHelp(); return 1; } } Debug::log(LOG, "Initializing xdph..."); g_pPortalManager->init(); return 0; }hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/meson.build000066400000000000000000000010111470750335700240330ustar00rootroot00000000000000globber = run_command('find', '.', '-name', '*.cpp', check: true) src = globber.stdout().strip().split('\n') executable('xdg-desktop-portal-hyprland', [src], dependencies: [ client_protos, dependency('gbm'), dependency('hyprlang'), dependency('hyprutils'), dependency('libdrm'), dependency('libpipewire-0.3'), dependency('sdbus-c++'), dependency('threads'), dependency('wayland-client'), ], include_directories: inc, install: true, install_dir: get_option('libexecdir') ) hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/000077500000000000000000000000001470750335700233645ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/GlobalShortcuts.cpp000066400000000000000000000206311470750335700272110ustar00rootroot00000000000000#include "GlobalShortcuts.hpp" #include "../core/PortalManager.hpp" #include "../helpers/Log.hpp" SKeybind::SKeybind(SP shortcut_) : shortcut(shortcut_) { shortcut->setPressed([this](CCHyprlandGlobalShortcutV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { g_pPortalManager->m_sPortals.globalShortcuts->onActivated(this, ((uint64_t)tv_sec_hi << 32) | (uint64_t)(tv_sec_lo)); }); shortcut->setReleased([this](CCHyprlandGlobalShortcutV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { g_pPortalManager->m_sPortals.globalShortcuts->onDeactivated(this, ((uint64_t)tv_sec_hi << 32) | (uint64_t)(tv_sec_lo)); }); } // CGlobalShortcutsPortal::SSession* CGlobalShortcutsPortal::getSession(sdbus::ObjectPath& path) { for (auto& s : m_vSessions) { if (s->sessionHandle == path) return s.get(); } return nullptr; } SKeybind* CGlobalShortcutsPortal::getShortcutById(const std::string& appID, const std::string& shortcutId) { for (auto& s : m_vSessions) { if (s->appid != appID) continue; for (auto& keybind : s->keybinds) { if (keybind->id == shortcutId) return keybind.get(); } } return nullptr; } SKeybind* CGlobalShortcutsPortal::registerShortcut(SSession* session, const DBusShortcut& shortcut) { std::string id = shortcut.get<0>(); std::unordered_map data = shortcut.get<1>(); std::string description; for (auto& [k, v] : data) { if (k == "description") description = v.get(); else Debug::log(LOG, "[globalshortcuts] unknown shortcut data type {}", k); } auto* PSHORTCUT = getShortcutById(session->appid, id); if (PSHORTCUT) Debug::log(WARN, "[globalshortcuts] shortcut {} already registered for appid {}", id, session->appid); else { PSHORTCUT = session->keybinds .emplace_back(std::make_unique( makeShared(m_sState.manager->sendRegisterShortcut(id.c_str(), session->appid.c_str(), description.c_str(), "")))) .get(); } PSHORTCUT->id = std::move(id); PSHORTCUT->description = std::move(description); PSHORTCUT->session = session; return PSHORTCUT; } dbUasv CGlobalShortcutsPortal::onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { Debug::log(LOG, "[globalshortcuts] New session:"); Debug::log(LOG, "[globalshortcuts] | {}", requestHandle.c_str()); Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); Debug::log(LOG, "[globalshortcuts] | appid: {}", appID); const auto PSESSION = m_vSessions.emplace_back(std::make_unique(appID, requestHandle, sessionHandle)).get(); // create objects PSESSION->session = createDBusSession(sessionHandle); PSESSION->session->onDestroy = [PSESSION]() { PSESSION->session.release(); }; PSESSION->request = createDBusRequest(requestHandle); PSESSION->request->onDestroy = [PSESSION]() { PSESSION->request.release(); }; for (auto& [k, v] : opts) { if (k == "shortcuts") { PSESSION->registered = true; std::vector shortcuts = v.get>(); for (auto& s : shortcuts) { registerShortcut(PSESSION, s); } Debug::log(LOG, "[globalshortcuts] registered {} shortcuts", shortcuts.size()); } } return {0, {}}; } dbUasv CGlobalShortcutsPortal::onBindShortcuts(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::vector shortcuts, std::string appID, std::unordered_map opts) { Debug::log(LOG, "[globalshortcuts] Bind keys:"); Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); const auto PSESSION = getSession(sessionHandle); if (!PSESSION) { Debug::log(ERR, "[globalshortcuts] No session?"); return {1, {}}; } std::vector shortcutsToReturn; PSESSION->registered = true; for (auto& s : shortcuts) { const auto* PSHORTCUT = registerShortcut(PSESSION, s); std::unordered_map shortcutData; shortcutData["description"] = sdbus::Variant{PSHORTCUT->description}; shortcutData["trigger_description"] = sdbus::Variant{""}; shortcutsToReturn.push_back({PSHORTCUT->id, shortcutData}); } Debug::log(LOG, "[globalshortcuts] registered {} shortcuts", shortcuts.size()); std::unordered_map data; data["shortcuts"] = sdbus::Variant{shortcutsToReturn}; return {0, data}; } dbUasv CGlobalShortcutsPortal::onListShortcuts(sdbus::ObjectPath sessionHandle, sdbus::ObjectPath requestHandle) { Debug::log(LOG, "[globalshortcuts] List keys:"); Debug::log(LOG, "[globalshortcuts] | {}", sessionHandle.c_str()); const auto PSESSION = getSession(sessionHandle); if (!PSESSION) { Debug::log(ERR, "[globalshortcuts] No session?"); return {1, {}}; } std::vector shortcuts; for (auto& s : PSESSION->keybinds) { std::unordered_map opts; opts["description"] = sdbus::Variant{s->description}; opts["trigger_description"] = sdbus::Variant{""}; shortcuts.push_back({s->id, opts}); } std::unordered_map data; data["shortcuts"] = sdbus::Variant{shortcuts}; return {0, data}; } CGlobalShortcutsPortal::CGlobalShortcutsPortal(SP mgr) { m_sState.manager = mgr; m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); m_pObject ->addVTable(sdbus::registerMethod("CreateSession") .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s, std::unordered_map m) { return onCreateSession(o1, o2, s, m); }), sdbus::registerMethod("BindShortcuts") .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::vector v1, std::string s1, std::unordered_map m2) { return onBindShortcuts(o1, o2, v1, s1, m2); }), sdbus::registerMethod("ListShortcuts").implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2) { return onListShortcuts(o1, o2); }), sdbus::registerSignal("Activated").withParameters>(), sdbus::registerSignal("Deactivated").withParameters>(), sdbus::registerSignal("ShortcutsChanged").withParameters>>()) .forInterface(INTERFACE_NAME); Debug::log(LOG, "[globalshortcuts] registered"); } void CGlobalShortcutsPortal::onActivated(SKeybind* pKeybind, uint64_t time) { const auto PSESSION = (CGlobalShortcutsPortal::SSession*)pKeybind->session; Debug::log(TRACE, "[gs] Session {} called activated on {}", PSESSION->sessionHandle.c_str(), pKeybind->id); m_pObject->emitSignal("Activated").onInterface(INTERFACE_NAME).withArguments(PSESSION->sessionHandle, pKeybind->id, time, std::unordered_map{}); } void CGlobalShortcutsPortal::onDeactivated(SKeybind* pKeybind, uint64_t time) { const auto PSESSION = (CGlobalShortcutsPortal::SSession*)pKeybind->session; Debug::log(TRACE, "[gs] Session {} called deactivated on {}", PSESSION->sessionHandle.c_str(), pKeybind->id); m_pObject->emitSignal("Deactivated").onInterface(INTERFACE_NAME).withArguments(PSESSION->sessionHandle, pKeybind->id, time, std::unordered_map{}); }hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/GlobalShortcuts.hpp000066400000000000000000000044721470750335700272230ustar00rootroot00000000000000#pragma once #include #include "hyprland-global-shortcuts-v1.hpp" #include "../shared/Session.hpp" #include "../dbusDefines.hpp" struct SKeybind { SKeybind(SP shortcut); std::string id, description, preferredTrigger; SP shortcut = nullptr; void* session = nullptr; }; class CGlobalShortcutsPortal { public: CGlobalShortcutsPortal(SP mgr); using DBusShortcut = sdbus::Struct>; dbUasv onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); dbUasv onBindShortcuts(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::vector shortcuts, std::string appID, std::unordered_map opts); dbUasv onListShortcuts(sdbus::ObjectPath sessionHandle, sdbus::ObjectPath requestHandle); void onActivated(SKeybind* pKeybind, uint64_t time); void onDeactivated(SKeybind* pKeybind, uint64_t time); struct SSession { std::string appid; sdbus::ObjectPath requestHandle, sessionHandle; std::unique_ptr request; std::unique_ptr session; bool registered = false; std::vector> keybinds; }; std::vector> m_vSessions; private: struct { SP manager; } m_sState; std::unique_ptr m_pObject; SSession* getSession(sdbus::ObjectPath& path); SKeybind* getShortcutById(const std::string& appID, const std::string& shortcutId); SKeybind* registerShortcut(SSession* session, const DBusShortcut& shortcut); const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.GlobalShortcuts"}; const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; };hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/Screencopy.cpp000066400000000000000000001626711470750335700262170ustar00rootroot00000000000000#include "Screencopy.hpp" #include "../core/PortalManager.hpp" #include "../helpers/Log.hpp" #include "../helpers/MiscFunctions.hpp" #include #include #include "linux-dmabuf-unstable-v1.hpp" #include constexpr static int MAX_RETRIES = 10; // static sdbus::Struct getFullRestoreStruct(const SSelectionData& data, uint32_t cursor) { std::unordered_map mapData; switch (data.type) { case TYPE_GEOMETRY: case TYPE_OUTPUT: mapData["output"] = sdbus::Variant{data.output}; break; case TYPE_WINDOW: mapData["windowHandle"] = sdbus::Variant{(uint64_t)data.windowHandle->resource()}; mapData["windowClass"] = sdbus::Variant{data.windowClass}; break; default: Debug::log(ERR, "[screencopy] wonk selection in token saving"); break; } mapData["timeIssued"] = sdbus::Variant{uint64_t(time(nullptr))}; mapData["token"] = sdbus::Variant{std::string("todo")}; mapData["withCursor"] = sdbus::Variant{cursor}; sdbus::Variant restoreData{mapData}; return sdbus::Struct{"hyprland", 3, restoreData}; } dbUasv CScreencopyPortal::onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { g_pPortalManager->m_sHelpers.toplevel->activate(); Debug::log(LOG, "[screencopy] New session:"); Debug::log(LOG, "[screencopy] | {}", requestHandle.c_str()); Debug::log(LOG, "[screencopy] | {}", sessionHandle.c_str()); Debug::log(LOG, "[screencopy] | appid: {}", appID); const auto PSESSION = m_vSessions.emplace_back(std::make_unique(appID, requestHandle, sessionHandle)).get(); // create objects PSESSION->session = createDBusSession(sessionHandle); PSESSION->session->onDestroy = [PSESSION, this]() { if (PSESSION->sharingData.active) { m_pPipewire->destroyStream(PSESSION); Debug::log(LOG, "[screencopy] Stream destroyed"); } PSESSION->session.release(); Debug::log(LOG, "[screencopy] Session destroyed"); // deactivate toplevel so it doesn't listen and waste battery g_pPortalManager->m_sHelpers.toplevel->deactivate(); }; PSESSION->request = createDBusRequest(requestHandle); PSESSION->request->onDestroy = [PSESSION]() { PSESSION->request.release(); }; return {0, {}}; } dbUasv CScreencopyPortal::onSelectSources(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map options) { Debug::log(LOG, "[screencopy] SelectSources:"); Debug::log(LOG, "[screencopy] | {}", requestHandle.c_str()); Debug::log(LOG, "[screencopy] | {}", sessionHandle.c_str()); Debug::log(LOG, "[screencopy] | appid: {}", appID); const auto PSESSION = getSession(sessionHandle); if (!PSESSION) { Debug::log(ERR, "[screencopy] SelectSources: no session found??"); throw sdbus::Error{sdbus::Error::Name{"NOSESSION"}, "No session found"}; return {1, {}}; } struct { bool exists = false; std::string token, output; uint64_t windowHandle; bool withCursor; uint64_t timeIssued; std::string windowClass; } restoreData; for (auto& [key, val] : options) { if (key == "cursor_mode") { PSESSION->cursorMode = val.get(); Debug::log(LOG, "[screencopy] option cursor_mode to {}", PSESSION->cursorMode); } else if (key == "restore_data") { // suv // v -> r(susbt) -> v2 // v -> a(sv) -> v3 std::string issuer; uint32_t version; auto suv = val.get>(); issuer = suv.get<0>(); version = suv.get<1>(); sdbus::Variant data = suv.get<2>(); if (issuer != "hyprland") { Debug::log(LOG, "[screencopy] Restore token from {}, ignoring", issuer); continue; } Debug::log(LOG, "[screencopy] Restore token from {} ver {}", issuer, version); if (version != 2 && version != 3) { Debug::log(LOG, "[screencopy] Restore token ver unsupported, skipping", issuer); continue; } if (version == 2) { auto susbt = data.get>(); restoreData.exists = true; restoreData.token = susbt.get<0>(); restoreData.windowHandle = susbt.get<1>(); restoreData.output = susbt.get<2>(); restoreData.withCursor = susbt.get<3>(); restoreData.timeIssued = susbt.get<4>(); Debug::log(LOG, "[screencopy] Restore token v2 {} with data: {} {} {} {}", restoreData.token, restoreData.windowHandle, restoreData.output, restoreData.withCursor, restoreData.timeIssued); } else { // ver 3 auto sv = data.get>(); restoreData.exists = true; for (auto& [tkkey, tkval] : sv) { if (tkkey == "output") restoreData.output = tkval.get(); else if (tkkey == "windowHandle") restoreData.windowHandle = tkval.get(); else if (tkkey == "windowClass") restoreData.windowClass = tkval.get(); else if (tkkey == "withCursor") restoreData.withCursor = (bool)tkval.get(); else if (tkkey == "timeIssued") restoreData.timeIssued = tkval.get(); else if (tkkey == "token") restoreData.token = tkval.get(); else Debug::log(LOG, "[screencopy] restore token v3, unknown prop {}", tkkey); } Debug::log(LOG, "[screencopy] Restore token v3 {} with data: {} {} {} {} {}", restoreData.token, restoreData.windowHandle, restoreData.windowClass, restoreData.output, restoreData.withCursor, restoreData.timeIssued); } } else if (key == "persist_mode") { PSESSION->persistMode = val.get(); Debug::log(LOG, "[screencopy] option persist_mode to {}", PSESSION->persistMode); } else { Debug::log(LOG, "[screencopy] unused option {}", key); } } // clang-format off const bool RESTOREDATAVALID = restoreData.exists && ( (!restoreData.output.empty() && g_pPortalManager->getOutputFromName(restoreData.output)) || // output exists (!restoreData.windowClass.empty() && g_pPortalManager->m_sHelpers.toplevel->handleFromClass(restoreData.windowClass)) // window exists ); // clang-format on SSelectionData SHAREDATA; if (RESTOREDATAVALID) { Debug::log(LOG, "[screencopy] restore data valid, not prompting"); const bool WINDOW = !restoreData.windowClass.empty(); const auto HANDLEMATCH = WINDOW && restoreData.windowHandle != 0 ? g_pPortalManager->m_sHelpers.toplevel->handleFromHandleFull(restoreData.windowHandle) : nullptr; SHAREDATA.output = restoreData.output; SHAREDATA.type = WINDOW ? TYPE_WINDOW : TYPE_OUTPUT; SHAREDATA.windowHandle = WINDOW ? (HANDLEMATCH ? HANDLEMATCH->handle : g_pPortalManager->m_sHelpers.toplevel->handleFromClass(restoreData.windowClass)->handle) : nullptr; SHAREDATA.windowClass = restoreData.windowClass; SHAREDATA.allowToken = true; // user allowed token before PSESSION->cursorMode = restoreData.withCursor; } else { Debug::log(LOG, "[screencopy] restore data invalid / missing, prompting"); SHAREDATA = promptForScreencopySelection(); } Debug::log(LOG, "[screencopy] SHAREDATA returned selection {}", (int)SHAREDATA.type); if (SHAREDATA.type == TYPE_WINDOW && !m_sState.toplevel) { Debug::log(ERR, "[screencopy] Requested type window for no toplevel export protocol!"); SHAREDATA.type = TYPE_INVALID; } else if (SHAREDATA.type == TYPE_OUTPUT || SHAREDATA.type == TYPE_GEOMETRY) { const auto POUTPUT = g_pPortalManager->getOutputFromName(SHAREDATA.output); if (POUTPUT) { static auto* const* PFPS = (Hyprlang::INT* const*)g_pPortalManager->m_sConfig.config->getConfigValuePtr("screencopy:max_fps")->getDataStaticPtr(); if (**PFPS <= 0) PSESSION->sharingData.framerate = POUTPUT->refreshRate; else PSESSION->sharingData.framerate = std::clamp(POUTPUT->refreshRate, 1.F, (float)**PFPS); } } PSESSION->selection = SHAREDATA; return {SHAREDATA.type == TYPE_INVALID ? 1 : 0, {}}; } dbUasv CScreencopyPortal::onStart(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::string parentWindow, std::unordered_map opts) { Debug::log(LOG, "[screencopy] Start:"); Debug::log(LOG, "[screencopy] | {}", requestHandle.c_str()); Debug::log(LOG, "[screencopy] | {}", sessionHandle.c_str()); Debug::log(LOG, "[screencopy] | appid: {}", appID); Debug::log(LOG, "[screencopy] | parent_window: {}", parentWindow); const auto PSESSION = getSession(sessionHandle); if (!PSESSION) { Debug::log(ERR, "[screencopy] Start: no session found??"); throw sdbus::Error{sdbus::Error::Name{"NOSESSION"}, "No session found"}; return {1, {}}; } startSharing(PSESSION); std::unordered_map options; if (PSESSION->selection.allowToken) { // give them a token :) options["restore_data"] = sdbus::Variant{getFullRestoreStruct(PSESSION->selection, PSESSION->cursorMode)}; options["persist_mode"] = sdbus::Variant{uint32_t{2}}; Debug::log(LOG, "[screencopy] Sent restore token to {}", PSESSION->sessionHandle.c_str()); } uint32_t type = 0; switch (PSESSION->selection.type) { case TYPE_OUTPUT: type = 1 << MONITOR; break; case TYPE_WINDOW: type = 1 << WINDOW; break; case TYPE_GEOMETRY: case TYPE_WORKSPACE: type = 1 << VIRTUAL; break; default: type = 0; break; } options["source_type"] = sdbus::Variant{type}; std::vector>> streams; std::unordered_map streamData; streamData["position"] = sdbus::Variant{sdbus::Struct{0, 0}}; streamData["size"] = sdbus::Variant{sdbus::Struct{PSESSION->sharingData.frameInfoSHM.w, PSESSION->sharingData.frameInfoSHM.h}}; streamData["source_type"] = sdbus::Variant{uint32_t{type}}; streams.emplace_back(sdbus::Struct>{PSESSION->sharingData.nodeID, streamData}); options["streams"] = sdbus::Variant{streams}; return {0, options}; } void CScreencopyPortal::startSharing(CScreencopyPortal::SSession* pSession) { pSession->sharingData.active = true; startFrameCopy(pSession); wl_display_dispatch(g_pPortalManager->m_sWaylandConnection.display); wl_display_roundtrip(g_pPortalManager->m_sWaylandConnection.display); if (pSession->sharingData.frameInfoDMA.fmt == DRM_FORMAT_INVALID) { Debug::log(ERR, "[screencopy] Couldn't obtain a format from dma"); // todo: blocks shm return; } m_pPipewire->createStream(pSession); while (pSession->sharingData.nodeID == SPA_ID_INVALID) { int ret = pw_loop_iterate(g_pPortalManager->m_sPipewire.loop, 0); if (ret < 0) { Debug::log(ERR, "[pipewire] pw_loop_iterate failed with {}", spa_strerror(ret)); return; } } Debug::log(LOG, "[screencopy] Sharing initialized"); g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(pSession); Debug::log(TRACE, "[sc] queued frame in {}ms", 1000.0 / pSession->sharingData.framerate); } void CScreencopyPortal::startFrameCopy(CScreencopyPortal::SSession* pSession) { pSession->startCopy(); Debug::log(TRACE, "[screencopy] frame callbacks initialized"); } void CScreencopyPortal::SSession::startCopy() { const auto POUTPUT = g_pPortalManager->getOutputFromName(selection.output); if (!sharingData.active) { Debug::log(TRACE, "[sc] startFrameCopy: not copying, inactive session"); return; } if (!POUTPUT && (selection.type == TYPE_GEOMETRY || selection.type == TYPE_OUTPUT)) { Debug::log(ERR, "[screencopy] Output {} not found??", selection.output); return; } if ((sharingData.frameCallback && (selection.type == TYPE_GEOMETRY || selection.type == TYPE_OUTPUT)) || (sharingData.windowFrameCallback && selection.type == TYPE_WINDOW)) { Debug::log(ERR, "[screencopy] tried scheduling on already scheduled cb (type {})", (int)selection.type); return; } if (selection.type == TYPE_GEOMETRY) { sharingData.frameCallback = makeShared(g_pPortalManager->m_sPortals.screencopy->m_sState.screencopy->sendCaptureOutputRegion( cursorMode, POUTPUT->output->resource(), selection.x, selection.y, selection.w, selection.h)); sharingData.transform = POUTPUT->transform; } else if (selection.type == TYPE_OUTPUT) { sharingData.frameCallback = makeShared(g_pPortalManager->m_sPortals.screencopy->m_sState.screencopy->sendCaptureOutput(cursorMode, POUTPUT->output->resource())); sharingData.transform = POUTPUT->transform; } else if (selection.type == TYPE_WINDOW) { if (!selection.windowHandle) { Debug::log(ERR, "[screencopy] selected invalid window?"); return; } sharingData.windowFrameCallback = makeShared( g_pPortalManager->m_sPortals.screencopy->m_sState.toplevel->sendCaptureToplevelWithWlrToplevelHandle(cursorMode, selection.windowHandle->resource())); sharingData.transform = WL_OUTPUT_TRANSFORM_NORMAL; } else { Debug::log(ERR, "[screencopy] Unsupported selection {}", (int)selection.type); return; } sharingData.status = FRAME_QUEUED; initCallbacks(); } void CScreencopyPortal::SSession::initCallbacks() { if (sharingData.frameCallback) { sharingData.frameCallback->setBuffer([this](CCZwlrScreencopyFrameV1* r, uint32_t format, uint32_t width, uint32_t height, uint32_t stride) { Debug::log(TRACE, "[sc] wlrOnBuffer for {}", (void*)this); sharingData.frameInfoSHM.w = width; sharingData.frameInfoSHM.h = height; sharingData.frameInfoSHM.fmt = drmFourccFromSHM((wl_shm_format)format); sharingData.frameInfoSHM.size = stride * height; sharingData.frameInfoSHM.stride = stride; // todo: done if ver < 3 }); sharingData.frameCallback->setReady([this](CCZwlrScreencopyFrameV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { Debug::log(TRACE, "[sc] wlrOnReady for {}", (void*)this); sharingData.status = FRAME_READY; sharingData.tvSec = ((((uint64_t)tv_sec_hi) << 32) + (uint64_t)tv_sec_lo); sharingData.tvNsec = tv_nsec; sharingData.tvTimestampNs = sharingData.tvSec * SPA_NSEC_PER_SEC + sharingData.tvNsec; Debug::log(TRACE, "[sc] frame timestamp sec: {} nsec: {} combined: {}ns", sharingData.tvSec, sharingData.tvNsec, sharingData.tvTimestampNs); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->enqueue(this); if (g_pPortalManager->m_sPortals.screencopy->m_pPipewire->streamFromSession(this)) g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); sharingData.frameCallback.reset(); }); sharingData.frameCallback->setFailed([this](CCZwlrScreencopyFrameV1* r) { Debug::log(TRACE, "[sc] wlrOnFailed for {}", (void*)this); sharingData.status = FRAME_FAILED; }); sharingData.frameCallback->setDamage([this](CCZwlrScreencopyFrameV1* r, uint32_t x, uint32_t y, uint32_t width, uint32_t height) { Debug::log(TRACE, "[sc] wlrOnDamage for {}", (void*)this); if (sharingData.damageCount > 3) { sharingData.damage[0] = {0, 0, sharingData.frameInfoDMA.w, sharingData.frameInfoDMA.h}; return; } sharingData.damage[sharingData.damageCount++] = {x, y, width, height}; Debug::log(TRACE, "[sc] wlr damage: {} {} {} {}", x, y, width, height); }); sharingData.frameCallback->setLinuxDmabuf([this](CCZwlrScreencopyFrameV1* r, uint32_t format, uint32_t width, uint32_t height) { Debug::log(TRACE, "[sc] wlrOnDmabuf for {}", (void*)this); sharingData.frameInfoDMA.w = width; sharingData.frameInfoDMA.h = height; sharingData.frameInfoDMA.fmt = format; }); sharingData.frameCallback->setBufferDone([this](CCZwlrScreencopyFrameV1* r) { Debug::log(TRACE, "[sc] wlrOnBufferDone for {}", (void*)this); const auto PSTREAM = g_pPortalManager->m_sPortals.screencopy->m_pPipewire->streamFromSession(this); if (!PSTREAM) { Debug::log(TRACE, "[sc] wlrOnBufferDone: no stream"); sharingData.frameCallback.reset(); sharingData.status = FRAME_NONE; return; } Debug::log(TRACE, "[sc] pw format {} size {}x{}", (int)PSTREAM->pwVideoInfo.format, PSTREAM->pwVideoInfo.size.width, PSTREAM->pwVideoInfo.size.height); Debug::log(TRACE, "[sc] wlr format {} size {}x{}", (int)sharingData.frameInfoSHM.fmt, sharingData.frameInfoSHM.w, sharingData.frameInfoSHM.h); Debug::log(TRACE, "[sc] wlr format dma {} size {}x{}", (int)sharingData.frameInfoDMA.fmt, sharingData.frameInfoDMA.w, sharingData.frameInfoDMA.h); const auto FMT = PSTREAM->isDMA ? sharingData.frameInfoDMA.fmt : sharingData.frameInfoSHM.fmt; if ((PSTREAM->pwVideoInfo.format != pwFromDrmFourcc(FMT) && PSTREAM->pwVideoInfo.format != pwStripAlpha(pwFromDrmFourcc(FMT))) || (PSTREAM->pwVideoInfo.size.width != sharingData.frameInfoDMA.w || PSTREAM->pwVideoInfo.size.height != sharingData.frameInfoDMA.h)) { Debug::log(LOG, "[sc] Incompatible formats, renegotiate stream"); sharingData.status = FRAME_RENEG; sharingData.frameCallback.reset(); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->updateStreamParam(PSTREAM); g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); sharingData.status = FRAME_NONE; return; } if (!PSTREAM->currentPWBuffer) { Debug::log(TRACE, "[sc] wlrOnBufferDone: dequeue, no current buffer"); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->dequeue(this); } if (!PSTREAM->currentPWBuffer) { sharingData.frameCallback.reset(); Debug::log(LOG, "[screencopy/pipewire] Out of buffers"); sharingData.status = FRAME_NONE; if (sharingData.copyRetries++ < MAX_RETRIES) { Debug::log(LOG, "[sc] Retrying screencopy ({}/{})", sharingData.copyRetries, MAX_RETRIES); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->updateStreamParam(PSTREAM); g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); } return; } sharingData.frameCallback->sendCopyWithDamage(PSTREAM->currentPWBuffer->wlBuffer->resource()); sharingData.copyRetries = 0; Debug::log(TRACE, "[sc] wlr frame copied"); }); } else if (sharingData.windowFrameCallback) { sharingData.windowFrameCallback->setBuffer([this](CCHyprlandToplevelExportFrameV1* r, uint32_t format, uint32_t width, uint32_t height, uint32_t stride) { Debug::log(TRACE, "[sc] hlOnBuffer for {}", (void*)this); sharingData.frameInfoSHM.w = width; sharingData.frameInfoSHM.h = height; sharingData.frameInfoSHM.fmt = drmFourccFromSHM((wl_shm_format)format); sharingData.frameInfoSHM.size = stride * height; sharingData.frameInfoSHM.stride = stride; // todo: done if ver < 3 }); sharingData.windowFrameCallback->setReady([this](CCHyprlandToplevelExportFrameV1* r, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { Debug::log(TRACE, "[sc] hlOnReady for {}", (void*)this); sharingData.status = FRAME_READY; sharingData.tvSec = ((((uint64_t)tv_sec_hi) << 32) + (uint64_t)tv_sec_lo); sharingData.tvNsec = tv_nsec; sharingData.tvTimestampNs = sharingData.tvSec * SPA_NSEC_PER_SEC + sharingData.tvNsec; Debug::log(TRACE, "[sc] frame timestamp sec: {} nsec: {} combined: {}ns", sharingData.tvSec, sharingData.tvNsec, sharingData.tvTimestampNs); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->enqueue(this); if (g_pPortalManager->m_sPortals.screencopy->m_pPipewire->streamFromSession(this)) g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); sharingData.windowFrameCallback.reset(); }); sharingData.windowFrameCallback->setFailed([this](CCHyprlandToplevelExportFrameV1* r) { Debug::log(TRACE, "[sc] hlOnFailed for {}", (void*)this); sharingData.status = FRAME_FAILED; }); sharingData.windowFrameCallback->setDamage([this](CCHyprlandToplevelExportFrameV1* r, uint32_t x, uint32_t y, uint32_t width, uint32_t height) { Debug::log(TRACE, "[sc] hlOnDamage for {}", (void*)this); if (sharingData.damageCount > 3) { sharingData.damage[0] = {0, 0, sharingData.frameInfoDMA.w, sharingData.frameInfoDMA.h}; return; } sharingData.damage[sharingData.damageCount++] = {x, y, width, height}; Debug::log(TRACE, "[sc] hl damage: {} {} {} {}", x, y, width, height); }); sharingData.windowFrameCallback->setLinuxDmabuf([this](CCHyprlandToplevelExportFrameV1* r, uint32_t format, uint32_t width, uint32_t height) { Debug::log(TRACE, "[sc] hlOnDmabuf for {}", (void*)this); sharingData.frameInfoDMA.w = width; sharingData.frameInfoDMA.h = height; sharingData.frameInfoDMA.fmt = format; }); sharingData.windowFrameCallback->setBufferDone([this](CCHyprlandToplevelExportFrameV1* r) { Debug::log(TRACE, "[sc] hlOnBufferDone for {}", (void*)this); const auto PSTREAM = g_pPortalManager->m_sPortals.screencopy->m_pPipewire->streamFromSession(this); if (!PSTREAM) { Debug::log(TRACE, "[sc] hlOnBufferDone: no stream"); sharingData.windowFrameCallback.reset(); sharingData.status = FRAME_NONE; return; } Debug::log(TRACE, "[sc] pw format {} size {}x{}", (int)PSTREAM->pwVideoInfo.format, PSTREAM->pwVideoInfo.size.width, PSTREAM->pwVideoInfo.size.height); Debug::log(TRACE, "[sc] hl format {} size {}x{}", (int)sharingData.frameInfoSHM.fmt, sharingData.frameInfoSHM.w, sharingData.frameInfoSHM.h); Debug::log(TRACE, "[sc] hl format dma {} size {}x{}", (int)sharingData.frameInfoDMA.fmt, sharingData.frameInfoDMA.w, sharingData.frameInfoDMA.h); const auto FMT = PSTREAM->isDMA ? sharingData.frameInfoDMA.fmt : sharingData.frameInfoSHM.fmt; if ((PSTREAM->pwVideoInfo.format != pwFromDrmFourcc(FMT) && PSTREAM->pwVideoInfo.format != pwStripAlpha(pwFromDrmFourcc(FMT))) || (PSTREAM->pwVideoInfo.size.width != sharingData.frameInfoDMA.w || PSTREAM->pwVideoInfo.size.height != sharingData.frameInfoDMA.h)) { Debug::log(LOG, "[sc] Incompatible formats, renegotiate stream"); sharingData.status = FRAME_RENEG; sharingData.windowFrameCallback.reset(); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->updateStreamParam(PSTREAM); g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); sharingData.status = FRAME_NONE; return; } if (!PSTREAM->currentPWBuffer) { Debug::log(TRACE, "[sc] hlOnBufferDone: dequeue, no current buffer"); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->dequeue(this); } if (!PSTREAM->currentPWBuffer) { sharingData.windowFrameCallback.reset(); Debug::log(LOG, "[screencopy/pipewire] Out of buffers"); sharingData.status = FRAME_NONE; if (sharingData.copyRetries++ < MAX_RETRIES) { Debug::log(LOG, "[sc] Retrying screencopy ({}/{})", sharingData.copyRetries, MAX_RETRIES); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->updateStreamParam(PSTREAM); g_pPortalManager->m_sPortals.screencopy->queueNextShareFrame(this); } return; } sharingData.windowFrameCallback->sendCopy(PSTREAM->currentPWBuffer->wlBuffer->resource(), false); sharingData.copyRetries = 0; Debug::log(TRACE, "[sc] hl frame copied"); }); } } void CScreencopyPortal::queueNextShareFrame(CScreencopyPortal::SSession* pSession) { const auto PSTREAM = m_pPipewire->streamFromSession(pSession); if (PSTREAM && !PSTREAM->streamState) return; // calculate frame delta and queue next frame const auto FRAMETOOKMS = std::chrono::duration_cast(std::chrono::system_clock::now() - pSession->sharingData.begunFrame).count() / 1000.0; const auto MSTILNEXTREFRESH = 1000.0 / (pSession->sharingData.framerate) - FRAMETOOKMS; pSession->sharingData.begunFrame = std::chrono::system_clock::now(); Debug::log(TRACE, "[screencopy] set fps {}, frame took {:.2f}ms, ms till next refresh {:.2f}, estimated actual fps: {:.2f}", pSession->sharingData.framerate, FRAMETOOKMS, MSTILNEXTREFRESH, std::clamp(1000.0 / FRAMETOOKMS, 1.0, (double)pSession->sharingData.framerate)); g_pPortalManager->addTimer( {std::clamp(MSTILNEXTREFRESH - 1.0 /* safezone */, 6.0, 1000.0), [pSession]() { g_pPortalManager->m_sPortals.screencopy->startFrameCopy(pSession); }}); } bool CScreencopyPortal::hasToplevelCapabilities() { return m_sState.toplevel; } CScreencopyPortal::SSession* CScreencopyPortal::getSession(sdbus::ObjectPath& path) { for (auto& s : m_vSessions) { if (s->sessionHandle == path) return s.get(); } return nullptr; } CScreencopyPortal::CScreencopyPortal(SP mgr) { m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); m_pObject ->addVTable(sdbus::registerMethod("CreateSession") .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s1, std::unordered_map m1) { return onCreateSession(o1, o2, s1, m1); }), sdbus::registerMethod("SelectSources") .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s1, std::unordered_map m1) { return onSelectSources(o1, o2, s1, m1); }), sdbus::registerMethod("Start").implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s1, std::string s2, std::unordered_map m1) { return onStart(o1, o2, s1, s2, m1); }), sdbus::registerProperty("AvailableSourceTypes").withGetter([]() { return uint32_t{VIRTUAL | MONITOR | WINDOW}; }), sdbus::registerProperty("AvailableCursorModes").withGetter([]() { return uint32_t{HIDDEN | EMBEDDED}; }), sdbus::registerProperty("version").withGetter([]() { return uint32_t{3}; })) .forInterface(INTERFACE_NAME); m_sState.screencopy = mgr; m_pPipewire = std::make_unique(); Debug::log(LOG, "[screencopy] init successful"); } void CScreencopyPortal::appendToplevelExport(SP proto) { m_sState.toplevel = proto; Debug::log(LOG, "[screencopy] Registered for toplevel export"); } bool CPipewireConnection::good() { return m_pContext && m_pCore; } CPipewireConnection::CPipewireConnection() { m_pContext = pw_context_new(g_pPortalManager->m_sPipewire.loop, nullptr, 0); if (!m_pContext) { Debug::log(ERR, "[pipewire] pw didn't allow for a context"); return; } m_pCore = pw_context_connect(m_pContext, nullptr, 0); if (!m_pCore) { Debug::log(ERR, "[pipewire] pw didn't allow for a context connection"); return; } Debug::log(LOG, "[pipewire] connected"); } void CPipewireConnection::removeSessionFrameCallbacks(CScreencopyPortal::SSession* pSession) { Debug::log(TRACE, "[pipewire] removeSessionFrameCallbacks called"); pSession->sharingData.frameCallback.reset(); pSession->sharingData.windowFrameCallback.reset(); pSession->sharingData.windowFrameCallback = nullptr; pSession->sharingData.frameCallback = nullptr; pSession->sharingData.status = FRAME_NONE; } CPipewireConnection::~CPipewireConnection() { if (m_pCore) pw_core_disconnect(m_pCore); if (m_pContext) pw_context_destroy(m_pContext); } // --------------- Pipewire Stream Handlers --------------- // static void pwStreamStateChange(void* data, pw_stream_state old, pw_stream_state state, const char* error) { const auto PSTREAM = (CPipewireConnection::SPWStream*)data; PSTREAM->pSession->sharingData.nodeID = pw_stream_get_node_id(PSTREAM->stream); Debug::log(TRACE, "[pw] pwStreamStateChange on {} from {} to {}, node id {}", (void*)PSTREAM, pw_stream_state_as_string(old), pw_stream_state_as_string(state), PSTREAM->pSession->sharingData.nodeID); switch (state) { case PW_STREAM_STATE_STREAMING: PSTREAM->streamState = true; if (PSTREAM->pSession->sharingData.status == FRAME_NONE) g_pPortalManager->m_sPortals.screencopy->startFrameCopy(PSTREAM->pSession); else { g_pPortalManager->m_sPortals.screencopy->m_pPipewire->removeSessionFrameCallbacks(PSTREAM->pSession); g_pPortalManager->m_sPortals.screencopy->startFrameCopy(PSTREAM->pSession); } break; default: { PSTREAM->streamState = false; g_pPortalManager->m_sPortals.screencopy->m_pPipewire->removeSessionFrameCallbacks(PSTREAM->pSession); break; } } if (state == PW_STREAM_STATE_UNCONNECTED) { g_pPortalManager->m_sPortals.screencopy->m_pPipewire->removeSessionFrameCallbacks(PSTREAM->pSession); g_pPortalManager->m_sPortals.screencopy->m_pPipewire->destroyStream(PSTREAM->pSession); } } static void pwStreamParamChanged(void* data, uint32_t id, const spa_pod* param) { const auto PSTREAM = (CPipewireConnection::SPWStream*)data; Debug::log(TRACE, "[pw] pwStreamParamChanged on {}", (void*)PSTREAM); if (id != SPA_PARAM_Format || !param) { Debug::log(TRACE, "[pw] invalid call in pwStreamParamChanged"); return; } spa_pod_dynamic_builder dynBuilder[3]; const spa_pod* params[4]; uint8_t params_buffer[3][1024]; spa_pod_dynamic_builder_init(&dynBuilder[0], params_buffer[0], sizeof(params_buffer[0]), 2048); spa_pod_dynamic_builder_init(&dynBuilder[1], params_buffer[1], sizeof(params_buffer[1]), 2048); spa_pod_dynamic_builder_init(&dynBuilder[2], params_buffer[2], sizeof(params_buffer[2]), 2048); spa_format_video_raw_parse(param, &PSTREAM->pwVideoInfo); Debug::log(TRACE, "[pw] Framerate: {}/{}", PSTREAM->pwVideoInfo.max_framerate.num, PSTREAM->pwVideoInfo.max_framerate.denom); PSTREAM->pSession->sharingData.framerate = PSTREAM->pwVideoInfo.max_framerate.num / PSTREAM->pwVideoInfo.max_framerate.denom; uint32_t data_type = 1 << SPA_DATA_MemFd; const struct spa_pod_prop* prop_modifier; if ((prop_modifier = spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier))) { Debug::log(TRACE, "[pipewire] pw requested dmabuf"); PSTREAM->isDMA = true; data_type = 1 << SPA_DATA_DmaBuf; RASSERT(PSTREAM->pwVideoInfo.format == pwFromDrmFourcc(PSTREAM->pSession->sharingData.frameInfoDMA.fmt), "invalid format in dma pw param change"); if ((prop_modifier->flags & SPA_POD_PROP_FLAG_DONT_FIXATE) > 0) { Debug::log(TRACE, "[pw] don't fixate"); const spa_pod* pod_modifier = &prop_modifier->value; uint32_t n_modifiers = SPA_POD_CHOICE_N_VALUES(pod_modifier) - 1; uint64_t* modifiers = (uint64_t*)SPA_POD_CHOICE_VALUES(pod_modifier); modifiers++; uint32_t flags = GBM_BO_USE_RENDERING; uint64_t modifier; uint32_t n_params; spa_pod_builder* builder[2] = {&dynBuilder[0].b, &dynBuilder[1].b}; gbm_bo* bo = gbm_bo_create_with_modifiers2(g_pPortalManager->m_sWaylandConnection.gbmDevice, PSTREAM->pSession->sharingData.frameInfoDMA.w, PSTREAM->pSession->sharingData.frameInfoDMA.h, PSTREAM->pSession->sharingData.frameInfoDMA.fmt, modifiers, n_modifiers, flags); if (bo) { modifier = gbm_bo_get_modifier(bo); gbm_bo_destroy(bo); goto fixate_format; } Debug::log(TRACE, "[pw] unable to allocate a dmabuf with modifiers. Falling back to the old api"); for (uint32_t i = 0; i < n_modifiers; i++) { switch (modifiers[i]) { case DRM_FORMAT_MOD_INVALID: flags = GBM_BO_USE_RENDERING; // ;cast->ctx->state->config->screencast_conf.force_mod_linear ? GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR : GBM_BO_USE_RENDERING; break; case DRM_FORMAT_MOD_LINEAR: flags = GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR; break; default: continue; } bo = gbm_bo_create(g_pPortalManager->m_sWaylandConnection.gbmDevice, PSTREAM->pSession->sharingData.frameInfoDMA.w, PSTREAM->pSession->sharingData.frameInfoDMA.h, PSTREAM->pSession->sharingData.frameInfoDMA.fmt, flags); if (bo) { modifier = gbm_bo_get_modifier(bo); gbm_bo_destroy(bo); goto fixate_format; } } Debug::log(ERR, "[pw] failed to alloc dma"); return; fixate_format: params[0] = fixate_format(&dynBuilder[2].b, pwFromDrmFourcc(PSTREAM->pSession->sharingData.frameInfoDMA.fmt), PSTREAM->pSession->sharingData.frameInfoDMA.w, PSTREAM->pSession->sharingData.frameInfoDMA.h, PSTREAM->pSession->sharingData.framerate, &modifier); n_params = g_pPortalManager->m_sPortals.screencopy->m_pPipewire->buildFormatsFor(builder, ¶ms[1], PSTREAM); n_params++; pw_stream_update_params(PSTREAM->stream, params, n_params); spa_pod_dynamic_builder_clean(&dynBuilder[0]); spa_pod_dynamic_builder_clean(&dynBuilder[1]); spa_pod_dynamic_builder_clean(&dynBuilder[2]); Debug::log(TRACE, "[pw] Format fixated:"); Debug::log(TRACE, "[pw] | buffer_type {}", "DMA (No fixate)"); Debug::log(TRACE, "[pw] | format: {}", (int)PSTREAM->pwVideoInfo.format); Debug::log(TRACE, "[pw] | modifier: {}", PSTREAM->pwVideoInfo.modifier); Debug::log(TRACE, "[pw] | size: {}x{}", PSTREAM->pwVideoInfo.size.width, PSTREAM->pwVideoInfo.size.height); Debug::log(TRACE, "[pw] | framerate {}", PSTREAM->pSession->sharingData.framerate); return; } } Debug::log(TRACE, "[pw] Format renegotiated:"); Debug::log(TRACE, "[pw] | buffer_type {}", PSTREAM->isDMA ? "DMA" : "SHM"); Debug::log(TRACE, "[pw] | format: {}", (int)PSTREAM->pwVideoInfo.format); Debug::log(TRACE, "[pw] | modifier: {}", PSTREAM->pwVideoInfo.modifier); Debug::log(TRACE, "[pw] | size: {}x{}", PSTREAM->pwVideoInfo.size.width, PSTREAM->pwVideoInfo.size.height); Debug::log(TRACE, "[pw] | framerate {}", PSTREAM->pSession->sharingData.framerate); uint32_t blocks = 1; params[0] = build_buffer(&dynBuilder[0].b, blocks, PSTREAM->pSession->sharingData.frameInfoSHM.size, PSTREAM->pSession->sharingData.frameInfoSHM.stride, data_type); params[1] = (const spa_pod*)spa_pod_builder_add_object(&dynBuilder[1].b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); params[2] = (const spa_pod*)spa_pod_builder_add_object(&dynBuilder[1].b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform))); params[3] = (const spa_pod*)spa_pod_builder_add_object( &dynBuilder[2].b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * 4, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * 4)); pw_stream_update_params(PSTREAM->stream, params, 4); spa_pod_dynamic_builder_clean(&dynBuilder[0]); spa_pod_dynamic_builder_clean(&dynBuilder[1]); spa_pod_dynamic_builder_clean(&dynBuilder[2]); } static void pwStreamAddBuffer(void* data, pw_buffer* buffer) { const auto PSTREAM = (CPipewireConnection::SPWStream*)data; Debug::log(TRACE, "[pw] pwStreamAddBuffer with {} on {}", (void*)buffer, (void*)PSTREAM); spa_data* spaData = buffer->buffer->datas; spa_data_type type; if ((spaData[0].type & (1u << SPA_DATA_MemFd)) > 0) { type = SPA_DATA_MemFd; Debug::log(WARN, "[pipewire] Asked for a wl_shm buffer which is legacy."); } else if ((spaData[0].type & (1u << SPA_DATA_DmaBuf)) > 0) { type = SPA_DATA_DmaBuf; } else { Debug::log(ERR, "[pipewire] wrong format in addbuffer"); return; } const auto PBUFFER = PSTREAM->buffers.emplace_back(g_pPortalManager->m_sPortals.screencopy->m_pPipewire->createBuffer(PSTREAM, type == SPA_DATA_DmaBuf)).get(); PBUFFER->pwBuffer = buffer; buffer->user_data = PBUFFER; Debug::log(TRACE, "[pw] buffer datas {}", buffer->buffer->n_datas); for (uint32_t plane = 0; plane < buffer->buffer->n_datas; plane++) { spaData[plane].type = type; spaData[plane].maxsize = PBUFFER->size[plane]; spaData[plane].mapoffset = 0; spaData[plane].chunk->size = PBUFFER->size[plane]; spaData[plane].chunk->stride = PBUFFER->stride[plane]; spaData[plane].chunk->offset = PBUFFER->offset[plane]; spaData[plane].flags = 0; spaData[plane].fd = PBUFFER->fd[plane]; spaData[plane].data = NULL; // clients have implemented to check chunk->size if the buffer is valid instead // of using the flags. Until they are patched we should use some arbitrary value. if (PBUFFER->isDMABUF && spaData[plane].chunk->size == 0) { spaData[plane].chunk->size = 9; // This was choosen by a fair d20. } } } static void pwStreamRemoveBuffer(void* data, pw_buffer* buffer) { const auto PSTREAM = (CPipewireConnection::SPWStream*)data; const auto PBUFFER = (SBuffer*)buffer->user_data; Debug::log(TRACE, "[pw] pwStreamRemoveBuffer with {} on {}", (void*)buffer, (void*)PSTREAM); if (!PBUFFER) return; if (PSTREAM->currentPWBuffer == PBUFFER) PSTREAM->currentPWBuffer = nullptr; if (PBUFFER->isDMABUF) gbm_bo_destroy(PBUFFER->bo); PBUFFER->wlBuffer.reset(); for (int plane = 0; plane < PBUFFER->planeCount; plane++) { close(PBUFFER->fd[plane]); } for (uint32_t plane = 0; plane < buffer->buffer->n_datas; plane++) { buffer->buffer->datas[plane].fd = -1; } std::erase_if(PSTREAM->buffers, [&](const auto& other) { return other.get() == PBUFFER; }); buffer->user_data = nullptr; } static const pw_stream_events pwStreamEvents = { .version = PW_VERSION_STREAM_EVENTS, .state_changed = pwStreamStateChange, .param_changed = pwStreamParamChanged, .add_buffer = pwStreamAddBuffer, .remove_buffer = pwStreamRemoveBuffer, }; // ------------------------------------------------------- // void CPipewireConnection::createStream(CScreencopyPortal::SSession* pSession) { const auto PSTREAM = m_vStreams.emplace_back(std::make_unique(pSession)).get(); pw_loop_enter(g_pPortalManager->m_sPipewire.loop); uint8_t buffer[2][1024]; spa_pod_dynamic_builder dynBuilder[2]; spa_pod_dynamic_builder_init(&dynBuilder[0], buffer[0], sizeof(buffer[0]), 2048); spa_pod_dynamic_builder_init(&dynBuilder[1], buffer[1], sizeof(buffer[1]), 2048); const std::string NAME = getRandName("xdph-streaming-"); PSTREAM->stream = pw_stream_new(m_pCore, NAME.c_str(), pw_properties_new(PW_KEY_MEDIA_CLASS, "Video/Source", nullptr)); Debug::log(TRACE, "[pw] New stream name {}", NAME); if (!PSTREAM->stream) { Debug::log(ERR, "[pipewire] refused to create stream"); g_pPortalManager->terminate(); return; } spa_pod_builder* builder[2] = {&dynBuilder[0].b, &dynBuilder[1].b}; const spa_pod* params[2]; const auto PARAMCOUNT = buildFormatsFor(builder, params, PSTREAM); spa_pod_dynamic_builder_clean(&dynBuilder[0]); spa_pod_dynamic_builder_clean(&dynBuilder[1]); pw_stream_add_listener(PSTREAM->stream, &PSTREAM->streamListener, &pwStreamEvents, PSTREAM); pw_stream_connect(PSTREAM->stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, (pw_stream_flags)(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS), params, PARAMCOUNT); pSession->sharingData.nodeID = pw_stream_get_node_id(PSTREAM->stream); Debug::log(TRACE, "[pw] Stream got nodeid {}", pSession->sharingData.nodeID); } void CPipewireConnection::destroyStream(CScreencopyPortal::SSession* pSession) { // Disconnecting the stream can cause reentrance to this function. if (pSession->sharingData.active == false) return; pSession->sharingData.active = false; const auto PSTREAM = streamFromSession(pSession); if (!PSTREAM || !PSTREAM->stream) return; if (!PSTREAM->buffers.empty()) { std::vector bufs; for (auto& b : PSTREAM->buffers) { bufs.push_back(b.get()); } for (auto& b : bufs) { pwStreamRemoveBuffer(PSTREAM, b->pwBuffer); } } pw_stream_flush(PSTREAM->stream, false); pw_stream_disconnect(PSTREAM->stream); pw_stream_destroy(PSTREAM->stream); std::erase_if(m_vStreams, [&](const auto& other) { return other.get() == PSTREAM; }); } static bool wlr_query_dmabuf_modifiers(uint32_t drm_format, uint32_t num_modifiers, uint64_t* modifiers, uint32_t* max_modifiers) { if (g_pPortalManager->m_vDMABUFMods.empty()) return false; if (num_modifiers == 0) { *max_modifiers = 0; for (auto& mod : g_pPortalManager->m_vDMABUFMods) { if (mod.fourcc == drm_format && (mod.mod == DRM_FORMAT_MOD_INVALID || gbm_device_get_format_modifier_plane_count(g_pPortalManager->m_sWaylandConnection.gbmDevice, mod.fourcc, mod.mod) > 0)) (*max_modifiers)++; } return true; } size_t i = 0; for (const auto& mod : g_pPortalManager->m_vDMABUFMods) { if (i >= num_modifiers) break; if (mod.fourcc == drm_format && (mod.mod == DRM_FORMAT_MOD_INVALID || gbm_device_get_format_modifier_plane_count(g_pPortalManager->m_sWaylandConnection.gbmDevice, mod.fourcc, mod.mod) > 0)) { modifiers[i] = mod.mod; ++i; } } *max_modifiers = num_modifiers; return true; } static bool build_modifierlist(CPipewireConnection::SPWStream* stream, uint32_t drm_format, uint64_t** modifiers, uint32_t* modifier_count) { if (!wlr_query_dmabuf_modifiers(drm_format, 0, nullptr, modifier_count)) { *modifiers = NULL; *modifier_count = 0; return false; } if (*modifier_count == 0) { Debug::log(ERR, "[pw] build_modifierlist: no mods"); *modifiers = NULL; return true; } *modifiers = (uint64_t*)calloc(*modifier_count, sizeof(uint64_t)); bool ret = wlr_query_dmabuf_modifiers(drm_format, *modifier_count, *modifiers, modifier_count); Debug::log(TRACE, "[pw] build_modifierlist: count {}", *modifier_count); return ret; } uint32_t CPipewireConnection::buildFormatsFor(spa_pod_builder* b[2], const spa_pod* params[2], CPipewireConnection::SPWStream* stream) { uint32_t paramCount = 0; uint32_t modCount = 0; uint64_t* modifiers = nullptr; if (build_modifierlist(stream, stream->pSession->sharingData.frameInfoDMA.fmt, &modifiers, &modCount) && modCount > 0) { Debug::log(LOG, "[pw] Building modifiers for dma"); paramCount = 2; params[0] = build_format(b[0], pwFromDrmFourcc(stream->pSession->sharingData.frameInfoDMA.fmt), stream->pSession->sharingData.frameInfoDMA.w, stream->pSession->sharingData.frameInfoDMA.h, stream->pSession->sharingData.framerate, modifiers, modCount); assert(params[0] != NULL); params[1] = build_format(b[1], pwFromDrmFourcc(stream->pSession->sharingData.frameInfoSHM.fmt), stream->pSession->sharingData.frameInfoSHM.w, stream->pSession->sharingData.frameInfoSHM.h, stream->pSession->sharingData.framerate, NULL, 0); assert(params[1] != NULL); } else { Debug::log(LOG, "[pw] Building modifiers for shm"); paramCount = 1; params[0] = build_format(b[0], pwFromDrmFourcc(stream->pSession->sharingData.frameInfoSHM.fmt), stream->pSession->sharingData.frameInfoSHM.w, stream->pSession->sharingData.frameInfoSHM.h, stream->pSession->sharingData.framerate, NULL, 0); } if (modifiers) free(modifiers); return paramCount; } bool CPipewireConnection::buildModListFor(CPipewireConnection::SPWStream* stream, uint32_t drmFmt, uint64_t** mods, uint32_t* modCount) { return true; } CPipewireConnection::SPWStream* CPipewireConnection::streamFromSession(CScreencopyPortal::SSession* pSession) { for (auto& s : m_vStreams) { if (s->pSession == pSession) return s.get(); } return nullptr; } void CPipewireConnection::enqueue(CScreencopyPortal::SSession* pSession) { const auto PSTREAM = streamFromSession(pSession); if (!PSTREAM) { Debug::log(ERR, "[pw] Attempted enqueue on invalid session??"); return; } Debug::log(TRACE, "[pw] enqueue on {}", (void*)PSTREAM); if (!PSTREAM->currentPWBuffer) { Debug::log(ERR, "[pipewire] no buffer in enqueue"); return; } spa_buffer* spaBuf = PSTREAM->currentPWBuffer->pwBuffer->buffer; const bool CORRUPT = PSTREAM->pSession->sharingData.status != FRAME_READY; if (CORRUPT) Debug::log(TRACE, "[pw] buffer corrupt"); Debug::log(TRACE, "[pw] Enqueue data:"); spa_meta_header* header = (spa_meta_header*)spa_buffer_find_meta_data(spaBuf, SPA_META_Header, sizeof(*header)); if (header) { header->pts = PSTREAM->pSession->sharingData.tvTimestampNs; header->flags = CORRUPT ? SPA_META_HEADER_FLAG_CORRUPTED : 0; header->seq = PSTREAM->seq++; header->dts_offset = 0; Debug::log(TRACE, "[pw] | seq {}", header->seq); Debug::log(TRACE, "[pw] | pts {}", header->pts); } spa_meta_videotransform* vt = (spa_meta_videotransform*)spa_buffer_find_meta_data(spaBuf, SPA_META_VideoTransform, sizeof(*vt)); if (vt) { vt->transform = pSession->sharingData.transform; Debug::log(TRACE, "[pw] | meta transform {}", vt->transform); } spa_meta* damage = spa_buffer_find_meta(spaBuf, SPA_META_VideoDamage); if (damage) { Debug::log(TRACE, "[pw] | meta has damage"); spa_region* damageRegion = (spa_region*)spa_meta_first(damage); uint32_t damageCounter = 0; do { if (damageCounter >= pSession->sharingData.damageCount) { *damageRegion = SPA_REGION(0, 0, 0, 0); Debug::log(TRACE, "[pw] | end damage @ {}: {} {} {} {}", damageCounter, damageRegion->position.x, damageRegion->position.y, damageRegion->size.width, damageRegion->size.height); break; } *damageRegion = SPA_REGION(pSession->sharingData.damage[damageCounter].x, pSession->sharingData.damage[damageCounter].y, pSession->sharingData.damage[damageCounter].w, pSession->sharingData.damage[damageCounter].h); Debug::log(TRACE, "[pw] | damage @ {}: {} {} {} {}", damageCounter, damageRegion->position.x, damageRegion->position.y, damageRegion->size.width, damageRegion->size.height); damageCounter++; } while (spa_meta_check(damageRegion + 1, damage) && damageRegion++); if (damageCounter < pSession->sharingData.damageCount) { // TODO: merge damage properly *damageRegion = SPA_REGION(0, 0, pSession->sharingData.frameInfoDMA.w, pSession->sharingData.frameInfoDMA.h); Debug::log(TRACE, "[pw] | damage overflow, damaged whole"); } } spa_data* datas = spaBuf->datas; Debug::log(TRACE, "[pw] | size {}x{}", PSTREAM->pSession->sharingData.frameInfoDMA.w, PSTREAM->pSession->sharingData.frameInfoDMA.h); for (uint32_t plane = 0; plane < spaBuf->n_datas; plane++) { datas[plane].chunk->flags = CORRUPT ? SPA_CHUNK_FLAG_CORRUPTED : SPA_CHUNK_FLAG_NONE; Debug::log(TRACE, "[pw] | plane {}", plane); Debug::log(TRACE, "[pw] | fd {}", datas[plane].fd); Debug::log(TRACE, "[pw] | maxsize {}", datas[plane].maxsize); Debug::log(TRACE, "[pw] | size {}", datas[plane].chunk->size); Debug::log(TRACE, "[pw] | stride {}", datas[plane].chunk->stride); Debug::log(TRACE, "[pw] | offset {}", datas[plane].chunk->offset); Debug::log(TRACE, "[pw] | flags {}", datas[plane].chunk->flags); } Debug::log(TRACE, "[pw] --------------------------------- End enqueue"); pw_stream_queue_buffer(PSTREAM->stream, PSTREAM->currentPWBuffer->pwBuffer); PSTREAM->currentPWBuffer = nullptr; } void CPipewireConnection::dequeue(CScreencopyPortal::SSession* pSession) { const auto PSTREAM = streamFromSession(pSession); if (!PSTREAM) { Debug::log(ERR, "[pw] Attempted dequeue on invalid session??"); return; } Debug::log(TRACE, "[pw] dequeue on {}", (void*)PSTREAM); const auto PWBUF = pw_stream_dequeue_buffer(PSTREAM->stream); if (!PWBUF) { Debug::log(TRACE, "[pw] dequeue failed"); PSTREAM->currentPWBuffer = nullptr; return; } const auto PBUF = (SBuffer*)PWBUF->user_data; PSTREAM->currentPWBuffer = PBUF; } std::unique_ptr CPipewireConnection::createBuffer(CPipewireConnection::SPWStream* pStream, bool dmabuf) { std::unique_ptr pBuffer = std::make_unique(); pBuffer->isDMABUF = dmabuf; Debug::log(TRACE, "[pw] createBuffer: type {}", dmabuf ? "dma" : "shm"); if (dmabuf) { pBuffer->w = pStream->pSession->sharingData.frameInfoDMA.w; pBuffer->h = pStream->pSession->sharingData.frameInfoDMA.h; pBuffer->fmt = pStream->pSession->sharingData.frameInfoDMA.fmt; uint32_t flags = GBM_BO_USE_RENDERING; if (pStream->pwVideoInfo.modifier != DRM_FORMAT_MOD_INVALID) { uint64_t* mods = (uint64_t*)&pStream->pwVideoInfo.modifier; pBuffer->bo = gbm_bo_create_with_modifiers2(g_pPortalManager->m_sWaylandConnection.gbmDevice, pBuffer->w, pBuffer->h, pBuffer->fmt, mods, 1, flags); } else { pBuffer->bo = gbm_bo_create(g_pPortalManager->m_sWaylandConnection.gbmDevice, pBuffer->w, pBuffer->h, pBuffer->fmt, flags); } if (!pBuffer->bo) { Debug::log(ERR, "[pw] Couldn't create a drm buffer"); return nullptr; } pBuffer->planeCount = gbm_bo_get_plane_count(pBuffer->bo); auto params = makeShared(g_pPortalManager->m_sWaylandConnection.linuxDmabuf->sendCreateParams()); if (!params) { Debug::log(ERR, "[pw] zwp_linux_dmabuf_v1_create_params failed"); gbm_bo_destroy(pBuffer->bo); return nullptr; } for (size_t plane = 0; plane < (size_t)pBuffer->planeCount; plane++) { pBuffer->size[plane] = 0; pBuffer->stride[plane] = gbm_bo_get_stride_for_plane(pBuffer->bo, plane); pBuffer->offset[plane] = gbm_bo_get_offset(pBuffer->bo, plane); uint64_t mod = gbm_bo_get_modifier(pBuffer->bo); pBuffer->fd[plane] = gbm_bo_get_fd_for_plane(pBuffer->bo, plane); if (pBuffer->fd[plane] < 0) { Debug::log(ERR, "[pw] gbm_bo_get_fd_for_plane failed"); params.reset(); gbm_bo_destroy(pBuffer->bo); for (size_t plane_tmp = 0; plane_tmp < plane; plane_tmp++) { close(pBuffer->fd[plane_tmp]); } return NULL; } params->sendAdd(pBuffer->fd[plane], plane, pBuffer->offset[plane], pBuffer->stride[plane], mod >> 32, mod & 0xffffffff); } pBuffer->wlBuffer = makeShared(params->sendCreateImmed(pBuffer->w, pBuffer->h, pBuffer->fmt, /* flags */ (zwpLinuxBufferParamsV1Flags)0)); params.reset(); if (!pBuffer->wlBuffer) { Debug::log(ERR, "[pw] zwp_linux_buffer_params_v1_create_immed failed"); gbm_bo_destroy(pBuffer->bo); for (size_t plane = 0; plane < (size_t)pBuffer->planeCount; plane++) { close(pBuffer->fd[plane]); } return nullptr; } } else { pBuffer->w = pStream->pSession->sharingData.frameInfoSHM.w; pBuffer->h = pStream->pSession->sharingData.frameInfoSHM.h; pBuffer->fmt = pStream->pSession->sharingData.frameInfoSHM.fmt; pBuffer->planeCount = 1; pBuffer->size[0] = pStream->pSession->sharingData.frameInfoSHM.size; pBuffer->stride[0] = pStream->pSession->sharingData.frameInfoSHM.stride; pBuffer->offset[0] = 0; pBuffer->fd[0] = anonymous_shm_open(); if (pBuffer->fd[0] == -1) { Debug::log(ERR, "[screencopy] anonymous_shm_open failed"); return nullptr; } if (ftruncate(pBuffer->fd[0], pBuffer->size[0]) < 0) { Debug::log(ERR, "[screencopy] ftruncate failed"); return nullptr; } pBuffer->wlBuffer = import_wl_shm_buffer(pBuffer->fd[0], wlSHMFromDrmFourcc(pStream->pSession->sharingData.frameInfoSHM.fmt), pStream->pSession->sharingData.frameInfoSHM.w, pStream->pSession->sharingData.frameInfoSHM.h, pStream->pSession->sharingData.frameInfoSHM.stride); if (!pBuffer->wlBuffer) { Debug::log(ERR, "[screencopy] import_wl_shm_buffer failed"); return nullptr; } } return pBuffer; } void CPipewireConnection::updateStreamParam(SPWStream* pStream) { Debug::log(TRACE, "[pw] update stream params"); uint8_t paramsBuf[2][1024]; spa_pod_dynamic_builder dynBuilder[2]; spa_pod_dynamic_builder_init(&dynBuilder[0], paramsBuf[0], sizeof(paramsBuf[0]), 2048); spa_pod_dynamic_builder_init(&dynBuilder[1], paramsBuf[1], sizeof(paramsBuf[1]), 2048); const spa_pod* params[2]; spa_pod_builder* builder[2] = {&dynBuilder[0].b, &dynBuilder[1].b}; uint32_t n_params = buildFormatsFor(builder, params, pStream); pw_stream_update_params(pStream->stream, params, n_params); spa_pod_dynamic_builder_clean(&dynBuilder[0]); spa_pod_dynamic_builder_clean(&dynBuilder[1]); } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/Screencopy.hpp000066400000000000000000000143361470750335700262160ustar00rootroot00000000000000#pragma once #include "wlr-screencopy-unstable-v1.hpp" #include "hyprland-toplevel-export-v1.hpp" #include #include "../shared/ScreencopyShared.hpp" #include #include "../shared/Session.hpp" #include "../dbusDefines.hpp" #include enum cursorModes { HIDDEN = 1, EMBEDDED = 2, METADATA = 4, }; enum sourceTypes { MONITOR = 1, WINDOW = 2, VIRTUAL = 4, }; enum frameStatus { FRAME_NONE = 0, FRAME_QUEUED, FRAME_READY, FRAME_FAILED, FRAME_RENEG, }; struct pw_context; struct pw_core; struct pw_stream; struct pw_buffer; struct SBuffer { bool isDMABUF = false; uint32_t w = 0, h = 0, fmt = 0; int planeCount = 0; int fd[4]; uint32_t size[4], stride[4], offset[4]; gbm_bo* bo = nullptr; SP wlBuffer = nullptr; pw_buffer* pwBuffer = nullptr; }; class CPipewireConnection; class CScreencopyPortal { public: CScreencopyPortal(SP); void appendToplevelExport(SP); dbUasv onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); dbUasv onSelectSources(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); dbUasv onStart(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::string parentWindow, std::unordered_map opts); struct SSession { std::string appid; sdbus::ObjectPath requestHandle, sessionHandle; uint32_t cursorMode = HIDDEN; uint32_t persistMode = 0; std::unique_ptr request; std::unique_ptr session; SSelectionData selection; void startCopy(); void initCallbacks(); struct { bool active = false; SP frameCallback = nullptr; SP windowFrameCallback = nullptr; frameStatus status = FRAME_NONE; uint64_t tvSec = 0; uint32_t tvNsec = 0; uint64_t tvTimestampNs = 0; uint32_t nodeID = 0; uint32_t framerate = 60; wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; std::chrono::system_clock::time_point begunFrame = std::chrono::system_clock::now(); uint32_t copyRetries = 0; struct { uint32_t w = 0, h = 0, size = 0, stride = 0, fmt = 0; } frameInfoSHM; struct { uint32_t w = 0, h = 0, fmt = 0; } frameInfoDMA; struct { uint32_t x = 0, y = 0, w = 0, h = 0; } damage[4]; uint32_t damageCount = 0; } sharingData; void onCloseRequest(sdbus::MethodCall&); void onCloseSession(sdbus::MethodCall&); }; void startFrameCopy(SSession* pSession); void queueNextShareFrame(SSession* pSession); bool hasToplevelCapabilities(); std::unique_ptr m_pPipewire; private: std::unique_ptr m_pObject; std::vector> m_vSessions; SSession* getSession(sdbus::ObjectPath& path); void startSharing(SSession* pSession); struct { SP screencopy = nullptr; SP toplevel = nullptr; } m_sState; const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.ScreenCast"}; const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; friend struct SSession; }; class CPipewireConnection { public: CPipewireConnection(); ~CPipewireConnection(); bool good(); void createStream(CScreencopyPortal::SSession* pSession); void destroyStream(CScreencopyPortal::SSession* pSession); void enqueue(CScreencopyPortal::SSession* pSession); void dequeue(CScreencopyPortal::SSession* pSession); struct SPWStream { CScreencopyPortal::SSession* pSession = nullptr; pw_stream* stream = nullptr; bool streamState = false; spa_hook streamListener; SBuffer* currentPWBuffer = nullptr; spa_video_info_raw pwVideoInfo; uint32_t seq = 0; bool isDMA = false; std::vector> buffers; }; std::unique_ptr createBuffer(SPWStream* pStream, bool dmabuf); SPWStream* streamFromSession(CScreencopyPortal::SSession* pSession); void removeSessionFrameCallbacks(CScreencopyPortal::SSession* pSession); uint32_t buildFormatsFor(spa_pod_builder* b[2], const spa_pod* params[2], SPWStream* stream); void updateStreamParam(SPWStream* pStream); private: std::vector> m_vStreams; bool buildModListFor(SPWStream* stream, uint32_t drmFmt, uint64_t** mods, uint32_t* modCount); pw_context* m_pContext = nullptr; pw_core* m_pCore = nullptr; };hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/Screenshot.cpp000066400000000000000000000155431470750335700262150ustar00rootroot00000000000000#include "Screenshot.hpp" #include "../core/PortalManager.hpp" #include "../helpers/Log.hpp" #include "../helpers/MiscFunctions.hpp" #include #include std::string lastScreenshot; // static dbUasv pickHyprPicker(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { const std::string HYPRPICKER_CMD = "hyprpicker --format=rgb --no-fancy"; std::string rgbColor = execAndGet(HYPRPICKER_CMD.c_str()); if (rgbColor.size() > 12) { Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); return {1, {}}; } std::array colors{0, 0, 0}; try { for (uint8_t i = 0; i < 2; i++) { uint64_t next = rgbColor.find(' '); if (next == std::string::npos) { Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); return {1, {}}; } colors[i] = std::stoi(rgbColor.substr(0, next)); rgbColor = rgbColor.substr(next + 1, rgbColor.size() - next); } colors[2] = std::stoi(rgbColor); } catch (...) { Debug::log(ERR, "Reading RGB values from hyprpicker failed. This is likely a string to integer error."); return {1, {}}; } auto [r, g, b] = colors; std::unordered_map results; results["color"] = sdbus::Variant{sdbus::Struct(r / 255.0, g / 255.0, b / 255.0)}; return {0, results}; } static dbUasv pickSlurp(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { const std::string PICK_COLOR_CMD = "grim -g \"$(slurp -p)\" -t ppm -"; std::string ppmColor = execAndGet(PICK_COLOR_CMD.c_str()); // unify whitespace ppmColor = std::regex_replace(ppmColor, std::regex("\\s+"), std::string(" ")); // check if we got a 1x1 PPM Image if (!ppmColor.starts_with("P6 1 1 ")) { Debug::log(ERR, "grim did not return a PPM Image for us."); return {1, {}}; } // convert it to a rgb value try { std::string maxValString = ppmColor.substr(7, ppmColor.size()); maxValString = maxValString.substr(0, maxValString.find(' ')); uint32_t maxVal = std::stoi(maxValString); double r, g, b; // 1 byte per triplet if (maxVal < 256) { std::string byteString = ppmColor.substr(11, 14); r = (uint8_t)byteString[0] / (maxVal * 1.0); g = (uint8_t)byteString[1] / (maxVal * 1.0); b = (uint8_t)byteString[2] / (maxVal * 1.0); } else { // 2 byte per triplet (MSB first) std::string byteString = ppmColor.substr(11, 17); r = ((byteString[0] << 8) | byteString[1]) / (maxVal * 1.0); g = ((byteString[2] << 8) | byteString[3]) / (maxVal * 1.0); b = ((byteString[4] << 8) | byteString[5]) / (maxVal * 1.0); } std::unordered_map results; results["color"] = sdbus::Variant{sdbus::Struct(r, g, b)}; return {0, results}; } catch (...) { Debug::log(ERR, "Converting PPM to RGB failed. This is likely a string to integer error."); } return {1, {}}; } CScreenshotPortal::CScreenshotPortal() { m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); m_pObject ->addVTable( sdbus::registerMethod("Screenshot").implementedAs([this](sdbus::ObjectPath o, std::string s1, std::string s2, std::unordered_map m) { return onScreenshot(o, s1, s2, m); }), sdbus::registerMethod("PickColor").implementedAs([this](sdbus::ObjectPath o, std::string s1, std::string s2, std::unordered_map m) { return onPickColor(o, s1, s2, m); }), sdbus::registerProperty("version").withGetter([]() { return uint32_t{2}; })) .forInterface(INTERFACE_NAME); Debug::log(LOG, "[screenshot] init successful"); } dbUasv CScreenshotPortal::onScreenshot(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { Debug::log(LOG, "[screenshot] New screenshot request:"); Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); Debug::log(LOG, "[screenshot] | appid: {}", appID); bool isInteractive = options.count("interactive") && options["interactive"].get() && inShellPath("slurp"); // make screenshot const auto RUNTIME_DIR = getenv("XDG_RUNTIME_DIR"); srand(time(nullptr)); const std::string HYPR_DIR = RUNTIME_DIR ? std::string{RUNTIME_DIR} + "/hypr/" : "/tmp/hypr/"; const std::string SNAP_FILE = std::format("xdph_screenshot_{:x}.png", rand()); // rand() is good enough const std::string FILE_PATH = HYPR_DIR + SNAP_FILE; const std::string SNAP_CMD = "grim '" + FILE_PATH + "'"; const std::string SNAP_INTERACTIVE_CMD = "grim -g \"$(slurp)\" '" + FILE_PATH + "'"; std::unordered_map results; results["uri"] = sdbus::Variant{"file://" + FILE_PATH}; std::filesystem::remove(FILE_PATH); std::filesystem::create_directory(HYPR_DIR); // remove last screenshot. This could cause issues if the app hasn't read the screenshot back yet, but oh well. if (!lastScreenshot.empty()) std::filesystem::remove(lastScreenshot); lastScreenshot = FILE_PATH; if (isInteractive) execAndGet(SNAP_INTERACTIVE_CMD.c_str()); else execAndGet(SNAP_CMD.c_str()); uint32_t responseCode = std::filesystem::exists(FILE_PATH) ? 0 : 1; return {responseCode, results}; } dbUasv CScreenshotPortal::onPickColor(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options) { Debug::log(LOG, "[screenshot] New PickColor request:"); Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); Debug::log(LOG, "[screenshot] | appid: {}", appID); bool hyprPickerInstalled = inShellPath("hyprpicker"); bool slurpInstalled = inShellPath("slurp"); if (!slurpInstalled && !hyprPickerInstalled) { Debug::log(ERR, "Neither slurp nor hyprpicker found. We can't pick colors."); return {1, {}}; } // use hyprpicker if installed, slurp as fallback if (hyprPickerInstalled) return pickHyprPicker(requestHandle, appID, parentWindow, options); else return pickSlurp(requestHandle, appID, parentWindow, options); } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/portals/Screenshot.hpp000066400000000000000000000013521470750335700262130ustar00rootroot00000000000000#pragma once #include #include "../dbusDefines.hpp" class CScreenshotPortal { public: CScreenshotPortal(); dbUasv onScreenshot(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options); dbUasv onPickColor(sdbus::ObjectPath requestHandle, std::string appID, std::string parentWindow, std::unordered_map options); private: std::unique_ptr m_pObject; const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.Screenshot"}; const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; }; hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/000077500000000000000000000000001470750335700231465ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/ScreencopyShared.cpp000066400000000000000000000340551470750335700271220ustar00rootroot00000000000000#include "ScreencopyShared.hpp" #include "../helpers/MiscFunctions.hpp" #include #include "../helpers/Log.hpp" #include #include #include "../core/PortalManager.hpp" #include #include #include std::string sanitizeNameForWindowList(const std::string& name) { std::string result = name; std::replace(result.begin(), result.end(), '\'', ' '); std::replace(result.begin(), result.end(), '\"', ' '); for (size_t i = 1; i < result.size(); ++i) { if (result[i - 1] == '>' && result[i] == ']') result[i] = ' '; } return result; } std::string buildWindowList() { std::string result = ""; if (!g_pPortalManager->m_sPortals.screencopy->hasToplevelCapabilities()) return result; for (auto& e : g_pPortalManager->m_sHelpers.toplevel->m_vToplevels) { result += std::format("{}[HC>]{}[HT>]{}[HE>]", (uint32_t)(((uint64_t)e->handle->resource()) & 0xFFFFFFFF), sanitizeNameForWindowList(e->windowClass), sanitizeNameForWindowList(e->windowTitle)); } return result; } SSelectionData promptForScreencopySelection() { SSelectionData data; const char* WAYLAND_DISPLAY = getenv("WAYLAND_DISPLAY"); const char* XCURSOR_SIZE = getenv("XCURSOR_SIZE"); const char* HYPRLAND_INSTANCE_SIGNATURE = getenv("HYPRLAND_INSTANCE_SIGNATURE"); static auto* const* PALLOWTOKENBYDEFAULT = (Hyprlang::INT* const*)g_pPortalManager->m_sConfig.config->getConfigValuePtr("screencopy:allow_token_by_default")->getDataStaticPtr(); // DANGEROUS: we are sending a list of app IDs and titles via env. Make sure it's in 'singlequotes' to avoid something like $(rm -rf /) // TODO: this is dumb, use a pipe or something. std::string cmd = std::format("WAYLAND_DISPLAY='{}' QT_QPA_PLATFORM='wayland' XCURSOR_SIZE='{}' HYPRLAND_INSTANCE_SIGNATURE='{}' XDPH_WINDOW_SHARING_LIST='{}' hyprland-share-picker{} 2>&1", WAYLAND_DISPLAY ? WAYLAND_DISPLAY : "", XCURSOR_SIZE ? XCURSOR_SIZE : "24", HYPRLAND_INSTANCE_SIGNATURE ? HYPRLAND_INSTANCE_SIGNATURE : "0", buildWindowList(), (**PALLOWTOKENBYDEFAULT ? " --allow-token" : "")); const auto RETVAL = execAndGet(cmd.c_str()); if (!RETVAL.contains("[SELECTION]")) { // failed if (RETVAL.contains("qt.qpa.plugin: Could not find the Qt platform plugin")) { // prompt the user to install qt5-wayland and qt6-wayland addHyprlandNotification("3", 7000, "0", "[xdph] Could not open the picker: qt5-wayland or qt6-wayland doesn't seem to be installed."); } return data; } const auto SELECTION = RETVAL.substr(RETVAL.find("[SELECTION]") + 11); Debug::log(LOG, "[sc] Selection: {}", SELECTION); const auto FLAGS = SELECTION.substr(0, SELECTION.find_first_of('/')); const auto SEL = SELECTION.substr(SELECTION.find_first_of('/') + 1); for (auto& flag : FLAGS) { if (flag == 'r') { data.allowToken = true; } else { Debug::log(LOG, "[screencopy] unknown flag from share-picker: {}", flag); } } if (SEL.find("screen:") == 0) { data.type = TYPE_OUTPUT; data.output = SEL.substr(7); data.output.pop_back(); } else if (SEL.find("window:") == 0) { data.type = TYPE_WINDOW; uint32_t handleLo = std::stoull(SEL.substr(7)); data.windowHandle = nullptr; const auto HANDLE = g_pPortalManager->m_sHelpers.toplevel->handleFromHandleLower(handleLo); if (HANDLE) { data.windowHandle = HANDLE->handle; data.windowClass = HANDLE->windowClass; } } else if (SEL.find("region:") == 0) { std::string running = SEL; running = running.substr(7); data.type = TYPE_GEOMETRY; data.output = running.substr(0, running.find_first_of('@')); running = running.substr(running.find_first_of('@') + 1); data.x = std::stoi(running.substr(0, running.find_first_of(','))); running = running.substr(running.find_first_of(',') + 1); data.y = std::stoi(running.substr(0, running.find_first_of(','))); running = running.substr(running.find_first_of(',') + 1); data.w = std::stoi(running.substr(0, running.find_first_of(','))); running = running.substr(running.find_first_of(',') + 1); data.h = std::stoi(running); } return data; } wl_shm_format wlSHMFromDrmFourcc(uint32_t format) { switch (format) { case DRM_FORMAT_ARGB8888: return WL_SHM_FORMAT_ARGB8888; case DRM_FORMAT_XRGB8888: return WL_SHM_FORMAT_XRGB8888; case DRM_FORMAT_RGBA8888: case DRM_FORMAT_RGBX8888: case DRM_FORMAT_ABGR8888: case DRM_FORMAT_XBGR8888: case DRM_FORMAT_BGRA8888: case DRM_FORMAT_BGRX8888: case DRM_FORMAT_NV12: case DRM_FORMAT_XRGB2101010: case DRM_FORMAT_XBGR2101010: case DRM_FORMAT_RGBX1010102: case DRM_FORMAT_BGRX1010102: case DRM_FORMAT_ARGB2101010: case DRM_FORMAT_ABGR2101010: case DRM_FORMAT_RGBA1010102: case DRM_FORMAT_BGRA1010102: return (wl_shm_format)format; default: Debug::log(ERR, "[screencopy] Unknown format {}", format); abort(); } } uint32_t drmFourccFromSHM(wl_shm_format format) { switch (format) { case WL_SHM_FORMAT_ARGB8888: return DRM_FORMAT_ARGB8888; case WL_SHM_FORMAT_XRGB8888: return DRM_FORMAT_XRGB8888; case WL_SHM_FORMAT_RGBA8888: case WL_SHM_FORMAT_RGBX8888: case WL_SHM_FORMAT_ABGR8888: case WL_SHM_FORMAT_XBGR8888: case WL_SHM_FORMAT_BGRA8888: case WL_SHM_FORMAT_BGRX8888: case WL_SHM_FORMAT_NV12: case WL_SHM_FORMAT_XRGB2101010: case WL_SHM_FORMAT_XBGR2101010: case WL_SHM_FORMAT_RGBX1010102: case WL_SHM_FORMAT_BGRX1010102: case WL_SHM_FORMAT_ARGB2101010: case WL_SHM_FORMAT_ABGR2101010: case WL_SHM_FORMAT_RGBA1010102: case WL_SHM_FORMAT_BGRA1010102: case WL_SHM_FORMAT_BGR888: return (uint32_t)format; default: Debug::log(ERR, "[screencopy] Unknown format {}", (int)format); abort(); } } spa_video_format pwFromDrmFourcc(uint32_t format) { switch (format) { case DRM_FORMAT_ARGB8888: return SPA_VIDEO_FORMAT_BGRA; case DRM_FORMAT_XRGB8888: return SPA_VIDEO_FORMAT_BGRx; case DRM_FORMAT_RGBA8888: return SPA_VIDEO_FORMAT_ABGR; case DRM_FORMAT_RGBX8888: return SPA_VIDEO_FORMAT_xBGR; case DRM_FORMAT_ABGR8888: return SPA_VIDEO_FORMAT_RGBA; case DRM_FORMAT_XBGR8888: return SPA_VIDEO_FORMAT_RGBx; case DRM_FORMAT_BGRA8888: return SPA_VIDEO_FORMAT_ARGB; case DRM_FORMAT_BGRX8888: return SPA_VIDEO_FORMAT_xRGB; case DRM_FORMAT_NV12: return SPA_VIDEO_FORMAT_NV12; case DRM_FORMAT_XRGB2101010: return SPA_VIDEO_FORMAT_xRGB_210LE; case DRM_FORMAT_XBGR2101010: return SPA_VIDEO_FORMAT_xBGR_210LE; case DRM_FORMAT_RGBX1010102: return SPA_VIDEO_FORMAT_RGBx_102LE; case DRM_FORMAT_BGRX1010102: return SPA_VIDEO_FORMAT_BGRx_102LE; case DRM_FORMAT_ARGB2101010: return SPA_VIDEO_FORMAT_ARGB_210LE; case DRM_FORMAT_ABGR2101010: return SPA_VIDEO_FORMAT_ABGR_210LE; case DRM_FORMAT_RGBA1010102: return SPA_VIDEO_FORMAT_RGBA_102LE; case DRM_FORMAT_BGRA1010102: return SPA_VIDEO_FORMAT_BGRA_102LE; case DRM_FORMAT_BGR888: return SPA_VIDEO_FORMAT_BGR; default: Debug::log(ERR, "[screencopy] Unknown format {}", (int)format); abort(); } } std::string getRandName(std::string prefix) { std::srand(time(NULL)); return prefix + std::format("{}{}{}{}{}{}", (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10), (int)(std::rand() % 10)); } spa_video_format pwStripAlpha(spa_video_format format) { switch (format) { case SPA_VIDEO_FORMAT_BGRA: return SPA_VIDEO_FORMAT_BGRx; case SPA_VIDEO_FORMAT_ABGR: return SPA_VIDEO_FORMAT_xBGR; case SPA_VIDEO_FORMAT_RGBA: return SPA_VIDEO_FORMAT_RGBx; case SPA_VIDEO_FORMAT_ARGB: return SPA_VIDEO_FORMAT_xRGB; case SPA_VIDEO_FORMAT_ARGB_210LE: return SPA_VIDEO_FORMAT_xRGB_210LE; case SPA_VIDEO_FORMAT_ABGR_210LE: return SPA_VIDEO_FORMAT_xBGR_210LE; case SPA_VIDEO_FORMAT_RGBA_102LE: return SPA_VIDEO_FORMAT_RGBx_102LE; case SPA_VIDEO_FORMAT_BGRA_102LE: return SPA_VIDEO_FORMAT_BGRx_102LE; default: return SPA_VIDEO_FORMAT_UNKNOWN; } } spa_pod* build_buffer(spa_pod_builder* b, uint32_t blocks, uint32_t size, uint32_t stride, uint32_t datatype) { assert(blocks > 0); assert(datatype > 0); spa_pod_frame f[1]; spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); spa_pod_builder_add(b, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(XDPH_PWR_BUFFERS, XDPH_PWR_BUFFERS_MIN, 32), 0); spa_pod_builder_add(b, SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), 0); if (size > 0) { spa_pod_builder_add(b, SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), 0); } if (stride > 0) { spa_pod_builder_add(b, SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), 0); } spa_pod_builder_add(b, SPA_PARAM_BUFFERS_align, SPA_POD_Int(XDPH_PWR_ALIGN), 0); spa_pod_builder_add(b, SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(datatype), 0); return (spa_pod*)spa_pod_builder_pop(b, &f[0]); } spa_pod* fixate_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifier) { spa_pod_frame f[1]; spa_video_format format_without_alpha = pwStripAlpha(format); spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); /* format */ if (modifier || format_without_alpha == SPA_VIDEO_FORMAT_UNKNOWN) { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); } else { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, format_without_alpha), 0); } /* modifiers */ if (modifier) { // implicit modifier spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); spa_pod_builder_long(b, *modifier); } spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), 0); // variable framerate spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(0, 1)), 0); 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); return (spa_pod*)spa_pod_builder_pop(b, &f[0]); } 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) { spa_pod_frame f[2]; int i, c; spa_video_format format_without_alpha = pwStripAlpha(format); spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); /* format */ if (modifier_count > 0 || format_without_alpha == SPA_VIDEO_FORMAT_UNKNOWN) { // modifiers are defined only in combinations with their format // we should not announce the format without alpha spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); } else { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, format_without_alpha), 0); } /* modifiers */ if (modifier_count > 0) { // build an enumeration of modifiers spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); // modifiers from the array for (i = 0, c = 0; i < modifier_count; i++) { spa_pod_builder_long(b, modifiers[i]); if (c++ == 0) spa_pod_builder_long(b, modifiers[i]); } spa_pod_builder_pop(b, &f[1]); } spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), 0); // variable framerate spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(0, 1)), 0); 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); return (spa_pod*)spa_pod_builder_pop(b, &f[0]); } void randname(char* buf) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); long r = ts.tv_nsec; for (int i = 0; i < 6; ++i) { assert(buf[i] == 'X'); buf[i] = 'A' + (r & 15) + (r & 16) * 2; r >>= 5; } } int anonymous_shm_open() { char name[] = "/xdph-shm-XXXXXX"; int retries = 100; do { randname(name + strlen(name) - 6); --retries; // shm_open guarantees that O_CLOEXEC is set int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); if (fd >= 0) { shm_unlink(name); return fd; } } while (retries > 0 && errno == EEXIST); return -1; } SP import_wl_shm_buffer(int fd, wl_shm_format fmt, int width, int height, int stride) { int size = stride * height; if (fd < 0) return nullptr; auto pool = makeShared(g_pPortalManager->m_sWaylandConnection.shm->sendCreatePool(fd, size)); auto buf = makeShared(pool->sendCreateBuffer(0, width, height, stride, fmt)); return buf; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/ScreencopyShared.hpp000066400000000000000000000036611470750335700271260ustar00rootroot00000000000000#pragma once #include #include extern "C" { #include #include #include #include #include #include #include } #include "wayland.hpp" #include "wlr-foreign-toplevel-management-unstable-v1.hpp" #include "../includes.hpp" #define XDPH_PWR_BUFFERS 4 #define XDPH_PWR_BUFFERS_MIN 2 #define XDPH_PWR_ALIGN 16 enum eSelectionType { TYPE_INVALID = -1, TYPE_OUTPUT = 0, TYPE_WINDOW, TYPE_GEOMETRY, TYPE_WORKSPACE, }; struct zwlr_foreign_toplevel_handle_v1; struct SSelectionData { eSelectionType type = TYPE_INVALID; std::string output; SP windowHandle = nullptr; uint32_t x = 0, y = 0, w = 0, h = 0; // for TYPE_GEOMETRY bool allowToken = false; // for restoring std::string windowClass; }; struct wl_buffer; SSelectionData promptForScreencopySelection(); uint32_t drmFourccFromSHM(wl_shm_format format); spa_video_format pwFromDrmFourcc(uint32_t format); wl_shm_format wlSHMFromDrmFourcc(uint32_t format); spa_video_format pwStripAlpha(spa_video_format format); std::string getRandName(std::string prefix); 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); spa_pod* fixate_format(spa_pod_builder* b, spa_video_format format, uint32_t width, uint32_t height, uint32_t framerate, uint64_t* modifier); spa_pod* build_buffer(spa_pod_builder* b, uint32_t blocks, uint32_t size, uint32_t stride, uint32_t datatype); int anonymous_shm_open(); SP import_wl_shm_buffer(int fd, wl_shm_format fmt, int width, int height, int stride);hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/Session.cpp000066400000000000000000000032161470750335700252770ustar00rootroot00000000000000#include "Session.hpp" #include "../core/PortalManager.hpp" #include "../helpers/Log.hpp" static int onCloseRequest(SDBusRequest* req) { Debug::log(TRACE, "[internal] Close Request {}", (void*)req); if (!req) return 0; req->onDestroy(); req->object.release(); return 0; } static int onCloseSession(SDBusSession* sess) { Debug::log(TRACE, "[internal] Close Session {}", (void*)sess); if (!sess) return 0; sess->onDestroy(); sess->object.release(); return 0; } std::unique_ptr createDBusSession(sdbus::ObjectPath handle) { Debug::log(TRACE, "[internal] Create Session {}", handle.c_str()); std::unique_ptr pSession = std::make_unique(); const auto PSESSION = pSession.get(); pSession->object = sdbus::createObject(*g_pPortalManager->getConnection(), handle); pSession->object->addVTable(sdbus::registerMethod("Close").implementedAs([PSESSION]() { onCloseSession(PSESSION); })).forInterface("org.freedesktop.impl.portal.Session"); return pSession; } std::unique_ptr createDBusRequest(sdbus::ObjectPath handle) { Debug::log(TRACE, "[internal] Create Request {}", handle.c_str()); std::unique_ptr pRequest = std::make_unique(); const auto PREQUEST = pRequest.get(); pRequest->object = sdbus::createObject(*g_pPortalManager->getConnection(), handle); pRequest->object->addVTable(sdbus::registerMethod("Close").implementedAs([PREQUEST]() { onCloseRequest(PREQUEST); })).forInterface("org.freedesktop.impl.portal.Request"); return pRequest; }hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/Session.hpp000066400000000000000000000010431470750335700253000ustar00rootroot00000000000000#pragma once #include "../includes.hpp" #include struct SDBusSession { std::unique_ptr object; sdbus::ObjectPath handle; std::function onDestroy; }; struct SDBusRequest { std::unique_ptr object; sdbus::ObjectPath handle; std::function onDestroy; }; std::unique_ptr createDBusSession(sdbus::ObjectPath handle); std::unique_ptr createDBusRequest(sdbus::ObjectPath handle);hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/ToplevelManager.cpp000066400000000000000000000062131470750335700267410ustar00rootroot00000000000000#include "ToplevelManager.hpp" #include "../helpers/Log.hpp" #include "../core/PortalManager.hpp" SToplevelHandle::SToplevelHandle(SP handle_) : handle(handle_) { handle->setTitle([this](CCZwlrForeignToplevelHandleV1* r, const char* title) { if (title) windowTitle = title; Debug::log(TRACE, "[toplevel] toplevel at {} set title to {}", (void*)this, windowTitle); }); handle->setAppId([this](CCZwlrForeignToplevelHandleV1* r, const char* class_) { if (class_) windowClass = class_; Debug::log(TRACE, "[toplevel] toplevel at {} set class to {}", (void*)this, windowClass); }); handle->setClosed([this](CCZwlrForeignToplevelHandleV1* r) { Debug::log(TRACE, "[toplevel] toplevel at {} closed", (void*)this); std::erase_if(g_pPortalManager->m_sHelpers.toplevel->m_vToplevels, [&](const auto& e) { return e.get() == this; }); }); } CToplevelManager::CToplevelManager(uint32_t name, uint32_t version) { m_sWaylandConnection = {name, version}; } void CToplevelManager::activate() { m_iActivateLocks++; Debug::log(LOG, "[toplevel] (activate) locks: {}", m_iActivateLocks); if (m_pManager || m_iActivateLocks < 1) return; m_pManager = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)g_pPortalManager->m_sWaylandConnection.registry->resource(), m_sWaylandConnection.name, &zwlr_foreign_toplevel_manager_v1_interface, m_sWaylandConnection.version)); m_pManager->setToplevel([this](CCZwlrForeignToplevelManagerV1* r, wl_proxy* newHandle) { Debug::log(TRACE, "[toplevel] New toplevel at {}", (void*)newHandle); m_vToplevels.emplace_back(makeShared(makeShared(newHandle))); }); m_pManager->setFinished([this](CCZwlrForeignToplevelManagerV1* r) { m_vToplevels.clear(); }); wl_display_roundtrip(g_pPortalManager->m_sWaylandConnection.display); Debug::log(LOG, "[toplevel] Activated, bound to {:x}, toplevels: {}", (uintptr_t)m_pManager, m_vToplevels.size()); } void CToplevelManager::deactivate() { m_iActivateLocks--; Debug::log(LOG, "[toplevel] (deactivate) locks: {}", m_iActivateLocks); if (!m_pManager || m_iActivateLocks > 0) return; m_pManager.reset(); m_vToplevels.clear(); Debug::log(LOG, "[toplevel] unbound manager"); } SP CToplevelManager::handleFromClass(const std::string& windowClass) { for (auto& tl : m_vToplevels) { if (tl->windowClass == windowClass) return tl; } return nullptr; } SP CToplevelManager::handleFromHandleLower(uint32_t handle) { for (auto& tl : m_vToplevels) { if (((uint64_t)tl->handle->resource() & 0xFFFFFFFF) == handle) return tl; } return nullptr; } SP CToplevelManager::handleFromHandleFull(uint64_t handle) { for (auto& tl : m_vToplevels) { if ((uint64_t)tl->handle->resource() == handle) return tl; } return nullptr; } hyprwm-xdg-desktop-portal-hyprland-fb1ce16/src/shared/ToplevelManager.hpp000066400000000000000000000023621470750335700267470ustar00rootroot00000000000000#pragma once #include "wayland.hpp" #include "wlr-foreign-toplevel-management-unstable-v1.hpp" #include #include #include #include "../includes.hpp" class CToplevelManager; struct SToplevelHandle { SToplevelHandle(SP handle); std::string windowClass; std::string windowTitle; SP handle = nullptr; CToplevelManager* mgr = nullptr; }; class CToplevelManager { public: CToplevelManager(uint32_t name, uint32_t version); void activate(); void deactivate(); SP handleFromClass(const std::string& windowClass); SP handleFromHandleLower(uint32_t handle); SP handleFromHandleFull(uint64_t handle); std::vector> m_vToplevels; private: SP m_pManager = nullptr; int64_t m_iActivateLocks = 0; struct { uint32_t name = 0; uint32_t version = 0; } m_sWaylandConnection; friend struct SToplevelHandle; };hyprwm-xdg-desktop-portal-hyprland-fb1ce16/subprojects/000077500000000000000000000000001470750335700234545ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/subprojects/hyprland-protocols/000077500000000000000000000000001470750335700273175ustar00rootroot00000000000000hyprwm-xdg-desktop-portal-hyprland-fb1ce16/subprojects/sdbus-cpp/000077500000000000000000000000001470750335700253545ustar00rootroot00000000000000