use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::time::Duration;
use crate::console::{Console, Renderable};
use crate::control::Control;
use crate::live_render::{LiveRender, VerticalOverflowMethod};
use crate::screen::Screen;
use crate::segment::Segment;
use crate::text::Text;
struct SharedState {
console: Console,
live_render: LiveRender,
renderable: Text,
get_renderable: Option<Box<dyn Fn() -> Text + Send>>,
screen: bool,
}
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>>,
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: Text) -> Self {
let live_render = LiveRender::new(renderable.clone());
let console = Console::new();
let state = Arc::new(Mutex::new(SharedState {
console,
live_render,
renderable,
get_renderable: None,
screen: false,
}));
Live {
state,
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() -> Text + 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 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, 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 {
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.vertical_overflow);
}
fn do_refresh(state: &Arc<Mutex<SharedState>>, vertical_overflow: VerticalOverflowMethod) {
let mut s = state.lock().unwrap();
let renderable = match &s.get_renderable {
Some(f) => f(),
None => s.renderable.clone(),
};
s.live_render.set_renderable(renderable.clone());
s.live_render.vertical_overflow = vertical_overflow;
let position_segments = s.live_render.position_cursor();
emit_control_segments(&mut s.console, &position_segments);
if s.screen {
let opts = s.console.options();
let _render_segments = s.live_render.rich_console(&s.console, &opts);
let screen = Screen::new(renderable);
s.console.print(&screen);
} else {
let opts = s.console.options();
let render_segments = s.live_render.rich_console(&s.console, &opts);
s.console.write_segments(&render_segments);
}
}
pub fn update_renderable(&mut self, renderable: Text, refresh: bool) {
{
let mut s = self.state.lock().unwrap();
s.live_render.set_renderable(renderable.clone());
s.renderable = renderable;
}
if refresh {
self.refresh();
}
}
pub fn update(&mut self, renderable: Text, refresh: bool) {
self.update_renderable(renderable, refresh);
}
pub fn renderable(&self) -> Text {
let s = self.state.lock().unwrap();
s.renderable.clone()
}
}
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()));
assert_eq!(live.renderable().plain(), "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(|| Text::new("dynamic", Style::null()));
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 mut live = Live::new(Text::new("initial", Style::null()))
.with_console(test_console())
.with_auto_refresh(false);
assert_eq!(live.renderable().plain(), "initial");
live.update_renderable(Text::new("updated", Style::null()), false);
assert_eq!(live.renderable().plain(), "updated");
}
#[test]
fn test_update_alias() {
let mut 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);
assert_eq!(live.renderable().plain(), "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);
assert_eq!(live.renderable().plain(), "refreshed");
live.stop();
}
#[test]
fn test_renderable_returns_current() {
let live = Live::new(Text::new("hello", Style::null()));
assert_eq!(live.renderable().plain(), "hello");
}
#[test]
fn test_update_also_updates_live_render() {
let mut 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);
assert_eq!(live.live_render().renderable.plain(), "new");
}
#[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);
Text::new("tick", Style::null())
});
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(|| Text::new("from_callback", Style::null()));
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 mut live = Live::new(Text::empty())
.with_console(test_console())
.with_auto_refresh(false);
live.update_renderable(Text::new("before start", Style::null()), false);
assert_eq!(live.renderable().plain(), "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 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();
}
}