getquotes 0.7.0

A simple cli tool to get quotes in your terminal using WikiQuotes
use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::env::home_dir;
use std::error::Error as StdError;
use std::fs::{create_dir_all, read_to_string, write};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Config {
    pub authors: Vec<String>,
    #[serde(default = "default_theme_color")]
    pub theme_color: String,
    #[serde(default = "default_quote_style")]
    pub quote_style: String,
    #[serde(default = "default_author_style")]
    pub author_style: String,
    #[serde(default = "default_nested_quote_style")]
    pub nested_quote_style: String,
    #[serde(default = "default_max_tries")]
    pub max_tries: usize,
    #[serde(default = "default_log_file")]
    pub log_file: String,
    #[serde(default = "default_rainbow_mode")]
    pub rainbow_mode: bool,
    #[serde(default = "default_layout")]
    pub layout: Layout,
    #[serde(default = "default_box_corners")]
    pub box_corners: BoxCorners,
    #[serde(default = "default_prefer_cache")]
    pub prefer_cache: bool,
    #[serde(default = "default_api_calls_per_minute")]
    pub api_calls_per_minute: usize,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Layout {
    Default,
    Box,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum BoxCorners {
    Pointy,
    Rounded,
}

pub fn default_theme_color() -> String {
    "#B7FFFA".to_string()
}

pub fn default_quote_style() -> String {
    "bold".to_string()
}

pub fn default_author_style() -> String {
    "green".to_string()
}

pub fn default_nested_quote_style() -> String {
    String::new()
}

pub fn default_max_tries() -> usize {
    30
}

pub fn default_log_file() -> String {
    String::from("getquotes.log")
}

pub fn default_rainbow_mode() -> bool {
    false
}

pub fn default_layout() -> Layout {
    Layout::Default
}

pub fn default_box_corners() -> BoxCorners {
    BoxCorners::Pointy
}

pub fn default_prefer_cache() -> bool {
    true
}

pub fn default_api_calls_per_minute() -> usize {
    10
}

pub fn default_authors() -> Vec<String> {
    vec![
        "Mahatma Gandhi".into(),
        "Albert Einstein".into(),
        "Martin Luther King, Jr.".into(),
        "Leonardo da Vinci".into(),
        "Walt Disney".into(),
        "Edgar Allan Poe".into(),
        "Sigmund Freud".into(),
        "Thomas A. Edison".into(),
        "Robin Williams".into(),
        "Steve Jobs".into(),
    ]
}

pub fn load_or_create_config() -> Result<Config, Box<dyn StdError + Send + Sync>> {
    let config_path = get_config_path()?;
    if !config_path.exists() {
        if let Some(parent_dir) = config_path.parent() {
            create_dir_all(parent_dir)?;
        }
        let default_config = Config {
            authors: default_authors(),
            theme_color: default_theme_color(),
            quote_style: default_quote_style(),
            author_style: default_author_style(),
            nested_quote_style: default_nested_quote_style(),
            max_tries: default_max_tries(),
            log_file: default_log_file(),
            rainbow_mode: default_rainbow_mode(),
            layout: default_layout(),
            box_corners: default_box_corners(),
            prefer_cache: default_prefer_cache(),
            api_calls_per_minute: default_api_calls_per_minute(),
        };
        let toml_string = toml::to_string_pretty(&default_config)?;
        write(&config_path, toml_string)?;
        info!("Config file created at: {config_path:?}");
        return Ok(default_config);
    }

    let toml_content = read_to_string(&config_path)?;
    let config: Config = toml::from_str(&toml_content)?;
    info!("Config file loaded from: {config_path:?}");
    Ok(config)
}

pub fn get_config_path() -> Result<PathBuf, Box<dyn StdError + Send + Sync>> {
    let home = home_dir()
        .ok_or_else(|| Box::<dyn StdError + Send + Sync>::from("Unable to find home directory."))?;
    let config_dir = home.join(".config/getquotes");
    create_dir_all(&config_dir)?;
    let config_path = config_dir.join("config.toml");
    Ok(config_path)
}

pub fn load_or_create_config_from_path(
    path: &str,
) -> Result<Config, Box<dyn StdError + Send + Sync>> {
    let config_path = PathBuf::from(path);
    if !config_path.exists() {
        if let Some(parent_dir) = config_path.parent() {
            create_dir_all(parent_dir)?;
        }
        let default_config = Config {
            authors: default_authors(),
            theme_color: default_theme_color(),
            quote_style: default_quote_style(),
            author_style: default_author_style(),
            nested_quote_style: default_nested_quote_style(),
            max_tries: default_max_tries(),
            log_file: default_log_file(),
            rainbow_mode: default_rainbow_mode(),
            layout: default_layout(),
            box_corners: default_box_corners(),
            prefer_cache: default_prefer_cache(),
            api_calls_per_minute: default_api_calls_per_minute(),
        };
        let toml_string = toml::to_string_pretty(&default_config)?;
        write(&config_path, toml_string)?;
        info!("Config file created at: {config_path:?}");
        return Ok(default_config);
    }

    let toml_content = read_to_string(&config_path)?;
    let config: Config = toml::from_str(&toml_content)?;
    info!("Config file loaded from: {config_path:?}");
    Ok(config)
}

pub fn parse_hex_color(hex_str: &str) -> Option<(u8, u8, u8)> {
    let clean_hex = hex_str.strip_prefix('#').unwrap_or(hex_str);
    if clean_hex.len() != 6 {
        return None;
    }

    let r = u8::from_str_radix(&clean_hex[0..2], 16).ok()?;
    let g = u8::from_str_radix(&clean_hex[2..4], 16).ok()?;
    let b = u8::from_str_radix(&clean_hex[4..6], 16).ok()?;
    Some((r, g, b))
}