feat: TOML parsing

implement TOML parsing
implement XDG config path discovery
fallback to ~/.config
This commit is contained in:
2026-06-02 01:02:08 +03:00
parent 4cb2b2726d
commit 7aaee83654
+216 -11
View File
@@ -142,6 +142,197 @@ Config defaultConfig() {
return cfg; return cfg;
} }
static bool getenvNonEmpty(const char* name, string& out) {
const char* v = std::getenv(name);
if (!v || !*v) return false;
out = v;
return true;
}
static fs::path defaultConfigPath() {
// XDG base dir spec: $XDG_CONFIG_HOME, fallback to ~/.config.
// We keep it as a single file so users can drop in one config.
string xdg;
if (getenvNonEmpty("XDG_CONFIG_HOME", xdg)) {
return fs::path(xdg) / "fetchit" / "config.toml";
}
string home;
if (getenvNonEmpty("HOME", home)) {
return fs::path(home) / ".config" / "fetchit" / "config.toml";
}
return {};
}
static fs::path findExistingConfigPath() {
const fs::path p = defaultConfigPath();
if (p.empty()) return {};
std::error_code ec;
if (fs::exists(p, ec) && !ec) return p;
return {};
}
static string toLowerAscii(string s) {
for (char& c : s) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return s;
}
static bool parseColorName(const string& name, Color& out) {
const string n = toLowerAscii(name);
// Keep this small and aligned with Color enum values used by fetchit.
if (n == "red") {
out = Color::Red;
return true;
}
if (n == "green") {
out = Color::Green;
return true;
}
if (n == "blue") {
out = Color::Blue;
return true;
}
if (n == "yellow") {
out = Color::Yellow;
return true;
}
if (n == "magenta") {
out = Color::Magenta;
return true;
}
if (n == "purple") {
out = Color::Purple;
return true;
}
if (n == "cyan") {
out = Color::Cyan;
return true;
}
if (n == "gray" || n == "grey") {
out = Color::Gray;
return true;
}
if (n == "dark") {
out = Color::Dark;
return true;
}
return false;
}
static Config loadConfigOrDefault(vector<string>& warnings) {
warnings.clear();
const fs::path configPath = findExistingConfigPath();
if (configPath.empty()) return defaultConfig();
// Start from defaults and apply overrides.
Config cfg = defaultConfig();
toml::table tbl;
try {
tbl = toml::parse_file(configPath.string());
} catch (const toml::parse_error& err) {
std::ostringstream oss;
oss << "fetchit: warning: invalid config " << configPath.string() << ": " << err << "; using defaults";
warnings.push_back(oss.str());
return defaultConfig();
}
// modules = ["distro", ...]
if (auto modulesArr = tbl["modules"].as_array()) {
vector<Module> modules;
modules.reserve(modulesArr->size());
for (const auto& node : *modulesArr) {
const auto* s = node.as_string();
if (!s) {
warnings.push_back("fetchit: warning: modules[] must be strings; ignoring non-string entry");
continue;
}
const string name = s->get();
const ModuleSpec* spec = findModuleSpec(name);
if (!spec) {
warnings.push_back("fetchit: warning: unknown module '" + name + "'; ignoring");
continue;
}
modules.push_back(spec->id);
}
if (modules.empty()) {
warnings.push_back("fetchit: warning: modules list is empty after validation; using defaults");
} else {
cfg.modules = std::move(modules);
}
} else if (tbl.contains("modules")) {
warnings.push_back("fetchit: warning: modules must be an array of strings; using defaults");
}
// [logo]
if (auto logoTbl = tbl["logo"].as_table()) {
if (auto enabled = (*logoTbl)["enabled"].value<bool>()) {
cfg.logoEnabled = *enabled;
} else if (logoTbl->contains("enabled")) {
warnings.push_back("fetchit: warning: logo.enabled must be a boolean; using default");
}
} else if (tbl.contains("logo")) {
warnings.push_back("fetchit: warning: logo must be a table; ignoring");
}
// [labels]
if (auto labelsTbl = tbl["labels"].as_table()) {
for (auto&& [k, v] : *labelsTbl) {
const string key = string(k.str());
const ModuleSpec* spec = findModuleSpec(key);
if (!spec) {
warnings.push_back("fetchit: warning: unknown labels key '" + key + "'; ignoring");
continue;
}
auto* s = v.as_string();
if (!s) {
warnings.push_back("fetchit: warning: labels." + key + " must be a string; ignoring");
continue;
}
cfg.labels[spec->id] = s->get();
}
} else if (tbl.contains("labels")) {
warnings.push_back("fetchit: warning: labels must be a table; ignoring");
}
// [colors]
if (auto colorsTbl = tbl["colors"].as_table()) {
for (auto&& [k, v] : *colorsTbl) {
const string key = string(k.str());
const ModuleSpec* spec = findModuleSpec(key);
if (!spec) {
warnings.push_back("fetchit: warning: unknown colors key '" + key + "'; ignoring");
continue;
}
auto* s = v.as_string();
if (!s) {
warnings.push_back("fetchit: warning: colors." + key + " must be a string; ignoring");
continue;
}
Color c;
const string colorName = s->get();
if (!parseColorName(colorName, c)) {
warnings.push_back("fetchit: warning: unknown color name '" + colorName + "' for colors." + key
+ "; ignoring");
continue;
}
cfg.colors[spec->id] = c;
}
} else if (tbl.contains("colors")) {
warnings.push_back("fetchit: warning: colors must be a table; ignoring");
}
return cfg;
}
struct DistroLogo { struct DistroLogo {
string id; string id;
Color color; Color color;
@@ -176,6 +367,12 @@ vector<gpuId> getGpuIds() {
int main () { int main () {
std::setlocale(LC_ALL, ""); std::setlocale(LC_ALL, "");
vector<string> configWarnings;
const Config config = loadConfigOrDefault(configWarnings);
for (const auto& w : configWarnings) {
std::cerr << w << "\n";
}
auto displayWidth = [&](const string& text) { auto displayWidth = [&](const string& text) {
std::mbstate_t state{}; std::mbstate_t state{};
const char* src = text.c_str(); const char* src = text.c_str();
@@ -208,18 +405,26 @@ int main () {
return color(c) + padded + color(Color::Reset) + value; return color(c) + padded + color(Color::Reset) + value;
}; };
vector<string> infoLines = { vector<string> infoLines;
formatLine(Color::Green, " distro:", getDistro()), infoLines.reserve(config.modules.size());
formatLine(Color::Magenta, " kernel:", getKernel()), for (Module m : config.modules) {
formatLine(Color::Blue, " uptime:", getUptime()), const ModuleSpec* spec = findModuleSpec(m);
formatLine(Color::Magenta, " shell:", getShell()), if (!spec) continue;
formatLine(Color::Yellow, "󰍛 CPU:", getCPU()),
formatLine(Color::Yellow, "󰾲 GPU:", getGPU()),
formatLine(Color::Red, " RAM:", getRAM()),
formatLine(Color::Blue, " OS Date:", getOsDate())
};
vector<string> logoLines = distroArt(); auto labelIt = config.labels.find(m);
const string& label = (labelIt != config.labels.end()) ? labelIt->second : spec->defaultLabel;
auto colorIt = config.colors.find(m);
const Color c = (colorIt != config.colors.end()) ? colorIt->second : spec->defaultColor;
const string value = spec->getter ? spec->getter() : string{};
infoLines.push_back(formatLine(c, label, value));
}
vector<string> logoLines;
if (config.logoEnabled) {
logoLines = distroArt();
}
size_t logoWidth = 0; size_t logoWidth = 0;
for (const auto& line : logoLines) { for (const auto& line : logoLines) {
if (line.size() > logoWidth) logoWidth = line.size(); if (line.size() > logoWidth) logoWidth = line.size();