| 61 | } |
| 62 | |
| 63 | fn spawn_interactive_spinner( |
| 64 | message: std::sync::Arc<std::sync::Mutex<String>>, |
| 65 | stop: std::sync::Arc<std::sync::atomic::AtomicBool>, |
| 66 | ) -> std::thread::JoinHandle<()> { |
| 67 | let msg = message.clone(); |
| 68 | let stp = stop.clone(); |
| 69 | // Hide cursor while spinner is active. |
| 70 | let _ = write!(std::io::stderr(), "\x1b[?25l"); |
| 71 | let _ = std::io::stderr().flush(); |
| 72 | std::thread::spawn(move || { |
| 73 | let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; |
| 74 | let mut idx = 0usize; |
| 75 | while !stp.load(std::sync::atomic::Ordering::Relaxed) { |
| 76 | let text = msg |
| 77 | .lock() |
| 78 | .map_or_else(|_| String::new(), |locked| locked.clone()); |
| 79 | if !text.is_empty() { |
| 80 | let frame = frames[idx % frames.len()]; |
| 81 | idx += 1; |
| 82 | // Truncate to avoid line wrapping on typical terminals. |
| 83 | let display: std::borrow::Cow<str> = if text.len() > 50 { |
| 84 | format!("…{}", &text[text.len() - 49..]).into() |
| 85 | } else { |
| 86 | text.as_str().into() |
| 87 | }; |
| 88 | let mut stderr = std::io::stderr(); |
| 89 | let _ = write!(stderr, "\r\x1b[2K{} {}", frame, display); |
| 90 | let _ = stderr.flush(); |
| 91 | } |
| 92 | std::thread::sleep(std::time::Duration::from_millis(80)); |
| 93 | } |
| 94 | }) |
| 95 | } |
| 96 | |
| 97 | pub(crate) fn set_message(&self, msg: &str) { |
| 98 | if let Ok(mut locked) = self.message.lock() { |