use std::io::{IsTerminal, Write};
use crate::color::ColorSystem;
use crate::protocol::{Highlighter, Renderable};
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
use crate::theme::Theme;
const DEFAULT_WIDTH: usize = 80;
const DEFAULT_HEIGHT: usize = 25;
pub type GetTime = std::sync::Arc<dyn Fn() -> f64 + Send + Sync>;
pub(crate) fn monotonic() -> f64 {
static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
START
.get_or_init(std::time::Instant::now)
.elapsed()
.as_secs_f64()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Justify {
#[default]
Default,
Left,
Center,
Right,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Fold,
Crop,
Ellipsis,
Ignore,
}
#[derive(Debug, Clone)]
pub struct ConsoleOptions {
pub min_width: usize,
pub max_width: usize,
pub height: Option<usize>,
pub justify: Justify,
pub overflow: Option<Overflow>,
pub no_wrap: Option<bool>,
pub highlight: Option<bool>,
pub markup: Option<bool>,
pub max_height: usize,
pub encoding: String,
pub is_terminal: bool,
pub legacy_windows: bool,
pub size: ConsoleDimensions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ConsoleDimensions {
pub width: usize,
pub height: usize,
}
impl Default for ConsoleOptions {
fn default() -> Self {
ConsoleOptions {
min_width: 1,
max_width: DEFAULT_WIDTH,
height: None,
justify: Justify::Default,
overflow: None,
no_wrap: None,
highlight: None,
markup: None,
max_height: DEFAULT_HEIGHT,
encoding: "utf-8".to_string(),
is_terminal: false,
legacy_windows: false,
size: ConsoleDimensions {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
},
}
}
}
impl ConsoleOptions {
pub fn update_width(&self, width: usize) -> ConsoleOptions {
let mut options = self.clone();
options.min_width = width;
options.max_width = width;
options
}
pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
let mut options = self.update_width(width);
options.height = Some(height);
options.max_height = height;
options
}
pub fn update_height(&self, height: usize) -> ConsoleOptions {
let mut options = self.clone();
options.height = Some(height);
options.max_height = height;
options
}
pub fn reset_height(&self) -> ConsoleOptions {
let mut options = self.clone();
options.height = None;
options
}
pub fn ascii_only(&self) -> bool {
!self.encoding.starts_with("utf")
}
}
#[derive(Clone, Default)]
pub struct RenderStrOptions<'a> {
pub style: crate::style::StyleType,
pub justify: Option<Justify>,
pub overflow: Option<Overflow>,
pub emoji: Option<bool>,
pub markup: Option<bool>,
pub highlight: Option<bool>,
pub highlighter: Option<&'a dyn Highlighter>,
}
pub type Environ = std::collections::HashMap<String, String>;
type BrokenPipeHandler = std::sync::Arc<dyn Fn(&Console) + Send + Sync>;
type GetDateTime = std::sync::Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>;
type FormatTime = std::sync::Arc<dyn Fn(std::time::SystemTime) -> Text + Send + Sync>;
#[derive(Clone)]
enum LogTimeFormat {
Strftime(String),
Callable(FormatTime),
}
fn environ_get(environ: Option<&Environ>, key: &str) -> Option<String> {
match environ {
Some(environ) => environ.get(key).cloned(),
None => std::env::var_os(key).map(|value| value.to_string_lossy().into_owned()),
}
}
fn env_int(environ: Option<&Environ>, key: &str) -> Option<usize> {
let value = environ_get(environ, key)?;
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
value.parse().ok()
}
pub trait RenderHook: Send + Sync {
fn process_renderables(
&self,
renderables: Vec<Box<dyn Renderable>>,
) -> Vec<Box<dyn Renderable>>;
}
struct Rendered(Vec<Segment>);
impl Renderable for Rendered {
fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
self.0.clone()
}
fn measure(
&self,
_console: &Console,
_options: &ConsoleOptions,
) -> crate::measure::Measurement {
let widest = Segment::split_lines(&self.0)
.iter()
.map(|line| Segment::get_line_length(line))
.max()
.unwrap_or(0);
crate::measure::Measurement::new(widest, widest)
}
fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
false
}
}
type CaptureStacks = std::collections::HashMap<std::thread::ThreadId, Vec<Vec<Segment>>>;
fn current_capture(captures: &mut CaptureStacks) -> Option<&mut Vec<Segment>> {
captures
.get_mut(&std::thread::current().id())
.and_then(|stack| stack.last_mut())
}
pub struct Console {
render_environment: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
region_sink: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>,
code_highlighting: Option<crate::protocol::CodeHighlighting>,
color_system: Option<ColorSystem>,
width: Option<usize>,
height: Option<usize>,
terminal_size: ConsoleDimensions,
environ: Option<std::sync::Arc<Environ>>,
is_terminal: bool,
no_color: bool,
quiet: std::sync::atomic::AtomicBool,
soft_wrap: bool,
style: Option<crate::style::StyleType>,
on_broken_pipe: Option<BrokenPipeHandler>,
get_time: GetTime,
emoji: bool,
highlight: bool,
legacy_windows: bool,
safe_box: bool,
ascii_only: bool,
theme_stack: Vec<Theme>,
base_style: Style,
highlighters: Vec<std::sync::Arc<dyn Highlighter + Send + Sync>>,
markup: bool,
emoji_variant: Option<crate::emoji::EmojiVariant>,
tab_size: usize,
captures: std::sync::Mutex<CaptureStacks>,
is_alt_screen: std::sync::atomic::AtomicBool,
record: std::sync::atomic::AtomicBool,
record_buffer: std::sync::Mutex<Vec<Segment>>,
log_render: std::sync::Mutex<crate::log_render::LogRender>,
log_time: bool,
log_path: bool,
log_time_format: LogTimeFormat,
get_datetime: GetDateTime,
render_hooks: std::sync::Mutex<Vec<std::sync::Arc<dyn RenderHook>>>,
live_stack: LiveStack,
}
type LiveStack =
std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>>>;
struct LockedHighlighter(std::sync::Mutex<Box<dyn Highlighter + Send>>);
impl Highlighter for LockedHighlighter {
fn highlight(&self, text: &mut Text) {
let highlighter = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
highlighter.highlight(text);
}
}
fn no_alt_screen() -> crate::errors::RichError {
crate::errors::RichError::NoAltScreen(
"Alt screen must be enabled to call update_screen".to_string(),
)
}
pub struct ScreenUpdate {
lines: Vec<Vec<Segment>>,
x: usize,
y: usize,
}
impl ScreenUpdate {
pub fn new(lines: Vec<Vec<Segment>>, x: usize, y: usize) -> Self {
ScreenUpdate { lines, x, y }
}
}
impl Renderable for ScreenUpdate {
fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
for (offset, line) in self.lines.iter().enumerate() {
let to = crate::control::move_to_code(self.x as u128, self.y as u128 + offset as u128);
segments.push(Segment::control(to));
segments.extend(line.iter().cloned());
}
segments
}
}
impl Clone for Console {
fn clone(&self) -> Self {
Console {
render_environment: self.render_environment.clone(),
region_sink: self.region_sink.clone(),
code_highlighting: self.code_highlighting.clone(),
color_system: self.color_system,
width: self.width,
height: self.height,
terminal_size: self.terminal_size,
environ: self.environ.clone(),
is_terminal: self.is_terminal,
no_color: self.no_color,
quiet: std::sync::atomic::AtomicBool::new(self.quiet()),
soft_wrap: self.soft_wrap,
style: self.style.clone(),
on_broken_pipe: self.on_broken_pipe.clone(),
get_time: self.get_time.clone(),
emoji: self.emoji,
highlight: self.highlight,
legacy_windows: self.legacy_windows,
safe_box: self.safe_box,
ascii_only: self.ascii_only,
theme_stack: self.theme_stack.clone(),
base_style: self.base_style.clone(),
highlighters: self.highlighters.clone(),
markup: self.markup,
emoji_variant: self.emoji_variant,
tab_size: self.tab_size,
captures: std::sync::Mutex::new(CaptureStacks::new()),
is_alt_screen: std::sync::atomic::AtomicBool::new(false),
record: std::sync::atomic::AtomicBool::new(self.record()),
record_buffer: std::sync::Mutex::new(Vec::new()),
render_hooks: std::sync::Mutex::new(Vec::new()),
live_stack: self.live_stack.clone(),
log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
log_time: self.log_time,
log_path: self.log_path,
log_time_format: self.log_time_format.clone(),
get_datetime: self.get_datetime.clone(),
}
}
}
pub struct ThemeContext<'a> {
console: &'a mut Console,
}
impl std::ops::Deref for ThemeContext<'_> {
type Target = Console;
fn deref(&self) -> &Console {
self.console
}
}
impl std::ops::DerefMut for ThemeContext<'_> {
fn deref_mut(&mut self) -> &mut Console {
self.console
}
}
impl Drop for ThemeContext<'_> {
fn drop(&mut self) {
let _ = self.console.pop_theme();
}
}
pub struct ScreenContext<'a> {
console: &'a Console,
hide_cursor: bool,
changed: bool,
screen: Option<crate::screen::Screen>,
style: crate::style::StyleType,
}
impl ScreenContext<'_> {
pub fn update(
&mut self,
renderables: Vec<Box<dyn Renderable>>,
style: Option<crate::style::StyleType>,
) {
if let Some(style) = style {
self.style = style;
}
let screen = if renderables.is_empty() {
self.screen
.take()
.unwrap_or_else(crate::screen::Screen::empty)
} else {
crate::screen::Screen::from_renderables(renderables)
};
let screen = screen.style(self.style.clone());
self.console.print(&screen);
self.screen = Some(screen);
}
pub fn console(&self) -> &Console {
self.console
}
}
impl Drop for ScreenContext<'_> {
fn drop(&mut self) {
if self.changed {
self.console.set_alt_screen(false);
if self.hide_cursor {
self.console.show_cursor(true);
}
}
}
}
fn lock_or_recover<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
impl Default for Console {
fn default() -> Self {
Console::new()
}
}
impl Console {
pub fn new() -> Self {
ConsoleBuilder::new().build()
}
pub fn builder() -> ConsoleBuilder {
ConsoleBuilder::new()
}
pub fn color_system(&self) -> Option<ColorSystem> {
self.color_system
}
pub fn no_color(&self) -> bool {
self.no_color
}
pub fn get_time(&self) -> f64 {
(self.get_time)()
}
pub fn width(&self) -> usize {
self.size().width
}
pub fn height(&self) -> usize {
self.size().height
}
pub fn set_width(&mut self, width: usize) {
self.width = Some(width);
}
pub fn set_height(&mut self, height: usize) {
self.height = Some(height);
}
pub fn set_size(&mut self, width: usize, height: usize) {
self.width = Some(width);
self.height = Some(height);
}
pub fn quiet(&self) -> bool {
self.quiet.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn set_quiet(&self, quiet: bool) {
self.quiet.store(quiet, std::sync::atomic::Ordering::SeqCst);
}
pub fn soft_wrap(&self) -> bool {
self.soft_wrap
}
pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
self.soft_wrap = soft_wrap;
}
pub fn style(&self) -> Option<&crate::style::StyleType> {
self.style.as_ref()
}
pub fn set_style(&mut self, style: Option<crate::style::StyleType>) {
self.style = style;
}
pub fn is_dumb_terminal(&self) -> bool {
self.is_terminal && term_is_dumb(self.environ.as_deref())
}
pub fn on_broken_pipe(&self) {
if let Some(handler) = &self.on_broken_pipe {
handler(self);
return;
}
self.set_quiet(true);
std::process::exit(1);
}
pub fn is_terminal(&self) -> bool {
self.is_terminal
}
pub fn legacy_windows(&self) -> bool {
self.legacy_windows
}
pub fn safe_box(&self) -> bool {
self.safe_box
}
pub fn ascii_only(&self) -> bool {
self.ascii_only
}
pub fn theme(&self) -> &Theme {
self.theme_stack
.last()
.expect("the theme stack always holds its base theme")
}
pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
self.theme().get_style(style)
}
pub fn push_theme(&mut self, theme: Theme, inherit: bool) {
let top = if inherit {
let mut merged = self.theme().clone();
merged.extend_from(&theme);
merged
} else {
theme
};
self.theme_stack.push(top);
}
pub fn pop_theme(&mut self) -> crate::errors::Result<()> {
if self.theme_stack.len() == 1 {
return Err(crate::errors::RichError::ThemeStack(
"Unable to pop base theme".to_string(),
));
}
self.theme_stack.pop();
Ok(())
}
pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
self.push_theme(theme, true);
ThemeContext { console: self }
}
pub fn base_style(&self) -> &Style {
&self.base_style
}
pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
self.highlighters
.push(std::sync::Arc::new(LockedHighlighter(
std::sync::Mutex::new(highlighter),
)));
}
pub fn options(&self) -> ConsoleOptions {
let size = self.size();
ConsoleOptions {
min_width: 1,
max_width: size.width,
height: None,
justify: Justify::Default,
overflow: None,
no_wrap: None,
highlight: None,
markup: None,
max_height: size.height,
encoding: self.encoding().to_string(),
is_terminal: self.is_terminal,
legacy_windows: self.legacy_windows,
size,
}
}
pub fn size(&self) -> ConsoleDimensions {
if let (Some(width), Some(height)) = (self.width, self.height) {
return ConsoleDimensions {
width: width.saturating_sub(usize::from(self.legacy_windows)),
height,
};
}
if self.is_dumb_terminal() {
return ConsoleDimensions {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
};
}
ConsoleDimensions {
width: self.width.unwrap_or(self.terminal_size.width),
height: self.height.unwrap_or(self.terminal_size.height),
}
}
pub fn encoding(&self) -> &'static str {
if self.ascii_only { "ascii" } else { "utf-8" }
}
pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
let segments = self.render_segments(renderable);
self.segments_to_string(&segments)
}
fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
self.render_segments_with(renderable, &self.options())
}
pub fn render_segments_with(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
) -> Vec<Segment> {
self.print_segments(renderable, options, false)
}
fn print_segments(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
end_line: bool,
) -> Vec<Segment> {
let mut options = options.clone();
let crop = !self.soft_wrap;
if self.soft_wrap {
options.no_wrap = options.no_wrap.or(Some(true));
options.overflow = options.overflow.or(Some(Overflow::Ignore));
}
let ends_line = end_line && Self::print_ends_line(renderable, &options);
let joined;
let joined_text = renderable.printed_text();
let renderable = match joined_text.clone() {
Some(text) => {
if matches!(
options.justify,
Justify::Left | Justify::Center | Justify::Right
) && text.measurement().1 == 0
{
return Vec::new();
}
joined = text;
&joined as &dyn Renderable
}
None => renderable,
};
let align = match options.justify {
Justify::Left if joined_text.is_none() => Some(crate::align::HorizontalAlign::Left),
Justify::Center if joined_text.is_none() => Some(crate::align::HorizontalAlign::Center),
Justify::Right if joined_text.is_none() => Some(crate::align::HorizontalAlign::Right),
_ => None,
};
if options.justify == Justify::Default && renderable.fit_to_measurement() {
let measurement = renderable.measure(self, &options);
options.max_width = measurement.maximum.min(options.max_width).max(1);
}
let mut segments = match align {
_ if options.max_width < 1 => Vec::new(),
Some(align) => crate::align::Align::render_child(renderable, align, self, &options),
None => renderable.rich_render(self, &options),
};
if !segments.is_empty() {
if ends_line {
segments.push(Segment::line());
}
if let Some(style) = self.console_style() {
segments = Segment::apply_style_with(&segments, Some(&style), None);
}
}
if end_line {
segments = self.process_render_hooks(vec![segments], &options).concat();
}
if segments.is_empty() || !crop {
return segments;
}
Segment::crop_lines(&segments, self.width())
}
fn console_style(&self) -> Option<Style> {
let style = self.style.as_ref()?;
self.get_style(style).ok().filter(|style| !style.is_null())
}
pub fn render(
&self,
renderable: &dyn Renderable,
options: Option<&ConsoleOptions>,
) -> Vec<Segment> {
let default_options;
let options = match options {
Some(options) => options,
None => {
default_options = self.options();
&default_options
}
};
if options.max_width < 1 {
return Vec::new();
}
renderable.rich_render(self, options)
}
fn emit(&self, segments: Vec<Segment>) {
self.emit_end(segments, true);
}
fn emit_end(&self, segments: Vec<Segment>, newline: bool) {
if segments.is_empty() || self.quiet() {
return;
}
let segments = {
let mut captures = self.lock_captures();
if let Some(buffer) = current_capture(&mut captures) {
buffer.extend(segments);
if newline {
buffer.push(Segment::line());
}
return;
}
segments
};
if self.record() {
let mut recorded = self.lock_record_buffer();
recorded.extend(segments.iter().cloned());
if newline {
recorded.push(Segment::line());
}
}
let mut output = self.segments_to_string(&segments);
if newline {
output.push('\n');
}
let stdout = std::io::stdout();
let mut lock = stdout.lock();
let mut written = lock.write_all(output.as_bytes());
if written.is_ok() && !newline {
written = lock.flush();
}
drop(lock);
if written.is_err_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe) {
self.on_broken_pipe();
}
}
pub fn input(&self, prompt: &str) -> std::io::Result<Option<String>> {
let prompt = self.build_text(prompt);
self.input_from(&prompt, &mut crate::prompt::StdinInput)
}
pub fn input_from(
&self,
prompt: &dyn Renderable,
stream: &mut dyn crate::prompt::InputSource,
) -> std::io::Result<Option<String>> {
let segments = self.render_segments(prompt);
if segments.iter().any(|segment| !segment.text.is_empty()) {
self.emit_end(segments, false);
}
stream.read_line()
}
pub fn render_lines(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
pad: bool,
) -> Vec<Vec<Segment>> {
self.render_lines_styled(renderable, options, None, pad)
}
pub fn render_lines_styled(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
style: Option<&Style>,
pad: bool,
) -> Vec<Vec<Segment>> {
let pad_style = style.cloned();
let style = style.filter(|style| !style.is_null());
let mut segments = if options.max_width < 1 {
Vec::new()
} else {
renderable.rich_render(self, options)
};
if let Some(style) = style {
segments = Segment::apply_style(&segments, style);
}
let only_empty = !segments.is_empty()
&& segments
.iter()
.all(|segment| !segment.control && segment.text.is_empty());
let mut lines = Segment::split_lines_owned(segments);
if lines.is_empty() && only_empty {
lines.push(Vec::new());
}
if pad {
for line in &mut lines {
*line = Segment::adjust_line_length(line, options.max_width, pad_style.clone());
}
}
if let Some(height) = options.height {
lines.truncate(height);
while lines.len() < height {
lines.push(if pad {
vec![Segment::new(
" ".repeat(options.max_width),
pad_style.clone(),
)]
} else {
Vec::new()
});
}
}
lines
}
pub fn render_export(&self, renderable: &dyn Renderable) -> String {
self.render_export_with(renderable, &self.options())
}
pub fn print(&self, renderable: &dyn Renderable) {
self.print_with(renderable, &self.options());
}
fn print_ends_line(renderable: &dyn Renderable, options: &ConsoleOptions) -> bool {
renderable.print_end_line(options)
|| (renderable.printed_text().is_none()
&& matches!(
options.justify,
Justify::Left | Justify::Center | Justify::Right
))
}
pub fn print_with(&self, renderable: &dyn Renderable, options: &ConsoleOptions) {
let segments = self.print_segments(renderable, options, true);
self.emit_end(segments, false);
}
pub fn render_export_with(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
) -> String {
let segments = self.print_segments(renderable, options, true);
self.segments_to_string(&segments)
}
pub fn control(&self, control: &crate::control::Control) {
let text = control.as_str();
if !text.is_empty() {
self.emit_end(vec![Segment::control(text)], false);
}
}
pub fn push_render_hook(&self, hook: std::sync::Arc<dyn RenderHook>) {
lock_or_recover(&self.render_hooks).push(hook);
}
pub fn pop_render_hook(&self) {
lock_or_recover(&self.render_hooks).pop();
}
fn process_render_hooks(
&self,
items: Vec<Vec<Segment>>,
options: &ConsoleOptions,
) -> Vec<Vec<Segment>> {
let hooks = lock_or_recover(&self.render_hooks).clone();
if hooks.is_empty() {
return items;
}
let mut renderables: Vec<Box<dyn Renderable>> = items
.into_iter()
.map(|segments| Box::new(Rendered(segments)) as Box<dyn Renderable>)
.collect();
for hook in &hooks {
renderables = hook.process_renderables(renderables);
}
renderables
.iter()
.map(|renderable| {
let mut segments = self.render(renderable.as_ref(), Some(options));
if !segments.is_empty() && renderable.print_end_line(options) {
segments.push(Segment::line());
}
segments
})
.collect()
}
pub fn set_live(&self, live: std::sync::Arc<dyn std::any::Any + Send + Sync>) -> bool {
let mut stack = lock_or_recover(&self.live_stack);
stack.push(live);
stack.len() == 1
}
pub fn clear_live(&self) {
lock_or_recover(&self.live_stack).pop();
}
pub(crate) fn live_stack(&self) -> Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
lock_or_recover(&self.live_stack).clone()
}
pub fn screen(
&self,
hide_cursor: bool,
style: Option<crate::style::StyleType>,
) -> ScreenContext<'_> {
let changed = self.set_alt_screen(true);
if changed && hide_cursor {
self.show_cursor(false);
}
let style = style.unwrap_or_else(|| "".into());
ScreenContext {
console: self,
hide_cursor,
changed,
screen: Some(crate::screen::Screen::empty().style(style.clone())),
style,
}
}
pub fn status(
&self,
status: crate::status::Status,
) -> crate::status::LiveStatus<std::io::Stdout> {
self.status_to(status, std::io::stdout())
}
pub fn status_to<W: Write + Send + 'static>(
&self,
status: crate::status::Status,
writer: W,
) -> crate::status::LiveStatus<W> {
status.start(self.clone(), writer)
}
pub fn is_alt_screen(&self) -> bool {
self.is_alt_screen.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn set_alt_screen(&self, enable: bool) -> bool {
if self.is_terminal && !self.legacy_windows {
self.control(&crate::control::Control::alt_screen(enable));
self.is_alt_screen
.store(enable, std::sync::atomic::Ordering::SeqCst);
return true;
}
false
}
pub fn update_screen(
&self,
renderable: &dyn Renderable,
region: Option<crate::region::Region>,
options: Option<&ConsoleOptions>,
) -> crate::errors::Result<()> {
if !self.is_alt_screen() {
return Err(no_alt_screen());
}
let render_options = options.cloned().unwrap_or_else(|| self.options());
let (x, y, render_options) = match region {
None => {
let height = render_options
.height
.filter(|&height| height > 0)
.unwrap_or(self.height());
let width = render_options.max_width;
(0, 0, render_options.update_dimensions(width, height))
}
Some(region) => (
region.x,
region.y,
render_options.update_dimensions(region.width, region.height),
),
};
let lines = self.render_lines(renderable, &render_options, true);
self.update_screen_lines(&lines, x, y)
}
pub fn update_screen_lines(
&self,
lines: &[Vec<Segment>],
x: usize,
y: usize,
) -> crate::errors::Result<()> {
if !self.is_alt_screen() {
return Err(no_alt_screen());
}
let update = ScreenUpdate::new(lines.to_vec(), x, y);
let segments = self.render(&update, None);
self.emit_end(segments, false);
Ok(())
}
pub fn show_cursor(&self, show: bool) {
self.control(&crate::control::Control::show_cursor(show));
}
pub fn clear(&self) {
self.control(&crate::control::Control::clear());
}
pub fn bell(&self) {
self.control(&crate::control::Control::bell());
}
pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
let segments = self.capture_segments(f);
self.segments_to_string(&segments)
}
pub fn begin_capture(&self) {
self.lock_captures()
.entry(std::thread::current().id())
.or_default()
.push(Vec::new());
}
pub fn end_capture(&self) -> String {
let id = std::thread::current().id();
let segments: Vec<Segment> = {
let mut captures = self.lock_captures();
let Some(stack) = captures.get_mut(&id) else {
return String::new();
};
let segments = stack.iter_mut().flat_map(std::mem::take).collect();
stack.pop();
if stack.is_empty() {
captures.remove(&id);
}
segments
};
self.segments_to_string(&segments)
}
pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
let segments = self.capture_segments(f);
segments_to_plain(&segments)
}
pub fn export_text_styled(&self, f: impl FnOnce(&Console)) -> String {
let segments = self.capture_segments(f);
self.segments_to_styled_text(&segments)
}
pub fn save_text(
&self,
path: impl AsRef<std::path::Path>,
styles: bool,
f: impl FnOnce(&Console),
) -> std::io::Result<()> {
let text = if styles {
self.export_text_styled(f)
} else {
self.export_text(f)
};
std::fs::write(path, text)
}
pub fn save_html(
&self,
path: impl AsRef<std::path::Path>,
f: impl FnOnce(&Console),
) -> std::io::Result<()> {
std::fs::write(path, self.export_html_classes(f))
}
pub fn save_svg(
&self,
path: impl AsRef<std::path::Path>,
title: &str,
unique_id: &str,
f: impl FnOnce(&Console),
) -> std::io::Result<()> {
std::fs::write(path, self.export_svg(title, unique_id, f))
}
pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
self.page_with(&crate::pager::SystemPager, styles, f)
}
pub fn page_with(
&self,
pager: &dyn crate::pager::Pager,
styles: bool,
f: impl FnOnce(&Console),
) -> std::io::Result<()> {
self.page_with_links(pager, styles, false, f)
}
pub fn page_with_links(
&self,
pager: &dyn crate::pager::Pager,
styles: bool,
links: bool,
f: impl FnOnce(&Console),
) -> std::io::Result<()> {
let mut segments = self.capture_segments(f);
let content = if !styles {
segments_to_plain(&segments)
} else {
if !links {
for segment in &mut segments {
if let Some(style) = segment.style.as_mut()
&& style.link().is_some()
{
*style = style.update_link(None);
}
}
}
self.segments_to_string(&segments)
};
pager.show(&content)
}
pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
}
pub fn export_html_themed(
&self,
theme: &crate::terminal_theme::TerminalTheme,
f: impl FnOnce(&Console),
) -> String {
let segments = self.capture_segments(f);
crate::export::export_html_inline(&segments, theme)
}
pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
}
pub fn export_html_classes_themed(
&self,
theme: &crate::terminal_theme::TerminalTheme,
f: impl FnOnce(&Console),
) -> String {
let segments = self.capture_segments(f);
crate::export::export_html_classes(&segments, theme)
}
pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
self.export_svg_themed(
&crate::terminal_theme::SVG_EXPORT_THEME,
title,
unique_id,
f,
)
}
pub fn export_svg_themed(
&self,
theme: &crate::terminal_theme::TerminalTheme,
title: &str,
unique_id: &str,
f: impl FnOnce(&Console),
) -> String {
let segments = self.capture_segments(f);
crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
}
pub fn export_html_with(
&self,
theme: &crate::terminal_theme::TerminalTheme,
code_format: Option<&str>,
inline_styles: bool,
f: impl FnOnce(&Console),
) -> Result<String, crate::export::ExportFormatError> {
let segments = self.capture_segments(f);
crate::export::export_html_with(&segments, theme, code_format, inline_styles)
}
#[allow(clippy::too_many_arguments)]
pub fn export_svg_with(
&self,
theme: &crate::terminal_theme::TerminalTheme,
title: &str,
unique_id: &str,
code_format: Option<&str>,
font_aspect_ratio: f64,
f: impl FnOnce(&Console),
) -> Result<String, crate::export::ExportFormatError> {
let segments = self.capture_segments(f);
crate::svg::export_svg_with(
&segments,
theme,
title,
unique_id,
self.width(),
code_format.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
font_aspect_ratio,
)
}
pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
self.capture_segments(f)
}
fn capture_segments(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
struct CaptureGuard<'a>(&'a Console);
impl CaptureGuard<'_> {
fn pop(&self) -> Vec<Segment> {
let id = std::thread::current().id();
let mut captures = self.0.lock_captures();
let Some(stack) = captures.get_mut(&id) else {
return Vec::new();
};
let captured = stack.pop().unwrap_or_default();
if stack.is_empty() {
captures.remove(&id);
}
captured
}
}
impl Drop for CaptureGuard<'_> {
fn drop(&mut self) {
self.pop();
}
}
self.lock_captures()
.entry(std::thread::current().id())
.or_default()
.push(Vec::new());
let guard = CaptureGuard(self);
f(self);
let captured = guard.pop();
std::mem::forget(guard);
captured
}
pub fn record(&self) -> bool {
self.record.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn set_record(&self, record: bool) {
self.record
.store(record, std::sync::atomic::Ordering::SeqCst);
}
fn take_recorded(&self, clear: bool) -> crate::errors::Result<Vec<Segment>> {
if !self.record() {
return Err(crate::errors::RichError::Console(
"To export console contents set record=True in the constructor or instance"
.to_string(),
));
}
let mut recorded = self.lock_record_buffer();
Ok(if clear {
std::mem::take(&mut *recorded)
} else {
recorded.clone()
})
}
pub fn export_text_recorded(&self, clear: bool, styles: bool) -> crate::errors::Result<String> {
let segments = self.take_recorded(clear)?;
Ok(if styles {
self.segments_to_styled_text(&segments)
} else {
segments_to_plain(&segments)
})
}
pub fn save_text_recorded(
&self,
path: impl AsRef<std::path::Path>,
clear: bool,
styles: bool,
) -> std::io::Result<()> {
let text = self
.export_text_recorded(clear, styles)
.map_err(std::io::Error::other)?;
std::fs::write(path, text)
}
pub fn export_html_recorded(
&self,
options: &ExportHtmlOptions<'_>,
) -> crate::errors::Result<String> {
let segments = self.take_recorded(options.clear)?;
let theme = options
.theme
.unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
crate::export::export_html_with(
&segments,
theme,
options.code_format,
options.inline_styles,
)
.map_err(|error| crate::errors::RichError::Console(error.to_string()))
}
pub fn save_html_recorded(
&self,
path: impl AsRef<std::path::Path>,
options: &ExportHtmlOptions<'_>,
) -> std::io::Result<()> {
let html = self
.export_html_recorded(options)
.map_err(std::io::Error::other)?;
std::fs::write(path, html)
}
pub fn export_svg_recorded(
&self,
options: &ExportSvgOptions<'_>,
) -> crate::errors::Result<String> {
let segments: Vec<Segment> = self
.take_recorded(options.clear)?
.into_iter()
.filter(|segment| !segment.control)
.collect();
let theme = options
.theme
.unwrap_or(&crate::terminal_theme::SVG_EXPORT_THEME);
crate::svg::export_svg_with(
&segments,
theme,
options.title,
options.unique_id,
self.width(),
options
.code_format
.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
options.font_aspect_ratio,
)
.map_err(|error| crate::errors::RichError::Console(error.to_string()))
}
pub fn save_svg_recorded(
&self,
path: impl AsRef<std::path::Path>,
options: &ExportSvgOptions<'_>,
) -> std::io::Result<()> {
let svg = self
.export_svg_recorded(options)
.map_err(std::io::Error::other)?;
std::fs::write(path, svg)
}
fn lock_record_buffer(&self) -> std::sync::MutexGuard<'_, Vec<Segment>> {
self.record_buffer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn segments_to_styled_text(&self, segments: &[Segment]) -> String {
let system = self.color_system().unwrap_or(ColorSystem::Truecolor);
segments
.iter()
.map(|segment| match &segment.style {
Some(style) => style.render(&segment.text, Some(system)),
None => segment.text.to_string(),
})
.collect()
}
fn lock_captures(&self) -> std::sync::MutexGuard<'_, CaptureStacks> {
self.captures
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn print_str(&self, content: &str) {
let text = self.build_text(content);
self.print(&text);
}
pub fn render_str_to_string(&self, content: &str) -> String {
let text = self.build_text(content);
self.render_to_string(&text)
}
pub fn build_text(&self, content: &str) -> Text {
self.try_build_text(content)
.unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
}
pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
if !self.markup {
return Ok(self.decorate(Text::new(self.expand_emoji_plain(content))));
}
let markup = self.parse_markup(content, self.emoji)?;
let mut text = self.decorate(Text::new(markup.plain()));
for span in markup.spans() {
text.push_span(span.clone());
}
Ok(text)
}
pub fn render_str(&self, content: &str, highlight: Option<bool>) -> Text {
let highlight = highlight.unwrap_or(self.highlight);
let markup = if !self.markup {
Text::new(self.expand_emoji_plain(content))
} else if content.contains('[') {
self.parse_markup(content, self.emoji)
.unwrap_or_else(|_| Text::new(self.expand_emoji(content)))
} else if content.contains(':') {
Text::new(self.expand_emoji(content))
} else {
Text::new(content)
};
if !highlight {
return markup;
}
let mut text = self.decorate_with_repr(Text::new(markup.plain()));
for span in markup.spans() {
text.push_span(span.clone());
}
text
}
pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
self.print(&self.try_build_text(content)?);
Ok(())
}
pub fn try_print_justified(
&self,
content: &str,
justify: Justify,
) -> crate::errors::Result<()> {
let text = self.try_build_text(content)?;
let mut options = self.options();
options.justify = justify;
self.emit(text.rich_render(self, &options));
Ok(())
}
pub(crate) fn expand_emoji(&self, content: &str) -> String {
if !self.emoji {
return content.to_string();
}
let variant = if content.contains('[') {
None
} else {
self.emoji_variant
};
crate::emoji::replace_with_variant(content, variant)
}
fn parse_markup(&self, content: &str, emoji: bool) -> crate::errors::Result<Text> {
if !content.contains('[') {
return Ok(Text::new(if emoji {
crate::emoji::replace_with_variant(content, self.emoji_variant)
} else {
content.to_string()
}));
}
if emoji {
crate::markup::render_emoji(content)
} else {
crate::markup::render(content)
}
}
fn expand_emoji_plain(&self, content: &str) -> String {
if self.emoji {
crate::emoji::replace_with_variant(content, self.emoji_variant)
} else {
content.to_string()
}
}
pub fn render_str_with(
&self,
content: &str,
options: &RenderStrOptions<'_>,
) -> crate::errors::Result<Text> {
let emoji = options.emoji.unwrap_or(self.emoji);
let markup = options.markup.unwrap_or(self.markup);
let highlight = options.highlight.unwrap_or(self.highlight);
let mut rich_text = if markup {
self.parse_markup(content, emoji)?
} else if emoji {
Text::new(crate::emoji::replace_with_variant(
content,
self.emoji_variant,
))
} else {
Text::new(content)
};
rich_text.set_base_style(options.style.clone());
rich_text.set_justify(options.justify.unwrap_or_default());
rich_text.set_overflow(options.overflow);
if !highlight {
return Ok(rich_text);
}
let mut text = Text::new(rich_text.plain());
match options.highlighter {
Some(highlighter) => highlighter.highlight(&mut text),
None => text = self.decorate_with_repr(text),
}
for span in rich_text.spans() {
text.push_span(span.clone());
}
Ok(text)
}
pub fn emoji(&self) -> bool {
self.emoji
}
pub fn highlight(&self) -> bool {
self.highlight
}
pub fn markup(&self) -> bool {
self.markup
}
pub fn emoji_variant(&self) -> Option<crate::emoji::EmojiVariant> {
self.emoji_variant
}
pub fn tab_size(&self) -> usize {
self.tab_size
}
pub fn set_tab_size(&mut self, tab_size: usize) {
self.tab_size = tab_size;
}
pub fn set_window_title(&self, title: &str) -> bool {
if self.is_terminal {
self.control(&crate::control::Control::title(title));
true
} else {
false
}
}
fn decorate(&self, mut text: Text) -> Text {
for highlighter in &self.highlighters {
highlighter.highlight(&mut text);
}
if self.highlight {
crate::highlighter::ReprHighlighter::new().highlight(&mut text);
}
text
}
fn decorate_with_repr(&self, mut text: Text) -> Text {
for highlighter in &self.highlighters {
highlighter.highlight(&mut text);
}
crate::highlighter::ReprHighlighter::new().highlight(&mut text);
text
}
pub fn print_justified(&self, content: &str, justify: Justify) {
let text = self.build_text(content);
let mut options = self.options();
options.justify = justify;
let segments = text.rich_render(self, &options);
self.emit(segments);
}
pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
let text = self.build_text(content);
let mut options = self.options();
options.justify = justify;
let segments = text.rich_render(self, &options);
self.segments_to_string(&segments)
}
pub fn segments_to_string(&self, segments: &[Segment]) -> String {
let system = self.color_system;
let colorless;
let segments = if self.no_color && system.is_some() {
colorless = Segment::remove_color(segments);
&colorless[..]
} else {
segments
};
let mut out = String::new();
for segment in segments {
if segment.control && !self.is_terminal {
continue;
}
match (&segment.style, system) {
(Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
_ => out.push_str(&segment.text),
}
}
out
}
}
fn segments_to_plain(segments: &[Segment]) -> String {
segments
.iter()
.filter(|s| !s.control)
.map(|s| s.text.as_str())
.collect()
}
impl Renderable for String {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.options_text(console, options, options.highlight)
.rich_render(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
let text = self.options_text(console, options, Some(false));
crate::measure::Measurement::get(console, options, &text)
}
}
trait OptionsText {
fn options_text(
&self,
console: &Console,
options: &ConsoleOptions,
highlight: Option<bool>,
) -> Text;
}
impl OptionsText for String {
fn options_text(
&self,
console: &Console,
options: &ConsoleOptions,
highlight: Option<bool>,
) -> Text {
let render_options = RenderStrOptions {
highlight,
markup: options.markup,
..Default::default()
};
console
.render_str_with(self, &render_options)
.unwrap_or_else(|_| {
console
.render_str_with(
self,
&RenderStrOptions {
markup: Some(false),
..render_options
},
)
.unwrap_or_else(|_| Text::new(self.as_str()))
})
}
}
impl Renderable for Text {
fn printed_text(&self) -> Option<Text> {
let mut text = self.clone();
text.clear_layout_options();
text.base_style_to_span();
Some(text)
}
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let justify = self.get_justify_option().unwrap_or(options.justify);
if self.is_empty() && matches!(justify, Justify::Default | Justify::Full) {
return vec![Segment::new("", None)];
}
let overflow = self
.get_overflow()
.or(options.overflow)
.unwrap_or(Overflow::Fold);
let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
self.render_joined_wrapped_tabs(
console.theme(),
console.base_style(),
options.max_width,
justify,
overflow,
no_wrap,
self.console_tab_size(console),
)
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
let (minimum, maximum) = self.measurement();
crate::measure::Measurement::new(
minimum.min(options.max_width),
maximum.min(options.max_width),
)
}
}
#[derive(Clone, Copy)]
pub struct ExportHtmlOptions<'a> {
pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
pub clear: bool,
pub code_format: Option<&'a str>,
pub inline_styles: bool,
}
impl Default for ExportHtmlOptions<'_> {
fn default() -> Self {
ExportHtmlOptions {
theme: None,
clear: true,
code_format: None,
inline_styles: false,
}
}
}
#[derive(Clone, Copy)]
pub struct ExportSvgOptions<'a> {
pub title: &'a str,
pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
pub clear: bool,
pub code_format: Option<&'a str>,
pub font_aspect_ratio: f64,
pub unique_id: &'a str,
}
impl Default for ExportSvgOptions<'_> {
fn default() -> Self {
ExportSvgOptions {
title: "Rich",
theme: None,
clear: true,
code_format: None,
font_aspect_ratio: 0.61,
unique_id: "terminal",
}
}
}
pub struct ConsoleBuilder {
force_terminal: Option<bool>,
color_system: Option<ColorSystem>,
color_system_set: bool,
width: Option<usize>,
height: Option<usize>,
no_color: Option<bool>,
get_time: Option<GetTime>,
emoji: Option<bool>,
highlight: Option<bool>,
legacy_windows: Option<bool>,
safe_box: Option<bool>,
ascii_only: Option<bool>,
theme: Option<Theme>,
markup: Option<bool>,
emoji_variant: Option<crate::emoji::EmojiVariant>,
tab_size: Option<usize>,
environ: Option<Environ>,
quiet: bool,
soft_wrap: bool,
style: Option<crate::style::StyleType>,
on_broken_pipe: Option<BrokenPipeHandler>,
record: bool,
log_time: bool,
log_path: bool,
log_time_format: Option<LogTimeFormat>,
get_datetime: Option<GetDateTime>,
}
impl ConsoleBuilder {
fn new() -> Self {
ConsoleBuilder {
force_terminal: None,
color_system: None,
color_system_set: false,
width: None,
height: None,
no_color: None,
get_time: None,
emoji: None,
highlight: None,
legacy_windows: None,
safe_box: None,
ascii_only: None,
theme: None,
markup: None,
emoji_variant: None,
tab_size: None,
environ: None,
quiet: false,
soft_wrap: false,
style: None,
on_broken_pipe: None,
record: false,
log_time: true,
log_path: true,
log_time_format: None,
get_datetime: None,
}
}
pub fn log_time(mut self, value: bool) -> Self {
self.log_time = value;
self
}
pub fn log_path(mut self, value: bool) -> Self {
self.log_path = value;
self
}
pub fn log_time_format(mut self, format: impl Into<String>) -> Self {
self.log_time_format = Some(LogTimeFormat::Strftime(format.into()));
self
}
pub fn log_time_formatter(
mut self,
format: impl Fn(std::time::SystemTime) -> Text + Send + Sync + 'static,
) -> Self {
self.log_time_format = Some(LogTimeFormat::Callable(std::sync::Arc::new(format)));
self
}
pub fn get_datetime(
mut self,
clock: impl Fn() -> std::time::SystemTime + Send + Sync + 'static,
) -> Self {
self.get_datetime = Some(std::sync::Arc::new(clock));
self
}
pub fn record(mut self, value: bool) -> Self {
self.record = value;
self
}
pub fn environ(mut self, environ: Environ) -> Self {
self.environ = Some(environ);
self
}
pub fn quiet(mut self, value: bool) -> Self {
self.quiet = value;
self
}
pub fn soft_wrap(mut self, value: bool) -> Self {
self.soft_wrap = value;
self
}
pub fn style(mut self, style: impl Into<crate::style::StyleType>) -> Self {
self.style = Some(style.into());
self
}
pub fn on_broken_pipe(mut self, handler: impl Fn(&Console) + Send + Sync + 'static) -> Self {
self.on_broken_pipe = Some(std::sync::Arc::new(handler));
self
}
pub fn markup(mut self, value: bool) -> Self {
self.markup = Some(value);
self
}
pub fn emoji_variant(mut self, variant: Option<crate::emoji::EmojiVariant>) -> Self {
self.emoji_variant = variant;
self
}
pub fn tab_size(mut self, tab_size: usize) -> Self {
self.tab_size = Some(tab_size);
self
}
pub fn force_terminal(mut self, value: bool) -> Self {
self.force_terminal = Some(value);
self
}
pub fn legacy_windows(mut self, value: bool) -> Self {
self.legacy_windows = Some(value);
self
}
pub fn safe_box(mut self, value: bool) -> Self {
self.safe_box = Some(value);
self
}
pub fn ascii_only(mut self, value: bool) -> Self {
self.ascii_only = Some(value);
self
}
pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
self.color_system = system;
self.color_system_set = true;
self
}
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: usize) -> Self {
self.height = Some(height);
self
}
pub fn no_color(mut self, value: bool) -> Self {
self.no_color = Some(value);
self
}
pub fn get_time(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
self.get_time = Some(std::sync::Arc::new(clock));
self
}
pub fn emoji(mut self, value: bool) -> Self {
self.emoji = Some(value);
self
}
pub fn highlight(mut self, value: bool) -> Self {
self.highlight = Some(value);
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = Some(theme);
self
}
pub fn build(self) -> Console {
let environ = self.environ.map(std::sync::Arc::new);
let env = environ.as_deref();
let legacy_windows = self.legacy_windows.unwrap_or(false);
let is_terminal = self
.force_terminal
.unwrap_or_else(|| detect_is_terminal(env));
let no_color = self
.no_color
.unwrap_or_else(|| environ_get(env, "NO_COLOR").is_some_and(|value| !value.is_empty()));
let color_system = if self.color_system_set {
self.color_system
} else if is_terminal && !term_is_dumb(env) {
Some(detect_color_system(env))
} else {
None
};
let width = self.width.or_else(|| {
env_int(env, "COLUMNS")
.map(|columns| columns.saturating_sub(usize::from(legacy_windows)))
});
let height = self.height.or_else(|| env_int(env, "LINES"));
let terminal_size = detect_terminal_size(legacy_windows);
Console {
render_environment: None,
region_sink: None,
code_highlighting: None,
color_system,
width,
height,
terminal_size,
environ,
is_terminal,
no_color,
quiet: std::sync::atomic::AtomicBool::new(self.quiet),
soft_wrap: self.soft_wrap,
style: self.style,
on_broken_pipe: self.on_broken_pipe,
get_time: self
.get_time
.unwrap_or_else(|| std::sync::Arc::new(monotonic)),
emoji: self.emoji.unwrap_or(true),
highlight: self.highlight.unwrap_or(true),
legacy_windows,
safe_box: self.safe_box.unwrap_or(true),
ascii_only: self.ascii_only.unwrap_or(false),
theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
base_style: Style::new(),
highlighters: Vec::new(),
markup: self.markup.unwrap_or(true),
emoji_variant: self.emoji_variant,
tab_size: self.tab_size.unwrap_or(crate::text::DEFAULT_TAB_SIZE),
captures: std::sync::Mutex::new(CaptureStacks::new()),
is_alt_screen: std::sync::atomic::AtomicBool::new(false),
record: std::sync::atomic::AtomicBool::new(self.record),
record_buffer: std::sync::Mutex::new(Vec::new()),
render_hooks: std::sync::Mutex::new(Vec::new()),
live_stack: LiveStack::default(),
log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
log_time: self.log_time,
log_path: self.log_path,
log_time_format: self
.log_time_format
.unwrap_or_else(|| LogTimeFormat::Strftime("[%X]".to_string())),
get_datetime: self
.get_datetime
.unwrap_or_else(|| std::sync::Arc::new(std::time::SystemTime::now)),
}
}
}
fn absolute_path(path: &str) -> String {
let path = std::path::Path::new(path);
if path.is_absolute() {
return path.to_string_lossy().into_owned();
}
std::env::current_dir()
.map(|directory| directory.join(path).to_string_lossy().into_owned())
.unwrap_or_else(|_| path.to_string_lossy().into_owned())
}
fn new_log_render(log_time: bool, log_path: bool) -> crate::log_render::LogRender {
crate::log_render::LogRender::new()
.show_time(log_time)
.show_path(log_path)
}
fn term_is_dumb(environ: Option<&Environ>) -> bool {
environ_get(environ, "TERM")
.is_some_and(|term| matches!(term.to_lowercase().as_str(), "dumb" | "unknown"))
}
fn detect_is_terminal(environ: Option<&Environ>) -> bool {
match environ_get(environ, "TTY_COMPATIBLE").as_deref() {
Some("0") => return false,
Some("1") => return true,
_ => {}
}
if let Some(force_color) = environ_get(environ, "FORCE_COLOR") {
return !force_color.is_empty();
}
std::io::stdout().is_terminal()
}
#[cfg(not(windows))]
fn term_colors(colors: &str) -> Option<ColorSystem> {
match colors {
"kitty" | "256color" => Some(ColorSystem::EightBit),
"16color" => Some(ColorSystem::Standard),
_ => None,
}
}
#[cfg_attr(windows, allow(unused_variables))]
fn detect_color_system(environ: Option<&Environ>) -> ColorSystem {
#[cfg(windows)]
{
let _ = anstyle_query::windows::enable_ansi_colors();
ColorSystem::Truecolor
}
#[cfg(not(windows))]
{
let color_term = environ_get(environ, "COLORTERM")
.unwrap_or_default()
.trim()
.to_lowercase();
if matches!(color_term.as_str(), "truecolor" | "24bit") {
return ColorSystem::Truecolor;
}
let term = environ_get(environ, "TERM")
.unwrap_or_default()
.trim()
.to_lowercase();
let colors = term.rsplit('-').next().unwrap_or("");
term_colors(colors).unwrap_or(ColorSystem::Standard)
}
}
fn detect_terminal_size(legacy_windows: bool) -> ConsoleDimensions {
let (width, height) = match terminal_size::terminal_size() {
Some((terminal_size::Width(w), terminal_size::Height(h))) => (w as usize, h as usize),
None => (0, 0),
};
let width = if width == 0 { DEFAULT_WIDTH } else { width };
let height = if height == 0 { DEFAULT_HEIGHT } else { height };
ConsoleDimensions {
width: width - usize::from(legacy_windows),
height,
}
}
impl crate::protocol::ConsoleCodeHighlighting for Console {
fn set_code_highlighting(&mut self, value: Option<crate::protocol::CodeHighlighting>) {
self.code_highlighting = value;
}
fn code_highlighting(&self) -> Option<&crate::protocol::CodeHighlighting> {
self.code_highlighting.as_ref()
}
}
impl crate::protocol::ConsoleRegions for Console {
fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>) {
self.region_sink = value;
}
fn region_sink(&self) -> Option<&dyn crate::protocol::RegionSink> {
self.region_sink.as_deref()
}
}
impl crate::protocol::ConsoleEnvironment for Console {
fn set_render_environment(
&mut self,
value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
) {
self.render_environment = value;
}
fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
self.render_environment.as_deref()
}
}
pub enum PrintObject<'a> {
Str(&'a str),
Text(&'a Text),
Renderable(&'a dyn Renderable),
}
#[derive(Clone)]
pub struct PrintOptions<'a> {
pub sep: &'a str,
pub end: &'a str,
pub style: Option<crate::style::StyleType>,
pub justify: Option<Justify>,
pub overflow: Option<Overflow>,
pub no_wrap: Option<bool>,
pub emoji: Option<bool>,
pub markup: Option<bool>,
pub highlight: Option<bool>,
pub width: Option<usize>,
pub height: Option<usize>,
pub crop: bool,
pub soft_wrap: Option<bool>,
pub new_line_start: bool,
}
impl Default for PrintOptions<'_> {
fn default() -> Self {
PrintOptions {
sep: " ",
end: "\n",
style: None,
justify: None,
overflow: None,
no_wrap: None,
emoji: None,
markup: None,
highlight: None,
width: None,
height: None,
crop: true,
soft_wrap: None,
new_line_start: false,
}
}
}
#[derive(Clone)]
pub enum LogObject<'a> {
Str(&'a str),
Text(&'a Text),
Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
}
#[derive(Clone)]
pub struct LogOptions<'a> {
pub sep: &'a str,
pub end: &'a str,
pub style: Option<crate::style::StyleType>,
pub justify: Option<Justify>,
pub emoji: Option<bool>,
pub markup: Option<bool>,
pub highlight: Option<bool>,
}
impl Default for LogOptions<'_> {
fn default() -> Self {
LogOptions {
sep: " ",
end: "\n",
style: None,
justify: None,
emoji: None,
markup: None,
highlight: None,
}
}
}
pub(crate) struct OwnedLog {
objects: Vec<OwnedLogObject>,
sep: String,
end: String,
style: Option<crate::style::StyleType>,
justify: Option<Justify>,
emoji: Option<bool>,
markup: Option<bool>,
highlight: Option<bool>,
file: String,
line: u32,
}
enum OwnedLogObject {
Str(String),
Text(Text),
Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
}
impl OwnedLog {
pub(crate) fn new(
objects: &[LogObject<'_>],
options: &LogOptions<'_>,
file: &str,
line: u32,
) -> Self {
OwnedLog {
objects: objects
.iter()
.map(|object| match object {
LogObject::Str(content) => OwnedLogObject::Str((*content).to_string()),
LogObject::Text(text) => OwnedLogObject::Text((*text).clone()),
LogObject::Renderable(renderable) => {
OwnedLogObject::Renderable(renderable.clone())
}
})
.collect(),
sep: options.sep.to_string(),
end: options.end.to_string(),
style: options.style.clone(),
justify: options.justify,
emoji: options.emoji,
markup: options.markup,
highlight: options.highlight,
file: file.to_string(),
line,
}
}
pub(crate) fn log(&self, console: &Console) -> crate::errors::Result<()> {
let objects: Vec<LogObject<'_>> = self
.objects
.iter()
.map(|object| match object {
OwnedLogObject::Str(content) => LogObject::Str(content),
OwnedLogObject::Text(text) => LogObject::Text(text),
OwnedLogObject::Renderable(renderable) => LogObject::Renderable(renderable.clone()),
})
.collect();
let options = LogOptions {
sep: &self.sep,
end: &self.end,
style: self.style.clone(),
justify: self.justify,
emoji: self.emoji,
markup: self.markup,
highlight: self.highlight,
};
console.log_at(&objects, &options, &self.file, self.line)
}
}
struct LogItem {
renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
align: Option<crate::align::HorizontalAlign>,
end: Option<String>,
styles: Vec<Style>,
}
impl Renderable for LogItem {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = match self.align {
Some(align) => {
crate::align::Align::render_child(&*self.renderable, align, console, options)
}
None => self.renderable.rich_render(console, options),
};
if let Some(end) = self.end.as_deref().filter(|end| !end.is_empty()) {
segments.push(Segment::new(end, None));
if end.ends_with('\n') {
segments.push(Segment::new("", None));
}
}
for style in &self.styles {
segments = Segment::apply_style_with(&segments, Some(style), None);
}
segments
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
crate::measure::Measurement::get(console, options, &*self.renderable)
}
}
impl Console {
#[track_caller]
pub fn log(
&self,
objects: &[LogObject<'_>],
options: &LogOptions<'_>,
) -> crate::errors::Result<()> {
let caller = std::panic::Location::caller();
self.log_at(objects, options, caller.file(), caller.line())
}
pub fn log_at(
&self,
objects: &[LogObject<'_>],
options: &LogOptions<'_>,
file: &str,
line: u32,
) -> crate::errors::Result<()> {
let mut styles = Vec::new();
if let Some(style) = &self.style {
styles.push(self.get_style(style)?);
}
if let Some(style) = &options.style {
styles.push(self.get_style(style)?);
}
styles.retain(|style| !style.is_null());
let align = match options.justify {
Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
_ => None,
};
let item = |renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
end: Option<String>|
-> std::sync::Arc<dyn Renderable + Send + Sync> {
std::sync::Arc::new(LogItem {
renderable,
align,
end,
styles: styles.clone(),
})
};
let end = options
.end
.strip_suffix('\n')
.unwrap_or(options.end)
.to_string();
let mut renderables: Vec<std::sync::Arc<dyn Renderable + Send + Sync>> = Vec::new();
if objects.is_empty() {
renderables.push(item(std::sync::Arc::new(Text::new("")), None));
}
let mut texts: Vec<Text> = Vec::new();
let flush =
|texts: &mut Vec<Text>,
renderables: &mut Vec<std::sync::Arc<dyn Renderable + Send + Sync>>| {
if !texts.is_empty() {
let mut separator = Text::new(options.sep);
separator.set_justify_option(options.justify);
let joined = separator.join(texts);
renderables.push(item(std::sync::Arc::new(joined), Some(end.clone())));
texts.clear();
}
};
for object in objects {
match object {
LogObject::Str(content) => {
let render_str = RenderStrOptions {
emoji: options.emoji,
markup: options.markup,
highlight: options.highlight,
..Default::default()
};
texts.push(self.render_str_with(content, &render_str)?);
}
LogObject::Text(text) => texts.push((*text).clone()),
LogObject::Renderable(renderable) => {
flush(&mut texts, &mut renderables);
renderables.push(item(renderable.clone(), None));
}
}
}
flush(&mut texts, &mut renderables);
let time = self.log_time.then(|| {
let now = (self.get_datetime)();
match &self.log_time_format {
LogTimeFormat::Strftime(format) => {
Text::new(crate::log_render::strftime(now, format))
}
LogTimeFormat::Callable(format) => format(now),
}
});
let path = file
.rsplit(std::path::MAIN_SEPARATOR)
.next()
.unwrap_or(file);
let link_path = (!file.starts_with('<')).then(|| absolute_path(file));
let table = self
.log_render
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.render_renderables(
self,
renderables,
time,
Text::new(""),
Some(path),
Some(line),
link_path.as_deref(),
);
let mut segments = self.render(&table, None);
if !segments.is_empty() {
segments.push(Segment::line());
}
let segments = self
.process_render_hooks(vec![segments], &self.options())
.concat();
let segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
.into_iter()
.flatten()
.collect();
self.emit_end(segments, false);
Ok(())
}
pub fn print_objects(
&self,
objects: &[PrintObject<'_>],
options: &PrintOptions<'_>,
) -> crate::errors::Result<()> {
let segments = self.render_objects(objects, options)?;
self.emit_end(segments, false);
Ok(())
}
pub fn render_objects(
&self,
objects: &[PrintObject<'_>],
options: &PrintOptions<'_>,
) -> crate::errors::Result<Vec<Segment>> {
let (mut no_wrap, mut overflow, mut crop) =
(options.no_wrap, options.overflow, options.crop);
if options.soft_wrap.unwrap_or(self.soft_wrap) {
no_wrap = no_wrap.or(Some(true));
overflow = overflow.or(Some(Overflow::Ignore));
crop = false;
}
let align = match options.justify {
Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
_ => None,
};
let mut render_options = self.options();
if let Some(justify) = options.justify {
render_options.justify = justify;
}
render_options.overflow = overflow;
render_options.no_wrap = no_wrap;
render_options.markup = options.markup;
render_options.highlight = options.highlight;
if let Some(width) = options.width {
render_options = render_options.update_width(width.min(self.width()));
}
if options.height.is_some() {
render_options.height = options.height;
}
let style = match &options.style {
Some(style) => Some(self.get_style(style)?),
None => None,
};
let console_style = match &self.style {
Some(style) => Some(self.get_style(style)?).filter(|style| !style.is_null()),
None => None,
};
let mut items: Vec<Vec<Segment>> = Vec::new();
let mut emit = |renderable: &dyn Renderable, end: Option<&str>| {
let mut rendered = if render_options.max_width < 1 {
Vec::new()
} else {
match align {
Some(align) => {
crate::align::Align::render_child(renderable, align, self, &render_options)
}
None => renderable.rich_render(self, &render_options),
}
};
match end {
Some(end) if !end.is_empty() => rendered.push(Segment::new(end, None)),
Some(_) => {}
None if !rendered.is_empty()
&& Self::print_ends_line(renderable, &render_options) =>
{
rendered.push(Segment::line())
}
None => {}
}
if let Some(console_style) = &console_style {
rendered = Segment::apply_style_with(&rendered, Some(console_style), None);
}
items.push(rendered);
};
let blank = [PrintObject::Str("")];
let objects = if objects.is_empty() && options.end != "\n" {
&blank[..]
} else {
objects
};
if objects.is_empty() {
emit(&Text::new(""), Some("\n"));
}
let mut texts: Vec<Text> = Vec::new();
let flush = |texts: &mut Vec<Text>, emit: &mut dyn FnMut(&dyn Renderable, Option<&str>)| {
if !texts.is_empty() {
let mut separator = Text::new(options.sep);
separator.set_justify_option(options.justify);
emit(&separator.join(texts), Some(options.end));
texts.clear();
}
};
for object in objects {
match object {
PrintObject::Str(content) => {
let render_str = RenderStrOptions {
emoji: options.emoji,
markup: options.markup,
highlight: options.highlight,
..Default::default()
};
texts.push(self.render_str_with(content, &render_str)?);
}
PrintObject::Text(text) => texts.push((*text).clone()),
PrintObject::Renderable(renderable) => {
flush(&mut texts, &mut emit);
emit(*renderable, None);
}
}
}
flush(&mut texts, &mut emit);
let mut segments: Vec<Segment> = Vec::new();
for mut rendered in self.process_render_hooks(items, &render_options) {
if let Some(style) = &style {
let mut styled = Vec::with_capacity(rendered.len());
for (line, terminated) in Segment::split_lines_terminator(&rendered) {
styled.extend(Segment::apply_style_with(&line, Some(style), None));
if terminated {
styled.push(Segment::line());
}
}
rendered = styled;
}
segments.extend(rendered);
}
if options.new_line_start {
let plain: String = segments
.iter()
.map(|segment| segment.text.as_str())
.collect();
if plain.lines().count() > 1 {
segments.insert(0, Segment::line());
}
}
if crop {
segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
.into_iter()
.flatten()
.collect();
}
Ok(segments)
}
pub fn line(&self, count: usize) {
if count > 0 {
self.emit_end(vec![Segment::new("\n".repeat(count), None)], false);
}
}
pub fn rule(
&self,
title: &str,
characters: &str,
style: impl Into<crate::style::StyleType>,
align: crate::align::HorizontalAlign,
) {
let rule = crate::rule::Rule::new(title)
.characters(characters)
.style(style)
.align(align);
self.print(&rule);
}
pub fn out(
&self,
objects: &[&str],
sep: &str,
end: &str,
style: Option<crate::style::StyleType>,
highlight: Option<bool>,
) -> crate::errors::Result<()> {
let raw = objects.join(sep);
self.print_objects(
&[PrintObject::Str(&raw)],
&PrintOptions {
end,
style,
highlight,
emoji: Some(false),
markup: Some(false),
no_wrap: Some(true),
overflow: Some(Overflow::Ignore),
crop: false,
..Default::default()
},
)
}
pub fn print_json(
&self,
json: &str,
options: &crate::json::JsonOptions,
) -> crate::errors::Result<()> {
let renderable = crate::json::Json::with_options(json, options)?;
self.print_objects(
&[PrintObject::Renderable(&renderable)],
&PrintOptions {
soft_wrap: Some(true),
..Default::default()
},
)
}
pub fn measure(
&self,
renderable: &dyn Renderable,
options: Option<&ConsoleOptions>,
) -> crate::measure::Measurement {
let default_options;
let options = match options {
Some(options) => options,
None => {
default_options = self.options();
&default_options
}
};
crate::measure::Measurement::get(self, options, renderable)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_console() -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.build()
}
#[test]
fn empty_text_and_empty_renderables_have_distinct_endings() {
let console = Console::builder().force_terminal(false).build();
assert_eq!(console.render_export(&Text::new("")), "\n");
#[cfg(feature = "markdown")]
assert_eq!(
console.render_export(&crate::markdown::Markdown::new("")),
""
);
assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
}
#[test]
fn try_build_text_reports_bad_markup() {
let console = test_console();
let err = console
.try_build_text("[/nope]")
.expect_err("an unmatched closing tag must be an error");
assert!(
matches!(err, crate::errors::RichError::Markup(_)),
"{err:?}"
);
assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
assert_eq!(strict.plain(), "hi");
assert_eq!(
strict.spans().len(),
console.build_text("[bold]hi[/]").spans().len()
);
}
#[test]
fn unknown_tag_names_render_as_no_ops() {
let console = test_console();
let text = console
.try_build_text("[nope]x[/]")
.expect("an unknown tag name is not a syntax error");
assert_eq!(console.render_to_string(&text), "x");
assert_eq!(
console.render_to_string(&console.build_text("[a.b.c]x[/]")),
"x"
);
assert!(console.try_build_text("[bold]a[/italic]").is_err());
assert!(console.try_build_text("[/nope]").is_err());
}
#[test]
fn markup_styles_bind_at_render_not_at_parse() {
let themed = |definition: &str| {
let mut theme = Theme::default_theme();
theme.insert("accent", Style::parse(definition).unwrap());
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.theme(theme)
.build()
};
let red = themed("bold red");
let green = themed("underline green");
let text = red.build_text("[accent]hi[/]");
assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
}
#[test]
fn try_build_text_expands_emoji_like_build_text() {
let console = test_console();
assert_eq!(
console
.try_build_text(":rocket: go")
.expect("valid")
.plain(),
console.build_text(":rocket: go").plain()
);
}
#[test]
fn renders_markup_string() {
let console = test_console();
assert_eq!(
console.render_str_to_string("[bold red]hi[/]"),
"\x1b[1;31mhi\x1b[0m"
);
}
#[test]
fn print_justify_pads_to_width() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(10)
.build();
assert_eq!(
console.render_justified_to_string("hi", Justify::Left),
"hi "
);
assert_eq!(
console.render_justified_to_string("hi", Justify::Center),
" hi "
);
assert_eq!(
console.render_justified_to_string("hi", Justify::Right),
" hi"
);
}
#[test]
fn capture_records_ansi_instead_of_stdout() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.build();
let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
}
#[test]
fn themed_exports_use_the_given_palette() {
use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.no_color(false)
.build();
let render = |c: &Console| c.print_str("hi");
let monokai = console.export_html_themed(&MONOKAI, render);
assert!(
monokai.contains("#0c0c0c"),
"monokai bg missing:\n{monokai}"
);
let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
let classes = console.export_html_classes_themed(&MONOKAI, render);
assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
assert!(console.export_html(render).contains("#ffffff"));
}
#[test]
fn page_with_honors_the_styles_flag() {
use std::sync::Mutex;
#[derive(Default)]
struct Recorder(Mutex<String>);
impl crate::pager::Pager for Recorder {
fn show(&self, content: &str) -> std::io::Result<()> {
*self.0.lock().unwrap() = content.to_string();
Ok(())
}
}
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.no_color(false)
.build();
let plain = Recorder::default();
console
.page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
.unwrap();
assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
let styled = Recorder::default();
console
.page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
.unwrap();
assert_eq!(
styled.0.lock().unwrap().as_str(),
"\x1b[1;31mhi\x1b[0m there\n"
);
let linked = |links: bool| {
let recorder = Recorder::default();
console
.page_with_links(&recorder, true, links, |c| {
c.print_str("[bold link=https://x.io]hi[/] there")
})
.unwrap();
recorder.0.lock().unwrap().clone()
};
assert_eq!(linked(false), "\x1b[1mhi\x1b[0m there\n");
assert_eq!(
linked(true),
"\x1b]8;;https://x.io\x1b\\\x1b[1mhi\x1b[0m\x1b]8;;\x1b\\ there\n"
);
}
#[test]
fn export_text_styled_renders_truecolor() {
let console = |system| {
Console::builder()
.force_terminal(true)
.color_system(system)
.width(20)
.no_color(false)
.build()
};
let export =
|console: Console| console.export_text_styled(|c| c.print_str("[#ff8800]x[/] y"));
assert_eq!(
export(console(Some(ColorSystem::Standard))),
"\x1b[91mx\x1b[0m y\n"
);
assert_eq!(export(console(None)), "\x1b[38;2;255;136;0mx\x1b[0m y\n");
}
#[test]
fn export_text_strips_styles() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.build();
let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
assert_eq!(out, "hi there\n");
}
#[test]
fn export_html_matches_upstream() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.no_color(false)
.build();
let html = console.export_html(|c| {
c.print_str("[bold red]hi[/] there");
c.print_str("plain line");
});
let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
assert_eq!(html, expected);
}
#[test]
fn export_html_classes_matches_upstream() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.no_color(false)
.build();
let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
let expected =
include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
assert_eq!(html, expected);
}
#[test]
fn render_lines_pads_with_the_given_style() {
let console = test_console();
let options = console.options().update_width(5).update_height(2);
let lines = console.render_lines(&Text::new("hi"), &options, true);
assert_eq!(lines.len(), 2);
let width_pad = lines[0].last().unwrap();
assert_eq!((width_pad.text.as_str(), &width_pad.style), (" ", &None));
assert_eq!(lines[1], vec![Segment::new(" ", None)]);
let red = Style::parse("red").unwrap();
let lines = console.render_lines_styled(&Text::new("hi"), &options, Some(&red), true);
assert_eq!(lines[0].last().unwrap().style, Some(red.clone()));
assert_eq!(lines[1], vec![Segment::new(" ", Some(red))]);
}
#[test]
fn capture_matches_direct_render() {
let console = test_console();
let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
assert_eq!(
console.capture(|c| c.print(&panel)),
console.render_export(&panel)
);
}
#[test]
fn no_color_strips_styles() {
let console = Console::builder()
.force_terminal(true)
.color_system(None)
.build();
assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
}
#[test]
fn used_theme_applies_through_the_guard_and_pops_on_drop() {
let mut console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.highlight(false)
.build();
let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
{
let themed = console.use_theme(theme);
let out = themed.capture(|c| c.print_str("[accent]x[/]"));
assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
}
assert!(console.theme().get("accent").is_none());
assert!(console.pop_theme().is_err(), "the base theme must remain");
}
#[test]
fn used_theme_is_popped_during_a_panic_unwind() {
let mut console = Console::builder().width(20).build();
let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _themed = console.use_theme(theme);
panic!("render failed");
}));
assert!(unwound.is_err());
assert!(console.theme().get("accent").is_none());
}
#[test]
fn print_objects_keeps_a_renderable_s_open_last_line() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(crate::color::ColorSystem::Truecolor))
.width(4)
.height(2)
.build();
let screen = crate::screen::Screen::new(Box::new(Text::new("hi")));
let printed = console.capture(|c| c.print(&screen));
let objects = console.capture(|c| {
c.print_objects(
&[PrintObject::Renderable(&screen)],
&PrintOptions::default(),
)
.unwrap()
});
assert_eq!(objects, printed);
assert!(!objects.ends_with('\n'), "{objects:?}");
}
#[test]
fn a_printed_text_defers_layout_to_the_print_options() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(crate::color::ColorSystem::Truecolor))
.width(6)
.highlight(false)
.build();
let text = Text::new("abcdefghij").overflow(Overflow::Ellipsis);
assert_eq!(console.render_export(&text), "abcdef\nghij\n");
let mut options = console.options();
options.overflow = Some(Overflow::Ellipsis);
assert_eq!(
console.render_export_with(&Text::new("abcdefghij"), &options),
"abcde…\n"
);
let wide = Console::builder()
.force_terminal(true)
.color_system(Some(crate::color::ColorSystem::Truecolor))
.width(20)
.highlight(false)
.build();
let tabbed = Text::new("a\tb").justify(Justify::Right);
assert_eq!(wide.render_export(&tabbed), "a b\n");
}
}