use crate::errors::{Error, Result};
use crate::markup::Markup;
use crate::measure::{Measurable, MeasureOptions, Measurement};
use crate::segment::{Control, ControlType, Segment, Segments};
use crate::style::Style;
use crate::text::Text;
use crossterm::terminal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::io::{self, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ColorSystem {
None,
Standard,
EightBit,
#[default]
TrueColor,
Windows,
}
impl ColorSystem {
#[inline]
#[must_use]
pub const fn has_colors(&self) -> bool {
!matches!(self, Self::None)
}
#[inline]
#[must_use]
pub const fn is_true_color(&self) -> bool {
matches!(self, Self::TrueColor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RecordMode {
#[default]
Off,
On,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ConsoleOptions {
pub max_width: usize,
pub min_width: usize,
pub justify: Justify,
pub overflow: Overflow,
pub no_wrap: bool,
pub highlight: bool,
pub markup: bool,
pub emoji: bool,
}
impl Default for ConsoleOptions {
fn default() -> Self {
Self {
max_width: 80,
min_width: 1,
justify: Justify::Default,
overflow: Overflow::Fold,
no_wrap: false,
highlight: false,
markup: true,
emoji: true,
}
}
}
impl ConsoleOptions {
#[inline]
#[must_use]
pub const fn new(max_width: usize) -> Self {
Self {
max_width,
min_width: 1,
justify: Justify::Default,
overflow: Overflow::Fold,
no_wrap: false,
highlight: false,
markup: true,
emoji: true,
}
}
#[inline]
#[must_use]
pub const fn max_width(mut self, width: usize) -> Self {
self.max_width = width;
self
}
#[inline]
#[must_use]
pub const fn justify(mut self, justify: Justify) -> Self {
self.justify = justify;
self
}
#[inline]
#[must_use]
pub const fn overflow(mut self, overflow: Overflow) -> Self {
self.overflow = overflow;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TerminalSize {
pub width: usize,
pub height: usize,
}
impl TerminalSize {
#[inline]
#[must_use]
pub const fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
#[derive(Debug, Clone, Default)]
pub struct CapturedOutput {
pub text: String,
}
impl CapturedOutput {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
text: String::new(),
}
}
}
#[derive(Debug)]
pub struct Console {
writer: Box<dyn ConsoleWriter>,
options: ConsoleOptions,
color_system: ColorSystem,
is_terminal: bool,
force_terminal: bool,
style: Option<Style>,
record_buffer: Vec<Segment>,
record_mode: RecordMode,
captured: Option<String>,
is_capturing: bool,
#[allow(dead_code)]
soft_wrap: bool,
width: Option<usize>,
height: Option<usize>,
}
pub trait ConsoleWriter: fmt::Debug + Send {
fn write_str(&mut self, s: &str) -> Result<()>;
fn flush(&mut self) -> Result<()>;
fn is_terminal(&self) -> bool;
}
#[derive(Debug)]
pub struct StdoutWriter;
impl ConsoleWriter for StdoutWriter {
fn write_str(&mut self, s: &str) -> Result<()> {
let mut stdout = io::stdout();
stdout.write_all(s.as_bytes())?;
Ok(())
}
fn flush(&mut self) -> Result<()> {
io::stdout().flush()?;
Ok(())
}
fn is_terminal(&self) -> bool {
atty_check()
}
}
#[derive(Debug)]
pub struct StderrWriter;
impl ConsoleWriter for StderrWriter {
fn write_str(&mut self, s: &str) -> Result<()> {
let mut stderr = io::stderr();
stderr.write_all(s.as_bytes())?;
Ok(())
}
fn flush(&mut self) -> Result<()> {
io::stderr().flush()?;
Ok(())
}
fn is_terminal(&self) -> bool {
atty_check_stderr()
}
}
#[derive(Debug, Default)]
pub struct StringWriter {
buffer: String,
}
impl StringWriter {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
buffer: String::new(),
}
}
#[inline]
#[must_use]
pub fn contents(&self) -> &str {
&self.buffer
}
#[inline]
pub fn take(&mut self) -> String {
std::mem::take(&mut self.buffer)
}
}
impl ConsoleWriter for StringWriter {
fn write_str(&mut self, s: &str) -> Result<()> {
self.buffer.push_str(s);
Ok(())
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn is_terminal(&self) -> bool {
true }
}
impl Default for Console {
fn default() -> Self {
Self::new()
}
}
impl Console {
#[must_use]
pub fn new() -> Self {
let is_terminal = atty_check();
Self {
writer: Box::new(StdoutWriter),
options: ConsoleOptions::default(),
color_system: detect_color_system(),
is_terminal,
force_terminal: false,
style: None,
record_buffer: Vec::new(),
record_mode: RecordMode::Off,
captured: None,
is_capturing: false,
soft_wrap: false,
width: None,
height: None,
}
}
#[must_use]
pub fn stderr() -> Self {
let is_terminal = atty_check_stderr();
Self {
writer: Box::new(StderrWriter),
options: ConsoleOptions::default(),
color_system: detect_color_system(),
is_terminal,
force_terminal: false,
style: None,
record_buffer: Vec::new(),
record_mode: RecordMode::Off,
captured: None,
is_capturing: false,
soft_wrap: false,
width: None,
height: None,
}
}
#[must_use]
pub fn with_writer<W: ConsoleWriter + 'static>(writer: W) -> Self {
let is_terminal = writer.is_terminal();
Self {
writer: Box::new(writer),
options: ConsoleOptions::default(),
color_system: ColorSystem::TrueColor,
is_terminal,
force_terminal: false,
style: None,
record_buffer: Vec::new(),
record_mode: RecordMode::Off,
captured: None,
is_capturing: false,
soft_wrap: false,
width: None,
height: None,
}
}
#[inline]
pub fn set_color_system(&mut self, color_system: ColorSystem) {
self.color_system = color_system;
}
#[inline]
#[must_use]
pub const fn color_system(&self) -> ColorSystem {
self.color_system
}
#[inline]
pub fn set_force_terminal(&mut self, force: bool) {
self.force_terminal = force;
}
#[inline]
#[must_use]
pub const fn is_terminal(&self) -> bool {
self.is_terminal || self.force_terminal
}
#[inline]
pub fn set_style(&mut self, style: Option<Style>) {
self.style = style;
}
#[inline]
#[must_use]
pub const fn style(&self) -> Option<&Style> {
self.style.as_ref()
}
#[inline]
pub fn set_width(&mut self, width: usize) {
self.width = Some(width);
}
#[inline]
pub fn set_height(&mut self, height: usize) {
self.height = Some(height);
}
#[must_use]
pub fn size(&self) -> TerminalSize {
if let (Some(w), Some(h)) = (self.width, self.height) {
return TerminalSize::new(w, h);
}
terminal::size()
.map(|(w, h)| TerminalSize::new(usize::from(w), usize::from(h)))
.unwrap_or(TerminalSize::new(80, 24))
}
#[inline]
#[must_use]
pub fn width(&self) -> usize {
self.width.unwrap_or_else(|| self.size().width)
}
#[inline]
#[must_use]
pub fn height(&self) -> usize {
self.height.unwrap_or_else(|| self.size().height)
}
#[inline]
#[must_use]
pub const fn options(&self) -> &ConsoleOptions {
&self.options
}
#[inline]
pub fn options_mut(&mut self) -> &mut ConsoleOptions {
&mut self.options
}
#[inline]
pub fn begin_recording(&mut self) {
self.record_mode = RecordMode::On;
self.record_buffer.clear();
}
#[inline]
pub fn end_recording(&mut self) -> Vec<Segment> {
self.record_mode = RecordMode::Off;
std::mem::take(&mut self.record_buffer)
}
#[inline]
pub fn begin_capture(&mut self) {
self.is_capturing = true;
self.captured = Some(String::new());
}
#[inline]
pub fn end_capture(&mut self) -> String {
self.is_capturing = false;
self.captured.take().unwrap_or_default()
}
pub fn write(&mut self, s: &str) -> Result<()> {
if self.is_capturing {
if let Some(ref mut captured) = self.captured {
captured.push_str(s);
}
return Ok(());
}
self.writer.write_str(s)?;
Ok(())
}
pub fn write_segment(&mut self, segment: &Segment) -> Result<()> {
if self.record_mode == RecordMode::On {
self.record_buffer.push(segment.clone());
}
let output = if self.color_system.has_colors() {
segment.to_ansi()
} else {
segment.text.clone()
};
self.write(&output)
}
pub fn write_segments(&mut self, segments: &Segments) -> Result<()> {
for segment in segments.iter() {
self.write_segment(segment)?;
}
Ok(())
}
#[inline]
pub fn flush(&mut self) -> Result<()> {
self.writer.flush()
}
pub fn print(&mut self, text: &str) -> Result<()> {
self.print_with_options(text, &self.options.clone())
}
pub fn print_with_options(&mut self, text: &str, options: &ConsoleOptions) -> Result<()> {
let rendered = if options.markup {
let markup = Markup::parse(text)?;
markup.to_text()
} else {
Text::from_str(text)
};
let segments = rendered.to_segments();
self.write_segments(&segments)?;
self.write("\n")?;
self.flush()
}
pub fn print_styled(&mut self, text: &str, style: &Style) -> Result<()> {
let styled_text = Text::styled(text, style.clone());
let segments = styled_text.to_segments();
self.write_segments(&segments)?;
self.write("\n")?;
self.flush()
}
pub fn print_text(&mut self, text: &Text) -> Result<()> {
let segments = text.to_segments();
self.write_segments(&segments)?;
self.write("\n")?;
self.flush()
}
pub fn log(&mut self, text: &str) -> Result<()> {
let prefix = "[LOG] ";
self.write(prefix)?;
self.print(text)
}
pub fn out(&mut self, text: &str) -> Result<()> {
self.write(text)?;
self.write("\n")?;
self.flush()
}
pub fn rule(&mut self, title: Option<&str>) -> Result<()> {
let width = self.width();
let rule_char = '─';
match title {
Some(t) if !t.is_empty() => {
let title_len = unicode_width::UnicodeWidthStr::width(t);
let padding: usize = 2; let rule_len = width
.saturating_sub(title_len)
.saturating_sub(padding.saturating_mul(2));
let half = rule_len.checked_div(2).unwrap_or(0);
let left = half;
let right = rule_len.saturating_sub(left);
let line = format!(
"{} {} {}",
rule_char.to_string().repeat(left),
t,
rule_char.to_string().repeat(right)
);
self.write(&line)?;
}
_ => {
self.write(&rule_char.to_string().repeat(width))?;
}
}
self.write("\n")?;
self.flush()
}
pub fn clear(&mut self) -> Result<()> {
let ctrl = Control::new(ControlType::Clear);
let seg = Segment::control(ctrl);
self.write_segment(&seg)?;
let home = Control::new(ControlType::Home);
let seg = Segment::control(home);
self.write_segment(&seg)?;
self.flush()
}
pub fn show_cursor(&mut self) -> Result<()> {
let ctrl = Control::new(ControlType::ShowCursor);
let seg = Segment::control(ctrl);
self.write_segment(&seg)?;
self.flush()
}
pub fn hide_cursor(&mut self) -> Result<()> {
let ctrl = Control::new(ControlType::HideCursor);
let seg = Segment::control(ctrl);
self.write_segment(&seg)?;
self.flush()
}
pub fn export_text(&self) -> Result<String> {
Ok(self
.record_buffer
.iter()
.filter(|s| !s.is_control())
.map(|s| s.text.as_str())
.collect())
}
pub fn export_html(&self) -> Result<String> {
let mut html = String::from("<pre><code>");
for segment in &self.record_buffer {
if segment.is_control() {
continue;
}
let escaped = html_escape(&segment.text);
match &segment.style {
Some(style) if !style.is_empty() => {
let css = style_to_css(style);
html.push_str(&format!("<span style=\"{css}\">{escaped}</span>"));
}
_ => {
html.push_str(&escaped);
}
}
}
html.push_str("</code></pre>");
Ok(html)
}
pub fn measure<M: Measurable>(&self, renderable: &M) -> Result<Measurement> {
let options = MeasureOptions::new(self.width());
renderable.measure(&options)
}
pub fn input(&mut self, prompt: &str) -> Result<String> {
self.print(prompt)?;
self.flush()?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| Error::ConsoleIo { source: e })?;
if input.ends_with('\n') {
input.pop();
if input.ends_with('\r') {
input.pop();
}
}
Ok(input)
}
}
pub use crate::text::Justify;
pub use crate::text::Overflow;
fn detect_color_system() -> ColorSystem {
if let Ok(term) = std::env::var("COLORTERM") {
if term == "truecolor" || term == "24bit" {
return ColorSystem::TrueColor;
}
}
if let Ok(term) = std::env::var("TERM") {
if term.contains("256color") {
return ColorSystem::EightBit;
}
if term.contains("color") || term.contains("xterm") || term.contains("screen") {
return ColorSystem::Standard;
}
}
ColorSystem::TrueColor
}
fn atty_check() -> bool {
crossterm::tty::IsTty::is_tty(&io::stdout())
}
fn atty_check_stderr() -> bool {
crossterm::tty::IsTty::is_tty(&io::stderr())
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn style_to_css(style: &Style) -> String {
let mut css = Vec::new();
if let Some(ref color) = style.color {
css.push(format!("color: {}", color_to_css(color)));
}
if let Some(ref bgcolor) = style.bgcolor {
css.push(format!("background-color: {}", color_to_css(bgcolor)));
}
if style.attributes.bold == Some(true) {
css.push("font-weight: bold".to_owned());
}
if style.attributes.italic == Some(true) {
css.push("font-style: italic".to_owned());
}
if style.attributes.underline == Some(true) {
css.push("text-decoration: underline".to_owned());
}
if style.attributes.strike == Some(true) {
css.push("text-decoration: line-through".to_owned());
}
css.join("; ")
}
fn color_to_css(color: &crate::color::Color) -> String {
use crate::color::{Color, StandardColor};
match color {
Color::Default => "inherit".to_owned(),
Color::Standard(std) => match std {
StandardColor::Black => "#000000",
StandardColor::Red => "#cc0000",
StandardColor::Green => "#00cc00",
StandardColor::Yellow => "#cccc00",
StandardColor::Blue => "#0000cc",
StandardColor::Magenta => "#cc00cc",
StandardColor::Cyan => "#00cccc",
StandardColor::White => "#cccccc",
StandardColor::BrightBlack => "#666666",
StandardColor::BrightRed => "#ff0000",
StandardColor::BrightGreen => "#00ff00",
StandardColor::BrightYellow => "#ffff00",
StandardColor::BrightBlue => "#0000ff",
StandardColor::BrightMagenta => "#ff00ff",
StandardColor::BrightCyan => "#00ffff",
StandardColor::BrightWhite => "#ffffff",
}
.to_owned(),
Color::Palette(idx) => format!("var(--palette-{idx})"),
Color::Rgb { r, g, b } => format!("rgb({r}, {g}, {b})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::{Color, StandardColor};
#[test]
fn test_console_new() {
let console = Console::new();
assert!(console.width() > 0);
assert!(console.height() > 0);
}
#[test]
fn test_console_with_writer() {
let writer = StringWriter::new();
let console = Console::with_writer(writer);
assert!(console.is_terminal());
}
#[test]
fn test_console_write() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.write("hello").ok();
console.flush().ok();
}
#[test]
fn test_console_capture() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.write("captured text").ok();
let captured = console.end_capture();
assert_eq!(captured, "captured text");
}
#[test]
fn test_console_size() {
let mut console = Console::new();
console.set_width(120);
console.set_height(40);
assert_eq!(console.width(), 120);
assert_eq!(console.height(), 40);
}
#[test]
fn test_console_options() {
let opts = ConsoleOptions::new(100);
assert_eq!(opts.max_width, 100);
let opts = opts.justify(Justify::Center);
assert_eq!(opts.justify, Justify::Center);
}
#[test]
fn test_console_options_default() {
let opts = ConsoleOptions::default();
assert_eq!(opts.max_width, 80);
assert_eq!(opts.min_width, 1);
assert_eq!(opts.justify, Justify::Default);
assert_eq!(opts.overflow, Overflow::Fold);
assert!(!opts.no_wrap);
assert!(!opts.highlight);
assert!(opts.markup);
assert!(opts.emoji);
}
#[test]
fn test_console_options_with_overflow() {
let opts = ConsoleOptions::new(80).overflow(Overflow::Ellipsis);
assert_eq!(opts.overflow, Overflow::Ellipsis);
}
#[test]
fn test_console_options_no_wrap_field() {
let mut opts = ConsoleOptions::new(80);
opts.no_wrap = true;
assert!(opts.no_wrap);
}
#[test]
fn test_terminal_size() {
let size = TerminalSize::new(80, 24);
assert_eq!(size.width, 80);
assert_eq!(size.height, 24);
}
#[test]
fn test_terminal_size_default() {
let size = TerminalSize::default();
assert_eq!(size.width, 0);
assert_eq!(size.height, 0);
}
#[test]
fn test_color_system() {
assert!(!ColorSystem::None.has_colors());
assert!(ColorSystem::Standard.has_colors());
assert!(ColorSystem::EightBit.has_colors());
assert!(ColorSystem::Windows.has_colors());
assert!(ColorSystem::TrueColor.is_true_color());
assert!(!ColorSystem::EightBit.is_true_color());
assert!(!ColorSystem::Standard.is_true_color());
assert!(!ColorSystem::None.is_true_color());
}
#[test]
fn test_color_system_default() {
let cs = ColorSystem::default();
assert_eq!(cs, ColorSystem::TrueColor);
}
#[test]
fn test_record_mode_default() {
let mode = RecordMode::default();
assert_eq!(mode, RecordMode::Off);
}
#[test]
fn test_string_writer() {
let mut writer = StringWriter::new();
writer.write_str("hello").ok();
assert_eq!(writer.contents(), "hello");
let taken = writer.take();
assert_eq!(taken, "hello");
assert!(writer.contents().is_empty());
}
#[test]
fn test_string_writer_flush() {
let mut writer = StringWriter::new();
writer.write_str("test").ok();
assert!(writer.flush().is_ok());
}
#[test]
fn test_html_escape() {
assert_eq!(html_escape("<script>"), "<script>");
assert_eq!(html_escape("a & b"), "a & b");
assert_eq!(html_escape("\"quoted\""), ""quoted"");
assert_eq!(html_escape("'single'"), "'single'");
}
#[test]
fn test_console_recording() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
console.write_segment(&Segment::new("test")).ok();
let recorded = console.end_recording();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded.first().map(|s| s.text.as_str()), Some("test"));
}
#[test]
fn test_export_text() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
console.write_segment(&Segment::new("test")).ok();
let text = console.export_text().ok().unwrap_or_default();
assert_eq!(text, "test");
}
#[test]
fn test_export_html() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
console.write_segment(&Segment::new("plain")).ok();
let styled = Segment::styled("bold", Style::new().bold());
console.write_segment(&styled).ok();
let html = console.export_html().ok().unwrap_or_default();
assert!(html.contains("<pre><code>"));
assert!(html.contains("plain"));
assert!(html.contains("</code></pre>"));
}
#[test]
fn test_console_print() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.print("[bold]Hello[/]").ok();
let captured = console.end_capture();
assert!(captured.contains("Hello"));
}
#[test]
fn test_console_print_styled() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.print_styled("Hello", &Style::new().bold()).ok();
let captured = console.end_capture();
assert!(captured.contains("Hello"));
}
#[test]
fn test_console_print_text() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
let text = Text::from_str("Hello World");
console.print_text(&text).ok();
let captured = console.end_capture();
assert!(captured.contains("Hello World"));
}
#[test]
fn test_console_out() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.out("raw text").ok();
let captured = console.end_capture();
assert!(captured.contains("raw text"));
}
#[test]
fn test_console_log() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.log("log message").ok();
let captured = console.end_capture();
assert!(captured.contains("[LOG]"));
assert!(captured.contains("log message"));
}
#[test]
fn test_console_rule() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_width(40);
console.begin_capture();
console.rule(None).ok();
let captured = console.end_capture();
assert!(captured.contains("─"));
}
#[test]
fn test_console_rule_with_title() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_width(40);
console.begin_capture();
console.rule(Some("Title")).ok();
let captured = console.end_capture();
assert!(captured.contains("Title"));
assert!(captured.contains("─"));
}
#[test]
fn test_console_clear() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.clear().ok();
let captured = console.end_capture();
assert!(captured.contains("\x1b["));
}
#[test]
fn test_console_show_hide_cursor() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.hide_cursor().ok();
console.show_cursor().ok();
let captured = console.end_capture();
assert!(captured.contains("\x1b["));
}
#[test]
fn test_console_write_segments() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
let mut segments = Segments::new();
segments.push(Segment::new("Hello "));
segments.push(Segment::new("World"));
console.write_segments(&segments).ok();
let captured = console.end_capture();
assert!(captured.contains("Hello "));
assert!(captured.contains("World"));
}
#[test]
fn test_console_set_color_system() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_color_system(ColorSystem::None);
assert_eq!(console.color_system(), ColorSystem::None);
console.set_color_system(ColorSystem::TrueColor);
assert_eq!(console.color_system(), ColorSystem::TrueColor);
}
#[test]
fn test_console_force_terminal() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_force_terminal(true);
assert!(console.is_terminal());
console.set_force_terminal(false);
}
#[test]
fn test_console_set_style() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
assert!(console.style().is_none());
console.set_style(Some(Style::new().bold()));
assert!(console.style().is_some());
console.set_style(None);
assert!(console.style().is_none());
}
#[test]
fn test_console_options_mut() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.options_mut().max_width = 120;
assert_eq!(console.options().max_width, 120);
}
#[test]
fn test_style_to_css() {
let style = Style::new()
.bold()
.italic()
.underline()
.strike()
.with_color(Color::Standard(StandardColor::Red))
.with_bgcolor(Color::Standard(StandardColor::White));
let css = style_to_css(&style);
assert!(css.contains("font-weight: bold"));
assert!(css.contains("font-style: italic"));
assert!(
css.contains("text-decoration: underline")
|| css.contains("text-decoration: line-through")
);
assert!(css.contains("color:"));
assert!(css.contains("background-color:"));
}
#[test]
fn test_color_to_css() {
assert_eq!(color_to_css(&Color::Default), "inherit");
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Red)),
"#cc0000"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Green)),
"#00cc00"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Blue)),
"#0000cc"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Black)),
"#000000"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::White)),
"#cccccc"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightRed)),
"#ff0000"
);
assert_eq!(color_to_css(&Color::Palette(42)), "var(--palette-42)");
assert_eq!(
color_to_css(&Color::Rgb {
r: 255,
g: 128,
b: 0
}),
"rgb(255, 128, 0)"
);
}
#[test]
fn test_console_write_segment_no_color() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_color_system(ColorSystem::None);
console.begin_capture();
let styled = Segment::styled("text", Style::new().bold());
console.write_segment(&styled).ok();
let captured = console.end_capture();
assert_eq!(captured, "text");
}
#[test]
fn test_console_print_with_options_no_markup() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
let opts = ConsoleOptions::new(80);
let mut opts_no_markup = opts;
opts_no_markup.markup = false;
console
.print_with_options("[bold]Hello[/]", &opts_no_markup)
.ok();
let captured = console.end_capture();
assert!(captured.contains("[bold]"));
}
#[test]
fn test_color_to_css_all_standard_colors() {
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Yellow)),
"#cccc00"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Magenta)),
"#cc00cc"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::Cyan)),
"#00cccc"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightBlack)),
"#666666"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightGreen)),
"#00ff00"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightYellow)),
"#ffff00"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightBlue)),
"#0000ff"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightMagenta)),
"#ff00ff"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightCyan)),
"#00ffff"
);
assert_eq!(
color_to_css(&Color::Standard(StandardColor::BrightWhite)),
"#ffffff"
);
}
#[test]
fn test_console_default() {
let console = Console::default();
assert!(console.width() > 0 || console.width() == 0); }
#[test]
fn test_console_options_highlight() {
let mut opts = ConsoleOptions::new(80);
opts.highlight = true;
assert!(opts.highlight);
}
#[test]
fn test_console_options_emoji() {
let mut opts = ConsoleOptions::new(80);
opts.emoji = false;
assert!(!opts.emoji);
}
#[test]
fn test_console_stderr() {
let console = Console::stderr();
assert!(console.width() > 0 || console.width() == 0);
}
#[test]
fn test_console_write_newline() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.write("\n").ok();
let captured = console.end_capture();
assert_eq!(captured, "\n");
}
#[test]
fn test_console_print_with_newline() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_capture();
console.print("Hello\n").ok();
let captured = console.end_capture();
assert!(captured.contains("Hello"));
assert!(captured.ends_with('\n'));
}
#[test]
fn test_console_measure_text() {
let console = Console::new();
let text = Text::from_str("Hello world");
let measurement = console.measure(&text).ok();
assert!(measurement.is_some());
}
#[test]
fn test_console_export_text_with_control() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
console.write_segment(&Segment::new("text")).ok();
console
.write_segment(&Segment::control(Control::new(ControlType::Clear)))
.ok();
let text = console.export_text().ok().unwrap_or_default();
assert_eq!(text, "text");
}
#[test]
fn test_console_export_html_with_control() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
console.write_segment(&Segment::new("text")).ok();
console
.write_segment(&Segment::control(Control::new(ControlType::Clear)))
.ok();
let html = console.export_html().ok().unwrap_or_default();
assert!(html.contains("text"));
assert!(!html.contains("Clear"));
}
#[test]
fn test_console_export_html_styled() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.begin_recording();
let style = Style::new()
.bold()
.italic()
.underline()
.strike()
.with_color(Color::Rgb { r: 255, g: 0, b: 0 })
.with_bgcolor(Color::Palette(42));
let styled = Segment::styled("styled text", style);
console.write_segment(&styled).ok();
let html = console.export_html().ok().unwrap_or_default();
assert!(html.contains("<span style="));
assert!(html.contains("styled text"));
}
#[test]
fn test_console_write_with_style() {
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_style(Some(Style::new().bold()));
console.begin_capture();
console.write("text").ok();
let captured = console.end_capture();
assert!(captured.contains("text"));
}
#[test]
fn test_color_system_windows() {
assert!(ColorSystem::Windows.has_colors());
assert!(!ColorSystem::Windows.is_true_color());
}
}