use crate::theme::Theme;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitPagingConfig {
#[serde(default)]
pub pager: String,
#[serde(default, rename = "externalDiffCommand")]
pub external_diff_command: String,
#[serde(default = "default_color_arg")]
pub color_arg: String,
#[serde(default)]
pub use_config: bool,
}
fn default_color_arg() -> String {
"always".to_string()
}
impl Default for GitPagingConfig {
fn default() -> Self {
Self {
pager: String::new(),
external_diff_command: String::new(),
color_arg: default_color_arg(),
use_config: false,
}
}
}
impl GitPagingConfig {
pub fn has_external_diff_command(&self) -> bool {
!self.external_diff_command.trim().is_empty()
}
pub fn has_pager(&self) -> bool {
!self.pager.trim().is_empty()
}
pub fn get_effective_command(&self) -> DiffCommandType {
if self.has_external_diff_command() {
DiffCommandType::External(self.external_diff_command.clone())
} else if self.has_pager() {
DiffCommandType::Pager(self.pager.clone())
} else {
DiffCommandType::GitDefault
}
}
}
#[derive(Debug, Clone)]
pub enum DiffCommandType {
GitDefault,
Pager(String),
External(String),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DiffCommand {
pub command: String,
pub args: Vec<String>,
#[serde(default = "default_display_name")]
pub display_name: String,
}
fn default_display_name() -> String {
"git diff".to_string()
}
impl Default for DiffCommand {
fn default() -> Self {
Self {
command: "diff".to_string(),
args: vec!["-u".to_string()],
display_name: default_display_name(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GitConfig {
#[serde(default)]
pub paging: GitPagingConfig,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Config {
#[serde(default)]
pub git: GitConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub diff_command: Option<DiffCommand>,
#[serde(default)]
pub theme: Theme,
}
impl Config {
pub fn get_diff_command_type(&self) -> DiffCommandType {
let effective_command = self.git.paging.get_effective_command();
match effective_command {
DiffCommandType::GitDefault => {
if let Some(ref legacy_cmd) = self.diff_command {
if legacy_cmd.command != "diff" {
let full_command = if legacy_cmd.args.is_empty() {
legacy_cmd.command.clone()
} else {
format!("{} {}", legacy_cmd.command, legacy_cmd.args.join(" "))
};
DiffCommandType::Pager(full_command)
} else {
DiffCommandType::GitDefault
}
} else {
DiffCommandType::GitDefault
}
}
_ => effective_command,
}
}
pub fn get_diff_display_name(&self) -> String {
match self.get_diff_command_type() {
DiffCommandType::GitDefault => "git diff".to_string(),
DiffCommandType::Pager(ref cmd) => {
let tool_name = cmd.split_whitespace().next().unwrap_or("pager");
format!("{tool_name} (pager)")
}
DiffCommandType::External(ref cmd) => {
let tool_name = cmd.split_whitespace().next().unwrap_or("external");
format!("{tool_name} (external)")
}
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
Self::load_from_path_buf(&config_path)
}
pub fn load_from_path(path: &str) -> Result<Self> {
let config_path = PathBuf::from(path);
Self::load_from_path_buf(&config_path)
}
fn load_from_path_buf(config_path: &PathBuf) -> Result<Self> {
if !config_path.exists() {
let config = Config::default();
if config_path == &Self::config_path()? {
config.save()?;
}
return Ok(config);
}
let contents = fs::read_to_string(config_path)
.with_context(|| format!("Failed to read config file: {config_path:?}"))?;
let config: Config =
serde_yaml::from_str(&contents).with_context(|| "Failed to parse config file")?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {parent:?}"))?;
}
let yaml = serde_yaml::to_string(self).with_context(|| "Failed to serialize config")?;
fs::write(&config_path, yaml)
.with_context(|| format!("Failed to write config file: {config_path:?}"))?;
Ok(())
}
fn config_path() -> Result<PathBuf> {
let home_dir = dirs::home_dir().context("Failed to get home directory")?;
Ok(home_dir.join(".config").join("ftdv").join("config.yaml"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
#[test]
fn test_config_default() {
let config = Config::default();
assert!(config.diff_command.is_none());
assert_eq!(config.git.paging.color_arg, "always");
assert!(!config.git.paging.has_pager());
assert!(!config.git.paging.has_external_diff_command());
}
#[test]
fn test_config_serialize_deserialize() {
let mut config = Config::default();
config.git.paging.pager = "delta --dark".to_string();
config.git.paging.color_arg = "always".to_string();
let yaml = serde_yaml::to_string(&config).unwrap();
let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(config.git.paging.pager, deserialized.git.paging.pager);
assert_eq!(
config.git.paging.color_arg,
deserialized.git.paging.color_arg
);
}
#[test]
fn test_pager_configuration() {
let mut config = Config::default();
config.git.paging.pager = "delta --side-by-side".to_string();
let yaml = serde_yaml::to_string(&config).unwrap();
let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(deserialized.git.paging.pager, "delta --side-by-side");
assert!(deserialized.git.paging.has_pager());
}
#[test]
fn test_external_diff_command() {
let mut config = Config::default();
config.git.paging.external_diff_command = "difft --color=always".to_string();
let yaml = serde_yaml::to_string(&config).unwrap();
let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(
deserialized.git.paging.external_diff_command,
"difft --color=always"
);
assert!(deserialized.git.paging.has_external_diff_command());
}
#[test]
fn test_diff_command_type_precedence() {
let mut config = Config::default();
config.git.paging.pager = "delta".to_string();
config.git.paging.external_diff_command = "difft".to_string();
match config.get_diff_command_type() {
DiffCommandType::External(cmd) => assert_eq!(cmd, "difft"),
_ => panic!("Expected external diff command"),
}
config.git.paging.external_diff_command = String::new();
match config.get_diff_command_type() {
DiffCommandType::Pager(cmd) => assert_eq!(cmd, "delta"),
_ => panic!("Expected pager command"),
}
}
#[test]
fn test_config_save_load() -> Result<()> {
let temp_dir = TempDir::new()?;
let temp_home = temp_dir.path().to_path_buf();
unsafe {
env::set_var("HOME", &temp_home);
}
let mut config = Config::default();
config.git.paging.pager = "delta --dark".to_string();
config.save()?;
let loaded_config = Config::load()?;
assert_eq!(config.git.paging.pager, loaded_config.git.paging.pager);
assert_eq!(
config.git.paging.color_arg,
loaded_config.git.paging.color_arg
);
Ok(())
}
}