mod app;
mod clipboard;
mod diff_parser;
mod git_diff;
mod payload;
use clap::Parser;
use git_diff::DiffOptions;
const DEFAULT_MAX_UNTRACKED_BYTES: u64 = 1024 * 1024;
#[derive(Debug, Parser)]
#[command(
name = "diffmark",
version,
about = "Review uncommitted git diffs in a TUI and copy inline comments for command-line AI agents."
)]
struct Args {
#[arg(long, default_value_t = 5)]
context: usize,
#[arg(long, default_value_t = 1.0)]
refresh: f64,
#[arg(long, default_value_t = 11)]
max_copy_lines: usize,
#[arg(long)]
no_untracked: bool,
#[arg(long, default_value_t = DEFAULT_MAX_UNTRACKED_BYTES)]
max_untracked_bytes: u64,
#[arg(long)]
no_osc52: bool,
#[arg(value_name = "PATH", trailing_var_arg = true)]
pathspecs: Vec<String>,
}
fn main() {
let code = match run() {
Ok(code) => code,
Err(err) => {
eprintln!("diffmark: {err:#}");
1
}
};
std::process::exit(code);
}
fn run() -> anyhow::Result<i32> {
let args = Args::parse();
if let Some(code) = validation_error_code(&args) {
return Ok(code);
}
app::run_tui(app_config(args))?;
Ok(0)
}
fn validation_error_code(args: &Args) -> Option<i32> {
if args.refresh <= 0.0 {
eprintln!("diffmark: --refresh must be > 0");
return Some(2);
}
if args.max_copy_lines == 0 {
eprintln!("diffmark: --max-copy-lines must be > 0");
return Some(2);
}
if !git_diff::is_git_repo() {
eprintln!("diffmark: not inside a git work tree");
return Some(2);
}
None
}
fn app_config(args: Args) -> app::AppConfig {
app::AppConfig {
diff: DiffOptions {
context: args.context,
include_untracked: !args.no_untracked,
max_untracked_bytes: args.max_untracked_bytes,
pathspecs: args.pathspecs,
},
refresh_interval: std::time::Duration::from_secs_f64(args.refresh),
max_copy_lines: args.max_copy_lines,
allow_osc52: !args.no_osc52,
}
}