1use anyhow::Context;
8
9use crate::config::{Config, default_config_path, provenance, resolve_existing_config_path};
10
11pub struct ShowArgs {
12 pub config_path: Option<std::path::PathBuf>,
16}
17
18pub fn show(args: ShowArgs) -> anyhow::Result<i32> {
19 let path = args
23 .config_path
24 .or_else(resolve_existing_config_path)
25 .unwrap_or_else(default_config_path);
26 let file_text = std::fs::read_to_string(&path).unwrap_or_default();
27
28 let cfg: Config = if file_text.is_empty() {
31 Config::default()
32 } else {
33 toml::from_str(&file_text).with_context(|| format!("parsing {}", path.display()))?
34 };
35 let rows = provenance::provenance_for(&file_text);
36 let effective = effective_values(&cfg);
37
38 println!("# rover effective configuration");
39 println!("# defaults | file ({}) | env", path.display());
40 println!();
41
42 let mut by_section: std::collections::BTreeMap<&str, Vec<&provenance::ProvenanceRow>> =
44 std::collections::BTreeMap::new();
45 for r in &rows {
46 let section = r.dotted.split_once('.').map(|(a, _)| a).unwrap_or("");
47 by_section.entry(section).or_default().push(r);
48 }
49 for (section, section_rows) in by_section {
50 if !section.is_empty() {
51 println!("[{section}]");
52 }
53 for r in section_rows {
54 let leaf = r
55 .dotted
56 .rsplit_once('.')
57 .map(|(_, b)| b)
58 .unwrap_or(&r.dotted);
59 let value = effective
60 .get(r.dotted.as_str())
61 .cloned()
62 .unwrap_or_else(|| "<unknown>".to_string());
63 let source = match r.source {
64 provenance::Source::Default => "defaults",
65 provenance::Source::File => "file",
66 provenance::Source::Env => "env",
67 };
68 println!(
71 "{leaf} = {value} # from: {source} ({dotted})",
72 dotted = r.dotted,
73 );
74 }
75 println!();
76 }
77 Ok(0)
78}
79
80pub struct SetArgs {
81 pub config_path: Option<std::path::PathBuf>,
82 pub key: String,
83 pub value: String,
84}
85
86pub fn set(args: SetArgs) -> anyhow::Result<i32> {
87 let path = args
90 .config_path
91 .or_else(resolve_existing_config_path)
92 .unwrap_or_else(default_config_path);
93 if let Some(parent) = path.parent() {
95 std::fs::create_dir_all(parent)
96 .with_context(|| format!("creating parent dir {}", parent.display()))?;
97 }
98 if !path.exists() {
100 std::fs::write(&path, "").with_context(|| format!("creating {}", path.display()))?;
101 }
102 match crate::config::edit::apply_set(&path, &args.key, &args.value) {
103 Ok(()) => {
104 eprintln!(
105 "✓ {} = {} (wrote {})",
106 args.key,
107 args.value,
108 path.display()
109 );
110 Ok(0)
111 }
112 Err(e) => {
113 eprintln!("error: {e}");
114 Ok(1)
115 }
116 }
117}
118
119fn effective_values(cfg: &Config) -> std::collections::HashMap<&'static str, String> {
123 let v = toml::Value::try_from(cfg).unwrap_or_else(|_| toml::Value::Table(Default::default()));
124 let mut out = std::collections::HashMap::new();
125 for dotted in provenance::known_leaves() {
126 if let Some(val) = lookup_dotted(&v, dotted) {
127 out.insert(*dotted, render_toml_value(&val));
128 }
129 }
130 out
131}
132
133fn lookup_dotted(v: &toml::Value, dotted: &str) -> Option<toml::Value> {
134 let mut cur = v.clone();
135 for part in dotted.split('.') {
136 let toml::Value::Table(t) = cur else {
137 return None;
138 };
139 cur = t.get(part)?.clone();
140 }
141 Some(cur)
142}
143
144fn render_toml_value(v: &toml::Value) -> String {
145 match v {
146 toml::Value::String(s) => format!("\"{s}\""),
147 toml::Value::Integer(n) => n.to_string(),
148 toml::Value::Float(f) => f.to_string(),
149 toml::Value::Boolean(b) => b.to_string(),
150 toml::Value::Array(_) | toml::Value::Table(_) => {
151 toml::to_string(v).unwrap_or_default().trim().to_string()
152 }
153 toml::Value::Datetime(d) => d.to_string(),
154 }
155}