Skip to content

Commit e441358

Browse files
authored
ui: add support for dynamic wallpapers (#294)
- '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
1 parent c6657e7 commit e441358

5 files changed

Lines changed: 143 additions & 15 deletions

File tree

src/config/ConfigManager.cpp

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,42 @@
11
#include "ConfigManager.hpp"
2+
#include <algorithm>
23
#include <filesystem>
34
#include <hyprlang.hpp>
45
#include <hyprutils/path/Path.hpp>
6+
#include <hyprutils/utils/ScopeGuard.hpp>
57
#include <string>
68
#include <sys/ucontext.h>
79
#include "../helpers/Logger.hpp"
810
#include "WallpaperMatcher.hpp"
911

12+
#include <magic.h>
13+
1014
using namespace std::string_literals;
1115

16+
[[nodiscard]] static bool isImage(const std::filesystem::path& path) {
17+
static constexpr std::array exts{".jpg", ".jpeg", ".png", ".bmp", ".webp", ".svg"};
18+
19+
auto ext = path.extension().string();
20+
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
21+
if (std::ranges::any_of(exts, [&ext](const auto& e) { return ext == e; }))
22+
return true;
23+
24+
magic_t magic = magic_open(MAGIC_MIME_TYPE);
25+
if (magic == nullptr)
26+
return false;
27+
28+
Hyprutils::Utils::CScopeGuard guard{[&magic] { magic_close(magic); }};
29+
30+
if (magic_load(magic, nullptr) != 0)
31+
return false;
32+
33+
const auto* result = magic_file(magic, path.string().c_str());
34+
if (result == nullptr)
35+
return false;
36+
37+
return std::string(result).starts_with("image/");
38+
}
39+
1240
static std::string getMainConfigPath() {
1341
static const auto paths = Hyprutils::Path::findConfig("hyprpaper");
1442

@@ -30,6 +58,7 @@ void CConfigManager::init() {
3058
m_config.addSpecialConfigValue("wallpaper", "monitor", Hyprlang::STRING{""});
3159
m_config.addSpecialConfigValue("wallpaper", "path", Hyprlang::STRING{""});
3260
m_config.addSpecialConfigValue("wallpaper", "fit_mode", Hyprlang::STRING{"cover"});
61+
m_config.addSpecialConfigValue("wallpaper", "timeout", Hyprlang::INT{0});
3362

3463
m_config.commence();
3564

@@ -55,19 +84,51 @@ static std::expected<std::string, std::string> resolvePath(const std::string_vie
5584
return CAN;
5685
}
5786

58-
static std::expected<std::string, std::string> getFullPath(const std::string_view& sv) {
59-
if (sv.empty())
87+
static std::expected<std::string, std::string> getPath(const std::string_view& path) {
88+
if (path.empty())
6089
return std::unexpected("empty path");
6190

62-
if (sv[0] == '~') {
91+
if (path[0] == '~') {
6392
static auto HOME = getenv("HOME");
6493
if (!HOME || HOME[0] == '\0')
6594
return std::unexpected("home path but no $HOME");
6695

67-
return resolvePath(std::string{HOME} + "/"s + std::string{sv.substr(1)});
96+
return resolvePath(std::string{HOME} + "/"s + std::string{path.substr(1)});
6897
}
6998

70-
return resolvePath(sv);
99+
return resolvePath(path);
100+
}
101+
102+
static std::expected<std::vector<std::string>, std::string> getFullPath(const std::string& sv) {
103+
if (sv.empty())
104+
return std::unexpected("empty path");
105+
106+
static constexpr const size_t maxImagesCount{1024};
107+
108+
std::vector<std::string> result;
109+
110+
const auto resolved = getPath(sv);
111+
if (!resolved)
112+
return std::unexpected(resolved.error());
113+
114+
const auto resolvedPath = resolved.value();
115+
if (!std::filesystem::exists(resolvedPath))
116+
return std::unexpected(std::format("File '{}' does not exist", resolvedPath));
117+
118+
if (std::filesystem::is_directory(resolvedPath))
119+
for (const auto& entry : std::filesystem::directory_iterator(resolvedPath, std::filesystem::directory_options::skip_permission_denied)) {
120+
if (entry.is_regular_file() && isImage(entry.path()))
121+
result.push_back(entry.path());
122+
123+
if (result.size() >= maxImagesCount)
124+
break;
125+
}
126+
else if (isImage(resolvedPath))
127+
result.push_back(resolvedPath);
128+
else
129+
return std::unexpected(std::format("File '{}' is neither an image nor a directory", resolvedPath));
130+
131+
return result;
71132
}
72133

73134
std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
@@ -78,11 +139,13 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
78139

79140
for (auto& key : keys) {
80141
std::string monitor, fitMode, path;
142+
int timeout;
81143

82144
try {
83145
monitor = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "monitor", key.c_str()));
84146
fitMode = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "fit_mode", key.c_str()));
85147
path = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("wallpaper", "path", key.c_str()));
148+
timeout = std::any_cast<Hyprlang::INT>(m_config.getSpecialConfigValue("wallpaper", "timeout", key.c_str()));
86149
} catch (...) {
87150
g_logger->log(LOG_ERR, "Failed parsing wallpaper for key {}", key);
88151
continue;
@@ -95,7 +158,12 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
95158
continue;
96159
}
97160

98-
result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .path = RESOLVE_PATH.value()});
161+
if (RESOLVE_PATH.value().empty()) {
162+
g_logger->log(LOG_ERR, "Provided path(s) '{}' does not contain a valid image", path);
163+
continue;
164+
}
165+
166+
result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .paths = RESOLVE_PATH.value(), .timeout = timeout});
99167
}
100168

