diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-03-30 20:44:56 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-31 04:44:56 +0100 |
| commit | a129a98c3adb6013ad4848d3884b2f0b49a225a5 (patch) | |
| tree | ddfdaa0d97f69e0dcdf939bafcb1b5cb90fd1bf3 /crates/atuin-client/src | |
| parent | feat: opt-in to sharing last command with ai (#3367) (diff) | |
| download | atuin-a129a98c3adb6013ad4848d3884b2f0b49a225a5.zip | |
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.
Diffstat (limited to 'crates/atuin-client/src')
| -rw-r--r-- | crates/atuin-client/src/settings.rs | 116 |
1 files changed, 103 insertions, 13 deletions
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<Self> { + /// 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<Config> { 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<String> { + 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<String> = 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<Self> { + 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()?; |
