concord 2.4.7

A terminal user interface client for Discord
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use crate::{Result, paths, support::private_file};

pub fn load_options() -> Result<AppOptions> {
    Ok(load_options_with_warnings()?.0)
}

pub fn load_options_with_warnings() -> Result<(AppOptions, Vec<String>)> {
    let path = config_path()?;
    load_options_from_path(&path)
}

pub fn load_keymap_options_with_warnings() -> Result<(KeymapOptions, Vec<String>)> {
    let path = keymap_path()?;
    load_keymap_options_from_path(&path)
}

pub fn load_ui_state_options_with_warnings() -> Result<(UiStateOptions, Vec<String>)> {
    let path = state_path()?;
    load_ui_state_options_from_path(&path)
}

pub fn load_theme_options_with_warnings() -> Result<(ThemeOptions, Vec<String>)> {
    let path = theme_path()?;
    load_theme_options_from_path(&path)
}

#[cfg(test)]
pub(crate) fn parse_theme_options_for_test(content: &str) -> Result<(ThemeOptions, Vec<String>)> {
    parse_theme_options(content)
}

fn load_options_from_path(path: &Path) -> Result<(AppOptions, Vec<String>)> {
    load_tolerant_options_from_path(path, parse_app_options)
}

fn load_tolerant_options_from_path<T>(
    path: &Path,
    parse: impl FnOnce(&str) -> Result<(T, Vec<String>)>,
) -> Result<(T, Vec<String>)>
where
    T: Default,
{
    match fs::read_to_string(path) {
        Ok(content) => parse(&content),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            Ok((T::default(), Vec::new()))
        }
        Err(error) => Err(error.into()),
    }
}

fn load_keymap_options_from_path(path: &Path) -> Result<(KeymapOptions, Vec<String>)> {
    load_tolerant_options_from_path(path, parse_keymap_options)
}

fn load_ui_state_options_from_path(path: &Path) -> Result<(UiStateOptions, Vec<String>)> {
    load_tolerant_options_from_path(path, parse_ui_state_options)
}

fn load_theme_options_from_path(path: &Path) -> Result<(ThemeOptions, Vec<String>)> {
    load_tolerant_options_from_path(path, parse_theme_options)
}

pub fn save_options(options: &AppOptions) -> Result<()> {
    let path = config_path()?;
    save_options_to_path(&path, options)
}

pub fn save_ui_state_options(options: &UiStateOptions) -> Result<()> {
    let path = state_path()?;
    save_ui_state_options_to_path(&path, options)
}

fn save_options_to_path(path: &Path, options: &AppOptions) -> Result<()> {
    write_private_toml(path, options)
}

fn save_ui_state_options_to_path(path: &Path, options: &UiStateOptions) -> Result<()> {
    let file_options = UiStateFileOptions {
        ui_state: options.clone(),
    };
    write_private_toml(path, &file_options)
}

fn write_private_toml<T>(path: &Path, value: &T) -> Result<()>
where
    T: serde::Serialize,
{
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
        private_file::set_private_dir_permissions(parent)?;
    }

    private_file::write_private_file(path, &toml::to_string_pretty(value)?)
}

fn config_path() -> Result<PathBuf> {
    resolved_path(
        paths::config_file(),
        "could not resolve user config directory",
    )
}

fn keymap_path() -> Result<PathBuf> {
    resolved_path(
        paths::keymap_file(),
        "could not resolve user config directory",
    )
}

fn theme_path() -> Result<PathBuf> {
    resolved_path(
        paths::theme_file(),
        "could not resolve user config directory",
    )
}

fn state_path() -> Result<PathBuf> {
    resolved_path(
        paths::state_file(),
        "could not resolve user state directory",
    )
}

fn resolved_path(path: Option<PathBuf>, missing_message: &'static str) -> Result<PathBuf> {
    path.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, missing_message).into())
}

mod options;
mod parse;
#[cfg(test)]
mod tests;

pub use options::*;
use parse::{parse_app_options, parse_keymap_options, parse_theme_options, parse_ui_state_options};