1use colored::*;
7use std::io::{self, Write};
8use std::sync::OnceLock;
9use std::time::Instant;
10
11static BOOT_T0: OnceLock<Instant> = OnceLock::new();
16
17fn boot_now() -> String {
21 let t0 = BOOT_T0.get_or_init(Instant::now);
22 let elapsed = t0.elapsed().as_secs_f64();
23 format!("[{elapsed:>8.3}]")
24}
25
26pub fn boot_reset_clock() {
30 let _ = BOOT_T0.set(Instant::now());
31}
32
33pub fn action(action: &str, target: &str) {
35 println!("{} {}", format!("[{}]", action).green().bold(), target);
36}
37
38pub fn success(message: &str) {
40 println!("{} {}", "✓".green().bold(), message.green());
41}
42
43pub fn info(message: &str) {
45 println!("{}", message);
46}
47
48pub fn warning(message: &str) {
50 println!("{} {}", "⚠".yellow().bold(), message.yellow());
51}
52
53pub fn error(message: &str) {
55 eprintln!("{} {}", "✗".red().bold(), message.red());
56}
57
58pub fn step(action: &str, target: &str) {
60 println!(" {} {}", format!("-> {}", action).cyan(), target);
61}
62
63pub fn sub_step(message: &str) {
65 println!(" {}", message);
66}
67
68pub fn check(message: &str) {
70 println!(" {} {}", "✓".green(), message);
71}
72
73pub fn cross(message: &str) {
75 eprintln!(" {} {}", "✗".red(), message);
76}
77
78pub fn summary(message: &str) {
80 println!("\n{}", message.dimmed());
81}
82
83const W_NAME: usize = 18;
100
101pub fn boot_banner() {
108 let version = env!("CARGO_PKG_VERSION");
109 let sha = option_env!("ROBONIX_GIT_SHA").unwrap_or("dev");
110 let builder = option_env!("ROBONIX_BUILDER").unwrap_or("unknown");
111 let build_time = option_env!("ROBONIX_BUILD_TIME").unwrap_or("unknown");
112 let rustc = option_env!("ROBONIX_RUSTC").unwrap_or("rustc unknown");
113 let target = option_env!("ROBONIX_TARGET").unwrap_or("unknown");
114
115 let lines = [
116 " ____ __ _ ",
117 " / __ \\____ / /_ ____ ____ (_) __ ",
118 " / /_/ / __ \\/ __ \\/ __ \\/ __ \\/ / |/_/",
119 " / _, _/ /_/ / /_/ / /_/ / / / / /> < ",
120 "/_/ |_|\\____/_.___/\\____/_/ /_/_/_/|_| ",
121 ];
122 println!();
123 for line in &lines {
124 println!("{}", line.cyan().bold());
125 }
126 println!("{}", " Embodied AI Operating System".dimmed(),);
127 println!();
128 let label_w = 9;
131 let row = |k: &str, v: &str| {
132 println!(
133 " {:label_w$} {}",
134 format!("{k}:").bold(),
135 v.dimmed(),
136 label_w = label_w
137 );
138 };
139 row("version", &format!("v{version} ({sha})"));
140 row("built", &format!("{build_time} on {builder}"));
141 row("compiler", rustc);
142 row("target", target);
143 println!();
144}
145
146pub fn boot_start(deploy_name: &str, manifest_path: &str) {
151 boot_reset_clock();
152 println!(
153 "{} booting {}",
154 boot_now().cyan(),
155 deploy_name.bold().green(),
156 );
157 println!("{} manifest {}", boot_now().cyan(), manifest_path.dimmed(),);
158}
159
160pub fn boot_ok(name: &str, detail: &str) {
164 println!(
165 "\r\x1b[K{} {} {:<width$} {}",
166 boot_now().cyan(),
167 "[ OK ]".green().bold(),
168 name,
169 detail.dimmed(),
170 width = W_NAME,
171 );
172}
173
174pub fn boot_fail(name: &str, detail: &str) {
176 eprintln!(
177 "\r\x1b[K{} {} {:<width$} {}",
178 boot_now().cyan(),
179 "[FAIL]".red().bold(),
180 name,
181 detail.red(),
182 width = W_NAME,
183 );
184}
185
186pub fn boot_skip(name: &str, detail: &str) {
189 println!(
190 "{} {} {:<width$} {}",
191 boot_now().cyan(),
192 "[SKIP]".yellow(),
193 name,
194 detail.dimmed(),
195 width = W_NAME,
196 );
197}
198
199pub fn boot_note(name: &str, detail: &str) {
202 println!(
203 "{} {} {:<width$} {}",
204 boot_now().cyan(),
205 "[ → ]".cyan(),
206 name,
207 detail.dimmed(),
208 width = W_NAME,
209 );
210}
211
212pub fn boot_wait(name: &str, detail: &str) {
214 println!(
215 "{} {} {:<width$} {}",
216 boot_now().cyan(),
217 "[....]".cyan(),
218 name,
219 detail.dimmed(),
220 width = W_NAME,
221 );
222}
223
224pub fn boot_section(label: &str) {
226 println!(
227 "\n{} {} {} {}",
228 boot_now().cyan(),
229 "::".dimmed(),
230 label.bold().yellow(),
231 "::".dimmed(),
232 );
233}
234
235pub fn boot_progress(name: &str, detail: &str, frame: usize) {
240 const GLYPHS: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
241 let g = GLYPHS[frame % GLYPHS.len()];
242 print!(
243 "\r\x1b[K{} {} {:<width$} {}",
244 boot_now().cyan(),
245 format!("[ {g} ]").cyan(),
246 name,
247 detail.dimmed(),
248 width = W_NAME,
249 );
250 let _ = io::stdout().flush();
251}
252
253pub fn boot_summary(ok: usize, total: usize, hint: &str) {
255 let elapsed = BOOT_T0
256 .get()
257 .map(|t| t.elapsed().as_secs_f64())
258 .unwrap_or(0.0);
259 let badge = if ok == total {
260 "OK".green().bold()
261 } else {
262 "DEGRADED".yellow().bold()
263 };
264 println!();
265 println!(
266 "[ {badge} ] robonix up — {ok}/{total} components in {elapsed:.3}s {}",
267 hint.dimmed()
268 );
269}
270
271pub struct Spinner {
273 message: String,
274 frames: Vec<char>,
275 handle: Option<tokio::task::JoinHandle<()>>,
276}
277
278impl Spinner {
279 pub fn new(message: String) -> Self {
281 Self {
282 message,
283 frames: vec!['|', '/', '-', '\\'],
284 handle: None,
285 }
286 }
287
288 pub fn start(&mut self) {
290 let message = self.message.clone();
291 let mut frame = 0;
292 let frames = self.frames.clone();
293
294 let handle = tokio::spawn(async move {
295 loop {
296 let spinner_char = frames[frame % frames.len()];
297 let line = format!(" {} {}", spinner_char, message);
298 print!("\r{}", line);
299 let _ = io::stdout().flush();
300 frame += 1;
301 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
302 }
303 });
304
305 self.handle = Some(handle);
306 }
307
308 pub fn finish_success(&mut self, final_message: &str) {
310 if let Some(handle) = self.handle.take() {
311 handle.abort();
312 }
313 print!("\r\x1b[K");
315 let line = format!(" {} {}", "✓".green(), final_message.green());
316 println!("{}", line);
317 let _ = io::stdout().flush();
318 }
319
320 pub fn finish_error(&mut self, final_message: &str) {
322 if let Some(handle) = self.handle.take() {
323 handle.abort();
324 }
325 print!("\r\x1b[K");
327 let line = format!(" {} {}", "✗".red(), final_message.red());
328 println!("{}", line);
329 let _ = io::stdout().flush();
330 }
331}
332
333impl Drop for Spinner {
334 fn drop(&mut self) {
335 if let Some(handle) = self.handle.take() {
336 handle.abort();
337 }
338 }
339}