use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use crate::pipeline::{Phase, human_bytes};
use crate::render::count;
const FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const TICK: std::time::Duration = std::time::Duration::from_millis(80);
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Plain,
Animated,
}
pub struct Progress {
mode: Mode,
label: Arc<Mutex<String>>,
running: Arc<AtomicBool>,
ticker: Option<JoinHandle<()>>,
}
impl Progress {
fn plain() -> Self {
Self {
mode: Mode::Plain,
label: Arc::new(Mutex::new(String::new())),
running: Arc::new(AtomicBool::new(false)),
ticker: None,
}
}
pub fn stderr() -> Self {
if !std::io::stderr().is_terminal() {
return Self::plain();
}
let label = Arc::new(Mutex::new(String::new()));
let running = Arc::new(AtomicBool::new(true));
let ticker = {
let (label, running) = (Arc::clone(&label), Arc::clone(&running));
std::thread::spawn(move || {
let mut frame = 0usize;
while running.load(Ordering::Relaxed) {
{
let text = lock(&label);
if !text.is_empty() {
let mut err = std::io::stderr().lock();
let _ = write!(err, "\r\x1b[2K{} {text}", FRAMES[frame % FRAMES.len()]);
let _ = err.flush();
}
}
frame += 1;
std::thread::sleep(TICK);
}
})
};
Self {
mode: Mode::Animated,
label,
running,
ticker: Some(ticker),
}
}
pub fn start(&self, label: impl Into<String>) {
match self.mode {
Mode::Plain => eprintln!(" {}...", label.into()),
Mode::Animated => *lock(&self.label) = label.into(),
}
}
pub fn finish(&self, line: impl std::fmt::Display) {
match self.mode {
Mode::Plain => eprintln!(" {line}"),
Mode::Animated => {
let mut label = lock(&self.label);
label.clear();
let mut err = std::io::stderr().lock();
let _ = writeln!(err, "\r\x1b[2K{line}");
let _ = err.flush();
}
}
}
}
impl Drop for Progress {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(ticker) = self.ticker.take() {
let _ = ticker.join();
}
if self.mode == Mode::Animated {
let mut err = std::io::stderr().lock();
let _ = write!(err, "\r\x1b[2K");
let _ = err.flush();
}
}
}
pub fn render(progress: &Progress, phase: Phase) {
match phase {
Phase::Scanning => progress.start("scanning the corpus"),
Phase::Scanned {
files,
changed,
removed,
} => {
if changed == 0 && removed == 0 {
progress.start(format!("scanned {}, none changed", count(files, "file")));
} else {
progress.start(format!(
"scanned {}, {changed} changed, {removed} removed",
count(files, "file")
));
}
}
Phase::Extracting { files } => {
progress.start(format!("extracting {}", count(files, "file")))
}
Phase::Resolving { files } => progress.start(format!("resolving {}", count(files, "file"))),
Phase::ScipStale => progress
.finish("! SCIP index is older than the newest source file — rerun `sinter scip`"),
Phase::Ready {
nodes,
edges,
elapsed,
} => progress.finish(format!(
"\u{2713} graph ready: {}, {} ({elapsed:.1?})",
count(nodes as usize, "symbol"),
count(edges as usize, "edge"),
)),
Phase::Compacting { before } => progress.start(format!(
"compacting {} (optional maintenance — the graph is already saved)",
human_bytes(before)
)),
Phase::Compacted { before, after } => {
let saved = before.saturating_sub(after);
let pct = if before > 0 {
saved as f64 / before as f64 * 100.0
} else {
0.0
};
progress.finish(format!(
"\u{2713} compacted: {} \u{2192} {} (-{pct:.0}%)",
human_bytes(before),
human_bytes(after)
));
}
}
}
fn lock(label: &Mutex<String>) -> std::sync::MutexGuard<'_, String> {
label.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_terminal_stderr_is_plain() {
let p = Progress::stderr();
assert!(p.mode == Mode::Plain, "expected plain mode off a terminal");
assert!(p.ticker.is_none());
}
}