pub mod live_render;
pub mod screen;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::time::Duration;
use arc_swap::ArcSwap;
use crate::console::{Console, Renderable};
use crate::control::Control;
use crate::segment::{ControlCode, ControlType, Segment};
use crate::text::Text;
use self::live_render::{LiveRender, VerticalOverflowMethod};
use self::screen::Screen;
struct LiveContent(Arc<dyn Renderable + Send + Sync>);
struct SharedState {
console: Console,
live_render: LiveRender,
get_renderable: Option<Box<dyn Fn() -> Arc<dyn Renderable + Send + Sync> + Send>>,
screen: bool,
last_segments: Option<Vec<Segment>>,
prev_lines: Vec<Vec<Segment>>,
}
fn emit_control_segments(console: &mut Console, segments: &[Segment]) {
for seg in segments {
if let Some(ref codes) = seg.control {
console.control(&Control::new(codes.clone()));
}
}
}
pub struct Live {
state: Arc<Mutex<SharedState>>,
renderable: Arc<ArcSwap<LiveContent>>,
auto_refresh: bool,
pub refresh_per_second: f64,
pub transient: bool,
vertical_overflow: VerticalOverflowMethod,
started: bool,
refresh_thread: Option<thread::JoinHandle<()>>,
stop_flag: Arc<(Mutex<bool>, Condvar)>,
}
impl Live {
pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
let arc: Arc<dyn Renderable + Send + Sync> = Arc::new(renderable);
let live_render = LiveRender::new_arc(arc.clone());
let console = Console::new();
let state = Arc::new(Mutex::new(SharedState {
console,
live_render,
get_renderable: None,
screen: false,
last_segments: None,
prev_lines: Vec::new(),
}));
Live {
state,
renderable: Arc::new(ArcSwap::from_pointee(LiveContent(arc))),
auto_refresh: true,
refresh_per_second: 4.0,
transient: false,
vertical_overflow: VerticalOverflowMethod::Ellipsis,
started: false,
refresh_thread: None,
stop_flag: Arc::new((Mutex::new(false), Condvar::new())),
}
}
#[must_use]
pub fn with_console(self, console: Console) -> Self {
{
let mut s = self.state.lock().unwrap();
s.console = console;
}
self
}
#[must_use]
pub fn with_auto_refresh(mut self, auto_refresh: bool) -> Self {
self.auto_refresh = auto_refresh;
self
}
#[must_use]
pub fn with_refresh_per_second(mut self, rate: f64) -> Self {
assert!(rate > 0.0, "refresh_per_second must be > 0");
self.refresh_per_second = rate;
self
}
#[must_use]
pub fn with_transient(mut self, transient: bool) -> Self {
self.transient = transient;
self
}
#[must_use]
pub fn with_screen(self, screen: bool) -> Self {
{
let mut s = self.state.lock().unwrap();
s.screen = screen;
}
self
}
#[must_use]
pub fn with_vertical_overflow(mut self, overflow: VerticalOverflowMethod) -> Self {
self.vertical_overflow = overflow;
{
let mut s = self.state.lock().unwrap();
s.live_render.vertical_overflow = overflow;
}
self
}
#[must_use]
pub fn with_get_renderable<F>(self, f: F) -> Self
where
F: Fn() -> Arc<dyn Renderable + Send + Sync> + Send + 'static,
{
{
let mut s = self.state.lock().unwrap();
s.get_renderable = Some(Box::new(f));
}
self
}
pub fn console(&self) -> ConsoleRef<'_> {
ConsoleRef {
guard: self.state.lock().unwrap(),
}
}
pub fn console_mut(&self) -> ConsoleRefMut<'_> {
ConsoleRefMut {
guard: self.state.lock().unwrap(),
}
}
pub fn is_started(&self) -> bool {
self.started
}
pub fn live_render(&self) -> LiveRenderRef<'_> {
LiveRenderRef {
guard: self.state.lock().unwrap(),
}
}
pub fn start(&mut self) {
if self.started {
return;
}
self.started = true;
{
let mut stopped = self.stop_flag.0.lock().unwrap();
*stopped = false;
}
{
let mut s = self.state.lock().unwrap();
s.console.show_cursor(false);
if s.screen {
s.console.set_alt_screen(true);
}
}
if self.auto_refresh {
let flag = Arc::clone(&self.stop_flag);
let state = Arc::clone(&self.state);
let renderable = Arc::clone(&self.renderable);
let vertical_overflow = self.vertical_overflow;
let interval = Duration::from_secs_f64(1.0 / self.refresh_per_second);
let handle = thread::spawn(move || loop {
let (lock, cvar) = &*flag;
let stopped = lock.lock().unwrap();
let result = cvar.wait_timeout(stopped, interval).unwrap();
if *result.0 {
break;
}
drop(result);
Self::do_refresh(&state, &renderable, vertical_overflow);
});
self.refresh_thread = Some(handle);
}
}
pub fn stop(&mut self) {
if !self.started {
return;
}
self.started = false;
{
let mut stopped = self.stop_flag.0.lock().unwrap();
*stopped = true;
self.stop_flag.1.notify_all();
}
if let Some(handle) = self.refresh_thread.take() {
let _ = handle.join();
}
let mut s = self.state.lock().unwrap();
if self.transient {
let segments = s.live_render.restore_cursor();
emit_control_segments(&mut s.console, &segments);
} else {
if s.live_render.last_render_height() > 0 {
s.console.write_segments(&[Segment::line()]);
}
}
s.console.show_cursor(true);
if s.screen {
s.console.set_alt_screen(false);
}
}
pub fn refresh(&self) {
Self::do_refresh(&self.state, &self.renderable, self.vertical_overflow);
}
fn do_refresh(
state: &Arc<Mutex<SharedState>>,
renderable: &Arc<ArcSwap<LiveContent>>,
vertical_overflow: VerticalOverflowMethod,
) {
let (content, is_screen) = {
let s = state.lock().unwrap();
let content: Arc<dyn Renderable + Send + Sync> = match &s.get_renderable {
Some(f) => f(),
None => renderable.load().0.clone(),
};
(content, s.screen)
};
if is_screen {
let mut s = state.lock().unwrap();
s.live_render.set_renderable(content.clone());
s.live_render.vertical_overflow = vertical_overflow;
s.console.begin_synchronized();
let home_ctrl = crate::control::Control::home();
s.console.control(&home_ctrl);
let opts = s.console.options();
let lines = s
.console
.render_lines(content.as_ref(), Some(&opts), None, false, false);
let mut flat = String::new();
let line_count = lines.len();
for (i, line) in lines.iter().enumerate() {
for seg in line {
if !seg.is_control() {
flat.push_str(&seg.text);
}
}
if i + 1 < line_count {
flat.push('\n');
}
}
let text = Text::new(&flat, crate::style::Style::null());
let screen = Screen::new(text);
s.console.print(&screen);
s.console.end_synchronized();
} else {
let mut s = state.lock().unwrap();
s.live_render.set_renderable(content);
s.live_render.vertical_overflow = vertical_overflow;
let opts = s.console.options();
let new_lines = s.live_render.gilt_console_lines(&s.console, &opts);
let render_segments: Vec<Segment> = {
let line_count = new_lines.len();
let mut segs = Vec::new();
for (i, line) in new_lines.iter().enumerate() {
segs.extend(line.iter().cloned());
if i + 1 < line_count {
segs.push(Segment::line());
}
}
segs
};
if s.last_segments.as_deref() == Some(render_segments.as_slice()) {
return;
}
s.console.begin_synchronized();
let prev_lines = std::mem::take(&mut s.prev_lines);
let prev_height = prev_lines.len();
let new_height = new_lines.len();
if prev_height == 0 {
let position_segments = s.live_render.position_cursor();
emit_control_segments(&mut s.console, &position_segments);
s.console.write_segments(&render_segments);
} else {
let position_segments = s.live_render.position_cursor();
let mut codes: Vec<ControlCode> = Vec::new();
codes.push(ControlCode::Simple(ControlType::CarriageReturn));
for _ in 0..prev_height.saturating_sub(1) {
codes.push(ControlCode::WithParam(ControlType::CursorUp, 1));
}
emit_control_segments(&mut s.console, &[Segment::new("", None, Some(codes))]);
for (i, new_line) in new_lines.iter().enumerate().take(new_height) {
let prev_line = prev_lines.get(i);
let line_changed = prev_line != Some(new_line);
if line_changed {
let erase = Segment::new(
"",
None,
Some(vec![
ControlCode::Simple(ControlType::CarriageReturn),
ControlCode::WithParam(ControlType::EraseInLine, 2),
]),
);
emit_control_segments(&mut s.console, &[erase]);
s.console.write_segments(new_line);
}
if i + 1 < new_height {
let down = Segment::new(
"",
None,
Some(vec![ControlCode::WithParam(ControlType::CursorDown, 1)]),
);
emit_control_segments(&mut s.console, &[down]);
}
}
if prev_height > new_height {
for _ in new_height..prev_height {
let erase_old = Segment::new(
"",
None,
Some(vec![
ControlCode::WithParam(ControlType::CursorDown, 1),
ControlCode::Simple(ControlType::CarriageReturn),
ControlCode::WithParam(ControlType::EraseInLine, 2),
]),
);
emit_control_segments(&mut s.console, &[erase_old]);
}
let lines_to_go_up = (prev_height - new_height) as i32;
if lines_to_go_up > 0 {
let go_up = Segment::new(
"",
None,
Some(vec![ControlCode::WithParam(
ControlType::CursorUp,
lines_to_go_up,
)]),
);
emit_control_segments(&mut s.console, &[go_up]);
}
}
let _ = position_segments; }
s.console.end_synchronized();
s.last_segments = Some(render_segments);
s.prev_lines = new_lines;
}
}
pub fn update_renderable(
&self,
renderable: impl Renderable + Send + Sync + 'static,
refresh: bool,
) {
let arc: Arc<dyn Renderable + Send + Sync> = Arc::new(renderable);
self.renderable.store(Arc::new(LiveContent(arc)));
if refresh {
self.refresh();
}
}
pub fn update(&self, renderable: impl Renderable + Send + Sync + 'static, refresh: bool) {
self.update_renderable(renderable, refresh);
}
pub fn set(&self, renderable: impl Renderable + Send + Sync + 'static) {
self.update_renderable(renderable, true);
}
pub fn from_renderable<R: Renderable + Send + Sync + 'static>(renderable: R) -> Self {
Self::new(renderable)
}
pub fn set_renderable_widget<R: Renderable + Send + Sync + 'static>(&self, renderable: R) {
self.set(renderable);
}
pub fn print_above(&self, above: impl Renderable + Send + Sync + 'static) {
let current = self.renderable.load().0.clone();
let mut s = self.state.lock().unwrap();
if s.screen {
return;
}
let restore = s.live_render.restore_cursor();
emit_control_segments(&mut s.console, &restore);
s.console.print(&above);
s.console.write_segments(&[Segment::line()]);
s.live_render.set_renderable(current);
let opts = s.console.options();
let render_segments = s.live_render.gilt_console(&s.console, &opts);
s.console.write_segments(&render_segments);
s.last_segments = None;
s.prev_lines = Vec::new();
}
pub fn run(initial: impl Renderable + Send + Sync + 'static) -> Self {
let mut live = Self::new(initial);
live.start();
live
}
pub fn current_renderable(&self) -> Arc<dyn Renderable + Send + Sync> {
self.renderable.load().0.clone()
}
pub fn render_to_text(&self) -> Text {
let content = self.current_renderable();
let s = self.state.lock().unwrap();
let opts = s.console.options();
let lines = s
.console
.render_lines(content.as_ref(), Some(&opts), None, false, false);
let mut result = String::new();
let line_count = lines.len();
for (i, line) in lines.into_iter().enumerate() {
for seg in &line {
if !seg.is_control() {
result.push_str(&seg.text);
}
}
if i + 1 < line_count {
result.push('\n');
}
}
Text::new(&result, crate::style::Style::null())
}
pub fn renderable(&self) -> Text {
self.render_to_text()
}
}
impl Drop for Live {
fn drop(&mut self) {
self.stop();
}
}
pub struct ConsoleRef<'a> {
guard: std::sync::MutexGuard<'a, SharedState>,
}
impl std::ops::Deref for ConsoleRef<'_> {
type Target = Console;
fn deref(&self) -> &Console {
&self.guard.console
}
}
pub struct ConsoleRefMut<'a> {
guard: std::sync::MutexGuard<'a, SharedState>,
}
impl std::ops::Deref for ConsoleRefMut<'_> {
type Target = Console;
fn deref(&self) -> &Console {
&self.guard.console
}
}
impl std::ops::DerefMut for ConsoleRefMut<'_> {
fn deref_mut(&mut self) -> &mut Console {
&mut self.guard.console
}
}
pub struct LiveRenderRef<'a> {
guard: std::sync::MutexGuard<'a, SharedState>,
}
impl std::ops::Deref for LiveRenderRef<'_> {
type Target = LiveRender;
fn deref(&self) -> &LiveRender {
&self.guard.live_render
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
use std::sync::atomic::{AtomicUsize, Ordering};
fn test_console() -> Console {
Console::builder()
.width(80)
.height(25)
.quiet(true)
.markup(false)
.no_color(true)
.force_terminal(true)
.build()
}
#[test]
fn test_default_construction() {
let live = Live::new(Text::new("hello", Style::null()));
assert!(!live.started);
assert!(live.auto_refresh);
assert!((live.refresh_per_second - 4.0).abs() < f64::EPSILON);
assert!(live.refresh_thread.is_none());
assert!(!live.transient);
assert_eq!(live.vertical_overflow, VerticalOverflowMethod::Ellipsis);
}
#[test]
fn test_construction_stores_renderable() {
let live = Live::new(Text::new("Hello", Style::null()));
let text = live.renderable();
assert!(text.plain().contains("Hello"));
}
#[test]
fn test_with_auto_refresh() {
let live = Live::new(Text::empty()).with_auto_refresh(false);
assert!(!live.auto_refresh);
}
#[test]
fn test_with_refresh_per_second() {
let live = Live::new(Text::empty()).with_refresh_per_second(10.0);
assert!((live.refresh_per_second - 10.0).abs() < f64::EPSILON);
}
#[test]
#[should_panic(expected = "refresh_per_second must be > 0")]
fn test_with_refresh_per_second_zero() {
let _ = Live::new(Text::empty()).with_refresh_per_second(0.0);
}
#[test]
#[should_panic(expected = "refresh_per_second must be > 0")]
fn test_with_refresh_per_second_negative() {
let _ = Live::new(Text::empty()).with_refresh_per_second(-1.0);
}
#[test]
fn test_with_transient() {
let live = Live::new(Text::empty()).with_transient(true);
assert!(live.transient);
}
#[test]
fn test_with_screen() {
let live = Live::new(Text::empty()).with_screen(true);
let s = live.state.lock().unwrap();
assert!(s.screen);
}
#[test]
fn test_with_vertical_overflow() {
let live = Live::new(Text::empty()).with_vertical_overflow(VerticalOverflowMethod::Crop);
assert_eq!(live.vertical_overflow, VerticalOverflowMethod::Crop);
let s = live.state.lock().unwrap();
assert_eq!(
s.live_render.vertical_overflow,
VerticalOverflowMethod::Crop
);
}
#[test]
fn test_with_console() {
let console = test_console();
let live = Live::new(Text::empty()).with_console(console);
assert_eq!(live.console().width(), 80);
}
#[test]
fn test_with_get_renderable() {
let live = Live::new(Text::empty()).with_get_renderable(|| {
Arc::new(Text::new("dynamic", Style::null())) as Arc<dyn Renderable + Send + Sync>
});
let s = live.state.lock().unwrap();
assert!(s.get_renderable.is_some());
}
#[test]
fn test_start_stop() {
let mut live = Live::new(Text::new("test", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
assert!(!live.is_started());
live.start();
assert!(live.is_started());
live.stop();
assert!(!live.is_started());
}
#[test]
fn test_double_start_is_noop() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.start();
assert!(live.is_started());
live.start(); assert!(live.is_started());
live.stop();
}
#[test]
fn test_double_stop_is_noop() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.start();
live.stop();
assert!(!live.is_started());
live.stop(); assert!(!live.is_started());
}
#[test]
fn test_stop_without_start_is_noop() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.stop(); assert!(!live.is_started());
}
#[test]
fn test_update_renderable_changes_content() {
let live = Live::new(Text::new("initial", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.update_renderable(Text::new("updated", Style::null()), false);
let txt = live.renderable();
assert!(txt.plain().contains("updated"));
}
#[test]
fn test_update_alias() {
let live = Live::new(Text::new("initial", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.update(Text::new("via_update", Style::null()), false);
let txt = live.renderable();
assert!(txt.plain().contains("via_update"));
}
#[test]
fn test_update_with_refresh() {
let mut live = Live::new(Text::new("initial", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.start();
live.update_renderable(Text::new("refreshed", Style::null()), true);
let txt = live.renderable();
assert!(txt.plain().contains("refreshed"));
live.stop();
}
#[test]
fn test_renderable_returns_current() {
let live = Live::new(Text::new("hello", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
let txt = live.renderable();
assert!(txt.plain().contains("hello"));
}
#[test]
fn test_update_also_updates_live_render() {
let live = Live::new(Text::new("old", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.update_renderable(Text::new("new", Style::null()), false);
let arc = live.current_renderable();
let txt = live.render_to_text();
assert!(txt.plain().contains("new"));
live.refresh();
let _ = arc; }
#[test]
fn test_auto_refresh_thread_starts_and_stops() {
let mut live = Live::new(Text::new("auto", Style::null()))
.with_console(test_console())
.with_auto_refresh(true)
.with_refresh_per_second(20.0);
live.start();
assert!(live.refresh_thread.is_some());
thread::sleep(Duration::from_millis(100));
live.stop();
assert!(live.refresh_thread.is_none());
}
#[test]
fn test_no_refresh_thread_when_disabled() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.start();
assert!(live.refresh_thread.is_none());
live.stop();
}
#[test]
fn test_refresh_thread_calls_refresh() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(true)
.with_refresh_per_second(100.0)
.with_get_renderable(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
Arc::new(Text::new("tick", Style::null())) as Arc<dyn Renderable + Send + Sync>
});
live.start();
thread::sleep(Duration::from_millis(150));
live.stop();
let count = counter.load(Ordering::SeqCst);
assert!(
count >= 2,
"expected at least 2 refresh calls, got {}",
count
);
}
#[test]
fn test_transient_mode_flag() {
let live = Live::new(Text::empty()).with_transient(true);
assert!(live.transient);
}
#[test]
fn test_transient_stop_does_not_panic() {
let mut live = Live::new(Text::new("gone", Style::null()))
.with_console(test_console())
.with_transient(true)
.with_auto_refresh(false);
live.start();
live.refresh();
live.stop();
}
#[test]
fn test_screen_mode_flag() {
let live = Live::new(Text::empty()).with_screen(true);
let s = live.state.lock().unwrap();
assert!(s.screen);
}
#[test]
fn test_screen_mode_start_stop() {
let mut live = Live::new(Text::new("screen", Style::null()))
.with_console(test_console())
.with_screen(true)
.with_auto_refresh(false);
live.start();
assert!(live.is_started());
live.stop();
assert!(!live.is_started());
}
#[test]
fn test_drop_calls_stop() {
let stop_flag;
{
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.start();
assert!(live.is_started());
stop_flag = Arc::clone(&live.stop_flag);
}
let stopped = stop_flag.0.lock().unwrap();
assert!(*stopped, "Drop should have called stop()");
}
#[test]
fn test_drop_with_auto_refresh_cleans_up() {
let stop_flag;
{
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(true)
.with_refresh_per_second(20.0);
live.start();
stop_flag = Arc::clone(&live.stop_flag);
}
let stopped = stop_flag.0.lock().unwrap();
assert!(*stopped, "Drop should have signalled the stop flag");
}
#[test]
fn test_drop_without_start_does_not_panic() {
let _live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(true);
}
#[test]
fn test_manual_refresh() {
let mut live = Live::new(Text::new("manual", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.start();
live.refresh();
live.refresh();
live.stop();
}
#[test]
fn test_get_renderable_callback_used_on_refresh() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false)
.with_get_renderable(|| {
Arc::new(Text::new("from_callback", Style::null()))
as Arc<dyn Renderable + Send + Sync>
});
live.start();
live.refresh();
live.stop();
}
#[test]
fn test_full_builder_chain() {
let live = Live::new(Text::new("test", Style::null()))
.with_console(test_console())
.with_auto_refresh(true)
.with_refresh_per_second(10.0)
.with_transient(false)
.with_screen(false)
.with_vertical_overflow(VerticalOverflowMethod::Visible);
assert!(live.auto_refresh);
assert!((live.refresh_per_second - 10.0).abs() < f64::EPSILON);
assert!(!live.transient);
assert_eq!(live.vertical_overflow, VerticalOverflowMethod::Visible);
}
#[test]
fn test_start_stop_start_again() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.start();
live.stop();
live.start();
assert!(live.is_started());
live.stop();
assert!(!live.is_started());
}
#[test]
fn test_update_before_start() {
let live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.update_renderable(Text::new("before start", Style::null()), false);
let txt = live.renderable();
assert!(txt.plain().contains("before start"));
}
#[test]
fn test_refresh_before_start() {
let live = Live::new(Text::new("pre-start", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
live.refresh();
}
#[test]
fn test_auto_refresh_restart() {
let mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(true)
.with_refresh_per_second(20.0);
live.start();
assert!(live.refresh_thread.is_some());
live.stop();
assert!(live.refresh_thread.is_none());
live.start();
assert!(live.refresh_thread.is_some());
live.stop();
assert!(live.refresh_thread.is_none());
}
#[test]
fn test_vertical_overflow_visible() {
let live = Live::new(Text::empty()).with_vertical_overflow(VerticalOverflowMethod::Visible);
assert_eq!(live.vertical_overflow, VerticalOverflowMethod::Visible);
}
#[test]
fn test_vertical_overflow_ellipsis_default() {
let live = Live::new(Text::empty());
assert_eq!(live.vertical_overflow, VerticalOverflowMethod::Ellipsis);
}
#[test]
fn live_stop_with_no_render_emits_no_newline() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("hello", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.stop();
let captured = live.state.lock().unwrap().console.end_capture();
assert!(
!captured.contains('\n'),
"expected no trailing newline when nothing was rendered, got: {:?}",
captured
);
}
#[test]
fn live_stop_after_render_emits_newline() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("progress", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.refresh();
live.stop();
let captured = live.state.lock().unwrap().console.end_capture();
assert!(
captured.contains('\n'),
"expected a trailing newline after rendering, got: {:?}",
captured
);
}
#[test]
fn test_console_accessor() {
let live = Live::new(Text::new("test", Style::null()))
.with_console(Console::builder().width(120).build());
assert_eq!(live.console().width(), 120);
}
#[test]
fn test_console_mut_accessor() {
let live = Live::new(Text::new("test", Style::null())).with_console(test_console());
let _console = live.console_mut();
}
#[test]
fn print_above_screen_mode_is_noop() {
let mut live = Live::new(Text::new("live", Style::null()))
.with_console(test_console())
.with_screen(true)
.with_auto_refresh(false);
live.start();
live.print_above(Text::new("above", Style::null())); live.stop();
}
#[test]
fn refresh_wraps_emit_in_synchronized_output() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("frame content", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.refresh();
let captured = live.state.lock().unwrap().console.end_capture();
assert!(
captured.contains("\x1b[?2026h"),
"frame should open synchronized output (CSI ?2026h): {captured:?}"
);
assert!(
captured.contains("\x1b[?2026l"),
"frame should close synchronized output (CSI ?2026l): {captured:?}"
);
let begin = captured.find("\x1b[?2026h").unwrap();
let end = captured.rfind("\x1b[?2026l").unwrap();
assert!(begin < end, "begin-sync must precede end-sync");
assert!(captured.contains("frame content"));
}
#[test]
fn print_above_emits_content_and_redraws_live_region() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("live_content", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.refresh();
live.print_above(Text::new("above_content", Style::null()));
live.stop();
let captured = live.state.lock().unwrap().console.end_capture();
assert!(
captured.contains("above_content"),
"above content missing; got: {:?}",
captured
);
assert!(
captured.contains("live_content"),
"live content missing after print_above; got: {:?}",
captured
);
}
#[test]
fn frame_skip_identical_refreshes_skip_second_write() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("same_content", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.refresh();
let after_first = live.state.lock().unwrap().console.end_capture();
live.state.lock().unwrap().console.begin_capture();
live.refresh();
let after_second = live.state.lock().unwrap().console.end_capture();
live.stop();
assert!(
!after_first.is_empty(),
"first refresh should produce output"
);
assert!(
after_second.is_empty(),
"second identical refresh should be skipped (no I/O), got: {:?}",
after_second
);
}
#[test]
fn frame_skip_different_content_is_emitted() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("first", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.start();
live.refresh();
live.state.lock().unwrap().console.begin_capture();
live.update_renderable(Text::new("second", Style::null()), false);
live.refresh();
let captured = live.state.lock().unwrap().console.end_capture();
live.stop();
assert!(
captured.contains("second"),
"changed content should have been emitted, got: {:?}",
captured
);
}
#[test]
fn test_from_renderable_stores_directly() {
let text = Text::new("from_renderable_test", Style::null());
let live = Live::from_renderable(text).with_console(test_console());
let txt = live.renderable();
assert!(txt.plain().contains("from_renderable_test"));
}
#[test]
fn test_current_renderable_is_arc() {
let live = Live::new(Text::new("arc_test", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
let arc = live.current_renderable();
let _arc2 = arc.clone();
}
#[test]
fn test_render_to_text_uses_live_console() {
let live = Live::new(Text::new("render_to_text_test", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
let txt = live.render_to_text();
assert!(txt.plain().contains("render_to_text_test"));
}
#[test]
fn line_diff_unchanged_lines_not_rewritten() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("alpha\nbeta\ngamma", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.state.lock().unwrap().console.begin_capture();
live.start();
live.refresh();
let _ = live.state.lock().unwrap().console.end_capture();
live.state.lock().unwrap().console.begin_capture();
live.update_renderable(
Text::new("alpha\nBETA_CHANGED\ngamma", Style::null()),
false,
);
live.refresh();
let second_capture = live.state.lock().unwrap().console.end_capture();
live.stop();
assert!(
second_capture.contains("BETA_CHANGED"),
"new middle-line text must be emitted, got: {:?}",
second_capture
);
assert!(
!second_capture.contains("alpha"),
"unchanged first line 'alpha' must NOT be rewritten in second capture, \
got: {:?}",
second_capture
);
assert!(
!second_capture.contains("gamma"),
"unchanged third line 'gamma' must NOT be rewritten in second capture, \
got: {:?}",
second_capture
);
}
#[test]
fn line_diff_final_content_correct() {
let console = Console::builder()
.width(80)
.height(25)
.quiet(false)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let mut live = Live::new(Text::new("line1\nline2\nline3", Style::null()))
.with_console(console)
.with_auto_refresh(false);
live.start();
live.refresh();
live.update_renderable(
Text::new("line1\nLINE2_UPDATED\nline3", Style::null()),
false,
);
live.state.lock().unwrap().console.begin_capture();
live.refresh();
let captured = live.state.lock().unwrap().console.end_capture();
live.stop();
assert!(
captured.contains("LINE2_UPDATED"),
"updated line must appear in output, got: {:?}",
captured
);
}
#[test]
fn test_live_renders_with_live_console_width() {
let console = Console::builder()
.width(120)
.height(25)
.quiet(true)
.markup(false)
.no_color(true)
.force_terminal(true)
.build();
let long_line = "A".repeat(100);
let live = Live::new(Text::new(&long_line, Style::null()))
.with_console(console)
.with_auto_refresh(false);
let rendered = live.render_to_text();
let plain = rendered.plain();
assert!(
plain.contains(&long_line),
"expected 100-char line to fit without wrapping at 120 cols, got: {:?}",
plain
);
}
}