use crate::agent::ui::colors::{ansi, format_elapsed};
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const ANIMATION_INTERVAL_MS: u64 = 80;
const PHRASE_CHANGE_INTERVAL_SECS: u64 = 8;
const WITTY_PHRASES: &[&str] = &[
"Analyzing your codebase...",
"Consulting the digital spirits...",
"Warming up the AI hamsters...",
"Polishing the algorithms...",
"Brewing fresh bytes...",
"Engaging cognitive processors...",
"Compiling brilliance...",
"Untangling neural nets...",
"Converting coffee into insights...",
"Scanning for patterns...",
"Traversing the AST...",
"Checking dependencies...",
"Looking for security issues...",
"Mapping the architecture...",
"Detecting frameworks...",
"Parsing configurations...",
"Analyzing code patterns...",
"Deep diving into your code...",
"Searching for vulnerabilities...",
"Exploring the codebase...",
"Processing your request...",
"Thinking deeply about this...",
"Gathering context...",
"Reading documentation...",
"Inspecting files...",
];
const TIPS: &[&str] = &[
"Tip: Use /model to switch AI models...",
"Tip: Use /provider to change providers...",
"Tip: Type /help for available commands...",
"Tip: Use /clear to reset conversation...",
"Tip: Try 'sync-ctl analyze' for full analysis...",
"Tip: Security scans support 5 modes (lightning to paranoid)...",
];
#[derive(Debug)]
pub enum SpinnerMessage {
UpdateText(String),
ToolExecuting { name: String, description: String },
ToolComplete { name: String },
Thinking(String),
Stop,
}
pub struct Spinner {
sender: mpsc::Sender<SpinnerMessage>,
is_running: Arc<AtomicBool>,
}
impl Spinner {
pub fn new(initial_text: &str) -> Self {
let (sender, receiver) = mpsc::channel(32);
let is_running = Arc::new(AtomicBool::new(true));
let is_running_clone = is_running.clone();
let initial = initial_text.to_string();
tokio::spawn(async move {
run_spinner(receiver, is_running_clone, initial).await;
});
Self { sender, is_running }
}
pub async fn set_text(&self, text: &str) {
let _ = self
.sender
.send(SpinnerMessage::UpdateText(text.to_string()))
.await;
}
pub async fn tool_executing(&self, name: &str, description: &str) {
let _ = self
.sender
.send(SpinnerMessage::ToolExecuting {
name: name.to_string(),
description: description.to_string(),
})
.await;
}
pub async fn tool_complete(&self, name: &str) {
let _ = self
.sender
.send(SpinnerMessage::ToolComplete {
name: name.to_string(),
})
.await;
}
pub async fn thinking(&self, subject: &str) {
let _ = self
.sender
.send(SpinnerMessage::Thinking(subject.to_string()))
.await;
}
pub async fn stop(&self) {
let _ = self.sender.send(SpinnerMessage::Stop).await;
tokio::time::sleep(Duration::from_millis(50)).await;
}
pub fn is_running(&self) -> bool {
self.is_running.load(Ordering::SeqCst)
}
}
async fn run_spinner(
mut receiver: mpsc::Receiver<SpinnerMessage>,
is_running: Arc<AtomicBool>,
initial_text: String,
) {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let start_time = Instant::now();
let mut frame_index = 0;
let mut current_text = initial_text;
let mut last_phrase_change = Instant::now();
let mut phrase_index = 0;
let mut current_tool: Option<String> = None;
let mut tools_completed: usize = 0;
let mut has_printed_tool_line = false;
let mut interval = tokio::time::interval(Duration::from_millis(ANIMATION_INTERVAL_MS));
let mut rng = StdRng::from_os_rng();
print!("{}", ansi::HIDE_CURSOR);
let _ = io::stdout().flush();
loop {
tokio::select! {
_ = interval.tick() => {
if !is_running.load(Ordering::SeqCst) {
break;
}
let elapsed = start_time.elapsed().as_secs();
let frame = SPINNER_FRAMES[frame_index % SPINNER_FRAMES.len()];
frame_index += 1;
if current_tool.is_none() && last_phrase_change.elapsed().as_secs() >= PHRASE_CHANGE_INTERVAL_SECS {
if rng.random_bool(0.25) {
let tip_idx = rng.random_range(0..TIPS.len());
current_text = TIPS[tip_idx].to_string();
} else {
phrase_index = (phrase_index + 1) % WITTY_PHRASES.len();
current_text = WITTY_PHRASES[phrase_index].to_string();
}
last_phrase_change = Instant::now();
}
if has_printed_tool_line {
if let Some(ref tool) = current_tool {
println!("{}{} {}🔧 {}{}", ansi::CURSOR_UP,
ansi::CLEAR_LINE,
ansi::PURPLE,
tool,
ansi::RESET,
);
}
print!("\r{} {}{}{} {} {}{}({}){}",
ansi::CLEAR_LINE,
ansi::CYAN,
frame,
ansi::RESET,
current_text,
ansi::GRAY,
ansi::DIM,
format_elapsed(elapsed),
ansi::RESET
);
} else {
print!("\r{} {}{}{} {} {}{}({}){}",
ansi::CLEAR_LINE,
ansi::CYAN,
frame,
ansi::RESET,
current_text,
ansi::GRAY,
ansi::DIM,
format_elapsed(elapsed),
ansi::RESET
);
}
let _ = io::stdout().flush();
}
Some(msg) = receiver.recv() => {
match msg {
SpinnerMessage::UpdateText(text) => {
current_text = text;
}
SpinnerMessage::ToolExecuting { name, description } => {
if !has_printed_tool_line {
print!("\r{} {}🔧 {}{}\n",
ansi::CLEAR_LINE,
ansi::PURPLE,
name,
ansi::RESET,
);
has_printed_tool_line = true;
}
current_tool = Some(name);
current_text = description;
last_phrase_change = Instant::now();
}
SpinnerMessage::ToolComplete { name: _ } => {
tools_completed += 1;
current_tool = None;
phrase_index = (phrase_index + 1) % WITTY_PHRASES.len();
current_text = WITTY_PHRASES[phrase_index].to_string();
}
SpinnerMessage::Thinking(subject) => {
current_text = format!("💭 {}", subject);
}
SpinnerMessage::Stop => {
is_running.store(false, Ordering::SeqCst);
break;
}
}
}
}
}
if has_printed_tool_line {
print!("\r{}", ansi::CLEAR_LINE);
print!("{}{}", ansi::CURSOR_UP, ansi::CLEAR_LINE);
} else {
print!("\r{}", ansi::CLEAR_LINE);
}
if tools_completed > 0 {
println!(
" {}✓{} {} tool{} used",
ansi::SUCCESS,
ansi::RESET,
tools_completed,
if tools_completed == 1 { "" } else { "s" }
);
}
print!("{}", ansi::SHOW_CURSOR);
let _ = io::stdout().flush();
}
pub struct InlineSpinner {
frames: Vec<&'static str>,
current: usize,
}
impl InlineSpinner {
pub fn new() -> Self {
Self {
frames: SPINNER_FRAMES.to_vec(),
current: 0,
}
}
pub fn next_frame(&mut self) -> &'static str {
let frame = self.frames[self.current % self.frames.len()];
self.current += 1;
frame
}
pub fn print(&mut self, message: &str) {
let frame = self.next_frame();
print!("{}{} {}", ansi::CLEAR_LINE, frame, message);
let _ = io::stdout().flush();
}
}
impl Default for InlineSpinner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inline_spinner() {
let mut spinner = InlineSpinner::new();
assert_eq!(spinner.next_frame(), "⠋");
assert_eq!(spinner.next_frame(), "⠙");
assert_eq!(spinner.next_frame(), "⠹");
}
}