stacksdapp_shell/
steps.rs1use colored::Colorize;
4use std::io::{self, Write};
5use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
6use std::sync::{Arc, Mutex, OnceLock};
7use std::thread::{self, JoinHandle};
8use std::time::Duration;
9
10const SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
12fn stdout_lock() -> &'static Mutex<()> {
14 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
15 LOCK.get_or_init(|| Mutex::new(()))
16}
17
18static ACTIVE_SPINNERS: AtomicUsize = AtomicUsize::new(0);
20
21pub fn println_human_safe(line: impl AsRef<str>) {
23 if crate::is_quiet() {
24 return;
25 }
26 let _guard = stdout_lock().lock().unwrap_or_else(|e| e.into_inner());
27 if ACTIVE_SPINNERS.load(Ordering::SeqCst) > 0 {
28 print!("\r\x1b[2K");
29 }
30 println!("{}", line.as_ref());
31 let _ = io::stdout().flush();
32}
33
34pub struct LiveStep {
36 label: String,
37 stop: Arc<AtomicBool>,
38 handle: Option<JoinHandle<()>>,
39 finished: bool,
40}
41
42impl LiveStep {
43 pub fn finish(mut self) {
44 self.complete(true);
45 }
46
47 pub fn fail(mut self) {
48 self.complete(false);
49 }
50
51 fn complete(&mut self, ok: bool) {
52 if self.finished {
53 return;
54 }
55 self.finished = true;
56 self.stop.store(true, Ordering::SeqCst);
57 if let Some(h) = self.handle.take() {
58 let _ = h.join();
59 }
60 let _guard = stdout_lock().lock().unwrap_or_else(|e| e.into_inner());
61 ACTIVE_SPINNERS.fetch_sub(1, Ordering::SeqCst);
62 print!("\r\x1b[2K");
63 if ok {
64 println!(
65 "{} {}",
66 "✓".truecolor(52, 211, 153).bold(),
67 self.label.white()
68 );
69 } else {
70 println!(
71 "{} {}",
72 "✗".truecolor(239, 68, 68).bold(),
73 self.label.white()
74 );
75 }
76 let _ = io::stdout().flush();
77 }
78}
79
80impl Drop for LiveStep {
81 fn drop(&mut self) {
82 if !self.finished {
83 self.complete(false);
84 }
85 }
86}
87
88pub fn print_banner(title: &str) {
90 if crate::is_quiet() {
91 return;
92 }
93 println!();
94 println!("{}", "━".repeat(46).truecolor(75, 85, 99));
95 println!("{:^46}", title.bold().white());
96 println!("{}", "━".repeat(46).truecolor(75, 85, 99));
97 println!();
98}
99
100pub fn kv(key: &str, value: &str) {
101 if crate::is_quiet() {
102 return;
103 }
104 println!("{:<12} {}", key.truecolor(156, 163, 175), value.white());
105}
106
107pub fn rule() {
108 if crate::is_quiet() {
109 return;
110 }
111 println!("{}", "─".repeat(46).truecolor(75, 85, 99));
112}
113
114pub fn begin_step(label: &str) -> LiveStep {
115 if crate::is_quiet() {
116 return LiveStep {
117 label: label.to_string(),
118 stop: Arc::new(AtomicBool::new(true)),
119 handle: None,
120 finished: true,
121 };
122 }
123
124 let stop = Arc::new(AtomicBool::new(false));
125 let stop_c = Arc::clone(&stop);
126 let label_c = label.to_string();
127
128 ACTIVE_SPINNERS.fetch_add(1, Ordering::SeqCst);
129 {
130 let _guard = stdout_lock().lock().unwrap_or_else(|e| e.into_inner());
131 print!(
132 "\r\x1b[2K{} {}",
133 SPINNER[0].truecolor(167, 139, 250),
134 label.truecolor(156, 163, 175)
135 );
136 let _ = io::stdout().flush();
137 }
138
139 let handle = thread::spawn(move || {
140 let mut i = 0usize;
141 while !stop_c.load(Ordering::Relaxed) {
142 {
143 let _guard = stdout_lock().lock().unwrap_or_else(|e| e.into_inner());
144 print!(
145 "\r\x1b[2K{} {}",
146 SPINNER[i % SPINNER.len()].truecolor(167, 139, 250),
147 label_c.truecolor(156, 163, 175)
148 );
149 let _ = io::stdout().flush();
150 }
151 i = i.wrapping_add(1);
152 thread::sleep(Duration::from_millis(80));
153 }
154 });
155
156 LiveStep {
157 label: label.to_string(),
158 stop,
159 handle: Some(handle),
160 finished: false,
161 }
162}
163
164pub fn step_ok(label: &str) {
165 if crate::is_quiet() {
166 return;
167 }
168 println!("{} {}", "✓".truecolor(52, 211, 153).bold(), label.white());
169}
170
171pub fn mint(s: &str) -> colored::ColoredString {
172 s.truecolor(52, 211, 153)
173}
174
175pub fn grey(s: &str) -> colored::ColoredString {
176 s.truecolor(156, 163, 175)
177}
178
179pub fn lavender(s: &str) -> colored::ColoredString {
180 s.truecolor(167, 139, 250)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::begin_step;
186 use crate::{init, Format, Shell};
187
188 #[test]
189 fn quiet_begin_step_does_not_print_completion() {
190 init(Shell {
191 verbosity: 0,
192 quiet: true,
193 format: Format::Human,
194 color: crate::ColorMode::Never,
195 });
196
197 let step = begin_step("Environment configured");
198 step.finish();
199 }
200}