#![allow(clippy::derivable_impls)]
use std::{fs, path::PathBuf, str::FromStr};
use anyhow::{Context, Result};
use clap::Parser;
use once_cell::sync::OnceCell;
use serde::Deserialize;
pub static CONFIG: OnceCell<Config> = OnceCell::new();
#[derive(Parser)]
#[command(version = env!("GEX_VERSION"), about)]
pub struct Clargs {
#[clap(default_value = ".")]
pub path: String,
#[clap(short, long, name = "PATH")]
pub config_file: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub options: Options,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Options {
pub auto_expand_files: bool,
pub auto_expand_hunks: bool,
pub lookahead_lines: usize,
pub truncate_lines: bool,
pub ws_error_highlight: WsErrorHighlight,
}
#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(try_from = "String")]
pub struct WsErrorHighlight {
pub old: bool,
pub new: bool,
pub context: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
auto_expand_files: false,
auto_expand_hunks: true,
lookahead_lines: 5,
truncate_lines: true,
ws_error_highlight: WsErrorHighlight::default(),
}
}
}
impl Config {
pub fn read_from_file(path: &Option<String>) -> Result<Option<(Self, Vec<String>)>> {
let mut config_path;
if let Some(path) = path {
config_path = PathBuf::from(path);
} else if let Some(path) = dirs::config_dir() {
config_path = path;
config_path.push("gex");
config_path.push("config.toml");
} else {
return Ok(None);
}
let Ok(config) = fs::read_to_string(config_path) else {
return Ok(None);
};
let de = toml::Deserializer::new(&config);
let mut unused_keys = Vec::new();
let config = serde_ignored::deserialize(de, |path| {
unused_keys.push(path.to_string());
})
.context("failed to parse config file")?;
Ok(Some((config, unused_keys)))
}
}
impl WsErrorHighlight {
const GIT_DEFAULT: Self = Self {
old: false,
new: true,
context: false,
};
const NONE: Self = Self {
old: false,
new: false,
context: false,
};
const ALL: Self = Self {
old: true,
new: true,
context: true,
};
}
impl Default for WsErrorHighlight {
fn default() -> Self {
let Ok(Ok(git_config)) = git2::Config::open_default().map(|mut config| config.snapshot())
else {
return Self::GIT_DEFAULT;
};
let Ok(value) = git_config.get_str("diff.wsErrorHighlight") else {
return Self::GIT_DEFAULT;
};
Self::from_str(value).unwrap_or(Self::GIT_DEFAULT)
}
}
impl TryFrom<String> for WsErrorHighlight {
type Error = anyhow::Error;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
Self::from_str(&s)
}
}
impl FromStr for WsErrorHighlight {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut result = Self::GIT_DEFAULT;
for opt in s.split(',') {
match opt {
"all" => result = Self::ALL,
"default" => result = Self::GIT_DEFAULT,
"none" => result = Self::NONE,
"old" => result.old = true,
"new" => result.new = true,
"context" => result.context = true,
otherwise => {
return Err(anyhow::Error::msg(format!(
"unrecognised option in `ws_error_highlight`: {otherwise}"
)))
}
}
}
Ok(result)
}
}