From a129a98c3adb6013ad4848d3884b2f0b49a225a5 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 30 Mar 2026 20:44:56 -0700 Subject: feat: Add 'atuin config' subcommand for reading and setting config values (#3368) Adds a new `atuin config` command with three subcommands for inspecting and modifying `config.toml` without opening an editor. --- crates/atuin-client/src/settings.rs | 116 ++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 13 deletions(-) (limited to 'crates/atuin-client/src/settings.rs') diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index b3359d19..78953320 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -1589,7 +1589,12 @@ impl Settings { Ok(config_file) } - pub fn new() -> Result { + /// Build a merged `Config` from defaults, config file, and environment. + /// + /// This resolves `data_dir`, initializes the data directory on disk, + /// and layers defaults → config file → env overrides. Both `new()` and + /// `get_config_value()` use this so the resolution logic lives in one place. + fn build_config() -> Result { let config_file = Self::get_config_path()?; // extract data_dir first so we can use it as the base for other path defaults @@ -1649,21 +1654,106 @@ impl Settings { config_builder }; - let config = config_builder.build()?; - let mut settings: Settings = config + // all paths should be expanded + let built = config_builder.build_cloned()?; + config_builder = [ + "db_path", + "record_store_path", + "key_path", + "daemon.socket_path", + "daemon.pidfile_path", + "logs.dir", + "logs.search.file", + "logs.daemon.file", + ] + .iter() + .map(|key| (key, built.get_string(key).unwrap_or_default())) + .filter_map(|(key, value)| match Self::expand_path(value) { + Ok(expanded) => Some((key, expanded)), + Err(e) => { + log::warn!("failed to expand path for {key}: {e}"); + None + } + }) + .fold(config_builder, |builder, (key, value)| { + builder + .set_override(key, value) + .unwrap_or_else(|_| panic!("failed to set absolute path override for {key}")) + }); + + config_builder.build().map_err(Into::into) + } + + /// Look up a single config value by dotted key (e.g. `"daemon.sync_frequency"`). + /// + /// Returns the effective value after merging defaults, config file, and + /// environment — without the side-effects of full `Settings` construction + /// (meta store init, path expansion, etc.). + pub fn get_config_value(key: &str) -> Result { + let config = Self::build_config()?; + let value: config::Value = config + .get(key) + .map_err(|e| eyre!("failed to get config value '{}': {}", key, e))?; + Ok(Self::format_resolved_value(&value, key)) + } + + fn format_resolved_value(value: &config::Value, prefix: &str) -> String { + use config::ValueKind; + + match &value.kind { + ValueKind::Nil => String::new(), + ValueKind::Boolean(b) => b.to_string(), + ValueKind::I64(i) => i.to_string(), + ValueKind::I128(i) => i.to_string(), + ValueKind::U64(u) => u.to_string(), + ValueKind::U128(u) => u.to_string(), + ValueKind::Float(f) => f.to_string(), + ValueKind::String(s) => s.clone(), + ValueKind::Array(arr) => { + let items: Vec = arr + .iter() + .map(|v| Self::format_resolved_value(v, "")) + .collect(); + format!("[{}]", items.join(", ")) + } + ValueKind::Table(map) => { + let mut lines = Vec::new(); + let mut keys: Vec<_> = map.keys().collect(); + keys.sort(); + + for k in keys { + let v = &map[k]; + let full_key = if prefix.is_empty() { + k.clone() + } else { + format!("{}.{}", prefix, k) + }; + + match &v.kind { + ValueKind::Table(_) => { + lines.push(Self::format_resolved_value(v, &full_key)); + } + _ => { + lines.push(format!( + "{} = {}", + full_key, + Self::format_resolved_value(v, "") + )); + } + } + } + + lines.join("\n") + } + } + } + + pub fn new() -> Result { + let config = Self::build_config()?; + let settings: Settings = config .try_deserialize() .map_err(|e| eyre!("failed to deserialize: {}", e))?; - // all paths should be expanded - settings.db_path = Self::expand_path(settings.db_path)?; - settings.record_store_path = Self::expand_path(settings.record_store_path)?; - settings.key_path = Self::expand_path(settings.key_path)?; - settings.daemon.socket_path = Self::expand_path(settings.daemon.socket_path)?; - settings.daemon.pidfile_path = Self::expand_path(settings.daemon.pidfile_path)?; - settings.logs.dir = Self::expand_path(settings.logs.dir)?; - settings.logs.search.file = Self::expand_path(settings.logs.search.file)?; - settings.logs.daemon.file = Self::expand_path(settings.logs.daemon.file)?; - // Validate UI settings settings.ui.validate()?; -- cgit v1.3.1