feat(config): implement config init/get/set/path commands

This commit is contained in:
Ciphercat 2026-03-05 18:28:24 +00:00
commit d2fa081266
4 changed files with 134 additions and 7 deletions

View file

@ -65,8 +65,20 @@ impl IdxConfig {
if let Ok(no_color) = std::env::var("IDX_NO_COLOR") {
cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true");
}
if let Ok(v) = std::env::var("IDX_CACHE_QUOTE_TTL")
&& let Ok(parsed) = v.parse::<u64>()
{
cfg.quote_ttl = parsed;
}
if let Ok(v) = std::env::var("IDX_CACHE_FUNDAMENTAL_TTL")
&& let Ok(parsed) = v.parse::<u64>()
{
cfg.fundamental_ttl = parsed;
}
cfg.output = cli.output;
if let Some(output) = cli.output {
cfg.output = output;
}
cfg.no_color = cfg.no_color || cli.no_color;
Ok(cfg)
@ -102,12 +114,89 @@ impl IdxConfig {
}
}
pub fn default_config_toml() -> String {
"[general]\nexchange = \"JK\"\noutput = \"table\"\ncolor = true\n\n[cache]\nquote_ttl = 300\nfundamental_ttl = 3600\n".to_string()
}
pub fn config_path() -> Result<PathBuf, IdxError> {
ProjectDirs::from("com", "idx", "idx")
ProjectDirs::from("", "", "idx")
.map(|d| d.config_dir().join("config.toml"))
.ok_or_else(|| IdxError::ConfigError("unable to resolve config dir".to_string()))
}
pub fn ensure_default_config() -> Result<PathBuf, IdxError> {
let path = config_path()?;
if path.exists() {
return Ok(path);
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| IdxError::Io(e.to_string()))?;
}
fs::write(&path, default_config_toml()).map_err(|e| IdxError::Io(e.to_string()))?;
Ok(path)
}
pub fn get_config_value(key: &str) -> Result<Option<String>, IdxError> {
let path = config_path()?;
if !path.exists() {
return Ok(None);
}
let raw = fs::read_to_string(path).map_err(|e| IdxError::Io(e.to_string()))?;
let value: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
let mut cur = &value;
for part in key.split('.') {
let Some(next) = cur.get(part) else {
return Ok(None);
};
cur = next;
}
Ok(Some(cur.to_string().trim_matches('"').to_string()))
}
pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> {
let path = ensure_default_config()?;
let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?;
let mut root: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
let mut parts = key.split('.').peekable();
let mut current = root
.as_table_mut()
.ok_or_else(|| IdxError::ConfigError("config root is not a table".to_string()))?;
while let Some(part) = parts.next() {
if parts.peek().is_none() {
current.insert(part.to_string(), parse_toml_value(value));
} else {
let entry = current
.entry(part.to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
if !entry.is_table() {
*entry = toml::Value::Table(toml::map::Map::new());
}
current = entry
.as_table_mut()
.ok_or_else(|| IdxError::ConfigError("invalid config path".to_string()))?;
}
}
fs::write(&path, toml::to_string_pretty(&root).map_err(|e| IdxError::ConfigError(e.to_string()))?)
.map_err(|e| IdxError::Io(e.to_string()))
}
fn parse_toml_value(value: &str) -> toml::Value {
if let Ok(v) = value.parse::<i64>() {
return toml::Value::Integer(v);
}
if let Ok(v) = value.parse::<f64>() {
return toml::Value::Float(v);
}
if let Ok(v) = value.parse::<bool>() {
return toml::Value::Boolean(v);
}
toml::Value::String(value.to_string())
}
#[cfg(test)]
mod tests {
use super::IdxConfig;