101169
return result;

src/config/ConfigManager.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ class CConfigManager {
1414
CConfigManager(CConfigManager&&) = delete;
1515

1616
struct SSetting {
17-
std::string monitor, fitMode, path;
18-
uint32_t id = 0;
17+
std::string monitor, fitMode;
18+
std::vector<std::string> paths;
19+
int timeout = 0;
20+
uint32_t id = 0;
1921
};
2022

2123
constexpr static const uint32_t SETTING_INVALID = 0;

src/ipc/IPC.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ void CWallpaperObject::apply() {
8585
g_matcher->addState(CConfigManager::SSetting{
8686
.monitor = std::move(m_monitor),
8787
.fitMode = fitModeToStr(m_fitMode),
88-
.path = std::move(m_path),
88+
.paths = std::vector{std::move(m_path)},
8989
});
9090

9191
m_object->sendSuccess();

src/ui/UI.cpp

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "UI.hpp"
2+
#include "../defines.hpp"
23
#include "../helpers/Logger.hpp"
34
#include "../ipc/HyprlandSocket.hpp"
45
#include "../ipc/IPC.hpp"
@@ -12,13 +13,34 @@ CUI::~CUI() {
1213
m_targets.clear();
1314
}
1415

15-
CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::string_view& path, Hyprtoolkit::eImageFitMode fitMode) : m_monitorName(output->port()) {
16+
class CWallpaperTarget::CImagesData {
17+
public:
18+
CImagesData(Hyprtoolkit::eImageFitMode fitMode, const std::vector<std::string>& images, const int timeout = 0) :
19+
fitMode(fitMode), images(images), timeout(timeout > 0 ? timeout : 30) {}
20+
21+
const Hyprtoolkit::eImageFitMode fitMode;
22+
const std::vector<std::string> images;
23+
const int timeout;
24+
25+
std::string nextImage() {
26+
current = (current + 1) % images.size();
27+
return images[current];
28+
}
29+
30+
private:
31+
size_t current = 0;
32+
};
33+
34+
CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoolkit::IOutput> output, const std::vector<std::string>& path, Hyprtoolkit::eImageFitMode fitMode,
35+
const int timeout) : m_monitorName(output->port()), m_backend(backend) {
1636
static const auto SPLASH_REPLY = HyprlandSocket::getFromSocket("/splash");
1737

1838
static const auto PENABLESPLASH = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "splash");
1939
static const auto PSPLASHOFFSET = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(g_config->hyprlang(), "splash_offset");
2040
static const auto PSPLASHALPHA = Hyprlang::CSimpleConfigValue<Hyprlang::FLOAT>(g_config->hyprlang(), "splash_opacity");
2141

42+
ASSERT(path.size() > 0);
43+
2244
m_window = Hyprtoolkit::CWindowBuilder::begin()
2345
->type(Hyprtoolkit::HT_WINDOW_LAYER)
2446
->prefferedOutput(output)
@@ -33,9 +55,10 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
3355
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})
3456
->color([] { return Hyprtoolkit::CHyprColor{0xFF000000}; })
3557
->commence();
36-
m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->commence();
58+
m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->commence();
59+
3760
m_image = Hyprtoolkit::CImageBuilder::begin()
38-
->path(std::string{path})
61+
->path(std::string{path.front()})
3962
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
4063
->sync(true)
4164
->fitMode(fitMode)
@@ -44,6 +67,12 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
4467
m_image->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE);
4568
m_image->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_CENTER, true);
4669

