#![warn(missing_docs)]
use std::io::{self, Write};
use std::mem::take;
use std::ops::DerefMut;
use std::sync::{Arc, Mutex};
use std::time::Instant;
mod ansi;
mod destination;
mod helpers;
pub mod models;
mod options;
mod width;
#[cfg(windows)]
mod windows;
pub mod _changelog {
#![doc = include_str!("../NEWS.md")]
#[allow(unused_imports)]
use super::*; }
use crate::ansi::insert_codes;
pub use crate::destination::Destination;
pub use crate::helpers::*;
pub use crate::options::Options;
pub trait Model {
fn render(&mut self, width: usize) -> String;
fn final_message(&mut self) -> String {
String::new()
}
}
pub struct View<M: Model> {
inner: Mutex<Option<InnerView<M>>>,
}
impl<M: Model> View<M> {
pub const fn new(model: M, options: Options) -> View<M> {
View {
inner: Mutex::new(Some(InnerView::new(model, options))),
}
}
fn call_inner<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut InnerView<M>) -> R,
{
f(self
.inner
.lock()
.expect("View mutex is not poisoned")
.as_mut()
.expect("View is not already destroyed"))
}
fn take_inner(self) -> InnerView<M> {
self.inner
.lock()
.expect("View mutex is not poisoned")
.take()
.expect("View is not already destroyed")
}
pub fn abandon(self) -> M {
self.take_inner().abandon().expect("Abandoned view")
}
pub fn finish(self) -> M {
self.take_inner().finish()
}
pub fn update<U, R>(&self, update_fn: U) -> R
where
U: FnOnce(&mut M) -> R,
{
self.call_inner(|inner| inner.update(update_fn))
}
pub fn suspend(&self) {
self.call_inner(|v| v.suspend().expect("suspend succeeds"))
}
pub fn clear(&self) {
self.call_inner(|v| v.clear().expect("clear succeeds"))
}
pub fn resume(&self) {
self.call_inner(|v| v.resume().expect("resume succeeds"))
}
pub fn set_fake_clock(&self, fake_clock: Instant) {
self.call_inner(|v| v.set_fake_clock(fake_clock))
}
pub fn inspect_model<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut M) -> R,
{
self.call_inner(|v| f(&mut v.model))
}
pub fn message<S: AsRef<str>>(&self, message: S) {
self.message_bytes(message.as_ref().as_bytes())
}
pub fn message_bytes<S: AsRef<[u8]>>(&self, message: S) {
self.call_inner(|v| v.write(message.as_ref()).expect("write message"));
}
pub fn captured_output(&self) -> Arc<Mutex<String>> {
self.call_inner(|v| v.captured_output())
}
pub fn take_captured_output(&self) -> String {
self.call_inner(|v| v.take_captured_output())
}
}
impl<M: Model> io::Write for &View<M> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
self.call_inner(|v| v.write(buf))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<M: Model> io::Write for View<M> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
self.call_inner(|v| v.write(buf))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<M: Model> Drop for View<M> {
fn drop(&mut self) {
if let Ok(mut inner_guard) = self.inner.try_lock() {
if let Some(inner) = Option::take(&mut inner_guard) {
inner.finish();
}
}
}
}
struct InnerView<M: Model> {
model: M,
suspended: bool,
state: State,
options: Options,
fake_clock: Option<Instant>,
capture_buffer: Option<Arc<Mutex<String>>>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
enum State {
New,
None,
ProgressDrawn {
last_drawn_time: Instant,
cursor_y: usize,
last_drawn_string: String,
},
Printed { last_printed: Instant },
IncompleteLine,
}
impl<M: Model> InnerView<M> {
const fn new(model: M, options: Options) -> InnerView<M> {
InnerView {
capture_buffer: None,
fake_clock: None,
model,
options,
state: State::New,
suspended: false,
}
}
fn finish(mut self) -> M {
let _ = self.clear();
let final_message = self.model.final_message();
if !final_message.is_empty() {
self.write_output(&format!("{final_message}\n"));
}
self.model
}
fn abandon(mut self) -> io::Result<M> {
match self.state {
State::ProgressDrawn { .. } => {
self.write_output("\n");
}
State::New | State::IncompleteLine | State::None | State::Printed { .. } => (),
}
self.state = State::None; Ok(self.model)
}
fn clock(&self) -> Instant {
self.fake_clock.unwrap_or_else(Instant::now)
}
fn init_destination(&mut self) {
if self.state == State::New {
if self.options.destination.initalize().is_err() {
self.options.progress_enabled = false;
}
self.state = State::None;
}
}
fn paint_progress(&mut self) -> io::Result<()> {
self.init_destination();
if !self.options.progress_enabled || self.suspended {
return Ok(());
}
let now = self.clock();
match self.state {
State::IncompleteLine => return Ok(()),
State::New | State::None => (),
State::Printed { last_printed } => {
if now - last_printed < self.options.print_holdoff {
return Ok(());
}
}
State::ProgressDrawn {
last_drawn_time, ..
} => {
if now - last_drawn_time < self.options.update_interval {
return Ok(());
}
}
}
if let Some(width) = self.options.destination.width() {
let mut rendered = self.model.render(width);
if rendered.ends_with('\n') {
rendered.pop();
}
let cursor_y = match self.state {
State::ProgressDrawn {
ref last_drawn_string,
..
} if *last_drawn_string == rendered => {
return Ok(());
}
State::ProgressDrawn { cursor_y, .. } => Some(cursor_y),
_ => None,
};
let (buf, cursor_y) = insert_codes(&rendered, cursor_y);
self.write_output(&buf);
self.state = State::ProgressDrawn {
last_drawn_time: now,
last_drawn_string: rendered,
cursor_y,
};
}
Ok(())
}
fn suspend(&mut self) -> io::Result<()> {
self.suspended = true;
self.clear()
}
fn resume(&mut self) -> io::Result<()> {
self.suspended = false;
self.paint_progress()
}
fn clear(&mut self) -> io::Result<()> {
match self.state {
State::ProgressDrawn { cursor_y, .. } => {
self.write_output(&format!(
"{}{}{}",
ansi::up_n_lines_and_home(cursor_y),
ansi::CLEAR_TO_END_OF_SCREEN,
ansi::ENABLE_LINE_WRAP,
));
self.state = State::None;
}
State::None | State::New | State::IncompleteLine | State::Printed { .. } => {}
}
Ok(())
}
fn update<U, R>(&mut self, update_fn: U) -> R
where
U: FnOnce(&mut M) -> R,
{
let r = update_fn(&mut self.model);
self.paint_progress().unwrap();
r
}
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
self.init_destination();
self.clear()?;
self.state = if buf.ends_with(b"\n") {
State::Printed {
last_printed: self.clock(),
}
} else {
State::IncompleteLine
};
self.write_output(std::str::from_utf8(buf).expect("message is not UTF-8"));
Ok(buf.len())
}
fn set_fake_clock(&mut self, fake_clock: Instant) {
assert!(self.options.fake_clock, "Options.fake_clock is not enabled");
self.fake_clock = Some(fake_clock);
}
fn write_output(&mut self, buf: &str) {
match &mut self.options.destination {
Destination::Stdout => {
print!("{buf}");
io::stdout().flush().unwrap();
}
Destination::Stderr => {
eprint!("{buf}");
io::stderr().flush().unwrap();
}
Destination::Capture => {
self.capture_buffer
.get_or_insert_with(|| Arc::new(Mutex::new(String::new())))
.lock()
.expect("lock capture_buffer")
.push_str(buf);
}
}
}
fn captured_output(&mut self) -> Arc<Mutex<String>> {
self.capture_buffer
.get_or_insert_with(|| Arc::new(Mutex::new(String::new())))
.clone()
}
fn take_captured_output(&mut self) -> String {
take(
self.capture_buffer
.as_mut()
.expect("output capture is not enabled")
.lock()
.expect("lock capture_buffer")
.deref_mut(),
)
}
}