1use colored::*;
7use std::io::{self, Write};
8use std::sync::OnceLock;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::Instant;
11
12static BOOT_T0: OnceLock<Instant> = OnceLock::new();
17static BOOT_VERBOSE: AtomicBool = AtomicBool::new(false);
18
19pub fn set_boot_verbose(verbose: bool) {
20 BOOT_VERBOSE.store(verbose, Ordering::Relaxed);
21}
22
23pub fn boot_verbose() -> bool {
24 BOOT_VERBOSE.load(Ordering::Relaxed)
25}
26
27fn clear_progress_prefix() -> &'static str {
28 if boot_verbose() { "" } else { "\r\x1b[K" }
29}
30
31fn boot_now() -> String {
35 let t0 = BOOT_T0.get_or_init(Instant::now);
36 let elapsed = t0.elapsed().as_secs_f64();
37 format!("[{elapsed:>8.3}]")
38}
39
40pub fn boot_reset_clock() {
44 let _ = BOOT_T0.set(Instant::now());
45}
46
47pub fn action(action: &str, target: &str) {
49 println!("{} {}", format!("[{}]", action).green().bold(), target);
50}
51
52pub fn success(message: &str) {
54 println!("{} {}", "✓".green().bold(), message.green());
55}
56
57pub fn info(message: &str) {
59 println!("{}", message);
60}
61
62pub fn warning(message: &str) {
64 println!("{} {}", "⚠".yellow().bold(), message.yellow());
65}
66
67pub fn error(message: &str) {
69 eprintln!("{} {}", "✗".red().bold(), message.red());
70}
71
72pub fn step(action: &str, target: &str) {
74 println!(" {} {}", format!("-> {}", action).cyan(), target);
75}
76
77pub fn sub_step(message: &str) {
79 println!(" {}", message);
80}
81
82pub fn check(message: &str) {
84 println!(" {} {}", "✓".green(), message);
85}
86
87pub fn cross(message: &str) {
89 eprintln!(" {} {}", "✗".red(), message);
90}
91
92pub fn summary(message: &str) {
94 println!("\n{}", message.dimmed());
95}
96
97const W_NAME: usize = 18;
114
115pub fn boot_banner() {
122 let version = env!("CARGO_PKG_VERSION");
123 let sha = option_env!("ROBONIX_GIT_SHA").unwrap_or("dev");
124 let builder = option_env!("ROBONIX_BUILDER").unwrap_or("unknown");
125 let build_time = option_env!("ROBONIX_BUILD_TIME").unwrap_or("unknown");
126 let rustc = option_env!("ROBONIX_RUSTC").unwrap_or("rustc unknown");
127 let target = option_env!("ROBONIX_TARGET").unwrap_or("unknown");
128
129 let lines = [
130 " ____ __ _ ",
131 " / __ \\____ / /_ ____ ____ (_) __ ",
132 " / /_/ / __ \\/ __ \\/ __ \\/ __ \\/ / |/_/",
133 " / _, _/ /_/ / /_/ / /_/ / / / / /> < ",
134 "/_/ |_|\\____/_.___/\\____/_/ /_/_/_/|_| ",
135 ];
136 println!();
137 for line in &lines {
138 println!("{}", line.cyan().bold());
139 }
140 println!("{}", " Embodied AI Operating System".dimmed(),);
141 println!();
142 let label_w = 9;
145 let row = |k: &str, v: &str| {
146 println!(
147 " {:label_w$} {}",
148 format!("{k}:").bold(),
149 v.dimmed(),
150 label_w = label_w
151 );
152 };
153 row("version", &format!("v{version} ({sha})"));
154 row("built", &format!("{build_time} on {builder}"));
155 row("compiler", rustc);
156 row("target", target);
157 println!();
158}
159
160pub fn boot_start(deploy_name: &str, manifest_path: &str) {
165 boot_reset_clock();
166 println!(
167 "{} booting {}",
168 boot_now().cyan(),
169 deploy_name.bold().green(),
170 );
171 println!("{} manifest {}", boot_now().cyan(), manifest_path.dimmed(),);
172}
173
174pub fn boot_ok(name: &str, detail: &str) {
178 println!(
179 "{}{} {} {:<width$} {}",
180 clear_progress_prefix(),
181 boot_now().cyan(),
182 "[ OK ]".green().bold(),
183 name,
184 detail.dimmed(),
185 width = W_NAME,
186 );
187}
188
189pub fn boot_fail(name: &str, detail: &str) {
191 eprintln!(
192 "{}{} {} {:<width$} {}",
193 clear_progress_prefix(),
194 boot_now().cyan(),
195 "[FAIL]".red().bold(),
196 name,
197 detail.red(),
198 width = W_NAME,
199 );
200}
201
202pub fn boot_skip(name: &str, detail: &str) {
205 println!(
206 "{}{} {} {:<width$} {}",
207 clear_progress_prefix(),
208 boot_now().cyan(),
209 "[SKIP]".yellow(),
210 name,
211 detail.dimmed(),
212 width = W_NAME,
213 );
214}
215
216pub fn boot_note(name: &str, detail: &str) {
219 println!(
220 "{}{} {} {:<width$} {}",
221 clear_progress_prefix(),
222 boot_now().cyan(),
223 "[ → ]".cyan(),
224 name,
225 detail.dimmed(),
226 width = W_NAME,
227 );
228}
229
230pub fn boot_update_avail(name: &str, detail: &str) {
235 println!(
236 "{}{} {} {:<width$} {}",
237 clear_progress_prefix(),
238 boot_now().cyan(),
239 "[ ↑ ]".yellow().bold(),
240 name,
241 detail.dimmed(),
242 width = W_NAME,
243 );
244}
245
246pub fn boot_wait(name: &str, detail: &str) {
248 println!(
249 "{}{} {} {:<width$} {}",
250 clear_progress_prefix(),
251 boot_now().cyan(),
252 "[....]".cyan(),
253 name,
254 detail.dimmed(),
255 width = W_NAME,
256 );
257}
258
259pub fn boot_section(label: &str) {
261 println!(
262 "{}\n{} {} {} {}",
263 clear_progress_prefix(),
264 boot_now().cyan(),
265 "::".dimmed(),
266 label.bold().yellow(),
267 "::".dimmed(),
268 );
269}
270
271pub fn boot_progress(name: &str, detail: &str, frame: usize) {
276 if boot_verbose() {
277 return;
278 }
279 const GLYPHS: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
280 let g = GLYPHS[frame % GLYPHS.len()];
281 print!(
282 "\r\x1b[K{} {} {:<width$} {}",
283 boot_now().cyan(),
284 format!("[ {g} ]").cyan(),
285 name,
286 detail.dimmed(),
287 width = W_NAME,
288 );
289 let _ = io::stdout().flush();
290}
291
292pub fn boot_summary(ok: usize, total: usize, hint: &str) {
294 let elapsed = BOOT_T0
295 .get()
296 .map(|t| t.elapsed().as_secs_f64())
297 .unwrap_or(0.0);
298 let badge = if ok == total {
299 "OK".green().bold()
300 } else {
301 "DEGRADED".yellow().bold()
302 };
303 println!();
304 println!(
305 "[ {badge} ] robonix up — {ok}/{total} components in {elapsed:.3}s {}",
306 hint.dimmed()
307 );
308}
309
310pub struct Spinner {
312 message: String,
313 frames: Vec<char>,
314 handle: Option<tokio::task::JoinHandle<()>>,
315}
316
317impl Spinner {
318 pub fn new(message: String) -> Self {
320 Self {
321 message,
322 frames: vec!['|', '/', '-', '\\'],
323 handle: None,
324 }
325 }
326
327 pub fn start(&mut self) {
329 let message = self.message.clone();
330 let mut frame = 0;
331 let frames = self.frames.clone();
332
333 let handle = tokio::spawn(async move {
334 loop {
335 let spinner_char = frames[frame % frames.len()];
336 let line = format!(" {} {}", spinner_char, message);
337 print!("\r{}", line);
338 let _ = io::stdout().flush();
339 frame += 1;
340 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
341 }
342 });
343
344 self.handle = Some(handle);
345 }
346
347 pub fn finish_success(&mut self, final_message: &str) {
349 if let Some(handle) = self.handle.take() {
350 handle.abort();
351 }
352 print!("\r\x1b[K");
354 let line = format!(" {} {}", "✓".green(), final_message.green());
355 println!("{}", line);
356 let _ = io::stdout().flush();
357 }
358
359 pub fn finish_error(&mut self, final_message: &str) {
361 if let Some(handle) = self.handle.take() {
362 handle.abort();
363 }
364 print!("\r\x1b[K");
366 let line = format!(" {} {}", "✗".red(), final_message.red());
367 println!("{}", line);
368 let _ = io::stdout().flush();
369 }
370}
371
372impl Drop for Spinner {
373 fn drop(&mut self) {
374 if let Some(handle) = self.handle.take() {
375 handle.abort();
376 }
377 }
378}