use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "ftdv")]
#[command(about = "A TUI diff pager inspired by diffnav")]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(value_name = "REF_OR_PATH")]
pub targets: Vec<String>,
#[arg(long, short)]
pub cached: bool,
#[arg(long, short)]
pub worktree: bool,
#[arg(long, value_name = "FILE")]
pub config: Option<String>,
#[arg(long, short)]
pub verbose: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Diff {
target1: String,
target2: Option<String>,
#[arg(long)]
cached: bool,
},
Status,
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
impl Cli {
pub fn parse_args() -> Self {
Cli::parse()
}
pub fn get_operation_mode(&self) -> OperationMode {
if let Some(command) = &self.command {
match command {
Commands::Diff {
target1,
target2,
cached,
} => {
if *cached {
OperationMode::GitCached
} else if let Some(target2) = target2 {
OperationMode::Compare {
target1: target1.clone(),
target2: target2.clone(),
}
} else {
OperationMode::GitDiff {
target: target1.clone(),
}
}
}
Commands::Status => OperationMode::GitStatus,
Commands::Completions { shell } => OperationMode::Completions { shell: *shell },
}
} else if self.cached {
OperationMode::GitCached
} else if self.targets.is_empty() {
OperationMode::GitWorkingDirectory
} else if self.targets.len() == 1 {
OperationMode::GitDiff {
target: self.targets[0].clone(),
}
} else if self.targets.len() == 2 {
OperationMode::Compare {
target1: self.targets[0].clone(),
target2: self.targets[1].clone(),
}
} else {
OperationMode::Invalid {
reason: "Too many arguments provided".to_string(),
}
}
}
}
#[derive(Debug, Clone)]
pub enum OperationMode {
GitWorkingDirectory,
GitCached,
GitDiff { target: String },
GitStatus,
Compare { target1: String, target2: String },
Completions { shell: clap_complete::Shell },
Invalid { reason: String },
}
impl OperationMode {
pub fn requires_git_repo(&self) -> bool {
match self {
OperationMode::GitWorkingDirectory
| OperationMode::GitCached
| OperationMode::GitDiff { .. }
| OperationMode::GitStatus => true,
OperationMode::Compare { .. }
| OperationMode::Completions { .. }
| OperationMode::Invalid { .. } => false,
}
}
#[allow(dead_code)]
pub fn description(&self) -> String {
match self {
OperationMode::GitWorkingDirectory => "Working directory changes".to_string(),
OperationMode::GitCached => "Staged changes".to_string(),
OperationMode::GitDiff { target } => format!("Changes from {target}"),
OperationMode::GitStatus => "Git status with diffs".to_string(),
OperationMode::Compare { target1, target2 } => {
format!("Comparing {target1} with {target2}")
}
OperationMode::Completions { .. } => "Generating completions".to_string(),
OperationMode::Invalid { reason } => format!("Invalid: {reason}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_args_gives_working_directory() {
let cli = Cli {
command: None,
targets: vec![],
cached: false,
worktree: false,
config: None,
verbose: false,
};
match cli.get_operation_mode() {
OperationMode::GitWorkingDirectory => (),
_ => panic!("Expected GitWorkingDirectory mode"),
}
}
#[test]
fn test_cached_flag() {
let cli = Cli {
command: None,
targets: vec![],
cached: true,
worktree: false,
config: None,
verbose: false,
};
match cli.get_operation_mode() {
OperationMode::GitCached => (),
_ => panic!("Expected GitCached mode"),
}
}
#[test]
fn test_single_target() {
let cli = Cli {
command: None,
targets: vec!["branch1".to_string()],
cached: false,
worktree: false,
config: None,
verbose: false,
};
match cli.get_operation_mode() {
OperationMode::GitDiff { target } => assert_eq!(target, "branch1"),
_ => panic!("Expected GitDiff mode"),
}
}
#[test]
fn test_two_targets() {
let cli = Cli {
command: None,
targets: vec!["branch1".to_string(), "branch2".to_string()],
cached: false,
worktree: false,
config: None,
verbose: false,
};
match cli.get_operation_mode() {
OperationMode::Compare { target1, target2 } => {
assert_eq!(target1, "branch1");
assert_eq!(target2, "branch2");
}
_ => panic!("Expected Compare mode"),
}
}
}