1use anyhow::{bail, Result};
6use owo_colors::{AnsiColors, OwoColorize};
7use std::{
8 cell::RefCell,
9 fmt,
10 io::{stderr, stdout, IsTerminal, Write},
11 str::FromStr,
12};
13
14pub use owo_colors::AnsiColors as Colors;
15
16#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
18pub enum Color {
19 #[default]
22 Auto,
23 Never,
25 Always,
27}
28
29impl FromStr for Color {
30 type Err = anyhow::Error;
31
32 fn from_str(value: &str) -> Result<Self> {
33 match value {
34 "auto" => Ok(Self::Auto),
35 "never" => Ok(Self::Never),
36 "always" => Ok(Self::Always),
37 _ => bail!("argument for --color must be auto, always, or never, but found `{value}`"),
38 }
39 }
40}
41
42impl fmt::Display for Color {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Auto => write!(f, "auto"),
46 Self::Never => write!(f, "never"),
47 Self::Always => write!(f, "always"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum Verbosity {
55 Verbose,
57 Normal,
59 Quiet,
61}
62
63pub(crate) struct TerminalState {
64 pub(crate) output: Output,
65 verbosity: Verbosity,
66 pub(crate) needs_clear: bool,
67}
68
69impl TerminalState {
70 pub(crate) fn clear_stderr(&mut self) {
72 if self.needs_clear {
73 if self.output.supports_color() {
74 imp::stderr_erase_line();
75 }
76
77 self.needs_clear = false;
78 }
79 }
80}
81
82impl fmt::Debug for TerminalState {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match &self.output {
85 Output::Write(_) => f
86 .debug_struct("Terminal")
87 .field("verbosity", &self.verbosity)
88 .finish(),
89 Output::Stream { color, .. } => f
90 .debug_struct("Terminal")
91 .field("verbosity", &self.verbosity)
92 .field("color", color)
93 .finish(),
94 }
95 }
96}
97
98#[derive(Debug)]
102pub struct Terminal(RefCell<TerminalState>);
103
104impl Terminal {
105 pub fn new(verbosity: Verbosity, color: Color) -> Self {
107 Self(RefCell::new(TerminalState {
108 output: Output::Stream {
109 is_terminal: stderr().is_terminal(),
110 color,
111 },
112 verbosity,
113 needs_clear: false,
114 }))
115 }
116
117 pub fn from_write(out: Box<dyn Write>) -> Self {
119 Self(RefCell::new(TerminalState {
120 output: Output::Write(out),
121 verbosity: Verbosity::Verbose,
122 needs_clear: false,
123 }))
124 }
125
126 pub fn status<T, U>(&self, status: T, message: U) -> Result<()>
128 where
129 T: fmt::Display,
130 U: fmt::Display,
131 {
132 let status_green = status.green();
133
134 let status = if self.0.borrow().output.supports_color() {
135 &status_green as &dyn fmt::Display
136 } else {
137 &status
138 };
139
140 self.print(status, Some(&message), true)
141 }
142
143 pub fn status_with_color<T, U>(
145 &self,
146 status: T,
147 message: U,
148 color: owo_colors::AnsiColors,
149 ) -> Result<()>
150 where
151 T: fmt::Display,
152 U: fmt::Display,
153 {
154 let status_color = status.color(color);
155
156 let status = if self.0.borrow().output.supports_color() {
157 &status_color as &dyn fmt::Display
158 } else {
159 &status
160 };
161
162 self.print(status, Some(&message), true)
163 }
164
165 pub fn note<T: fmt::Display>(&self, message: T) -> Result<()> {
167 let status = "note";
168 let status_cyan = status.cyan();
169
170 let status = if self.0.borrow().output.supports_color() {
171 &status_cyan as &dyn fmt::Display
172 } else {
173 &status
174 };
175
176 self.print(status, Some(&message), false)
177 }
178
179 pub fn warn<T: fmt::Display>(&self, message: T) -> Result<()> {
181 let status = "warning";
182 let status_yellow = status.yellow();
183
184 let status = if self.0.borrow().output.supports_color() {
185 &status_yellow as &dyn fmt::Display
186 } else {
187 &status
188 };
189
190 self.print(status, Some(&message), false)
191 }
192
193 pub fn error<T: fmt::Display>(&self, message: T) -> Result<()> {
195 let status = "error";
196 let status_red = status.red();
197
198 let status = if self.0.borrow().output.supports_color() {
199 &status_red as &dyn fmt::Display
200 } else {
201 &status
202 };
203
204 let mut state = self.0.borrow_mut();
206 state.clear_stderr();
207 state.output.print(status, Some(&message), false)
208 }
209
210 pub fn write_stdout(
214 &self,
215 fragment: impl fmt::Display,
216 color: Option<AnsiColors>,
217 ) -> Result<()> {
218 self.0.borrow_mut().output.write_stdout(fragment, color)
219 }
220
221 fn print(
223 &self,
224 status: &dyn fmt::Display,
225 message: Option<&dyn fmt::Display>,
226 justified: bool,
227 ) -> Result<()> {
228 let mut state = self.0.borrow_mut();
229 match state.verbosity {
230 Verbosity::Quiet => Ok(()),
231 _ => {
232 state.clear_stderr();
233 state.output.print(status, message, justified)
234 }
235 }
236 }
237
238 pub fn width(&self) -> Option<usize> {
240 match &self.0.borrow().output {
241 Output::Stream { .. } => imp::stderr_width(),
242 _ => None,
243 }
244 }
245
246 pub fn verbosity(&self) -> Verbosity {
248 self.0.borrow().verbosity
249 }
250
251 pub(crate) fn state_mut(&self) -> std::cell::RefMut<'_, TerminalState> {
252 self.0.borrow_mut()
253 }
254}
255
256pub(crate) enum Output {
258 Write(Box<dyn Write>),
260 Stream { is_terminal: bool, color: Color },
262}
263
264impl Output {
265 pub(crate) fn supports_color(&self) -> bool {
266 match self {
267 Output::Write(_) => false,
268 Output::Stream { is_terminal, color } => match color {
269 Color::Auto => *is_terminal,
270 Color::Never => false,
271 Color::Always => true,
272 },
273 }
274 }
275
276 pub(crate) fn print(
278 &mut self,
279 status: &dyn fmt::Display,
280 message: Option<&dyn fmt::Display>,
281 justified: bool,
282 ) -> Result<()> {
283 match *self {
284 Output::Stream { .. } => {
285 let stderr = &mut stderr();
286 let status_bold = status.bold();
287
288 let status = if self.supports_color() {
289 &status_bold as &dyn fmt::Display
290 } else {
291 &status
292 };
293
294 if justified {
295 write!(stderr, "{status:>12}")?;
296 } else {
297 write!(stderr, "{status}:")?;
298 }
299
300 match message {
301 Some(message) => writeln!(stderr, " {}", message)?,
302 None => write!(stderr, " ")?,
303 }
304 }
305 Output::Write(ref mut w) => {
306 if justified {
307 write!(w, "{status:>12}")?;
308 } else {
309 write!(w, "{status}:")?;
310 }
311 match message {
312 Some(message) => writeln!(w, " {}", message)?,
313 None => write!(w, " ")?,
314 }
315 }
316 }
317
318 Ok(())
319 }
320
321 fn write_stdout(
322 &mut self,
323 fragment: impl fmt::Display,
324 color: Option<AnsiColors>,
325 ) -> Result<()> {
326 match *self {
327 Self::Stream { .. } => {
328 let mut stdout = stdout();
329
330 match color {
331 Some(color) => {
332 let colored_fragment = fragment.color(color);
333 let fragment: &dyn fmt::Display = if self.supports_color() {
334 &colored_fragment as &dyn fmt::Display
335 } else {
336 &fragment
337 };
338
339 write!(stdout, "{fragment}")?;
340 }
341 None => write!(stdout, "{fragment}")?,
342 }
343 }
344 Self::Write(ref mut w) => {
345 write!(w, "{fragment}")?;
346 }
347 }
348 Ok(())
349 }
350}
351
352#[cfg(unix)]
353mod imp {
354 use std::mem;
355
356 pub fn stderr_width() -> Option<usize> {
357 unsafe {
358 let mut winsize: libc::winsize = mem::zeroed();
359 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
362 return None;
363 }
364 if winsize.ws_col > 0 {
365 Some(winsize.ws_col as usize)
366 } else {
367 None
368 }
369 }
370 }
371
372 pub fn stderr_erase_line() {
373 eprint!("\x1B[K");
377 }
378}
379
380#[cfg(windows)]
381mod imp {
382 use std::{cmp, mem, ptr};
383 use windows_sys::core::PCSTR;
384 use windows_sys::Win32::Foundation::CloseHandle;
385 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
386 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
387 use windows_sys::Win32::Storage::FileSystem::{
388 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
389 };
390 use windows_sys::Win32::System::Console::{
391 GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
392 };
393
394 pub fn stderr_width() -> Option<usize> {
395 unsafe {
396 let stdout = GetStdHandle(STD_ERROR_HANDLE);
397 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
398 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
399 return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
400 }
401
402 let h = CreateFileA(
406 "CONOUT$\0".as_ptr() as PCSTR,
407 GENERIC_READ | GENERIC_WRITE,
408 FILE_SHARE_READ | FILE_SHARE_WRITE,
409 ptr::null_mut(),
410 OPEN_EXISTING,
411 0,
412 0,
413 );
414
415 if h == INVALID_HANDLE_VALUE {
416 return None;
417 }
418
419 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
420 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
421 CloseHandle(h);
422 if rc != 0 {
423 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
424 return Some(cmp::min(60, width));
433 }
434
435 None
436 }
437 }
438
439 pub fn stderr_erase_line() {
440 match stderr_width() {
441 Some(width) => {
442 let blank = " ".repeat(width);
443 eprint!("{}\r", blank);
444 }
445 _ => (),
446 }
447 }
448}