use crate::text::Text;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Debug)]
pub struct Live {
content: Arc<Mutex<Option<Text>>>,
line_count: Arc<Mutex<usize>>,
refresh_per_second: f64,
transient: bool,
auto_refresh: bool,
running: Arc<AtomicBool>,
thread_handle: Option<thread::JoinHandle<()>>,
}
impl Default for Live {
fn default() -> Self {
Self::new()
}
}
impl Live {
#[must_use]
#[inline]
pub fn new() -> Self {
Self {
content: Arc::new(Mutex::new(None)),
line_count: Arc::new(Mutex::new(0)),
refresh_per_second: 4.0,
transient: false,
auto_refresh: true,
running: Arc::new(AtomicBool::new(false)),
thread_handle: None,
}
}
#[must_use]
#[inline]
pub fn with_content<T: Into<Text>>(content: T) -> Self {
let live = Self::new();
if let Ok(mut guard) = live.content.lock() {
*guard = Some(content.into());
}
live
}
#[must_use]
#[inline]
pub fn refresh_per_second(mut self, rate: f64) -> Self {
self.refresh_per_second = rate;
self
}
#[must_use]
#[inline]
pub const fn transient(mut self, transient: bool) -> Self {
self.transient = transient;
self
}
#[must_use]
#[inline]
pub const fn auto_refresh(mut self, auto_refresh: bool) -> Self {
self.auto_refresh = auto_refresh;
self
}
pub fn update<T: Into<Text>>(&self, content: T) {
if let Ok(mut guard) = self.content.lock() {
*guard = Some(content.into());
}
}
pub fn refresh(&self) {
self.render_content();
}
pub fn start(&mut self) {
if self.running.load(Ordering::SeqCst) {
return;
}
self.running.store(true, Ordering::SeqCst);
eprint!("\x1b[?25l");
let _ = std::io::stderr().flush();
if self.auto_refresh {
let running = Arc::clone(&self.running);
let content = Arc::clone(&self.content);
let line_count = Arc::clone(&self.line_count);
let interval = Duration::from_secs_f64(1.0 / self.refresh_per_second);
let handle = thread::spawn(move || {
while running.load(Ordering::SeqCst) {
Self::render_content_inner(&content, &line_count);
thread::sleep(interval);
}
});
self.thread_handle = Some(handle);
} else {
self.render_content();
}
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
if self.transient {
if let Ok(count) = self.line_count.lock() {
if *count > 0 {
Self::clear_lines(*count);
}
}
}
eprint!("\x1b[?25h");
let _ = std::io::stderr().flush();
}
#[must_use]
#[inline]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
fn render_content(&self) {
Self::render_content_inner(&self.content, &self.line_count);
}
fn render_content_inner(content: &Arc<Mutex<Option<Text>>>, line_count: &Arc<Mutex<usize>>) {
let Ok(content_guard) = content.lock() else {
return;
};
let Some(ref text) = *content_guard else {
return;
};
let prev_lines = match line_count.lock() {
Ok(guard) => *guard,
Err(_) => return,
};
if prev_lines > 0 {
Self::clear_lines(prev_lines);
}
let segments = text.to_segments();
let output = segments.to_ansi();
let new_lines = output
.chars()
.filter(|&c| c == '\n')
.count()
.saturating_add(1);
eprint!("{}", output);
let _ = std::io::stderr().flush();
if let Ok(mut guard) = line_count.lock() {
*guard = new_lines;
}
}
fn clear_lines(count: usize) {
for _ in 0..count {
eprint!("\x1b[1A\x1b[2K");
}
eprint!("\r");
let _ = std::io::stderr().flush();
}
pub fn run<F, R>(mut self, f: F) -> R
where
F: FnOnce(&Self) -> R,
{
self.start();
let result = f(&self);
self.stop();
result
}
}
impl Drop for Live {
fn drop(&mut self) {
if self.is_running() {
self.stop();
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_live_new() {
let live = Live::new();
assert!(!live.is_running());
assert!((live.refresh_per_second - 4.0).abs() < f64::EPSILON);
assert!(!live.transient);
assert!(live.auto_refresh);
}
#[test]
fn test_live_default() {
let live = Live::default();
assert!(!live.is_running());
assert!((live.refresh_per_second - 4.0).abs() < f64::EPSILON);
}
#[test]
fn test_live_with_content() {
let live = Live::with_content("Hello");
let content = live.content.lock().unwrap();
assert!(content.is_some());
}
#[test]
fn test_live_with_content_text() {
let text = Text::from_str("Hello World");
let live = Live::with_content(text);
let content = live.content.lock().unwrap();
assert!(content.is_some());
}
#[test]
fn test_live_refresh_rate() {
let live = Live::new().refresh_per_second(10.0);
assert!((live.refresh_per_second - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_live_refresh_rate_low() {
let live = Live::new().refresh_per_second(0.5);
assert!((live.refresh_per_second - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_live_transient() {
let live = Live::new().transient(true);
assert!(live.transient);
}
#[test]
fn test_live_transient_false() {
let live = Live::new().transient(false);
assert!(!live.transient);
}
#[test]
fn test_live_auto_refresh() {
let live = Live::new().auto_refresh(false);
assert!(!live.auto_refresh);
}
#[test]
fn test_live_auto_refresh_true() {
let live = Live::new().auto_refresh(true);
assert!(live.auto_refresh);
}
#[test]
fn test_live_update() {
let live = Live::new();
live.update("Test");
let content = live.content.lock().unwrap();
assert!(content.is_some());
}
#[test]
fn test_live_update_multiple() {
let live = Live::new();
live.update("First");
live.update("Second");
live.update("Third");
let content = live.content.lock().unwrap();
assert!(content.is_some());
}
#[test]
fn test_live_not_running_initially() {
let live = Live::new();
assert!(!live.is_running());
}
#[test]
fn test_live_refresh_no_content() {
let live = Live::new();
live.refresh();
}
#[test]
fn test_live_refresh_with_content() {
let live = Live::new();
live.update("Test content");
live.refresh();
}
#[test]
fn test_live_start_stop_no_autorefresh() {
let mut live = Live::new().auto_refresh(false);
live.update("Content");
live.start();
assert!(live.is_running());
live.stop();
assert!(!live.is_running());
}
#[test]
fn test_live_double_start() {
let mut live = Live::new().auto_refresh(false);
live.start();
assert!(live.is_running());
live.start();
assert!(live.is_running());
live.stop();
}
#[test]
fn test_live_start_stop_autorefresh() {
let mut live = Live::new().auto_refresh(true).refresh_per_second(100.0); live.update("Content");
live.start();
assert!(live.is_running());
thread::sleep(Duration::from_millis(20));
live.stop();
assert!(!live.is_running());
}
#[test]
fn test_live_transient_clear() {
let mut live = Live::new().auto_refresh(false).transient(true);
live.update("Will be cleared");
live.start();
live.stop();
}
#[test]
fn test_live_builder_chain() {
let live = Live::new()
.refresh_per_second(20.0)
.transient(true)
.auto_refresh(false);
assert!((live.refresh_per_second - 20.0).abs() < f64::EPSILON);
assert!(live.transient);
assert!(!live.auto_refresh);
}
#[test]
fn test_live_drop_stops() {
let mut live = Live::new().auto_refresh(false);
live.start();
assert!(live.is_running());
drop(live);
}
#[test]
fn test_live_run() {
let live = Live::new().auto_refresh(false);
let result = live.run(|l| {
l.update("During run");
42
});
assert_eq!(result, 42);
}
#[test]
fn test_live_clear_lines() {
Live::clear_lines(0);
Live::clear_lines(1);
Live::clear_lines(5);
}
#[test]
fn test_live_line_count_initial() {
let live = Live::new();
let count = live.line_count.lock().unwrap();
assert_eq!(*count, 0);
}
}