1use comfy_table::Table as ComfyTable;
14use indicatif::{ProgressBar, ProgressStyle};
15use owo_colors::OwoColorize;
16use std::io::{self, IsTerminal};
17use std::time::Duration;
18
19pub use typub_log::{FnReporter, NullReporter, ProgressReporter};
21pub use typub_log::{debug, error, info, init, is_verbose, trace, warn};
22
23pub mod i18n;
24
25fn use_colors() -> bool {
27 io::stdout().is_terminal() && std::env::var("NO_COLOR").is_err()
28}
29
30mod icons {
33 pub const SUCCESS: &str = "✓";
34 pub const ERROR: &str = "✗";
35 pub const WARNING: &str = "⚠";
36 pub const INFO: &str = "ℹ";
37 pub const ARROW: &str = "→";
38 pub const PENDING: &str = "○";
39 pub const DONE: &str = "●";
40 pub const SKIP: &str = "⊘";
41}
42
43macro_rules! styled {
47 ($text:expr, $color:ident) => {
48 if use_colors() {
49 format!("{}", $text.$color())
50 } else {
51 $text.to_string()
52 }
53 };
54 ($text:expr, $color:ident, bold) => {
55 if use_colors() {
56 format!("{}", $text.$color().bold())
57 } else {
58 $text.to_string()
59 }
60 };
61}
62
63pub fn success(message: &str) {
67 eprintln!("{} {}", styled!(icons::SUCCESS, green), message);
68}
69
70pub fn error(message: &str) {
72 eprintln!("{} {}", styled!(icons::ERROR, red), styled!(message, red));
73}
74
75pub fn warn(message: &str) {
77 eprintln!(
78 "{} {}",
79 styled!(icons::WARNING, yellow),
80 styled!(message, yellow)
81 );
82}
83
84pub fn info(message: &str) {
86 eprintln!("{} {}", styled!(icons::INFO, blue), message);
87}
88
89pub fn debug(message: &str) {
91 if is_verbose() {
92 eprintln!("{} {}", styled!("[debug]", bright_black), message);
93 }
94}
95
96pub fn header(title: &str) {
100 eprintln!();
101 eprintln!("{}", styled!(title, cyan, bold));
102 let separator = "─".repeat(title.len().max(40));
103 eprintln!("{}", styled!(&separator, bright_black));
104}
105
106pub fn step(number: usize, total: usize, message: &str) {
108 let step_info = format!("[{}/{}]", number, total);
109 eprintln!("{} {}", styled!(&step_info, cyan), message);
110}
111
112pub fn item(label: &str, value: &str) {
114 eprintln!(
115 " {} {}: {}",
116 styled!(icons::ARROW, bright_black),
117 styled!(label, cyan),
118 value
119 );
120}
121
122pub fn platform_status(platform: &str, published: bool, url: Option<&str>) {
124 let (icon, platform_styled) = if published {
125 (styled!(icons::DONE, green), styled!(platform, green))
126 } else {
127 (
128 styled!(icons::PENDING, bright_black),
129 styled!(platform, bright_black),
130 )
131 };
132
133 if let Some(url) = url {
134 eprintln!(
135 " {} {} {}",
136 icon,
137 platform_styled,
138 styled!(url, bright_black)
139 );
140 } else {
141 eprintln!(" {} {}", icon, platform_styled);
142 }
143}
144
145pub fn log_publish_start(title: &str, platforms: &[&str]) {
149 header(&format!("Publishing: {}", title));
150 info(&format!("Targets: {}", platforms.join(", ")));
151}
152
153pub fn log_publish_success(platform: &str, url: Option<&str>) {
155 if let Some(url) = url {
156 eprintln!(
157 " {} {} {} {}",
158 styled!(icons::SUCCESS, green),
159 styled!(platform, green, bold),
160 styled!(icons::ARROW, bright_black),
161 styled!(url, cyan)
162 );
163 } else {
164 eprintln!(
165 " {} {}",
166 styled!(icons::SUCCESS, green),
167 styled!(platform, green, bold)
168 );
169 }
170}
171
172pub fn log_skip(platform: &str, reason: &str) {
174 eprintln!(
175 " {} {} ({})",
176 styled!(icons::SKIP, bright_black),
177 styled!(platform, bright_black),
178 styled!(reason, bright_black)
179 );
180}
181
182pub fn log_dry_run(platform: &str) {
184 eprintln!(
185 " {} Would publish to: {}",
186 styled!("[DRY RUN]", yellow),
187 platform
188 );
189}
190
191pub fn spinner(message: &str) -> ProgressBar {
195 let pb = ProgressBar::new_spinner();
196 let mut style = ProgressStyle::default_spinner();
197 if let Ok(s) = style.clone().template("{spinner:.cyan} {msg}") {
198 style = s.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏");
199 }
200 pb.set_style(style);
201 pb.set_message(message.to_string());
202 pb.enable_steady_tick(Duration::from_millis(80));
203 pb
204}
205
206pub fn spinner_success(pb: ProgressBar, message: &str) {
208 if let Ok(style) = ProgressStyle::default_spinner().template("{msg}") {
209 pb.set_style(style);
210 }
211 pb.finish_with_message(format!("{} {}", styled!(icons::SUCCESS, green), message));
212}
213
214pub fn spinner_error(pb: ProgressBar, message: &str) {
216 if let Ok(style) = ProgressStyle::default_spinner().template("{msg}") {
217 pb.set_style(style);
218 }
219 pb.finish_with_message(format!(
220 "{} {}",
221 styled!(icons::ERROR, red),
222 styled!(message, red)
223 ));
224}
225
226pub fn progress_bar(len: u64, message: &str) -> ProgressBar {
228 let pb = ProgressBar::new(len);
229 let mut style = ProgressStyle::default_bar();
230 if let Ok(s) = style
231 .clone()
232 .template("{msg} [{bar:30.cyan/bright_black}] {pos}/{len}")
233 {
234 style = s.progress_chars("━━─");
235 }
236 pb.set_style(style);
237 pb.set_message(message.to_string());
238 pb
239}
240
241pub struct MultiProgress {
245 name: String,
246 current: usize,
247 total: usize,
248 spinner: Option<ProgressBar>,
249}
250
251impl MultiProgress {
252 pub fn new(name: &str, total: usize) -> Self {
253 header(name);
254 Self {
255 name: name.to_string(),
256 current: 0,
257 total,
258 spinner: None,
259 }
260 }
261
262 pub fn step(&mut self, message: &str) {
264 if let Some(pb) = self.spinner.take() {
266 spinner_success(pb, "Done");
267 }
268
269 self.current += 1;
270 let step_msg = format!("[{}/{}] {}", self.current, self.total, message);
271 self.spinner = Some(spinner(&step_msg));
272 }
273
274 pub fn finish(self) {
276 if let Some(pb) = self.spinner {
277 spinner_success(pb, "Done");
278 }
279 success(&format!("{} completed", self.name));
280 }
281
282 pub fn finish_error(self, err: &str) {
284 if let Some(pb) = self.spinner {
285 spinner_error(pb, err);
286 }
287 error(&format!("{} failed: {}", self.name, err));
288 }
289}
290
291pub struct Table {
295 headers: Vec<String>,
296 rows: Vec<Vec<String>>,
297 widths: Vec<usize>,
298}
299
300impl Table {
301 pub fn new(headers: &[&str]) -> Self {
302 let headers: Vec<String> = headers.iter().map(|s| s.to_string()).collect();
303 let widths = headers.iter().map(|h| h.len()).collect();
304 Self {
305 headers,
306 rows: Vec::new(),
307 widths,
308 }
309 }
310
311 pub fn add_row(&mut self, row: &[&str]) {
312 let row: Vec<String> = row.iter().map(|s| s.to_string()).collect();
313 for (i, cell) in row.iter().enumerate() {
314 if i < self.widths.len() {
315 self.widths[i] = self.widths[i].max(cell.len());
316 }
317 }
318 self.rows.push(row);
319 }
320
321 pub fn print(&self) {
323 let header_cells: Vec<String> = self
325 .headers
326 .iter()
327 .enumerate()
328 .map(|(i, h)| format!("{:width$}", h, width = self.widths[i]))
329 .collect();
330 let header_line = header_cells.join(" ");
331 println!("{}", styled!(&header_line, bold));
332
333 let sep: Vec<String> = self.widths.iter().map(|w| "─".repeat(*w)).collect();
335 println!("{}", styled!(&sep.join("──"), bright_black));
336
337 for row in &self.rows {
339 let cells: Vec<String> = row
340 .iter()
341 .enumerate()
342 .map(|(i, cell)| {
343 let width = self.widths.get(i).copied().unwrap_or(cell.len());
344 format!("{:width$}", cell, width = width)
345 })
346 .collect();
347 println!("{}", cells.join(" "));
348 }
349 }
350}
351
352pub struct IndicatifReporter {
358 bar: ProgressBar,
359}
360
361impl IndicatifReporter {
362 pub fn spinner(message: &str) -> Self {
364 let bar = spinner(message);
365 Self { bar }
366 }
367
368 pub fn progress(len: u64, message: &str) -> Self {
370 let bar = progress_bar(len, message);
371 Self { bar }
372 }
373}
374
375impl ProgressReporter for IndicatifReporter {
376 fn set_message(&self, message: &str) {
377 self.bar.set_message(message.to_string());
378 }
379
380 fn set_progress(&self, current: u64, total: u64) {
381 if total > 0 {
382 self.bar.set_length(total);
383 self.bar.set_position(current);
384 }
385 }
386
387 fn finish_success(&self, message: &str) {
388 spinner_success_ref(&self.bar, message);
389 }
390
391 fn finish_error(&self, message: &str) {
392 spinner_error_ref(&self.bar, message);
393 }
394
395 fn inc(&self, delta: u64) {
396 self.bar.inc(delta);
397 }
398}
399
400fn spinner_success_ref(pb: &ProgressBar, message: &str) {
402 if let Ok(style) = ProgressStyle::default_spinner().template("{msg}") {
403 pb.set_style(style);
404 }
405 pb.finish_with_message(format!("{} {}", styled!(icons::SUCCESS, green), message));
406}
407
408fn spinner_error_ref(pb: &ProgressBar, message: &str) {
410 if let Ok(style) = ProgressStyle::default_spinner().template("{msg}") {
411 pb.set_style(style);
412 }
413 pb.finish_with_message(format!(
414 "{} {}",
415 styled!(icons::ERROR, red),
416 styled!(message, red)
417 ));
418}
419
420fn format_size(bytes: u64) -> String {
424 const KB: u64 = 1024;
425 const MB: u64 = KB * 1024;
426 const GB: u64 = MB * 1024;
427
428 if bytes >= GB {
429 format!("{:.1} GB", bytes as f64 / GB as f64)
430 } else if bytes >= MB {
431 format!("{:.1} MB", bytes as f64 / MB as f64)
432 } else if bytes >= KB {
433 format!("{:.1} KB", bytes as f64 / KB as f64)
434 } else {
435 format!("{} B", bytes)
436 }
437}
438
439pub fn log_asset_analysis(
443 title: &str,
444 total_count: usize,
445 new_count: usize,
446 new_size_bytes: u64,
447 cached_count: usize,
448 cached_size_bytes: u64,
449) {
450 let mut table = ComfyTable::new();
451 table.load_preset(comfy_table::presets::NOTHING);
452
453 table.set_header(vec!["", "Count", "Size"]);
455
456 table.add_row(vec![
458 "📦 Total",
459 &total_count.to_string(),
460 &format_size(new_size_bytes + cached_size_bytes),
461 ]);
462
463 table.add_row(vec![
465 "✅ New (will upload)",
466 &new_count.to_string(),
467 &format_size(new_size_bytes),
468 ]);
469
470 table.add_row(vec![
472 "🔄 Cached (will skip)",
473 &cached_count.to_string(),
474 &format_size(cached_size_bytes),
475 ]);
476
477 eprintln!();
478 eprintln!("{}", styled!(title, cyan, bold));
479 eprintln!("{}", table);
480}