mod ci;
use ci::{CiState, format_duration};
use clx::progress::{
ProgressJob, ProgressJobBuilder, ProgressJobDoneBehavior, ProgressOutput, ProgressStatus,
};
use clx::style;
use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
const TTY_MAX_VISIBLE_FETCH_ROWS: usize = 5;
fn overflow_fetch_label(count: usize) -> String {
let word = pluralizer::pluralize("package", count as isize, false);
format!("{count} more {word}…")
}
pub struct InstallProgress {
mode: Mode,
}
#[derive(Clone)]
enum Mode {
Tty {
root: Arc<ProgressJob>,
total: Arc<AtomicUsize>,
fetch_state: Arc<Mutex<FetchState>>,
},
Ci(Arc<CiState>),
}
struct FetchState {
visible: usize,
overflow: usize,
overflow_row: Option<Arc<ProgressJob>>,
}
impl Clone for InstallProgress {
fn clone(&self) -> Self {
if let Mode::Ci(s) = &self.mode {
s.alive.fetch_add(1, Ordering::Relaxed);
}
Self {
mode: self.mode.clone(),
}
}
}
impl InstallProgress {
pub fn try_new() -> Option<Self> {
if clx::progress::output() == ProgressOutput::Text {
return None;
}
if std::io::stderr().is_terminal() && !is_ci::cached() {
Some(Self::new_tty())
} else {
Some(Self::new_ci())
}
}
fn new_tty() -> Self {
let header = format!(
"{} {} {}",
style::emagenta("aube").bold(),
style::edim(env!("CARGO_PKG_VERSION")),
style::edim("by en.dev"),
);
let root = ProgressJobBuilder::new()
.body("{{aube}}{{phase}} {{progress_bar(flex=true)}} {{cur}}/{{total}}")
.body_text(Some("{{aube}}{{phase}} {{cur}}/{{total}}"))
.prop("aube", &header)
.prop("phase", "")
.progress_current(0)
.progress_total(0)
.on_done(ProgressJobDoneBehavior::Hide)
.start();
Self {
mode: Mode::Tty {
root,
total: Arc::new(AtomicUsize::new(0)),
fetch_state: Arc::new(Mutex::new(FetchState {
visible: 0,
overflow: 0,
overflow_row: None,
})),
},
}
}
fn new_ci() -> Self {
let state = Arc::new(CiState::new());
CiState::spawn_heartbeat(&state);
Self {
mode: Mode::Ci(state),
}
}
pub fn set_total(&self, total: usize) {
match &self.mode {
Mode::Tty { root, total: t, .. } => {
t.store(total, Ordering::Relaxed);
root.progress_total(total);
}
Mode::Ci(s) => {
s.resolved.store(total, Ordering::Relaxed);
}
}
}
pub fn inc_total(&self, n: usize) {
match &self.mode {
Mode::Tty { root, total, .. } => {
let new_total = total.fetch_add(n, Ordering::Relaxed) + n;
root.progress_total(new_total);
}
Mode::Ci(s) => {
s.resolved.fetch_add(n, Ordering::Relaxed);
}
}
}
pub fn set_phase(&self, phase: &str) {
match &self.mode {
Mode::Tty { root, .. } => {
if phase.is_empty() {
root.prop("phase", "");
} else {
root.prop("phase", &format!("{}", style::edim(format!(" — {phase}"))));
}
}
Mode::Ci(s) => s.set_phase(phase),
}
}
pub fn inc_reused(&self, n: usize) {
match &self.mode {
Mode::Tty { root, .. } => root.increment(n),
Mode::Ci(s) => {
s.reused.fetch_add(n, Ordering::Relaxed);
}
}
}
pub fn inc_downloaded_bytes(&self, bytes: u64) {
if let Mode::Ci(s) = &self.mode {
s.downloaded_bytes.fetch_add(bytes, Ordering::Relaxed);
}
}
pub fn start_fetch(&self, name: &str, version: &str) -> FetchRow {
match &self.mode {
Mode::Tty {
root, fetch_state, ..
} => {
let mut st = fetch_state.lock().unwrap();
if st.visible < TTY_MAX_VISIBLE_FETCH_ROWS {
st.visible += 1;
drop(st);
let child = ProgressJobBuilder::new()
.body(" {{spinner()}} {{label | flex}}")
.body_text(None::<String>)
.prop("label", &format!("{name}@{version}"))
.status(ProgressStatus::Running)
.on_done(ProgressJobDoneBehavior::Hide)
.build();
let child = root.add(child);
return FetchRow {
inner: FetchRowInner::Tty {
child,
root: Arc::downgrade(root),
fetch_state: Arc::downgrade(fetch_state),
visible: true,
},
completed: false,
};
}
st.overflow += 1;
if st.overflow_row.is_none() {
let row = ProgressJobBuilder::new()
.body(" {{spinner()}} {{label | flex}}")
.body_text(None::<String>)
.prop("label", &overflow_fetch_label(st.overflow))
.status(ProgressStatus::Running)
.on_done(ProgressJobDoneBehavior::Hide)
.build();
st.overflow_row = Some(root.add(row));
} else if let Some(row) = &st.overflow_row {
row.prop("label", &overflow_fetch_label(st.overflow));
}
FetchRow {
inner: FetchRowInner::Tty {
child: st.overflow_row.as_ref().unwrap().clone(),
root: Arc::downgrade(root),
fetch_state: Arc::downgrade(fetch_state),
visible: false,
},
completed: false,
}
}
Mode::Ci(s) => FetchRow {
inner: FetchRowInner::Ci(Arc::downgrade(s)),
completed: false,
},
}
}
pub fn finish(&self, print_ci_summary: bool) {
match &self.mode {
Mode::Tty { root, .. } => {
root.set_status(ProgressStatus::Done);
clx::progress::stop_clear();
}
Mode::Ci(s) => s.stop(print_ci_summary),
}
}
pub fn print_install_summary(
&self,
linked: usize,
top_level_linked: usize,
total_packages: usize,
elapsed: Duration,
) {
if linked == 0 && top_level_linked == 0 {
let msg = if total_packages == 0 {
"Already up to date".to_string()
} else {
format!(
"Already up to date ({})",
pluralizer::pluralize("package", total_packages as isize, true)
)
};
let line = format!(
"{} {} {} {} {}",
style::emagenta("aube").bold(),
style::edim(env!("CARGO_PKG_VERSION")),
style::edim("by en.dev"),
style::edim("·"),
style::egreen(msg).bold(),
);
let _ = writeln!(std::io::stderr(), "{line}");
return;
}
if linked == 0 {
return;
}
if !matches!(self.mode, Mode::Tty { .. }) {
return;
}
let msg = format!(
"✓ installed {} in {}",
pluralizer::pluralize("package", linked as isize, true),
format_duration(elapsed)
);
let line = format!(
"{} {} {} {} {}",
style::emagenta("aube").bold(),
style::edim(env!("CARGO_PKG_VERSION")),
style::edim("by en.dev"),
style::edim("·"),
style::egreen(msg).bold(),
);
let _ = writeln!(std::io::stderr(), "{line}");
}
}
impl Drop for InstallProgress {
fn drop(&mut self) {
match &self.mode {
Mode::Tty { root, .. } => {
if Arc::strong_count(root) == 1 {
root.set_status(ProgressStatus::Done);
clx::progress::stop_clear();
}
}
Mode::Ci(s) => {
if s.alive.fetch_sub(1, Ordering::Relaxed) == 1 {
s.stop(false);
}
}
}
}
}
pub struct FetchRow {
inner: FetchRowInner,
completed: bool,
}
enum FetchRowInner {
Tty {
child: Arc<ProgressJob>,
root: Weak<ProgressJob>,
fetch_state: Weak<Mutex<FetchState>>,
visible: bool,
},
Ci(Weak<CiState>),
}
impl FetchRow {
fn finish_inner(&mut self) {
if self.completed {
return;
}
self.completed = true;
match &self.inner {
FetchRowInner::Tty {
child,
root,
fetch_state,
visible,
} => {
if let Some(root) = root.upgrade() {
root.increment(1);
}
if *visible {
child.set_status(ProgressStatus::Done);
if let Some(st) = fetch_state.upgrade() {
let mut st = st.lock().unwrap();
if st.visible > 0 {
st.visible -= 1;
}
}
} else if let Some(st) = fetch_state.upgrade() {
let mut st = st.lock().unwrap();
if st.overflow > 0 {
st.overflow -= 1;
}
if st.overflow == 0 {
if let Some(row) = st.overflow_row.take() {
row.set_status(ProgressStatus::Done);
}
} else if let Some(row) = &st.overflow_row {
row.prop("label", &overflow_fetch_label(st.overflow));
}
}
}
FetchRowInner::Ci(weak) => {
if let Some(s) = weak.upgrade() {
s.downloaded.fetch_add(1, Ordering::Relaxed);
}
}
}
}
}
impl Drop for FetchRow {
fn drop(&mut self) {
self.finish_inner();
}
}
#[derive(Clone, Copy, Default)]
pub struct PausingWriter;
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for PausingWriter {
type Writer = PausingWriterGuard;
fn make_writer(&'a self) -> Self::Writer {
PausingWriterGuard { buf: Vec::new() }
}
}
pub struct PausingWriterGuard {
buf: Vec<u8>,
}
impl Write for PausingWriterGuard {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.buf.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Drop for PausingWriterGuard {
fn drop(&mut self) {
if self.buf.is_empty() {
return;
}
let buf = std::mem::take(&mut self.buf);
let was_paused = clx::progress::is_paused();
if !was_paused {
clx::progress::pause();
}
let _: () = clx::progress::with_terminal_lock(|| {
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(&buf);
let _ = stderr.flush();
});
if !was_paused {
clx::progress::resume();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overflow_fetch_label_pluralizes_count() {
assert_eq!(overflow_fetch_label(1), "1 more package…");
assert_eq!(overflow_fetch_label(2), "2 more packages…");
}
}