use std::fmt;
use std::io::{self, Write};
use std::sync::Arc;
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)
}
}
#[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() }
}
}
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,
}
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,
}
}
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,
}
}
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) {
let _ = write!(self.file, "\x1b[?25h");
let _ = self.file.flush();
}
pub fn hide_cursor(&mut self) {
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 begin_capture(&mut self) {
}
pub fn end_capture(&mut self) -> String {
String::new() }
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) {
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)
.finish()
}
}
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 std::sync::Mutex;
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);
}
#[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());
}
}