70+
if (path.size() > 1) {
71+
m_imagesData = makeUnique<CImagesData>(fitMode, path, timeout);
72+
m_timer =
73+
m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
74+
}
75+
4776
m_window->m_rootElement->addChild(m_bg);
4877
m_window->m_rootElement->addChild(m_null);
4978
m_null->addChild(m_image);
@@ -68,6 +97,26 @@ CWallpaperTarget::CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::s
6897
m_window->open();
6998
}
7099

100+
CWallpaperTarget::~CWallpaperTarget() {
101+
if (m_timer && !m_timer->passed())
102+
m_timer->cancel();
103+
}
104+
105+
void CWallpaperTarget::onRepeatTimer() {
106+
107+
ASSERT(m_imagesData);
108+
109+
m_image->rebuild()
110+
->path(m_imagesData->nextImage())
111+
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
112+
->sync(true)
113+
->fitMode(m_imagesData->fitMode)
114+
->commence();
115+
116+
m_timer =
117+
m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP<Hyprtoolkit::CTimer> self, void*) { onRepeatTimer(); }, nullptr);
118+
}
119+
71120
void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
72121
g_matcher->registerOutput(mon->port());
73122
if (IPC::g_IPCSocket)
@@ -158,5 +207,5 @@ void CUI::targetChanged(const SP<Hyprtoolkit::IOutput>& mon) {
158207

159208
std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); });
160209

161-
m_targets.emplace_back(makeShared<CWallpaperTarget>(mon, TARGET->get().path, toFitMode(TARGET->get().fitMode)));
210+
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout));
162211
}

src/ui/UI.hpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <vector>
44

55
#include <hyprtoolkit/core/Backend.hpp>
6+
#include <hyprtoolkit/core/Timer.hpp>
67
#include <hyprtoolkit/window/Window.hpp>
78
#include <hyprtoolkit/element/Text.hpp>
89
#include <hyprtoolkit/element/Null.hpp>
@@ -15,8 +16,9 @@
1516

1617
class CWallpaperTarget {
1718
public:
18-
CWallpaperTarget(SP<Hyprtoolkit::IOutput> output, const std::string_view& path, Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER);
19-
~CWallpaperTarget() = default;
19+
CWallpaperTarget(SP<Hyprtoolkit::IBackend> backend, SP<Hyprtoolkit::IOutput> output, const std::vector<std::string>& path,
20+
Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER, const int timeout = 0);
21+
~CWallpaperTarget();
2022

2123
CWallpaperTarget(const CWallpaperTarget&) = delete;
2224
CWallpaperTarget(CWallpaperTarget&) = delete;
@@ -25,6 +27,13 @@ class CWallpaperTarget {
2527
std::string m_monitorName;
2628

2729
private:
30+
void onRepeatTimer();
31+
32+
class CImagesData;
33+
34+
UP<CImagesData> m_imagesData;
35+
ASP<Hyprtoolkit::CTimer> m_timer;
36+
SP<Hyprtoolkit::IBackend> m_backend;
2837
SP<Hyprtoolkit::IWindow> m_window;
2938
SP<Hyprtoolkit::CNullElement> m_null;
3039
SP<Hyprtoolkit::CRectangleElement> m_bg;

0 commit comments

Comments
 (0)