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/// [keys]
16/// esc_navigates_back = true
17/// ```
18#[derive(Debug, Default, Deserialize)]
19#[serde(default)]
20pub struct Config {
21    /// Keybinding-related settings.
22    pub keys: KeysConfig,
23}
24
25/// Keyboard interaction settings.
26#[derive(Debug, Deserialize)]
27#[serde(default)]
28pub struct KeysConfig {
29    /// Changes `<Esc>` behavior in the interactive UI.
30    ///
31    /// If `true`, pressing `<Esc>` in the main pane ascends to the parent directory.
32    /// If `false`, pressing `<Esc>` follows the default quit behavior, as if `q` was pressed.
33    ///
34    /// Default: `true`.
35    #[serde(default = "default_esc_navigates_back")]
36    pub esc_navigates_back: bool,
37}
38
39fn default_esc_navigates_back() -> bool {
40    true
41}
42
43impl Default for KeysConfig {
44    fn default() -> Self {
45        Self {
46            esc_navigates_back: default_esc_navigates_back(),
47        }
48    }
49}
50
51impl Config {
52    /// Load configuration from disk.
53    ///
54    /// Behavior:
55    /// - If no platform configuration directory is available, returns defaults.
56    /// - If the config file does not exist, returns defaults.
57    /// - If the config file exists but cannot be read, returns an error with path context.
58    /// - If TOML parsing fails, returns an error with path context.
59    ///
60    /// Unknown keys are ignored. Missing supported keys fall back to defaults.
61    pub fn load() -> Result<Self> {
62        let Ok(path) = Self::path() else {
63            log::info!("Configuration path couldn't be determined. Using defaults.");
64            return Ok(Config::default());
65        };
66
67        let contents = match std::fs::read_to_string(&path) {
68            Ok(c) => c,
69            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
70                log::info!(
71                    "Configuration not loaded from {}: file not found. Using defaults.",
72                    path.display()
73                );
74                return Ok(Config::default());
75            }
76            Err(e) => {
77                return Err(e)
78                    .with_context(|| format!("Failed to read config at {}", path.display()));
79            }
80        };
81
82        toml::from_str(&contents)
83            .with_context(|| format!("Failed to parse config at {}", path.display()))
84    }
85
86    /// Default TOML content used when initializing a new configuration file.
87    pub fn default_file_content() -> &'static str {
88        concat!(
89            "# dua-cli configuration\n",
90            "#\n",
91            "[keys]\n",
92            "# If true, pressing <Esc> in the main pane ascends to the parent directory.\n",
93            "# If false, <Esc> follows the default quit behavior.\n",
94            "esc_navigates_back = true\n",
95        )
96    }
97
98    /// Return the expected configuration file location for the current platform.
99    ///
100    /// The path is:
101    /// - Linux/Unix: `$XDG_CONFIG_HOME/dua-cli/config.toml` (or equivalent fallback)
102    /// - Windows: `%APPDATA%\\dua-cli\\config.toml`
103    /// - macOS: `~/Library/Application Support/dua-cli/config.toml`
104    ///
105    /// Returns an error if the platform config directory cannot be determined.
106    pub fn path() -> Result<PathBuf> {
107        // Use the OS-specific configuration directory (e.g. $XDG_CONFIG_HOME, %APPDATA%, or
108        // ~/Library/Application Support) as provided by the `dirs` crate.
109        let config_dir = dirs::config_dir()
110            .ok_or_else(|| anyhow!("platform config directory is unavailable"))?;
111        Ok(config_dir.join("dua-cli").join("config.toml"))
112    }
113}