use std::io::IsTerminal;
use crate::cli::output::{OutputConfig, OutputFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Live,
Append,
Plain,
}
#[derive(Debug)]
pub struct Glyphs {
pub start: &'static str,
pub bar: &'static str,
pub end: &'static str,
pub done: &'static str,
pub active: &'static str,
pub ok: &'static str,
pub warn: &'static str,
pub fail: &'static str,
pub dot: &'static str,
pub arrow: &'static str,
pub ellipsis: &'static str,
pub on: &'static str,
pub off: &'static str,
pub spinner: &'static [&'static str],
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub horizontal: &'static str,
pub vertical: &'static str,
}
pub const UNICODE: Glyphs = Glyphs {
start: "┌",
bar: "│",
end: "└",
done: "◇",
active: "◆",
ok: "✓",
warn: "!",
fail: "✗",
dot: "·",
arrow: "›",
ellipsis: "…",
on: "●",
off: "○",
spinner: &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
top_left: "╭",
top_right: "╮",
bottom_left: "╰",
bottom_right: "╯",
horizontal: "─",
vertical: "│",
};
pub const ASCII: Glyphs = Glyphs {
start: "+",
bar: "|",
end: "+",
done: "+",
active: ">",
ok: "+",
warn: "!",
fail: "x",
dot: "-",
arrow: ">",
ellipsis: "...",
on: "(*)",
off: "( )",
spinner: &["|", "/", "-", "\\"],
top_left: "+",
top_right: "+",
bottom_left: "+",
bottom_right: "+",
horizontal: "-",
vertical: "|",
};
const MIN_WIDTH: usize = 60;
const MAX_WIDTH: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Caps {
pub mode: Mode,
pub unicode: bool,
pub color: bool,
pub verbose: bool,
pub width: usize,
}
impl Caps {
pub fn new(mode: Mode, color: bool, verbose: bool, width: usize) -> Self {
Self {
mode,
unicode: mode == Mode::Live,
color: color && mode == Mode::Live,
verbose,
width: width.clamp(MIN_WIDTH, MAX_WIDTH),
}
}
pub fn detect(output: &OutputConfig) -> Option<Self> {
if output.format == OutputFormat::Json || output.quiet {
return None;
}
let mode = if !std::io::stderr().is_terminal() {
Mode::Plain
} else if enable_vt() {
Mode::Live
} else {
Mode::Append
};
Some(Self::new(
mode,
output.color,
output.verbose,
stderr_width(),
))
}
pub fn glyphs(&self) -> &'static Glyphs {
if self.unicode {
&UNICODE
} else {
&ASCII
}
}
}
fn stderr_width() -> usize {
console::Term::stderr()
.size_checked()
.map_or(80, |(_, cols)| usize::from(cols))
}
#[cfg(windows)]
pub fn enable_vt() -> bool {
use winapi::um::winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE};
enable_vt_on(STD_OUTPUT_HANDLE);
enable_vt_on(STD_ERROR_HANDLE)
}
#[cfg(windows)]
fn enable_vt_on(std_handle: u32) -> bool {
use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::processenv::GetStdHandle;
use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
unsafe {
let handle = GetStdHandle(std_handle);
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return false;
}
let mut mode = 0;
if GetConsoleMode(handle, &mut mode) == 0 {
return false;
}
if mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
return true;
}
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
}
}
#[cfg(not(windows))]
pub fn enable_vt() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_live_keeps_colour_and_unicode() {
let caps = Caps::new(Mode::Live, true, false, 80);
assert!(caps.unicode && caps.color);
assert_eq!(caps.glyphs().done, "◇");
}
#[test]
fn test_append_and_plain_are_ascii_without_colour() {
for mode in [Mode::Append, Mode::Plain] {
let caps = Caps::new(mode, true, false, 80);
assert!(!caps.unicode, "{mode:?} must use ASCII");
assert!(!caps.color, "{mode:?} must not colour");
assert!(caps.glyphs().spinner.iter().all(|f| f.is_ascii()));
}
}
#[test]
fn test_width_is_clamped() {
assert_eq!(Caps::new(Mode::Plain, false, false, 20).width, 60);
assert_eq!(Caps::new(Mode::Plain, false, false, 300).width, 100);
assert_eq!(Caps::new(Mode::Plain, false, false, 80).width, 80);
}
#[test]
fn test_ascii_set_is_ascii() {
let g = &ASCII;
for s in [
g.start,
g.bar,
g.end,
g.done,
g.active,
g.ok,
g.warn,
g.fail,
g.dot,
g.arrow,
g.ellipsis,
g.on,
g.off,
g.top_left,
g.top_right,
g.bottom_left,
g.bottom_right,
g.horizontal,
g.vertical,
] {
assert!(s.is_ascii(), "{s:?} is not ASCII");
}
}
#[test]
fn test_json_and_quiet_open_no_rail() {
let mut out = OutputConfig {
format: OutputFormat::Json,
verbose: false,
debug: false,
quiet: false,
color: false,
};
assert!(Caps::detect(&out).is_none());
out.format = OutputFormat::Human;
out.quiet = true;
assert!(Caps::detect(&out).is_none());
}
}