diff --git a/README.md b/README.md index 80e097a..d886dfa 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ preload = /path/to/next_image.png wallpaper = monitor1,/path/to/image.png #if more than one monitor in use, can load a 2nd image wallpaper = monitor2,/path/to/next_image.png +# you can also specify a directory path to randomly select a wallpaper from +wallpaper = monitor3,/path/to/directory # .. more monitors #enable splash text rendering over the wallpaper @@ -88,6 +90,9 @@ splash = true #fully disable ipc # ipc = off +#enable deferred preloading (reduces memory usage but may cause slight delay when switching wallpapers) +# defer_preload = 1 + ``` diff --git a/src/Hyprpaper.cpp b/src/Hyprpaper.cpp index 1825288..4f1ea86 100644 --- a/src/Hyprpaper.cpp +++ b/src/Hyprpaper.cpp @@ -3,6 +3,8 @@ #include #include #include +#include "helpers/RandomGenerator.hpp" +#include "helpers/ImagePicker.hpp" CHyprpaper::CHyprpaper() = default; @@ -152,35 +154,66 @@ void CHyprpaper::unloadWallpaper(const std::string& path) { } void CHyprpaper::preloadAllWallpapersFromConfig() { + if (std::any_cast(g_pConfigManager->config->getConfigValue("defer_preload"))) { + g_pConfigManager->m_dRequestedPreloads.clear(); + return; + } + if (g_pConfigManager->m_dRequestedPreloads.empty()) return; for (auto& wp : g_pConfigManager->m_dRequestedPreloads) { + // For directory-based preloads, always reselect a random file. + // For files, we'll skip if already loaded. + bool shouldSkip = !std::filesystem::is_directory(wp); - // check if it doesnt exist - bool exists = false; - for (auto& [ewp, cls] : m_mWallpaperTargets) { - if (ewp == wp) { - Debug::log(LOG, "Ignoring request to preload {} as it already is preloaded!", ewp); - exists = true; - break; + if (std::filesystem::is_directory(wp)) { + // Always unload previous wallpaper from this directory + std::vector toUnload; + for (auto& [loadedPath, target] : m_mWallpaperTargets) { + std::filesystem::path pLoaded(loadedPath); + if (pLoaded.parent_path() == std::filesystem::path(wp)) { + toUnload.push_back(loadedPath); + break; // Only unload one wallpaper per directory + } } + for (auto& path : toUnload) { + unloadWallpaper(path); + } + + // Use our helper to pick a random image. + std::string randomImage = getRandomImageFromDirectory(wp); + if (randomImage.empty()) { + Debug::log(LOG, "No valid images in directory {}", wp); + continue; + } + wp = randomImage; } - if (exists) - continue; + // For non-directory requests, skip if already loaded. + if (shouldSkip) { + bool exists = false; + for (auto& [ewp, cls] : m_mWallpaperTargets) { + if (ewp == wp) { + Debug::log(LOG, "Ignoring request to preload {} as it already is preloaded!", ewp); + exists = true; + break; + } + } + if (exists) + continue; + } m_mWallpaperTargets[wp] = CWallpaperTarget(); if (std::filesystem::is_symlink(wp)) { - auto real_wp = std::filesystem::read_symlink(wp); + auto real_wp = std::filesystem::read_symlink(wp); std::filesystem::path absolute_path = std::filesystem::path(wp).parent_path() / real_wp; - absolute_path = absolute_path.lexically_normal(); - m_mWallpaperTargets[wp].create(absolute_path); + absolute_path = absolute_path.lexically_normal(); + m_mWallpaperTargets[wp].create(absolute_path.string()); } else { m_mWallpaperTargets[wp].create(wp); } } - g_pConfigManager->m_dRequestedPreloads.clear(); } @@ -274,15 +307,15 @@ SMonitor* CHyprpaper::getMonitorFromName(const std::string& monname) { void CHyprpaper::ensurePoolBuffersPresent() { bool anyNewBuffers = false; - for (auto& [file, wt] : m_mWallpaperTargets) { + for (auto& [path, wt] : m_mWallpaperTargets) { for (auto& m : m_vMonitors) { if (m->size == Vector2D()) continue; - auto it = std::find_if(m_vBuffers.begin(), m_vBuffers.end(), [wt = &wt, &m](const std::unique_ptr& el) { + auto it = std::find_if(m_vBuffers.begin(), m_vBuffers.end(), [&](const std::unique_ptr& el) { auto scale = std::round((m->pCurrentLayerSurface && m->pCurrentLayerSurface->pFractionalScaleInfo ? m->pCurrentLayerSurface->fScale : m->scale) * 120.0) / 120.0; - return el->target == wt->m_szPath && vectorDeltaLessThan(el->pixelSize, m->size * scale, 1); + return el->target == path && vectorDeltaLessThan(el->pixelSize, m->size * scale, 1); }); if (it == m_vBuffers.end()) { @@ -291,9 +324,9 @@ void CHyprpaper::ensurePoolBuffersPresent() { auto scale = std::round((m->pCurrentLayerSurface && m->pCurrentLayerSurface->pFractionalScaleInfo ? m->pCurrentLayerSurface->fScale : m->scale) * 120.0) / 120.0; createBuffer(PBUFFER, m->size.x * scale, m->size.y * scale, WL_SHM_FORMAT_ARGB8888); - PBUFFER->target = wt.m_szPath; + PBUFFER->target = path; - Debug::log(LOG, "Buffer created for target {}, Shared Memory usage: {:.1f}MB", wt.m_szPath, PBUFFER->size / 1000000.f); + Debug::log(LOG, "Buffer created for target {}, Shared Memory usage: {:.1f}MB", path, PBUFFER->size / 1000000.f); anyNewBuffers = true; } diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 6e6901a..238c6ae 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -2,54 +2,87 @@ #include "../Hyprpaper.hpp" #include #include +#include "../helpers/RandomGenerator.hpp" +#include "../helpers/ImagePicker.hpp" + +// Utility function to safely expand a tilde at the beginning of a path. +static std::string expandTilde(const std::string& path) { + if (!path.empty() && path[0] == '~') { + if (const char* home = getenv("HOME")) + return std::string(home) + path.substr(1); + } + return path; +} static Hyprlang::CParseResult handleWallpaper(const char* C, const char* V) { - const std::string COMMAND = C; - const std::string VALUE = V; + const std::string COMMAND = C; + const std::string VALUE = V; Hyprlang::CParseResult result; - if (VALUE.find_first_of(',') == std::string::npos) { + // Cache the position of the comma delimiter. + auto delimPos = VALUE.find_first_of(','); + if (delimPos == std::string::npos) { result.setError("wallpaper failed (syntax)"); return result; } - auto MONITOR = VALUE.substr(0, VALUE.find_first_of(',')); - auto WALLPAPER = g_pConfigManager->trimPath(VALUE.substr(VALUE.find_first_of(',') + 1)); + auto MONITOR = VALUE.substr(0, delimPos); + auto WALLPAPER = g_pConfigManager->trimPath(VALUE.substr(delimPos + 1)); bool contain = false; - if (WALLPAPER.find("contain:") == 0) { WALLPAPER = WALLPAPER.substr(8); contain = true; } bool tile = false; - if (WALLPAPER.find("tile:") == 0) { WALLPAPER = WALLPAPER.substr(5); tile = true; } - if (WALLPAPER[0] == '~') { - static const char* const ENVHOME = getenv("HOME"); - WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); - } + // Use the helper to expand '~' + WALLPAPER = expandTilde(WALLPAPER); std::error_code ec; - if (!std::filesystem::exists(WALLPAPER, ec)) { - result.setError((std::string{"wallpaper failed ("} + (ec ? ec.message() : std::string{"no such file"}) + std::string{": "} + WALLPAPER + std::string{")"}).c_str()); + result.setError(((ec ? ec.message() : std::string{"no such file"}) + std::string{": "} + WALLPAPER).c_str()); return result; } - if (std::find(g_pConfigManager->m_dRequestedPreloads.begin(), g_pConfigManager->m_dRequestedPreloads.end(), WALLPAPER) == g_pConfigManager->m_dRequestedPreloads.end() && - !g_pHyprpaper->isPreloaded(WALLPAPER)) { - result.setError("wallpaper failed (not preloaded)"); - return result; + // If it's a directory, always force a reload + if (std::filesystem::is_directory(WALLPAPER)) { + std::string currentWallpaper; + // First get the current wallpaper if it exists + for (auto& [loadedPath, target] : g_pHyprpaper->m_mWallpaperTargets) { + if (std::filesystem::path(loadedPath).parent_path() == std::filesystem::path(WALLPAPER)) { + currentWallpaper = loadedPath; + g_pHyprpaper->unloadWallpaper(loadedPath); + break; + } + } + + // Use our helper with an exclusion to avoid reselecting the same image. + std::string randomImage = getRandomImageFromDirectory(WALLPAPER, currentWallpaper); + if (randomImage.empty()) { + result.setError("No valid images in directory"); + return result; + } + WALLPAPER = randomImage; + } + + if (!g_pHyprpaper->isPreloaded(WALLPAPER)) { + if (std::any_cast(g_pConfigManager->config->getConfigValue("defer_preload"))) { + g_pHyprpaper->m_mWallpaperTargets[WALLPAPER] = CWallpaperTarget(); + g_pHyprpaper->m_mWallpaperTargets[WALLPAPER].create(WALLPAPER); + } else { + result.setError("wallpaper failed (not preloaded)"); + return result; + } } g_pHyprpaper->clearWallpaperFromMonitor(MONITOR); - g_pHyprpaper->m_mMonitorActiveWallpapers[MONITOR] = WALLPAPER; + g_pHyprpaper->m_mMonitorActiveWallpapers[MONITOR] = WALLPAPER; g_pHyprpaper->m_mMonitorWallpaperRenderData[MONITOR].contain = contain; g_pHyprpaper->m_mMonitorWallpaperRenderData[MONITOR].tile = tile; @@ -57,7 +90,7 @@ static Hyprlang::CParseResult handleWallpaper(const char* C, const char* V) { for (auto& m : g_pHyprpaper->m_vMonitors) { if (!m->hasATarget || m->wildcard) { g_pHyprpaper->clearWallpaperFromMonitor(m->name); - g_pHyprpaper->m_mMonitorActiveWallpapers[m->name] = WALLPAPER; + g_pHyprpaper->m_mMonitorActiveWallpapers[m->name] = WALLPAPER; g_pHyprpaper->m_mMonitorWallpaperRenderData[m->name].contain = contain; g_pHyprpaper->m_mMonitorWallpaperRenderData[m->name].tile = tile; } @@ -76,13 +109,10 @@ static Hyprlang::CParseResult handlePreload(const char* C, const char* V) { const std::string VALUE = V; auto WALLPAPER = VALUE; - if (WALLPAPER[0] == '~') { - static const char* const ENVHOME = getenv("HOME"); - WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); - } + // Use the helper function for tilde expansion. + WALLPAPER = expandTilde(WALLPAPER); std::error_code ec; - if (!std::filesystem::exists(WALLPAPER, ec)) { Hyprlang::CParseResult result; result.setError(((ec ? ec.message() : std::string{"no such file"}) + std::string{": "} + WALLPAPER).c_str()); @@ -130,10 +160,8 @@ static Hyprlang::CParseResult handleUnload(const char* C, const char* V) { if (VALUE == "all" || VALUE == "unused") return handleUnloadAll(C, V); - if (WALLPAPER[0] == '~') { - static const char* const ENVHOME = getenv("HOME"); - WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); - } + // Use the helper function for tilde expansion. + WALLPAPER = expandTilde(WALLPAPER); g_pHyprpaper->unloadWallpaper(WALLPAPER); @@ -189,6 +217,7 @@ CConfigManager::CConfigManager() { config->addConfigValue("splash", Hyprlang::INT{0L}); config->addConfigValue("splash_offset", Hyprlang::FLOAT{2.F}); config->addConfigValue("splash_color", Hyprlang::INT{0x55ffffff}); + config->addConfigValue("defer_preload", Hyprlang::INT{0L}); config->registerHandler(&handleWallpaper, "wallpaper", {.allowFlags = false}); config->registerHandler(&handleUnload, "unload", {.allowFlags = false}); diff --git a/src/helpers/ImagePicker.cpp b/src/helpers/ImagePicker.cpp new file mode 100644 index 0000000..3d1c74d --- /dev/null +++ b/src/helpers/ImagePicker.cpp @@ -0,0 +1,33 @@ +#include "ImagePicker.hpp" +#include "RandomGenerator.hpp" +#include +#include +#include + +std::string getRandomImageFromDirectory(const std::string &dirPath, const std::string &ignore) { + std::vector images; + for (const auto &entry : std::filesystem::directory_iterator(dirPath)) { + if (!entry.is_regular_file()) + continue; + + auto ext = entry.path().extension().string(); + if (ext != ".png" && ext != ".jpg" && ext != ".jpeg" && + ext != ".webp" && ext != ".jxl") + continue; + + if (!ignore.empty() && entry.path().string() == ignore) + continue; + + images.push_back(entry.path()); + } + + if (images.empty()) { + // If no alternative was found but the ignored file exists, return it. + if (!ignore.empty() && std::filesystem::exists(ignore)) + return ignore; + return ""; + } + + std::shuffle(images.begin(), images.end(), CRandomGenerator::get().getGenerator()); + return images.front().string(); +} diff --git a/src/helpers/ImagePicker.hpp b/src/helpers/ImagePicker.hpp new file mode 100644 index 0000000..7df300f --- /dev/null +++ b/src/helpers/ImagePicker.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +// Returns a random image path from the given directory. +// If 'ignore' is provided, that file (if present) will be excluded from the selection. +// Returns an empty string if no valid image is found. +std::string getRandomImageFromDirectory(const std::string &dirPath, const std::string &ignore = ""); diff --git a/src/helpers/RandomGenerator.hpp b/src/helpers/RandomGenerator.hpp new file mode 100644 index 0000000..89e855b --- /dev/null +++ b/src/helpers/RandomGenerator.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +class CRandomGenerator { +public: + CRandomGenerator() : mGenerator(mRandomDevice()) {} + + // Generate random index for vector size + size_t getRandomIndex(size_t size) { + if (size == 0) return 0; + std::uniform_int_distribution distribution(0, size - 1); + return distribution(mGenerator); + } + + // Get raw generator if needed + std::mt19937& getGenerator() { + return mGenerator; + } + +private: + std::random_device mRandomDevice; + std::mt19937 mGenerator; +}; + +// Global pointer to random generator +inline std::unique_ptr gRandomGenerator; \ No newline at end of file