use crate::backend::{
Backend, ascii::AsciiBackend, osc::OscBackend, silent::SilentBackend, tmux::TmuxBackend,
};
use crate::detect::{
self, DetectOptions, EnvReader, Multiplexer, TerminalCapability, detect_multiplexer,
};
use crate::estimate::Estimator;
use crate::throttle::Throttle;
use termpulse_core::ProgressState;
pub struct Controller {
backend: Box<dyn Backend>,
throttle: Throttle,
estimator: Estimator,
capability: TerminalCapability,
}
impl Controller {
pub fn auto() -> Self {
Self::with_options(&DetectOptions::default())
}
pub fn with_options(opts: &DetectOptions) -> Self {
let capability = detect::detect(opts);
let mux = detect_multiplexer(&EnvReader::REAL);
let backend: Box<dyn Backend> = match capability {
TerminalCapability::OscProgress => {
if mux == Multiplexer::Tmux {
Box::new(TmuxBackend::stderr())
} else {
Box::new(OscBackend::stderr())
}
}
TerminalCapability::AsciFallback => Box::new(AsciiBackend::stderr()),
_ => Box::new(SilentBackend),
};
Self {
backend,
throttle: Throttle::new(),
estimator: Estimator::default(),
capability,
}
}
pub fn with_backend(backend: Box<dyn Backend>, capability: TerminalCapability) -> Self {
Self {
backend,
throttle: Throttle::new(),
estimator: Estimator::default(),
capability,
}
}
pub fn set(&mut self, percent: u8, label: &str) {
let clamped = percent.min(100);
self.estimator.update(f64::from(clamped));
if self
.throttle
.should_emit(ProgressState::Normal, Some(clamped), label)
{
self.backend
.emit(ProgressState::Normal, Some(clamped), label);
}
}
pub fn indeterminate(&mut self, label: &str) {
if self
.throttle
.should_emit(ProgressState::Indeterminate, None, label)
{
self.backend.emit(ProgressState::Indeterminate, None, label);
}
}
pub fn done(&mut self, label: &str) {
self.throttle.reset(); self.backend.emit(ProgressState::Normal, Some(100), label);
self.backend.clear();
}
pub fn fail(&mut self, label: &str) {
self.throttle.reset();
self.backend.emit(ProgressState::Error, None, label);
self.backend.clear();
}
pub fn pause(&mut self, label: &str) {
if self
.throttle
.should_emit(ProgressState::Paused, None, label)
{
self.backend.emit(ProgressState::Paused, None, label);
}
}
pub fn clear(&mut self) {
self.throttle.reset();
self.backend.clear();
}
pub fn capability(&self) -> TerminalCapability {
self.capability
}
pub fn backend_name(&self) -> &'static str {
self.backend.name()
}
pub fn eta_display(&self) -> String {
self.estimator.eta_display()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Backend;
use std::sync::{Arc, Mutex};
type CallLog = Arc<Mutex<Vec<(ProgressState, Option<u8>, String)>>>;
#[derive(Clone)]
struct RecordingBackend {
calls: CallLog,
cleared: Arc<Mutex<u32>>,
}
impl RecordingBackend {
fn new() -> Self {
Self {
calls: Arc::new(Mutex::new(Vec::new())),
cleared: Arc::new(Mutex::new(0)),
}
}
}
impl Backend for RecordingBackend {
fn emit(&mut self, state: ProgressState, percent: Option<u8>, label: &str) {
self.calls
.lock()
.unwrap()
.push((state, percent, label.to_string()));
}
fn clear(&mut self) {
*self.cleared.lock().unwrap() += 1;
}
fn name(&self) -> &'static str {
"recording"
}
}
#[test]
fn set_emits_progress() {
let rec = RecordingBackend::new();
let calls = rec.calls.clone();
let mut ctrl = Controller::with_backend(Box::new(rec), TerminalCapability::OscProgress);
ctrl.set(50, "Building");
let log = calls.lock().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(
log[0],
(ProgressState::Normal, Some(50), "Building".to_string())
);
}
#[test]
fn done_emits_100_and_clears() {
let rec = RecordingBackend::new();
let calls = rec.calls.clone();
let cleared = rec.cleared.clone();
let mut ctrl = Controller::with_backend(Box::new(rec), TerminalCapability::OscProgress);
ctrl.done("Done");
let log = calls.lock().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0].1, Some(100));
assert_eq!(*cleared.lock().unwrap(), 1);
}
#[test]
fn fail_emits_error_and_clears() {
let rec = RecordingBackend::new();
let calls = rec.calls.clone();
let cleared = rec.cleared.clone();
let mut ctrl = Controller::with_backend(Box::new(rec), TerminalCapability::OscProgress);
ctrl.fail("Error");
let log = calls.lock().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0].0, ProgressState::Error);
assert_eq!(*cleared.lock().unwrap(), 1);
}
#[test]
fn percent_clamped_to_100() {
let rec = RecordingBackend::new();
let calls = rec.calls.clone();
let mut ctrl = Controller::with_backend(Box::new(rec), TerminalCapability::OscProgress);
ctrl.set(200, "Over");
let log = calls.lock().unwrap();
assert_eq!(log[0].1, Some(100));
}
#[test]
fn throttle_deduplicates() {
let rec = RecordingBackend::new();
let calls = rec.calls.clone();
let mut ctrl = Controller::with_backend(Box::new(rec), TerminalCapability::OscProgress);
ctrl.set(50, "A");
ctrl.set(50, "A"); ctrl.set(50, "A");
let log = calls.lock().unwrap();
assert_eq!(log.len(), 1);
}
}