Skip to content

Commit ff34f8a

Browse files
committed
feat(cli): add styled terminal output
1 parent 17d30d1 commit ff34f8a

13 files changed

Lines changed: 1072 additions & 136 deletions

File tree

langcodec-cli/src/ai.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::sync::Arc;
22

3-
use mentra::{BuiltinProvider, provider::{self, Provider}};
3+
use mentra::{
4+
BuiltinProvider,
5+
provider::{self, Provider},
6+
};
47

58
#[derive(Debug, Clone, PartialEq, Eq)]
69
pub(crate) enum ProviderKind {

langcodec-cli/src/convert.rs

Lines changed: 175 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::formats::{self, parse_custom_format};
22
use crate::transformers::custom_format_to_resource;
3+
use crate::ui;
34
use crate::validation::{self, validate_custom_format_file};
45

56
use langcodec::{Codec, ReadOptions, convert_auto, formats::FormatType};
@@ -30,7 +31,13 @@ pub fn run_unified_convert_command(
3031
.as_deref()
3132
.is_some_and(|s| s.eq_ignore_ascii_case("xcstrings"));
3233
if wants_xcstrings {
33-
println!("Converting to xcstrings with default sourceLanguage if missing...");
34+
println!(
35+
"{}",
36+
ui::status_line_stdout(
37+
ui::Tone::Info,
38+
"Converting to xcstrings with default sourceLanguage if missing...",
39+
)
40+
);
3441
match read_resources_from_any_input(&input, options.input_format.as_ref(), strict).and_then(
3542
|mut resources| {
3643
// Determine source_language priority: explicit flag > metadata > default
@@ -81,11 +88,20 @@ pub fn run_unified_convert_command(
8188
},
8289
) {
8390
Ok(()) => {
84-
println!("✅ Successfully converted to xcstrings");
91+
println!(
92+
"{}",
93+
ui::status_line_stdout(
94+
ui::Tone::Success,
95+
"Successfully converted to xcstrings",
96+
)
97+
);
8598
return;
8699
}
87100
Err(e) => {
88-
println!("❌ Conversion to xcstrings failed");
101+
println!(
102+
"{}",
103+
ui::status_line_stdout(ui::Tone::Error, "Conversion to xcstrings failed")
104+
);
89105
// Preserve legacy expectation for invalid JSON: surface an inference hint
90106
if input.ends_with(".json") {
91107
eprintln!("Cannot infer input format");
@@ -112,8 +128,14 @@ pub fn run_unified_convert_command(
112128
};
113129

114130
println!(
115-
"Converting input to .langcodec (Resource JSON array){}...",
116-
filter_msg
131+
"{}",
132+
ui::status_line_stdout(
133+
ui::Tone::Info,
134+
&format!(
135+
"Converting input to .langcodec (Resource JSON array){}...",
136+
filter_msg
137+
),
138+
)
117139
);
118140
match read_resources_from_any_input(&input, options.input_format.as_ref(), strict).and_then(
119141
|resources| {
@@ -157,8 +179,14 @@ pub fn run_unified_convert_command(
157179
};
158180

159181
println!(
160-
"✅ Successfully converted to .langcodec (Resource JSON array){}",
161-
filter_msg
182+
"{}",
183+
ui::status_line_stdout(
184+
ui::Tone::Success,
185+
&format!(
186+
"Successfully converted to .langcodec (Resource JSON array){}",
187+
filter_msg
188+
),
189+
)
162190
);
163191
return;
164192
}
@@ -177,7 +205,13 @@ pub fn run_unified_convert_command(
177205
String::new()
178206
};
179207

180-
println!("❌ Conversion to .langcodec failed{}", filter_msg);
208+
println!(
209+
"{}",
210+
ui::status_line_stdout(
211+
ui::Tone::Error,
212+
&format!("Conversion to .langcodec failed{}", filter_msg),
213+
)
214+
);
181215
eprintln!("Error: {}", e);
182216
std::process::exit(1);
183217
}
@@ -189,13 +223,28 @@ pub fn run_unified_convert_command(
189223
options.input_format.as_deref(),
190224
options.output_format.as_deref(),
191225
) {
192-
println!("Strict mode: converting with explicit format hints only...");
226+
println!(
227+
"{}",
228+
ui::status_line_stdout(
229+
ui::Tone::Info,
230+
"Strict mode: converting with explicit format hints only...",
231+
)
232+
);
193233
if let Err(e) = try_explicit_format_conversion(&input, &output, input_fmt, output_fmt) {
194-
println!("❌ Strict conversion failed");
234+
println!(
235+
"{}",
236+
ui::status_line_stdout(ui::Tone::Error, "Strict conversion failed")
237+
);
195238
eprintln!("Error: {}", e);
196239
std::process::exit(1);
197240
}
198-
println!("✅ Successfully converted in strict mode");
241+
println!(
242+
"{}",
243+
ui::status_line_stdout(
244+
ui::Tone::Success,
245+
"Successfully converted in strict mode",
246+
)
247+
);
199248
return;
200249
}
201250

@@ -204,30 +253,72 @@ pub fn run_unified_convert_command(
204253
|| input.ends_with(".yml")
205254
|| input.ends_with(".langcodec")
206255
{
207-
println!("Strict mode: converting custom format without fallback...");
256+
println!(
257+
"{}",
258+
ui::status_line_stdout(
259+
ui::Tone::Info,
260+
"Strict mode: converting custom format without fallback...",
261+
)
262+
);
208263
if let Err(e) = try_custom_format_conversion(&input, &output, &options.input_format) {
209-
println!("❌ Strict conversion failed");
264+
println!(
265+
"{}",
266+
ui::status_line_stdout(ui::Tone::Error, "Strict conversion failed")
267+
);
210268
eprintln!("Error: {}", e);
211269
std::process::exit(1);
212270
}
213-
println!("✅ Successfully converted in strict mode");
271+
println!(
272+
"{}",
273+
ui::status_line_stdout(
274+
ui::Tone::Success,
275+
"Successfully converted in strict mode",
276+
)
277+
);
214278
return;
215279
}
216280

217-
println!("Strict mode: converting using extension-based standard formats only...");
281+
println!(
282+
"{}",
283+
ui::status_line_stdout(
284+
ui::Tone::Info,
285+
"Strict mode: converting using extension-based standard formats only...",
286+
)
287+
);
218288
if let Err(e) = convert_auto(&input, &output) {
219-
println!("❌ Strict conversion failed");
289+
println!(
290+
"{}",
291+
ui::status_line_stdout(ui::Tone::Error, "Strict conversion failed")
292+
);
220293
eprintln!("Error: {}", e);
221294
std::process::exit(1);
222295
}
223-
println!("✅ Successfully converted in strict mode");
296+
println!(
297+
"{}",
298+
ui::status_line_stdout(
299+
ui::Tone::Success,
300+
"Successfully converted in strict mode",
301+
)
302+
);
224303
return;
225304
}
226305

227306
// Strategy 1: Try standard lib crate conversion first
228-
println!("Trying standard format detection from file extensions...");
307+
println!(
308+
"{}",
309+
ui::status_line_stdout(
310+
ui::Tone::Info,
311+
"Trying standard format detection from file extensions...",
312+
)
313+
);
229314
if let Ok(()) = convert_auto(&input, &output) {
230-
println!("✅ Successfully converted using standard format detection");
315+
println!(
316+
"{}",
317+
ui::status_line_stdout(
318+
ui::Tone::Success,
319+
"Successfully converted using standard format detection",
320+
)
321+
);
231322
return;
232323
}
233324

@@ -239,30 +330,66 @@ pub fn run_unified_convert_command(
239330
{
240331
// For JSON files without explicit format, try standard format detection first
241332
if input.ends_with(".json") && options.input_format.is_none() {
242-
println!("Trying standard JSON format detection...");
333+
println!(
334+
"{}",
335+
ui::status_line_stdout(
336+
ui::Tone::Info,
337+
"Trying standard JSON format detection...",
338+
)
339+
);
243340
// Try to use the standard format detection which will show proper JSON parsing errors
244341
if let Err(e) = convert_auto(&input, &output) {
245-
println!("Trying custom JSON format conversion...");
342+
println!(
343+
"{}",
344+
ui::status_line_stdout(
345+
ui::Tone::Info,
346+
"Trying custom JSON format conversion...",
347+
)
348+
);
246349
// If standard detection fails, try custom formats
247350
if let Ok(()) = try_custom_format_conversion(&input, &output, &options.input_format)
248351
{
249-
println!("✅ Successfully converted using custom JSON format");
352+
println!(
353+
"{}",
354+
ui::status_line_stdout(
355+
ui::Tone::Success,
356+
"Successfully converted using custom JSON format",
357+
)
358+
);
250359
return;
251360
}
252361
// If both fail, show the standard error message
253-
println!("❌ Conversion failed");
362+
println!(
363+
"{}",
364+
ui::status_line_stdout(ui::Tone::Error, "Conversion failed")
365+
);
254366
eprintln!("Error: {}", e);
255367
std::process::exit(1);
256368
}
257369
} else {
258370
// For YAML and langcodec files, try custom formats directly
259-
println!("Converting using custom format...");
371+
println!(
372+
"{}",
373+
ui::status_line_stdout(ui::Tone::Info, "Converting using custom format...")
374+
);
260375
if let Err(e) = try_custom_format_conversion(&input, &output, &options.input_format) {
261-
println!("❌ Custom format conversion failed");
376+
println!(
377+
"{}",
378+
ui::status_line_stdout(
379+
ui::Tone::Error,
380+
"Custom format conversion failed",
381+
)
382+
);
262383
eprintln!("Error: {}", e);
263384
std::process::exit(1);
264385
}
265-
println!("✅ Successfully converted using custom format");
386+
println!(
387+
"{}",
388+
ui::status_line_stdout(
389+
ui::Tone::Success,
390+
"Successfully converted using custom format",
391+
)
392+
);
266393
return;
267394
}
268395
}
@@ -271,18 +398,36 @@ pub fn run_unified_convert_command(
271398
if let (Some(input_fmt), Some(output_fmt)) =
272399
(options.input_format.clone(), options.output_format.clone())
273400
{
274-
println!("Converting with explicit format hints...");
401+
println!(
402+
"{}",
403+
ui::status_line_stdout(ui::Tone::Info, "Converting with explicit format hints...")
404+
);
275405
if let Err(e) = try_explicit_format_conversion(&input, &output, &input_fmt, &output_fmt) {
276-
println!("❌ Explicit format conversion failed");
406+
println!(
407+
"{}",
408+
ui::status_line_stdout(
409+
ui::Tone::Error,
410+
"Explicit format conversion failed",
411+
)
412+
);
277413
eprintln!("Error: {}", e);
278414
std::process::exit(1);
279415
}
280-
println!("✅ Successfully converted with explicit formats");
416+
println!(
417+
"{}",
418+
ui::status_line_stdout(
419+
ui::Tone::Success,
420+
"Successfully converted with explicit formats",
421+
)
422+
);
281423
return;
282424
}
283425

284426
// If all strategies failed, provide helpful error message
285-
println!("❌ All conversion strategies failed");
427+
println!(
428+
"{}",
429+
ui::status_line_stdout(ui::Tone::Error, "All conversion strategies failed")
430+
);
286431
print_conversion_error(&input, &output);
287432
std::process::exit(1);
288433
}

langcodec-cli/src/diff.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::convert::read_resources_from_any_input;
2+
use crate::ui;
23
use crate::validation::{validate_file_path, validate_language_code, validate_output_path};
34
use langcodec::{DiffOptions as LibDiffOptions, DiffReport, Translation, diff_resources};
45

@@ -29,14 +30,56 @@ fn translation_as_text(value: &Translation) -> String {
2930
fn print_or_write(output: Option<&String>, content: &str) -> Result<(), String> {
3031
if let Some(path) = output {
3132
std::fs::write(path, content).map_err(|e| format!("Failed to write {}: {}", path, e))?;
32-
println!("Report written: {}", path);
33+
println!(
34+
"{}",
35+
ui::status_line_stdout(ui::Tone::Success, &format!("Report written: {}", path))
36+
);
3337
} else {
3438
println!("{}", content);
3539
}
3640
Ok(())
3741
}
3842

3943
fn render_human(report: &DiffReport) -> String {
44+
if ui::stdout_styled() {
45+
let mut lines = Vec::new();
46+
lines.push(ui::header("Diff"));
47+
lines.push(ui::key_value("Languages", report.summary.languages));
48+
lines.push(ui::key_value("Added", report.summary.added));
49+
lines.push(ui::key_value("Removed", report.summary.removed));
50+
lines.push(ui::key_value("Changed", report.summary.changed));
51+
lines.push(ui::key_value("Unchanged", report.summary.unchanged));
52+
53+
for lang in &report.languages {
54+
lines.push(ui::section(&format!("Language {}", lang.language)));
55+
lines.push(ui::divider(28));
56+
lines.push(ui::key_value("added", lang.added.len()));
57+
lines.push(ui::key_value("removed", lang.removed.len()));
58+
lines.push(ui::key_value("changed", lang.changed.len()));
59+
lines.push(ui::key_value("unchanged", lang.unchanged));
60+
if !lang.added.is_empty() {
61+
lines.push(ui::key_value("added keys", lang.added.join(", ")));
62+
}
63+
if !lang.removed.is_empty() {
64+
lines.push(ui::key_value("removed keys", lang.removed.join(", ")));
65+
}
66+
if !lang.changed.is_empty() {
67+
let mut changed_lines = Vec::new();
68+
for item in &lang.changed {
69+
changed_lines.push(format!(
70+
"{} ({} → {})",
71+
ui::accent(&item.key),
72+
translation_as_text(&item.target),
73+
translation_as_text(&item.source)
74+
));
75+
}
76+
lines.push(ui::key_value("changed keys", changed_lines.join(", ")));
77+
}
78+
}
79+
80+
return lines.join("\n");
81+
}
82+
4083
let mut lines = Vec::new();
4184
lines.push("=== Diff ===".to_string());
4285
lines.push(format!("Languages: {}", report.summary.languages));

0 commit comments

Comments
 (0)