use crate::AmbiguousWidth;
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;
type AmbiguousWidthDetector = fn() -> Option<AmbiguousWidth>;
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));
static AMBIGUOUS_WIDTH_DETECTOR: Lazy<Mutex<AmbiguousWidthDetector>> =
Lazy::new(|| Mutex::new(default_ambiguous_width_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 set_ambiguous_width_detector(detector: AmbiguousWidthDetector) {
*AMBIGUOUS_WIDTH_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()
}
pub fn detect_ambiguous_width_override() -> Option<AmbiguousWidth> {
let detector = *AMBIGUOUS_WIDTH_DETECTOR.lock().unwrap();
detector()
}
fn default_width_detector() -> Option<usize> {
resolve_terminal_width(std::env::var_os("COLUMNS").as_deref(), || {
terminal_size::terminal_size().map(|(width, _)| width.0 as usize)
})
}
fn resolve_terminal_width(
columns: Option<&std::ffi::OsStr>,
probe_terminal: impl FnOnce() -> Option<usize>,
) -> Option<usize> {
columns
.and_then(std::ffi::OsStr::to_str)
.and_then(|value| value.parse::<usize>().ok())
.filter(|&width| width > 0)
.or_else(probe_terminal)
}
fn default_tty_detector() -> bool {
Term::stdout().is_term()
}
fn default_color_detector() -> bool {
Term::stdout().features().colors_supported()
}
fn default_ambiguous_width_detector() -> Option<AmbiguousWidth> {
None
}
pub fn reset_detectors() {
set_terminal_width_detector(default_width_detector);
set_tty_detector(default_tty_detector);
set_color_capability_detector(default_color_detector);
set_ambiguous_width_detector(default_ambiguous_width_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 proptest::prelude::*;
use serial_test::serial;
use std::ffi::{OsStr, OsString};
struct ColumnsGuard(Option<OsString>);
impl ColumnsGuard {
fn set(value: &str) -> Self {
let original = std::env::var_os("COLUMNS");
std::env::set_var("COLUMNS", value);
Self(original)
}
}
impl Drop for ColumnsGuard {
fn drop(&mut self) {
match self.0.take() {
Some(value) => std::env::set_var("COLUMNS", value),
None => std::env::remove_var("COLUMNS"),
}
}
}
#[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 default_width_resolution_honors_columns() {
let _guard = DetectorGuard::new();
let _columns = ColumnsGuard::set("47");
reset_detectors();
assert_eq!(detect_terminal_width(), Some(47));
}
#[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();
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn columns_values_are_resolved_without_panicking(value in any::<String>()) {
let expected = value
.parse::<usize>()
.ok()
.filter(|&width| width > 0)
.or(Some(73));
prop_assert_eq!(
resolve_terminal_width(Some(OsStr::new(&value)), || Some(73)),
expected,
);
}
#[test]
fn every_positive_columns_width_precedes_the_terminal_probe(width in 1usize..) {
prop_assert_eq!(
resolve_terminal_width(Some(OsStr::new(&width.to_string())), || {
panic!("valid COLUMNS must prevent terminal probing")
}),
Some(width),
);
}
}
}