use super::adaptive::{
AdaptiveColor, Background, ColorLevel, detect_background, detect_color_level,
detect_hyperlink_support,
};
use super::theme::Theme;
use super::writer::OutputWriter;
use colored::Color;
use serde::Serialize;
use std::env;
#[derive(Debug, thiserror::Error)]
pub enum OutputError {
#[error(transparent)]
Encoding(#[from] serde_json::Error),
#[error("structured output could not be delivered")]
Delivery(#[source] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMode {
#[default]
Human,
Plain,
Json,
Quiet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Json,
Toon,
}
impl OutputFormat {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_lowercase().as_str() {
"json" => Some(Self::Json),
"toon" => Some(Self::Toon),
_ => None,
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Json => write!(f, "json"),
OutputFormat::Toon => write!(f, "toon"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Verbosity {
Quiet,
#[default]
Normal,
Verbose,
}
#[derive(Debug, Clone, Copy)]
pub struct TerminalCaps {
pub width: u16,
pub height: u16,
pub supports_color: bool,
pub supports_unicode: bool,
pub color_level: ColorLevel,
pub supports_hyperlinks: bool,
pub background: Background,
}
impl Default for TerminalCaps {
fn default() -> Self {
Self {
width: 80,
height: 24,
supports_color: false,
supports_unicode: false,
color_level: ColorLevel::default(),
supports_hyperlinks: false,
background: Background::default(),
}
}
}
impl TerminalCaps {
pub fn detect() -> Self {
let (width, height) = terminal_size::terminal_size()
.map(|(w, h)| (w.0, h.0))
.unwrap_or((80, 24));
let supports_color = Self::detect_color_support();
let supports_unicode = Self::detect_unicode_support();
let color_level = detect_color_level();
let supports_hyperlinks = detect_hyperlink_support();
let background = detect_background();
Self {
width,
height,
supports_color,
supports_unicode,
color_level,
supports_hyperlinks,
background,
}
}
fn detect_color_support() -> bool {
if env::var("NO_COLOR").is_ok() {
return false;
}
if let Some(forced) = force_color_env() {
return forced;
}
if let Ok(term) = env::var("TERM")
&& term == "dumb"
{
return false;
}
if env::var("CLICOLOR_FORCE").is_ok() {
return true;
}
is_terminal::is_terminal(std::io::stdout())
}
fn detect_unicode_support() -> bool {
for var in &["LC_ALL", "LC_CTYPE", "LANG"] {
if let Ok(val) = env::var(var) {
let val_lower = val.to_lowercase();
if val_lower.contains("utf-8") || val_lower.contains("utf8") {
return true;
}
}
}
if env::var("WT_SESSION").is_ok() {
return true;
}
#[cfg(windows)]
{
if env::var("ConEmuANSI").is_ok() {
return true;
}
return false;
}
#[cfg(not(windows))]
{
true
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OutputConfig {
pub force_mode: Option<OutputMode>,
pub color: ColorChoice,
pub format: OutputFormat,
pub json: bool,
pub verbose: bool,
pub quiet: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"always" | "yes" | "true" => Self::Always,
"never" | "no" | "false" => Self::Never,
_ => Self::Auto,
}
}
}
#[derive(Debug)]
pub struct OutputContext {
mode: OutputMode,
verbosity: Verbosity,
caps: TerminalCaps,
theme: Theme,
format: OutputFormat,
stdout: OutputWriter,
stderr: OutputWriter,
}
impl OutputContext {
pub fn new(config: OutputConfig) -> Self {
let stdout = OutputWriter::stdout();
let stderr = OutputWriter::stderr();
Self::with_writers(config, stdout, stderr)
}
pub fn with_writers(config: OutputConfig, stdout: OutputWriter, stderr: OutputWriter) -> Self {
let caps = TerminalCaps::detect();
let mode = Self::detect_mode(&config, &stdout, &caps);
let verbosity = Self::detect_verbosity(&config);
let format = config.format;
let colors_enabled = match config.color {
ColorChoice::Always => true,
ColorChoice::Never => false,
ColorChoice::Auto => mode == OutputMode::Human && caps.supports_color,
};
let supports_unicode = caps.supports_unicode;
let supports_hyperlinks = mode == OutputMode::Human && caps.supports_hyperlinks;
if config.color == ColorChoice::Auto
&& env::var("NO_COLOR").is_err()
&& force_color_env().is_some()
{
colored::control::set_override(colors_enabled);
}
let theme = Theme::new(colors_enabled, supports_unicode, supports_hyperlinks);
Self {
mode,
verbosity,
caps,
theme,
format,
stdout,
stderr,
}
}
fn detect_mode(
config: &OutputConfig,
stdout: &OutputWriter,
caps: &TerminalCaps,
) -> OutputMode {
if let Some(mode) = config.force_mode {
return mode;
}
if config.json {
return OutputMode::Json;
}
if config.quiet {
return OutputMode::Quiet;
}
if config.color == ColorChoice::Never {
return OutputMode::Plain;
}
if config.color == ColorChoice::Always {
return OutputMode::Human;
}
if env::var("NO_COLOR").is_ok() {
return OutputMode::Plain;
}
if let Some(forced) = force_color_env() {
return if forced {
OutputMode::Human
} else {
OutputMode::Plain
};
}
if env::var("CLICOLOR_FORCE").is_ok() {
return OutputMode::Human;
}
if env::var("TERM").is_ok_and(|t| t == "dumb") {
return OutputMode::Plain;
}
if !stdout.is_tty() {
return OutputMode::Plain;
}
if !caps.supports_color {
return OutputMode::Plain;
}
OutputMode::Human
}
fn detect_verbosity(config: &OutputConfig) -> Verbosity {
if config.quiet {
Verbosity::Quiet
} else if config.verbose {
Verbosity::Verbose
} else {
Verbosity::Normal
}
}
pub fn mode(&self) -> OutputMode {
self.mode
}
pub fn format(&self) -> OutputFormat {
self.format
}
pub fn verbosity(&self) -> Verbosity {
self.verbosity
}
pub fn is_tty(&self) -> bool {
self.stdout.is_tty()
}
pub fn is_json(&self) -> bool {
self.mode == OutputMode::Json
}
pub fn is_toon(&self) -> bool {
self.is_json() && self.format == OutputFormat::Toon
}
pub fn is_quiet(&self) -> bool {
self.mode == OutputMode::Quiet || self.verbosity == Verbosity::Quiet
}
pub fn is_verbose(&self) -> bool {
self.verbosity == Verbosity::Verbose
}
pub fn terminal_width(&self) -> u16 {
self.caps.width
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn style(&self) -> &Theme {
&self.theme
}
pub fn is_light_background(&self) -> bool {
self.caps.background == Background::Light
}
pub fn is_dark_background(&self) -> bool {
self.caps.background == Background::Dark
}
pub fn background(&self) -> Background {
self.caps.background
}
pub fn color_level(&self) -> ColorLevel {
if self.mode == OutputMode::Plain {
ColorLevel::None
} else {
self.caps.color_level
}
}
pub fn supports_hyperlinks(&self) -> bool {
self.mode == OutputMode::Human && self.caps.supports_hyperlinks
}
pub fn resolve_color(&self, adaptive: &AdaptiveColor) -> Color {
adaptive.resolve(self.caps.background)
}
pub fn success(&self, msg: &str) {
if self.is_quiet() || self.is_json() {
return;
}
self.stderr.write_line(&self.theme.format_success(msg));
}
pub fn error(&self, msg: &str) {
if self.is_json() {
return;
}
self.stderr.write_line(&self.theme.format_error(msg));
}
pub fn warning(&self, msg: &str) {
if self.is_quiet() || self.is_json() {
return;
}
self.stderr.write_line(&self.theme.format_warning(msg));
}
pub fn info(&self, msg: &str) {
if self.is_quiet() || self.is_json() {
return;
}
self.stderr.write_line(&self.theme.format_info(msg));
}
pub fn debug(&self, msg: &str) {
if !self.is_verbose() || self.is_json() {
return;
}
self.stderr
.write_line(&format!("[DEBUG] {}", self.theme.muted(msg)));
}
pub fn print(&self, msg: &str) {
if self.is_json() {
return;
}
self.stdout.write_line(msg);
}
pub fn header(&self, title: &str) {
if self.is_json() {
return;
}
self.stdout.write_line(&self.theme.format_header(title));
}
pub fn key_value(&self, key: &str, value: &str) {
if self.is_json() {
return;
}
self.stdout
.write_line(&self.theme.format_key_value(key, value, 16));
}
pub fn key_value_width(&self, key: &str, value: &str, width: usize) {
if self.is_json() {
return;
}
self.stdout
.write_line(&self.theme.format_key_value(key, value, width));
}
pub fn table(&self, headers: &[&str], rows: &[Vec<String>]) {
if self.is_json() {
return;
}
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
let header_line: Vec<String> = headers
.iter()
.zip(widths.iter())
.map(|(h, w)| format!("{:width$}", h, width = *w))
.collect();
self.stdout
.write_line(&self.theme.highlight(&header_line.join(" ")).to_string());
let sep: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
self.stdout.write_line(&sep.join(" "));
for row in rows {
let row_line: Vec<String> = row
.iter()
.zip(widths.iter())
.map(|(cell, w)| format!("{:width$}", cell, width = *w))
.collect();
self.stdout.write_line(&row_line.join(" "));
}
}
pub fn json<T: Serialize>(&self, value: &T) -> Result<(), OutputError> {
if !self.is_json() {
return Ok(());
}
let output = self.encode_machine(value, true)?;
self.stdout
.write_line_checked(&output)
.map_err(OutputError::Delivery)
}
pub fn json_force<T: Serialize>(&self, value: &T) -> Result<(), OutputError> {
let json = serde_json::to_string_pretty(value)?;
self.stdout
.write_line_checked(&json)
.map_err(OutputError::Delivery)
}
pub fn json_compact<T: Serialize>(&self, value: &T) -> Result<(), OutputError> {
if !self.is_json() {
return Ok(());
}
let output = self.encode_machine(value, false)?;
self.stdout
.write_line_checked(&output)
.map_err(OutputError::Delivery)
}
fn encode_machine<T: Serialize>(&self, value: &T, pretty: bool) -> serde_json::Result<String> {
match self.format {
OutputFormat::Json => {
if pretty {
serde_json::to_string_pretty(value)
} else {
serde_json::to_string(value)
}
}
OutputFormat::Toon => {
let json_value = serde_json::to_value(value)?;
Ok(toon_rust::encode(json_value, None))
}
}
}
pub fn flush(&self) {
self.stdout.flush();
self.stderr.flush();
}
}
pub fn default_context() -> OutputContext {
OutputContext::new(OutputConfig::default())
}
fn force_color_env() -> Option<bool> {
parse_force_color(env::var("FORCE_COLOR").ok().as_deref())
}
fn parse_force_color(value: Option<&str>) -> Option<bool> {
value.map(rch_common::ui::context::force_color_enables)
}
#[cfg(test)]
mod force_color_tests {
use super::parse_force_color;
#[test]
fn force_color_values_follow_the_documented_contract() {
assert_eq!(parse_force_color(None), None);
assert_eq!(parse_force_color(Some("0")), Some(false));
assert_eq!(parse_force_color(Some(" 0 ")), Some(false));
assert_eq!(parse_force_color(Some("1")), Some(true));
assert_eq!(parse_force_color(Some("")), Some(true));
assert_eq!(parse_force_color(Some("3")), Some(true));
assert_eq!(parse_force_color(Some("false")), Some(false));
assert_eq!(parse_force_color(Some("FALSE")), Some(false));
assert_eq!(parse_force_color(Some("true")), Some(true));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::writer::SharedOutputBuffer;
use serde_json::Value;
use tracing::info;
fn log_test_start(name: &str) {
info!("TEST START: {}", name);
}
fn make_test_context(
config: OutputConfig,
) -> (OutputContext, SharedOutputBuffer, SharedOutputBuffer) {
let stdout_buf = SharedOutputBuffer::new();
let stderr_buf = SharedOutputBuffer::new();
let stdout = stdout_buf.as_writer(true);
let stderr = stderr_buf.as_writer(true);
let ctx = OutputContext::with_writers(config, stdout, stderr);
(ctx, stdout_buf, stderr_buf)
}
#[test]
fn structured_output_formats_and_encoding_errors_preserve_real_files() {
use std::collections::BTreeMap;
use std::io::Write as _;
let retained = tempfile::tempdir().unwrap().keep();
let payload = serde_json::json!({"status": "observed", "count": 7});
let invalid = BTreeMap::from([(vec![1_u8, 2], "unsupported key")]);
for (index, (format, compact, forced)) in [
(OutputFormat::Json, false, false),
(OutputFormat::Json, true, false),
(OutputFormat::Toon, false, false),
(OutputFormat::Toon, true, false),
(OutputFormat::Json, false, true),
(OutputFormat::Toon, false, true),
]
.into_iter()
.enumerate()
{
let path = retained.join(format!("output-{index}"));
let mut file = std::fs::File::create_new(&path).unwrap();
file.write_all(b"retained prefix\n").unwrap();
file.flush().unwrap();
let ctx = OutputContext::with_writers(
OutputConfig {
force_mode: Some(if forced {
OutputMode::Plain
} else {
OutputMode::Json
}),
color: ColorChoice::Never,
format,
..Default::default()
},
OutputWriter::new(file, false),
OutputWriter::stderr(),
);
let failure = if forced {
ctx.json_force(&invalid)
} else if compact {
ctx.json_compact(&invalid)
} else {
ctx.json(&invalid)
}
.unwrap_err();
assert!(matches!(failure, OutputError::Encoding(_)));
assert_eq!(std::fs::read(&path).unwrap(), b"retained prefix\n");
if forced {
ctx.json_force(&payload)
} else if compact {
ctx.json_compact(&payload)
} else {
ctx.json(&payload)
}
.unwrap();
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"retained prefix\n") && bytes.ends_with(b"\n"));
let encoded = &bytes[b"retained prefix\n".len()..];
let actual = if format == OutputFormat::Toon && !forced {
serde_json::from_str::<Value>(
&toon_rust::toon_to_json(std::str::from_utf8(encoded).unwrap()).unwrap(),
)
.unwrap()
} else {
serde_json::from_slice::<Value>(encoded).unwrap()
};
assert!(
json_values_loose_eq(&actual, &payload),
"{format:?} compact={compact} forced={forced}: {actual}"
);
}
}
#[cfg(unix)]
#[test]
fn structured_output_classifies_real_closed_socket_writes_and_flushes() {
use std::os::unix::net::UnixStream;
for format in [OutputFormat::Json, OutputFormat::Toon] {
for operation in ["pretty", "compact", "forced"] {
for buffered in [false, true] {
let (socket, peer) = UnixStream::pair().unwrap();
socket
.set_write_timeout(Some(std::time::Duration::from_secs(2)))
.unwrap();
drop(peer);
let stdout = if buffered {
OutputWriter::new(std::io::BufWriter::with_capacity(8192, socket), false)
} else {
OutputWriter::new(socket, false)
};
let ctx = OutputContext::with_writers(
OutputConfig {
force_mode: Some(if operation == "forced" {
OutputMode::Plain
} else {
OutputMode::Json
}),
color: ColorChoice::Never,
format,
..Default::default()
},
stdout,
OutputWriter::stderr(),
);
let payload = serde_json::json!({"status": "observed"});
let error = match operation {
"pretty" => ctx.json(&payload),
"compact" => ctx.json_compact(&payload),
"forced" => ctx.json_force(&payload),
_ => unreachable!(),
}
.unwrap_err();
let OutputError::Delivery(cause) = error else {
panic!("closed socket must be a delivery failure");
};
assert_eq!(cause.kind(), std::io::ErrorKind::BrokenPipe);
}
}
}
}
fn json_values_loose_eq(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Number(left_num), Value::Number(right_num)) => {
match (left_num.as_f64(), right_num.as_f64()) {
(Some(left_val), Some(right_val)) => {
(left_val - right_val).abs() <= f64::EPSILON
}
_ => false,
}
}
(Value::Array(left_arr), Value::Array(right_arr)) => {
left_arr.len() == right_arr.len()
&& left_arr
.iter()
.zip(right_arr.iter())
.all(|(left_item, right_item)| json_values_loose_eq(left_item, right_item))
}
(Value::Object(left_obj), Value::Object(right_obj)) => {
left_obj.len() == right_obj.len()
&& left_obj.iter().all(|(key, left_val)| {
right_obj
.get(key)
.map(|right_val| json_values_loose_eq(left_val, right_val))
.unwrap_or(false)
})
}
_ => left == right,
}
}
#[test]
fn test_output_mode_default() {
log_test_start("test_output_mode_default");
let mode = OutputMode::default();
assert_eq!(mode, OutputMode::Human);
}
#[test]
fn test_verbosity_default() {
log_test_start("test_verbosity_default");
let v = Verbosity::default();
assert_eq!(v, Verbosity::Normal);
}
#[test]
fn test_color_choice_parse() {
log_test_start("test_color_choice_parse");
assert_eq!(ColorChoice::parse("always"), ColorChoice::Always);
assert_eq!(ColorChoice::parse("never"), ColorChoice::Never);
assert_eq!(ColorChoice::parse("auto"), ColorChoice::Auto);
assert_eq!(ColorChoice::parse("invalid"), ColorChoice::Auto);
}
#[test]
fn test_output_format_default() {
log_test_start("test_output_format_default");
let config = OutputConfig::default();
assert_eq!(config.format, OutputFormat::Json);
}
#[test]
fn test_json_mode_json_output() {
log_test_start("test_json_mode_json_output");
let config = OutputConfig {
json: true,
..Default::default()
};
let (ctx, stdout_buf, _) = make_test_context(config);
assert!(ctx.is_json());
#[derive(Serialize)]
struct TestData {
value: i32,
}
ctx.json(&TestData { value: 42 }).unwrap();
let output = stdout_buf.to_string_lossy();
assert!(output.contains("42"));
assert!(output.contains("value"));
}
#[test]
fn test_machine_mode_toon_output() {
log_test_start("test_machine_mode_toon_output");
let config = OutputConfig {
json: true,
format: OutputFormat::Toon,
..Default::default()
};
let (ctx, stdout_buf, _) = make_test_context(config);
#[derive(Serialize)]
struct TestData {
name: String,
count: i32,
}
let value = TestData {
name: "alpha".to_string(),
count: 7,
};
ctx.json(&value).unwrap();
let output = stdout_buf.to_string_lossy();
let output_json = toon_rust::toon_to_json(&output).unwrap();
let actual_json: serde_json::Value = serde_json::from_str(&output_json).unwrap();
let expected = serde_json::to_value(&value).unwrap();
assert!(
json_values_loose_eq(&actual_json, &expected),
"TOON output did not match expected JSON"
);
}
#[test]
fn test_json_mode_suppresses_human_output() {
log_test_start("test_json_mode_suppresses_human_output");
let config = OutputConfig {
json: true,
..Default::default()
};
let (ctx, stdout_buf, stderr_buf) = make_test_context(config);
ctx.header("Test");
ctx.print("Should not appear");
ctx.success("Also should not appear");
assert!(stdout_buf.to_string_lossy().is_empty());
assert!(stderr_buf.to_string_lossy().is_empty());
}
#[test]
fn test_verbose_and_json_both_work() {
log_test_start("test_verbose_and_json_both_work");
let config = OutputConfig {
json: true,
verbose: true,
..Default::default()
};
let (ctx, stdout_buf, stderr_buf) = make_test_context(config);
assert!(ctx.is_json());
assert!(ctx.is_verbose());
#[derive(Serialize)]
struct TestData {
mode: &'static str,
}
ctx.debug("hidden debug detail");
let json_result = ctx.json(&TestData {
mode: "verbose-json",
});
assert!(json_result.is_ok());
let output = stdout_buf.to_string_lossy();
let parsed_result = serde_json::from_str::<serde_json::Value>(&output);
assert!(
parsed_result.is_ok(),
"verbose JSON output should be valid JSON: {output}"
);
let parsed = parsed_result.unwrap_or(serde_json::Value::Null);
assert_eq!(parsed["mode"], "verbose-json");
assert!(stderr_buf.to_string_lossy().is_empty());
}
#[test]
fn test_verbose_and_quiet_conflict_handling() {
log_test_start("test_verbose_and_quiet_conflict_handling");
let config = OutputConfig {
verbose: true,
quiet: true,
..Default::default()
};
let (ctx, _, stderr_buf) = make_test_context(config);
assert!(ctx.is_quiet());
assert!(!ctx.is_verbose());
ctx.debug("hidden debug detail");
ctx.info("hidden info detail");
assert!(stderr_buf.to_string_lossy().is_empty());
ctx.error("visible error detail");
let output = stderr_buf.to_string_lossy();
assert!(output.contains("visible error detail"));
}
#[test]
fn test_quiet_mode_suppresses_info() {
log_test_start("test_quiet_mode_suppresses_info");
let config = OutputConfig {
quiet: true,
..Default::default()
};
let (ctx, _, stderr_buf) = make_test_context(config);
ctx.info("Should not appear");
ctx.success("Also hidden");
assert!(stderr_buf.to_string_lossy().is_empty());
}
#[test]
fn test_error_always_shows() {
log_test_start("test_error_always_shows");
let config = OutputConfig {
quiet: true,
..Default::default()
};
let (ctx, _, stderr_buf) = make_test_context(config);
ctx.error("This should appear");
let output = stderr_buf.to_string_lossy();
assert!(output.contains("This should appear"));
}
#[test]
fn test_verbose_debug_messages() {
log_test_start("test_verbose_debug_messages");
let config = OutputConfig {
verbose: true,
..Default::default()
};
let (ctx, _, stderr_buf) = make_test_context(config);
ctx.debug("Debug info");
let output = stderr_buf.to_string_lossy();
assert!(output.contains("Debug info"));
}
#[test]
fn test_non_verbose_hides_debug() {
log_test_start("test_non_verbose_hides_debug");
let config = OutputConfig::default();
let (ctx, _, stderr_buf) = make_test_context(config);
ctx.debug("Should not appear");
assert!(stderr_buf.to_string_lossy().is_empty());
}
#[test]
fn test_table_output() {
log_test_start("test_table_output");
let config = OutputConfig::default();
let (ctx, stdout_buf, _) = make_test_context(config);
ctx.table(
&["Name", "Status"],
&[
vec!["worker-1".to_string(), "healthy".to_string()],
vec!["worker-2".to_string(), "degraded".to_string()],
],
);
let output = stdout_buf.to_string_lossy();
assert!(output.contains("Name"));
assert!(output.contains("Status"));
assert!(output.contains("worker-1"));
assert!(output.contains("healthy"));
}
#[test]
fn test_terminal_caps_defaults() {
log_test_start("test_terminal_caps_defaults");
let caps = TerminalCaps::default();
assert_eq!(caps.width, 80);
assert_eq!(caps.height, 24);
assert!(!caps.supports_color);
assert!(!caps.supports_unicode);
assert_eq!(caps.color_level, ColorLevel::Ansi16);
assert!(!caps.supports_hyperlinks);
assert_eq!(caps.background, Background::Dark);
}
#[test]
fn test_context_background_methods() {
log_test_start("test_context_background_methods");
let config = OutputConfig::default();
let (ctx, _, _) = make_test_context(config);
let bg = ctx.background();
if bg == Background::Light {
assert!(ctx.is_light_background());
assert!(!ctx.is_dark_background());
} else {
assert!(!ctx.is_light_background());
assert!(ctx.is_dark_background());
}
}
#[test]
fn test_context_color_level() {
log_test_start("test_context_color_level");
let config = OutputConfig::default();
let (ctx, _, _) = make_test_context(config);
let level = ctx.color_level();
assert!(
level == ColorLevel::None
|| level == ColorLevel::Ansi16
|| level == ColorLevel::Ansi256
|| level == ColorLevel::TrueColor
);
}
#[test]
fn test_context_color_level_plain_mode() {
log_test_start("test_context_color_level_plain_mode");
let config = OutputConfig {
force_mode: Some(OutputMode::Plain),
..Default::default()
};
let (ctx, _, _) = make_test_context(config);
assert_eq!(ctx.color_level(), ColorLevel::None);
}
#[test]
fn test_context_resolve_color() {
log_test_start("test_context_resolve_color");
use crate::ui::adaptive::palette;
let config = OutputConfig::default();
let (ctx, _, _) = make_test_context(config);
let color = ctx.resolve_color(&palette::SUCCESS);
let _ = color;
}
#[test]
fn test_context_supports_hyperlinks_human_mode() {
log_test_start("test_context_supports_hyperlinks_human_mode");
let config = OutputConfig {
force_mode: Some(OutputMode::Human),
..Default::default()
};
let (ctx, _, _) = make_test_context(config);
let _ = ctx.supports_hyperlinks();
}
#[test]
fn test_context_supports_hyperlinks_plain_mode() {
log_test_start("test_context_supports_hyperlinks_plain_mode");
let config = OutputConfig {
force_mode: Some(OutputMode::Plain),
..Default::default()
};
let (ctx, _, _) = make_test_context(config);
assert!(!ctx.supports_hyperlinks());
}
}