feat(config): re-add source= include directive support

Re-implements the source= directive for including external config files,
which was originally added in PR #267 for v0.7.6 but lost during the
v0.8.0 hyprtoolkit rewrite.

Features:
- Include external config files using source=/path/to/file.conf
- Glob pattern support (e.g., source=~/.config/hypr/hyprpaper.d/*.conf)
- Tilde expansion for home directory paths
- Relative paths resolved relative to the current config file
- Proper error handling and logging for missing/invalid files

This restores parity with Hyprland's source= behavior, enabling modular
configuration management that was lost in the v0.8.0 transition.

Fixes: hyprwm/hyprpaper#302
This commit is contained in:
John Mylchreest 2025-12-31 19:54:34 +00:00
parent e4413583bc
commit 023d7e6e62
No known key found for this signature in database
2 changed files with 98 additions and 0 deletions

View file

@ -1,6 +1,7 @@
#include "ConfigManager.hpp"
#include <algorithm>
#include <filesystem>
#include <glob.h>
#include <hyprlang.hpp>
#include <hyprutils/path/Path.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
@ -37,6 +38,9 @@ using namespace std::string_literals;
return std::string(result).starts_with("image/");
}
// Forward declaration for the source handler
static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE);
static std::string getMainConfigPath() {
static const auto paths = Hyprutils::Path::findConfig("hyprpaper");
@ -60,6 +64,8 @@ void CConfigManager::init() {
m_config.addSpecialConfigValue("wallpaper", "fit_mode", Hyprlang::STRING{"cover"});
m_config.addSpecialConfigValue("wallpaper", "timeout", Hyprlang::INT{0});
m_config.registerHandler(&handleSource, "source", Hyprlang::SHandlerOptions{});
m_config.commence();
auto result = m_config.parse();
@ -168,3 +174,93 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
return result;
}
static std::string absolutePath(const std::string& rawpath, const std::string& currentConfigPath) {
if (rawpath.empty())
return "";
std::filesystem::path path(rawpath);
// Handle tilde expansion
if (!rawpath.empty() && rawpath[0] == '~') {
static auto HOME = getenv("HOME");
if (HOME && HOME[0] != '\0')
path = std::string{HOME} + rawpath.substr(1);
}
// Make relative paths relative to the current config file's directory
if (!path.is_absolute() && !currentConfigPath.empty()) {
auto configDir = std::filesystem::path(currentConfigPath).parent_path();
path = configDir / path;
}
return std::filesystem::absolute(path).string();
}
static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE) {
Hyprlang::CParseResult result;
std::string value = VALUE;
// Trim whitespace
while (!value.empty() && std::isspace(value.front()))
value.erase(value.begin());
while (!value.empty() && std::isspace(value.back()))
value.pop_back();
if (value.empty()) {
result.setError("source= requires a file path");
return result;
}
const auto PATH = absolutePath(value, g_config->getCurrentConfigPath());
if (PATH.empty()) {
result.setError("source= path is empty");
return result;
}
// Support glob patterns
glob_t globResult;
int globStatus = glob(PATH.c_str(), GLOB_TILDE | GLOB_NOSORT, nullptr, &globResult);
if (globStatus == GLOB_NOMATCH) {
globfree(&globResult);
// No glob match - try as a literal path
if (!std::filesystem::exists(PATH)) {
result.setError(std::format("source file '{}' not found", PATH).c_str());
return result;
}
// Parse the single file
auto parseResult = g_config->hyprlang()->parseFile(PATH.c_str());
if (parseResult.error) {
result.setError(std::format("error parsing '{}': {}", PATH, parseResult.getError()).c_str());
}
return result;
}
if (globStatus != 0) {
globfree(&globResult);
result.setError(std::format("glob error for pattern '{}'", PATH).c_str());
return result;
}
// Process all matched files
for (size_t i = 0; i < globResult.gl_pathc; i++) {
const std::string matchedPath = globResult.gl_pathv[i];
if (!std::filesystem::is_regular_file(matchedPath)) {
g_logger->log(LOG_WARN, "source: skipping non-regular file '{}'", matchedPath);
continue;
}
auto parseResult = g_config->hyprlang()->parseFile(matchedPath.c_str());
if (parseResult.error) {
g_logger->log(LOG_ERR, "error parsing '{}': {}", matchedPath, parseResult.getError());
}
}
globfree(&globResult);
return result;
}

View file

@ -27,6 +27,8 @@ class CConfigManager {
std::vector<SSetting> getSettings();
const std::string& getCurrentConfigPath() const { return m_currentConfigPath; }
private:
Hyprlang::CConfig m_config;