ui: add support for dynamic wallpapers

- 'path' can contain multiple images comma separated (including
  directories)
- 'timeout' is used to define how long the wallpaper should be shown
  in seconds.
  Default: 30s
This commit is contained in:
Yurii Bilous 2025-12-16 21:31:07 +02:00
parent b431a94cfb
commit 9c75c84882
5 changed files with 135 additions and 15 deletions

View file

@ -1,7 +1,10 @@
#include "ConfigManager.hpp"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <hyprlang.hpp>
#include <hyprutils/path/Path.hpp>
#include <hyprutils/string/ConstVarList.hpp>
#include <string>
#include <sys/ucontext.h>
#include "../helpers/Logger.hpp"
@ -9,6 +12,14 @@
using namespace std::string_literals;
[[nodiscard]] static bool isImage(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(), ::tolower);
return std::ranges::any_of(exts, [&ext](const auto& e) { return ext == e; });
}
static std::string getMainConfigPath() {
static const auto paths = Hyprutils::Path::findConfig("hyprpaper");
@ -30,6 +41,7 @@ void CConfigManager::init() {
m_config.addSpecialConfigValue("wallpaper", "monitor", Hyprlang::STRING{""});
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.commence();
@ -55,19 +67,60 @@ static std::expected<std::string, std::string> resolvePath(const std::string_vie
return CAN;
}
static std::expected<std::string, std::string> getFullPath(const std::string_view& sv) {
if (sv.empty())
static std::expected<std::string, std::string> getPath(const std::string_view& path) {
if (path.empty())
return std::unexpected("empty path");
if (sv[0] == '~') {
if (path[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(std::string{HOME} + "/"s + std::string{path.substr(1)});
}
return resolvePath(sv);
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 constexpr const size_t maxImagesCount{100};
const auto paths = Hyprutils::String::CConstVarList(sv);
std::vector<std::string> result;
result.reserve(paths.size());
for (const auto& path : paths) {
if (result.size() >= maxImagesCount) {
g_logger->log(LOG_WARN, "Maximum number of images ({}) reached", maxImagesCount);
break;
}
const auto resolved = getPath(path);
if (!resolved)
return std::unexpected(resolved.error());
const auto resolvedPath = resolved.value();
if (!std::filesystem::exists(resolvedPath)) {
g_logger->log(LOG_WARN, "File '{}' does not exist", resolvedPath);
continue;
}
if (std::filesystem::is_directory(resolvedPath))
for (const auto& entry : std::filesystem::directory_iterator(resolvedPath, std::filesystem::directory_options::skip_permission_denied)) {
if (entry.is_regular_file() && isImage(entry.path())) {
result.push_back(entry.path());
}
}
else if (isImage(resolvedPath))
result.push_back(resolvedPath);
else
g_logger->log(LOG_WARN, "File '{}' is not an image", resolvedPath);
}
return result;
}
std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
@ -78,11 +131,13 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
for (auto& key : keys) {
std::string monitor, fitMode, path;
int timeout;
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()));
} catch (...) {
g_logger->log(LOG_ERR, "Failed parsing wallpaper for key {}", key);
continue;
@ -95,7 +150,12 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
continue;
}
result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .path = RESOLVE_PATH.value()});
if (RESOLVE_PATH.value().empty()) {
g_logger->log(LOG_ERR, "Provided path(s) '{}' does not contain a valid image", path);
continue;
}
result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .paths = RESOLVE_PATH.value(), .timeout = timeout});
}
return result;

View file

@ -14,8 +14,10 @@ class CConfigManager {
CConfigManager(CConfigManager&&) = delete;
struct SSetting {
std::string monitor, fitMode, path;
uint32_t id = 0;
std::string monitor, fitMode;
std::vector<std::string> paths;
int timeout;
uint32_t id = 0;
};
constexpr static const uint32_t SETTING_INVALID = 0;

View file

@ -85,7 +85,7 @@ void CWallpaperObject::apply() {
g_matcher->addState(CConfigManager::SSetting{
.monitor = std::move(m_monitor),
.fitMode = fitModeToStr(m_fitMode),
.path = std::move(m_path),
.paths = std::vector{std::move(m_path)},
});
m_object->sendSuccess();

View file

@ -1,4 +1,5 @@
#include "UI.hpp"
#include "../defines.hpp"
#include "../helpers/Logger.hpp"
#include "../ipc/HyprlandSocket.hpp"
#include "../ipc/IPC.hpp"
@ -12,13 +13,34 @@ CUI::~CUI() {
m_targets.clear();
}
CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::string_view& path, Hyprtoolkit::eImageFitMode fitMode) : m_monitorName(output->port()) {
struct CWallpaperTarget::ImagesData {
public:
ImagesData(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() {
current = (current + 1) % images.size();
return images[current];
}
private:
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) {
static const auto SPLASH_REPLY = HyprlandSocket::getFromSocket("/splash");
static const auto PENABLESPLASH = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "splash");
static const auto PSPLASHOFFSET = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "splash_offset");
static const auto PSPLASHALPHA = Hyprlang::CSimpleConfigValue<Hyprlang::FLOAT>(g_config->hyprlang(), "splash_opacity");
ASSERT(path.size() > 0);
m_window = Hyprtoolkit::CWindowBuilder::begin()
->type(Hyprtoolkit::HT_WINDOW_LAYER)
->prefferedOutput(output)
@ -33,9 +55,10 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})
->color([] { return Hyprtoolkit::CHyprColor{0xFF000000}; })
->commence();
m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->commence();
m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->commence();
m_image = Hyprtoolkit::CImageBuilder::begin()
->path(std::string{path})
->path(std::string{path.front()})
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
->sync(true)
->fitMode(fitMode)
@ -44,6 +67,12 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
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<ImagesData>(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_window->m_rootElement->addChild(m_bg);
m_window->m_rootElement->addChild(m_null);
m_null->addChild(m_image);
@ -68,6 +97,26 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
m_window->open();
}
CWallpaperTarget::~CWallpaperTarget() {
if (m_timer && !m_timer->passed())
m_timer->cancel();
}
void CWallpaperTarget::onRepeatTimer() {
ASSERT(m_imagesData);
m_image->rebuild()
->path(m_imagesData->nextImage())
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
->sync(true)
->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);
}
void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
g_matcher->registerOutput(mon->port());
if (IPC::g_IPCSocket)
@ -158,5 +207,5 @@ 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>(mon, TARGET->get().path, toFitMode(TARGET->get().fitMode)));
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout));
}

View file

@ -3,6 +3,7 @@
#include <vector>
#include <hyprtoolkit/core/Backend.hpp>
#include <hyprtoolkit/core/Timer.hpp>
#include <hyprtoolkit/window/Window.hpp>
#include <hyprtoolkit/element/Text.hpp>
#include <hyprtoolkit/element/Null.hpp>
@ -15,8 +16,9 @@
class CWallpaperTarget {
public:
CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::string_view& path, Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER);
~CWallpaperTarget() = default;
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);
~CWallpaperTarget();
CWallpaperTarget(const CWallpaperTarget&) = delete;
CWallpaperTarget(CWallpaperTarget&) = delete;
@ -25,6 +27,13 @@ class CWallpaperTarget {
std::string m_monitorName;
private:
void onRepeatTimer();
struct ImagesData;
UP<ImagesData> m_imagesData;
ASP<Hyprtoolkit::CTimer> m_timer;
SP<Hyprtoolkit::IBackend> m_backend;
SP<Hyprtoolkit::IWindow> m_window;
SP<Hyprtoolkit::CNullElement> m_null;
SP<Hyprtoolkit::CRectangleElement> m_bg;