use std::io::IsTerminal;
use super::text::terminal_width;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMode {
#[default]
Rich,
Plain,
Json,
Quiet,
}
impl OutputMode {
#[must_use]
pub const fn supports_color(&self) -> bool {
matches!(self, Self::Rich)
}
#[must_use]
pub const fn is_structured(&self) -> bool {
matches!(self, Self::Json)
}
#[must_use]
pub const fn is_quiet(&self) -> bool {
matches!(self, Self::Quiet)
}
#[must_use]
pub const fn is_human_readable(&self) -> bool {
matches!(self, Self::Rich | Self::Plain)
}
}
#[derive(Debug, Clone)]
pub struct OutputContext {
mode: OutputMode,
width: usize,
height: Option<usize>,
is_tty: bool,
}
impl Default for OutputContext {
fn default() -> Self {
Self::detect()
}
}
impl OutputContext {
#[must_use]
pub const fn new(mode: OutputMode, width: usize, height: Option<usize>, is_tty: bool) -> Self {
Self {
mode,
width,
height,
is_tty,
}
}
#[must_use]
pub fn detect() -> Self {
let is_tty = std::io::stdout().is_terminal();
let width = terminal_width();
let height = terminal_height();
let no_color = std::env::var("NO_COLOR").is_ok();
let mode = if no_color || !is_tty {
OutputMode::Plain
} else {
OutputMode::Rich
};
Self {
mode,
width,
height,
is_tty,
}
}
#[must_use]
pub fn json() -> Self {
Self {
mode: OutputMode::Json,
..Self::detect()
}
}
#[must_use]
pub fn quiet() -> Self {
Self {
mode: OutputMode::Quiet,
..Self::detect()
}
}
#[must_use]
pub fn with_mode(mode: OutputMode) -> Self {
Self {
mode,
..Self::detect()
}
}
#[must_use]
pub fn from_flags(json: bool, quiet: bool, no_color: bool) -> Self {
let is_tty = std::io::stdout().is_terminal();
let width = terminal_width();
let height = terminal_height();
let mode = if json {
OutputMode::Json
} else if quiet {
OutputMode::Quiet
} else if no_color || !is_tty {
OutputMode::Plain
} else {
OutputMode::Rich
};
Self {
mode,
width,
height,
is_tty,
}
}
#[must_use]
pub const fn mode(&self) -> OutputMode {
self.mode
}
#[must_use]
pub const fn width(&self) -> usize {
self.width
}
#[must_use]
pub const fn height(&self) -> Option<usize> {
self.height
}
#[must_use]
pub const fn is_tty(&self) -> bool {
self.is_tty
}
#[must_use]
pub const fn supports_color(&self) -> bool {
self.mode.supports_color()
}
#[must_use]
pub const fn with_mode_override(mut self, mode: OutputMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub const fn with_width(mut self, width: usize) -> Self {
self.width = width;
self
}
}
#[must_use]
pub fn terminal_height() -> Option<usize> {
if let Ok(lines) = std::env::var("LINES")
&& let Ok(value) = lines.trim().parse::<usize>()
&& value > 0
{
return Some(value);
}
if let Ok((_, rows)) = crossterm::terminal::size()
&& rows > 0
{
return Some(rows as usize);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_mode_supports_color() {
assert!(OutputMode::Rich.supports_color());
assert!(!OutputMode::Plain.supports_color());
assert!(!OutputMode::Json.supports_color());
assert!(!OutputMode::Quiet.supports_color());
}
#[test]
fn test_output_mode_is_structured() {
assert!(!OutputMode::Rich.is_structured());
assert!(!OutputMode::Plain.is_structured());
assert!(OutputMode::Json.is_structured());
assert!(!OutputMode::Quiet.is_structured());
}
#[test]
fn test_output_mode_is_human_readable() {
assert!(OutputMode::Rich.is_human_readable());
assert!(OutputMode::Plain.is_human_readable());
assert!(!OutputMode::Json.is_human_readable());
assert!(!OutputMode::Quiet.is_human_readable());
}
#[test]
fn test_context_from_flags_json() {
let ctx = OutputContext::from_flags(true, false, false);
assert_eq!(ctx.mode(), OutputMode::Json);
}
#[test]
fn test_context_from_flags_quiet() {
let ctx = OutputContext::from_flags(false, true, false);
assert_eq!(ctx.mode(), OutputMode::Quiet);
}
#[test]
fn test_context_from_flags_no_color() {
let ctx = OutputContext::from_flags(false, false, true);
assert_eq!(ctx.mode(), OutputMode::Plain);
}
#[test]
fn test_context_json_constructor() {
let ctx = OutputContext::json();
assert_eq!(ctx.mode(), OutputMode::Json);
}
#[test]
fn test_context_quiet_constructor() {
let ctx = OutputContext::quiet();
assert_eq!(ctx.mode(), OutputMode::Quiet);
}
#[test]
fn test_context_width_override() {
let ctx = OutputContext::detect().with_width(120);
assert_eq!(ctx.width(), 120);
}
#[test]
fn test_context_mode_override() {
let ctx = OutputContext::detect().with_mode_override(OutputMode::Plain);
assert_eq!(ctx.mode(), OutputMode::Plain);
}
#[test]
fn test_terminal_width_fallback() {
let width = terminal_width();
assert!(width >= 80);
}
}