#[cfg(feature = "cli")]
use clap::Args;
use std::io::BufRead;
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
pub use crate::write::EolMode;
#[derive(Debug, Default)]
#[cfg_attr(feature = "cli", derive(Args))]
pub struct GlobalFlags {
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub json: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub jsonl: bool,
#[cfg_attr(feature = "cli", arg(long, short = 'q', global = true))]
pub quiet: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub verbose: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub cwd: Option<String>,
#[cfg_attr(feature = "cli", arg(long, global = true, action = clap::ArgAction::Append))]
pub glob: Vec<String>,
#[cfg_attr(feature = "cli", arg(long, global = true, action = clap::ArgAction::Append))]
pub exclude: Vec<String>,
#[cfg_attr(feature = "cli", arg(long, global = true, action = clap::ArgAction::Append, value_name = "FILENAME"))]
pub ignore_file: Vec<String>,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub files_from: Option<String>,
#[cfg_attr(
feature = "cli",
arg(long, global = true, value_enum, default_value = "auto")
)]
pub color: ColorMode,
#[cfg_attr(feature = "cli", clap(skip))]
pub diff: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub apply: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub check: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub ensure_final_newline: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub normalize_eol: Option<EolMode>,
#[cfg_attr(feature = "cli", clap(skip))]
pub trim_trailing_whitespace: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub respect_editorconfig: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub collapse_blanks: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub confirm: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub format: Option<String>,
#[cfg_attr(feature = "cli", clap(skip))]
pub format_timeout: Option<u64>,
}
#[derive(Debug, Default)]
#[cfg_attr(feature = "cli", derive(Args))]
pub struct WriteFlags {
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub diff: bool,
#[cfg_attr(feature = "cli", arg(long, global = true, conflicts_with = "check"))]
pub apply: bool,
#[cfg_attr(feature = "cli", arg(long, global = true, conflicts_with = "apply"))]
pub check: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub ensure_final_newline: bool,
#[cfg_attr(feature = "cli", arg(long, global = true, value_enum))]
pub normalize_eol: Option<EolMode>,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub trim_trailing_whitespace: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub respect_editorconfig: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub collapse_blanks: bool,
#[cfg_attr(feature = "cli", arg(long, global = true, conflicts_with_all = ["apply", "check"]))]
pub confirm: bool,
#[cfg_attr(feature = "cli", arg(long, global = true))]
pub format: Option<String>,
#[cfg_attr(feature = "cli", arg(long, global = true, default_value = "30"))]
pub format_timeout: Option<u64>,
}
pub(crate) fn confirm_prompt(prompt: &str) -> bool {
use std::io::Write;
let _ = std::io::stdout().flush();
let is_tty = std::io::IsTerminal::is_terminal(&std::io::stdin());
confirm_prompt_interactive(prompt, is_tty, &mut std::io::stdin().lock())
}
pub(crate) fn confirm_prompt_interactive(
prompt: &str,
is_tty: bool,
reader: &mut impl BufRead,
) -> bool {
if !is_tty {
return false;
}
eprint!("{prompt} [Y/n] ");
let mut buf = String::new();
match reader.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;
self.collapse_blanks = w.collapse_blanks;
self.format = w.format.clone();
self.format_timeout = w.format_timeout;
}
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;
}
#[cfg(feature = "cli")]
{
anstream::stdout().is_terminal()
}
#[cfg(not(feature = "cli"))]
{
false
}
}
}
}
pub fn show_status(&self) -> bool {
!self.quiet && !self.json && !self.jsonl && {
#[cfg(feature = "cli")]
{
anstream::stderr().is_terminal()
}
#[cfg(not(feature = "cli"))]
{
false
}
}
}
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)]
impl GlobalFlags {
pub fn test_default() -> Self {
GlobalFlags {
color: ColorMode::Never,
..GlobalFlags::default()
}
}
#[allow(clippy::field_reassign_with_default)]
pub fn test_with_cwd(dir: &std::path::Path) -> Self {
let mut g = Self::with_cwd(dir);
g.color = ColorMode::Never;
g
}
pub fn test_apply() -> Self {
let mut g = Self::test_default();
g.apply = true;
g
}
}
impl GlobalFlags {
pub fn with_cwd(cwd: impl AsRef<std::path::Path>) -> Self {
GlobalFlags {
cwd: Some(cwd.as_ref().to_string_lossy().into_owned()),
..Default::default()
}
}
pub fn with_cwd_and_json(cwd: impl AsRef<std::path::Path>) -> Self {
GlobalFlags {
cwd: Some(cwd.as_ref().to_string_lossy().into_owned()),
json: true,
..Default::default()
}
}
pub fn with_cwd_and_jsonl(cwd: impl AsRef<std::path::Path>) -> Self {
GlobalFlags {
cwd: Some(cwd.as_ref().to_string_lossy().into_owned()),
jsonl: true,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_apply_returns_true_when_apply_set() {
let g = GlobalFlags::test_apply();
assert!(g.should_apply());
}
#[test]
fn should_apply_returns_false_by_default() {
let g = GlobalFlags::test_default();
assert!(!g.should_apply());
}
#[test]
fn confirm_prompt_non_tty_returns_false() {
let mut reader = std::io::Cursor::new(b"y\n");
assert!(!confirm_prompt_interactive("Apply?", false, &mut reader));
}
#[test]
fn confirm_prompt_tty_accepts_default_yes() {
let mut reader = std::io::Cursor::new(b"\n");
assert!(confirm_prompt_interactive("Apply?", true, &mut reader));
}
#[test]
fn should_apply_confirm_non_tty_returns_false() {
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return;
}
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);
}
#[test]
fn resolve_cwd_nonexistent_errors() {
let g = GlobalFlags {
cwd: Some("/nonexistent/path/that/does/not/exist".into()),
..GlobalFlags::default()
};
let err = g.resolve_cwd().unwrap_err().to_string();
assert!(err.contains("does not exist"), "unexpected: {err}");
}
#[test]
fn resolve_cwd_not_a_directory_errors() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("file.txt");
std::fs::write(&file, "x").unwrap();
let g = GlobalFlags {
cwd: Some(file.to_string_lossy().into_owned()),
..GlobalFlags::default()
};
let err = g.resolve_cwd().unwrap_err().to_string();
assert!(err.contains("not a directory"), "unexpected: {err}");
}
}