feat: TOML parsing
implement TOML parsing implement XDG config path discovery fallback to ~/.config
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user