Skip to main content

scx_forge_agent/
progress.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: GPL-2.0-only
3//! TTY-only progress spinners for local blocking phases.
4
5use std::io::{IsTerminal, Write};
6use std::sync::{
7    atomic::{AtomicBool, Ordering},
8    Arc,
9};
10use std::thread::{self, JoinHandle};
11use std::time::Duration;
12
13use crate::color::Style;
14
15pub struct ProgressSpinner {
16    running: Option<Arc<AtomicBool>>,
17    handle: Option<JoinHandle<()>>,
18}
19
20impl ProgressSpinner {
21    pub fn stdout(label: impl Into<String>, color: Style) -> Self {
22        if !std::io::stdout().is_terminal() {
23            return Self {
24                running: None,
25                handle: None,
26            };
27        }
28
29        let label = label.into();
30        let running = Arc::new(AtomicBool::new(true));
31        let thread_running = running.clone();
32        let handle = thread::spawn(move || {
33            let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
34            let mut i = 0usize;
35            while thread_running.load(Ordering::Relaxed) {
36                print!(
37                    "\r{} {}",
38                    color.dim(frames[i % frames.len()]),
39                    color.dim(&label)
40                );
41                let _ = std::io::stdout().flush();
42                i += 1;
43                thread::sleep(Duration::from_millis(80));
44            }
45        });
46
47        Self {
48            running: Some(running),
49            handle: Some(handle),
50        }
51    }
52
53    pub fn stop(&mut self) {
54        if let Some(running) = self.running.take() {
55            running.store(false, Ordering::Relaxed);
56        }
57        if let Some(handle) = self.handle.take() {
58            let _ = handle.join();
59            print!("\r\x1b[K");
60            let _ = std::io::stdout().flush();
61        }
62    }
63}
64
65impl Drop for ProgressSpinner {
66    fn drop(&mut self) {
67        self.stop();
68    }
69}