Skip to content

Commit 1f8d1fc

Browse files
authored
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: #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
1 parent 9271162 commit 1f8d1fc

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

src/config/ConfigManager.cpp

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#include "ConfigManager.hpp"
22
#include <algorithm>
33
#include <filesystem>
4+
#include <glob.h>
45
#include <hyprlang.hpp>
56
#include <hyprutils/path/Path.hpp>
7+
#include <hyprutils/string/String.hpp>
68
#include <hyprutils/utils/ScopeGuard.hpp>
79
#include <string>
8-
#include <sys/ucontext.h>
910
#include "../helpers/Logger.hpp"
1011
#include "WallpaperMatcher.hpp"
1112

@@ -37,7 +38,10 @@ using namespace std::string_literals;
3738
return std::string(result).starts_with("image/");
3839
}
3940

40-
static std::string getMainConfigPath() {
41+
// Forward declaration for the source handler
42+
static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE);
43+
44+
static std::string getMainConfigPath() {
4145
static const auto paths = Hyprutils::Path::findConfig("hyprpaper");
4246

4347
return paths.first.value_or("");
@@ -60,6 +64,8 @@ bool CConfigManager::init() {
6064
m_config.addSpecialConfigValue("wallpaper", "fit_mode", Hyprlang::STRING{"cover"});
6165
m_config.addSpecialConfigValue("wallpaper", "timeout", Hyprlang::INT{0});
6266

67+
m_config.registerHandler(&handleSource, "source", Hyprlang::SHandlerOptions{});
68+
6369
m_config.commence();
6470

6571
auto result = m_config.parse();
@@ -77,6 +83,10 @@ Hyprlang::CConfig* CConfigManager::hyprlang() {
7783
return &m_config;
7884
}
7985

86+
const std::string& CConfigManager::getCurrentConfigPath() const {
87+
return m_currentConfigPath;
88+
}
89+
8090
static std::expected<std::string, std::string> resolvePath(const std::string_view& sv) {
8191
std::error_code ec;
8292
const auto CAN = std::filesystem::canonical(sv, ec);
@@ -87,16 +97,22 @@ static std::expected<std::string, std::string> resolvePath(const std::string_vie
8797
return CAN;
8898
}
8999

90-
static std::expected<std::string, std::string> getPath(const std::string_view& path) {
91-
if (path.empty())
100+
static std::expected<std::string, std::string> getPath(const std::string_view& sv, const std::string& basePath = "") {
101+
if (sv.empty())
92102
return std::unexpected("empty path");
93103

94-
if (path[0] == '~') {
104+
std::string path{sv};
105+
106+
if (sv[0] == '~') {
95107
static auto HOME = getenv("HOME");
96108
if (!HOME || HOME[0] == '\0')
97109
return std::unexpected("home path but no $HOME");
98110

99-
return resolvePath(std::string{HOME} + "/"s + std::string{path.substr(1)});
111+
path = std::string{HOME} + "/"s + std::string{sv.substr(1)};
112+
} else if (!std::filesystem::path(sv).is_absolute() && !basePath.empty()) {
113+
// Make relative paths relative to the base path's directory
114+
auto baseDir = std::filesystem::path(basePath).parent_path();
115+
path = (baseDir / sv).string();
100116
}
101117

102118
return resolvePath(path);
@@ -171,3 +187,73 @@ std::vector<CConfigManager::SSetting> CConfigManager::getSettings() {
171187

172188
return result;
173189
}
190+
191+
static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE) {
192+
Hyprlang::CParseResult result;
193+
194+
const auto value = Hyprutils::String::trim(VALUE);
195+
196+
if (value.empty()) {
197+
result.setError("source= requires a file path");
198+
return result;
199+
}
200+
201+
const auto RESOLVED_PATH = getPath(value, g_config->getCurrentConfigPath());
202+
203+
if (!RESOLVED_PATH) {
204+
result.setError(std::format("source= path error: {}", RESOLVED_PATH.error()).c_str());
205+
return result;
206+
}
207+
208+
const auto& PATH = RESOLVED_PATH.value();
209+
210+
g_logger->log(LOG_DEBUG, "source: including '{}'", PATH);
211+
212+
// Support glob patterns
213+
glob_t globResult;
214+
Hyprutils::Utils::CScopeGuard scopeGuard([&globResult]() { globfree(&globResult); });
215+
216+
int globStatus = glob(PATH.c_str(), GLOB_TILDE | GLOB_NOSORT, nullptr, &globResult);
217+
218+
if (globStatus == GLOB_NOMATCH) {
219+
// No glob match - try as a literal path
220+
std::error_code ec;
221+
const auto exists = std::filesystem::exists(PATH, ec);
222+
if (ec || !exists) {
223+
result.setError(std::format("source file '{}' not found", PATH).c_str());
224+
return result;
225+
}
226+
227+
// Parse the single file
228+
g_logger->log(LOG_DEBUG, "source: parsing file '{}'", PATH);
229+
auto parseResult = g_config->hyprlang()->parseFile(PATH.c_str());
230+
if (parseResult.error)
231+
result.setError(std::format("error parsing '{}': {}", PATH, parseResult.getError()).c_str());
232+
return result;
233+
}
234+
235+
if (globStatus != 0) {
236+
result.setError(std::format("glob error for pattern '{}'", PATH).c_str());
237+
return result;
238+
}
239+
240+
// Process all matched files
241+
g_logger->log(LOG_DEBUG, "source: glob matched {} file(s)", globResult.gl_pathc);
242+
for (size_t i = 0; i < globResult.gl_pathc; i++) {
243+
const std::string matchedPath = globResult.gl_pathv[i];
244+
245+
std::error_code ec;
246+
const auto isFile = std::filesystem::is_regular_file(matchedPath, ec);
247+
if (ec || !isFile) {
248+
g_logger->log(LOG_WARN, "source: skipping non-regular file '{}'", matchedPath);
249+
continue;
250+
}
251+
252+
g_logger->log(LOG_DEBUG, "source: parsing file '{}'", matchedPath);
253+
auto parseResult = g_config->hyprlang()->parseFile(matchedPath.c_str());
254+
if (parseResult.error)
255+
g_logger->log(LOG_ERR, "error parsing '{}': {}", matchedPath, parseResult.getError());
256+
}
257+
258+
return result;
259+
}

src/config/ConfigManager.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class CConfigManager {
2727

2828
std::vector<SSetting> getSettings();
2929

30+
const std::string& getCurrentConfigPath() const;
31+
3032
private:
3133
Hyprlang::CConfig m_config;
3234

0 commit comments

Comments
 (0)