rdar 0.6.3

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! Hand-rolled progress spinner: stderr only, TTY-only,
//! auto-disabled when piped / quiet / JSON. No dependencies.

use std::io::{IsTerminal, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;

const FRAMES: [char; 10] = ['', '', '', '', '', '', '', '', '', ''];

/// Live counters shared between scan workers and the spinner.
#[derive(Default)]
pub struct Counters {
    pub files: AtomicU64,
    pub parsed: AtomicU64,
    pub cached: AtomicU64,
}

pub struct Spinner {
    stop: Arc<AtomicBool>,
    handle: Option<JoinHandle<()>>,
}

impl Spinner {
    /// Starts the spinner thread when stderr is a TTY and `quiet` is false;
    /// otherwise a no-op handle.
    pub fn start(label: &'static str, counters: Arc<Counters>, quiet: bool) -> Spinner {
        let stop = Arc::new(AtomicBool::new(false));
        if quiet || !std::io::stderr().is_terminal() {
            return Spinner { stop, handle: None };
        }
        let stop2 = Arc::clone(&stop);
        let handle = std::thread::spawn(move || {
            let mut frame = 0usize;
            while !stop2.load(Ordering::Relaxed) {
                let files = counters.files.load(Ordering::Relaxed);
                let parsed = counters.parsed.load(Ordering::Relaxed);
                let cached = counters.cached.load(Ordering::Relaxed);
                let mut err = std::io::stderr().lock();
                let _ = write!(
                    err,
                    "\r\u{1b}[2K{} {label} {files} files \u{b7} {parsed} parsed \u{b7} {cached} cached",
                    FRAMES[frame % FRAMES.len()],
                );
                let _ = err.flush();
                frame += 1;
                std::thread::sleep(Duration::from_millis(100));
            }
            let mut err = std::io::stderr().lock();
            let _ = write!(err, "\r\u{1b}[2K");
            let _ = err.flush();
        });
        Spinner {
            stop,
            handle: Some(handle),
        }
    }

    /// Stop and clear the line.
    pub fn finish(mut self) {
        self.stop_inner();
    }

    fn stop_inner(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(handle) = self.handle.take() {
            let _ = handle.join();
        }
    }
}

impl Drop for Spinner {
    fn drop(&mut self) {
        self.stop_inner();
    }
}