use crate::git::{Progress, Stage};
use ratatui::crossterm::terminal;
use std::io::{self, IsTerminal, Write};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::thread;
use std::time::Duration;
const GRACE: Duration = Duration::from_millis(150);
const REDRAW: Duration = Duration::from_millis(60);
const BAR: usize = 20;
const MARK: [&str; 2] = ["⡎⠉ ⡇⠀ ⡇⢸ ⡇⢸ ⠉⡹", "⣈⡱ ⣇⣀ ⢇⡸ ⢇⡸ ⣎⣀"];
const SCRAMBLED: &str = "\x1b[2;90m";
const LOCKED: &str = "\x1b[1;96m";
const OFF: &str = "\x1b[0m";
pub fn while_loading<T>(work: impl FnOnce(&Progress) -> T) -> T {
let progress = Progress::default();
if !io::stderr().is_terminal() {
return work(&progress);
}
let (stop, stopped) = mpsc::channel::<()>();
thread::scope(|scope| {
let shared = &progress;
scope.spawn(move || draw_until(&stopped, shared));
let out = work(shared);
drop(stop);
out
})
}
fn draw_until(stopped: &Receiver<()>, progress: &Progress) {
if !matches!(stopped.recv_timeout(GRACE), Err(RecvTimeoutError::Timeout)) {
return;
}
let mut err = io::stderr();
let mut noise = 0x2545_F491_4F6C_DD1D_u64;
let mut up = "";
loop {
let (line, share) = progress_line(progress.stage());
let mark = wordmark(share, &mut noise);
write!(err, "{up}\r{mark}\r\x1b[K{line}").ok();
err.flush().ok();
up = "\x1b[2A";
if !matches!(stopped.recv_timeout(REDRAW), Err(RecvTimeoutError::Timeout)) {
break;
}
}
write!(err, "\x1b[2A\r\x1b[J").ok();
err.flush().ok();
}
fn progress_line(stage: Stage) -> (String, f64) {
let (line, share) = match stage {
Stage::Finding(found) => (format!("finding repos ... {found}"), 0.0),
Stage::Working(done, total) => {
let full = (done * BAR).checked_div(total).unwrap_or(BAR);
let bar: String = (0..BAR).map(|i| if i < full { '#' } else { '-' }).collect();
(
format!("reading repos [{bar}] {done}/{total}"),
full as f64 / BAR as f64,
)
}
};
let cols = terminal::size().map_or(80, |(cols, _)| usize::from(cols));
(line.chars().take(cols.saturating_sub(1)).collect(), share)
}
fn wordmark(share: f64, noise: &mut u64) -> String {
let cols = MARK[0].chars().count();
let locked = (share * cols as f64).round() as usize;
let mut out = String::new();
for row in MARK {
for (i, ch) in row.chars().enumerate() {
if ch == ' ' || i < locked {
out.push_str(LOCKED);
out.push(ch);
} else {
out.push_str(SCRAMBLED);
out.push(scramble(noise));
}
out.push_str(OFF);
}
out.push_str("\x1b[K\n");
}
out
}
fn scramble(noise: &mut u64) -> char {
*noise ^= *noise << 13;
*noise ^= *noise >> 7;
*noise ^= *noise << 17;
char::from_u32(0x2800 + (*noise % 64) as u32).unwrap_or('⠿')
}