use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::theme::Rgb;
use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub github: GithubConfig,
pub theme: ThemeConfig,
pub ui: UiConfig,
pub keys: HashMap<String, Vec<String>>,
pub home: Option<Vec<crate::home::SectionConfig>>,
pub offline: crate::offline::keep::Limits,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct GithubConfig {
pub host: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ThemeConfig {
pub mode: ModeSetting,
pub seed: Option<String>,
pub color_depth: DepthSetting,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum ModeSetting {
#[default]
Auto,
Light,
Dark,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum DepthSetting {
#[default]
#[serde(rename = "auto")]
Auto,
#[serde(rename = "truecolor", alias = "24bit")]
TrueColor,
#[serde(rename = "256")]
Ansi256,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct UiConfig {
pub nerd_font: bool,
pub density: Density,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Density {
#[default]
Auto,
Compact,
Comfortable,
}
impl Config {
pub fn parse(text: &str) -> Result<Self> {
Ok(toml_edit::de::from_str(text)?)
}
pub fn load(path: &Path) -> Result<Self> {
match std::fs::read_to_string(path) {
Ok(text) => {
Self::parse(&text).with_context(|| format!("invalid config {}", path.display()))
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(err) => Err(err).with_context(|| format!("cannot read {}", path.display())),
}
}
pub fn seed(&self) -> Result<Rgb> {
match &self.theme.seed {
None => Ok(crate::theme::DEFAULT_SEED),
Some(s) => Rgb::parse_hex(s)
.with_context(|| format!("theme.seed `{s}` is not a #rrggbb color")),
}
}
}
pub fn default_config_path() -> Option<PathBuf> {
use etcetera::BaseStrategy;
let base = etcetera::choose_base_strategy().ok()?;
Some(base.config_dir().join("ghtui").join("config.toml"))
}
pub fn cache_dir() -> Option<PathBuf> {
use etcetera::BaseStrategy;
let native = etcetera::base_strategy::choose_native_strategy().ok()?;
Some(native.cache_dir().join("ghtui"))
}
pub fn data_dir() -> Option<PathBuf> {
use etcetera::BaseStrategy;
let native = etcetera::base_strategy::choose_native_strategy().ok()?;
Some(native.data_dir().join("ghtui"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config_is_default() {
let config = Config::parse("").unwrap();
assert_eq!(config.theme.mode, ModeSetting::Auto);
assert_eq!(config.theme.color_depth, DepthSetting::Auto);
assert!(!config.ui.nerd_font);
assert_eq!(config.seed().unwrap(), crate::theme::DEFAULT_SEED);
}
#[test]
fn parses_full_config() {
let config = Config::parse(
r##"
[github]
host = "ghe.example.com"
[theme]
mode = "light"
seed = "#6750a4"
color_depth = "256"
[ui]
nerd_font = true
density = "compact"
[keys]
down = ["j", "<C-n>"]
"##,
)
.unwrap();
assert_eq!(config.github.host.as_deref(), Some("ghe.example.com"));
assert_eq!(config.theme.mode, ModeSetting::Light);
assert_eq!(config.theme.color_depth, DepthSetting::Ansi256);
assert_eq!(config.seed().unwrap(), Rgb::from_u32(0x6750a4));
assert!(config.ui.nerd_font);
assert_eq!(config.ui.density, Density::Compact);
assert_eq!(config.keys["down"], ["j", "<C-n>"]);
}
#[test]
fn rejects_typos_and_bad_values() {
Config::parse("[theme]\nmood = \"dark\"").unwrap_err();
Config::parse("[theme]\nmode = \"dim\"").unwrap_err();
let bad_seed = Config::parse("[theme]\nseed = \"blue\"").unwrap();
bad_seed.seed().unwrap_err();
}
#[test]
fn missing_file_is_default() {
let dir = tempfile::tempdir().unwrap();
let config = Config::load(&dir.path().join("nope.toml")).unwrap();
assert!(config.keys.is_empty());
}
}