From d9657a95cb6706860332ff47dad444a96bcc874a Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:59:47 +0000 Subject: [PATCH] hyprctl: use new hyprpaper ipc format (#12537) --------- Co-authored-by: Mihai Fufezan --- .github/actions/setup_base/action.yml | 9 ++ .gitignore | 2 + flake.lock | 27 +++++ flake.nix | 7 ++ hyprctl/CMakeLists.txt | 25 +++- hyprctl/hw-protocols/hyprpaper_core.xml | 144 +++++++++++++++++++++++ hyprctl/{ => src}/Strings.hpp | 7 +- hyprctl/src/helpers/Memory.hpp | 11 ++ hyprctl/src/hyprpaper/Hyprpaper.cpp | 148 ++++++++++++++++++++++++ hyprctl/src/hyprpaper/Hyprpaper.hpp | 8 ++ hyprctl/{ => src}/main.cpp | 14 +-- nix/default.nix | 3 + nix/overlays.nix | 1 + 13 files changed, 392 insertions(+), 14 deletions(-) create mode 100644 hyprctl/hw-protocols/hyprpaper_core.xml rename hyprctl/{ => src}/Strings.hpp (96%) create mode 100644 hyprctl/src/helpers/Memory.hpp create mode 100644 hyprctl/src/hyprpaper/Hyprpaper.cpp create mode 100644 hyprctl/src/hyprpaper/Hyprpaper.hpp rename hyprctl/{ => src}/main.cpp (98%) diff --git a/.github/actions/setup_base/action.yml b/.github/actions/setup_base/action.yml index 665d7f07d..b586566d2 100644 --- a/.github/actions/setup_base/action.yml +++ b/.github/actions/setup_base/action.yml @@ -75,6 +75,15 @@ runs: cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` cmake --install build + - name: Get hyprwire-git + shell: bash + run: | + git clone https://github.com/hyprwm/hyprwire --recursive + cd hyprwire + cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build + cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` + cmake --install build + - name: Get hyprutils-git shell: bash run: | diff --git a/.gitignore b/.gitignore index 4ced16785..669e215b3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ src/render/shaders/*.inc src/render/shaders/Shaders.hpp hyprctl/hyprctl +hyprctl/hw-protocols/*.c* +hyprctl/hw-protocols/*.h* gmon.out *.out diff --git a/flake.lock b/flake.lock index 6db66e403..95b0cecf6 100644 --- a/flake.lock +++ b/flake.lock @@ -297,6 +297,32 @@ "type": "github" } }, + "hyprwire": { + "inputs": { + "hyprutils": [ + "hyprutils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1764773840, + "narHash": "sha256-9UcCdwe7vPgEcJJ64JseBQL0ZJZoxp/2iFuvfRI+9zk=", + "owner": "hyprwm", + "repo": "hyprwire", + "rev": "3f1997d6aeced318fb141810fded2255da811293", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprwire", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1764517877, @@ -345,6 +371,7 @@ "hyprlang": "hyprlang", "hyprutils": "hyprutils", "hyprwayland-scanner": "hyprwayland-scanner", + "hyprwire": "hyprwire", "nixpkgs": "nixpkgs", "pre-commit-hooks": "pre-commit-hooks", "systems": "systems", diff --git a/flake.nix b/flake.nix index 6799144b4..49f82cdfb 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,13 @@ inputs.systems.follows = "systems"; }; + hyprwire = { + url = "github:hyprwm/hyprwire"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.systems.follows = "systems"; + inputs.hyprutils.follows = "hyprutils"; + }; + xdph = { url = "github:hyprwm/xdg-desktop-portal-hyprland"; inputs.nixpkgs.follows = "nixpkgs"; diff --git a/hyprctl/CMakeLists.txt b/hyprctl/CMakeLists.txt index db5ef6157..7071ede9f 100644 --- a/hyprctl/CMakeLists.txt +++ b/hyprctl/CMakeLists.txt @@ -5,11 +5,32 @@ project( DESCRIPTION "Control utility for Hyprland" ) -pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 re2) +pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 hyprwire re2) -add_executable(hyprctl "main.cpp") +file(GLOB_RECURSE HYPRCTL_SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "hw-protocols/*.cpp" "include/*.hpp") + +add_executable(hyprctl ${HYPRCTL_SRCFILES}) target_link_libraries(hyprctl PUBLIC PkgConfig::hyprctl_deps) +target_include_directories(hyprctl PRIVATE "hw-protocols") + +# Hyprwire + +function(hyprprotocol protoPath protoName) + set(path ${CMAKE_CURRENT_SOURCE_DIR}/${protoPath}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-spec.hpp + COMMAND hyprwire-scanner --client ${path}/${protoName}.xml + ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + target_sources(hyprctl PRIVATE hw-protocols/${protoName}-client.cpp + hw-protocols/${protoName}-client.hpp + hw-protocols/${protoName}-spec.hpp) +endfunction() + +hyprprotocol(hw-protocols hyprpaper_core) # binary install(TARGETS hyprctl) diff --git a/hyprctl/hw-protocols/hyprpaper_core.xml b/hyprctl/hw-protocols/hyprpaper_core.xml new file mode 100644 index 000000000..fa2edc0a0 --- /dev/null +++ b/hyprctl/hw-protocols/hyprpaper_core.xml @@ -0,0 +1,144 @@ + + + + BSD 3-Clause License + + Copyright (c) 2025, Hypr Development + + 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. + + + + + This is the core manager object for hyprpaper operations + + + + + Creates a wallpaper object + + + + + + + Emitted when a new monitor is added. + + + + + + + Emitted when a monitor is removed. + + + + + + + Destroys this object. Children remain alive until destroyed. + + + + + + + + + + + + + + + + + + + + + + + + This is an object describing a wallpaper + + + + + Set a file path for the wallpaper. This has to be an absolute path from the fs root. + This is required. + + + + + + + Set a fit mode for the wallpaper. This is set to cover by default. + + + + + + + Set a monitor for the wallpaper. Setting this to empty (or not setting at all) will + treat this as a wildcard fallback. + + See hyprpaper_core_manager.add_monitor and hyprpaper_core_manager.remove_monitor + for tracking monitor names. + + + + + + + Applies this object's state to the wallpaper state. Will emit .success on success, + and .failed on failure. + + This object becomes inert after .succeess or .failed, the only valid operation + is to destroy it afterwards. + + + + + + Wallpaper was applied successfully. + + + + + + Wallpaper was not applied. See the error field for more information. + + + + + + + Destroys this object. + + + + diff --git a/hyprctl/Strings.hpp b/hyprctl/src/Strings.hpp similarity index 96% rename from hyprctl/Strings.hpp rename to hyprctl/src/Strings.hpp index 67e4f992c..549d84bb6 100644 --- a/hyprctl/Strings.hpp +++ b/hyprctl/src/Strings.hpp @@ -74,11 +74,8 @@ flags: const std::string_view HYPRPAPER_HELP = R"#(usage: hyprctl [flags] hyprpaper requests: - listactive → Lists all active images - listloaded → Lists all loaded images - preload → Preloads image - unload → Unloads image. Pass 'all' as path to unload all images - wallpaper → Issue a wallpaper to call a config wallpaper dynamically + wallpaper → Issue a wallpaper to call a config wallpaper dynamically. + Arguments are [mon],[path],[fit_mode]. Fit mode is optional. flags: See 'hyprctl --help')#"; diff --git a/hyprctl/src/helpers/Memory.hpp b/hyprctl/src/helpers/Memory.hpp new file mode 100644 index 000000000..1d3a9e07c --- /dev/null +++ b/hyprctl/src/helpers/Memory.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +using namespace Hyprutils::Memory; + +#define SP CSharedPointer +#define WP CWeakPointer +#define UP CUniquePointer diff --git a/hyprctl/src/hyprpaper/Hyprpaper.cpp b/hyprctl/src/hyprpaper/Hyprpaper.cpp new file mode 100644 index 000000000..afa7f653d --- /dev/null +++ b/hyprctl/src/hyprpaper/Hyprpaper.cpp @@ -0,0 +1,148 @@ +#include "Hyprpaper.hpp" +#include "../helpers/Memory.hpp" + +#include +#include +#include + +#include + +#include +using namespace Hyprutils::String; + +using namespace std::string_literals; + +constexpr const char* SOCKET_NAME = ".hyprpaper.sock"; +static SP g_coreImpl; + +constexpr const uint32_t PROTOCOL_VERSION_SUPPORTED = 1; + +// +static hyprpaperCoreWallpaperFitMode fitFromString(const std::string_view& sv) { + if (sv == "contain") + return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_CONTAIN; + if (sv == "fit" || sv == "stretch") + return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_STRETCH; + if (sv == "tile") + return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE; + return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER; +} + +static std::expected resolvePath(const std::string_view& sv) { + std::error_code ec; + auto can = std::filesystem::canonical(sv, ec); + + if (ec) + return std::unexpected(std::format("invalid path: {}", ec.message())); + + return can; +} + +static std::expected getFullPath(const std::string_view& sv) { + if (sv.empty()) + return std::unexpected("empty path"); + + if (sv[0] == '~') { + static auto HOME = getenv("HOME"); + if (!HOME || HOME[0] == '\0') + return std::unexpected("home path but no $HOME"); + + return resolvePath(std::string{HOME} + "/"s + std::string{sv.substr(1)}); + } + + return resolvePath(sv); +} + +std::expected Hyprpaper::makeHyprpaperRequest(const std::string_view& rq) { + if (!rq.contains(' ')) + return std::unexpected("Invalid request"); + + if (!rq.starts_with("/hyprpaper ")) + return std::unexpected("Invalid request"); + + std::string_view LHS, RHS; + auto spacePos = rq.find(' ', 12); + LHS = rq.substr(11, spacePos - 11); + RHS = rq.substr(spacePos + 1); + + if (LHS != "wallpaper") + return std::unexpected("Unknown hyprpaper request"); + + CVarList2 args(std::string{RHS}, 0, ','); + + const std::string MONITOR = std::string{args[0]}; + const auto& PATH_RAW = args[1]; + const auto& FIT = args[2]; + + if (PATH_RAW.empty()) + return std::unexpected("not enough args"); + + const auto RTDIR = getenv("XDG_RUNTIME_DIR"); + + if (!RTDIR || RTDIR[0] == '\0') + return std::unexpected("can't send: no XDG_RUNTIME_DIR"); + + const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + + if (!HIS || HIS[0] == '\0') + return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)"); + + const auto PATH = getFullPath(PATH_RAW); + + if (!PATH) + return std::unexpected(std::format("bad path: {}", PATH_RAW)); + + auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME; + + auto socket = Hyprwire::IClientSocket::open(socketPath); + + if (!socket) + return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)"); + + g_coreImpl = makeShared(1); + + socket->addImplementation(g_coreImpl); + + if (!socket->waitForHandshake()) + return std::unexpected("can't send: wire handshake failed"); + + auto spec = socket->getSpec(g_coreImpl->protocol()->specName()); + + if (!spec) + return std::unexpected("can't send: hyprpaper doesn't have the spec?!"); + + auto manager = makeShared(socket->bindProtocol(g_coreImpl->protocol(), PROTOCOL_VERSION_SUPPORTED)); + + if (!manager) + return std::unexpected("wire error: couldn't create manager"); + + auto wallpaper = makeShared(manager->sendGetWallpaperObject()); + + if (!wallpaper) + return std::unexpected("wire error: couldn't create wallpaper object"); + + bool canExit = false; + std::optional err; + + wallpaper->setFailed([&canExit, &err](uint32_t code) { + canExit = true; + err = std::format("failed to set wallpaper, code {}", code); + }); + wallpaper->setSuccess([&canExit]() { canExit = true; }); + + wallpaper->sendPath(PATH->c_str()); + wallpaper->sendMonitorName(MONITOR.c_str()); + if (!FIT.empty()) + wallpaper->sendFitMode(fitFromString(FIT)); + + wallpaper->sendApply(); + + while (!canExit) { + socket->dispatchEvents(true); + } + + if (err) + return std::unexpected(*err); + + return {}; +} \ No newline at end of file diff --git a/hyprctl/src/hyprpaper/Hyprpaper.hpp b/hyprctl/src/hyprpaper/Hyprpaper.hpp new file mode 100644 index 000000000..167b0a8d5 --- /dev/null +++ b/hyprctl/src/hyprpaper/Hyprpaper.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace Hyprpaper { + std::expected makeHyprpaperRequest(const std::string_view& rq); +}; \ No newline at end of file diff --git a/hyprctl/main.cpp b/hyprctl/src/main.cpp similarity index 98% rename from hyprctl/main.cpp rename to hyprctl/src/main.cpp index e15a17f5f..7146c6350 100644 --- a/hyprctl/main.cpp +++ b/hyprctl/src/main.cpp @@ -31,6 +31,7 @@ using namespace Hyprutils::String; using namespace Hyprutils::Memory; #include "Strings.hpp" +#include "hyprpaper/Hyprpaper.hpp" std::string instanceSignature; bool quiet = false; @@ -305,10 +306,6 @@ int requestIPC(std::string_view filename, std::string_view arg) { return 0; } -int requestHyprpaper(std::string_view arg) { - return requestIPC(".hyprpaper.sock", arg); -} - int requestHyprsunset(std::string_view arg) { return requestIPC(".hyprsunset.sock", arg); } @@ -500,9 +497,12 @@ int main(int argc, char** argv) { if (fullRequest.contains("/--batch")) batchRequest(fullRequest, json); - else if (fullRequest.contains("/hyprpaper")) - exitStatus = requestHyprpaper(fullRequest); - else if (fullRequest.contains("/hyprsunset")) + else if (fullRequest.contains("/hyprpaper")) { + auto result = Hyprpaper::makeHyprpaperRequest(fullRequest); + if (!result) + log(std::format("error: {}", result.error())); + exitStatus = !result; + } else if (fullRequest.contains("/hyprsunset")) exitStatus = requestHyprsunset(fullRequest); else if (fullRequest.contains("/switchxkblayout")) exitStatus = request(fullRequest, 2); diff --git a/nix/default.nix b/nix/default.nix index 45fd273b8..38ff0bc33 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -19,6 +19,7 @@ hyprlang, hyprutils, hyprwayland-scanner, + hyprwire, libGL, libdrm, libexecinfo, @@ -122,6 +123,7 @@ in nativeBuildInputs = [ hyprwayland-scanner + hyprwire makeWrapper cmake pkg-config @@ -144,6 +146,7 @@ in hyprland-protocols hyprlang hyprutils + hyprwire libdrm libGL libinput diff --git a/nix/overlays.nix b/nix/overlays.nix index c7ef95b86..2a68ce8db 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -28,6 +28,7 @@ in { inputs.hyprlang.overlays.default inputs.hyprutils.overlays.default inputs.hyprwayland-scanner.overlays.default + inputs.hyprwire.overlays.default self.overlays.udis86 # Hyprland packages themselves