df3 0.1.2

Advanced disk free utility - modern alternative to df command
Documentation
// File: src\config.rs
// Author: Hadi Cahyadi <cumulus13@gmail.com>
// Date: 2026-06-16
// Description:
// License: MIT

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub colors: ColorConfig,
    pub display: DisplayConfig,
    pub defaults: DefaultConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorConfig {
    /// Header text color
    #[serde(default = "default_header_color")]
    pub header_color: String,

    /// Border color
    #[serde(default = "default_border_color")]
    pub border_color: String,

    /// Filesystem name color
    #[serde(default = "default_fs_color")]
    pub filesystem_color: String,

    /// High usage color (above 90%)
    #[serde(default = "default_high_usage_color")]
    pub high_usage_color: String,

    /// Warning usage color (75-90%)
    #[serde(default = "default_warning_usage_color")]
    pub warning_usage_color: String,

    /// Normal usage color (below 75%)
    #[serde(default = "default_normal_usage_color")]
    pub normal_usage_color: String,

    /// Available space color
    #[serde(default = "default_avail_color")]
    pub avail_color: String,

    /// Mount point color
    #[serde(default = "default_mount_color")]
    pub mount_color: String,

    /// Timestamp color
    #[serde(default = "default_timestamp_color")]
    pub timestamp_color: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig {
    /// Show filesystem type column
    #[serde(default = "default_true")]
    pub show_type: bool,

    /// Show inode information by default
    #[serde(default = "default_false")]
    pub show_inodes: bool,

    /// Default block size
    #[serde(default = "default_block_size")]
    pub default_block_size: u64,

    /// Table style: modern, sharp, rounded, psql, markdown, ascii
    #[serde(default = "default_table_style")]
    pub table_style: String,

    /// Compact mode (no borders between rows)
    #[serde(default = "default_false")]
    pub compact: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultConfig {
    /// Default human readable
    #[serde(default = "default_true")]
    pub human_readable: bool,

    /// Default local only
    #[serde(default = "default_false")]
    pub local_only: bool,

    /// Default filesystem types to show
    #[serde(default = "default_empty_vec")]
    pub fs_types: Vec<String>,

    /// Default filesystem types to exclude
    #[serde(default = "default_exclude_types")]
    pub exclude_types: Vec<String>,
}

fn default_header_color() -> String {
    "#00FFFF".to_string()
}
fn default_border_color() -> String {
    "#808080".to_string()
}
fn default_fs_color() -> String {
    "#FFFFFF".to_string()
}
fn default_high_usage_color() -> String {
    "#FF0000".to_string()
}
fn default_warning_usage_color() -> String {
    "#FFFF00".to_string()
}
fn default_normal_usage_color() -> String {
    "#00FF00".to_string()
}
fn default_avail_color() -> String {
    "#00FF00".to_string()
}
fn default_mount_color() -> String {
    "#00FFFF".to_string()
}
fn default_timestamp_color() -> String {
    "#808080".to_string()
}
fn default_true() -> bool {
    true
}
fn default_false() -> bool {
    false
}
fn default_block_size() -> u64 {
    1024
}
fn default_table_style() -> String {
    "modern".to_string()
}
fn default_empty_vec() -> Vec<String> {
    vec![]
}
fn default_exclude_types() -> Vec<String> {
    vec![
        "tmpfs".to_string(),
        "devtmpfs".to_string(),
        "squashfs".to_string(),
    ]
}

impl Default for Config {
    fn default() -> Self {
        Config {
            colors: ColorConfig {
                header_color: default_header_color(),
                border_color: default_border_color(),
                filesystem_color: default_fs_color(),
                high_usage_color: default_high_usage_color(),
                warning_usage_color: default_warning_usage_color(),
                normal_usage_color: default_normal_usage_color(),
                avail_color: default_avail_color(),
                mount_color: default_mount_color(),
                timestamp_color: default_timestamp_color(),
            },
            display: DisplayConfig {
                show_type: true,
                show_inodes: false,
                default_block_size: 1024,
                table_style: "modern".to_string(),
                compact: false,
            },
            defaults: DefaultConfig {
                human_readable: true,
                local_only: false,
                fs_types: vec![],
                exclude_types: default_exclude_types(),
            },
        }
    }
}

impl Config {
    pub fn load(config_path: Option<&str>) -> Result<Config> {
        if let Some(path) = config_path {
            let content = std::fs::read_to_string(path)?;
            return Ok(toml::from_str(&content)?);
        }

        // Try default locations
        if let Some(config_path) = get_default_config_path() {
            if config_path.exists() {
                let content = std::fs::read_to_string(&config_path)?;
                return Ok(toml::from_str(&content)?);
            }
        }

        Ok(Config::default())
    }

    pub fn generate_default_config() -> Result<()> {
        if let Some(config_path) = get_default_config_path() {
            if let Some(parent) = config_path.parent() {
                std::fs::create_dir_all(parent)?;
            }
            let config = Config::default();
            let toml_string = toml::to_string_pretty(&config)?;
            std::fs::write(&config_path, toml_string)?;
            println!("Default config created at: {}", config_path.display());
        }
        Ok(())
    }
}

fn get_default_config_path() -> Option<PathBuf> {
    if cfg!(target_os = "windows") {
        std::env::var("APPDATA")
            .ok()
            .map(|p| PathBuf::from(p).join("df3").join("config.toml"))
    } else if cfg!(target_os = "macos") {
        dirs::home_dir().map(|p| {
            p.join("Library")
                .join("Application Support")
                .join("df3")
                .join("config.toml")
        })
    } else {
        dirs::home_dir().map(|p| p.join(".config").join("df3").join("config.toml"))
    }
}