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

* 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

* feat(config): add debug logging for source= directive

Adds LOG_DEBUG calls to help troubleshoot config include issues:
- Log when source= directive is encountered with resolved path
- Log number of files matched by glob patterns
- Log each file before parsing

* refactor(config): address PR review feedback & rebase against main

- Use Hyprutils::String::trim() instead of manual whitespace trimming
- Use Hyprutils::Utils::CScopeGuard for glob cleanup instead of manual
  globfree() calls
- Remove braces from short single-line if statements per style guide
- Remove duplicate absolutePath(), extend getPath() with optional
  basePath parameter instead

For source= directives, relative paths need to resolve relative to the
config file's directory, not CWD. So `source = ./themes/dark.conf` in
`~/.config/hypr/hyprpaper.conf` resolves to
`~/.config/hypr/themes/dark.conf`, not `$CWD/themes/dark.conf`.

* fix(config): address PR review feedback

- Use std::error_code for filesystem calls to avoid exceptions
- Move getCurrentConfigPath() body from header to cpp file
- Apply clang-format
This commit is contained in:
John Mylchreest 2026-01-02 21:16:43 +00:00 committed by GitHub
parent 9271162ef0
commit 1f8d1fc0ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 94 additions and 6 deletions

View file

@ -1,11 +1,12 @@
#include "ConfigManager.hpp"
#include <algorithm>
#include <filesystem>
#include <glob.h>
#include <hyprlang.hpp>
#include <hyprutils/path/Path.hpp>
#include <hyprutils/string/String.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <string>
#include <sys/ucontext.h>
#include "../helpers/Logger.hpp"
#include "WallpaperMatcher.hpp"
@ -37,7 +38,10 @@ using namespace std::string_literals;
return std::string(result).starts_with("image/");
}
static std::string getMainConfigPath() {
// 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");
return paths.first.value_or("");
@ -60,6 +64,8 @@ bool 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();
@ -77,6 +83,10 @@ Hyprlang::CConfig* CConfigManager::hyprlang() {
return &m_config;
}
const std::string& CConfigManager::getCurrentConfigPath() const {
return m_currentConfigPath;
}
static std::expected<std::string, std::string> resolvePath(const std::string_view& sv) {
std::error_code ec;
const auto CAN = std::filesystem::canonical(sv, ec);
@ -87,16 +97,22 @@ static std::expected<std::string, std::string> resolvePath(const std::string_vie
return CAN;
}
static std::expected<std::string, std::string> getPath(const std::string_view& path) {
if (path.empty())
static std::expected<std::string, std::string> getPath(const std::string_view& sv, const std::string& basePath = "") {
if (sv.empty())
return std::unexpected("empty path");
if (path[0] == '~') {
std::string path{sv};
if (sv[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{path.substr(1)});
path = std::string{HOME} + "/"s + std::string{sv.substr(1)};
} else if (!std::filesystem::path(sv).is_absolute() && !basePath.empty()) {
// Make relative paths relative to the base path's directory
auto baseDir = std::filesystem::path(basePath).parent_path();
path = (baseDir / sv).string();
}
return resolvePath(path);
@ -171,3 +187,73 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
return result;
}
static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE) {
Hyprlang::CParseResult result;
const auto value = Hyprutils::String::trim(VALUE);
if (value.empty()) {
result.setError("source= requires a file path");
return result;
}
const auto RESOLVED_PATH = getPath(value, g_config->getCurrentConfigPath());
if (!RESOLVED_PATH) {
result.setError(std::format("source= path error: {}", RESOLVED_PATH.error()).c_str());
return result;
}
const auto& PATH = RESOLVED_PATH.value();
g_logger->log(LOG_DEBUG, "source: including '{}'", PATH);
// Support glob patterns
glob_t globResult;
Hyprutils::Utils::CScopeGuard scopeGuard([&globResult]() { globfree(&globResult); });
int globStatus = glob(PATH.c_str(), GLOB_TILDE | GLOB_NOSORT, nullptr, &globResult);
if (globStatus == GLOB_NOMATCH) {
// No glob match - try as a literal path
std::error_code ec;
const auto exists = std::filesystem::exists(PATH, ec);
if (ec || !exists) {
result.setError(std::format("source file '{}' not found", PATH).c_str());
return result;
}
// Parse the single file
g_logger->log(LOG_DEBUG, "source: parsing file '{}'", PATH);
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) {
result.setError(std::format("glob error for pattern '{}'", PATH).c_str());
return result;
}
// Process all matched files
g_logger->log(LOG_DEBUG, "source: glob matched {} file(s)", globResult.gl_pathc);
for (size_t i = 0; i < globResult.gl_pathc; i++) {
const std::string matchedPath = globResult.gl_pathv[i];
std::error_code ec;
const auto isFile = std::filesystem::is_regular_file(matchedPath, ec);
if (ec || !isFile) {
g_logger->log(LOG_WARN, "source: skipping non-regular file '{}'", matchedPath);
continue;
}
g_logger->log(LOG_DEBUG, "source: parsing file '{}'", matchedPath);
auto parseResult = g_config->hyprlang()->parseFile(matchedPath.c_str());
if (parseResult.error)
g_logger->log(LOG_ERR, "error parsing '{}': {}", matchedPath, parseResult.getError());
}
return result;
}

View file

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