use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct Config {
pub format: Option<crate::ByteFormat>,
pub keys: KeysConfig,
pub notifications: NotificationsConfig,
pub gitignore: Option<bool>,
pub cleanup_heuristics: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct NotificationsConfig {
pub scan_finished: bool,
pub delete_finished: bool,
}
impl Default for NotificationsConfig {
fn default() -> Self {
Self {
scan_finished: true,
delete_finished: true,
}
}
}
impl NotificationsConfig {
pub fn any_enabled(&self) -> bool {
self.scan_finished || self.delete_finished
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct KeysConfig {
#[serde(default = "default_esc_navigates_back")]
pub esc_navigates_back: bool,
}
fn default_esc_navigates_back() -> bool {
true
}
impl Default for KeysConfig {
fn default() -> Self {
Self {
esc_navigates_back: default_esc_navigates_back(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let Ok(path) = Self::path() else {
log::info!("Configuration path couldn't be determined. Using defaults.");
return Ok(Config::default());
};
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
log::info!(
"Configuration not loaded from {}: file not found. Using defaults.",
path.display()
);
return Ok(Config::default());
}
Err(e) => {
return Err(e)
.with_context(|| format!("Failed to read config at {}", path.display()));
}
};
toml::from_str(&contents)
.with_context(|| format!("Failed to parse config at {}", path.display()))
}
#[must_use]
pub fn default_file_content() -> &'static str {
concat!(
"# dua-cli configuration\n",
"#\n",
"# Byte count format to use when --format and DUA_FORMAT are not set.\n",
"# Supported values: metric, binary, bytes, gb, gib, mb, mib.\n",
"# format = \"binary\"\n",
"#\n",
"# Controls whether Git-ignored entry detection is enabled in interactive mode.\n",
"# Supported values: true, false.\n",
"# If unset, behavior defaults to true.\n",
"# gitignore = true\n",
"#\n",
"# Controls whether cleanup heuristics are enabled in interactive mode.\n",
"# Supported values: true, false.\n",
"# If unset, behavior defaults to true.\n",
"# cleanup_heuristics = true\n",
"#\n",
"[keys]\n",
"# If true, pressing <Esc> in the main pane ascends to the parent directory.\n",
"# If false, <Esc> follows the default quit behavior.\n",
"esc_navigates_back = true\n",
"#\n",
"[notifications]\n",
"# Send terminal notifications when interactive operations finish while unfocused.\n",
"scan_finished = true\n",
"delete_finished = true\n",
)
}
pub fn path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| anyhow!("platform config directory is unavailable"))?;
Ok(config_dir.join("dua-cli").join("config.toml"))
}
}
#[cfg(test)]
mod tests {
use super::Config;
#[test]
fn notifications_default_to_enabled_and_can_be_disabled() {
let defaults: Config = toml::from_str("").expect("valid config");
assert!(defaults.notifications.scan_finished);
assert!(defaults.notifications.delete_finished);
let configured: Config = toml::from_str(
r"
[notifications]
scan_finished = false
delete_finished = false
",
)
.expect("valid config");
assert!(!configured.notifications.scan_finished);
assert!(!configured.notifications.delete_finished);
}
#[test]
fn notifications_are_enabled_if_any_notification_is_enabled() {
let disabled: Config = toml::from_str(
r"
[notifications]
scan_finished = false
delete_finished = false
",
)
.expect("valid config");
assert!(!disabled.notifications.any_enabled());
let partly_enabled: Config = toml::from_str(
r"
[notifications]
scan_finished = false
",
)
.expect("valid config");
assert!(partly_enabled.notifications.any_enabled());
}
#[test]
fn parses_configured_byte_format() {
let config: Config = toml::from_str(
r#"
format = "mb"
[keys]
esc_navigates_back = false
"#,
)
.expect("valid config");
assert_eq!(config.format, Some(crate::ByteFormat::MB));
assert!(!config.keys.esc_navigates_back);
}
#[test]
fn parses_configured_gitignore() {
let config: Config = toml::from_str(
r#"
format = "mb"
gitignore = false
[keys]
esc_navigates_back = false
"#,
)
.expect("valid config");
assert_eq!(config.gitignore, Some(false));
}
#[test]
fn gitignore_defaults_to_enabled() {
let config: Config = toml::from_str(
r#"
format = "mb"
[keys]
esc_navigates_back = false
"#,
)
.expect("valid config");
assert_eq!(config.gitignore, None);
}
#[test]
fn parses_configured_cleanup_heuristics() {
let config: Config = toml::from_str(
r#"
format = "mb"
cleanup_heuristics = false
[keys]
esc_navigates_back = false
"#,
)
.expect("valid config");
assert_eq!(config.cleanup_heuristics, Some(false));
}
#[test]
fn cleanup_heuristics_defaults_to_enabled() {
let config: Config = toml::from_str(
r#"
format = "mb"
[keys]
esc_navigates_back = false
"#,
)
.expect("valid config");
assert_eq!(config.cleanup_heuristics, None);
}
}