Skip to main content

datui_lib/config/
config_command.rs

1//! `datui config`: write the default file, list the files read, list every key.
2
3use 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
10/// What `datui config ACTION` prints, and its exit code. `overrides` are the `-c`
11/// settings, which `keys` counts as a layer.
12pub 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
39/// The files read, lowest precedence first; the user's own file last, said to be
40/// missing when it is.
41fn 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
52/// Every key: kind, default, the value in effect and the layer that set it.
53fn 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
100/// The keys a setting stands for: itself, or for `glyphs.*` each name the layers set.
101fn 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
124/// A value on one line, long lists shortened.
125fn 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
136/// `text` cut to `most` characters, the cut marked.
137fn shorten(text: String, most: usize) -> String {
138    crate::glyphs::fit_cells(&text, most, "...").into_owned()
139}
140
141fn value_at<'a>(table: &'a toml::Table, key: &str) -> Option<&'a toml::Value> {
142    let mut parts = key.split('.');
143    let mut value = table.get(parts.next()?)?;
144    for part in parts {
145        value = value.as_table()?.get(part)?;
146    }
147    Some(value)
148}
149
150#[cfg(test)]
151mod tests;