mirror of
https://github.com/hyprwm/hyprpaper.git
synced 2026-05-03 23:28:02 +02:00
Merge a86ec42373 into a67b9ed738
This commit is contained in:
commit
9bede48780
5 changed files with 472 additions and 46 deletions
|
|
@ -1,11 +1,13 @@
|
|||
#include "ConfigManager.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <glob.h>
|
||||
#include <hyprlang.hpp>
|
||||
#include <hyprutils/path/Path.hpp>
|
||||
#include <hyprutils/string/String.hpp>
|
||||
#include <hyprutils/utils/ScopeGuard.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include "../helpers/Logger.hpp"
|
||||
#include "WallpaperMatcher.hpp"
|
||||
|
|
@ -63,6 +65,7 @@ bool CConfigManager::init() {
|
|||
m_config.addSpecialConfigValue("wallpaper", "path", Hyprlang::STRING{""});
|
||||
m_config.addSpecialConfigValue("wallpaper", "fit_mode", Hyprlang::STRING{"cover"});
|
||||
m_config.addSpecialConfigValue("wallpaper", "timeout", Hyprlang::INT{0});
|
||||
m_config.addSpecialConfigValue("wallpaper", "triggers", Hyprlang::STRING{""});
|
||||
|
||||
m_config.registerHandler(&handleSource, "source", Hyprlang::SHandlerOptions{});
|
||||
|
||||
|
|
@ -118,19 +121,11 @@ static std::expected<std::string, std::string> getPath(const std::string_view& s
|
|||
return resolvePath(path);
|
||||
}
|
||||
|
||||
static std::expected<std::vector<std::string>, std::string> getFullPath(const std::string& sv) {
|
||||
if (sv.empty())
|
||||
return std::unexpected("empty path");
|
||||
|
||||
static std::expected<std::vector<std::string>, std::string> getFullPathResolved(const std::string& resolvedPath) {
|
||||
static constexpr const size_t maxImagesCount{1024};
|
||||
|
||||
std::vector<std::string> result;
|
||||
|
||||
const auto resolved = getPath(sv);
|
||||
if (!resolved)
|
||||
return std::unexpected(resolved.error());
|
||||
|
||||
const auto resolvedPath = resolved.value();
|
||||
if (!std::filesystem::exists(resolvedPath))
|
||||
return std::unexpected(std::format("File '{}' does not exist", resolvedPath));
|
||||
|
||||
|
|
@ -150,6 +145,106 @@ static std::expected<std::vector<std::string>, std::string> getFullPath(const st
|
|||
return result;
|
||||
}
|
||||
|
||||
struct SParsedTriggers {
|
||||
uint32_t flags = 0;
|
||||
int fileChangeDebounceMs = 0;
|
||||
bool hasTimeout = false;
|
||||
int timeout = 0;
|
||||
bool hasValidEntry = false;
|
||||
};
|
||||
|
||||
static std::optional<int> parsePositiveInt(const std::string_view& sv) {
|
||||
if (sv.empty())
|
||||
return std::nullopt;
|
||||
|
||||
try {
|
||||
size_t idx = 0;
|
||||
int val = std::stoi(std::string{sv}, &idx);
|
||||
if (idx != sv.size() || val <= 0)
|
||||
return std::nullopt;
|
||||
return val;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static std::optional<int> parseNonNegativeInt(const std::string_view& sv) {
|
||||
if (sv.empty())
|
||||
return std::nullopt;
|
||||
|
||||
try {
|
||||
size_t idx = 0;
|
||||
int val = std::stoi(std::string{sv}, &idx);
|
||||
if (idx != sv.size() || val < 0)
|
||||
return std::nullopt;
|
||||
return val;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static SParsedTriggers parseTriggers(const std::string_view& triggerSpec, const std::string_view& keyName) {
|
||||
SParsedTriggers out;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos <= triggerSpec.size()) {
|
||||
const auto nextComma = triggerSpec.find(',', pos);
|
||||
const auto token =
|
||||
Hyprutils::String::trim(nextComma == std::string::npos ? triggerSpec.substr(pos) : triggerSpec.substr(pos, nextComma - pos));
|
||||
|
||||
if (!token.empty()) {
|
||||
std::string lowerToken{token};
|
||||
std::transform(lowerToken.begin(), lowerToken.end(), lowerToken.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
if (lowerToken == "sighup") {
|
||||
out.flags |= CConfigManager::SSetting::TRIGGER_SIGHUP;
|
||||
out.hasValidEntry = true;
|
||||
} else if (lowerToken == "sigusr1") {
|
||||
out.flags |= CConfigManager::SSetting::TRIGGER_SIGUSR1;
|
||||
out.hasValidEntry = true;
|
||||
} else if (lowerToken == "sigusr2") {
|
||||
out.flags |= CConfigManager::SSetting::TRIGGER_SIGUSR2;
|
||||
out.hasValidEntry = true;
|
||||
} else if (lowerToken.starts_with("timeout")) {
|
||||
auto arg = Hyprutils::String::trim(token.substr(std::string_view{"timeout"}.size()));
|
||||
if (!arg.empty() && arg.front() == '=')
|
||||
arg = Hyprutils::String::trim(arg.substr(1));
|
||||
|
||||
const auto parsedTimeout = parsePositiveInt(arg);
|
||||
if (!parsedTimeout)
|
||||
g_logger->log(LOG_WARN, "Invalid timeout trigger '{}' for wallpaper key {}, expected: timeout <seconds>", token, keyName);
|
||||
else {
|
||||
out.hasTimeout = true;
|
||||
out.timeout = *parsedTimeout;
|
||||
out.hasValidEntry = true;
|
||||
}
|
||||
} else if (lowerToken.starts_with("file_change")) {
|
||||
out.flags |= CConfigManager::SSetting::TRIGGER_FILE_CHANGE;
|
||||
out.hasValidEntry = true;
|
||||
|
||||
auto arg = Hyprutils::String::trim(token.substr(std::string_view{"file_change"}.size()));
|
||||
if (!arg.empty() && arg.front() == '=')
|
||||
arg = Hyprutils::String::trim(arg.substr(1));
|
||||
|
||||
if (!arg.empty()) {
|
||||
const auto parsedDebounce = parseNonNegativeInt(arg);
|
||||
if (!parsedDebounce)
|
||||
g_logger->log(LOG_WARN, "Invalid file_change trigger '{}' for wallpaper key {}, expected: file_change [milliseconds]", token, keyName);
|
||||
else
|
||||
out.fileChangeDebounceMs = *parsedDebounce;
|
||||
}
|
||||
} else
|
||||
g_logger->log(LOG_WARN, "Unknown trigger '{}' for wallpaper key {}", token, keyName);
|
||||
}
|
||||
|
||||
if (nextComma == std::string::npos)
|
||||
break;
|
||||
pos = nextComma + 1;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
|
||||
std::vector<CConfigManager::SSetting> result;
|
||||
|
||||
|
|
@ -157,20 +252,28 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
|
|||
result.reserve(keys.size());
|
||||
|
||||
for (auto& key : keys) {
|
||||
std::string monitor, fitMode, path;
|
||||
int timeout;
|
||||
std::string monitor, fitMode, path, triggers;
|
||||
int timeout, fileChangeDebounceMs = 0;
|
||||
uint32_t triggerFlags = 0;
|
||||
|
||||
try {
|
||||
monitor = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "monitor", key.c_str()));
|
||||
fitMode = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "fit_mode", key.c_str()));
|
||||
path = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "path", key.c_str()));
|
||||
timeout = std::any_cast<Hyprlang::INT>(m_config.getSpecialConfigValue("wallpaper", "timeout", key.c_str()));
|
||||
triggers = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "triggers", key.c_str()));
|
||||
} catch (...) {
|
||||
g_logger->log(LOG_ERR, "Failed parsing wallpaper for key {}", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto RESOLVE_PATH = getFullPath(path);
|
||||
const auto RESOLVED_INPUT_PATH = getPath(path);
|
||||
if (!RESOLVED_INPUT_PATH) {
|
||||
g_logger->log(LOG_ERR, "Failed to resolve path {}: {}", path, RESOLVED_INPUT_PATH.error());
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto RESOLVE_PATH = getFullPathResolved(RESOLVED_INPUT_PATH.value());
|
||||
|
||||
if (!RESOLVE_PATH) {
|
||||
g_logger->log(LOG_ERR, "Failed to resolve path {}: {}", path, RESOLVE_PATH.error());
|
||||
|
|
@ -182,7 +285,24 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
|
|||
continue;
|
||||
}
|
||||
|
||||
result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .paths = RESOLVE_PATH.value(), .timeout = timeout});
|
||||
const auto PARSED_TRIGGERS = parseTriggers(triggers, key);
|
||||
triggerFlags = PARSED_TRIGGERS.flags;
|
||||
fileChangeDebounceMs = PARSED_TRIGGERS.fileChangeDebounceMs;
|
||||
|
||||
if (PARSED_TRIGGERS.hasTimeout)
|
||||
timeout = PARSED_TRIGGERS.timeout;
|
||||
else if (PARSED_TRIGGERS.hasValidEntry && timeout <= 0)
|
||||
timeout = -1;
|
||||
|
||||
result.emplace_back(SSetting{
|
||||
.monitor = std::move(monitor),
|
||||
.fitMode = std::move(fitMode),
|
||||
.paths = RESOLVE_PATH.value(),
|
||||
.timeout = timeout,
|
||||
.triggers = triggerFlags,
|
||||
.fileChangeDebounceMs = fileChangeDebounceMs,
|
||||
.sourcePath = RESOLVED_INPUT_PATH.value(),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,19 @@ class CConfigManager {
|
|||
CConfigManager(CConfigManager&&) = delete;
|
||||
|
||||
struct SSetting {
|
||||
enum : uint32_t {
|
||||
TRIGGER_SIGHUP = 1U << 0,
|
||||
TRIGGER_SIGUSR1 = 1U << 1,
|
||||
TRIGGER_SIGUSR2 = 1U << 2,
|
||||
TRIGGER_FILE_CHANGE = 1U << 3,
|
||||
};
|
||||
|
||||
std::string monitor, fitMode;
|
||||
std::vector<std::string> paths;
|
||||
int timeout = 0;
|
||||
uint32_t triggers = 0;
|
||||
int fileChangeDebounceMs = 0;
|
||||
std::string sourcePath;
|
||||
uint32_t id = 0;
|
||||
};
|
||||
|
||||
|
|
|
|||
343
src/ui/UI.cpp
343
src/ui/UI.cpp
|
|
@ -1,18 +1,38 @@
|
|||
#include "UI.hpp"
|
||||
#include "../config/WallpaperMatcher.hpp"
|
||||
#include "../defines.hpp"
|
||||
#include "../helpers/Logger.hpp"
|
||||
#include "../helpers/GlobalState.hpp"
|
||||
#include "../helpers/Logger.hpp"
|
||||
#include "../ipc/HyprlandSocket.hpp"
|
||||
#include "../ipc/IPC.hpp"
|
||||
#include "../config/WallpaperMatcher.hpp"
|
||||
|
||||
#include <hyprtoolkit/core/Output.hpp>
|
||||
|
||||
#include <hyprutils/string/String.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
#include <sys/signalfd.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
static constexpr auto FILE_WATCH_POLL_INTERVAL = 500ms;
|
||||
|
||||
CUI::CUI() = default;
|
||||
|
||||
CUI::~CUI() {
|
||||
if (m_signalFD >= 0)
|
||||
close(m_signalFD);
|
||||
|
||||
m_targets.clear();
|
||||
}
|
||||
|
||||
|
|
@ -22,26 +42,171 @@ static std::string_view pruneDesc(const std::string_view& sv) {
|
|||
return sv;
|
||||
}
|
||||
|
||||
static Hyprtoolkit::eImageFitMode toFitMode(const std::string_view& sv) {
|
||||
if (sv.starts_with("contain"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_CONTAIN;
|
||||
if (sv.starts_with("cover"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_COVER;
|
||||
if (sv.starts_with("tile"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_TILE;
|
||||
if (sv.starts_with("fill"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_STRETCH;
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_COVER;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void hashCombine(size_t& seed, const T& val) {
|
||||
seed ^= std::hash<T>{}(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
struct SPathSnapshot {
|
||||
std::vector<std::string> images;
|
||||
std::unordered_map<std::string, uint64_t> writeTimes;
|
||||
size_t signature = 0;
|
||||
};
|
||||
|
||||
static bool isSupportedImagePath(const std::filesystem::path& path) {
|
||||
static constexpr std::array exts{".jpg", ".jpeg", ".png", ".bmp", ".webp", ".svg"};
|
||||
|
||||
auto ext = path.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
return std::ranges::any_of(exts, [&ext](const auto& e) { return ext == e; });
|
||||
}
|
||||
|
||||
static uint64_t toNs(std::filesystem::file_time_type tp) {
|
||||
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(tp.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
static SPathSnapshot snapshotSourcePath(const std::string& sourcePath) {
|
||||
SPathSnapshot out;
|
||||
|
||||
if (sourcePath.empty())
|
||||
return out;
|
||||
|
||||
std::error_code ec;
|
||||
const auto path = std::filesystem::path{sourcePath};
|
||||
|
||||
const auto addFile = [&out](const std::filesystem::path& filePath) {
|
||||
std::error_code ec;
|
||||
const auto mtime = std::filesystem::last_write_time(filePath, ec);
|
||||
if (ec)
|
||||
return;
|
||||
|
||||
const auto pathStr = filePath.string();
|
||||
out.images.emplace_back(pathStr);
|
||||
out.writeTimes[pathStr] = toNs(mtime);
|
||||
};
|
||||
|
||||
if (std::filesystem::is_directory(path, ec) && !ec) {
|
||||
auto directoryIt = std::filesystem::directory_iterator(path, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
auto endIt = std::filesystem::directory_iterator{};
|
||||
|
||||
for (; !ec && directoryIt != endIt; directoryIt.increment(ec)) {
|
||||
const auto& entry = *directoryIt;
|
||||
std::error_code iterEc;
|
||||
if (!entry.is_regular_file(iterEc) || iterEc)
|
||||
continue;
|
||||
if (!isSupportedImagePath(entry.path()))
|
||||
continue;
|
||||
addFile(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
if (std::filesystem::is_regular_file(path, ec) && !ec && isSupportedImagePath(path))
|
||||
addFile(path);
|
||||
|
||||
std::sort(out.images.begin(), out.images.end());
|
||||
|
||||
size_t sig = 0;
|
||||
for (const auto& image : out.images) {
|
||||
hashCombine(sig, image);
|
||||
const auto mtIt = out.writeTimes.find(image);
|
||||
if (mtIt != out.writeTimes.end())
|
||||
hashCombine(sig, mtIt->second);
|
||||
}
|
||||
|
||||
out.signature = sig;
|
||||
return out;
|
||||
}
|
||||
|
||||
class CWallpaperTarget::CImagesData {
|
||||
public:
|
||||
CImagesData(Hyprtoolkit::eImageFitMode fitMode, const std::vector<std::string>& images, const int timeout = 0) :
|
||||
fitMode(fitMode), images(images), timeout(timeout > 0 ? timeout : 30) {}
|
||||
|
||||
const Hyprtoolkit::eImageFitMode fitMode;
|
||||
const std::vector<std::string> images;
|
||||
const int timeout;
|
||||
|
||||
std::string nextImage() {
|
||||
if (images.empty())
|
||||
return "";
|
||||
|
||||
current = (current + 1) % images.size();
|
||||
return images[current];
|
||||
}
|
||||
|
||||
std::string currentImage() const {
|
||||
if (images.empty())
|
||||
return "";
|
||||
|
||||
return images[current];
|
||||
}
|
||||
|
||||
bool contains(const std::string& path) const {
|
||||
return std::ranges::find(images, path) != images.end();
|
||||
}
|
||||
|
||||
bool setCurrentImage(const std::string& path) {
|
||||
const auto IT = std::ranges::find(images, path);
|
||||
if (IT == images.end())
|
||||
return false;
|
||||
|
||||
current = std::distance(images.begin(), IT);
|
||||
return true;
|
||||
}
|
||||
|
||||
void updateImages(std::vector<std::string> newImages, const std::string& currentPath) {
|
||||
if (newImages.empty()) {
|
||||
images.clear();
|
||||
current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t nextCurrent = 0;
|
||||
|
||||
const auto IT = std::ranges::find(newImages, currentPath);
|
||||
if (IT != newImages.end())
|
||||
nextCurrent = std::distance(newImages.begin(), IT);
|
||||
|
||||
images = std::move(newImages);
|
||||
current = std::min(nextCurrent, images.size() - 1);
|
||||
}
|
||||
|
||||
private:
|
||||
size_t current = 0;
|
||||
std::vector<std::string> images;
|
||||
size_t current = 0;
|
||||
};
|
||||
|
||||
CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoolkit::IOutput> output, const std::vector<std::string>& path, Hyprtoolkit::eImageFitMode fitMode,
|
||||
const int timeout) : m_monitorName(output->port()), m_backend(backend) {
|
||||
class CWallpaperTarget::CFileWatchData {
|
||||
public:
|
||||
CFileWatchData(std::string sourcePath, int debounceMs) : sourcePath(std::move(sourcePath)), debounceMs(std::max(0, debounceMs)) {
|
||||
lastSnapshot = snapshotSourcePath(this->sourcePath);
|
||||
}
|
||||
|
||||
std::string sourcePath;
|
||||
int debounceMs = 0;
|
||||
SPathSnapshot lastSnapshot;
|
||||
SPathSnapshot pendingSnapshot;
|
||||
bool dirty = false;
|
||||
std::chrono::steady_clock::time_point lastChange;
|
||||
};
|
||||
|
||||
CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoolkit::IOutput> output, const std::vector<std::string>& path,
|
||||
Hyprtoolkit::eImageFitMode fitMode, const int timeout, const uint32_t triggers, const int fileChangeDebounceMs,
|
||||
const std::string& sourcePath) :
|
||||
m_monitorName(output->port()), m_backend(backend), m_triggers(triggers) {
|
||||
static const auto SPLASH_REPLY = HyprlandSocket::getFromSocket("/splash");
|
||||
|
||||
static const auto PENABLESPLASH = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "splash");
|
||||
|
|
@ -78,10 +243,14 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoo
|
|||
m_image->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE);
|
||||
m_image->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_CENTER, true);
|
||||
|
||||
if (path.size() > 1) {
|
||||
m_imagesData = makeUnique<CImagesData>(fitMode, path, timeout);
|
||||
m_timer =
|
||||
m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
|
||||
m_imagesData = makeUnique<CImagesData>(fitMode, path, timeout);
|
||||
|
||||
if (path.size() > 1 && timeout != -1)
|
||||
m_timer = m_backend->addTimer(std::chrono::seconds(m_imagesData->timeout), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
|
||||
|
||||
if ((m_triggers & CConfigManager::SSetting::TRIGGER_FILE_CHANGE) && !sourcePath.empty()) {
|
||||
m_fileWatchData = makeUnique<CFileWatchData>(sourcePath, fileChangeDebounceMs);
|
||||
m_fileWatchTimer = m_backend->addTimer(FILE_WATCH_POLL_INTERVAL, [this](ASP<Hyprtoolkit::CTimer> self, void*) { onFileWatchTimer(); }, nullptr);
|
||||
}
|
||||
|
||||
m_window->m_rootElement->addChild(m_bg);
|
||||
|
|
@ -111,13 +280,16 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoo
|
|||
CWallpaperTarget::~CWallpaperTarget() {
|
||||
if (m_timer && !m_timer->passed())
|
||||
m_timer->cancel();
|
||||
|
||||
if (m_fileWatchTimer && !m_fileWatchTimer->passed())
|
||||
m_fileWatchTimer->cancel();
|
||||
}
|
||||
|
||||
void CWallpaperTarget::onRepeatTimer() {
|
||||
void CWallpaperTarget::setImagePath(const std::string& path) {
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
ASSERT(m_imagesData);
|
||||
|
||||
m_lastPath = m_imagesData->nextImage();
|
||||
m_lastPath = path;
|
||||
|
||||
m_image->rebuild()
|
||||
->path(std::string{m_lastPath})
|
||||
|
|
@ -126,10 +298,88 @@ void CWallpaperTarget::onRepeatTimer() {
|
|||
->fitMode(m_imagesData->fitMode)
|
||||
->commence();
|
||||
|
||||
m_timer =
|
||||
m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
|
||||
if (IPC::g_IPCSocket)
|
||||
IPC::g_IPCSocket->onWallpaperChanged(m_monitorName, m_lastPath);
|
||||
}
|
||||
|
||||
IPC::g_IPCSocket->onWallpaperChanged(m_monitorName, m_lastPath);
|
||||
void CWallpaperTarget::cycleImage() {
|
||||
ASSERT(m_imagesData);
|
||||
|
||||
setImagePath(m_imagesData->nextImage());
|
||||
}
|
||||
|
||||
void CWallpaperTarget::onRepeatTimer() {
|
||||
cycleImage();
|
||||
|
||||
m_timer = m_backend->addTimer(std::chrono::seconds(m_imagesData->timeout), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
|
||||
}
|
||||
|
||||
bool CWallpaperTarget::supportsSignal(int signal) const {
|
||||
switch (signal) {
|
||||
case SIGHUP: return m_triggers & CConfigManager::SSetting::TRIGGER_SIGHUP;
|
||||
case SIGUSR1: return m_triggers & CConfigManager::SSetting::TRIGGER_SIGUSR1;
|
||||
case SIGUSR2: return m_triggers & CConfigManager::SSetting::TRIGGER_SIGUSR2;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CWallpaperTarget::onSignal(int signal) {
|
||||
if (!supportsSignal(signal))
|
||||
return;
|
||||
|
||||
cycleImage();
|
||||
}
|
||||
|
||||
void CWallpaperTarget::onFileWatchTimer() {
|
||||
ASSERT(m_fileWatchData);
|
||||
|
||||
const auto snapshot = snapshotSourcePath(m_fileWatchData->sourcePath);
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if (snapshot.signature != m_fileWatchData->lastSnapshot.signature) {
|
||||
m_fileWatchData->pendingSnapshot = snapshot;
|
||||
m_fileWatchData->dirty = true;
|
||||
m_fileWatchData->lastChange = now;
|
||||
}
|
||||
|
||||
if (m_fileWatchData->dirty) {
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_fileWatchData->lastChange).count();
|
||||
if (elapsedMs >= m_fileWatchData->debounceMs) {
|
||||
auto& oldSnapshot = m_fileWatchData->lastSnapshot;
|
||||
auto newSnapshot = std::move(m_fileWatchData->pendingSnapshot);
|
||||
|
||||
m_fileWatchData->dirty = false;
|
||||
|
||||
if (!newSnapshot.images.empty()) {
|
||||
std::vector<std::string> changedOrAdded;
|
||||
changedOrAdded.reserve(newSnapshot.images.size());
|
||||
|
||||
for (const auto& image : newSnapshot.images) {
|
||||
const auto NEW_IT = newSnapshot.writeTimes.find(image);
|
||||
const auto OLD_IT = oldSnapshot.writeTimes.find(image);
|
||||
if (OLD_IT == oldSnapshot.writeTimes.end())
|
||||
changedOrAdded.push_back(image);
|
||||
else if (NEW_IT != newSnapshot.writeTimes.end() && NEW_IT->second != OLD_IT->second)
|
||||
changedOrAdded.push_back(image);
|
||||
}
|
||||
|
||||
const bool hadCurrentBefore = m_imagesData->contains(m_lastPath);
|
||||
|
||||
m_imagesData->updateImages(newSnapshot.images, m_lastPath);
|
||||
|
||||
if (!changedOrAdded.empty()) {
|
||||
m_imagesData->setCurrentImage(changedOrAdded.front());
|
||||
setImagePath(m_imagesData->currentImage());
|
||||
} else if (!hadCurrentBefore || !m_imagesData->contains(m_lastPath))
|
||||
setImagePath(m_imagesData->currentImage());
|
||||
}
|
||||
|
||||
m_fileWatchData->lastSnapshot = std::move(newSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
m_fileWatchTimer = m_backend->addTimer(FILE_WATCH_POLL_INTERVAL, [this](ASP<Hyprtoolkit::CTimer> self, void*) { onFileWatchTimer(); }, nullptr);
|
||||
}
|
||||
|
||||
void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
|
||||
|
|
@ -144,10 +394,35 @@ void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
|
|||
});
|
||||
}
|
||||
|
||||
void CUI::onSignalFDReadable() {
|
||||
if (m_signalFD < 0)
|
||||
return;
|
||||
|
||||
signalfd_siginfo fdsi;
|
||||
|
||||
while (true) {
|
||||
const auto readBytes = read(m_signalFD, &fdsi, sizeof(fdsi));
|
||||
|
||||
if (readBytes == static_cast<ssize_t>(sizeof(fdsi))) {
|
||||
for (const auto& target : m_targets) {
|
||||
target->onSignal(static_cast<int>(fdsi.ssi_signo));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (readBytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
return;
|
||||
|
||||
if (readBytes < 0)
|
||||
g_logger->log(LOG_WARN, "Failed reading signal fd: {}", std::strerror(errno));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool CUI::run() {
|
||||
static const auto PENABLEIPC = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "ipc");
|
||||
|
||||
//
|
||||
Hyprtoolkit::IBackend::SBackendCreationData data;
|
||||
data.pLogConnection = makeShared<Hyprutils::CLI::CLoggerConnection>(*g_logger);
|
||||
data.pLogConnection->setName("hyprtoolkit");
|
||||
|
|
@ -158,6 +433,21 @@ bool CUI::run() {
|
|||
if (!m_backend)
|
||||
return false;
|
||||
|
||||
sigset_t signalMask;
|
||||
sigemptyset(&signalMask);
|
||||
sigaddset(&signalMask, SIGHUP);
|
||||
sigaddset(&signalMask, SIGUSR1);
|
||||
sigaddset(&signalMask, SIGUSR2);
|
||||
|
||||
if (sigprocmask(SIG_BLOCK, &signalMask, nullptr) == 0) {
|
||||
m_signalFD = signalfd(-1, &signalMask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
if (m_signalFD >= 0)
|
||||
m_backend->addFd(m_signalFD, [this]() { onSignalFDReadable(); });
|
||||
else
|
||||
g_logger->log(LOG_WARN, "Failed setting up signalfd: {}", std::strerror(errno));
|
||||
} else
|
||||
g_logger->log(LOG_WARN, "Failed blocking trigger signals: {}", std::strerror(errno));
|
||||
|
||||
if (*PENABLEIPC)
|
||||
IPC::g_IPCSocket = makeUnique<IPC::CSocket>();
|
||||
|
||||
|
|
@ -171,7 +461,6 @@ bool CUI::run() {
|
|||
|
||||
g_logger->log(LOG_DEBUG, "Found {} output(s)", MONITORS.size());
|
||||
|
||||
// load the config now, then bind
|
||||
for (const auto& m : MONITORS) {
|
||||
targetChanged(m);
|
||||
}
|
||||
|
|
@ -187,18 +476,6 @@ SP<Hyprtoolkit::IBackend> CUI::backend() {
|
|||
return m_backend;
|
||||
}
|
||||
|
||||
static Hyprtoolkit::eImageFitMode toFitMode(const std::string_view& sv) {
|
||||
if (sv.starts_with("contain"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_CONTAIN;
|
||||
if (sv.starts_with("cover"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_COVER;
|
||||
if (sv.starts_with("tile"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_TILE;
|
||||
if (sv.starts_with("fill"))
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_STRETCH;
|
||||
return Hyprtoolkit::IMAGE_FIT_MODE_COVER;
|
||||
}
|
||||
|
||||
void CUI::targetChanged(const std::string_view& monName) {
|
||||
const auto MONITORS = m_backend->getOutputs();
|
||||
SP<Hyprtoolkit::IOutput> monitor;
|
||||
|
|
@ -228,7 +505,9 @@ void CUI::targetChanged(const SP<Hyprtoolkit::IOutput>& mon) {
|
|||
|
||||
std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); });
|
||||
|
||||
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout));
|
||||
m_targets.emplace_back(
|
||||
makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().triggers,
|
||||
TARGET->get().fileChangeDebounceMs, TARGET->get().sourcePath));
|
||||
}
|
||||
|
||||
const std::vector<SP<CWallpaperTarget>>& CUI::targets() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <hyprtoolkit/core/Backend.hpp>
|
||||
|
|
@ -17,23 +19,34 @@
|
|||
class CWallpaperTarget {
|
||||
public:
|
||||
CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoolkit::IOutput> output, const std::vector<std::string>& path,
|
||||
Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER, const int timeout = 0);
|
||||
Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER, const int timeout = 0, const uint32_t triggers = 0,
|
||||
const int fileChangeDebounceMs = 0, const std::string& sourcePath = "");
|
||||
~CWallpaperTarget();
|
||||
|
||||
CWallpaperTarget(const CWallpaperTarget&) = delete;
|
||||
CWallpaperTarget(CWallpaperTarget&) = delete;
|
||||
CWallpaperTarget(CWallpaperTarget&&) = delete;
|
||||
|
||||
void onSignal(int signal);
|
||||
|
||||
std::string m_monitorName, m_lastPath;
|
||||
|
||||
private:
|
||||
void onRepeatTimer();
|
||||
void onFileWatchTimer();
|
||||
void setImagePath(const std::string& path);
|
||||
void cycleImage();
|
||||
bool supportsSignal(int signal) const;
|
||||
|
||||
class CImagesData;
|
||||
class CFileWatchData;
|
||||
|
||||
UP<CImagesData> m_imagesData;
|
||||
UP<CFileWatchData> m_fileWatchData;
|
||||
ASP<Hyprtoolkit::CTimer> m_timer;
|
||||
ASP<Hyprtoolkit::CTimer> m_fileWatchTimer;
|
||||
SP<Hyprtoolkit::IBackend> m_backend;
|
||||
uint32_t m_triggers = 0;
|
||||
SP<Hyprtoolkit::IWindow> m_window;
|
||||
SP<Hyprtoolkit::CNullElement> m_null;
|
||||
SP<Hyprtoolkit::CRectangleElement> m_bg;
|
||||
|
|
@ -54,9 +67,12 @@ class CUI {
|
|||
void targetChanged(const SP<Hyprtoolkit::IOutput>& mon);
|
||||
void targetChanged(const std::string_view& monName);
|
||||
void registerOutput(const SP<Hyprtoolkit::IOutput>& mon);
|
||||
void onSignalFDReadable();
|
||||
|
||||
SP<Hyprtoolkit::IBackend> m_backend;
|
||||
|
||||
int m_signalFD = -1;
|
||||
|
||||
std::vector<SP<CWallpaperTarget>> m_targets;
|
||||
|
||||
struct {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ ConditionEnvironment=WAYLAND_DISPLAY
|
|||
[Service]
|
||||
Type=simple
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/bin/hyprpaper
|
||||
ExecReload=kill -HUP $MAINPID
|
||||
Slice=session.slice
|
||||
Restart=on-failure
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue