Skip to main content

dua/
config.rs

1use anyhow::{Context, Result, anyhow};
2
3use serde::Deserialize;
4
5use std::path::PathBuf;
6
7/// Runtime configuration used by interactive and CLI components.
8///
9/// The configuration file is optional. If it cannot be found, defaults are used.
10/// See [`Config::load`] for details on fallback and error behavior.
11///
12/// Expected TOML structure:
13///
14/// ```toml
15/// format = "binary"
16///
17/// [keys]
18/// esc_navigates_back = true
19/// ```
20#[derive(Debug, Default, Deserialize)]
21#[serde(default)]
22pub struct Config {
23    /// Byte count format to use when `--format` and `DUA_FORMAT` are not set.
24    pub format: Option<crate::ByteFormat>,
25
26    /// Keybinding-related settings.
27    pub keys: KeysConfig,
28}
29
30/// Keyboard interaction settings.
31#[derive(Debug, Deserialize)]
32#[serde(default)]
33pub struct KeysConfig {
34    /// Changes `<Esc>` behavior in the interactive UI.
35    ///
36    /// If `true`, pressing `<Esc>` in the main pane ascends to the parent directory.
37    /// If `false`, pressing `<Esc>` follows the default quit behavior, as if `q` was pressed.
38    ///
39    /// Default: `true`.
40    #[serde(default = "default_esc_navigates_back")]
41    pub esc_navigates_back: bool,
42}
43
44fn default_esc_navigates_back() -> bool {
45    true
46}
47
48impl Default for KeysConfig {
49    fn default() -> Self {
50        Self {
51            esc_navigates_back: default_esc_navigates_back(),
52        }
53    }
54}
55
56impl Config {
57    /// Load configuration from disk.
58    ///
59    /// Behavior:
60    /// - If no platform configuration directory is available, returns defaults.
61    /// - If the config file does not exist, returns defaults.
62    /// - If the config file exists but cannot be read, returns an error with path context.
63    /// - If TOML parsing fails, returns an error with path context.
64    ///
65    /// Unknown keys are ignored. Missing supported keys fall back to defaults.
66    pub fn load() -> Result<Self> {
67        let Ok(path) = Self::path() else {
68            log::info!("Configuration path couldn't be determined. Using defaults.");
69            return Ok(Config::default());
70        };
71
72        let contents = match std::fs::read_to_string(&path) {
73            Ok(c) => c,
74            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
75                log::info!(
76                    "Configuration not loaded from {}: file not found. Using defaults.",
77                    path.display()
78                );
79                return Ok(Config::default());
80            }
81            Err(e) => {
82                return Err(e)
83                    .with_context(|| format!("Failed to read config at {}", path.display()));
84            }
85        };
86
87        toml::from_str(&contents)
88            .with_context(|| format!("Failed to parse config at {}", path.display()))
89    }
90
91    /// Default TOML content used when initializing a new configuration file.
92    pub fn default_file_content() -> &'static str {
93        concat!(
94            "# dua-cli configuration\n",
95            "#\n",
96            "# Byte count format to use when --format and DUA_FORMAT are not set.\n",
97            "# Supported values: metric, binary, bytes, gb, gib, mb, mib.\n",
98            "# format = \"binary\"\n",
99            "#\n",
100            "[keys]\n",
101            "# If true, pressing <Esc> in the main pane ascends to the parent directory.\n",
102            "# If false, <Esc> follows the default quit behavior.\n",
103            "esc_navigates_back = true\n",
104        )
105    }
106
107    /// Return the expected configuration file location for the current platform.
108    ///
109    /// The path is:
110    /// - Linux/Unix: `$XDG_CONFIG_HOME/dua-cli/config.toml` (or equivalent fallback)
111    /// - Windows: `%APPDATA%\\dua-cli\\config.toml`
112    /// - macOS: `~/Library/Application Support/dua-cli/config.toml`
113    ///
114    /// Returns an error if the platform config directory cannot be determined.
115    pub fn path() -> Result<PathBuf> {
116        // Use the OS-specific configuration directory (e.g. $XDG_CONFIG_HOME, %APPDATA%, or
117        // ~/Library/Application Support) as provided by the `dirs` crate.
118        let config_dir = dirs::config_dir()
119            .ok_or_else(|| anyhow!("platform config directory is unavailable"))?;
120        Ok(config_dir.join("dua-cli").join("config.toml"))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::Config;
127
128    #[test]
129    fn parses_configured_byte_format() {
130        let config: Config = toml::from_str(
131            r#"
132            format = "mb"
133
134            [keys]
135            esc_navigates_back = false
136            "#,
137        )
138        .expect("valid config");
139
140        assert_eq!(config.format, Some(crate::ByteFormat::MB));
141        assert!(!config.keys.esc_navigates_back);
142    }
143}