use std::io::IsTerminal;
pub fn stdout_is_tty() -> bool {
std::io::stdout().is_terminal()
}
pub fn stdin_is_tty() -> bool {
std::io::stdin().is_terminal()
}
pub fn wants_json(json_flag: bool) -> bool {
wants_json_inner(json_flag, stdout_is_tty())
}
pub fn wants_json_inner(json_flag: bool, stdout_tty: bool) -> bool {
json_flag || !stdout_tty
}
#[allow(dead_code)]
pub fn confirm(prompt: &str, bypass_flag: &str) -> Result<bool, String> {
confirm_inner(
prompt,
bypass_flag,
stdin_is_tty(),
&mut std::io::stdin().lock(),
&mut std::io::stderr(),
)
}
#[allow(dead_code)]
pub fn confirm_inner<R: std::io::BufRead, W: std::io::Write>(
prompt: &str,
bypass_flag: &str,
stdin_tty: bool,
reader: &mut R,
writer: &mut W,
) -> Result<bool, String> {
if !stdin_tty {
return Err(format!(
"interactive confirmation required; re-run with {bypass_flag} to proceed non-interactively"
));
}
let _ = write!(writer, "{prompt} [y/N] ");
let _ = writer.flush();
let mut line = String::new();
reader
.read_line(&mut line)
.map_err(|e| format!("failed to read confirmation: {e}"))?;
let answer = line.trim().to_ascii_lowercase();
Ok(answer == "y" || answer == "yes")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wants_json_explicit_flag_always_wins() {
assert!(wants_json_inner(true, true));
assert!(wants_json_inner(true, false));
}
#[test]
fn wants_json_piped_auto_upgrades() {
assert!(wants_json_inner(false, false));
}
#[test]
fn wants_json_interactive_terminal_stays_human() {
assert!(!wants_json_inner(false, true));
}
#[test]
fn confirm_refuses_without_tty_and_names_bypass_flag() {
let mut input: &[u8] = b"";
let mut out: Vec<u8> = Vec::new();
let err = confirm_inner("Delete everything?", "--yes", false, &mut input, &mut out)
.expect_err("must refuse when stdin is not a TTY");
assert!(
err.contains("--yes"),
"error should name the bypass flag, got: {err}"
);
assert!(err.contains("interactive"), "error should explain why: {err}");
}
#[test]
fn confirm_reads_yes_on_tty() {
let mut input: &[u8] = b"y\n";
let mut out: Vec<u8> = Vec::new();
let ok = confirm_inner("Proceed?", "--yes", true, &mut input, &mut out).unwrap();
assert!(ok);
}
#[test]
fn confirm_reads_no_on_tty() {
let mut input: &[u8] = b"n\n";
let mut out: Vec<u8> = Vec::new();
let ok = confirm_inner("Proceed?", "--yes", true, &mut input, &mut out).unwrap();
assert!(!ok);
}
#[test]
fn confirm_empty_input_defaults_to_no() {
let mut input: &[u8] = b"\n";
let mut out: Vec<u8> = Vec::new();
let ok = confirm_inner("Proceed?", "--yes", true, &mut input, &mut out).unwrap();
assert!(!ok);
}
}