#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::{
env,
io::{self, IsTerminal},
};
pub mod prelude {
pub use crate::{
ColorSupport, Interactivity, TerminalDimensionError, TerminalHeight, TerminalSize,
TerminalWidth, detect_color_support, stderr_interactivity, stdin_interactivity,
stdout_interactivity,
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalDimensionError {
ZeroWidth,
ZeroHeight,
}
impl fmt::Display for TerminalDimensionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ZeroWidth => formatter.write_str("terminal width must be greater than zero"),
Self::ZeroHeight => formatter.write_str("terminal height must be greater than zero"),
}
}
}
impl std::error::Error for TerminalDimensionError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TerminalWidth {
columns: u16,
}
impl TerminalWidth {
pub const fn new(columns: u16) -> Result<Self, TerminalDimensionError> {
if columns == 0 {
Err(TerminalDimensionError::ZeroWidth)
} else {
Ok(Self { columns })
}
}
#[must_use]
pub const fn columns(self) -> u16 {
self.columns
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TerminalHeight {
rows: u16,
}
impl TerminalHeight {
pub const fn new(rows: u16) -> Result<Self, TerminalDimensionError> {
if rows == 0 {
Err(TerminalDimensionError::ZeroHeight)
} else {
Ok(Self { rows })
}
}
#[must_use]
pub const fn rows(self) -> u16 {
self.rows
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TerminalSize {
width: TerminalWidth,
height: TerminalHeight,
}
impl TerminalSize {
#[must_use]
pub const fn new(width: TerminalWidth, height: TerminalHeight) -> Self {
Self { width, height }
}
pub const fn try_new(columns: u16, rows: u16) -> Result<Self, TerminalDimensionError> {
Ok(Self::new(
match TerminalWidth::new(columns) {
Ok(width) => width,
Err(error) => return Err(error),
},
match TerminalHeight::new(rows) {
Ok(height) => height,
Err(error) => return Err(error),
},
))
}
#[must_use]
pub const fn width(self) -> TerminalWidth {
self.width
}
#[must_use]
pub const fn height(self) -> TerminalHeight {
self.height
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ColorSupport {
NoColor,
Basic,
Ansi256,
TrueColor,
}
impl ColorSupport {
#[must_use]
pub fn from_env_values(
no_color: Option<&str>,
term: Option<&str>,
colorterm: Option<&str>,
) -> Self {
if no_color.is_some() {
return Self::NoColor;
}
if colorterm.is_some_and(|value| {
value.eq_ignore_ascii_case("truecolor") || value.eq_ignore_ascii_case("24bit")
}) {
return Self::TrueColor;
}
match term {
Some(value) if value.eq_ignore_ascii_case("dumb") => Self::NoColor,
Some(value) if value.contains("256color") => Self::Ansi256,
Some("") | None => Self::NoColor,
Some(_) => Self::Basic,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Interactivity {
Interactive,
NonInteractive,
}
impl Interactivity {
#[must_use]
pub const fn from_bool(is_terminal: bool) -> Self {
if is_terminal {
Self::Interactive
} else {
Self::NonInteractive
}
}
#[must_use]
pub const fn is_interactive(self) -> bool {
matches!(self, Self::Interactive)
}
}
#[must_use]
pub fn detect_color_support() -> ColorSupport {
let no_color = env::var_os("NO_COLOR").map(|_| "");
let term = env::var("TERM").ok();
let colorterm = env::var("COLORTERM").ok();
ColorSupport::from_env_values(no_color, term.as_deref(), colorterm.as_deref())
}
#[must_use]
pub fn stdin_interactivity() -> Interactivity {
Interactivity::from_bool(io::stdin().is_terminal())
}
#[must_use]
pub fn stdout_interactivity() -> Interactivity {
Interactivity::from_bool(io::stdout().is_terminal())
}
#[must_use]
pub fn stderr_interactivity() -> Interactivity {
Interactivity::from_bool(io::stderr().is_terminal())
}
#[cfg(test)]
mod tests {
use super::{
ColorSupport, Interactivity, TerminalDimensionError, TerminalHeight, TerminalSize,
TerminalWidth,
};
#[test]
fn validates_terminal_dimensions() -> Result<(), TerminalDimensionError> {
let size = TerminalSize::try_new(80, 24)?;
assert_eq!(size.width().columns(), 80);
assert_eq!(size.height().rows(), 24);
assert_eq!(
TerminalWidth::new(0),
Err(TerminalDimensionError::ZeroWidth)
);
assert_eq!(
TerminalHeight::new(0),
Err(TerminalDimensionError::ZeroHeight)
);
Ok(())
}
#[test]
fn infers_color_support_from_env_values() {
assert_eq!(
ColorSupport::from_env_values(Some("1"), Some("xterm-256color"), Some("truecolor")),
ColorSupport::NoColor
);
assert_eq!(
ColorSupport::from_env_values(None, Some("xterm-256color"), None),
ColorSupport::Ansi256
);
assert_eq!(
ColorSupport::from_env_values(None, Some("xterm"), Some("truecolor")),
ColorSupport::TrueColor
);
assert_eq!(
ColorSupport::from_env_values(None, Some("dumb"), None),
ColorSupport::NoColor
);
}
#[test]
fn converts_interactivity_from_bool() {
assert!(Interactivity::from_bool(true).is_interactive());
assert!(!Interactivity::from_bool(false).is_interactive());
}
}