diff --git a/.gitignore b/.gitignore
index 8d38ed4..e4c8953 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,9 @@ result
protocols/*.hpp
protocols/*.cpp
+hw-protocols/*.hpp
+hw-protocols/*.cpp
+
.cache/
hyprctl/hyprctl
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 955d7f9..45a5b70 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,7 +41,7 @@ execute_process(
OUTPUT_VARIABLE GIT_DIRTY
OUTPUT_STRIP_TRAILING_WHITESPACE)
-include_directories(.)
+include_directories(. hw-protocols protocols)
set(CMAKE_CXX_STANDARD 23)
add_compile_options(-DWLR_USE_UNSTABLE)
add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value
@@ -59,6 +59,7 @@ pkg_check_modules(
hyprutils>=0.2.4
wayland-client
hyprtoolkit>=0.4.0
+ hyprwire
pixman-1
libdrm)
@@ -66,6 +67,8 @@ file(GLOB_RECURSE SRCFILES "src/*.cpp")
add_executable(hyprpaper ${SRCFILES})
+# Wayland
+
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
pkg_get_variable(WAYLAND_SCANNER_PKGDATA_DIR wayland-scanner pkgdatadir)
@@ -107,6 +110,25 @@ protocolnew("stable/xdg-shell" "xdg-shell" false)
protocolnew("staging/cursor-shape" "cursor-shape-v1" false)
protocolnew("stable/tablet" "tablet-v2" false)
+# Hyprwire
+
+function(hyprprotocolServer protoPath protoName)
+ set(path ${CMAKE_SOURCE_DIR}/${protoPath})
+ add_custom_command(
+ OUTPUT ${CMAKE_SOURCE_DIR}/hw-protocols/${protoName}-server.cpp
+ ${CMAKE_SOURCE_DIR}/hw-protocols/${protoName}-server.hpp
+ COMMAND hyprwire-scanner ${path}/${protoName}.xml
+ ${CMAKE_SOURCE_DIR}/hw-protocols/
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+ target_sources(hyprpaper PRIVATE hw-protocols/${protoName}-server.cpp
+ hw-protocols/${protoName}-server.hpp
+ )
+endfunction()
+
+hyprprotocolServer(hw-protocols hyprpaper_core)
+
+#
+
string(REPLACE "\"" " " GIT_COMMIT_MESSAGE_ESCAPED "${GIT_COMMIT_MESSAGE}")
target_compile_definitions(hyprpaper
PRIVATE "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
diff --git a/hw-protocols/hyprpaper_core.xml b/hw-protocols/hyprpaper_core.xml
new file mode 100644
index 0000000..fa2edc0
--- /dev/null
+++ b/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/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp
index 36c2e07..ef37ef9 100644
--- a/src/config/ConfigManager.cpp
+++ b/src/config/ConfigManager.cpp
@@ -94,7 +94,7 @@ std::vector CConfigManager::getSettings() {
continue;
}
- result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .path = RESOLVE_PATH.value(), .id = ++m_maxId});
+ result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .path = RESOLVE_PATH.value()});
}
return result;
diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp
index 237a9a2..65fca52 100644
--- a/src/config/ConfigManager.hpp
+++ b/src/config/ConfigManager.hpp
@@ -29,7 +29,6 @@ class CConfigManager {
Hyprlang::CConfig m_config;
std::string m_currentConfigPath;
- uint32_t m_maxId = 0;
};
inline UP g_config;
diff --git a/src/config/WallpaperMatcher.cpp b/src/config/WallpaperMatcher.cpp
index e93c4ec..3e60003 100644
--- a/src/config/WallpaperMatcher.cpp
+++ b/src/config/WallpaperMatcher.cpp
@@ -3,12 +3,18 @@
#include
void CWallpaperMatcher::addState(CConfigManager::SSetting&& s) {
+ s.id = ++m_maxId;
+
std::erase_if(m_settings, [&s](const auto& e) { return e.monitor == s.monitor; });
m_settings.emplace_back(std::move(s));
recalcStates();
}
void CWallpaperMatcher::addStates(std::vector&& s) {
+ for (auto& ss : s) {
+ ss.id = ++m_maxId;
+ }
+
std::erase_if(m_settings, [&s](const auto& e) { return std::ranges::any_of(s, [&e](const auto& el) { return el.monitor == e.monitor; }); });
m_settings.append_range(std::move(s));
recalcStates();
@@ -25,6 +31,10 @@ void CWallpaperMatcher::unregisterOutput(const std::string_view& s) {
recalcStates();
}
+bool CWallpaperMatcher::outputExists(const std::string_view& s) {
+ return std::ranges::contains(m_monitorNames, s);
+}
+
std::optional> CWallpaperMatcher::getSetting(const std::string_view& monName) {
for (const auto& m : m_monitorStates) {
if (m.name != monName)
diff --git a/src/config/WallpaperMatcher.hpp b/src/config/WallpaperMatcher.hpp
index 51c6bf5..b889c24 100644
--- a/src/config/WallpaperMatcher.hpp
+++ b/src/config/WallpaperMatcher.hpp
@@ -23,6 +23,7 @@ class CWallpaperMatcher {
void registerOutput(const std::string_view&);
void unregisterOutput(const std::string_view&);
+ bool outputExists(const std::string_view&);
std::optional> getSetting(const std::string_view& monName);
@@ -44,6 +45,8 @@ class CWallpaperMatcher {
std::vector m_monitorNames;
std::vector m_monitorStates;
+ uint32_t m_maxId = 0;
+
SMonitorState& getState(const std::string_view& monName);
};
diff --git a/src/ipc/IPC.cpp b/src/ipc/IPC.cpp
new file mode 100644
index 0000000..43d020f
--- /dev/null
+++ b/src/ipc/IPC.cpp
@@ -0,0 +1,135 @@
+#include "IPC.hpp"
+#include "../helpers/Logger.hpp"
+#include "../config/WallpaperMatcher.hpp"
+#include "../ui/UI.hpp"
+
+#include
+
+using namespace IPC;
+using namespace std::string_literals;
+
+constexpr const char* SOCKET_NAME = ".hyprpaper.sock";
+
+static SP g_coreImpl;
+
+CWallpaperObject::CWallpaperObject(SP&& obj) : m_object(std::move(obj)) {
+ m_object->setDestroy([this]() { std::erase_if(g_IPCSocket->m_wallpaperObjects, [this](const auto& e) { return e.get() == this; }); });
+ m_object->setOnDestroy([this]() { std::erase_if(g_IPCSocket->m_wallpaperObjects, [this](const auto& e) { return e.get() == this; }); });
+
+ m_object->setPath([this](const char* s) {
+ if (m_inert)
+ m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");
+
+ m_path = s;
+ });
+
+ m_object->setFitMode([this](hyprpaperCoreWallpaperFitMode f) {
+ if (m_inert)
+ m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");
+
+ if (f > HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE)
+ m_object->error(HYPRPAPER_CORE_APPLYING_ERROR_UNKNOWN_ERROR, "Invalid fit mode");
+ m_fitMode = f;
+ });
+
+ m_object->setMonitorName([this](const char* s) {
+ if (m_inert)
+ m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");
+
+ m_monitor = s;
+ });
+
+ m_object->setApply([this]() {
+ if (m_inert)
+ m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");
+
+ apply();
+ });
+}
+
+static std::string fitModeToStr(hyprpaperCoreWallpaperFitMode m) {
+ switch (m) {
+ case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_CONTAIN: return "contain";
+ case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER: return "cover";
+ case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE: return "tile";
+ case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_STRETCH: return "fit";
+ default: return "cover";
+ }
+}
+
+void CWallpaperObject::apply() {
+
+ m_inert = true;
+
+ if (!m_monitor.empty() && !g_matcher->outputExists(m_monitor)) {
+ m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_MONITOR);
+ return;
+ }
+
+ if (m_path.empty()) {
+ m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH);
+ return;
+ }
+
+ if (m_path[0] != '/') {
+ m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH);
+ return;
+ }
+
+ std::error_code ec;
+ if (!std::filesystem::exists(m_path, ec) || ec) {
+ m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH);
+ return;
+ }
+
+ g_matcher->addState(CConfigManager::SSetting{
+ .monitor = std::move(m_monitor),
+ .fitMode = fitModeToStr(m_fitMode),
+ .path = std::move(m_path),
+ });
+
+ m_object->sendSuccess();
+}
+
+CSocket::CSocket() {
+ const auto RTDIR = getenv("XDG_RUNTIME_DIR");
+
+ if (!RTDIR)
+ return;
+
+ const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
+
+ if (!HIS) {
+ g_logger->log(LOG_WARN, "not running under hyprland, IPC will be disabled.");
+ return;
+ }
+
+ m_socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
+
+ std::error_code ec;
+ std::filesystem::remove(m_socketPath, ec);
+
+ m_socket = Hyprwire::IServerSocket::open(m_socketPath);
+
+ if (!m_socket)
+ return;
+
+ g_coreImpl = makeShared(1, [this](SP obj) {
+ auto manager = m_managers.emplace_back(makeShared(std::move(obj)));
+
+ manager->setDestroy([this, weak = WP{manager}] { std::erase(m_managers, weak); });
+ manager->setOnDestroy([this, weak = WP{manager}] { std::erase(m_managers, weak); });
+
+ manager->setGetWallpaperObject([this, weak = WP{manager}](uint32_t id) {
+ if (!weak)
+ return;
+
+ m_wallpaperObjects.emplace_back(makeShared(
+ makeShared(m_socket->createObject(weak->getObject()->client(), weak->getObject(), "hyprpaper_wallpaper", id))));
+ });
+ });
+
+ m_socket->addImplementation(g_coreImpl);
+
+ g_ui->backend()->addFd(m_socket->extractLoopFD(), [this]() { m_socket->dispatchEvents(); });
+}
\ No newline at end of file
diff --git a/src/ipc/IPC.hpp b/src/ipc/IPC.hpp
new file mode 100644
index 0000000..d11e287
--- /dev/null
+++ b/src/ipc/IPC.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "../helpers/Memory.hpp"
+
+#include
+#include
+
+namespace IPC {
+ class CWallpaperObject {
+ public:
+ CWallpaperObject(SP&& obj);
+ ~CWallpaperObject() = default;
+
+ private:
+ void apply();
+
+ SP m_object;
+
+ std::string m_path;
+ hyprpaperCoreWallpaperFitMode m_fitMode = HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER;
+ std::string m_monitor;
+
+ bool m_inert = false;
+ };
+
+ class CSocket {
+ public:
+ CSocket();
+ ~CSocket() = default;
+
+ private:
+ SP m_socket;
+
+ std::string m_socketPath = "";
+
+ std::vector> m_managers;
+ std::vector> m_wallpaperObjects;
+
+ friend class CWallpaperObject;
+ };
+
+ inline UP g_IPCSocket;
+};
\ No newline at end of file
diff --git a/src/ipc/Socket.cpp b/src/ipc/Socket.cpp
deleted file mode 100644
index c6650c5..0000000
--- a/src/ipc/Socket.cpp
+++ /dev/null
@@ -1,124 +0,0 @@
-#include "Socket.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-void CIPCSocket::initialize() {
- // std::thread([&]() {
- // const auto SOCKET = socket(AF_UNIX, SOCK_STREAM, 0);
-
- // if (SOCKET < 0) {
- // Debug::log(ERR, "Couldn't start the hyprpaper Socket. (1) IPC will not work.");
- // return;
- // }
-
- // sockaddr_un SERVERADDRESS = {.sun_family = AF_UNIX};
-
- // const auto HISenv = getenv("HYPRLAND_INSTANCE_SIGNATURE");
- // const auto RUNTIMEdir = getenv("XDG_RUNTIME_DIR");
- // const std::string USERID = std::to_string(getpwuid(getuid())->pw_uid);
-
- // const auto USERDIR = RUNTIMEdir ? RUNTIMEdir + std::string{"/hypr/"} : "/run/user/" + USERID + "/hypr/";
-
- // std::string socketPath = HISenv ? USERDIR + std::string(HISenv) + "/.hyprpaper.sock" : USERDIR + ".hyprpaper.sock";
-
- // if (!HISenv)
- // mkdir(USERDIR.c_str(), S_IRWXU);
-
- // unlink(socketPath.c_str());
-
- // strcpy(SERVERADDRESS.sun_path, socketPath.c_str());
-
- // bind(SOCKET, (sockaddr*)&SERVERADDRESS, SUN_LEN(&SERVERADDRESS));
-
- // // 10 max queued.
- // listen(SOCKET, 10);
-
- // sockaddr_in clientAddress = {};
- // socklen_t clientSize = sizeof(clientAddress);
-
- // char readBuffer[1024] = {0};
-
- // Debug::log(LOG, "hyprpaper socket started at {} (fd: {})", socketPath, SOCKET);
- // while (1) {
- // const auto ACCEPTEDCONNECTION = accept(SOCKET, (sockaddr*)&clientAddress, &clientSize);
- // if (ACCEPTEDCONNECTION < 0) {
- // Debug::log(ERR, "Couldn't listen on the hyprpaper Socket. (3) IPC will not work.");
- // break;
- // } else {
- // do {
- // Debug::log(LOG, "Accepted incoming socket connection request on fd {}", ACCEPTEDCONNECTION);
- // std::lock_guard lg(g_pHyprpaper->m_mtTickMutex);
-
- // auto messageSize = read(ACCEPTEDCONNECTION, readBuffer, 1024);
- // readBuffer[messageSize == 1024 ? 1023 : messageSize] = '\0';
- // if (messageSize == 0)
- // break;
- // std::string request(readBuffer);
-
- // m_szRequest = request;
- // m_bRequestReady = true;
-
- // g_pHyprpaper->tick(true);
- // while (!m_bReplyReady) { // wait for Hyprpaper to finish processing the request
- // std::this_thread::sleep_for(std::chrono::milliseconds(1));
- // }
- // write(ACCEPTEDCONNECTION, m_szReply.c_str(), m_szReply.length());
- // m_bReplyReady = false;
- // m_szReply = "";
-
- // } while (1);
- // Debug::log(LOG, "Closing Accepted Connection");
- // close(ACCEPTEDCONNECTION);
- // }
- // }
-
- // close(SOCKET);
- // }).detach();
-}
-
-bool CIPCSocket::mainThreadParseRequest() {
-
- // if (!m_bRequestReady)
- // return false;
-
- // std::string copy = m_szRequest;
-
- // if (copy == "")
- // return false;
-
- // // now we can work on the copy
-
- // Debug::log(LOG, "Received a request: {}", copy);
-
- // // set default reply
- // m_szReply = "ok";
- // m_bReplyReady = true;
- // m_bRequestReady = false;
-
- // // config commands
- // if (copy.find("wallpaper") == 0) {
-
- // const auto RESULT = g_pConfigManager->config->parseDynamic(copy.substr(0, copy.find_first_of(' ')).c_str(), copy.substr(copy.find_first_of(' ') + 1).c_str());
-
- // if (RESULT.error) {
- // m_szReply = RESULT.getError();
- // return false;
- // }
-
- // return true;
- // }
-
- // m_szReply = "invalid command";
- // return false;
-}
diff --git a/src/ipc/Socket.hpp b/src/ipc/Socket.hpp
deleted file mode 100644
index a071efc..0000000
--- a/src/ipc/Socket.hpp
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma once
-
-#include "../helpers/Memory.hpp"
-#include
-#include
-
-class CIPCSocket {
- public:
- void initialize();
-
- bool mainThreadParseRequest();
-
- private:
- std::mutex m_mtRequestMutex;
- std::string m_szRequest = "";
- std::string m_szReply = "";
-
- bool m_bRequestReady = false;
- bool m_bReplyReady = false;
-};
-
-inline UP g_pIPCSocket;
\ No newline at end of file
diff --git a/src/ui/UI.cpp b/src/ui/UI.cpp
index 7aea9b6..b9f09a6 100644
--- a/src/ui/UI.cpp
+++ b/src/ui/UI.cpp
@@ -1,6 +1,7 @@
#include "UI.hpp"
#include "../helpers/Logger.hpp"
#include "../ipc/HyprlandSocket.hpp"
+#include "../ipc/IPC.hpp"
#include "../config/WallpaperMatcher.hpp"
#include
@@ -80,6 +81,8 @@ bool CUI::run() {
if (!m_backend)
return false;
+ IPC::g_IPCSocket = makeUnique();
+
const auto MONITORS = m_backend->getOutputs();
for (const auto& m : MONITORS) {