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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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