use crate::atuin_client::settings::Settings; use clap::{Args, Subcommand, ValueEnum}; use eyre::Result; use toml_edit::{Document, DocumentMut, Item, Table, TableLike, Value}; #[derive(Subcommand, Debug)] #[command(infer_subcommands = true)] pub(crate) enum Cmd { /// Get a configuration value from your config.toml file /// or after defaults and overrides are applied #[command()] Get(GetCmd), /// Set a configuration value in your config.toml file #[command()] Set(SetCmd), /// Print all configuration values from your config.toml file /// in TOML format /// /// If a key is provided, only print the value of that key and all its children #[command()] Print(PrintCmd), } impl Cmd { pub(crate) async fn run(self, settings: &Settings) -> Result<()> { match self { Self::Get(get) => get.run(settings).await, Self::Set(set) => set.run(settings).await, Self::Print(print) => print.run(settings).await, } } } /// Get a configuration value from your config.toml file, /// or optionally the effective value after defaults and overrides are applied. #[derive(Args, Debug)] pub(crate) struct GetCmd { /// The configuration key to get pub(crate) key: String, /// Print the value after defaults and overrides are applied #[arg(long, short)] pub(crate) resolved: bool, /// Print both the config file value and the resolved value #[arg(long, short)] pub(crate) verbose: bool, } impl GetCmd { pub(crate) async fn run(&self, _settings: &Settings) -> Result<()> { let key = self.key.trim(); if key.is_empty() || key.contains(char::is_whitespace) { eyre::bail!("Config key must be non-empty and must not contain whitespace"); } if self.verbose { println!("Config file:"); self.print_current_value(key, " ").await?; println!("\nResolved:"); Self::print_effective_value(key, " "); return Ok(()); } if self.resolved { Self::print_effective_value(key, ""); } else { self.print_current_value(key, "").await?; } Ok(()) } async fn print_current_value(&self, key: &str, prefix: &str) -> Result<()> { let config_file = Settings::get_config_path()?; let config_str = tokio::fs::read_to_string(&config_file).await?; let doc = config_str.parse::>()?; let current = get_deep_key(&doc, key); match current { Some(item) if item.is_table() || item.is_inline_table() => { let table = item .as_table_like() .expect("is_table()/is_inline_table() but no table"); println!("{prefix}[{key}]"); dump_table(table, prefix, &mut vec![key.to_string()])?; } Some(item) => { let val = item.to_string(); let val = val.trim().trim_matches('"'); println!("{prefix}{val}"); } None => { println!("{prefix}(not set in config file)"); } } Ok(()) } fn print_effective_value(key: &str, prefix: &str) { match Settings::get_config_value(key) { Ok(value) => { for line in value.lines() { println!("{prefix}{line}"); } } Err(_) => { println!("{prefix}(unknown key)"); } } } } #[derive(Args, Debug)] pub(crate) struct SetCmd { /// The configuration key to set pub(crate) key: String, /// The value to set pub(crate) value: String, /// Store value as an explicit type #[arg(long = "type", short, value_enum, default_value_t = ValueType::Auto, value_name = "TYPE")] pub(crate) the_type: ValueType, } #[derive(ValueEnum, Debug, Clone, PartialEq, Eq)] pub(crate) enum ValueType { /// Automatically determine the type of the value Auto, /// Store value as a string String, /// Store value as a boolean Boolean, /// Store value as an integer Integer, /// Store the value as a float Float, } impl SetCmd { pub(crate) async fn run(self, _settings: &Settings) -> Result<()> { let key = self.key.trim(); if key.is_empty() || key.contains(char::is_whitespace) { eyre::bail!("Config key must be non-empty and must not contain whitespace"); } let config_file = Settings::get_config_path()?; let config_str = tokio::fs::read_to_string(&config_file).await?; let mut doc: DocumentMut = config_str.parse()?; // When using auto type detection, try to match the existing value's type // so we don't accidentally change e.g. "300" (string) to 300 (integer) let existing_type = detect_existing_type(&doc, key); let value = self.parse_value(existing_type.as_ref())?; set_deep_key(&mut doc, key, value)?; tokio::fs::write(&config_file, doc.to_string()).await?; Ok(()) } fn parse_value(&self, existing_type: Option<&ValueType>) -> Result { let raw = &self.value; // Explicit --type takes priority, then existing value type, then auto-detect let effective_type = if self.the_type != ValueType::Auto { &self.the_type } else if let Some(existing) = existing_type { existing } else { &ValueType::Auto }; match effective_type { ValueType::String => Ok(Value::from(raw.as_str())), ValueType::Boolean => { let b: bool = raw .parse() .map_err(|_| eyre::eyre!("invalid boolean value: {raw}"))?; Ok(Value::from(b)) } ValueType::Integer => { let i: i64 = raw .parse() .map_err(|_| eyre::eyre!("invalid integer value: {raw}"))?; Ok(Value::from(i)) } ValueType::Float => { let f: f64 = raw .parse() .map_err(|_| eyre::eyre!("invalid float value: {raw}"))?; Ok(Value::from(f)) } ValueType::Auto => { if raw == "true" || raw == "false" { return Ok(Value::from(raw == "true")); } if let Ok(i) = raw.parse::() { return Ok(Value::from(i)); } if let Ok(f) = raw.parse::() { return Ok(Value::from(f)); } Ok(Value::from(raw.as_str())) } } } } #[derive(Args, Debug)] pub(crate) struct PrintCmd { /// Print the value of a specific key and all its children pub(crate) key: Option, } impl PrintCmd { pub(crate) async fn run(&self, _settings: &Settings) -> Result<()> { let config_file = Settings::get_config_path()?; let config_str = tokio::fs::read_to_string(&config_file).await?; let doc = config_str.parse::>()?; if let Some(key) = &self.key { let current = get_deep_key(&doc, key); if let Some(current) = current { if current.is_table() || current.is_inline_table() { println!("[{key}]"); dump_table( current .as_table_like() .expect("is_table()/is_inline_table() but no table"), "", &mut vec![key.clone()], )?; } else { println!("{}", current.to_string().trim().trim_matches('"')); } } else { println!("key not found"); } } else { dump_table(doc.as_table(), "", &mut Vec::new())?; } Ok(()) } } fn dump_table(table: &dyn TableLike, prefix: &str, stack: &mut Vec) -> Result<()> { for (key, value) in table.iter() { if value.is_table() || value.is_inline_table() { stack.push(key.to_string()); let table = value .as_table_like() .expect("is_table()/is_inline_table() but no table"); println!("\n{}[{}]", prefix, stack.join(".")); dump_table(table, prefix, stack)?; stack.pop(); } else { println!("{prefix}{key} = {value}"); } } Ok(()) } fn get_deep_key<'doc>(doc: &'doc Document, key: &str) -> Option<&'doc Item> { let parts = key.split('.'); let mut current: Option<&Item> = Some(doc.as_item()); for part in parts { current = current .and_then(|item| item.as_table_like()) .and_then(|table| table.get(part)); } current } /// Detect the TOML type of an existing key in the document, so `set` with auto /// type detection preserves the original type rather than guessing from the value string. fn detect_existing_type(doc: &DocumentMut, key: &str) -> Option { let parts: Vec<&str> = key.split('.').collect(); let mut current: &dyn TableLike = doc.as_table(); for &part in &parts[..parts.len().saturating_sub(1)] { current = current.get(part)?.as_table_like()?; } let last = parts.last()?; let v = current.get(last)?.as_value()?; if v.is_str() { Some(ValueType::String) } else if v.is_bool() { Some(ValueType::Boolean) } else if v.is_integer() { Some(ValueType::Integer) } else if v.is_float() { Some(ValueType::Float) } else { None } } fn set_deep_key(doc: &mut DocumentMut, key: &str, value: Value) -> Result<()> { let parts: Vec<&str> = key.split('.').collect(); if parts.is_empty() { eyre::bail!("empty config key"); } let mut current: &mut dyn TableLike = doc.as_table_mut(); // Navigate/create intermediate tables for &part in &parts[..parts.len() - 1] { if !current.contains_key(part) { current.insert(part, Item::Table(Table::new())); } current = current .get_mut(part) .expect("just inserted or already exists") .as_table_like_mut() .ok_or_else(|| eyre::eyre!("'{}' exists but is not a table", part))?; } let last = *parts.last().unwrap(); // Don't silently overwrite a table with a scalar value if let Some(existing) = current.get(last) && (existing.is_table() || existing.is_inline_table()) { eyre::bail!( "'{}' is a table; use a dotted key like '{}.key' to set a value within it", key, key ); } current.insert(last, Item::Value(value)); Ok(()) }