use std::io::{IsTerminal, Write, stderr};
use std::thread::{JoinHandle, spawn};
use std::time::{Duration, Instant};
use flume::RecvTimeoutError;
use crate::format::truncate;
pub enum DisplayMessage {
Start(usize, usize), Calibrate(usize),
Idle(usize),
Counters(Vec<String>),
Done,
}
enum WorkerState {
Idle,
Running(usize),
Calibrating,
}
const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', ' '];
const FRAME: Duration = Duration::from_millis(500);
pub fn term_width() -> usize {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
ws.ws_col as usize
} else {
80
}
}
}
fn fit(line: &str, cols: usize) -> String {
line.chars().take(cols.saturating_sub(1)).collect()
}
pub fn spawn_display(
jobs: usize,
command_labels: Vec<String>,
rx: flume::Receiver<DisplayMessage>,
) -> JoinHandle<()> {
spawn(move || {
if !stderr().is_terminal() {
while let Ok(msg) = rx.recv() {
if matches!(msg, DisplayMessage::Done) {
break;
}
}
return;
}
let mut worker_state: Vec<WorkerState> = (0..jobs).map(|_| WorkerState::Idle).collect();
let mut counter_msgs: Vec<String> = command_labels
.iter()
.map(|label| format!("{label}: pending"))
.collect();
let n_lines = worker_state.len() + counter_msgs.len();
let mut frame = 0usize;
let mut first = true;
let mut last_anim = Instant::now();
let draw =
|frame: usize, worker_state: &[WorkerState], counter_msgs: &[String], first: bool| {
let cols = term_width();
let glyph = SPINNER[frame % SPINNER.len()];
let mut out = stderr().lock();
if !first {
let _ = write!(out, "\x1b[{n_lines}A");
}
for (w, state) in worker_state.iter().enumerate() {
let prefix = format!(" {glyph} worker {w}: ");
let line = match state {
WorkerState::Running(idx) => {
let budget = cols.saturating_sub(1 + prefix.chars().count());
format!("{prefix}{}", truncate(&command_labels[*idx], budget))
}
WorkerState::Calibrating => format!("{prefix}<calibration>"),
WorkerState::Idle => format!("{prefix}idle"),
};
let _ = write!(out, "\r\x1b[2K{}\n", fit(&line, cols));
}
for msg in counter_msgs {
let _ = write!(out, "\r\x1b[2K{}\n", fit(&format!(" {msg}"), cols));
}
let _ = out.flush();
};
draw(frame, &worker_state, &counter_msgs, first);
first = false;
loop {
match rx.recv_timeout(FRAME) {
Ok(DisplayMessage::Start(w, idx)) => worker_state[w] = WorkerState::Running(idx),
Ok(DisplayMessage::Calibrate(w)) => worker_state[w] = WorkerState::Calibrating,
Ok(DisplayMessage::Idle(w)) => worker_state[w] = WorkerState::Idle,
Ok(DisplayMessage::Counters(lines)) => counter_msgs = lines,
Ok(DisplayMessage::Done) | Err(RecvTimeoutError::Disconnected) => {
let mut out = stderr().lock();
let _ = write!(out, "\x1b[{n_lines}A\x1b[J");
let _ = out.flush();
break;
}
Err(RecvTimeoutError::Timeout) => {}
}
if last_anim.elapsed() >= FRAME {
frame = frame.wrapping_add(1);
last_anim = Instant::now();
}
draw(frame, &worker_state, &counter_msgs, first);
}
})
}