pub(super) mod ba;
pub(super) mod error;
mod steady;
pub(super) mod guard;
#[cfg(any(feature = "signals_sigint", feature = "signals_sigwinch"))]
pub(super) mod signals;
use crate::{
ansi::{
AnsiColor,
NoAnsi,
},
Msg,
MsgKind,
ProglessError,
ProglessTaskGuard,
};
use dactyl::{
NiceClock,
NiceElapsed,
NicePercent,
NiceU32,
traits::{
NiceInflection,
SaturatingFrom,
},
};
use fyi_ansi::csi;
use std::{
collections::BTreeSet,
io::{
IoSlice,
StderrLock,
Write,
},
num::{
NonZeroU8,
NonZeroU16,
NonZeroU32,
NonZeroU64,
NonZeroUsize,
NonZeroU128,
},
sync::{
Arc,
Mutex,
atomic::{
AtomicU8,
AtomicU16,
AtomicU32,
Ordering::SeqCst,
},
},
time::{
Duration,
Instant,
},
};
#[cfg(feature = "signals_sigint")] use std::sync::Once;
use steady::ProglessSteady;
static BAR_DONE: [u8; 256] = [b'#'; 256];
static BAR_UNDONE: [u8; 256] = [b'-'; 256];
const CLS: &[u8] = b"\x1b[J";
#[cfg(feature = "signals_sigint")]
static PRINTED_SIGINT: Once = Once::new();
macro_rules! mutex {
($m:expr) => ($m.lock().unwrap_or_else(std::sync::PoisonError::into_inner));
}
use mutex;
const TICK_NEW: u8 =
TICK_BAR | TICK_TOTAL | TICKING;
const TICK_PERCENT: u8 =
TICK_DONE | TICK_TOTAL;
const TICK_RESET: u8 =
TICK_BAR | TICK_DOING | TICK_DONE | TICK_TITLE | TICK_TOTAL | TICKING;
const TICK_RESIZED: u8 =
TICK_BAR | TICK_DOING | TICK_TITLE;
const TICK_DRAWABLE: u8 =
TICK_BAR | TICK_DOING | TICK_DONE | TICK_TITLE | TICK_TOTAL;
const TICK_BAR: u8 = 0b0000_0001;
const TICK_DOING: u8 = 0b0000_0010;
const TICK_DONE: u8 = 0b0000_0100;
const TICK_TITLE: u8 = 0b0000_1000;
const TICK_TOTAL: u8 = 0b0001_0000;
const TICKING: u8 = 0b0010_0000;
#[cfg(feature = "signals_sigint")]
const SIGINT: u8 = 0b0100_0000;
const MIN_BARS_WIDTH: u8 = 10;
const MIN_DRAW_WIDTH: u8 = 10;
#[derive(Debug)]
struct ProglessInner {
buf: Mutex<ProglessBuffer>,
flags: AtomicU8,
last_size: AtomicU16,
cycle: AtomicU8,
started: Instant,
elapsed: AtomicU32,
title: Mutex<Option<Msg>>,
done_total: Mutex<ProglessDoneTotal>,
doing: Mutex<BTreeSet<String>>,
}
impl Default for ProglessInner {
#[inline]
fn default() -> Self {
Self {
buf: Mutex::new(ProglessBuffer::DEFAULT),
flags: AtomicU8::new(0),
last_size: AtomicU16::new(0),
cycle: AtomicU8::new(0),
started: Instant::now(),
elapsed: AtomicU32::new(0),
title: Mutex::new(None),
done_total: Mutex::new(ProglessDoneTotal::DEFAULT),
doing: Mutex::new(BTreeSet::default()),
}
}
}
impl From<NonZeroU32> for ProglessInner {
#[inline]
fn from(total: NonZeroU32) -> Self {
Self {
flags: AtomicU8::new(TICK_NEW),
done_total: Mutex::new(ProglessDoneTotal::new(total)),
..Self::default()
}
}
}
macro_rules! inner_nz_from {
($($ty:ty),+ $(,)?) => ($(
impl From<$ty> for ProglessInner {
#[inline]
fn from(total: $ty) -> Self {
Self {
flags: AtomicU8::new(TICK_NEW),
done_total: Mutex::new(ProglessDoneTotal::new(NonZeroU32::from(total))),
..Self::default()
}
}
}
)+)
}
inner_nz_from!(NonZeroU8, NonZeroU16);
macro_rules! inner_nz_tryfrom {
($($ty:ty),+ $(,)?) => ($(
impl TryFrom<$ty> for ProglessInner {
type Error = ProglessError;
#[inline]
#[allow(
clippy::allow_attributes,
trivial_numeric_casts,
reason = "We don't need another goddamn macro. Haha.",
)]
#[allow(clippy::cast_possible_truncation, reason = "We're checking for fit.")]
fn try_from(total: $ty) -> Result<Self, Self::Error> {
if let Some(total) = u32::try_from(total.get()).ok().and_then(NonZeroU32::new) {
Ok(Self {
flags: AtomicU8::new(TICK_NEW),
done_total: Mutex::new(ProglessDoneTotal::new(total)),
..Self::default()
})
}
else { Err(ProglessError::TotalOverflow) }
}
}
)+)
}
inner_nz_tryfrom!(NonZeroU64, NonZeroUsize, NonZeroU128);
macro_rules! inner_tryfrom {
($($ty:ty),+ $(,)?) => ($(
impl TryFrom<$ty> for ProglessInner {
type Error = ProglessError;
#[inline]
fn try_from(total: $ty) -> Result<Self, Self::Error> {
u32::try_from(total)
.map_err(|_| ProglessError::TotalOverflow)
.and_then(Self::try_from)
}
}
)+)
}
inner_tryfrom!(
u8, u16, u64, usize, u128,
i8, i16, i32, i64, isize, i128,
);
impl TryFrom<u32> for ProglessInner {
type Error = ProglessError;
#[inline]
fn try_from(total: u32) -> Result<Self, Self::Error> {
NonZeroU32::new(total)
.ok_or(ProglessError::EmptyTotal)
.map(Self::from)
}
}
impl ProglessInner {
fn stop(&self) {
let flags = self.flags.swap(0, SeqCst);
if TICKING == flags & TICKING {
let mut handle = std::io::stderr().lock();
mutex!(self.done_total).stop();
self.elapsed.store(
u32::saturating_from(self.started.elapsed().as_secs()),
SeqCst
);
mutex!(self.doing).clear();
let _res = handle.write_all(CLS).and_then(|()| handle.flush());
}
}
}
impl ProglessInner {
#[inline]
fn cycle(&self) -> u8 { self.cycle.load(SeqCst) }
#[inline]
#[must_use]
fn done(&self) -> Option<NonZeroU32> {
if self.running() {
let (done, _) = self.done_total();
NonZeroU32::new(done)
}
else { None }
}
#[must_use]
fn done_total(&self) -> (u32, NonZeroU32) {
mutex!(self.done_total).done_total()
}
#[inline]
fn running(&self) -> bool { TICKING == self.flags.load(SeqCst) & TICKING }
}
impl ProglessInner {
fn add_guard(&self, task: &str) -> Option<String> {
if self.running() {
if let Some(task) = progless_task(task) && mutex!(self.doing).insert(task.clone()) {
self.flags.fetch_or(TICK_DOING, SeqCst);
Some(task)
}
else { Some(String::new()) }
}
else { None }
}
fn remove_guard(&self, task: &str, cycle: u8, increment: bool) {
if self.running() && self.cycle.load(SeqCst) == cycle {
if ! task.is_empty() && mutex!(self.doing).remove(task) {
self.flags.fetch_or(TICK_DOING, SeqCst);
}
if increment { self.increment_n(1); }
}
}
#[inline]
fn increment_n(&self, n: u32) {
if n != 0 && self.running() {
if mutex!(self.done_total).increment_n(n) {
self.flags.fetch_or(TICK_DONE | TICK_BAR, SeqCst);
}
else { self.stop(); }
}
}
fn push_msg(&self, msg: Msg) -> Result<(), Msg> {
let msg = msg.with_newline(true);
if self.running() {
let mut handle = std::io::stderr().lock();
let res = handle.write_all(CLS)
.and_then(|()| handle.write_all(msg.as_bytes()))
.and_then(|()| handle.flush())
.is_err();
drop(handle);
self.tick(true);
if res { return Err(msg); }
}
else { msg.eprint(); }
Ok(())
}
fn reset(&self, total: NonZeroU32) {
self.stop();
self.cycle.fetch_add(1, SeqCst); mutex!(self.done_total).set_total(total);
self.flags.store(TICK_RESET, SeqCst);
}
fn set_done(&self, done: u32) {
if self.running() {
if mutex!(self.done_total).set_done(done) {
self.flags.fetch_or(TICK_DONE | TICK_BAR, SeqCst);
}
else { self.stop(); }
}
}
fn set_title(&self, title: Option<Msg>) {
*mutex!(self.title) = title.map(|m| m.with_newline(false));
if self.running() { self.flags.fetch_or(TICK_TITLE, SeqCst); }
}
fn set_title_msg(&self, msg: &str) {
if let Some(title) = mutex!(self.title).as_mut() {
title.set_msg(msg);
if self.running() { self.flags.fetch_or(TICK_TITLE, SeqCst); }
}
}
#[cfg(feature = "signals_sigint")]
fn sigint(&self) -> bool {
let flags = self.flags.load(SeqCst);
if TICKING == flags & (SIGINT | TICKING) {
PRINTED_SIGINT.call_once(|| {
let _res = self.push_msg(Msg::warning("Early shutdown in progress."));
});
self.flags.fetch_or(SIGINT, SeqCst);
true
}
else { TICKING == flags & TICKING }
}
}
impl ProglessInner {
#[expect(clippy::cast_possible_truncation, reason = "It is what it is.")]
fn tick(&self, force: bool) -> bool {
if ! self.running() { return false; }
let mut handle = std::io::stderr().lock();
let Some((width, height)) = self.tick_set_size() else {
return true;
};
if width.get() < MIN_DRAW_WIDTH {
let _res = handle.write_all(CLS).and_then(|()| handle.flush());
return true;
}
let mut ticked = match self.tick_set_drawable() {
Some(t) => t,
None =>
if force { 0 }
else { return true; },
};
let mut buf = mutex!(self.buf);
if 0 != ticked & (TICK_DONE | TICK_TOTAL | TICK_BAR) {
let (done, total) = self.done_total();
if TICK_DONE == ticked & TICK_DONE { buf.done.replace(done); }
if TICK_TOTAL == ticked & TICK_TOTAL { buf.total.replace(total.get()); }
if 0 != ticked & TICK_PERCENT {
let percent =
if done == 0 { 0.0 }
else if done >= total.get() { 1.0 }
else { (f64::from(done) / f64::from(total.get())) as f32 };
buf.percent.replace(percent);
}
buf.set_bars(width, done, total);
}
if TICK_TITLE == ticked & TICK_TITLE {
let before = buf.doing.is_empty() || ! buf.title.is_empty();
buf.set_title(mutex!(self.title).as_ref(), width, height);
if ! before && ! buf.title.is_empty() {
ticked |= TICK_DOING;
}
}
if TICK_DOING == ticked & TICK_DOING {
buf.set_doing(&mutex!(self.doing), width, height);
}
buf.print(width, &mut handle);
true
}
fn tick_set_drawable(&self) -> Option<u8> {
let secs = self.tick_set_secs();
let flags = self.flags.fetch_and(! TICK_DRAWABLE, SeqCst) & TICK_DRAWABLE;
if secs || flags != 0 { Some(flags) }
else { None }
}
fn tick_set_secs(&self) -> bool {
let secs: u32 = u32::saturating_from(self.started.elapsed().as_secs());
if secs == self.elapsed.swap(secs, SeqCst) { false }
else {
mutex!(self.buf).elapsed.replace(secs);
true
}
}
#[cfg(feature = "signals_sigwinch")]
fn tick_resize(&self) -> bool {
if self.running() {
if let Some((width, height)) = term_size() {
let wh = u16::from_le_bytes([width.get(), height.get()]);
if wh != self.last_size.swap(wh, SeqCst) {
self.flags.fetch_or(TICK_RESIZED, SeqCst);
}
}
true
}
else { false }
}
#[cfg(feature = "signals_sigwinch")]
fn tick_set_size(&self) -> Option<(NonZeroU8, NonZeroU8)> {
let [width, height] = self.last_size.load(SeqCst).to_le_bytes();
let width = NonZeroU8::new(width)?;
let height = NonZeroU8::new(height)?;
Some((width, height))
}
#[cfg(not(feature = "signals_sigwinch"))]
fn tick_set_size(&self) -> Option<(NonZeroU8, NonZeroU8)> {
let (width, height) = term_size()?;
let wh = u16::from_le_bytes([width.get(), height.get()]);
if wh == self.last_size.swap(wh, SeqCst) { Some((width, height)) }
else {
self.flags.fetch_or(TICK_RESIZED, SeqCst);
None
}
}
}
#[derive(Debug, Clone, Copy)]
struct ProglessDoneTotal {
done: u32,
total: NonZeroU32,
}
impl ProglessDoneTotal {
const DEFAULT: Self = Self { done: 0, total: NonZeroU32::MIN };
#[must_use]
const fn new(total: NonZeroU32) -> Self {
Self { done: 0, total }
}
#[must_use]
const fn done_total(self) -> (u32, NonZeroU32) { (self.done, self.total) }
#[must_use]
const fn increment_n(&mut self, n: u32) -> bool {
self.set_done(self.done.saturating_add(n))
}
#[must_use]
const fn set_done(&mut self, done: u32) -> bool {
if done < self.total.get() {
self.done = done;
true
}
else {
self.done = self.total.get();
false
}
}
const fn set_total(&mut self, total: NonZeroU32) {
self.done = 0;
self.total = total;
}
const fn stop(&mut self) { self.done = self.total.get(); }
}
#[derive(Debug)]
struct ProglessBuffer {
title: Vec<u8>,
elapsed: NiceClock,
bar_done: &'static [u8],
bar_undone: &'static [u8],
done: NiceU32,
total: NiceU32,
percent: NicePercent,
doing: Vec<u8>,
lines_doing: u8,
}
impl ProglessBuffer {
const DEFAULT: Self = Self {
title: Vec::new(),
elapsed: NiceClock::MIN,
bar_done: &[],
bar_undone: &[],
done: NiceU32::MIN,
total: NiceU32::MIN,
percent: NicePercent::MIN,
doing: Vec::new(),
lines_doing: 0,
};
}
impl ProglessBuffer {
#[inline(never)]
fn print(&self, width: NonZeroU8, handle: &mut StderrLock<'static>) -> bool {
use std::io::ErrorKind;
static CLOSE: [&[u8]; 256] = [
b"\x1b[0m\r", b"\x1b[0m\r\x1b[1A", b"\x1b[0m\r\x1b[2A", b"\x1b[0m\r\x1b[3A", b"\x1b[0m\r\x1b[4A", b"\x1b[0m\r\x1b[5A", b"\x1b[0m\r\x1b[6A", b"\x1b[0m\r\x1b[7A", b"\x1b[0m\r\x1b[8A", b"\x1b[0m\r\x1b[9A", b"\x1b[0m\r\x1b[10A", b"\x1b[0m\r\x1b[11A", b"\x1b[0m\r\x1b[12A", b"\x1b[0m\r\x1b[13A", b"\x1b[0m\r\x1b[14A", b"\x1b[0m\r\x1b[15A",
b"\x1b[0m\r\x1b[16A", b"\x1b[0m\r\x1b[17A", b"\x1b[0m\r\x1b[18A", b"\x1b[0m\r\x1b[19A", b"\x1b[0m\r\x1b[20A", b"\x1b[0m\r\x1b[21A", b"\x1b[0m\r\x1b[22A", b"\x1b[0m\r\x1b[23A", b"\x1b[0m\r\x1b[24A", b"\x1b[0m\r\x1b[25A", b"\x1b[0m\r\x1b[26A", b"\x1b[0m\r\x1b[27A", b"\x1b[0m\r\x1b[28A", b"\x1b[0m\r\x1b[29A", b"\x1b[0m\r\x1b[30A", b"\x1b[0m\r\x1b[31A",
b"\x1b[0m\r\x1b[32A", b"\x1b[0m\r\x1b[33A", b"\x1b[0m\r\x1b[34A", b"\x1b[0m\r\x1b[35A", b"\x1b[0m\r\x1b[36A", b"\x1b[0m\r\x1b[37A", b"\x1b[0m\r\x1b[38A", b"\x1b[0m\r\x1b[39A", b"\x1b[0m\r\x1b[40A", b"\x1b[0m\r\x1b[41A", b"\x1b[0m\r\x1b[42A", b"\x1b[0m\r\x1b[43A", b"\x1b[0m\r\x1b[44A", b"\x1b[0m\r\x1b[45A", b"\x1b[0m\r\x1b[46A", b"\x1b[0m\r\x1b[47A",
b"\x1b[0m\r\x1b[48A", b"\x1b[0m\r\x1b[49A", b"\x1b[0m\r\x1b[50A", b"\x1b[0m\r\x1b[51A", b"\x1b[0m\r\x1b[52A", b"\x1b[0m\r\x1b[53A", b"\x1b[0m\r\x1b[54A", b"\x1b[0m\r\x1b[55A", b"\x1b[0m\r\x1b[56A", b"\x1b[0m\r\x1b[57A", b"\x1b[0m\r\x1b[58A", b"\x1b[0m\r\x1b[59A", b"\x1b[0m\r\x1b[60A", b"\x1b[0m\r\x1b[61A", b"\x1b[0m\r\x1b[62A", b"\x1b[0m\r\x1b[63A",
b"\x1b[0m\r\x1b[64A", b"\x1b[0m\r\x1b[65A", b"\x1b[0m\r\x1b[66A", b"\x1b[0m\r\x1b[67A", b"\x1b[0m\r\x1b[68A", b"\x1b[0m\r\x1b[69A", b"\x1b[0m\r\x1b[70A", b"\x1b[0m\r\x1b[71A", b"\x1b[0m\r\x1b[72A", b"\x1b[0m\r\x1b[73A", b"\x1b[0m\r\x1b[74A", b"\x1b[0m\r\x1b[75A", b"\x1b[0m\r\x1b[76A", b"\x1b[0m\r\x1b[77A", b"\x1b[0m\r\x1b[78A", b"\x1b[0m\r\x1b[79A",
b"\x1b[0m\r\x1b[80A", b"\x1b[0m\r\x1b[81A", b"\x1b[0m\r\x1b[82A", b"\x1b[0m\r\x1b[83A", b"\x1b[0m\r\x1b[84A", b"\x1b[0m\r\x1b[85A", b"\x1b[0m\r\x1b[86A", b"\x1b[0m\r\x1b[87A", b"\x1b[0m\r\x1b[88A", b"\x1b[0m\r\x1b[89A", b"\x1b[0m\r\x1b[90A", b"\x1b[0m\r\x1b[91A", b"\x1b[0m\r\x1b[92A", b"\x1b[0m\r\x1b[93A", b"\x1b[0m\r\x1b[94A", b"\x1b[0m\r\x1b[95A",
b"\x1b[0m\r\x1b[96A", b"\x1b[0m\r\x1b[97A", b"\x1b[0m\r\x1b[98A", b"\x1b[0m\r\x1b[99A", b"\x1b[0m\r\x1b[100A", b"\x1b[0m\r\x1b[101A", b"\x1b[0m\r\x1b[102A", b"\x1b[0m\r\x1b[103A", b"\x1b[0m\r\x1b[104A", b"\x1b[0m\r\x1b[105A", b"\x1b[0m\r\x1b[106A", b"\x1b[0m\r\x1b[107A", b"\x1b[0m\r\x1b[108A", b"\x1b[0m\r\x1b[109A", b"\x1b[0m\r\x1b[110A", b"\x1b[0m\r\x1b[111A",
b"\x1b[0m\r\x1b[112A", b"\x1b[0m\r\x1b[113A", b"\x1b[0m\r\x1b[114A", b"\x1b[0m\r\x1b[115A", b"\x1b[0m\r\x1b[116A", b"\x1b[0m\r\x1b[117A", b"\x1b[0m\r\x1b[118A", b"\x1b[0m\r\x1b[119A", b"\x1b[0m\r\x1b[120A", b"\x1b[0m\r\x1b[121A", b"\x1b[0m\r\x1b[122A", b"\x1b[0m\r\x1b[123A", b"\x1b[0m\r\x1b[124A", b"\x1b[0m\r\x1b[125A", b"\x1b[0m\r\x1b[126A", b"\x1b[0m\r\x1b[127A",
b"\x1b[0m\r\x1b[128A", b"\x1b[0m\r\x1b[129A", b"\x1b[0m\r\x1b[130A", b"\x1b[0m\r\x1b[131A", b"\x1b[0m\r\x1b[132A", b"\x1b[0m\r\x1b[133A", b"\x1b[0m\r\x1b[134A", b"\x1b[0m\r\x1b[135A", b"\x1b[0m\r\x1b[136A", b"\x1b[0m\r\x1b[137A", b"\x1b[0m\r\x1b[138A", b"\x1b[0m\r\x1b[139A", b"\x1b[0m\r\x1b[140A", b"\x1b[0m\r\x1b[141A", b"\x1b[0m\r\x1b[142A", b"\x1b[0m\r\x1b[143A",
b"\x1b[0m\r\x1b[144A", b"\x1b[0m\r\x1b[145A", b"\x1b[0m\r\x1b[146A", b"\x1b[0m\r\x1b[147A", b"\x1b[0m\r\x1b[148A", b"\x1b[0m\r\x1b[149A", b"\x1b[0m\r\x1b[150A", b"\x1b[0m\r\x1b[151A", b"\x1b[0m\r\x1b[152A", b"\x1b[0m\r\x1b[153A", b"\x1b[0m\r\x1b[154A", b"\x1b[0m\r\x1b[155A", b"\x1b[0m\r\x1b[156A", b"\x1b[0m\r\x1b[157A", b"\x1b[0m\r\x1b[158A", b"\x1b[0m\r\x1b[159A",
b"\x1b[0m\r\x1b[160A", b"\x1b[0m\r\x1b[161A", b"\x1b[0m\r\x1b[162A", b"\x1b[0m\r\x1b[163A", b"\x1b[0m\r\x1b[164A", b"\x1b[0m\r\x1b[165A", b"\x1b[0m\r\x1b[166A", b"\x1b[0m\r\x1b[167A", b"\x1b[0m\r\x1b[168A", b"\x1b[0m\r\x1b[169A", b"\x1b[0m\r\x1b[170A", b"\x1b[0m\r\x1b[171A", b"\x1b[0m\r\x1b[172A", b"\x1b[0m\r\x1b[173A", b"\x1b[0m\r\x1b[174A", b"\x1b[0m\r\x1b[175A",
b"\x1b[0m\r\x1b[176A", b"\x1b[0m\r\x1b[177A", b"\x1b[0m\r\x1b[178A", b"\x1b[0m\r\x1b[179A", b"\x1b[0m\r\x1b[180A", b"\x1b[0m\r\x1b[181A", b"\x1b[0m\r\x1b[182A", b"\x1b[0m\r\x1b[183A", b"\x1b[0m\r\x1b[184A", b"\x1b[0m\r\x1b[185A", b"\x1b[0m\r\x1b[186A", b"\x1b[0m\r\x1b[187A", b"\x1b[0m\r\x1b[188A", b"\x1b[0m\r\x1b[189A", b"\x1b[0m\r\x1b[190A", b"\x1b[0m\r\x1b[191A",
b"\x1b[0m\r\x1b[192A", b"\x1b[0m\r\x1b[193A", b"\x1b[0m\r\x1b[194A", b"\x1b[0m\r\x1b[195A", b"\x1b[0m\r\x1b[196A", b"\x1b[0m\r\x1b[197A", b"\x1b[0m\r\x1b[198A", b"\x1b[0m\r\x1b[199A", b"\x1b[0m\r\x1b[200A", b"\x1b[0m\r\x1b[201A", b"\x1b[0m\r\x1b[202A", b"\x1b[0m\r\x1b[203A", b"\x1b[0m\r\x1b[204A", b"\x1b[0m\r\x1b[205A", b"\x1b[0m\r\x1b[206A", b"\x1b[0m\r\x1b[207A",
b"\x1b[0m\r\x1b[208A", b"\x1b[0m\r\x1b[209A", b"\x1b[0m\r\x1b[210A", b"\x1b[0m\r\x1b[211A", b"\x1b[0m\r\x1b[212A", b"\x1b[0m\r\x1b[213A", b"\x1b[0m\r\x1b[214A", b"\x1b[0m\r\x1b[215A", b"\x1b[0m\r\x1b[216A", b"\x1b[0m\r\x1b[217A", b"\x1b[0m\r\x1b[218A", b"\x1b[0m\r\x1b[219A", b"\x1b[0m\r\x1b[220A", b"\x1b[0m\r\x1b[221A", b"\x1b[0m\r\x1b[222A", b"\x1b[0m\r\x1b[223A",
b"\x1b[0m\r\x1b[224A", b"\x1b[0m\r\x1b[225A", b"\x1b[0m\r\x1b[226A", b"\x1b[0m\r\x1b[227A", b"\x1b[0m\r\x1b[228A", b"\x1b[0m\r\x1b[229A", b"\x1b[0m\r\x1b[230A", b"\x1b[0m\r\x1b[231A", b"\x1b[0m\r\x1b[232A", b"\x1b[0m\r\x1b[233A", b"\x1b[0m\r\x1b[234A", b"\x1b[0m\r\x1b[235A", b"\x1b[0m\r\x1b[236A", b"\x1b[0m\r\x1b[237A", b"\x1b[0m\r\x1b[238A", b"\x1b[0m\r\x1b[239A",
b"\x1b[0m\r\x1b[240A", b"\x1b[0m\r\x1b[241A", b"\x1b[0m\r\x1b[242A", b"\x1b[0m\r\x1b[243A", b"\x1b[0m\r\x1b[244A", b"\x1b[0m\r\x1b[245A", b"\x1b[0m\r\x1b[246A", b"\x1b[0m\r\x1b[247A", b"\x1b[0m\r\x1b[248A", b"\x1b[0m\r\x1b[249A", b"\x1b[0m\r\x1b[250A", b"\x1b[0m\r\x1b[251A", b"\x1b[0m\r\x1b[252A", b"\x1b[0m\r\x1b[253A", b"\x1b[0m\r\x1b[254A", b"\x1b[0m\r\x1b[255A",
];
let mut parts: &mut [IoSlice] =
if width.get() < 40 {
&mut [
IoSlice::new(concat!(
"\x1b[J ",
csi!(reset, bold, light_cyan),
"» ",
csi!(!fg),
).as_bytes()), IoSlice::new(self.percent.as_bytes()), IoSlice::new(concat!(csi!(), "\r").as_bytes()), ]
}
else {
let lines =
if self.title.is_empty() { self.lines_doing }
else { self.lines_doing.saturating_add(1) };
&mut [
IoSlice::new(CLS),
IoSlice::new(&self.title),
IoSlice::new(concat!(
csi!(reset, dim),
"[",
csi!(reset, bold),
).as_bytes()),
IoSlice::new(self.elapsed.as_bytes()),
IoSlice::new(concat!(
csi!(reset, dim),
"] [",
csi!(reset, bold, light_cyan),
).as_bytes()),
IoSlice::new(self.bar_done),
IoSlice::new(csi!(blue).as_bytes()),
IoSlice::new(self.bar_undone),
IoSlice::new(concat!(
csi!(reset, dim),
"]",
csi!(reset, bold, light_cyan),
" ",
).as_bytes()),
IoSlice::new(self.done.as_bytes()),
IoSlice::new(concat!(
csi!(reset, dim),
"/",
csi!(reset, bold, blue),
).as_bytes()),
IoSlice::new(self.total.as_bytes()),
IoSlice::new(concat!(csi!(!fg), " ").as_bytes()),
IoSlice::new(self.percent.as_bytes()),
IoSlice::new(csi!(reset, magenta).as_bytes()),
IoSlice::new(&self.doing),
IoSlice::new(CLOSE[usize::from(lines)]),
]
};
IoSlice::advance_slices(&mut parts, 0);
loop {
match handle.write_vectored(parts) {
Ok(0) => return false,
Ok(n) => IoSlice::advance_slices(&mut parts, n),
Err(e) =>
if e.kind() == ErrorKind::Interrupted {} else { return false; },
}
if parts.is_empty() { break; }
}
handle.flush().is_ok()
}
}
impl ProglessBuffer {
fn set_bars(&mut self, width: NonZeroU8, done: u32, total: NonZeroU32) {
let mut w_done = 0_u8;
let mut w_undone = 0_u8;
let space: u8 = width.get().saturating_sub(u8::saturating_from(
19 +
self.done.len() +
self.total.len() +
self.percent.len()
));
if MIN_BARS_WIDTH <= space {
if done == 0 { w_undone = space; }
else if done == total.get() { w_done = space; }
else {
w_done = u8::saturating_from((done * u32::from(space)).wrapping_div(total.get()));
w_undone = space.saturating_sub(w_done);
}
debug_assert_eq!(
w_done + w_undone,
space,
"BUG: bar space was miscalculated."
);
}
self.bar_done = &BAR_DONE[..usize::from(w_done)];
self.bar_undone = &BAR_UNDONE[..usize::from(w_undone)];
}
fn set_doing(
&mut self,
doing: &BTreeSet<String>,
width: NonZeroU8,
height: NonZeroU8,
) {
const PREFIX: &[u8; 9] = &[b'\n', 32, 32, 32, 32, 226, 134, 179, 32];
self.doing.truncate(0);
self.lines_doing = 0;
let width = usize::from(width.get().saturating_sub(12));
if
2 <= width &&
usize::from(! self.title.is_empty()) + 1 + doing.len() <= usize::from(height.get())
{
for line in doing {
let keep = crate::length_width(line, width);
if keep != 0 {
self.doing.extend_from_slice(PREFIX);
self.doing.extend(line.bytes().take(keep));
self.lines_doing += 1;
}
}
}
}
fn set_title(&mut self, title: Option<&Msg>, width: NonZeroU8, height: NonZeroU8) {
self.title.truncate(0);
if 2 <= height.get() && let Some(title) = title {
let title = title.fitted(usize::from(width.get()));
if ! title.is_empty() {
let slice = title.as_bytes();
let end = slice.iter().copied().position(|b| b == b'\n').unwrap_or(slice.len());
if end != 0 {
self.title.extend_from_slice(&slice[..end]);
self.title.push(b'\n');
}
}
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "progress")))]
#[derive(Debug, Clone, Default)]
pub struct Progless {
steady: Arc<ProglessSteady>,
inner: Arc<ProglessInner>,
}
impl<T> From<T> for Progless
where ProglessInner: From<T> {
#[inline]
fn from(total: T) -> Self {
let inner = Arc::new(ProglessInner::from(total));
Self {
steady: Arc::new(ProglessSteady::from(Arc::clone(&inner))),
inner,
}
}
}
impl From<Progless> for Msg {
#[inline]
fn from(src: Progless) -> Self {
let elapsed = NiceElapsed::from(src.inner.started);
let mut msg = String::with_capacity(13 + elapsed.len());
msg.push_str("Finished in ");
msg.push_str(elapsed.as_str());
msg.push('.');
Self::done(msg).with_newline(true)
}
}
macro_rules! outer_tryfrom {
($($ty:ty),+ $(,)?) => ($(
impl TryFrom<$ty> for Progless {
type Error = ProglessError;
#[inline]
fn try_from(total: $ty) -> Result<Self, Self::Error> {
let inner = Arc::new(ProglessInner::try_from(total)?);
Ok(Self {
steady: Arc::new(ProglessSteady::from(Arc::clone(&inner))),
inner,
})
}
}
)+)
}
outer_tryfrom!(
u8, u16, u32, u64, usize, u128,
i8, i16, i32, i64, isize, i128,
NonZeroU64, NonZeroUsize, NonZeroU128,
);
impl Progless {
pub const CURSOR_HIDE: &str = "\x1b[?25l";
pub const CURSOR_UNHIDE: &str = "\x1b[?25h";
pub const MAX_TOTAL: usize = cfg_select! {
target_pointer_width = "16" => 65_535,
_ => 4_294_967_295,
};
pub const MAX_TOTAL_ERROR: ProglessError = ProglessError::TotalOverflow;
}
impl Progless {
#[must_use]
#[inline]
pub fn with_title<S>(self, title: Option<S>) -> Self
where S: Into<Msg> {
let title = title.and_then(|m| {
let m = m.into();
if m.is_empty() { None }
else { Some(m) }
});
self.inner.set_title(title);
self
}
#[must_use]
#[inline]
pub fn with_reticulating_splines<S>(self, app: S) -> Self
where S: AsRef<str> {
self.set_reticulating_splines(app);
self
}
#[expect(clippy::must_use_candidate, reason = "Caller might not care.")]
#[inline]
pub fn finish(&self) -> Duration {
self.inner.stop();
self.steady.stop();
self.inner.started.elapsed()
}
#[must_use]
pub fn summary<S>(&self, kind: MsgKind, singular: S, plural: S) -> Msg
where S: AsRef<str> {
let (done, _) = self.inner.done_total();
Msg::new(kind, format!(
"{} in {}.",
done.nice_inflect(singular.as_ref(), plural.as_ref()),
NiceElapsed::from(self.inner.started),
))
.with_newline(true)
}
}
impl Progless {
#[inline]
pub fn increment(&self) { self.inner.increment_n(1); }
#[inline]
pub fn increment_n(&self, n: u32) { self.inner.increment_n(n); }
#[must_use]
pub fn task<S>(&self, txt: S) -> Option<ProglessTaskGuard<'_>>
where S: AsRef<str> {
self.inner.add_guard(txt.as_ref())
.map(|task| ProglessTaskGuard::from_parts(
task,
self.inner.cycle(),
&self.inner,
))
}
#[inline]
pub fn push_msg(&self, msg: Msg) -> Result<(), Msg> { self.inner.push_msg(msg) }
pub fn reset(&self, total: NonZeroU32) {
self.inner.reset(total);
self.steady.start(Arc::clone(&self.inner));
}
pub fn try_reset(&self, total: u32) -> Result<(), ProglessError> {
let total = NonZeroU32::new(total).ok_or(ProglessError::EmptyTotal)?;
self.reset(total);
Ok(())
}
#[inline]
#[must_use]
pub fn done(&self) -> Option<NonZeroU32> { self.inner.done() }
#[inline]
pub fn set_done(&self, done: u32) { self.inner.set_done(done); }
#[inline]
pub fn set_title<S>(&self, title: Option<S>)
where S: Into<Msg> {
let title = title.and_then(|m| {
let m = m.into();
if m.is_empty() { None }
else { Some(m) }
});
self.inner.set_title(title);
}
#[inline]
pub fn set_title_msg(&self, msg: &str) { self.inner.set_title_msg(msg); }
#[inline]
pub fn set_reticulating_splines<S>(&self, app: S)
where S: AsRef<str> {
self.inner.set_title(Some(Msg::new(
(app.as_ref(), AnsiColor::Misc199),
"Reticulating splines\u{2026}"
)));
}
}
fn progless_task(src: &str) -> Option<String> {
let src = src.trim_end();
if src.is_empty() { return None; }
let mut out = String::with_capacity(src.len());
for c in NoAnsi::new(src) {
if c.is_whitespace() { out.push(' '); }
else if ! c.is_control() { out.push(c); }
}
if out.is_empty() { None }
else { Some(out) }
}
#[cfg(unix)]
#[must_use]
#[inline]
fn term_size() -> Option<(NonZeroU8, NonZeroU8)> {
use terminal_size::{Height, Width};
let (Width(w), Height(h)) = terminal_size::terminal_size_of(std::io::stderr())?;
let w = NonZeroU8::new(u8::saturating_from(w.saturating_sub(1)))?;
let h = NonZeroU8::new(u8::saturating_from(h).saturating_sub(1))?;
Some((w, h))
}
#[cfg(not(unix))]
#[must_use]
#[inline]
fn term_size() -> Option<(NonZeroU8, NonZeroU8)> {
use terminal_size::{Height, Width};
let (Width(w), Height(h)) = terminal_size::terminal_size()?;
let w = NonZeroU8::new(u8::saturating_from(w.saturating_sub(1)))?;
let h = NonZeroU8::new(u8::saturating_from(h).saturating_sub(1))?;
Some((w, h))
}