use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Mutex, OnceLock, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use console::Term;
use super::job::ProgressJob;
use super::output::{ProgressOutput, output};
use super::render::{refresh, refresh_once};
static ENV_NO_PROGRESS: OnceLock<bool> = OnceLock::new();
static ENV_TEXT_MODE: OnceLock<bool> = OnceLock::new();
fn check_env_bool(var_name: &str) -> bool {
std::env::var(var_name)
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
fn env_no_progress() -> bool {
*ENV_NO_PROGRESS.get_or_init(|| check_env_bool("CLX_NO_PROGRESS"))
}
pub(crate) fn env_text_mode() -> bool {
*ENV_TEXT_MODE.get_or_init(|| check_env_bool("CLX_TEXT_MODE"))
}
#[must_use]
pub fn is_disabled() -> bool {
env_no_progress()
}
pub(crate) static LINES: Mutex<usize> = Mutex::new(0);
pub static TERM_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
pub(crate) static REFRESH_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
pub(crate) static STOPPING: AtomicBool = AtomicBool::new(false);
static NOTIFY: Mutex<Option<mpsc::Sender<()>>> = Mutex::new(None);
pub static STARTED: Mutex<bool> = Mutex::new(false);
static PAUSED: AtomicBool = AtomicBool::new(false);
pub(crate) static JOBS: Mutex<Vec<Arc<ProgressJob>>> = Mutex::new(vec![]);
pub(crate) static TERA: Mutex<Option<tera::Tera>> = Mutex::new(None);
static INTERVAL: Mutex<Duration> = Mutex::new(Duration::from_millis(200));
pub(crate) static LAST_OSC_PERCENTAGE: Mutex<Option<u8>> = Mutex::new(None);
pub(crate) static LAST_OUTPUT: Mutex<String> = Mutex::new(String::new());
pub(crate) static RENDER_CTX: OnceLock<Mutex<super::render::RenderContext>> = OnceLock::new();
#[cfg(unix)]
static RESIZE_SIGNALED: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
extern "C" fn handle_sigwinch(_: nix::libc::c_int) {
RESIZE_SIGNALED.store(true, Ordering::Relaxed);
}
#[cfg(unix)]
fn register_resize_handler() {
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
let handler = SigHandler::Handler(handle_sigwinch);
let action = SigAction::new(handler, SaFlags::SA_RESTART, SigSet::empty());
unsafe {
let _ = sigaction(Signal::SIGWINCH, &action);
}
}
#[cfg(unix)]
pub(crate) fn check_resize_signaled() -> bool {
RESIZE_SIGNALED.swap(false, Ordering::Relaxed)
}
#[cfg(not(unix))]
pub(crate) fn check_resize_signaled() -> bool {
false
}
pub(crate) fn term() -> &'static Term {
static TERM: LazyLock<Term> = LazyLock::new(Term::stderr);
&TERM
}
#[must_use]
pub fn with_terminal_lock<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = TERM_LOCK.lock().unwrap();
let result = f();
drop(_guard);
result
}
#[must_use]
pub fn interval() -> Duration {
*INTERVAL.lock().unwrap()
}
pub fn set_interval(interval: Duration) {
*INTERVAL.lock().unwrap() = interval;
}
pub fn is_paused() -> bool {
PAUSED.load(Ordering::Relaxed)
}
pub fn pause() {
PAUSED.store(true, Ordering::Relaxed);
if *STARTED.lock().unwrap() {
let _ = clear();
}
}
pub fn resume() {
PAUSED.store(false, Ordering::Relaxed);
if !*STARTED.lock().unwrap() {
return;
}
if output() == ProgressOutput::UI {
notify();
}
}
pub(crate) fn notify() {
if is_disabled() || STOPPING.load(Ordering::Relaxed) {
return;
}
start();
if let Some(tx) = NOTIFY.lock().unwrap().clone() {
let _ = tx.send(());
}
}
fn notify_wait(timeout: Duration) -> bool {
let (tx, rx) = mpsc::channel();
NOTIFY.lock().unwrap().replace(tx);
rx.recv_timeout(timeout).is_ok()
}
pub fn flush() {
if !*STARTED.lock().unwrap() {
return;
}
if let Err(err) = refresh() {
eprintln!("clx: {err:?}");
}
}
fn start() {
let mut started = STARTED.lock().unwrap();
if *started
|| is_disabled()
|| matches!(output(), ProgressOutput::Text | ProgressOutput::Quiet)
|| STOPPING.load(Ordering::Relaxed)
{
return;
}
*started = true;
drop(started);
#[cfg(unix)]
register_resize_handler();
thread::spawn(move || {
let mut refresh_after = Instant::now();
loop {
if refresh_after > Instant::now() {
thread::sleep(refresh_after - Instant::now());
}
refresh_after = Instant::now() + interval() / 2;
match refresh() {
Ok(true) => {}
Ok(false) => {
break;
}
Err(err) => {
eprintln!("clx: {err:?}");
*LINES.lock().unwrap() = 0;
}
}
if check_resize_signaled() {
LAST_OUTPUT.lock().unwrap().clear();
continue;
}
notify_wait(interval());
}
});
}
pub fn stop() {
STOPPING.store(true, Ordering::Relaxed);
let _ = refresh_once();
clear_osc_progress();
*STARTED.lock().unwrap() = false;
*LINES.lock().unwrap() = 0;
let _ = std::io::stderr().flush();
}
pub fn stop_clear() {
STOPPING.store(true, Ordering::Relaxed);
let _ = clear();
clear_osc_progress();
*STARTED.lock().unwrap() = false;
let _ = std::io::stderr().flush();
}
#[must_use]
pub fn job_count() -> usize {
JOBS.lock().unwrap().len()
}
#[must_use]
pub fn active_jobs() -> usize {
fn count_active(jobs: &[Arc<ProgressJob>]) -> usize {
jobs.iter()
.map(|job| {
let is_active = job.status.lock().unwrap().is_active();
let children = job.children.lock().unwrap();
let child_count = count_active(&children);
(if is_active { 1 } else { 0 }) + child_count
})
.sum()
}
count_active(&JOBS.lock().unwrap())
}
pub(crate) fn clear() -> crate::Result<()> {
let term = term();
let mut lines = LINES.lock().unwrap();
if *lines > 0 {
let _guard = TERM_LOCK.lock().unwrap();
term.move_cursor_up(*lines)?;
term.move_cursor_left(term.size().1 as usize)?;
term.clear_to_end_of_screen()?;
drop(_guard);
}
*lines = 0;
Ok(())
}
use crate::osc::{ProgressState, clear_progress, set_progress};
pub(crate) fn update_osc_progress(jobs: &[Arc<ProgressJob>]) {
if !crate::osc::is_enabled() || jobs.is_empty() {
return;
}
if let Some((current, total)) = jobs[0].overall_progress() {
if total > 0 {
let overall_percentage =
(current as f64 / total as f64 * 100.0).clamp(0.0, 100.0) as u8;
let mut last_pct = LAST_OSC_PERCENTAGE.lock().unwrap();
let has_failed_jobs = check_for_failed_jobs(jobs);
let osc_state = if has_failed_jobs {
ProgressState::Error
} else {
ProgressState::Normal
};
if *last_pct != Some(overall_percentage) || (has_failed_jobs && last_pct.is_none()) {
set_progress(osc_state, overall_percentage);
*last_pct = Some(overall_percentage);
}
return;
}
}
let (total_progress, job_count, has_failed_jobs) = calculate_average_progress(jobs);
if job_count > 0 {
let overall_percentage =
(total_progress / job_count as f64 * 100.0).clamp(0.0, 100.0) as u8;
let mut last_pct = LAST_OSC_PERCENTAGE.lock().unwrap();
let osc_state = if has_failed_jobs {
ProgressState::Error
} else {
ProgressState::Normal
};
if *last_pct != Some(overall_percentage) || (has_failed_jobs && last_pct.is_none()) {
set_progress(osc_state, overall_percentage);
*last_pct = Some(overall_percentage);
}
}
}
fn check_for_failed_jobs(jobs: &[Arc<ProgressJob>]) -> bool {
let mut stack: Vec<Arc<ProgressJob>> = jobs.to_vec();
while let Some(job) = stack.pop() {
if job.status.lock().unwrap().is_failed() {
return true;
}
let children = job.children.lock().unwrap();
for child in children.iter() {
stack.push(child.clone());
}
}
false
}
fn calculate_average_progress(jobs: &[Arc<ProgressJob>]) -> (f64, usize, bool) {
let mut all_jobs: Vec<Arc<ProgressJob>> = Vec::new();
let mut stack: Vec<Arc<ProgressJob>> = jobs.to_vec();
while let Some(job) = stack.pop() {
all_jobs.push(job.clone());
let children = job.children.lock().unwrap();
for child in children.iter() {
stack.push(child.clone());
}
}
let mut total_progress = 0.0f64;
let mut job_count = 0;
let mut has_failed_jobs = false;
for job in all_jobs.iter() {
if let Some((current, total)) = job.overall_progress() {
if total > 0 {
let progress = (current as f64 / total as f64).clamp(0.0, 1.0);
total_progress += progress;
job_count += 1;
}
} else {
let status = job.status.lock().unwrap();
let progress = match &*status {
s if s.is_running() => 0.5,
s if s.is_done() => 1.0,
s if s.is_failed() => {
has_failed_jobs = true;
1.0
}
_ => 1.0,
};
total_progress += progress;
job_count += 1;
}
}
(total_progress, job_count, has_failed_jobs)
}
pub(crate) fn clear_osc_progress() {
if crate::osc::is_enabled() {
clear_progress();
*LAST_OSC_PERCENTAGE.lock().unwrap() = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_terminal_lock() {
let result = with_terminal_lock(|| 42);
assert_eq!(result, 42);
let result = with_terminal_lock(|| "hello".to_string());
assert_eq!(result, "hello");
}
#[test]
fn test_interval_get_set() {
let original = interval();
set_interval(Duration::from_millis(500));
assert_eq!(interval(), Duration::from_millis(500));
set_interval(original);
}
#[test]
fn test_resize_signal_check() {
let result = check_resize_signaled();
assert!(!result);
}
}