Skip to main content

canic_host/terminal/
activity.rs

1//! Module: terminal::activity
2//!
3//! Responsibility: keep one interactive activity line alive during blocking work.
4//! Does not own: command execution, result reporting, or durable phase timing.
5//! Boundary: non-interactive output is append-only; ANSI cursor control is TTY-only.
6
7use super::TerminalStyle;
8use std::{
9    io::{self, Write},
10    sync::{
11        Arc,
12        atomic::{AtomicBool, Ordering},
13    },
14    thread::{self, JoinHandle},
15    time::{Duration, Instant},
16};
17
18const FRAMES: [&str; 4] = ["|", "/", "-", "\\"];
19const FRAME_INTERVAL: Duration = Duration::from_millis(120);
20
21pub struct TerminalActivity {
22    stop: Arc<AtomicBool>,
23    handle: Option<JoinHandle<()>>,
24    interactive: bool,
25}
26
27impl TerminalActivity {
28    #[must_use]
29    pub fn start(message: impl Into<String>) -> Self {
30        let message = message.into();
31        let style = TerminalStyle::detected();
32        if !style.interactive() {
33            println!("  .. {message}");
34            return Self {
35                stop: Arc::new(AtomicBool::new(true)),
36                handle: None,
37                interactive: false,
38            };
39        }
40
41        let stop = Arc::new(AtomicBool::new(false));
42        let worker_stop = Arc::clone(&stop);
43        let handle = thread::spawn(move || {
44            let started_at = Instant::now();
45            let mut frame = 0;
46            while !worker_stop.load(Ordering::Acquire) {
47                let elapsed = started_at.elapsed().as_secs();
48                print!(
49                    "\r\u{1b}[2K  {} {}  {}",
50                    style.warning(FRAMES[frame % FRAMES.len()]),
51                    message,
52                    style.muted(&format!("{elapsed}s"))
53                );
54                let _ = io::stdout().flush();
55                frame += 1;
56                thread::park_timeout(FRAME_INTERVAL);
57            }
58        });
59
60        Self {
61            stop,
62            handle: Some(handle),
63            interactive: true,
64        }
65    }
66
67    pub fn finish(mut self) {
68        self.stop_and_clear();
69    }
70
71    fn stop_and_clear(&mut self) {
72        self.stop.store(true, Ordering::Release);
73        if let Some(handle) = self.handle.take() {
74            handle.thread().unpark();
75            let _ = handle.join();
76        }
77        if self.interactive {
78            print!("\r\u{1b}[2K");
79            let _ = io::stdout().flush();
80        }
81    }
82}
83
84impl Drop for TerminalActivity {
85    fn drop(&mut self) {
86        self.stop_and_clear();
87    }
88}