use crate::color::ColorSystem;
use crate::color_env::{detect_color_env, ColorEnvOverride};
use crate::console_caps::ConsoleCapabilities;
use crate::control::Control;
use crate::error::ConsoleError;
use crate::pager::Pager;
use crate::segment::Segment;
use crate::style::Style;
use crate::style_interner::StyleInterner;
use crate::text::{JustifyMethod, OverflowMethod, Text};
use crate::theme::{Theme, ThemeStack};
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
pub fn detect_color_system_from(colorterm: Option<&str>, term: Option<&str>) -> ColorSystem {
if let Some(ct) = colorterm {
let ct_lower = ct.to_lowercase();
if ct_lower.contains("truecolor") || ct_lower.contains("24bit") {
return ColorSystem::TrueColor;
}
}
if let Some(t) = term {
if t.ends_with("256color") {
return ColorSystem::EightBit;
}
if !t.is_empty() && t != "dumb" {
return ColorSystem::Standard;
}
}
ColorSystem::TrueColor
}
fn detect_is_terminal() -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
use std::io::IsTerminal as _;
std::io::stdout().is_terminal()
}
#[cfg(target_arch = "wasm32")]
{
matches!(
std::env::var("TERM").as_deref(),
Ok(t) if !t.is_empty() && t != "dumb"
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConsoleDimensions {
pub width: usize,
pub height: usize,
}
#[derive(Debug, Clone)]
pub struct ConsoleOptions {
pub size: ConsoleDimensions,
pub legacy_windows: bool,
pub min_width: usize,
pub max_width: usize,
pub is_terminal: bool,
pub encoding: Cow<'static, str>,
pub max_height: usize,
pub justify: Option<JustifyMethod>,
pub overflow: Option<OverflowMethod>,
pub no_wrap: Option<bool>,
pub highlight: Option<bool>,
pub markup: Option<bool>,
pub height: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct ConsoleOptionsUpdates {
pub width: Option<usize>,
pub min_width: Option<usize>,
pub max_width: Option<usize>,
pub justify: Option<Option<JustifyMethod>>,
pub overflow: Option<Option<OverflowMethod>>,
pub no_wrap: Option<bool>,
pub highlight: Option<Option<bool>>,
pub markup: Option<Option<bool>>,
pub height: Option<Option<usize>>,
pub max_height: Option<usize>,
}
impl ConsoleOptions {
pub fn ascii_only(&self) -> bool {
!self.encoding.to_lowercase().starts_with("utf")
}
pub fn copy(&self) -> Self {
self.clone()
}
pub fn update_width(&self, width: usize) -> Self {
let mut opts = self.clone();
opts.size.width = width;
opts.max_width = width;
opts.min_width = opts.min_width.min(width);
opts
}
pub fn update_height(&self, height: usize) -> Self {
let mut opts = self.clone();
opts.height = Some(height);
opts
}
pub fn update_dimensions(&self, width: usize, height: usize) -> Self {
let mut opts = self.clone();
opts.size = ConsoleDimensions { width, height };
opts.max_width = width;
opts.height = Some(height);
opts
}
pub fn reset_height(&self) -> Self {
let mut opts = self.clone();
opts.height = None;
opts
}
pub fn with_updates(&self, updates: &ConsoleOptionsUpdates) -> Self {
let mut opts = self.clone();
if let Some(w) = updates.width {
opts.size.width = w;
opts.max_width = w;
}
if let Some(min_w) = updates.min_width {
opts.min_width = min_w;
}
if let Some(max_w) = updates.max_width {
opts.max_width = max_w;
}
if let Some(ref j) = updates.justify {
opts.justify = *j;
}
if let Some(ref o) = updates.overflow {
opts.overflow = *o;
}
if let Some(nw) = updates.no_wrap {
opts.no_wrap = Some(nw);
}
if let Some(ref h) = updates.highlight {
opts.highlight = *h;
}
if let Some(ref m) = updates.markup {
opts.markup = *m;
}
if let Some(ref h) = updates.height {
opts.height = *h;
}
if let Some(mh) = updates.max_height {
opts.max_height = mh;
}
opts
}
}
pub trait Renderable {
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment>;
fn content_hash(&self) -> Option<u64> {
None
}
}
impl Renderable for Text {
fn content_hash(&self) -> Option<u64> {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
const TEXT_MARKER: u64 = 0x0000_0001;
let plain = self.plain();
let mut h = FNV_OFFSET;
for byte in plain.as_bytes() {
h ^= *byte as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h ^= TEXT_MARKER;
h = h.wrapping_mul(FNV_PRIME);
Some(h)
}
fn gilt_console(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut text = self.clone();
if let Some(justify) = &options.justify {
text.justify = Some(*justify);
}
if let Some(overflow) = &options.overflow {
text.overflow = Some(*overflow);
}
if options.no_wrap == Some(true) || options.overflow == Some(OverflowMethod::Ignore) {
text.render()
} else {
let tab_size = text.tab_size.unwrap_or(8);
let lines = text.wrap(
options.max_width,
text.justify,
text.overflow,
tab_size,
text.no_wrap.unwrap_or(false),
);
let mut segments = Vec::new();
for line in lines.iter() {
segments.extend(line.render());
}
segments
}
}
}
impl Renderable for str {
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let text = console.render_str(self, None, options.justify, options.overflow);
text.gilt_console(console, options)
}
}
impl Renderable for String {
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.as_str().gilt_console(console, options)
}
}
#[path = "console_builder.rs"]
mod console_builder;
pub use console_builder::ConsoleBuilder;
#[path = "console_capture.rs"]
mod console_capture;
pub use console_capture::{CaptureGuard, ScreenGuard};
#[path = "console_render.rs"]
mod console_render;
pub struct Console {
color_system: Option<ColorSystem>,
width_override: Option<usize>,
height_override: Option<usize>,
force_terminal: Option<bool>,
#[allow(dead_code)] tab_size: usize,
record: bool,
markup_enabled: bool,
highlight_enabled: bool,
#[allow(dead_code)] soft_wrap: bool,
no_color: bool,
quiet: bool,
#[allow(dead_code)] safe_box: bool,
legacy_windows: bool,
base_style: Option<Style>,
log_path: bool,
theme_stack: ThemeStack,
buffer: Vec<Segment>,
buffer_index: usize,
record_buffer: Vec<Segment>,
is_alt_screen: bool,
capture_buffer: Option<Vec<Segment>>,
live_stack: Vec<usize>,
style_interner: Arc<Mutex<StyleInterner>>,
capabilities: ConsoleCapabilities,
pub(crate) writer_override: Option<Box<dyn std::io::Write + Send + Sync>>,
pub(crate) sync_depth: usize,
#[cfg(feature = "asciinema")]
pub(crate) asciinema_clock: Option<crate::console::console_asciinema::AsciinemaClock>,
#[cfg(feature = "asciinema")]
pub(crate) asciinema_active: bool,
#[cfg(feature = "asciinema")]
pub(crate) asciinema_start: f64,
#[cfg(feature = "asciinema")]
pub(crate) asciinema_events: Vec<(f64, String)>,
}
impl Console {
pub fn new() -> Self {
ConsoleBuilder::default().build()
}
pub fn builder() -> ConsoleBuilder {
ConsoleBuilder::default()
}
pub fn stderr() -> Self {
#[cfg(not(target_arch = "wasm32"))]
let is_tty = {
use std::io::IsTerminal as _;
std::io::stderr().is_terminal()
};
#[cfg(target_arch = "wasm32")]
let is_tty = matches!(
std::env::var("TERM").as_deref(),
Ok(t) if !t.is_empty() && t != "dumb"
);
ConsoleBuilder::default()
.force_terminal(is_tty)
.build()
.with_writer(std::io::stderr())
}
pub(crate) fn from_builder(builder: ConsoleBuilder) -> Self {
let has_explicit_cs = matches!(
builder.color_system.as_deref(),
Some("standard" | "256" | "truecolor" | "windows")
);
let force_terminal_on = builder.force_terminal == Some(true);
let color_system = if let Some(cs) = builder.color_system_override {
Some(cs)
} else if has_explicit_cs {
match builder.color_system.as_deref() {
Some("standard") => Some(ColorSystem::Standard),
Some("256") => Some(ColorSystem::EightBit),
Some("truecolor") => Some(ColorSystem::TrueColor),
Some("windows") => Some(ColorSystem::Windows),
_ => unreachable!(),
}
} else if builder.no_color_explicit && builder.no_color {
None
} else {
match detect_color_env() {
ColorEnvOverride::NoColor => None,
ColorEnvOverride::ForceColor => Some(ColorSystem::EightBit),
ColorEnvOverride::ForceColorTruecolor => Some(ColorSystem::TrueColor),
ColorEnvOverride::None => {
if builder.no_color {
None
} else if !force_terminal_on && !detect_is_terminal() {
None
} else {
let colorterm = std::env::var("COLORTERM").ok();
let term = std::env::var("TERM").ok();
Some(detect_color_system_from(
colorterm.as_deref(),
term.as_deref(),
))
}
}
}
};
let effective_no_color = builder.no_color || color_system.is_none();
let theme = builder.theme.unwrap_or_else(|| Theme::new(None, true));
#[allow(unused_mut)]
let mut theme_stack = ThemeStack::new(theme);
#[cfg(all(feature = "json", not(target_arch = "wasm32")))]
{
use crate::console::console_builder::load_theme_from_path;
let path_to_load: Option<std::path::PathBuf> = if let Some(p) = builder.theme_path {
Some(p)
} else {
std::env::var("GILT_THEME")
.ok()
.map(std::path::PathBuf::from)
};
if let Some(path) = path_to_load {
if let Some(loaded) = load_theme_from_path(&path) {
theme_stack.push_theme(loaded, true);
}
}
}
let builder_is_terminal = if let Some(forced) = builder.force_terminal {
forced
} else {
detect_is_terminal()
};
let capabilities = ConsoleCapabilities::from_env(builder_is_terminal);
crate::windows_vt::enable_windows_vt();
Console {
color_system,
width_override: builder.width,
height_override: builder.height,
force_terminal: builder.force_terminal,
tab_size: builder.tab_size,
record: builder.record,
markup_enabled: builder.markup,
highlight_enabled: builder.highlight,
soft_wrap: builder.soft_wrap,
no_color: effective_no_color,
quiet: builder.quiet,
safe_box: builder.safe_box,
legacy_windows: false,
base_style: None,
log_path: builder.log_path,
theme_stack,
buffer: Vec::new(),
buffer_index: 0,
record_buffer: Vec::new(),
is_alt_screen: false,
capture_buffer: None,
live_stack: Vec::new(),
style_interner: Arc::new(Mutex::new(StyleInterner::new())),
writer_override: None,
sync_depth: 0,
capabilities,
#[cfg(feature = "asciinema")]
asciinema_clock: None,
#[cfg(feature = "asciinema")]
asciinema_active: false,
#[cfg(feature = "asciinema")]
asciinema_start: 0.0,
#[cfg(feature = "asciinema")]
asciinema_events: Vec::new(),
}
}
pub fn with_writer<W: std::io::Write + Send + Sync + 'static>(mut self, writer: W) -> Self {
self.writer_override = Some(Box::new(std::io::BufWriter::new(writer)));
self
}
pub fn capabilities(&self) -> &ConsoleCapabilities {
&self.capabilities
}
pub fn set_capabilities(&mut self, caps: ConsoleCapabilities) {
self.capabilities = caps;
}
pub fn is_recording(&self) -> bool {
self.record
}
pub fn width(&self) -> usize {
if let Some(w) = self.width_override {
return w;
}
let (w, _) = Self::detect_terminal_size();
w
}
pub fn height(&self) -> usize {
if let Some(h) = self.height_override {
return h;
}
let (_, h) = Self::detect_terminal_size();
h
}
pub fn size(&self) -> ConsoleDimensions {
ConsoleDimensions {
width: self.width(),
height: self.height(),
}
}
pub fn options(&self) -> ConsoleOptions {
let size = self.size();
ConsoleOptions {
size,
legacy_windows: self.legacy_windows,
min_width: 1,
max_width: size.width,
is_terminal: self.is_terminal(),
encoding: Cow::Borrowed("utf-8"),
max_height: size.height,
justify: None,
overflow: None,
no_wrap: None,
highlight: Some(self.highlight_enabled),
markup: Some(self.markup_enabled),
height: None,
}
}
pub fn color_system_name(&self) -> Option<&str> {
self.color_system.as_ref().map(|cs| match cs {
ColorSystem::Standard => "standard",
ColorSystem::EightBit => "256",
ColorSystem::TrueColor => "truecolor",
ColorSystem::Windows => "windows",
})
}
pub fn color_system(&self) -> Option<ColorSystem> {
self.color_system
}
pub fn encoding(&self) -> &str {
"utf-8"
}
pub fn is_terminal(&self) -> bool {
if let Some(forced) = self.force_terminal {
return forced;
}
match crate::color::color_env::detect_tty_compatible() {
crate::color::color_env::TtyOverride::ForceTty => return true,
crate::color::color_env::TtyOverride::ForceNotTty => return false,
crate::color::color_env::TtyOverride::None => {}
}
detect_is_terminal()
}
pub fn is_interactive(&self) -> bool {
match crate::color::color_env::detect_tty_interactive() {
crate::color::color_env::TtyOverride::ForceTty => true,
crate::color::color_env::TtyOverride::ForceNotTty => false,
crate::color::color_env::TtyOverride::None => self.is_terminal(),
}
}
pub fn is_dumb_terminal(&self) -> bool {
match std::env::var("TERM") {
Ok(term) => term == "dumb",
Err(_) => false,
}
}
pub fn detect_terminal_size() -> (usize, usize) {
let env_width = std::env::var("COLUMNS")
.ok()
.and_then(|v| v.parse::<usize>().ok());
let env_height = std::env::var("LINES")
.ok()
.and_then(|v| v.parse::<usize>().ok());
let (query_width, query_height) = query_terminal_size();
let width = env_width.or(query_width).unwrap_or(80);
let height = env_height.or(query_height).unwrap_or(25);
(width, height)
}
pub fn get_style(&self, name: &str) -> Result<Style, ConsoleError> {
if let Some(style) = self.theme_stack.get(name) {
return Ok(style.clone());
}
Style::parse_strict(name).map_err(|e| {
ConsoleError::RenderError(format!("Failed to get style '{}': {}", name, e))
})
}
pub fn style_interner(&self) -> &Arc<Mutex<StyleInterner>> {
&self.style_interner
}
pub fn push_theme(&mut self, theme: Theme) {
self.theme_stack.push_theme(theme, true);
}
pub fn pop_theme(&mut self) {
let _ = self.theme_stack.pop_theme();
}
pub fn control(&mut self, ctrl: &Control) {
if !self.quiet && !self.is_dumb_terminal() {
self.write_segments(std::slice::from_ref(&ctrl.segment));
}
}
pub fn bell(&mut self) {
self.control(&Control::bell());
}
pub fn clear(&mut self) {
self.control(&Control::clear());
}
pub fn show_cursor(&mut self, show: bool) {
self.control(&Control::show_cursor(show));
}
pub fn set_alt_screen(&mut self, enable: bool) -> bool {
if enable == self.is_alt_screen {
return false;
}
self.is_alt_screen = enable;
self.control(&Control::alt_screen(enable));
true
}
pub fn set_window_title(&mut self, title: &str) -> bool {
if !self.is_terminal() {
return false;
}
self.control(&Control::title(title));
true
}
pub fn begin_synchronized(&mut self) {
self.sync_depth += 1;
self.control(&Control::begin_sync());
}
pub fn end_synchronized(&mut self) {
self.control(&Control::end_sync());
if self.sync_depth > 0 {
self.sync_depth -= 1;
}
if self.sync_depth == 0 && !self.quiet {
use std::io::Write as _;
if let Some(w) = self.writer_override.as_mut() {
let _ = w.flush();
}
}
}
pub fn synchronized<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Console) -> R,
{
self.begin_synchronized();
struct SyncGuard {
segment: crate::segment::Segment,
done: bool,
}
impl Drop for SyncGuard {
fn drop(&mut self) {
if !self.done {
use std::io::Write as _;
let _ = std::io::stderr().write_all(self.segment.text.as_bytes());
}
}
}
let end_seg = crate::control::Control::end_sync().segment.clone();
let mut guard = SyncGuard {
segment: end_seg,
done: false,
};
let result = f(self);
self.end_synchronized();
guard.done = true;
result
}
pub fn notify(&mut self, title: &str, body: &str) {
self.control(&Control::notify(title, body));
}
pub fn set_taskbar_progress(&mut self, state: crate::segment::TaskbarState, percent: u8) {
self.control(&Control::taskbar_progress(state, percent));
}
pub fn copy_to_clipboard(&mut self, text: &str) {
self.control(&Control::set_clipboard(text));
}
pub fn request_clipboard(&mut self) {
self.control(&Control::request_clipboard());
}
pub fn pager(&mut self, pager_command: Option<&str>) {
let text = self.export_text(true, false);
let pager = match pager_command {
Some(cmd) => Pager::new().with_command(cmd),
None => Pager::new(),
};
let _ = pager.show(&text);
}
pub fn enter_screen(&mut self, hide_cursor: bool) {
self.set_alt_screen(true);
if hide_cursor {
self.show_cursor(false);
}
}
pub fn exit_screen(&mut self, hide_cursor: bool) {
if hide_cursor {
self.show_cursor(true);
}
self.set_alt_screen(false);
}
pub fn update_screen(&mut self, x: usize, y: usize, renderable: &dyn Renderable) {
if !self.is_alt_screen {
return;
}
let ctrl = crate::utils::control::Control::move_to(x as i32, y as i32);
self.write_segments(&[ctrl.segment]);
self.print(renderable);
}
pub fn update_screen_lines(&mut self, x: usize, y: usize, lines: &[Vec<Segment>]) {
if !self.is_alt_screen {
return;
}
for (i, line) in lines.iter().enumerate() {
let ctrl = crate::utils::control::Control::move_to(x as i32, (y + i) as i32);
self.write_segments(&[ctrl.segment]);
self.write_segments(line);
}
}
pub fn push_live(&mut self, live_id: usize) -> bool {
self.live_stack.push(live_id);
true
}
pub fn pop_live(&mut self) -> Option<usize> {
self.live_stack.pop()
}
pub fn current_live(&self) -> Option<usize> {
self.live_stack.last().copied()
}
pub fn live_depth(&self) -> usize {
self.live_stack.len()
}
pub fn set_live(&mut self, live_id: Option<usize>) {
match live_id {
Some(id) => {
if let Some(top) = self.live_stack.last_mut() {
*top = id;
} else {
self.live_stack.push(id);
}
}
None => self.live_stack.clear(),
}
}
pub fn clear_live(&mut self) {
self.live_stack.clear();
}
}
impl Default for Console {
fn default() -> Self {
Self::new()
}
}
#[path = "console_export.rs"]
mod console_export;
#[allow(unused_imports)]
use console_export::*;
#[path = "console_recording.rs"]
mod console_recording;
pub use console_recording::Recording;
#[cfg(feature = "asciinema")]
#[path = "console_asciinema.rs"]
pub mod console_asciinema;
#[cfg(all(feature = "terminal-size", not(target_arch = "wasm32")))]
fn query_terminal_size() -> (Option<usize>, Option<usize>) {
match terminal_size::terminal_size() {
Some((terminal_size::Width(w), terminal_size::Height(h))) => {
(Some(w as usize), Some(h as usize))
}
None => (None, None),
}
}
#[cfg(not(all(feature = "terminal-size", not(target_arch = "wasm32"))))]
fn query_terminal_size() -> (Option<usize>, Option<usize>) {
(None, None)
}
#[cfg(test)]
#[path = "console_tests.rs"]
mod tests;