use std::sync::Mutex;
use std::time::Instant;
mod board;
mod live;
mod row;
pub use board::{Board, Kind, Row, State};
pub use live::{Region, Target};
const TICK: std::time::Duration = std::time::Duration::from_millis(100);
static SESSION: Mutex<Option<Session>> = Mutex::new(None);
static TICKER: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
struct Session {
board: Board,
region: Option<live::Region>,
frame: usize,
name_width: usize,
}
fn live_allowed() -> Option<usize> {
use std::io::IsTerminal;
if !std::io::stderr().is_terminal() || !super::stderr_colored() {
return None;
}
width_from(crate::engine::query::terminal::window_size())
}
fn width_from(size: Option<(u16, u16)>) -> Option<usize> {
size.map(|(_rows, cols)| cols as usize)
}
pub fn begin(resources: impl IntoIterator<Item = (Kind, String)>) {
if !super::progress_enabled() {
return;
}
let board = Board::new(resources);
let name_width = board
.live_rows()
.iter()
.map(|r| r.name.chars().count())
.max()
.unwrap_or(0)
.clamp(12, 40);
let region = live_allowed().map(|_| live::Region::new(live::Target::Stderr));
let live = region.is_some();
if let Ok(mut slot) = SESSION.lock() {
*slot = Some(Session {
board,
region,
frame: 0,
name_width,
});
}
if live {
spawn_ticker();
}
repaint();
}
pub fn start(kind: &str, name: &str, verb: &str) {
let Some(kind) = Kind::from_noun(kind) else {
return;
};
let sink = {
let Ok(mut slot) = SESSION.lock() else {
return;
};
match slot.as_mut() {
Some(session) => {
session.board.start(kind, name, verb, Instant::now());
if session.region.is_some() {
Sink::Live
} else {
Sink::Plain
}
}
None => Sink::None,
}
};
emit(sink, kind, name, verb);
}
enum Sink {
Live,
Plain,
None,
}
fn emit(sink: Sink, kind: Kind, name: &str, verb: &str) {
match sink {
Sink::Live => repaint(),
Sink::Plain => super::write_progress_line(kind.noun(), name, verb),
Sink::None => {
if super::progress_enabled() {
super::write_progress_line(kind.noun(), name, verb);
}
}
}
}
pub(super) fn finish(kind: &str, name: &str, verb: &str) -> bool {
let Some(kind) = Kind::from_noun(kind) else {
return false;
};
let sink = {
let Ok(mut slot) = SESSION.lock() else {
return false;
};
match slot.as_mut() {
Some(session) => {
session.board.finish(kind, name, verb, Instant::now());
if session.region.is_some() {
Sink::Live
} else {
Sink::Plain
}
}
None => Sink::None,
}
};
if matches!(sink, Sink::None) {
return false;
}
emit(sink, kind, name, verb);
true
}
pub fn end() {
if let Ok(mut slot) = TICKER.lock() {
if let Some(handle) = slot.take() {
handle.abort();
}
}
let Ok(mut slot) = SESSION.lock() else {
return;
};
if let Some(mut session) = slot.take() {
if session.region.is_some() {
let width = live_allowed().unwrap_or(0);
let now = Instant::now();
let scrollback: Vec<String> = session
.board
.take_completed_prefix()
.iter()
.map(|r| row::render(r, session.name_width, 0, now, width))
.collect();
let leftover: Vec<String> = session
.board
.live_rows()
.iter()
.map(|r| row::render(r, session.name_width, 0, now, width))
.collect();
if let Some(region) = session.region.as_mut() {
region.show(&scrollback, &leftover);
}
}
}
}
fn repaint() {
let Ok(mut slot) = SESSION.lock() else {
return;
};
let Some(session) = slot.as_mut() else {
return;
};
let frame = session.frame;
let name_width = session.name_width;
let Session { board, region, .. } = session;
let Some(region) = region.as_mut() else {
return;
};
let width = live_allowed().unwrap_or(0);
let now = Instant::now();
let scrollback: Vec<String> = board
.take_completed_prefix()
.iter()
.map(|r| row::render(r, name_width, frame, now, width))
.collect();
let mut lines = Vec::new();
let rows = board.live_rows();
if !rows.is_empty() {
let (done, total) = board.tally();
lines.push(row::summary(done, total));
lines.extend(
rows.iter()
.map(|r| row::render(r, name_width, frame, now, width)),
);
}
region.show(&scrollback, &lines);
}
fn spawn_ticker() {
let handle = tokio::spawn(async {
let mut interval = tokio::time::interval(TICK);
loop {
interval.tick().await;
{
let Ok(mut slot) = SESSION.lock() else { return };
match slot.as_mut() {
Some(session) => session.frame = session.frame.wrapping_add(1),
None => return,
}
}
repaint();
}
});
if let Ok(mut slot) = TICKER.lock() {
*slot = Some(handle);
}
}
#[cfg(test)]
mod tests {
use super::width_from;
#[test]
fn the_width_is_the_columns_not_the_rows() {
assert_eq!(width_from(Some((30, 100))), Some(100));
assert_eq!(width_from(None), None);
}
}