use std::any::Any;
use std::cell::{Cell, RefCell};
use std::io::Write;
use std::panic::{self, AssertUnwindSafe};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::console::{Console, ConsoleOptions};
use crate::control::Control;
use crate::file_proxy::{FileProxy, ProxyBuffer, ProxyStream, ProxyTarget};
use crate::live_render::VerticalOverflow;
use crate::protocol::Renderable;
use crate::segment::Segment;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LiveOptions {
pub transient: bool,
pub screen: bool,
pub redirect_stdout: bool,
pub redirect_stderr: bool,
pub vertical_overflow: VerticalOverflow,
pub refresh_on_start: bool,
}
impl Default for LiveOptions {
fn default() -> Self {
LiveOptions {
transient: false,
screen: false,
redirect_stdout: true,
redirect_stderr: true,
vertical_overflow: VerticalOverflow::Ellipsis,
refresh_on_start: true,
}
}
}
type Snapshot = Arc<Vec<Segment>>;
struct LiveEntry {
content: Mutex<Option<Snapshot>>,
frame: Mutex<Frame>,
}
#[derive(Clone)]
enum Slot {
Own,
Other(Snapshot),
}
struct Frame {
shape: Option<(usize, usize)>,
vertical_overflow: VerticalOverflow,
slots: Vec<Slot>,
generation: u64,
stale: bool,
hooked: bool,
started: bool,
transient: bool,
alt_screen: bool,
interactive: bool,
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
impl LiveEntry {
fn new(interactive: bool) -> Self {
LiveEntry {
content: Mutex::new(None),
frame: Mutex::new(Frame {
shape: None,
vertical_overflow: VerticalOverflow::Ellipsis,
slots: vec![Slot::Own],
generation: 0,
stale: false,
hooked: false,
started: false,
transient: false,
alt_screen: false,
interactive,
}),
}
}
fn frame(&self) -> std::sync::MutexGuard<'_, Frame> {
lock(&self.frame)
}
fn content(&self) -> Option<Snapshot> {
lock(&self.content).clone()
}
fn set_content(&self, content: Snapshot) {
*lock(&self.content) = Some(content);
}
}
fn entry_of(item: &Arc<dyn Any + Send + Sync>) -> Option<Arc<LiveEntry>> {
item.clone().downcast::<LiveEntry>().ok()
}
fn group_slots(stack: &[Arc<dyn Any + Send + Sync>], first: &Arc<LiveEntry>) -> Vec<Slot> {
stack
.iter()
.filter_map(entry_of)
.filter_map(|entry| {
if Arc::ptr_eq(&entry, first) {
Some(Slot::Own)
} else {
entry.content().map(Slot::Other)
}
})
.collect()
}
impl Frame {
fn set_slots(&mut self, slots: Vec<Slot>) {
self.slots = slots;
self.generation += 1;
}
fn draws(&self) -> bool {
self.hooked && (self.interactive || (!self.started && !self.transient))
}
fn arrange(&mut self, rendered: &str, live: &str, shape: (usize, usize)) -> String {
let reset = if !self.interactive {
String::new()
} else if self.alt_screen {
Control::home().as_str().to_string()
} else {
crate::live_render::position_cursor(self.shape)
.as_str()
.to_string()
};
self.shape = Some(shape);
format!("{reset}{rendered}{live}")
}
fn draw(&mut self, console: &Console, own: &Own<'_>, rendered: &str) -> String {
if !self.draws() {
return rendered.to_string();
}
let (live, shape, _) = render_frame(
console,
own,
&self.slots,
self.alt_screen,
self.vertical_overflow,
);
self.arrange(rendered, &live, shape)
}
}
enum Own<'a> {
Render(&'a dyn Renderable),
Snapshot(Option<Snapshot>),
}
struct FrameContent<'a> {
own: &'a Own<'a>,
slots: &'a [Slot],
alt_screen: bool,
stash: RefCell<Option<Snapshot>>,
}
struct FrameGroup<'a, 'b>(&'b FrameContent<'a>);
impl Renderable for FrameGroup<'_, '_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let content = self.0;
let mut segments = Vec::new();
let mut first = true;
for slot in content.slots {
let rendered = match (slot, content.own) {
(Slot::Own, Own::Render(renderable)) => {
let rendered = console.render(*renderable, Some(options));
*content.stash.borrow_mut() = Some(Arc::new(rendered.clone()));
rendered
}
(Slot::Own, Own::Snapshot(snapshot)) => {
snapshot.as_deref().cloned().unwrap_or_default()
}
(Slot::Other(snapshot), _) => snapshot.as_ref().clone(),
};
if rendered.is_empty() {
continue;
}
if !first {
segments.push(Segment::line());
}
first = false;
segments.extend(rendered);
}
segments
}
}
impl Renderable for FrameContent<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
if self.alt_screen {
crate::screen::render_screen(Some(&FrameGroup(self)), None, console, options)
} else {
FrameGroup(self).rich_render(console, options)
}
}
}
struct FrameRender<'a> {
content: FrameContent<'a>,
vertical_overflow: VerticalOverflow,
shape: Cell<(usize, usize)>,
}
impl Renderable for FrameRender<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let (segments, shape) = crate::live_render::render(
&self.content,
None,
self.vertical_overflow,
console,
options,
);
self.shape.set(shape);
segments
}
fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
false
}
}
fn render_frame(
console: &Console,
own: &Own<'_>,
slots: &[Slot],
alt_screen: bool,
vertical_overflow: VerticalOverflow,
) -> (String, (usize, usize), Option<Snapshot>) {
let render = FrameRender {
content: FrameContent {
own,
slots,
alt_screen,
stash: RefCell::new(None),
},
vertical_overflow,
shape: Cell::new((0, 0)),
};
let live = console.render_to_string(&render);
let shape = render.shape.get();
(live, shape, render.content.stash.into_inner())
}
pub struct Live<W: Write> {
console: Console,
writer: W,
content: Box<dyn Renderable>,
entry: Arc<LiveEntry>,
started: bool,
hooked: bool,
nested: bool,
transient: bool,
screen: bool,
alt_screen: bool,
redirect_stdout: bool,
redirect_stderr: bool,
vertical_overflow: VerticalOverflow,
stdout_buffer: ProxyBuffer,
stderr_buffer: ProxyBuffer,
get_renderable: Option<GetRenderable>,
pending: Option<Box<dyn Renderable>>,
}
type GetRenderable = Box<dyn Fn() -> Box<dyn Renderable>>;
impl<W: Write> Live<W> {
pub fn new(renderable: Box<dyn Renderable>, console: Console, writer: W) -> Self {
let interactive = console.is_terminal() && !console.is_dumb_terminal();
Live {
console,
writer,
content: renderable,
entry: Arc::new(LiveEntry::new(interactive)),
started: false,
hooked: false,
nested: false,
transient: false,
screen: false,
alt_screen: false,
redirect_stdout: true,
redirect_stderr: true,
vertical_overflow: VerticalOverflow::Ellipsis,
stdout_buffer: ProxyBuffer::default(),
stderr_buffer: ProxyBuffer::default(),
get_renderable: None,
pending: None,
}
}
pub fn get_renderable(mut self, get: impl Fn() -> Box<dyn Renderable> + 'static) -> Self {
self.content = get();
self.get_renderable = Some(Box::new(get));
self
}
pub fn options(self, options: &LiveOptions) -> Self {
self.transient(options.transient)
.screen(options.screen)
.redirect_stdout(options.redirect_stdout)
.redirect_stderr(options.redirect_stderr)
.vertical_overflow(options.vertical_overflow)
}
pub fn transient(mut self, transient: bool) -> Self {
self.transient = transient;
self.entry.frame().transient = self.is_transient();
self
}
pub fn screen(mut self, screen: bool) -> Self {
self.screen = screen;
self.entry.frame().transient = self.is_transient();
self
}
pub fn redirect_stdout(mut self, redirect: bool) -> Self {
self.redirect_stdout = redirect;
self
}
pub fn redirect_stderr(mut self, redirect: bool) -> Self {
self.redirect_stderr = redirect;
self
}
pub fn vertical_overflow(mut self, vertical_overflow: VerticalOverflow) -> Self {
self.vertical_overflow = vertical_overflow;
self.entry.frame().vertical_overflow = vertical_overflow;
self
}
pub fn is_started(&self) -> bool {
self.started
}
pub fn console(&self) -> &Console {
&self.console
}
fn is_transient(&self) -> bool {
self.transient || self.screen
}
fn is_dumb_terminal(&self) -> bool {
self.console.is_dumb_terminal()
}
fn is_interactive(&self) -> bool {
self.console.is_terminal() && !self.console.is_dumb_terminal()
}
fn write_str(&mut self, text: &str) {
if !text.is_empty() {
let _ = self.writer.write_all(text.as_bytes());
}
}
fn control(&mut self, control: &Control) {
if !self.is_dumb_terminal() {
self.write_str(control.as_str());
}
}
fn show_cursor(&mut self, show: bool) {
if self.console.is_terminal() {
self.control(&Control::show_cursor(show));
}
}
fn set_alt_screen(&mut self, enable: bool) -> bool {
if self.console.is_terminal() && !self.console.legacy_windows() {
self.control(&Control::alt_screen(enable));
return true;
}
false
}
fn first(&self) -> Option<Arc<LiveEntry>> {
let stack = self.console.live_stack();
let first = entry_of(stack.first()?)?;
(!Arc::ptr_eq(&first, &self.entry)).then_some(first)
}
pub fn start(&mut self) {
self.start_with(true);
}
pub fn start_with(&mut self, refresh: bool) {
if self.started {
return;
}
self.started = true;
self.entry.frame().started = true;
if !self.console.set_live(self.entry.clone()) {
self.nested = true;
self.publish();
return;
}
if self.screen {
self.alt_screen = self.set_alt_screen(true);
}
self.show_cursor(false);
self.stdout_buffer = ProxyBuffer::default();
self.stderr_buffer = ProxyBuffer::default();
self.hooked = true;
{
let mut frame = self.entry.frame();
frame.hooked = true;
frame.alt_screen = self.alt_screen;
}
if refresh {
self.refresh();
}
}
pub fn update(&mut self, renderable: Box<dyn Renderable>) {
self.update_with(renderable, true);
}
pub fn update_with(&mut self, renderable: Box<dyn Renderable>, refresh: bool) {
self.get_renderable = None;
self.pending = Some(renderable);
if refresh {
self.refresh();
} else if self.started && (self.nested || self.console.live_stack().len() > 1) {
self.publish();
}
}
fn take_content(&mut self) {
if let Some(get) = &self.get_renderable {
self.content = get();
} else if let Some(renderable) = self.pending.take() {
self.content = renderable;
}
}
fn publish(&mut self) {
let alt_screen = match self.first() {
Some(first) => first.frame().alt_screen,
None => self.alt_screen,
};
let renderable = self.pending.as_deref().unwrap_or(self.content.as_ref());
let (_, _, stash) = render_frame(
&self.console,
&Own::Render(renderable),
&[Slot::Own],
alt_screen,
self.vertical_overflow,
);
if let Some(stash) = stash {
self.entry.set_content(stash);
}
}
pub fn refresh(&mut self) {
self.take_content();
if self.nested {
self.publish();
self.refresh_first();
return;
}
let stack = self.console.live_stack();
let slots = match stack.first().and_then(entry_of) {
Some(first) if Arc::ptr_eq(&first, &self.entry) => group_slots(&stack, &self.entry),
_ => vec![Slot::Own],
};
if self.is_interactive() || (!self.started && !self.is_transient()) {
self.emit_with("", Some(slots));
} else {
let mut frame = self.entry.frame();
frame.set_slots(slots);
frame.stale = false;
}
}
fn tick(&mut self) {
if self.nested {
self.take_content();
self.publish();
} else {
self.refresh();
}
}
fn refresh_first(&mut self) {
let stack = self.console.live_stack();
let Some(first) = stack.first().and_then(entry_of) else {
return;
};
if Arc::ptr_eq(&first, &self.entry) {
return;
}
let slots = group_slots(&stack, &first);
let own = Own::Snapshot(first.content());
let output = {
let mut frame = first.frame();
frame.set_slots(slots);
frame.stale = true;
frame.draw(&self.console, &own, "")
};
self.write_str(&output);
}
pub fn print(&mut self, renderable: &dyn Renderable) {
let rendered = self.console.capture(|console| console.print(renderable));
self.emit(&rendered);
}
pub fn print_str(&mut self, content: &str) {
let rendered = self.console.capture(|console| console.print_str(content));
self.emit(&rendered);
}
#[track_caller]
pub fn log(
&mut self,
objects: &[crate::console::LogObject<'_>],
options: &crate::console::LogOptions<'_>,
) -> crate::errors::Result<()> {
let caller = std::panic::Location::caller();
self.log_at(objects, options, caller.file(), caller.line())
}
pub fn log_at(
&mut self,
objects: &[crate::console::LogObject<'_>],
options: &crate::console::LogOptions<'_>,
file: &str,
line: u32,
) -> crate::errors::Result<()> {
let mut result = Ok(());
let rendered = self.console.capture(|console| {
result = console.log_at(objects, options, file, line);
});
self.emit(&rendered);
result
}
fn emit(&mut self, rendered: &str) {
self.emit_with(rendered, None);
}
fn emit_with(&mut self, rendered: &str, slots: Option<Vec<Slot>>) {
let entry = self.entry.clone();
if !self.hooked {
if let Some(slots) = slots {
entry.frame().set_slots(slots);
}
self.emit_through_first(rendered);
return;
}
let (slots, generation, draws, stale) = {
let mut frame = entry.frame();
if let Some(slots) = slots {
frame.set_slots(slots);
frame.stale = false;
}
let stale = std::mem::take(&mut frame.stale);
(frame.slots.clone(), frame.generation, frame.draws(), stale)
};
if stale {
self.take_content();
}
if !draws {
self.write_str(rendered);
return;
}
let (live, shape, stash) = render_frame(
&self.console,
&Own::Render(self.content.as_ref()),
&slots,
self.alt_screen,
self.vertical_overflow,
);
if let (Some(stash), None) = (&stash, &self.pending) {
entry.set_content(stash.clone());
}
let mut frame = entry.frame();
let output = if frame.generation == generation {
frame.arrange(rendered, &live, shape)
} else {
let own = Own::Snapshot(stash.or_else(|| entry.content()));
frame.draw(&self.console, &own, rendered)
};
if !output.is_empty() {
let _ = self.writer.write_all(output.as_bytes());
}
}
fn emit_through_first(&mut self, rendered: &str) {
let output = match self.first() {
Some(first) => {
let own = Own::Snapshot(first.content());
let mut frame = first.frame();
frame.draw(&self.console, &own, rendered)
}
None => rendered.to_string(),
};
self.write_str(&output);
}
pub fn stop(&mut self) {
if !self.started {
return;
}
self.started = false;
self.entry.frame().started = false;
self.console.clear_live();
if self.nested {
self.nested = false;
if !self.is_transient() {
self.take_content();
let content = self.content.as_ref();
let rendered = self.console.capture(|console| console.print(content));
self.emit_through_first(&rendered);
}
let _ = self.writer.flush();
return;
}
self.vertical_overflow = VerticalOverflow::Visible;
self.entry.frame().vertical_overflow = VerticalOverflow::Visible;
let alt_screen = self.alt_screen;
if !alt_screen {
self.refresh();
}
for stream in [ProxyStream::Stdout, ProxyStream::Stderr] {
if let Some(pending) = self.proxy_buffer(stream).flush() {
self.print_str(&pending);
}
}
self.hooked = false;
let entry = self.entry.clone();
let mut frame = entry.frame();
frame.hooked = false;
let height = frame.shape.map_or(0, |(_, height)| height);
if !alt_screen && self.console.is_terminal() && height > 0 {
self.write_str("\n");
}
self.show_cursor(true);
if alt_screen {
self.set_alt_screen(false);
}
if self.is_transient() && !alt_screen {
let restore = crate::live_render::restore_cursor(frame.shape);
self.control(&restore);
}
self.alt_screen = false;
frame.alt_screen = false;
drop(frame);
let _ = self.writer.flush();
}
fn abort(&mut self) {
if !self.started && !self.hooked {
return;
}
if self.started {
self.console.clear_live();
}
self.started = false;
{
let mut frame = self.entry.frame();
frame.started = false;
frame.hooked = false;
}
if std::mem::take(&mut self.nested) {
return;
}
self.hooked = false;
if self.console.is_terminal() {
let _ = write!(self.writer, "\n{}", Control::show_cursor(true).as_str());
if self.alt_screen {
let _ = write!(self.writer, "{}", Control::alt_screen(false).as_str());
}
let _ = self.writer.flush();
}
self.alt_screen = false;
self.entry.frame().alt_screen = false;
}
fn redirecting(&self, stream: ProxyStream) -> bool {
let enabled = match stream {
ProxyStream::Stdout => self.redirect_stdout,
ProxyStream::Stderr => self.redirect_stderr,
};
self.started && enabled && self.console.is_terminal()
}
fn proxy_buffer(&mut self, stream: ProxyStream) -> &mut ProxyBuffer {
match stream {
ProxyStream::Stdout => &mut self.stdout_buffer,
ProxyStream::Stderr => &mut self.stderr_buffer,
}
}
pub fn stdout(&mut self) -> FileProxy<&mut Self, std::io::Stdout> {
FileProxy::new(self, ProxyStream::Stdout, std::io::stdout())
}
pub fn stderr(&mut self) -> FileProxy<&mut Self, std::io::Stderr> {
FileProxy::new(self, ProxyStream::Stderr, std::io::stderr())
}
pub fn file_proxy<F: Write>(
&mut self,
stream: ProxyStream,
file: F,
) -> FileProxy<&mut Self, F> {
FileProxy::new(self, stream, file)
}
pub fn writer(&self) -> &W {
&self.writer
}
pub fn into_writer(self) -> W {
self.writer
}
}
impl<W: Write> ProxyTarget for &mut Live<W> {
fn proxy_write(&mut self, stream: ProxyStream, bytes: &[u8]) -> bool {
if !self.redirecting(stream) {
return false;
}
if let Some(text) = self.proxy_buffer(stream).write(bytes) {
self.print(&text);
}
true
}
fn proxy_flush(&mut self, stream: ProxyStream) -> bool {
if !self.redirecting(stream) {
return false;
}
if let Some(pending) = self.proxy_buffer(stream).flush() {
self.print_str(&pending);
}
true
}
}
type LiveJob<W> = Box<dyn FnOnce(&mut Live<W>) + Send>;
enum LiveMessage<W: Write> {
Update(Box<dyn Renderable + Send>),
Refresh,
Run(LiveJob<W>),
Stop,
}
impl<W: Write + Send + 'static> Live<W> {
pub fn spawn(
renderable: Box<dyn Renderable + Send>,
console: Console,
writer: W,
refresh_per_second: f64,
) -> AutoLive<W> {
Live::spawn_options(
renderable,
console,
writer,
refresh_per_second,
LiveOptions::default(),
)
}
pub fn spawn_with(
renderable: Box<dyn Renderable + Send>,
console: Console,
writer: W,
refresh_per_second: f64,
transient: bool,
) -> AutoLive<W> {
Live::spawn_options(
renderable,
console,
writer,
refresh_per_second,
LiveOptions {
transient,
..LiveOptions::default()
},
)
}
pub fn spawn_options(
renderable: Box<dyn Renderable + Send>,
console: Console,
writer: W,
refresh_per_second: f64,
options: LiveOptions,
) -> AutoLive<W> {
Live::spawn_inner(
move |console, writer| Live::new(renderable, console, writer),
console,
writer,
refresh_per_second,
options,
)
}
pub fn spawn_get_renderable(
get: impl Fn() -> Box<dyn Renderable> + Send + 'static,
console: Console,
writer: W,
refresh_per_second: f64,
options: LiveOptions,
) -> AutoLive<W> {
Live::spawn_inner(
move |console, writer| {
Live::new(Box::new(crate::text::Text::new("")), console, writer).get_renderable(get)
},
console,
writer,
refresh_per_second,
options,
)
}
fn spawn_inner(
make: impl FnOnce(Console, W) -> Live<W> + Send + 'static,
console: Console,
writer: W,
refresh_per_second: f64,
options: LiveOptions,
) -> AutoLive<W> {
let (sender, receiver) = mpsc::channel::<LiveMessage<W>>();
let (started, wait_started) = mpsc::channel::<()>();
let interval = Duration::from_secs_f64(1.0 / refresh_per_second.max(f64::MIN_POSITIVE));
let terminal = console.is_terminal();
let handle = thread::spawn(move || {
let mut live = make(console, writer).options(&options);
let outcome = panic::catch_unwind(AssertUnwindSafe(|| {
live.start_with(options.refresh_on_start);
let _ = started.send(());
loop {
match receiver.recv_timeout(interval) {
Ok(LiveMessage::Update(renderable)) => live.update(renderable),
Ok(LiveMessage::Refresh) => live.refresh(),
Err(RecvTimeoutError::Timeout) => live.tick(),
Ok(LiveMessage::Run(job)) => job(&mut live),
Ok(LiveMessage::Stop) | Err(RecvTimeoutError::Disconnected) => {
live.stop();
break;
}
}
}
}));
let failure = outcome.err().map(|payload| {
live.abort();
panic_message(payload.as_ref())
});
(live.into_writer(), failure)
});
let _ = wait_started.recv();
AutoLive {
handle: LiveHandle {
sender: Some(sender),
redirect_stdout: options.redirect_stdout && terminal,
redirect_stderr: options.redirect_stderr && terminal,
holder: None,
},
thread: Some(handle),
}
}
}
pub struct LiveHandle<W: Write> {
sender: Option<mpsc::Sender<LiveMessage<W>>>,
redirect_stdout: bool,
redirect_stderr: bool,
holder: Option<Holder>,
}
pub(crate) type Holder = Arc<Mutex<Option<thread::ThreadId>>>;
impl<W: Write> Clone for LiveHandle<W> {
fn clone(&self) -> Self {
LiveHandle {
sender: self.sender.clone(),
redirect_stdout: self.redirect_stdout,
redirect_stderr: self.redirect_stderr,
holder: self.holder.clone(),
}
}
}
impl<W: Write + Send + 'static> LiveHandle<W> {
pub(crate) fn detached() -> Self {
LiveHandle {
sender: None,
redirect_stdout: false,
redirect_stderr: false,
holder: None,
}
}
pub(crate) fn with_holder(mut self, holder: Holder) -> Self {
self.holder = Some(holder);
self
}
pub(crate) fn run(&self, job: impl FnOnce(&mut Live<W>) + Send + 'static) -> bool {
let Some(sender) = &self.sender else {
return false;
};
let held = self.holder.as_ref().is_some_and(|holder| {
*holder
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
== Some(thread::current().id())
});
if held {
return sender.send(LiveMessage::Run(Box::new(job))).is_ok();
}
let (done, wait) = mpsc::channel();
let job = move |live: &mut Live<W>| {
job(live);
let _ = done.send(());
};
if sender.send(LiveMessage::Run(Box::new(job))).is_err() {
return false;
}
let _ = wait.recv();
true
}
pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
self.run(move |live| live.print(renderable.as_ref()));
}
pub fn print_str(&self, content: impl Into<String>) {
let content = content.into();
self.run(move |live| live.print_str(&content));
}
#[track_caller]
pub fn log(
&self,
objects: &[crate::console::LogObject<'_>],
options: &crate::console::LogOptions<'_>,
) -> crate::errors::Result<()> {
let caller = std::panic::Location::caller();
self.log_at(objects, options, caller.file(), caller.line())
}
pub fn log_at(
&self,
objects: &[crate::console::LogObject<'_>],
options: &crate::console::LogOptions<'_>,
file: &str,
line: u32,
) -> crate::errors::Result<()> {
let call = crate::console::OwnedLog::new(objects, options, file, line);
let result = Arc::new(Mutex::new(Ok(())));
let slot = result.clone();
self.run(move |live| {
let mut outcome = Ok(());
let rendered = live.console.capture(|console| outcome = call.log(console));
live.emit(&rendered);
*slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = outcome;
});
let mut result = result
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
std::mem::replace(&mut *result, Ok(()))
}
fn redirecting(&self, stream: ProxyStream) -> bool {
match stream {
ProxyStream::Stdout => self.redirect_stdout,
ProxyStream::Stderr => self.redirect_stderr,
}
}
pub fn file_proxy<F: Write>(&self, stream: ProxyStream, file: F) -> FileProxy<Self, F> {
FileProxy::new(self.clone(), stream, file)
}
}
impl<W: Write + Send + 'static> ProxyTarget for LiveHandle<W> {
fn proxy_write(&mut self, stream: ProxyStream, bytes: &[u8]) -> bool {
if !self.redirecting(stream) {
return false;
}
let bytes = bytes.to_vec();
self.run(move |live| {
(&mut *live).proxy_write(stream, &bytes);
})
}
fn proxy_flush(&mut self, stream: ProxyStream) -> bool {
if !self.redirecting(stream) {
return false;
}
self.run(move |live| {
(&mut *live).proxy_flush(stream);
})
}
}
pub struct AutoLive<W: Write + Send + 'static> {
handle: LiveHandle<W>,
thread: Option<JoinHandle<(W, Option<String>)>>,
}
#[derive(Debug)]
pub struct LivePanic<W> {
pub writer: W,
pub message: String,
}
fn panic_message(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"<non-string panic payload>".to_string()
}
}
impl<W: Write + Send + 'static> AutoLive<W> {
pub fn update(&self, renderable: Box<dyn Renderable + Send>) {
self.send(LiveMessage::Update(renderable));
}
pub fn refresh(&self) {
self.send(LiveMessage::Refresh);
}
fn send(&self, message: LiveMessage<W>) -> bool {
match &self.handle.sender {
Some(sender) => sender.send(message).is_ok(),
None => false,
}
}
pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
self.handle.print(renderable);
}
pub fn print_str(&self, content: impl Into<String>) {
self.handle.print_str(content);
}
#[track_caller]
pub fn log(
&self,
objects: &[crate::console::LogObject<'_>],
options: &crate::console::LogOptions<'_>,
) -> crate::errors::Result<()> {
let caller = std::panic::Location::caller();
self.handle
.log_at(objects, options, caller.file(), caller.line())
}
pub fn handle(&self) -> LiveHandle<W> {
self.handle.clone()
}
pub fn stdout(&self) -> FileProxy<LiveHandle<W>, std::io::Stdout> {
self.handle
.file_proxy(ProxyStream::Stdout, std::io::stdout())
}
pub fn stderr(&self) -> FileProxy<LiveHandle<W>, std::io::Stderr> {
self.handle
.file_proxy(ProxyStream::Stderr, std::io::stderr())
}
pub fn file_proxy<F: Write>(
&self,
stream: ProxyStream,
file: F,
) -> FileProxy<LiveHandle<W>, F> {
self.handle.file_proxy(stream, file)
}
pub fn refresh_wait(&self) {
let (done, wait) = mpsc::channel();
let job = move |live: &mut Live<W>| {
live.refresh();
let _ = done.send(());
};
if self.send(LiveMessage::Run(Box::new(job))) {
let _ = wait.recv();
}
}
pub fn stop(self) -> W {
match self.try_stop() {
Ok(writer) => writer,
Err(failure) => failure.writer,
}
}
pub fn try_stop(mut self) -> Result<W, LivePanic<W>> {
self.send(LiveMessage::Stop);
let handle = self
.thread
.take()
.expect("thread handle present until stop/drop");
let (writer, failure) = match handle.join() {
Ok(result) => result,
Err(payload) => panic::resume_unwind(payload),
};
match failure {
None => Ok(writer),
Some(message) => Err(LivePanic { writer, message }),
}
}
}
impl<W: Write + Send + 'static> Drop for AutoLive<W> {
fn drop(&mut self) {
if let Some(handle) = self.thread.take() {
self.send(LiveMessage::Stop);
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
use crate::text::Text;
fn console() -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.no_color(false)
.build()
}
#[test]
fn redirected_live_emits_only_the_final_frame() {
let console = Console::builder().force_terminal(false).width(20).build();
let mut live = Live::new(Box::new(Text::new("first")), console, Vec::<u8>::new());
live.start();
live.update(Box::new(Text::new("last")));
live.refresh();
assert!(live.writer().is_empty());
live.stop();
assert_eq!(live.writer(), b"last");
}
#[test]
fn manual_refresh_stream_matches_upstream() {
let mut live = Live::new(
Box::new(Text::new("frame one")),
console(),
Vec::<u8>::new(),
);
live.start();
live.update(Box::new(Text::new("frame two")));
live.update(Box::new(Text::new("frame three")));
live.stop();
let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
assert_eq!(String::from_utf8(live.writer().clone()).unwrap(), expected);
}
#[test]
fn auto_refresh_thread_produces_the_same_stream() {
let auto = Live::spawn(
Box::new(Text::new("frame one")),
console(),
Vec::<u8>::new(),
0.1,
);
auto.update(Box::new(Text::new("frame two")));
auto.update(Box::new(Text::new("frame three")));
let output = auto.stop();
let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
struct PanicsLater {
renders: std::sync::Arc<std::sync::atomic::AtomicUsize>,
fail_after: usize,
}
impl Renderable for PanicsLater {
fn rich_render(
&self,
console: &Console,
options: &crate::console::ConsoleOptions,
) -> Vec<crate::segment::Segment> {
use std::sync::atomic::Ordering;
if self.renders.fetch_add(1, Ordering::SeqCst) >= self.fail_after {
panic!("render failed");
}
Text::new("ok").rich_render(console, options)
}
}
#[test]
fn a_final_refresh_that_panics_still_restores_the_cursor() {
let auto = Live::spawn(
Box::new(PanicsLater {
renders: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
fail_after: 1,
}),
console(),
Vec::<u8>::new(),
0.1,
);
let (output, error) = match auto.try_stop() {
Ok(output) => (output, None),
Err(failure) => (failure.writer, Some(failure.message)),
};
let output = String::from_utf8(output).unwrap();
assert!(
output.ends_with("\x1b[?25h"),
"cursor left hidden: {output:?}"
);
assert_eq!(error.as_deref(), Some("render failed"));
}
#[test]
fn a_panicking_render_still_restores_the_cursor() {
let renders = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let auto = Live::spawn(
Box::new(PanicsLater {
renders: renders.clone(),
fail_after: 1,
}),
console(),
Vec::<u8>::new(),
0.1,
);
auto.refresh_wait(); let (output, error) = match auto.try_stop() {
Ok(output) => (output, None),
Err(failure) => (failure.writer, Some(failure.message)),
};
let output = String::from_utf8(output).unwrap();
assert!(output.starts_with("\x1b[?25lok"), "{output:?}");
assert!(
output.ends_with("\x1b[?25h"),
"cursor left hidden: {output:?}"
);
assert_eq!(error.as_deref(), Some("render failed"));
let auto = Live::spawn(
Box::new(PanicsLater {
renders: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
fail_after: 1,
}),
console(),
Vec::<u8>::new(),
0.1,
);
auto.refresh();
let output = String::from_utf8(auto.stop()).unwrap();
assert!(
output.ends_with("\x1b[?25h"),
"cursor left hidden: {output:?}"
);
}
#[test]
fn dropping_the_handle_finalizes_the_display() {
let console = console();
let auto = Live::spawn(Box::new(Text::new("only")), console, Vec::<u8>::new(), 0.1);
drop(auto); }
#[derive(Clone, Default)]
struct SharedSink(Arc<Mutex<Vec<u8>>>);
impl Write for SharedSink {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl SharedSink {
fn output(&self) -> String {
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
}
}
fn within_time(scenario: impl FnOnce() + Send + 'static) {
let (done, wait) = mpsc::channel();
let worker = thread::spawn(move || {
scenario();
let _ = done.send(());
});
match wait.recv_timeout(Duration::from_secs(30)) {
Ok(()) => worker.join().unwrap(),
Err(RecvTimeoutError::Disconnected) => {
if let Err(payload) = worker.join() {
panic::resume_unwind(payload);
}
}
Err(RecvTimeoutError::Timeout) => panic!("nested displays deadlocked"),
}
}
fn spawn_text(console: &Console, sink: &SharedSink, content: &str) -> AutoLive<SharedSink> {
Live::spawn(
Box::new(Text::new(content)),
console.clone(),
sink.clone(),
500.0,
)
}
#[test]
fn a_nested_display_is_drawn_inside_the_first() {
let console = console();
let sink = SharedSink::default();
let mut outer = Live::new(Box::new(Text::new("outer")), console.clone(), sink.clone());
outer.start();
let mut inner = Live::new(Box::new(Text::new("inner")), console.clone(), sink.clone());
inner.start();
inner.refresh();
assert!(
sink.output().ends_with("outer\ninner"),
"{:?}",
sink.output()
);
inner.stop();
outer.stop();
assert!(console.clone().set_live(Arc::new(())));
console.clear_live();
}
#[test]
fn threaded_displays_start_and_stop_in_either_order() {
for inner_first in [true, false] {
within_time(move || {
let console = console();
let sink = SharedSink::default();
let outer = spawn_text(&console, &sink, "outer");
let inner = spawn_text(&console, &sink, "inner");
for step in 0..50 {
inner.update(Box::new(Text::new(format!("inner {step}"))));
outer.refresh();
inner.refresh_wait();
inner.print_str("printed");
}
if inner_first {
inner.stop();
outer.stop();
} else {
outer.stop();
inner.stop();
}
let output = sink.output();
assert!(output.contains("outer\ninner 49"), "{output:?}");
assert!(output.ends_with("\x1b[?25h") || output.ends_with('\n'));
});
}
}
#[test]
fn a_nested_display_stops_while_the_first_refreshes() {
within_time(|| {
let console = console();
let sink = SharedSink::default();
let outer = spawn_text(&console, &sink, "outer");
for round in 0..200 {
let mut inner = Live::new(
Box::new(Text::new(format!("inner {round}"))),
console.clone(),
sink.clone(),
);
inner.start();
inner.refresh();
let nested = spawn_text(&console, &sink, "nested");
nested.refresh_wait();
drop(nested);
inner.stop();
}
outer.stop();
assert!(console.set_live(Arc::new(())));
console.clear_live();
});
}
#[test]
fn nested_progress_refreshes_from_inside_the_first_progress() {
within_time(|| {
let console = console();
let sink = SharedSink::default();
let first =
crate::progress::Progress::new().start(console.clone(), sink.clone(), 500.0);
let task = first.add_task("first", 10.0, 0.0);
let nested =
crate::progress::Progress::new().start(console.clone(), sink.clone(), 500.0);
let inner_task = nested.add_task("nested", 10.0, 0.0);
for _ in 0..20 {
first.with(|progress| {
progress.advance(task, 0.5);
nested.advance(inner_task, 0.5);
nested.refresh();
});
thread::sleep(Duration::from_millis(1));
}
nested.stop();
first.stop();
let output = sink.output();
assert!(output.contains("first"), "{output:?}");
assert!(output.contains("nested"), "{output:?}");
});
}
}