This commit is contained in:
Vaxry 2025-12-03 14:48:12 +00:00
parent ca8c3e4144
commit 144c7b6b54
Signed by: vaxry
GPG key ID: 665806380871D640
12 changed files with 365 additions and 149 deletions

3
.gitignore vendored
View file

@ -22,6 +22,9 @@ result
protocols/*.hpp
protocols/*.cpp
hw-protocols/*.hpp
hw-protocols/*.cpp
.cache/
hyprctl/hyprctl

View file

@ -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}\"")

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprpaper_core" version="1">
<copyright>
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.
</copyright>
<object name="hyprpaper_core_manager" version="1">
<description summary="manager object">
This is the core manager object for hyprpaper operations
</description>
<c2s name="get_wallpaper_object">
<description summary="Get a wallpaper object">
Creates a wallpaper object
</description>
<returns iface="hyprpaper_wallpaper"/>
</c2s>
<s2c name="add_monitor">
<description summary="New monitor added">
Emitted when a new monitor is added.
</description>
<arg name="monitor_name" type="varchar" summary="the monitor's name"/>
</s2c>
<s2c name="remove_monitor">
<description summary="A monitor was removed">
Emitted when a monitor is removed.
</description>
<arg name="monitor_name" type="varchar" summary="the monitor's name"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object. Children remain alive until destroyed.
</description>
</c2s>
</object>
<enum name="wallpaper_fit_mode">
<value idx="0" name="stretch"/>
<value idx="1" name="cover"/>
<value idx="2" name="contain"/>
<value idx="3" name="tile"/>
</enum>
<enum name="wallpaper_errors">
<value idx="0" name="inert_wallpaper_object" description="attempted to use an inert wallpaper object"/>
</enum>
<enum name="applying_error">
<value idx="0" name="invalid_path" description="path provided was invalid"/>
<value idx="1" name="invalid_monitor" description="monitor provided was invalid"/>
<value idx="2" name="unknown_error" description="unknown error"/>
</enum>
<object name="hyprpaper_wallpaper" version="1">
<description summary="wallpaper object">
This is an object describing a wallpaper
</description>
<c2s name="path">
<description summary="Set a path">
Set a file path for the wallpaper. This has to be an absolute path from the fs root.
This is required.
</description>
<arg name="wallpaper" type="varchar" summary="path"/>
</c2s>
<c2s name="fit_mode">
<description summary="Set a fit mode">
Set a fit mode for the wallpaper. This is set to cover by default.
</description>
<arg name="fit_mode" type="enum" interface="wallpaper_fit_mode" summary="path"/>
</c2s>
<c2s name="monitor_name">
<description summary="Set the monitor name">
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.
</description>
<arg name="monitor_name" type="varchar" summary="monitor name"/>
</c2s>
<c2s name="apply">
<description summary="Apply this wallpaper">
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.
</description>
</c2s>
<s2c name="success">
<description summary="Operation succeeded">
Wallpaper was applied successfully.
</description>
</s2c>
<s2c name="failed">
<description summary="Operation failed">
Wallpaper was not applied. See the error field for more information.
</description>
<arg name="error" type="enum" interface="hyprpaper_wallpaper_application_error" summary="path"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object.
</description>
</c2s>
</object>
</protocol>

View file

@ -94,7 +94,7 @@ std::vector<CConfigManager::SSetting> 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;

View file

@ -29,7 +29,6 @@ class CConfigManager {
Hyprlang::CConfig m_config;
std::string m_currentConfigPath;
uint32_t m_maxId = 0;
};
inline UP<CConfigManager> g_config;

View file

@ -3,12 +3,18 @@
#include <algorithm>
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<CConfigManager::SSetting>&& 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::rw<const CConfigManager::SSetting>> CWallpaperMatcher::getSetting(const std::string_view& monName) {
for (const auto& m : m_monitorStates) {
if (m.name != monName)

View file

@ -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<rw<const CConfigManager::SSetting>> getSetting(const std::string_view& monName);
@ -44,6 +45,8 @@ class CWallpaperMatcher {
std::vector<std::string> m_monitorNames;
std::vector<SMonitorState> m_monitorStates;
uint32_t m_maxId = 0;
SMonitorState& getState(const std::string_view& monName);
};

135
src/ipc/IPC.cpp Normal file
View file

@ -0,0 +1,135 @@
#include "IPC.hpp"
#include "../helpers/Logger.hpp"
#include "../config/WallpaperMatcher.hpp"
#include "../ui/UI.hpp"
#include <filesystem>
using namespace IPC;
using namespace std::string_literals;
constexpr const char* SOCKET_NAME = ".hyprpaper.sock";
static SP<CHyprpaperCoreImpl> g_coreImpl;
CWallpaperObject::CWallpaperObject(SP<CHyprpaperWallpaperObject>&& 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<CHyprpaperCoreImpl>(1, [this](SP<Hyprwire::IObject> obj) {
auto manager = m_managers.emplace_back(makeShared<CHyprpaperCoreManagerObject>(std::move(obj)));
manager->setDestroy([this, weak = WP<CHyprpaperCoreManagerObject>{manager}] { std::erase(m_managers, weak); });
manager->setOnDestroy([this, weak = WP<CHyprpaperCoreManagerObject>{manager}] { std::erase(m_managers, weak); });
manager->setGetWallpaperObject([this, weak = WP<CHyprpaperCoreManagerObject>{manager}](uint32_t id) {
if (!weak)
return;
m_wallpaperObjects.emplace_back(makeShared<CWallpaperObject>(
makeShared<CHyprpaperWallpaperObject>(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(); });
}

43
src/ipc/IPC.hpp Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "../helpers/Memory.hpp"
#include <hyprwire/hyprwire.hpp>
#include <hyprpaper_core-server.hpp>
namespace IPC {
class CWallpaperObject {
public:
CWallpaperObject(SP<CHyprpaperWallpaperObject>&& obj);
~CWallpaperObject() = default;
private:
void apply();
SP<CHyprpaperWallpaperObject> 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<Hyprwire::IServerSocket> m_socket;
std::string m_socketPath = "";
std::vector<SP<CHyprpaperCoreManagerObject>> m_managers;
std::vector<SP<CWallpaperObject>> m_wallpaperObjects;
friend class CWallpaperObject;
};
inline UP<CSocket> g_IPCSocket;
};

View file

@ -1,124 +0,0 @@
#include "Socket.hpp"
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <pwd.h>
#include <thread>
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<std::mutex> 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;
}

View file

@ -1,22 +0,0 @@
#pragma once
#include "../helpers/Memory.hpp"
#include <mutex>
#include <string>
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<CIPCSocket> g_pIPCSocket;

View file

@ -1,6 +1,7 @@
#include "UI.hpp"
#include "../helpers/Logger.hpp"
#include "../ipc/HyprlandSocket.hpp"
#include "../ipc/IPC.hpp"
#include "../config/WallpaperMatcher.hpp"
#include <hyprtoolkit/core/Output.hpp>
@ -80,6 +81,8 @@ bool CUI::run() {
if (!m_backend)
return false;
IPC::g_IPCSocket = makeUnique<IPC::CSocket>();
const auto MONITORS = m_backend->getOutputs();
for (const auto& m : MONITORS) {