1use std::cell::Cell;
6use std::fmt::Display;
7use std::io::{self, BufRead, IsTerminal, Write};
8
9thread_local! {
10 static INTERACTIVE: Cell<bool> = const { Cell::new(false) };
11}
12
13pub struct ConsoleGuard {
15 _priv: (),
16}
17
18impl Drop for ConsoleGuard {
19 fn drop(&mut self) {
20 INTERACTIVE.with(|c| c.set(false));
21 }
22}
23
24pub fn enter_cli() -> ConsoleGuard {
26 INTERACTIVE.with(|c| c.set(true));
27 ConsoleGuard { _priv: () }
28}
29
30pub fn is_interactive() -> bool {
32 INTERACTIVE.with(|c| c.get())
33}
34
35pub fn info(msg: impl Display) {
37 if is_interactive() {
38 println!("{msg}");
39 }
40}
41
42pub fn warn(msg: impl Display) {
44 if is_interactive() {
45 eprintln!("warn: {msg}");
46 }
47}
48
49pub fn error(msg: impl Display) {
51 if is_interactive() {
52 eprintln!("error: {msg}");
53 }
54}
55
56pub fn line(msg: impl Display) {
58 if is_interactive() {
59 println!("{msg}");
60 }
61}
62
63pub fn table(headers: &[&str], rows: &[Vec<String>]) {
65 if !is_interactive() {
66 return;
67 }
68 let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
69 for row in rows {
70 for (i, cell) in row.iter().enumerate() {
71 if let Some(w) = widths.get_mut(i) {
72 *w = (*w).max(cell.len());
73 }
74 }
75 }
76 let fmt_row = |cols: &[&str]| {
77 cols.iter()
78 .enumerate()
79 .map(|(i, c)| format!("{:<width$}", c, width = widths.get(i).copied().unwrap_or(0)))
80 .collect::<Vec<_>>()
81 .join(" ")
82 };
83 println!("{}", fmt_row(headers));
84 let sep: String = widths
85 .iter()
86 .map(|w| "-".repeat(*w))
87 .collect::<Vec<_>>()
88 .join(" ");
89 println!("{sep}");
90 for row in rows {
91 let refs: Vec<&str> = row.iter().map(String::as_str).collect();
92 println!("{}", fmt_row(&refs));
93 }
94}
95
96pub fn ask(prompt: impl Display) -> Result<String, String> {
98 if !is_interactive() {
99 return Err("console ask is only available during `tasks run`".into());
100 }
101 print!("{prompt} ");
102 let _ = io::stdout().flush();
103 let mut line = String::new();
104 io::stdin()
105 .lock()
106 .read_line(&mut line)
107 .map_err(|e| e.to_string())?;
108 if !io::stdin().is_terminal() {
110 println!();
111 }
112 Ok(line.trim_end_matches(['\r', '\n']).to_string())
113}
114
115pub fn confirm(prompt: impl Display, default: bool) -> bool {
117 if !is_interactive() {
118 return default;
119 }
120 let hint = if default { "Y/n" } else { "y/N" };
121 print!("{prompt} [{hint}] ");
122 let _ = io::stdout().flush();
123 let mut line = String::new();
124 if io::stdin().lock().read_line(&mut line).is_err() {
125 return default;
126 }
127 if !io::stdin().is_terminal() {
128 println!();
129 }
130 let t = line.trim().to_ascii_lowercase();
131 if t.is_empty() {
132 return default;
133 }
134 matches!(t.as_str(), "y" | "yes")
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn quiet_outside_cli() {
143 assert!(!is_interactive());
144 assert!(ask("x").is_err());
145 assert!(!confirm("x", false));
146 assert!(confirm("x", true));
147 info("should not panic");
148 table(&["a"], &[vec!["1".into()]]);
149 }
150
151 #[test]
152 fn guard_enables_interactive() {
153 {
154 let _g = enter_cli();
155 assert!(is_interactive());
156 }
157 assert!(!is_interactive());
158 }
159}