1use std::io::{self, IsTerminal, Read, Write};
20use std::sync::atomic::Ordering::{AcqRel, Acquire, Relaxed};
21use std::sync::atomic::{AtomicU64, AtomicU8};
22use std::sync::{Arc, Mutex};
23use std::thread::{self, JoinHandle};
24use std::time::{Duration, Instant};
25
26use crossterm::{
27 cursor::{Hide, MoveTo, MoveToColumn, MoveToNextLine, MoveToPreviousLine, Show},
28 execute, queue,
29 style::{Color, Print, ResetColor, SetForegroundColor},
30 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
31};
32
33use crate::art::Art;
34use crate::ordering::{Directional, Ordering};
35use crate::render::Style;
36
37const DEFAULT_ART: &str = include_str!("../assets/dragon.txt");
39const FPS: u64 = 30;
40
41const RUNNING: u8 = 0;
43const FINISH_KEEP: u8 = 1; const FINISH_CLEAR: u8 = 2; struct Shared {
48 pos: AtomicU64,
49 total: AtomicU64, state: AtomicU8,
51 message: Mutex<String>,
52 art: Art,
53 ranks: crate::rank::RankMap,
54 style: Style,
55}
56
57impl Shared {
58 fn inc(&self, delta: u64) {
59 self.pos.fetch_add(delta, Relaxed);
60 }
61 fn set(&self, pos: u64) {
62 self.pos.store(pos, Relaxed);
63 }
64 fn set_message(&self, msg: String) {
65 if let Ok(mut guard) = self.message.lock() {
66 *guard = msg;
67 }
68 }
69}
70
71pub struct Loader {
77 shared: Arc<Shared>,
78 joiner: Mutex<Option<JoinHandle<()>>>,
79 tty: bool,
80}
81
82impl Loader {
83 pub fn new(total: u64) -> Self {
85 Builder::new().total(total).start()
86 }
87
88 pub fn spinner() -> Self {
90 Builder::new().start()
91 }
92
93 pub fn builder() -> Builder {
95 Builder::new()
96 }
97
98 pub fn inc(&self, delta: u64) {
100 self.shared.inc(delta);
101 }
102
103 pub fn set(&self, pos: u64) {
105 self.shared.set(pos);
106 }
107
108 pub fn set_length(&self, total: u64) {
110 self.shared.total.store(total, Relaxed);
111 }
112
113 pub fn set_message<S: Into<String>>(&self, msg: S) {
115 self.shared.set_message(msg.into());
116 }
117
118 pub fn position(&self) -> u64 {
120 self.shared.pos.load(Relaxed)
121 }
122
123 pub fn handle(&self) -> Handle {
126 Handle {
127 shared: Arc::clone(&self.shared),
128 }
129 }
130
131 pub fn wrap_read<R: Read>(&self, reader: R) -> ProgressReader<R> {
134 ProgressReader {
135 inner: reader,
136 handle: self.handle(),
137 }
138 }
139
140 pub fn finish(&self) {
142 self.finalize(FINISH_KEEP);
143 }
144
145 pub fn finish_and_clear(&self) {
147 self.finalize(FINISH_CLEAR);
148 }
149
150 fn finalize(&self, how: u8) {
151 let won = self
152 .shared
153 .state
154 .compare_exchange(RUNNING, how, AcqRel, Relaxed)
155 .is_ok();
156 if self.tty {
157 if let Ok(mut guard) = self.joiner.lock() {
158 if let Some(handle) = guard.take() {
159 let _ = handle.join();
160 }
161 }
162 } else if won && how == FINISH_KEEP {
163 print!(
165 "{}",
166 crate::frame::to_string(&self.shared.art, &self.shared.ranks, 1.0)
167 );
168 let _ = io::stdout().flush();
169 }
170 }
171}
172
173impl Drop for Loader {
174 fn drop(&mut self) {
175 self.finalize(FINISH_KEEP);
176 }
177}
178
179#[derive(Clone)]
182pub struct Handle {
183 shared: Arc<Shared>,
184}
185
186impl Handle {
187 pub fn inc(&self, delta: u64) {
189 self.shared.inc(delta);
190 }
191 pub fn set(&self, pos: u64) {
193 self.shared.set(pos);
194 }
195 pub fn set_message<S: Into<String>>(&self, msg: S) {
197 self.shared.set_message(msg.into());
198 }
199 pub fn position(&self) -> u64 {
201 self.shared.pos.load(Relaxed)
202 }
203}
204
205pub struct Builder {
207 total: u64,
208 art: Option<Art>,
209 ordering: Box<dyn Ordering>,
210 style: Style,
211 message: String,
212}
213
214impl Builder {
215 fn new() -> Self {
216 Builder {
217 total: 0,
218 art: None,
219 ordering: Box::new(Directional::default()),
220 style: Style::default(),
221 message: String::new(),
222 }
223 }
224
225 pub fn total(mut self, total: u64) -> Self {
227 self.total = total;
228 self
229 }
230
231 pub fn art(mut self, art: Art) -> Self {
233 self.art = Some(art);
234 self
235 }
236
237 pub fn ordering(mut self, ordering: impl Ordering + 'static) -> Self {
239 self.ordering = Box::new(ordering);
240 self
241 }
242
243 pub fn style(mut self, style: Style) -> Self {
245 self.style = style;
246 self
247 }
248
249 pub fn message<S: Into<String>>(mut self, message: S) -> Self {
251 self.message = message.into();
252 self
253 }
254
255 pub fn start(self) -> Loader {
257 let art = self.art.unwrap_or_else(|| Art::parse(DEFAULT_ART));
258 let ranks = self.ordering.rank(&art);
259 let shared = Arc::new(Shared {
260 pos: AtomicU64::new(0),
261 total: AtomicU64::new(self.total),
262 state: AtomicU8::new(RUNNING),
263 message: Mutex::new(self.message),
264 art,
265 ranks,
266 style: self.style,
267 });
268 let tty = io::stdout().is_terminal();
269 let joiner = if tty {
270 let shared = Arc::clone(&shared);
271 Mutex::new(Some(thread::spawn(move || run(shared))))
272 } else {
273 Mutex::new(None)
274 };
275 Loader {
276 shared,
277 joiner,
278 tty,
279 }
280 }
281}
282
283pub trait ProgressIteratorExt: Iterator + Sized {
289 fn inkling(self) -> InklingIter<Self> {
291 let total = self.size_hint().1.unwrap_or(0) as u64;
292 let loader = if total > 0 {
293 Loader::new(total)
294 } else {
295 Loader::spinner()
296 };
297 InklingIter {
298 inner: self,
299 loader: Some(loader),
300 }
301 }
302
303 fn inkling_with(self, loader: Loader) -> InklingIter<Self> {
305 InklingIter {
306 inner: self,
307 loader: Some(loader),
308 }
309 }
310}
311
312impl<I: Iterator> ProgressIteratorExt for I {}
313
314pub struct InklingIter<I> {
316 inner: I,
317 loader: Option<Loader>,
318}
319
320impl<I: Iterator> Iterator for InklingIter<I> {
321 type Item = I::Item;
322
323 fn next(&mut self) -> Option<Self::Item> {
324 let next = self.inner.next();
325 match next {
326 Some(_) => {
327 if let Some(loader) = &self.loader {
328 loader.inc(1);
329 }
330 }
331 None => {
332 if let Some(loader) = self.loader.take() {
333 loader.finish();
334 }
335 }
336 }
337 next
338 }
339
340 fn size_hint(&self) -> (usize, Option<usize>) {
341 self.inner.size_hint()
342 }
343}
344
345impl<I> Drop for InklingIter<I> {
346 fn drop(&mut self) {
347 if let Some(loader) = self.loader.take() {
348 loader.finish();
349 }
350 }
351}
352
353pub struct ProgressReader<R> {
359 inner: R,
360 handle: Handle,
361}
362
363impl<R: Read> Read for ProgressReader<R> {
364 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
365 let n = self.inner.read(buf)?;
366 self.handle.inc(n as u64);
367 Ok(n)
368 }
369}
370
371fn run(shared: Arc<Shared>) {
376 let mut out = io::stdout();
377 let (w, h) = (shared.art.width(), shared.art.height());
378 let rows = terminal::size().map(|(_, r)| r).unwrap_or(0);
379 let fullscreen = rows < h + 2;
384
385 let (ox, oy) = if fullscreen {
386 let (cols, vr) = terminal::size().unwrap_or((w, h + 2));
387 let _ = execute!(out, EnterAlternateScreen, Hide, Clear(ClearType::All));
388 (
389 cols.saturating_sub(crate::render::art_cols(&shared.art)) / 2,
390 vr.saturating_sub(h + 1) / 2,
391 )
392 } else {
393 let _ = execute!(out, Hide);
394 (0, 0)
395 };
396
397 let frame = Duration::from_millis(1000 / FPS);
398 let start = Instant::now();
399 let mut displayed = 0.0f32;
400 let mut first = true;
401
402 loop {
403 let finishing = shared.state.load(Acquire) != RUNNING;
404 let total = shared.total.load(Relaxed);
405 let pos = shared.pos.load(Relaxed);
406 let t = start.elapsed().as_secs_f32();
407 let target = if total == 0 {
408 0.1 + 0.9 * (0.5 - 0.5 * (t * 1.5).cos()) } else {
410 (pos as f32 / total as f32).clamp(0.0, 1.0)
411 };
412 displayed += (target - displayed) * 0.3; let progress = if finishing { 1.0 } else { displayed };
414
415 let _ = if fullscreen {
416 draw_frame(&mut out, &shared, ox, oy, progress, t)
417 } else {
418 draw_inline(&mut out, &shared, progress, t, first)
419 };
420 first = false;
421
422 if finishing {
423 let cleared = shared.state.load(Relaxed) == FINISH_CLEAR;
424 if fullscreen {
425 let _ = execute!(out, ResetColor, Show, LeaveAlternateScreen);
426 if !cleared {
427 let _ = persist_final(&mut out, &shared);
428 }
429 } else if cleared {
430 let _ = clear_inline(&mut out, h + 1);
431 let _ = execute!(out, Show);
432 } else {
433 let _ = queue!(out, Print("\r\n"));
435 let _ = execute!(out, Show);
436 }
437 let _ = out.flush();
438 break;
439 }
440 thread::sleep(frame);
441 }
442}
443
444fn draw_frame(
445 out: &mut io::Stdout,
446 shared: &Shared,
447 ox: u16,
448 oy: u16,
449 progress: f32,
450 t: f32,
451) -> io::Result<()> {
452 let art = &shared.art;
453 let (w, h) = (art.width(), art.height());
454 let style = &shared.style;
455
456 queue!(out, Print(crate::render::SYNC_BEGIN))?;
457 for y in 0..h {
458 queue!(out, MoveTo(ox, oy + y))?;
459 let mut last: Option<(u8, u8, u8)> = None;
460 for x in 0..w {
461 match shared.ranks.rank_at(x, y) {
462 Some(r) if r <= progress => {
463 if style.color {
464 let c = crate::render::cell_rgb(style, progress, r, x, y, t);
465 if last != Some(c) {
466 queue!(
467 out,
468 SetForegroundColor(Color::Rgb {
469 r: c.0,
470 g: c.1,
471 b: c.2
472 })
473 )?;
474 last = Some(c);
475 }
476 }
477 queue!(out, Print(art.glyph(x, y)))?;
478 }
479 _ => {
480 if last.take().is_some() {
481 queue!(out, ResetColor)?;
482 }
483 queue!(out, Print(' '))?;
484 }
485 }
486 }
487 if last.is_some() {
488 queue!(out, ResetColor)?;
489 }
490 }
491
492 queue!(out, MoveTo(ox, oy + h), Clear(ClearType::CurrentLine))?;
494 let msg = shared
495 .message
496 .lock()
497 .ok()
498 .map(|m| m.clone())
499 .unwrap_or_default();
500 if !msg.is_empty() {
501 let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
502 let shown = crate::render::truncate_to_cols(&msg, cols.saturating_sub(1));
503 if style.color {
504 queue!(
505 out,
506 SetForegroundColor(Color::Rgb {
507 r: 120,
508 g: 134,
509 b: 168
510 })
511 )?;
512 }
513 queue!(out, Print(shown), ResetColor)?;
514 }
515 queue!(out, Print(crate::render::SYNC_END))?;
516 out.flush()
517}
518
519fn draw_inline(
522 out: &mut io::Stdout,
523 shared: &Shared,
524 progress: f32,
525 t: f32,
526 first: bool,
527) -> io::Result<()> {
528 let art = &shared.art;
529 let (w, h) = (art.width(), art.height());
530 let style = &shared.style;
531
532 queue!(out, Print(crate::render::SYNC_BEGIN))?;
533 if !first {
534 queue!(out, MoveToPreviousLine(h))?;
535 }
536 for y in 0..h {
537 queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
538 let mut last: Option<(u8, u8, u8)> = None;
539 for x in 0..w {
540 match shared.ranks.rank_at(x, y) {
541 Some(r) if r <= progress => {
542 if style.color {
543 let c = crate::render::cell_rgb(style, progress, r, x, y, t);
544 if last != Some(c) {
545 queue!(
546 out,
547 SetForegroundColor(Color::Rgb {
548 r: c.0,
549 g: c.1,
550 b: c.2
551 })
552 )?;
553 last = Some(c);
554 }
555 }
556 queue!(out, Print(art.glyph(x, y)))?;
557 }
558 _ => {
559 if last.take().is_some() {
560 queue!(out, ResetColor)?;
561 }
562 queue!(out, Print(' '))?;
563 }
564 }
565 }
566 if last.is_some() {
567 queue!(out, ResetColor)?;
568 }
569 queue!(out, MoveToNextLine(1))?;
570 }
571
572 queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
574 let msg = shared
575 .message
576 .lock()
577 .ok()
578 .map(|m| m.clone())
579 .unwrap_or_default();
580 if !msg.is_empty() {
581 let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
582 let shown = crate::render::truncate_to_cols(&msg, cols.saturating_sub(1));
583 if style.color {
584 queue!(
585 out,
586 SetForegroundColor(Color::Rgb {
587 r: 120,
588 g: 134,
589 b: 168
590 })
591 )?;
592 }
593 queue!(out, Print(shown), ResetColor)?;
594 }
595 queue!(out, Print(crate::render::SYNC_END))?;
596 out.flush()
597}
598
599fn clear_inline(out: &mut io::Stdout, lines: u16) -> io::Result<()> {
601 queue!(out, MoveToPreviousLine(lines - 1))?;
602 for _ in 0..lines {
603 queue!(
604 out,
605 MoveToColumn(0),
606 Clear(ClearType::CurrentLine),
607 MoveToNextLine(1)
608 )?;
609 }
610 queue!(out, MoveToPreviousLine(lines))?;
611 out.flush()
612}
613
614fn persist_final(out: &mut io::Stdout, shared: &Shared) -> io::Result<()> {
616 let art = &shared.art;
617 let (w, h) = (art.width(), art.height());
618 let style = &shared.style;
619 for y in 0..h {
620 let mut last_ink = 0u16;
621 let mut any = false;
622 for x in 0..w {
623 if art.is_ink(x, y) {
624 last_ink = x;
625 any = true;
626 }
627 }
628 if any {
629 let mut last: Option<(u8, u8, u8)> = None;
630 for x in 0..=last_ink {
631 if art.is_ink(x, y) {
632 if style.color {
633 let c = crate::render::cell_rgb(style, 1.0, 0.0, x, y, 0.0);
634 if last != Some(c) {
635 queue!(
636 out,
637 SetForegroundColor(Color::Rgb {
638 r: c.0,
639 g: c.1,
640 b: c.2
641 })
642 )?;
643 last = Some(c);
644 }
645 }
646 queue!(out, Print(art.glyph(x, y)))?;
647 } else {
648 if last.take().is_some() {
649 queue!(out, ResetColor)?;
650 }
651 queue!(out, Print(' '))?;
652 }
653 }
654 if last.is_some() {
655 queue!(out, ResetColor)?;
656 }
657 }
658 queue!(out, Print("\r\n"))?;
659 }
660 out.flush()
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use crate::art::Art;
667
668 #[test]
669 fn loader_and_handle_are_send_sync() {
670 fn assert_send_sync<T: Send + Sync>() {}
671 assert_send_sync::<Loader>();
672 assert_send_sync::<Handle>();
673 }
674
675 #[test]
676 fn position_tracks_updates() {
677 let loader = Loader::builder().total(10).message("x").start();
678 loader.inc(3);
679 loader.set(7);
680 assert_eq!(loader.position(), 7);
681 loader.finish_and_clear();
682 }
683
684 #[test]
685 fn iterator_yields_every_item() {
686 let loader = Loader::builder().total(5).art(Art::parse("##")).start();
687 let collected: Vec<i32> = (0..5).inkling_with(loader).collect();
688 assert_eq!(collected, vec![0, 1, 2, 3, 4]);
689 }
690}