use std::fmt;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use crate::align::AlignMethod;
use crate::color::{Color, ColorSystem};
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
use crate::theme::Theme;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConsoleDimensions {
pub width: usize,
pub height: usize,
}
impl ConsoleDimensions {
pub fn detect() -> Self {
if let Some((w, h)) = terminal_size::terminal_size() {
Self {
width: w.0 as usize,
height: h.0 as usize,
}
} else {
Self {
width: 80,
height: 25,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OverflowMethod {
Fold,
Crop,
Ellipsis,
Ignore,
}
#[derive(Debug, Clone)]
pub struct ConsoleOptions {
pub size: ConsoleDimensions,
pub is_terminal: bool,
pub encoding: String,
pub min_width: usize,
pub max_width: usize,
pub max_height: usize,
pub justify: Option<AlignMethod>,
pub overflow: Option<OverflowMethod>,
pub no_wrap: bool,
pub ascii_only: bool,
pub markup: bool,
pub highlight: bool,
pub height: Option<usize>,
pub legacy_windows: bool,
}
impl Default for ConsoleOptions {
fn default() -> Self {
Self {
size: ConsoleDimensions::detect(),
is_terminal: true,
encoding: "utf-8".into(),
min_width: 1,
max_width: 80,
max_height: 25,
justify: None,
overflow: None,
no_wrap: false,
ascii_only: false,
markup: true,
highlight: true,
height: None,
legacy_windows: false,
}
}
}
impl ConsoleOptions {
pub fn update_width(&self, max_width: usize) -> Self {
let mut opts = self.clone();
opts.max_width = max_width;
opts
}
pub fn update_height(&self, height: usize) -> Self {
let mut opts = self.clone();
opts.height = Some(height);
opts
}
pub fn shrink_width(&self, amount: usize) -> Self {
let mut opts = self.clone();
opts.max_width = opts.max_width.saturating_sub(amount);
opts
}
}
#[derive(Clone)]
pub enum RenderItem {
Segment(Segment),
Nested(DynRenderable),
}
impl fmt::Debug for RenderItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Segment(s) => write!(f, "Segment({})", &s.text),
Self::Nested(_) => write!(f, "Nested(...)"),
}
}
}
impl From<Segment> for RenderItem {
fn from(s: Segment) -> Self { Self::Segment(s) }
}
impl From<DynRenderable> for RenderItem {
fn from(r: DynRenderable) -> Self { Self::Nested(r) }
}
#[derive(Debug, Clone)]
pub struct RenderResult {
pub lines: Vec<Vec<Segment>>,
pub items: Vec<RenderItem>,
}
impl RenderResult {
pub fn new() -> Self {
Self { lines: Vec::new(), items: Vec::new() }
}
pub fn from_text(text: &str) -> Self {
Self {
lines: vec![vec![Segment::new(text)]],
items: vec![RenderItem::Segment(Segment::new(text))],
}
}
pub fn from_segments(segments: Vec<Segment>) -> Self {
let items: Vec<RenderItem> = segments.iter().map(|s| RenderItem::Segment(s.clone())).collect();
Self { lines: vec![segments], items }
}
pub fn from_lines(lines: Vec<Vec<Segment>>) -> Self {
Self { lines, items: Vec::new() }
}
pub fn from_items(items: Vec<RenderItem>) -> Self {
Self { lines: Vec::new(), items }
}
pub fn push_item(&mut self, item: impl Into<RenderItem>) {
self.items.push(item.into());
}
pub fn push_renderable(&mut self, r: impl Renderable + Send + Sync + 'static) {
self.items.push(RenderItem::Nested(DynRenderable::new(r)));
}
pub fn flatten(&self, options: &ConsoleOptions) -> Vec<Segment> {
let mut out: Vec<Segment> = Vec::new();
flatten_items(&self.items, options, &mut out);
if out.is_empty() {
for line in &self.lines {
for seg in line {
out.push(seg.clone());
}
}
}
out
}
pub fn to_ansi(&self) -> String {
let mut out = String::new();
if !self.items.is_empty() {
let flat = self.flatten(&ConsoleOptions::default());
for seg in &flat {
out.push_str(&seg.to_ansi());
}
} else {
for line in &self.lines {
for seg in line {
out.push_str(&seg.to_ansi());
}
}
}
out
}
}
fn flatten_items(items: &[RenderItem], options: &ConsoleOptions, out: &mut Vec<Segment>) {
for item in items {
match item {
RenderItem::Segment(seg) => out.push(seg.clone()),
RenderItem::Nested(renderable) => {
let nested = renderable.render(options);
flatten_items(&nested.items, options, out);
}
}
}
}
pub trait Renderable {
fn render(&self, options: &ConsoleOptions) -> RenderResult;
fn measure(&self, _options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
None
}
}
impl Renderable for String {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
self.as_str().render(options)
}
}
impl Renderable for &str {
fn render(&self, _options: &ConsoleOptions) -> RenderResult {
RenderResult::from_text(self)
}
}
impl Renderable for Text {
fn render(&self, _options: &ConsoleOptions) -> RenderResult {
let rendered = self.render();
let lines: Vec<Vec<Segment>> = rendered
.lines()
.map(|l| vec![Segment::new(l)])
.collect();
RenderResult { lines, items: Vec::new() }
}
}
#[derive(Clone)]
pub struct DynRenderable {
inner: Arc<dyn Renderable + Send + Sync>,
}
impl DynRenderable {
pub fn new(r: impl Renderable + Send + Sync + 'static) -> Self {
Self { inner: Arc::new(r) }
}
}
impl fmt::Debug for DynRenderable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynRenderable").finish()
}
}
impl Renderable for DynRenderable {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
self.inner.render(options)
}
fn measure(&self, options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
self.inner.measure(options)
}
}
#[derive(Debug, Clone)]
pub struct Group {
pub children: Vec<DynRenderable>,
}
impl Group {
pub fn new() -> Self {
Self { children: Vec::new() }
}
pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
self.children.push(DynRenderable::new(renderable));
}
}
impl Renderable for Group {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
let mut all_lines: Vec<Vec<Segment>> = Vec::new();
for child in &self.children {
let result = child.render(options);
all_lines.extend(result.lines);
}
RenderResult { lines: all_lines, items: Vec::new() }
}
}
struct CaptureWriter {
buf: Arc<Mutex<Vec<u8>>>,
}
impl Write for CaptureWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut data = self.buf.lock().unwrap();
data.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub struct Capture {
buf: Arc<Mutex<Vec<u8>>>,
}
impl Capture {
pub fn new(_console: &Console) -> Self {
Self { buf: Arc::new(Mutex::new(Vec::new())) }
}
pub fn get(&self) -> String {
let data = self.buf.lock().unwrap();
String::from_utf8_lossy(&data).to_string()
}
}
pub use crate::pager::{Pager, PagerContext, SystemPager};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureError {
AlreadyCapturing,
NotCapturing,
InvalidUtf8,
}
impl fmt::Display for CaptureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyCapturing => write!(f, "capture already in progress"),
Self::NotCapturing => write!(f, "no capture active"),
Self::InvalidUtf8 => write!(f, "captured output is not valid UTF-8"),
}
}
}
impl std::error::Error for CaptureError {}
pub struct NewLine;
impl Renderable for NewLine {
fn render(&self, _options: &ConsoleOptions) -> RenderResult {
RenderResult::from_text("\n")
}
}
pub struct NoChange;
impl Renderable for NoChange {
fn render(&self, _options: &ConsoleOptions) -> RenderResult {
RenderResult::new()
}
}
pub struct RenderHook {
hook: Box<dyn Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send>,
}
impl RenderHook {
pub fn new<F: Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send + 'static>(f: F) -> Self {
Self { hook: Box::new(f) }
}
pub fn apply(&self, lines: &[Vec<Segment>]) -> Vec<Vec<Segment>> {
(self.hook)(lines)
}
}
impl fmt::Debug for RenderHook {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RenderHook").finish()
}
}
pub struct ThemeContext {
console_ptr: *mut Console,
previous_theme: Theme,
}
impl ThemeContext {
pub(crate) fn new(console: &mut Console, previous_theme: Theme) -> Self {
Self {
console_ptr: console as *mut Console,
previous_theme,
}
}
}
impl Drop for ThemeContext {
fn drop(&mut self) {
unsafe {
(*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
}
}
}
pub struct Console {
pub file: Box<dyn Write + Send>,
pub color_system: ColorSystem,
pub theme: Theme,
pub options: ConsoleOptions,
width: Option<usize>,
height: Option<usize>,
is_terminal: bool,
pub quiet: bool,
pub soft_wrap: bool,
alt_screen: bool,
cursor_visible: bool,
render_hooks: Vec<RenderHook>,
capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
saved_file: Option<Box<dyn Write + Send>>,
}
impl Console {
pub fn new() -> Self {
let is_terminal = atty::is(atty::Stream::Stdout);
let color_system = detect_color_system();
let size = ConsoleDimensions::detect();
Self {
file: Box::new(io::stdout()) as Box<dyn Write + Send>,
color_system,
theme: crate::theme::default_theme(),
options: ConsoleOptions {
size,
is_terminal,
max_width: size.width,
max_height: size.height,
..Default::default()
},
width: None,
height: None,
is_terminal,
quiet: false,
soft_wrap: false,
alt_screen: false,
cursor_visible: true,
render_hooks: Vec::new(),
capture_buf: None,
saved_file: None,
}
}
pub fn with_file(file: Box<dyn Write + Send>) -> Self {
let _is_terminal = false;
Self {
file,
color_system: ColorSystem::Standard,
theme: crate::theme::default_theme(),
options: ConsoleOptions {
size: ConsoleDimensions { width: 80, height: 25 },
is_terminal: false,
max_width: 80,
max_height: 25,
..Default::default()
},
width: None,
height: None,
is_terminal: false,
quiet: false,
soft_wrap: false,
alt_screen: false,
cursor_visible: true,
render_hooks: Vec::new(),
capture_buf: None,
saved_file: None,
}
}
pub fn set_width(&mut self, width: usize) {
self.width = Some(width);
self.options.max_width = width;
}
pub fn set_height(&mut self, height: usize) {
self.height = Some(height);
self.options.max_height = height;
}
pub fn width(&self) -> usize {
self.width.unwrap_or(self.options.size.width)
}
pub fn height(&self) -> usize {
self.height.unwrap_or(self.options.size.height)
}
pub fn render_lines(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
style: Option<&Style>,
_pad: bool,
) -> Vec<Vec<Segment>> {
let result = renderable.render(options);
if let Some(st) = style {
result
.lines
.into_iter()
.map(|line| {
line.into_iter()
.map(|seg| {
let new_style = if let Some(ref s) = seg.style {
s.combine(st)
} else {
st.clone()
};
Segment::styled(seg.text, new_style)
})
.collect()
})
.collect()
} else {
result.lines
}
}
pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
self.theme
.get(name)
.cloned()
.or_else(|| {
if !default.is_empty() {
Some(Style::from_str(default))
} else {
None
}
})
}
pub fn render_str(&self, text: &str, style: &str) -> Text {
let st = self.get_style(style, "");
let mut t = Text::new(text);
if let Some(s) = st {
t = t.style(s);
}
t
}
pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
if self.quiet { return; }
let mut first = true;
for obj in objects {
if !first {
let _ = write!(self.file, "{sep}");
}
first = false;
let result = obj.render(&self.options);
let ansi = result.to_ansi();
let _ = write!(self.file, "{ansi}");
}
let _ = write!(self.file, "{end}");
let _ = self.file.flush();
}
pub fn println(&mut self, renderable: &dyn Renderable) {
if self.quiet { return; }
let result = renderable.render(&self.options);
let ansi = result.to_ansi();
let _ = writeln!(self.file, "{ansi}");
let _ = self.file.flush();
}
pub fn print_str(&mut self, text: &str) {
if self.quiet { return; }
let ansi = if self.options.markup {
let parsed = crate::markup::render(text);
parsed.render()
} else {
text.to_string()
};
let _ = write!(self.file, "{ansi}");
let _ = self.file.flush();
}
pub fn print_json(&mut self, data: &serde_json::Value) {
if self.quiet { return; }
let formatted = crate::json::render_json(data);
let result = formatted.render(&self.options);
let ansi = result.to_ansi();
let _ = writeln!(self.file, "{ansi}");
let _ = self.file.flush();
}
pub fn clear(&mut self) {
if self.quiet { return; }
let _ = write!(self.file, "\x1b[2J\x1b[H");
let _ = self.file.flush();
}
pub fn show_cursor(&mut self) {
self.cursor_visible = true;
let _ = write!(self.file, "\x1b[?25h");
let _ = self.file.flush();
}
pub fn hide_cursor(&mut self) {
self.cursor_visible = false;
let _ = write!(self.file, "\x1b[?25l");
let _ = self.file.flush();
}
pub fn set_window_title(&mut self, title: &str) {
let _ = write!(self.file, "\x1b]0;{title}\x07");
let _ = self.file.flush();
}
pub fn color_ansi(&self, color: &Color) -> String {
let downgraded = color.downgrade(self.color_system);
downgraded.to_string()
}
pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
let result = renderable.render(options);
result.flatten(options)
}
pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
if let Some(m) = renderable.measure(options) {
return m;
}
let segments = self.render(renderable, options);
let max_w = segments.iter()
.map(|s| s.cell_length())
.max()
.unwrap_or(0);
crate::measure::Measurement::new(max_w, options.max_width)
}
pub fn rule(
&mut self,
title: impl Into<String>,
characters: Option<&str>,
style: Option<Style>,
align: Option<AlignMethod>,
) {
if self.quiet { return; }
let mut rule = crate::rule::Rule::new().title(title);
if let Some(chars) = characters { rule = rule.characters(chars); }
if let Some(st) = style { rule = rule.style(st); }
if let Some(a) = align { rule = rule.align(a); }
let result = rule.render(&self.options);
let ansi = result.to_ansi();
let _ = write!(self.file, "{ansi}");
let _ = self.file.flush();
}
pub fn bell(&mut self) {
if self.quiet { return; }
let _ = write!(self.file, "\x07");
let _ = self.file.flush();
}
pub fn line(&mut self, count: usize) {
if self.quiet { return; }
for _ in 0..count {
let _ = writeln!(self.file);
}
let _ = self.file.flush();
}
pub fn log(&mut self, objects: &[&dyn Renderable]) {
if self.quiet { return; }
let now = chrono::Local::now();
let time_str = format!("[{}]", now.format("%H:%M:%S"));
let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
let _ = write!(self.file, "{time_str} ");
let _ = write!(self.file, "{}", Style::new().reset_ansi());
self.print(objects, " ", "\n");
}
pub fn push_theme(&mut self, theme: Theme) {
let mut new_theme = theme.clone();
new_theme.inherit = Some(Box::new(self.theme.clone()));
self.theme = new_theme;
}
pub fn pop_theme(&mut self) {
if let Some(ref inherit) = self.theme.inherit {
self.theme = *inherit.clone();
}
}
pub fn export_html(&self, renderable: &dyn Renderable) -> String {
let result = renderable.render(&self.options);
let ansi = result.to_ansi();
crate::export::export_html(&crate::export::ExportHtmlOptions {
code: crate::export::strip_ansi_escapes(&ansi),
..Default::default()
})
}
pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
let html = self.export_html(renderable);
crate::export::save_html(path, &crate::export::ExportHtmlOptions {
code: html,
..Default::default()
})
}
pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
let result = renderable.render(&self.options);
let ansi = result.to_ansi();
crate::export::export_svg(&crate::export::ExportSvgOptions {
code: crate::export::strip_ansi_escapes(&ansi),
..Default::default()
})
}
pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
let svg = self.export_svg(renderable);
crate::export::save_svg(path, &crate::export::ExportSvgOptions {
code: svg,
..Default::default()
})
}
pub fn export_text(&self, renderable: &dyn Renderable) -> String {
let result = renderable.render(&self.options);
let ansi = result.to_ansi();
crate::export::export_text(&crate::export::ExportTextOptions {
text: ansi,
strip_ansi: true,
})
}
pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
let text = self.export_text(renderable);
crate::export::save_text(path, &crate::export::ExportTextOptions {
text,
strip_ansi: false,
})
}
pub fn set_quiet(&mut self, quiet: bool) {
self.quiet = quiet;
}
pub fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
self.soft_wrap = soft_wrap;
}
pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
self.soft_wrap = soft_wrap;
self
}
pub fn input(&mut self, prompt: &str, password: bool) -> String {
let _ = write!(self.file, "{prompt}");
let _ = self.file.flush();
if password {
self.read_password()
} else {
let mut input = String::new();
let _ = io::stdin().read_line(&mut input);
input.trim().to_string()
}
}
fn read_password(&mut self) -> String {
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::io::Read;
match enable_raw_mode() {
Ok(()) => {
let stdin = io::stdin();
let mut handle = stdin.lock();
let mut buf = [0u8; 1];
let mut password = String::new();
loop {
match handle.read_exact(&mut buf) {
Ok(()) => match buf[0] {
b'\r' | b'\n' => {
let _ = writeln!(self.file);
let _ = self.file.flush();
break;
}
b'\x03' => {
let _ = writeln!(self.file);
let _ = self.file.flush();
break;
}
b'\x7f' | b'\x08' => {
password.pop();
}
c => {
password.push(c as char);
let _ = write!(self.file, "*");
let _ = self.file.flush();
}
},
Err(_) => break,
}
}
let _ = disable_raw_mode();
password
}
Err(_) => {
let mut input = String::new();
let _ = io::stdin().read_line(&mut input);
input.trim().to_string()
}
}
}
pub fn screen(&mut self) -> crate::screen::ScreenContext {
let mut ctx = crate::screen::ScreenContext::new();
ctx.enter();
ctx
}
pub fn set_alt_screen(&mut self, enable: bool) {
self.alt_screen = enable;
if enable {
let _ = write!(self.file, "\x1b[?1049h");
} else {
let _ = write!(self.file, "\x1b[?1049l");
}
let _ = self.file.flush();
}
pub fn is_terminal(&self) -> bool {
self.is_terminal
}
pub fn set_size(&mut self, width: usize, height: usize) {
self.width = Some(width);
self.height = Some(height);
self.options.max_width = width;
self.options.max_height = height;
self.options.size = crate::console::ConsoleDimensions { width, height };
}
pub fn on_broken_pipe(&self) {
}
}
impl Default for Console {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Console {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Console")
.field("color_system", &self.color_system)
.field("width", &self.width())
.field("height", &self.height())
.field("is_terminal", &self.is_terminal)
.field("alt_screen", &self.alt_screen)
.field("cursor_visible", &self.cursor_visible)
.field("quiet", &self.quiet)
.field("soft_wrap", &self.soft_wrap)
.finish()
}
}
impl Console {
pub fn begin_capture(&mut self) {
let buf = Arc::new(Mutex::new(Vec::new()));
let writer = Box::new(CaptureWriter { buf: buf.clone() });
self.saved_file = Some(std::mem::replace(&mut self.file, writer));
self.capture_buf = Some(buf);
}
pub fn end_capture(&mut self) -> Capture {
let buf = self.capture_buf.take().expect("not currently capturing");
if let Some(saved) = self.saved_file.take() {
self.file = saved;
}
Capture { buf }
}
pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> String {
self.begin_capture();
f(self);
let cap = self.end_capture();
cap.get()
}
pub fn pager(&mut self, styles: bool) -> PagerContext {
PagerContext::new(Pager::new().color(styles))
}
pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
if !self.quiet {
let result = prompt.render(&self.options);
let ansi = result.to_ansi();
let _ = write!(self.file, "{ansi}");
let _ = self.file.flush();
}
let mut input = String::new();
let _ = io::stdin().read_line(&mut input);
input.trim().to_string()
}
pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
if self.quiet { return; }
let msg = format!(
"[bold red]Exception[/bold red]: No current exception info. "
);
let msg_text = crate::text::Text::from_markup(&msg);
let result = msg_text.render();
let _ = writeln!(self.file, "{result}");
let _ = self.file.flush();
}
pub fn print_json_str(&mut self, json: &str) {
if self.quiet { return; }
if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
self.print_json(&value);
} else {
let _ = writeln!(self.file, "[invalid JSON]");
let _ = self.file.flush();
}
}
pub fn render_to_lines(
&self,
renderable: &dyn Renderable,
options: &ConsoleOptions,
) -> Vec<Vec<Segment>> {
let result = renderable.render(options);
let has_items = !result.items.is_empty();
let mut lines = if result.lines.is_empty() && has_items {
let flat = result.flatten(options);
if flat.is_empty() {
Vec::new() } else {
vec![flat]
}
} else {
result.lines
};
if !self.render_hooks.is_empty() {
for hook in &self.render_hooks {
lines = hook.apply(&lines);
}
}
lines
}
pub fn render_ansi(&self, text: &str) -> String {
let t = self.render_str(text, "");
t.render()
}
pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
crate::export::export_svg(options)
}
pub fn size(&self) -> ConsoleDimensions {
ConsoleDimensions {
width: self.width(),
height: self.height(),
}
}
pub fn is_dumb_terminal(&self) -> bool {
std::env::var("TERM").map_or(false, |t| t == "dumb")
}
pub fn is_alt_screen(&self) -> bool {
self.alt_screen
}
pub fn set_cursor_visible(&mut self, visible: bool) {
self.cursor_visible = visible;
if visible {
let _ = write!(self.file, "\x1b[?25h");
} else {
let _ = write!(self.file, "\x1b[?25l");
}
let _ = self.file.flush();
}
pub fn use_theme(&mut self, theme: Theme) -> ThemeContext {
let prev = std::mem::replace(&mut self.theme, theme);
ThemeContext::new(self, prev)
}
pub fn clear_live(&mut self) {
if self.alt_screen {
let _ = write!(self.file, "\x1b[2J\x1b[H");
} else {
let _ = write!(self.file, "\x1b[2J\x1b[H");
}
let _ = self.file.flush();
}
pub fn set_live(&mut self, _live: &crate::live::Live) {
}
pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
let opts = options.unwrap_or(&self.options);
let segments = self.render(renderable, opts);
let mut output = String::new();
for seg in &segments {
output.push_str(&seg.to_ansi());
}
let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
let _ = self.file.flush();
}
pub fn update_screen_lines(&mut self, lines: &[Vec<Segment>], options: Option<&ConsoleOptions>) {
let _ = options;
let mut output = String::new();
for line in lines {
for seg in line {
output.push_str(&seg.to_ansi());
}
output.push('\n');
}
let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
let _ = self.file.flush();
}
pub fn push_render_hook(&mut self, hook: RenderHook) {
self.render_hooks.push(hook);
}
pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
self.render_hooks.pop()
}
}
fn detect_color_system() -> ColorSystem {
if let Ok(val) = std::env::var("COLORTERM") {
if val == "truecolor" || val == "24bit" {
return ColorSystem::TrueColor;
}
}
if let Ok(term) = std::env::var("TERM") {
if term.contains("256color") {
return ColorSystem::EightBit;
}
if term == "xterm-kitty" {
return ColorSystem::TrueColor;
}
}
if std::env::var("NO_COLOR").is_ok() {
return ColorSystem::Standard;
}
if atty::is(atty::Stream::Stdout) {
ColorSystem::TrueColor
} else {
ColorSystem::Standard
}
}
use once_cell::sync::Lazy;
static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
Mutex::new(Console::new())
});
pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
GLOBAL_CONSOLE.lock().unwrap()
}
pub fn print_objects(objects: &[&dyn Renderable]) {
let mut console = GLOBAL_CONSOLE.lock().unwrap();
console.print(objects, " ", "\n");
}
pub fn print_str(text: &str) {
let mut console = GLOBAL_CONSOLE.lock().unwrap();
console.print_str(text);
}
pub fn print_json_val(data: &serde_json::Value) {
let mut console = GLOBAL_CONSOLE.lock().unwrap();
console.print_json(data);
}
pub fn reconfigure(
width: Option<usize>,
height: Option<usize>,
color_system: Option<ColorSystem>,
) {
let mut console = GLOBAL_CONSOLE.lock().unwrap();
if let Some(w) = width {
console.set_width(w);
}
if let Some(h) = height {
console.set_height(h);
}
if let Some(cs) = color_system {
console.color_system = cs;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_result_from_text() {
let r = RenderResult::from_text("hello");
assert_eq!(r.lines.len(), 1);
assert_eq!(r.lines[0][0].text, "hello");
}
#[test]
fn test_console_options_default() {
let opts = ConsoleOptions::default();
assert!(opts.markup);
}
#[test]
fn test_console_quiet_default() {
let console = Console::new();
assert!(!console.quiet);
}
#[test]
fn test_console_quiet_setter() {
let mut console = Console::new();
console.set_quiet(true);
assert!(console.quiet);
}
#[test]
fn test_console_quiet_builder() {
let console = Console::new().quiet(true);
assert!(console.quiet);
}
#[test]
fn test_console_quiet_suppresses_print() {
let mut console = Console::new();
console.quiet = true;
console.print(&[], " ", "\n");
console.println(&"test");
console.print_str("test");
}
#[test]
fn test_console_soft_wrap_default() {
let console = Console::new();
assert!(!console.soft_wrap);
}
#[test]
fn test_console_soft_wrap_setter() {
let mut console = Console::new();
console.set_soft_wrap(true);
assert!(console.soft_wrap);
}
#[test]
fn test_console_soft_wrap_builder() {
let console = Console::new().soft_wrap(true);
assert!(console.soft_wrap);
}
#[test]
fn test_console_is_terminal() {
let console = Console::new();
let detected = console.is_terminal();
assert_eq!(detected, atty::is(atty::Stream::Stdout));
}
#[test]
fn test_console_set_size() {
let mut console = Console::new();
console.set_size(120, 30);
assert_eq!(console.width(), 120);
assert_eq!(console.height(), 30);
assert_eq!(console.options.max_width, 120);
assert_eq!(console.options.max_height, 30);
}
#[test]
fn test_console_set_alt_screen() {
let mut console = Console::new();
console.set_alt_screen(true);
console.set_alt_screen(false);
}
#[test]
fn test_console_on_broken_pipe() {
let console = Console::new();
console.on_broken_pipe(); }
#[test]
fn test_console_input_normal() {
let _console = Console::new();
}
#[test]
fn test_console_debug() {
let console = Console::new();
let debug = format!("{:?}", console);
assert!(debug.contains("Console"));
}
#[test]
fn test_console_with_file_has_no_terminal() {
let console = Console::with_file(Box::new(std::io::sink()));
assert!(!console.is_terminal());
}
#[test]
fn test_newline_renderable() {
let nl = NewLine;
let result = nl.render(&ConsoleOptions::default());
let ansi = result.to_ansi();
assert_eq!(ansi, "\n");
}
#[test]
fn test_nochange_renderable() {
let nc = NoChange;
let result = nc.render(&ConsoleOptions::default());
assert!(result.lines.is_empty());
assert!(result.items.is_empty());
}
#[test]
fn test_capture_begin_end() {
let mut console = Console::with_file(Box::new(std::io::sink()));
console.begin_capture();
let _ = write!(console.file, "captured text");
let cap = console.end_capture();
assert_eq!(cap.get(), "captured text");
}
#[test]
fn test_capture_with_closure() {
let mut console = Console::with_file(Box::new(std::io::sink()));
let output = console.capture(|c| {
let _ = write!(c.file, "hello from capture");
});
assert_eq!(output, "hello from capture");
}
#[test]
fn test_capture_new_empty() {
let console = Console::new();
let cap = Capture::new(&console);
assert_eq!(cap.get(), "");
}
#[test]
fn test_system_pager_default() {
let pager = SystemPager::new();
let _ = pager.show("");
}
#[test]
fn test_pager_enabled() {
let pager = Pager::new();
assert!(pager.is_enabled());
let disabled = pager.enabled(false);
assert!(!disabled.is_enabled());
}
#[test]
fn test_render_hook() {
let hook = RenderHook::new(|lines| {
let hooked: Vec<Vec<Segment>> = lines.iter().map(|line| {
let mut new_line = line.clone();
new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
new_line
}).collect();
hooked
});
let lines = vec![vec![Segment::new("test")]];
let result = hook.apply(&lines);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 2);
assert_eq!(result[0][1].text, "HOOKED");
}
#[test]
fn test_console_size() {
let mut console = Console::new();
console.set_size(100, 40);
let dims = console.size();
assert_eq!(dims.width, 100);
assert_eq!(dims.height, 40);
}
#[test]
fn test_console_is_dumb_terminal() {
let console = Console::new();
let _ = console.is_dumb_terminal();
}
#[test]
fn test_console_is_alt_screen() {
let mut console = Console::new();
assert!(!console.is_alt_screen());
console.alt_screen = true;
assert!(console.is_alt_screen());
}
#[test]
fn test_console_render_ansi() {
let console = Console::new();
let ansi = console.render_ansi("test");
assert!(ansi.contains("test") || ansi.contains("\x1b["));
}
#[test]
fn test_console_render_to_lines() {
let console = Console::new();
let opts = ConsoleOptions::default();
let lines = console.render_to_lines(&"hello", &opts);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0][0].text, "hello");
}
#[test]
fn test_console_input_renderable() {
let _console = Console::new();
}
#[test]
fn test_console_print_exception_noop() {
let mut console = Console::new();
console.print_exception(None, 3);
}
#[test]
fn test_console_render_hooks_push_pop() {
let mut console = Console::new();
let hook = RenderHook::new(|lines| lines.to_vec());
console.push_render_hook(hook);
assert_eq!(console.render_hooks.len(), 1);
let popped = console.pop_render_hook();
assert!(popped.is_some());
assert!(console.render_hooks.is_empty());
}
#[test]
fn test_console_reconfigure() {
reconfigure(Some(120), Some(40), None);
reconfigure(None, None, Some(ColorSystem::Standard));
reconfigure(None, None, None);
}
#[test]
fn test_pager_context_write() {
let pager = Pager::new().enabled(false);
let mut ctx = PagerContext::new(pager);
ctx.feed("test content");
}
#[test]
fn test_theme_context() {
let mut console = Console::new();
let custom_theme = Theme::new();
let original = console.theme.clone();
{
let _ctx = console.use_theme(custom_theme);
}
assert_eq!(console.theme.styles.len(), original.styles.len());
}
}