use clap::Args;
use std::io::BufRead;
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum EolMode {
#[default]
Keep,
Lf,
Crlf,
}
#[derive(Debug, Default, Args)]
pub struct GlobalFlags {
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub jsonl: bool,
#[arg(long, short = 'q', global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub cwd: Option<String>,
#[arg(long, global = true, action = clap::ArgAction::Append)]
pub glob: Vec<String>,
#[arg(long, global = true)]
pub files_from: Option<String>,
#[arg(long, global = true, value_enum, default_value = "auto")]
pub color: ColorMode,
#[clap(skip)]
pub diff: bool,
#[clap(skip)]
pub apply: bool,
#[clap(skip)]
pub check: bool,
#[clap(skip)]
pub ensure_final_newline: bool,
#[clap(skip)]
pub normalize_eol: Option<EolMode>,
#[clap(skip)]
pub trim_trailing_whitespace: bool,
#[clap(skip)]
pub respect_editorconfig: bool,
#[clap(skip)]
pub confirm: bool,
}
#[derive(Debug, Default, Args)]
pub struct WriteFlags {
#[arg(long, global = true)]
pub diff: bool,
#[arg(long, global = true, conflicts_with = "check")]
pub apply: bool,
#[arg(long, global = true, conflicts_with = "apply")]
pub check: bool,
#[arg(long, global = true)]
pub ensure_final_newline: bool,
#[arg(long, global = true, value_enum)]
pub normalize_eol: Option<EolMode>,
#[arg(long, global = true)]
pub trim_trailing_whitespace: bool,
#[arg(long, global = true)]
pub respect_editorconfig: bool,
#[arg(long, global = true, conflicts_with_all = ["apply", "check"])]
pub confirm: bool,
}
pub(crate) fn confirm_prompt(prompt: &str) -> bool {
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return false;
}
eprint!("{prompt} [Y/n] ");
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) | Err(_) => false,
Ok(_) => {
let answer = buf.trim().to_lowercase();
answer.is_empty() || answer == "y" || answer == "yes"
}
}
}
impl GlobalFlags {
pub fn merge_write(&mut self, w: &WriteFlags) {
self.diff = w.diff;
self.apply = w.apply;
self.check = w.check;
self.confirm = w.confirm;
self.ensure_final_newline = w.ensure_final_newline;
self.normalize_eol = w.normalize_eol;
self.trim_trailing_whitespace = w.trim_trailing_whitespace;
self.respect_editorconfig = w.respect_editorconfig;
}
pub fn should_apply(&self) -> bool {
if self.apply {
return true;
}
self.confirm && confirm_prompt("Apply?")
}
pub fn should_color(&self) -> bool {
match self.color {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
anstream::stdout().is_terminal()
}
}
}
pub fn show_status(&self) -> bool {
!self.quiet && !self.json && !self.jsonl && anstream::stderr().is_terminal()
}
pub fn resolve_cwd(&self) -> anyhow::Result<std::path::PathBuf> {
if let Some(ref cwd) = self.cwd {
let path = std::path::PathBuf::from(cwd);
if !path.exists() {
anyhow::bail!("--cwd directory does not exist: {cwd}");
}
if !path.is_dir() {
anyhow::bail!("--cwd is not a directory: {cwd}");
}
Ok(path)
} else {
std::env::current_dir().map_err(Into::into)
}
}
pub fn emit_json<T: serde::Serialize>(&self, value: &T) -> anyhow::Result<bool> {
if self.json {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(true)
} else if self.jsonl {
println!("{}", serde_json::to_string(value)?);
Ok(true)
} else {
Ok(false)
}
}
pub fn read_files_from(&self) -> anyhow::Result<Option<Vec<String>>> {
let source = match self.files_from.as_deref() {
Some(s) => s,
None => return Ok(None),
};
let lines: Vec<String> = if source == "-" {
std::io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.filter(|l| !l.is_empty())
.collect()
} else {
std::fs::read_to_string(source)
.map_err(|e| anyhow::anyhow!("failed to read --files-from '{}': {e}", source))?
.lines()
.filter(|l| !l.is_empty())
.map(String::from)
.collect()
};
Ok(Some(lines))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_apply_returns_true_when_apply_set() {
let g = GlobalFlags {
apply: true,
..GlobalFlags::default()
};
assert!(g.should_apply());
}
#[test]
fn should_apply_returns_false_by_default() {
let g = GlobalFlags::default();
assert!(!g.should_apply());
}
#[test]
fn should_apply_confirm_non_tty_returns_false() {
let g = GlobalFlags {
confirm: true,
..GlobalFlags::default()
};
assert!(!g.should_apply());
}
#[test]
fn merge_write_copies_confirm() {
let w = WriteFlags {
confirm: true,
..WriteFlags::default()
};
let mut g = GlobalFlags::default();
g.merge_write(&w);
assert!(g.confirm);
}
#[test]
fn verbose_flag_defaults_to_false() {
let g = GlobalFlags::default();
assert!(!g.verbose);
}
}