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 (cols.saturating_sub(w) / 2, vr.saturating_sub(h + 1) / 2)
389 } else {
390 let _ = execute!(out, Hide);
391 (0, 0)
392 };
393
394 let frame = Duration::from_millis(1000 / FPS);
395 let start = Instant::now();
396 let mut displayed = 0.0f32;
397 let mut first = true;
398
399 loop {
400 let finishing = shared.state.load(Acquire) != RUNNING;
401 let total = shared.total.load(Relaxed);
402 let pos = shared.pos.load(Relaxed);
403 let t = start.elapsed().as_secs_f32();
404 let target = if total == 0 {
405 0.1 + 0.9 * (0.5 - 0.5 * (t * 1.5).cos()) } else {
407 (pos as f32 / total as f32).clamp(0.0, 1.0)
408 };
409 displayed += (target - displayed) * 0.3; let progress = if finishing { 1.0 } else { displayed };
411
412 let _ = if fullscreen {
413 draw_frame(&mut out, &shared, ox, oy, progress, t)
414 } else {
415 draw_inline(&mut out, &shared, progress, t, first)
416 };
417 first = false;
418
419 if finishing {
420 let cleared = shared.state.load(Relaxed) == FINISH_CLEAR;
421 if fullscreen {
422 let _ = execute!(out, ResetColor, Show, LeaveAlternateScreen);
423 if !cleared {
424 let _ = persist_final(&mut out, &shared);
425 }
426 } else if cleared {
427 let _ = clear_inline(&mut out, h + 1);
428 let _ = execute!(out, Show);
429 } else {
430 let _ = queue!(out, Print("\r\n"));
432 let _ = execute!(out, Show);
433 }
434 let _ = out.flush();
435 break;
436 }
437 thread::sleep(frame);
438 }
439}
440
441fn draw_frame(
442 out: &mut io::Stdout,
443 shared: &Shared,
444 ox: u16,
445 oy: u16,
446 progress: f32,
447 t: f32,
448) -> io::Result<()> {
449 let art = &shared.art;
450 let (w, h) = (art.width(), art.height());
451 let style = &shared.style;
452
453 for y in 0..h {
454 queue!(out, MoveTo(ox, oy + y))?;
455 let mut last: Option<(u8, u8, u8)> = None;
456 for x in 0..w {
457 match shared.ranks.rank_at(x, y) {
458 Some(r) if r <= progress => {
459 if style.color {
460 let c = crate::render::cell_rgb(style, progress, r, x, y, t);
461 if last != Some(c) {
462 queue!(
463 out,
464 SetForegroundColor(Color::Rgb {
465 r: c.0,
466 g: c.1,
467 b: c.2
468 })
469 )?;
470 last = Some(c);
471 }
472 }
473 queue!(out, Print(art.glyph(x, y)))?;
474 }
475 _ => {
476 if last.take().is_some() {
477 queue!(out, ResetColor)?;
478 }
479 queue!(out, Print(' '))?;
480 }
481 }
482 }
483 if last.is_some() {
484 queue!(out, ResetColor)?;
485 }
486 }
487
488 queue!(out, MoveTo(ox, oy + h), Clear(ClearType::CurrentLine))?;
490 let msg = shared
491 .message
492 .lock()
493 .ok()
494 .map(|m| m.clone())
495 .unwrap_or_default();
496 if !msg.is_empty() {
497 let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
498 let shown: String = msg.chars().take(cols.saturating_sub(1) as usize).collect();
499 if style.color {
500 queue!(
501 out,
502 SetForegroundColor(Color::Rgb {
503 r: 120,
504 g: 134,
505 b: 168
506 })
507 )?;
508 }
509 queue!(out, Print(shown), ResetColor)?;
510 }
511 out.flush()
512}
513
514fn draw_inline(
517 out: &mut io::Stdout,
518 shared: &Shared,
519 progress: f32,
520 t: f32,
521 first: bool,
522) -> io::Result<()> {
523 let art = &shared.art;
524 let (w, h) = (art.width(), art.height());
525 let style = &shared.style;
526
527 if !first {
528 queue!(out, MoveToPreviousLine(h))?;
529 }
530 for y in 0..h {
531 queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
532 let mut last: Option<(u8, u8, u8)> = None;
533 for x in 0..w {
534 match shared.ranks.rank_at(x, y) {
535 Some(r) if r <= progress => {
536 if style.color {
537 let c = crate::render::cell_rgb(style, progress, r, x, y, t);
538 if last != Some(c) {
539 queue!(
540 out,
541 SetForegroundColor(Color::Rgb {
542 r: c.0,
543 g: c.1,
544 b: c.2
545 })
546 )?;
547 last = Some(c);
548 }
549 }
550 queue!(out, Print(art.glyph(x, y)))?;
551 }
552 _ => {
553 if last.take().is_some() {
554 queue!(out, ResetColor)?;
555 }
556 queue!(out, Print(' '))?;
557 }
558 }
559 }
560 if last.is_some() {
561 queue!(out, ResetColor)?;
562 }
563 queue!(out, MoveToNextLine(1))?;
564 }
565
566 queue!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
568 let msg = shared
569 .message
570 .lock()
571 .ok()
572 .map(|m| m.clone())
573 .unwrap_or_default();
574 if !msg.is_empty() {
575 let cols = terminal::size().map(|(c, _)| c).unwrap_or(80);
576 let shown: String = msg.chars().take(cols.saturating_sub(1) as usize).collect();
577 if style.color {
578 queue!(
579 out,
580 SetForegroundColor(Color::Rgb {
581 r: 120,
582 g: 134,
583 b: 168
584 })
585 )?;
586 }
587 queue!(out, Print(shown), ResetColor)?;
588 }
589 out.flush()
590}
591
592fn clear_inline(out: &mut io::Stdout, lines: u16) -> io::Result<()> {
594 queue!(out, MoveToPreviousLine(lines - 1))?;
595 for _ in 0..lines {
596 queue!(
597 out,
598 MoveToColumn(0),
599 Clear(ClearType::CurrentLine),
600 MoveToNextLine(1)
601 )?;
602 }
603 queue!(out, MoveToPreviousLine(lines))?;
604 out.flush()
605}
606
607fn persist_final(out: &mut io::Stdout, shared: &Shared) -> io::Result<()> {
609 let art = &shared.art;
610 let (w, h) = (art.width(), art.height());
611 let style = &shared.style;
612 for y in 0..h {
613 let mut last_ink = 0u16;
614 let mut any = false;
615 for x in 0..w {
616 if art.is_ink(x, y) {
617 last_ink = x;
618 any = true;
619 }
620 }
621 if any {
622 let mut last: Option<(u8, u8, u8)> = None;
623 for x in 0..=last_ink {
624 if art.is_ink(x, y) {
625 if style.color {
626 let c = crate::render::cell_rgb(style, 1.0, 0.0, x, y, 0.0);
627 if last != Some(c) {
628 queue!(
629 out,
630 SetForegroundColor(Color::Rgb {
631 r: c.0,
632 g: c.1,
633 b: c.2
634 })
635 )?;
636 last = Some(c);
637 }
638 }
639 queue!(out, Print(art.glyph(x, y)))?;
640 } else {
641 if last.take().is_some() {
642 queue!(out, ResetColor)?;
643 }
644 queue!(out, Print(' '))?;
645 }
646 }
647 if last.is_some() {
648 queue!(out, ResetColor)?;
649 }
650 }
651 queue!(out, Print("\r\n"))?;
652 }
653 out.flush()
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::art::Art;
660
661 #[test]
662 fn loader_and_handle_are_send_sync() {
663 fn assert_send_sync<T: Send + Sync>() {}
664 assert_send_sync::<Loader>();
665 assert_send_sync::<Handle>();
666 }
667
668 #[test]
669 fn position_tracks_updates() {
670 let loader = Loader::builder().total(10).message("x").start();
671 loader.inc(3);
672 loader.set(7);
673 assert_eq!(loader.position(), 7);
674 loader.finish_and_clear();
675 }
676
677 #[test]
678 fn iterator_yields_every_item() {
679 let loader = Loader::builder().total(5).art(Art::parse("##")).start();
680 let collected: Vec<i32> = (0..5).inkling_with(loader).collect();
681 assert_eq!(collected, vec![0, 1, 2, 3, 4]);
682 }
683}