use std::io::{self, IsTerminal, Read, Write};
use std::sync::atomic::Ordering::{AcqRel, Acquire, Relaxed};
use std::sync::atomic::{AtomicU64, AtomicU8};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crossterm::{
cursor::{Hide, MoveTo, MoveToColumn, MoveToNextLine, MoveToPreviousLine, Show},
execute, queue,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use crate::art::Art;
use crate::ordering::{Directional, Ordering};
use crate::render::Style;
const DEFAULT_ART: &str = include_str!("../assets/dragon.txt");
const FPS: u64 = 30;
const RUNNING: u8 = 0;
const FINISH_KEEP: u8 = 1; const FINISH_CLEAR: u8 = 2;
struct Shared {
pos: AtomicU64,
total: AtomicU64, state: AtomicU8,
message: Mutex<String>,
art: Art,
ranks: crate::rank::RankMap,
style: Style,
}
impl Shared {
fn inc(&self, delta: u64) {
self.pos.fetch_add(delta, Relaxed);
}
fn set(&self, pos: u64) {
self.pos.store(pos, Relaxed);
}
fn set_message(&self, msg: String) {
if let Ok(mut guard) = self.message.lock() {
*guard = msg;
}
}
}
pub struct Loader {
shared: Arc<Shared>,
joiner: Mutex<Option<JoinHandle<()>>>,
tty: bool,
}
impl Loader {
pub fn new(total: u64) -> Self {
Builder::new().total(total).start()
}
pub fn spinner() -> Self {
Builder::new().start()
}
pub fn builder() -> Builder {
Builder::new()
}
pub fn inc(&self, delta: u64) {
self.shared.inc(delta);
}
pub fn set(&self, pos: u64) {
self.shared.set(pos);
}
pub fn set_length(&self, total: u64) {
self.shared.total.store(total, Relaxed);
}
pub fn set_message<S: Into<String>>(&self, msg: S) {
self.shared.set_message(msg.into());
}
pub fn position(&self) -> u64 {
self.shared.pos.load(Relaxed)
}
pub fn handle(&self) -> Handle {
Handle {
shared: Arc::clone(&self.shared),
}
}
pub fn wrap_read<R: Read>(&self, reader: R) -> ProgressReader<R> {
ProgressReader {
inner: reader,
handle: self.handle(),
}
}
pub fn finish(&self) {
self.finalize(FINISH_KEEP);
}
pub fn finish_and_clear(&self) {
self.finalize(FINISH_CLEAR);
}
fn finalize(&self, how: u8) {
let won = self
.shared
.state
.compare_exchange(RUNNING, how, AcqRel, Relaxed)
.is_ok();
if self.tty {
if let Ok(mut guard) = self.joiner.lock() {
if let Some(handle) = guard.take() {
let _ = handle.join();
}
}
} else if won && how == FINISH_KEEP {
print!(
"{}",
crate::frame::to_string(&self.shared.art, &self.shared.ranks, 1.0)
);
let _ = io::stdout().flush();
}
}
}
impl Drop for Loader {
fn drop(&mut self) {
self.finalize(FINISH_KEEP);
}
}
#[derive(Clone)]
pub struct Handle {
shared: Arc<Shared>,
}
impl Handle {
pub fn inc(&self, delta: u64) {
self.shared.inc(delta);
}
pub fn set(&self, pos: u64) {
self.shared.set(pos);
}
pub fn set_message<S: Into<String>>(&self, msg: S) {
self.shared.set_message(msg.into());
}
pub fn position(&self) -> u64 {
self.shared.pos.load(Relaxed)
}
}
pub struct Builder {
total: u64,
art: Option<Art>,
ordering: Box<dyn Ordering>,
style: Style,
message: String,
}
impl Builder {
fn new() -> Self {
Builder {
total: 0,
art: None,
ordering: Box::new(Directional::default()),
style: Style::default(),
message: String::new(),
}
}
pub fn total(mut self, total: u64) -> Self {
self.total = total;
self
}
pub fn art(mut self, art: Art) -> Self {
self.art = Some(art);
self
}
pub fn ordering(mut self, ordering: impl Ordering + 'static) -> Self {
self.ordering = Box::new(ordering);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn message<S: Into<String>>(mut self, message: S) -> Self {
self.message = message.into();
self
}
pub fn start(self) -> Loader {
let art = self.art.unwrap_or_else(|| Art::parse(DEFAULT_ART));
let ranks = self.ordering.rank(&art);
let shared = Arc::new(Shared {
pos: AtomicU64::new(0),
total: AtomicU64::new(self.total),
state: AtomicU8::new(RUNNING),
message: Mutex::new(self.message),
art,
ranks,
style: self.style,
});
let tty = io::stdout().is_terminal();
let joiner = if tty {
let shared = Arc::clone(&shared);
Mutex::new(Some(thread::spawn(move || run(shared))))
} else {
Mutex::new(None)
};
Loader {
shared,
joiner,
tty,
}
}
}
pub trait ProgressIteratorExt: Iterator + Sized {
fn inkling(self) -> InklingIter<Self> {
let total = self.size_hint().1.unwrap_or(0) as u64;
let loader = if total > 0 {
Loader::new(total)
} else {
Loader::spinner()
};
InklingIter {
inner: self,
loader: Some(loader),
}
}
fn inkling_with(self, loader: Loader) -> InklingIter<Self> {
InklingIter {
inner: self,
loader: Some(loader),
}
}
}
impl<I: Iterator> ProgressIteratorExt for I {}
pub struct InklingIter<I> {
inner: I,
loader: Option<Loader>,
}
impl<I: Iterator> Iterator for InklingIter<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
let next = self.inner.next();
match next {
Some(_) => {
if let Some(loader) = &self.loader {
loader.inc(1);
}
}
None => {
if let Some(loader) = self.loader.take() {
loader.finish();
}
}
}
next
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I> Drop for InklingIter<I> {
fn drop(&mut self) {
if let Some(loader) = self.loader.take() {
loader.finish();
}
}
}
pub struct ProgressReader<R> {
inner: R,
handle: Handle,
}
impl<R: Read> Read for ProgressReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
self.handle.inc(n as u64);
Ok(n)
}
}
fn run(shared: Arc<Shared>) {
let mut out = io::stdout();
let (w, h) = (shared.art.width(), shared.art.height());
let rows = terminal::size().map(|(_, r)| r).unwrap_or(0);
let fullscreen = rows < h + 2;
let (ox, oy) = if fullscreen {
let (cols, vr) = terminal::size().unwrap_or((w, h + 2));
let _ = execute!(out, EnterAlternateScreen, Hide, Clear(ClearType::All));
(
cols.saturating_sub(crate::render::art_cols(&shared.art)) / 2,
vr.saturating_sub(h + 1) / 2,
)
} else {
let _ = execute!(out, Hide);
(0, 0)
};
let frame = Duration::from_millis(1000 / FPS);
let start = Instant::now();
let mut displayed = 0.0f32;
let mut first = true;
loop {
let finishing = shared.state.load(Acquire) != RUNNING;
let total = shared.total.load(Relaxed);
let pos = shared.pos.load(Relaxed);
let t = start.elapsed().as_secs_f32();
let target = if total == 0 {
0.1 + 0.9 * (0.5 - 0.5 * (t * 1.5).cos()) } else {
(pos as f32 / total as f32).clamp(0.0, 1.0)
};
displayed += (target - displayed) * 0.3; let progress = if finishing { 1.0 } else { displayed };
let _ = if fullscreen {
draw_frame(&mut out, &shared, ox, oy, progress, t)
} else {
draw_inline(&mut out, &shared, progress, t, first)
};
first = false;
if finishing {
let cleared = shared.state.load(Relaxed) == FINISH_CLEAR;
if fullscreen {
let _ = execute!(out, ResetColor, Show, LeaveAlternateScreen);
if !cleared {
let _ = persist_final(&mut out, &shared);
}
} else if cleared {
let _ = clear_inline(&mut out, h + 1);
let _ = execute!(out, Show);
} else {
let _ = queue!(out, Print("\r\n"));
let _ = execute!(out, Show);
}
let _ = out.flush();
break;
}
thread::sleep(frame);
}
}
fn draw_frame(
out: &mut io::Stdout,
shared: &Shared,
ox: u16,
oy: u16,
progress: f32,
t: f32,
) -> io::Result<()> {
let art = &shared.art;
let (w, h) = (art.width(), art.height());
let style = &shared.style;
queue!(out, Print(crate::render::SYNC_BEGIN))?;
for y in 0..h {
queue!(out, MoveTo(ox, oy + y))?;
let mut last: Option<(u8, u8, u8)> = None;
for x in 0..w {
match shared.ranks.rank_at(x, y) {
Some(r) if r <= progress => {
if style.color {
let c = crate::render::cell_rgb(style, progress, r, x, y, t);
if last != Some(c) {
queue!(
out,
SetForegroundColor(Color::Rgb {
r: c.0,
g: c.1,
b: c.2
})
)?;
last = Some(c);
}
}
queue!(out, Print(art.glyph(x, y)))?;
}
_ => {
if last.take().is_some() {
queue!(out, ResetColor)?;
}
queue!(out, Print(' '))?;
}
}
}
if last.is_some() {
queue!(out, ResetColor)?;
}
}
queue!(out, MoveTo(ox, oy + h), Clear(ClearType::CurrentLine))?;
let msg = shared
.message
.lock()
.ok()
.map(|m| m.clone())
.unwrap_or_default();
if !msg.is_empty() {
let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
let shown = crate::render::truncate_to_cols(&msg, cols.saturating_sub(1));
if style.color {
queue!(
out,
SetForegroundColor(Color::Rgb {
r: 120,
g: 134,
b: 168
})
)?;
}
queue!(out, Print(shown), ResetColor)?;
}
queue!(out, Print(crate::render::SYNC_END))?;
out.flush()
}
fn draw_inline(
out: &mut io::Stdout,
shared: &Shared,
progress: f32,
t: f32,
first: bool,
) -> io::Result<()> {
let art = &shared.art;
let (w, h) = (art.width(), art.height());
let style = &shared.style;
queue!(out, Print(crate::render::SYNC_BEGIN))?;
if !first {
queue!(out, MoveToPreviousLine(h))?;
}
for y in 0..h {
queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
let mut last: Option<(u8, u8, u8)> = None;
for x in 0..w {
match shared.ranks.rank_at(x, y) {
Some(r) if r <= progress => {
if style.color {
let c = crate::render::cell_rgb(style, progress, r, x, y, t);
if last != Some(c) {
queue!(
out,
SetForegroundColor(Color::Rgb {
r: c.0,
g: c.1,
b: c.2
})
)?;
last = Some(c);
}
}
queue!(out, Print(art.glyph(x, y)))?;
}
_ => {
if last.take().is_some() {
queue!(out, ResetColor)?;
}
queue!(out, Print(' '))?;
}
}
}
if last.is_some() {
queue!(out, ResetColor)?;
}
queue!(out, MoveToNextLine(1))?;
}
queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
let msg = shared
.message
.lock()
.ok()
.map(|m| m.clone())
.unwrap_or_default();
if !msg.is_empty() {
let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
let shown = crate::render::truncate_to_cols(&msg, cols.saturating_sub(1));
if style.color {
queue!(
out,
SetForegroundColor(Color::Rgb {
r: 120,
g: 134,
b: 168
})
)?;
}
queue!(out, Print(shown), ResetColor)?;
}
queue!(out, Print(crate::render::SYNC_END))?;
out.flush()
}
fn clear_inline(out: &mut io::Stdout, lines: u16) -> io::Result<()> {
queue!(out, MoveToPreviousLine(lines - 1))?;
for _ in 0..lines {
queue!(
out,
MoveToColumn(0),
Clear(ClearType::CurrentLine),
MoveToNextLine(1)
)?;
}
queue!(out, MoveToPreviousLine(lines))?;
out.flush()
}
fn persist_final(out: &mut io::Stdout, shared: &Shared) -> io::Result<()> {
let art = &shared.art;
let (w, h) = (art.width(), art.height());
let style = &shared.style;
for y in 0..h {
let mut last_ink = 0u16;
let mut any = false;
for x in 0..w {
if art.is_ink(x, y) {
last_ink = x;
any = true;
}
}
if any {
let mut last: Option<(u8, u8, u8)> = None;
for x in 0..=last_ink {
if art.is_ink(x, y) {
if style.color {
let c = crate::render::cell_rgb(style, 1.0, 0.0, x, y, 0.0);
if last != Some(c) {
queue!(
out,
SetForegroundColor(Color::Rgb {
r: c.0,
g: c.1,
b: c.2
})
)?;
last = Some(c);
}
}
queue!(out, Print(art.glyph(x, y)))?;
} else {
if last.take().is_some() {
queue!(out, ResetColor)?;
}
queue!(out, Print(' '))?;
}
}
if last.is_some() {
queue!(out, ResetColor)?;
}
}
queue!(out, Print("\r\n"))?;
}
out.flush()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::art::Art;
#[test]
fn loader_and_handle_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Loader>();
assert_send_sync::<Handle>();
}
#[test]
fn position_tracks_updates() {
let loader = Loader::builder().total(10).message("x").start();
loader.inc(3);
loader.set(7);
assert_eq!(loader.position(), 7);
loader.finish_and_clear();
}
#[test]
fn iterator_yields_every_item() {
let loader = Loader::builder().total(5).art(Art::parse("##")).start();
let collected: Vec<i32> = (0..5).inkling_with(loader).collect();
assert_eq!(collected, vec![0, 1, 2, 3, 4]);
}
}