es_fluent_cli/utils/
ui.rs

1// CLI output formatting with consistent styling using indicatif and colored.
2// We stick to standard println!/eprintln! for textual output to ensure ANSI color compatibility.
3
4use crate::core::CrateInfo;
5use colored::Colorize as _;
6use indicatif::{ProgressBar, ProgressStyle};
7use std::path::Path;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Duration;
10
11const PD_TICK: Duration = Duration::from_millis(100);
12
13static E2E_MODE: AtomicBool = AtomicBool::new(false);
14
15/// Enable E2E mode for deterministic output (no colors, fixed durations, hidden progress bars).
16pub fn set_e2e_mode(enabled: bool) {
17    E2E_MODE.store(enabled, Ordering::SeqCst);
18    if enabled {
19        colored::control::set_override(false);
20    }
21}
22
23pub fn is_e2e() -> bool {
24    E2E_MODE.load(Ordering::SeqCst)
25}
26
27fn format_duration(duration: Duration) -> String {
28    if is_e2e() {
29        "[DURATION]".to_string()
30    } else {
31        humantime::format_duration(duration).to_string()
32    }
33}
34
35pub fn init_logging() {
36    // No-op: we rely on standard output for CLI presentation.
37    // Kept to avoid breaking main.rs calls.
38}
39
40pub fn create_spinner(msg: &str) -> ProgressBar {
41    if is_e2e() {
42        return ProgressBar::hidden();
43    }
44    let pb = ProgressBar::new_spinner();
45    pb.set_style(
46        ProgressStyle::default_spinner()
47            .template("{spinner:.green} {msg}")
48            .unwrap()
49            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
50    );
51    pb.set_message(msg.to_string());
52    pb.enable_steady_tick(PD_TICK);
53    pb
54}
55
56pub fn create_progress_bar(len: u64, msg: &str) -> ProgressBar {
57    if is_e2e() {
58        return ProgressBar::hidden();
59    }
60    let pb = ProgressBar::new(len);
61    pb.set_style(
62        ProgressStyle::default_bar()
63            .template("{spinner:.green} {msg} [{bar:40.cyan/blue}] {pos}/{len}")
64            .unwrap()
65            .progress_chars("#>-"),
66    );
67    pb.set_message(msg.to_string());
68    pb.enable_steady_tick(PD_TICK);
69    pb
70}
71
72// Deprecated/Legacy output helpers - redirected to println/eprintln to preserve formatting
73// Tracing proved problematic for raw ANSI passthrough in some environments or configs.
74
75pub fn print_header() {
76    println!("{}", "Fluent FTL Generator".dimmed());
77}
78
79pub fn print_discovered(crates: &[CrateInfo]) {
80    if crates.is_empty() {
81        eprintln!("{}", "No crates with i18n.toml found.".red());
82    } else {
83        println!(
84            "{} {}",
85            "Discovered".dimmed(),
86            format!("{} crate(s)", crates.len()).green()
87        );
88    }
89}
90
91pub fn print_missing_lib_rs(crate_name: &str) {
92    println!(
93        "{} {}",
94        "Skipping".dimmed(),
95        format!("{} (missing lib.rs)", crate_name).yellow()
96    );
97}
98
99// Action-specific printers
100
101pub fn print_generating(crate_name: &str) {
102    println!("{} {}", "Generating FTL for".dimmed(), crate_name.green());
103}
104
105pub fn print_generated(crate_name: &str, duration: Duration, resource_count: usize) {
106    println!(
107        "{} {} ({} resources)",
108        format!("{} generated in", crate_name).dimmed(),
109        format_duration(duration).green(),
110        resource_count.to_string().cyan()
111    );
112}
113
114pub fn print_cleaning(crate_name: &str) {
115    println!("{} {}", "Cleaning FTL for".dimmed(), crate_name.green());
116}
117
118pub fn print_cleaned(crate_name: &str, duration: Duration, resource_count: usize) {
119    println!(
120        "{} {} ({} resources)",
121        format!("{} cleaned in", crate_name).dimmed(),
122        format_duration(duration).green(),
123        resource_count.to_string().cyan()
124    );
125}
126
127pub fn print_generation_error(crate_name: &str, error: &str) {
128    eprintln!(
129        "{} {}: {}",
130        "Generation failed for".red(),
131        crate_name.white().bold(),
132        error
133    );
134}
135
136pub fn print_package_not_found(package: &str) {
137    println!(
138        "{} '{}'",
139        "No crate found matching package filter:".yellow(),
140        package.white().bold()
141    );
142}
143
144pub fn print_check_header() {
145    println!("{}", "Fluent FTL Checker".dimmed());
146}
147
148pub fn print_checking(crate_name: &str) {
149    println!("{} {}", "Checking".dimmed(), crate_name.green());
150}
151
152pub fn print_check_error(crate_name: &str, error: &str) {
153    eprintln!(
154        "{} {}: {}",
155        "Check failed for".red(),
156        crate_name.white().bold(),
157        error
158    );
159}
160
161pub fn print_check_success() {
162    println!("{}", "No issues found!".green());
163}
164
165pub fn print_format_header() {
166    println!("{}", "Fluent FTL Formatter".dimmed());
167}
168
169pub fn print_would_format(path: &Path) {
170    println!("{} {}", "Would format:".yellow(), path.display());
171}
172
173pub fn print_formatted(path: &Path) {
174    println!("{} {}", "Formatted:".green(), path.display());
175}
176
177pub fn print_format_dry_run_summary(count: usize) {
178    println!(
179        "{} {} file(s) would be formatted",
180        "Dry run:".yellow(),
181        count
182    );
183}
184
185pub fn print_format_summary(formatted: usize, unchanged: usize) {
186    println!(
187        "{} {} formatted, {} unchanged",
188        "Done:".green(),
189        formatted,
190        unchanged
191    );
192}
193
194pub fn print_sync_header() {
195    println!("{}", "Fluent FTL Sync".dimmed());
196}
197
198pub fn print_syncing(crate_name: &str) {
199    println!("{} {}", "Syncing".dimmed(), crate_name.green());
200}
201
202pub fn print_would_add_keys(count: usize, locale: &str, crate_name: &str) {
203    println!(
204        "{} {} key(s) to {} ({})",
205        "Would add".yellow(),
206        count,
207        locale.cyan(),
208        crate_name.bold()
209    );
210}
211
212pub fn print_added_keys(count: usize, locale: &str) {
213    println!("{} {} key(s) to {}", "Added".green(), count, locale.cyan());
214}
215
216pub fn print_synced_key(key: &str) {
217    println!("  {} {}", "->".dimmed(), key);
218}
219
220pub fn print_all_in_sync() {
221    println!("{}", "All locales are in sync!".green());
222}
223
224pub fn print_sync_dry_run_summary(keys: usize, locales: usize) {
225    println!(
226        "{} {} key(s) across {} locale(s)",
227        "Would sync".yellow(),
228        keys,
229        locales
230    );
231}
232
233pub fn print_sync_summary(keys: usize, locales: usize) {
234    println!(
235        "{} {} key(s) synced to {} locale(s)",
236        "Done:".green(),
237        keys,
238        locales
239    );
240}
241
242pub fn print_no_locales_specified() {
243    println!(
244        "{}",
245        "No locales specified. Use --locale <LOCALE> or --all".yellow()
246    );
247}
248
249pub fn print_no_crates_found() {
250    eprintln!("{}", "No crates with i18n.toml found.".red());
251}
252
253pub fn print_locale_not_found(locale: &str, available: &[String]) {
254    let available_str = if available.is_empty() {
255        "none".to_string()
256    } else {
257        available.join(", ")
258    };
259    eprintln!(
260        "{} '{}'. Available locales: {}",
261        "Locale not found:".red(),
262        locale.white().bold(),
263        available_str.cyan()
264    );
265}
266
267pub fn print_diff(old: &str, new: &str) {
268    // If e2e mode, just print a marker or simplified diff to avoid colored crate dependency affecting things
269    // But we still want to see the diff content.
270    // Use the existing logic but colors will be suppressed by `colored::control::set_override(false)`.
271
272    use similar::{ChangeTag, TextDiff};
273
274    let diff = TextDiff::from_lines(old, new);
275
276    for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
277        if idx > 0 {
278            println!("{}", "  ...".dimmed());
279        }
280        for op in group {
281            for change in diff.iter_changes(op) {
282                let sign = match change.tag() {
283                    ChangeTag::Delete => "-",
284                    ChangeTag::Insert => "+",
285                    ChangeTag::Equal => " ",
286                };
287                let line = format!("{} {}", sign, change);
288                match change.tag() {
289                    ChangeTag::Delete => print!("{}", line.red()),
290                    ChangeTag::Insert => print!("{}", line.green()),
291                    ChangeTag::Equal => print!("{}", line.dimmed()),
292                }
293            }
294        }
295    }
296}