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