use console::Term;
use once_cell::sync::Lazy;
use std::sync::Mutex;
type WidthDetector = fn() -> Option<usize>;
type TtyDetector = fn() -> bool;
type ColorDetector = fn() -> bool;
static WIDTH_DETECTOR: Lazy<Mutex<WidthDetector>> =
Lazy::new(|| Mutex::new(default_width_detector));
static TTY_DETECTOR: Lazy<Mutex<TtyDetector>> = Lazy::new(|| Mutex::new(default_tty_detector));
static COLOR_DETECTOR: Lazy<Mutex<ColorDetector>> =
Lazy::new(|| Mutex::new(default_color_detector));
pub fn set_terminal_width_detector(detector: WidthDetector) {
*WIDTH_DETECTOR.lock().unwrap() = detector;
}
pub fn set_tty_detector(detector: TtyDetector) {
*TTY_DETECTOR.lock().unwrap() = detector;
}
pub fn set_color_capability_detector(detector: ColorDetector) {
*COLOR_DETECTOR.lock().unwrap() = detector;
}
pub fn detect_terminal_width() -> Option<usize> {
let detector = *WIDTH_DETECTOR.lock().unwrap();
detector()
}
pub fn detect_is_tty() -> bool {
let detector = *TTY_DETECTOR.lock().unwrap();
detector()
}
pub fn detect_color_capability() -> bool {
let detector = *COLOR_DETECTOR.lock().unwrap();
detector()
}
fn default_width_detector() -> Option<usize> {
terminal_size::terminal_size().map(|(w, _)| w.0 as usize)
}
fn default_tty_detector() -> bool {
Term::stdout().is_term()
}
fn default_color_detector() -> bool {
Term::stdout().features().colors_supported()
}
pub fn reset_detectors() {
set_terminal_width_detector(default_width_detector);
set_tty_detector(default_tty_detector);
set_color_capability_detector(default_color_detector);
}
#[must_use = "the guard only resets detectors when dropped; bind it to a variable"]
pub struct DetectorGuard {
_private: (),
}
impl DetectorGuard {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for DetectorGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for DetectorGuard {
fn drop(&mut self) {
reset_detectors();
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn width_override_is_honored() {
let _guard = DetectorGuard::new();
set_terminal_width_detector(|| Some(42));
assert_eq!(detect_terminal_width(), Some(42));
set_terminal_width_detector(|| None);
assert_eq!(detect_terminal_width(), None);
}
#[test]
#[serial]
fn tty_override_is_honored() {
let _guard = DetectorGuard::new();
set_tty_detector(|| true);
assert!(detect_is_tty());
set_tty_detector(|| false);
assert!(!detect_is_tty());
}
#[test]
#[serial]
fn color_override_is_honored() {
let _guard = DetectorGuard::new();
set_color_capability_detector(|| true);
assert!(detect_color_capability());
set_color_capability_detector(|| false);
assert!(!detect_color_capability());
}
#[test]
#[serial]
fn reset_replaces_panicking_overrides() {
let _guard = DetectorGuard::new();
fn boom_width() -> Option<usize> {
panic!("width detector must not be called after reset")
}
fn boom_bool() -> bool {
panic!("bool detector must not be called after reset")
}
set_terminal_width_detector(boom_width);
set_tty_detector(boom_bool);
set_color_capability_detector(boom_bool);
reset_detectors();
let _ = detect_terminal_width();
let _ = detect_is_tty();
let _ = detect_color_capability();
}
#[test]
#[serial]
fn guard_restores_on_drop() {
{
let _guard = DetectorGuard::new();
set_terminal_width_detector(|| Some(1));
set_tty_detector(|| true);
set_color_capability_detector(|| true);
assert_eq!(detect_terminal_width(), Some(1));
}
fn boom() -> Option<usize> {
panic!("override leaked past guard drop")
}
set_terminal_width_detector(boom);
drop(DetectorGuard::new());
let _ = detect_terminal_width();
}
}