1use std::path::Path;
4
5use datui_cli::ConfigAction;
6use datui_cli::settings::{DefaultValue, Kind, Override, SETTINGS, Setting};
7
8use crate::config::{AppConfig, ConfigLayer, ConfigManager, LayerSource};
9
10pub fn command(
13 manager: &ConfigManager,
14 action: &ConfigAction,
15 overrides: &[Override],
16) -> (String, i32) {
17 let file = manager.config_path("config.toml");
18 match action {
19 ConfigAction::Init { force } => match manager.write_default_config(*force) {
20 Ok(path) => (format!("Wrote {}\n", path.display()), 0),
21 Err(e) => (format!("{e}\n"), 1),
22 },
23 ConfigAction::Path => match AppConfig::read_layers(&file, &[]) {
24 Ok(layers) => (paths(&file, &layers), 0),
25 Err(e) => (format!("{e}\n"), 1),
26 },
27 ConfigAction::Keys => {
28 let read = AppConfig::read_layers(&file, overrides).and_then(|layers| {
29 AppConfig::from_read_layers(&file, overrides, &layers).map(|c| (layers, c))
30 });
31 match read {
32 Ok((layers, config)) => (keys(&config, &layers), 0),
33 Err(e) => (format!("{e}\n"), 1),
34 }
35 }
36 }
37}
38
39fn paths(file: &Path, layers: &[(LayerSource, ConfigLayer)]) -> String {
42 let mut out = String::new();
43 for (source, _) in layers {
44 out.push_str(&format!("{source}\n"));
45 }
46 if layers.is_empty() {
47 out.push_str(&format!("{} (not there: defaults apply)\n", file.display()));
48 }
49 out
50}
51
52fn keys(config: &AppConfig, layers: &[(LayerSource, ConfigLayer)]) -> String {
54 let toml::Value::Table(effective) =
55 toml::Value::try_from(config).unwrap_or(toml::Value::Table(Default::default()))
56 else {
57 unreachable!("a struct serializes to a table")
58 };
59 let mut rows: Vec<[String; 5]> = vec![[
60 "KEY".into(),
61 "TYPE".into(),
62 "DEFAULT".into(),
63 "VALUE".into(),
64 "SET BY".into(),
65 ]];
66 for setting in SETTINGS {
67 for key in expand(setting, layers) {
68 let value = value_at(&effective, &key).map_or_else(|| "unset".to_string(), shown);
69 let set_by = layers
70 .iter()
71 .rev()
72 .find(|(_, layer)| layer.get(&key).is_some())
73 .map_or_else(|| "default".to_string(), |(source, _)| source.to_string());
74 rows.push([
75 key,
76 shorten(setting.kind.describe().replace("\\|", "|"), 24),
77 shorten(default_text(setting), 32),
78 value,
79 set_by,
80 ]);
81 }
82 }
83 let widths: Vec<usize> = (0..4)
84 .map(|i| rows.iter().map(|r| r[i].chars().count()).max().unwrap_or(0))
85 .collect();
86 let mut out = String::new();
87 for row in rows {
88 for (i, cell) in row.iter().enumerate() {
89 if i < 4 {
90 out.push_str(&format!("{cell:<width$} ", width = widths[i]));
91 } else {
92 out.push_str(cell);
93 }
94 }
95 out.push('\n');
96 }
97 out
98}
99
100fn expand(setting: &Setting, layers: &[(LayerSource, ConfigLayer)]) -> Vec<String> {
102 let Some(prefix) = setting.key.strip_suffix(".*") else {
103 return vec![setting.key.to_string()];
104 };
105 let mut names: Vec<String> = layers
106 .iter()
107 .filter_map(|(_, layer)| layer.get(prefix).and_then(toml::Value::as_table))
108 .flat_map(|table| table.keys().map(|name| format!("{prefix}.{name}")))
109 .collect();
110 names.sort();
111 names.dedup();
112 names
113}
114
115fn default_text(setting: &Setting) -> String {
116 match setting.default {
117 DefaultValue::Value(v) => v.to_string(),
118 DefaultValue::Unset(_) if setting.kind == Kind::Tables => "none".into(),
119 DefaultValue::Unset(_) => "unset".into(),
120 DefaultValue::Color { dark, light } => format!("{dark} / {light}"),
121 }
122}
123
124fn shown(value: &toml::Value) -> String {
126 let text = match value {
127 toml::Value::Array(items) if items.iter().any(toml::Value::is_table) => {
128 format!("{} entries", items.len())
129 }
130 toml::Value::Table(_) => "table".to_string(),
131 other => other.to_string(),
132 };
133 shorten(text, 40)
134}
135
136fn shorten(text: String, most: usize) -> String {
138 if text.chars().count() > most {
139 let cut: String = text.chars().take(most - 3).collect();
140 format!("{cut}...")
141 } else {
142 text
143 }
144}
145
146fn value_at<'a>(table: &'a toml::Table, key: &str) -> Option<&'a toml::Value> {
147 let mut parts = key.split('.');
148 let mut value = table.get(parts.next()?)?;
149 for part in parts {
150 value = value.as_table()?.get(part)?;
151 }
152 Some(value)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn manager_with(text: Option<&str>) -> (tempfile::TempDir, ConfigManager) {
160 let dir = tempfile::tempdir().unwrap();
161 if let Some(text) = text {
162 std::fs::write(dir.path().join("config.toml"), text).unwrap();
163 }
164 let manager = ConfigManager::with_dir(dir.path().to_path_buf());
165 (dir, manager)
166 }
167
168 #[test]
169 fn init_writes_once_and_force_replaces() {
170 let (dir, manager) = manager_with(None);
171 let (text, code) = command(&manager, &ConfigAction::Init { force: false }, &[]);
172 assert_eq!(code, 0, "{text}");
173 assert!(dir.path().join("config.toml").exists());
174 let (text, code) = command(&manager, &ConfigAction::Init { force: false }, &[]);
175 assert_eq!(code, 1);
176 assert!(text.contains("--force"), "{text}");
177 assert_eq!(
178 command(&manager, &ConfigAction::Init { force: true }, &[]).1,
179 0
180 );
181 }
182
183 #[test]
184 fn path_lists_imports_before_the_file() {
185 let (dir, manager) = manager_with(Some("import = [\"theme.toml\"]\n"));
186 std::fs::write(dir.path().join("theme.toml"), "[theme]\nmode = \"light\"\n").unwrap();
187 let (text, code) = command(&manager, &ConfigAction::Path, &[]);
188 assert_eq!(code, 0);
189 let lines: Vec<&str> = text.lines().collect();
190 assert_eq!(lines.len(), 2, "{text}");
191 assert!(
192 lines[0].ends_with("theme.toml") && lines[1].ends_with("config.toml"),
193 "{text}"
194 );
195
196 let (_dir, manager) = manager_with(None);
197 let (text, _) = command(&manager, &ConfigAction::Path, &[]);
198 assert!(text.contains("not there"), "{text}");
199 }
200
201 #[test]
202 fn keys_say_what_set_each_value() {
203 let (_dir, manager) =
204 manager_with(Some("[display]\nrow_numbers = true\nrow_start_index = 0\n"));
205 let overrides = vec!["display.row_numbers_start=7".parse().unwrap()];
206 let (text, code) = command(&manager, &ConfigAction::Keys, &overrides);
207 assert_eq!(code, 0, "{text}");
208 let row = |key: &str| {
209 text.lines()
210 .find(|l| l.starts_with(&format!("{key} ")))
211 .unwrap_or_else(|| panic!("{key} listed"))
212 .split_whitespace()
213 .collect::<Vec<_>>()
214 };
215 let numbers = row("display.row_numbers");
216 let [.., value, source] = numbers.as_slice() else {
218 panic!("{numbers:?}");
219 };
220 assert_eq!(*value, "true");
221 assert!(source.ends_with("config.toml"), "{numbers:?}");
222 assert_eq!(row("display.row_numbers_start")[3..], ["7", "-c"]);
223 assert_eq!(row("display.mouse")[3..], ["true", "default"]);
224 for setting in SETTINGS.iter().filter(|s| !s.key.ends_with(".*")) {
225 assert!(text.contains(setting.key), "{} listed", setting.key);
226 }
227 }
228}