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