1use std::io::Write;
7
8use serde::Serialize;
9use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
10
11use crate::diagnostic::Diagnostic;
12use crate::sync::SyncReport;
13use crate::sync::apply::{ActionOutcome, ActionTaken};
14
15pub fn use_color() -> bool {
19 std::env::var_os("NO_COLOR").is_none()
20}
21
22fn color_choice() -> ColorChoice {
23 if use_color() {
24 ColorChoice::Auto
25 } else {
26 ColorChoice::Never
27 }
28}
29
30#[derive(Debug, Serialize)]
32pub struct ListEntry {
33 pub source: String,
34 pub item: String,
35 pub kind: String,
36 pub version: String,
37 pub status: String,
38}
39
40#[derive(Debug, Serialize)]
42pub struct CatalogEntry {
43 pub name: String,
44 pub description: String,
45 pub kind: String,
46}
47
48pub fn print_catalog(agents: &[CatalogEntry], skills: &[CatalogEntry], kind_filter: Option<&str>) {
50 let show_agents =
51 kind_filter.is_none() || kind_filter == Some("agents") || kind_filter == Some("agent");
52 let show_skills =
53 kind_filter.is_none() || kind_filter == Some("skills") || kind_filter == Some("skill");
54
55 if show_agents && !agents.is_empty() {
56 println!("AGENTS");
57 for entry in agents {
58 if entry.description.is_empty() {
59 println!("- {}", entry.name);
60 } else {
61 println!("- {}: {}", entry.name, entry.description);
62 }
63 }
64 }
65
66 if show_agents && !agents.is_empty() && show_skills && !skills.is_empty() {
67 println!();
68 }
69
70 if show_skills && !skills.is_empty() {
71 println!("SKILLS");
72 for entry in skills {
73 if entry.description.is_empty() {
74 println!("- {}", entry.name);
75 } else {
76 println!("- {}: {}", entry.name, entry.description);
77 }
78 }
79 }
80
81 if (show_agents && agents.is_empty() && show_skills && skills.is_empty())
82 || (show_agents && !show_skills && agents.is_empty())
83 || (show_skills && !show_agents && skills.is_empty())
84 {
85 println!(" no managed items");
86 }
87}
88
89pub fn print_sync_report(report: &SyncReport, json: bool) {
91 if json {
92 print_sync_report_json(report);
93 } else {
94 print_sync_report_human(report);
95 }
96}
97
98fn is_dry_run(report: &SyncReport) -> bool {
101 report.dry_run
102}
103
104fn print_sync_report_json(report: &SyncReport) {
105 #[derive(Serialize)]
106 struct JsonTargetOutcome {
107 name: String,
108 synced: usize,
109 removed: usize,
110 errors: Vec<String>,
111 }
112
113 #[derive(Serialize)]
114 struct JsonReport {
115 ok: bool,
116 dry_run: bool,
117 installed: usize,
118 updated: usize,
119 removed: usize,
120 conflicts: usize,
121 kept: usize,
122 skipped: usize,
123 targets: Vec<JsonTargetOutcome>,
124 diagnostics: Vec<Diagnostic>,
125 }
126
127 let mut installed = 0;
128 let mut updated = 0;
129 let mut removed = 0;
130 let mut conflicts = 0;
131 let mut kept = 0;
132 let mut skipped = 0;
133
134 for outcome in &report.applied.outcomes {
135 match outcome.action {
136 ActionTaken::Installed | ActionTaken::Symlinked => installed += 1,
137 ActionTaken::Updated => updated += 1,
138 ActionTaken::Merged => updated += 1,
139 ActionTaken::Conflicted => conflicts += 1,
140 ActionTaken::Removed => removed += 1,
141 ActionTaken::Kept => kept += 1,
142 ActionTaken::Skipped => skipped += 1,
143 }
144 }
145
146 for outcome in &report.pruned {
147 if matches!(outcome.action, ActionTaken::Removed) {
148 removed += 1;
149 }
150 }
151
152 let targets = report
153 .target_outcomes
154 .iter()
155 .map(|outcome| JsonTargetOutcome {
156 name: outcome.target.clone(),
157 synced: outcome.items_synced,
158 removed: outcome.items_removed,
159 errors: outcome.errors.clone(),
160 })
161 .collect();
162
163 let json_report = JsonReport {
164 ok: conflicts == 0,
165 dry_run: report.dry_run,
166 installed,
167 updated,
168 removed,
169 conflicts,
170 kept,
171 skipped,
172 targets,
173 diagnostics: report.diagnostics.clone(),
174 };
175
176 println!(
177 "{}",
178 serde_json::to_string(&json_report).unwrap_or_default()
179 );
180}
181
182fn print_sync_report_human(report: &SyncReport) {
183 let mut stdout = StandardStream::stdout(color_choice());
184
185 let mut installed = 0usize;
186 let mut updated = 0usize;
187 let mut removed = 0usize;
188 let mut conflicts = 0usize;
189 let mut kept = 0usize;
190
191 for outcome in &report.applied.outcomes {
193 match outcome.action {
194 ActionTaken::Installed | ActionTaken::Symlinked => {
195 installed += 1;
196 print_action_line(&mut stdout, "+", Color::Green, outcome);
197 }
198 ActionTaken::Updated | ActionTaken::Merged => {
199 updated += 1;
200 print_action_line(&mut stdout, "~", Color::Yellow, outcome);
201 }
202 ActionTaken::Conflicted => {
203 conflicts += 1;
204 print_action_line(&mut stdout, "!", Color::Red, outcome);
205 }
206 ActionTaken::Removed => {
207 removed += 1;
208 print_action_line(&mut stdout, "-", Color::Red, outcome);
209 }
210 ActionTaken::Kept => {
211 kept += 1;
212 }
213 ActionTaken::Skipped => {}
214 }
215 }
216
217 for outcome in &report.pruned {
218 if matches!(outcome.action, ActionTaken::Removed) {
219 removed += 1;
220 print_action_line(&mut stdout, "-", Color::Red, outcome);
221 }
222 }
223
224 let _ = writeln!(stdout);
226 let dry = is_dry_run(report);
227 if installed > 0 {
228 if dry {
229 let _ = writeln!(stdout, " would install {installed} new items");
230 } else {
231 let _ = writeln!(stdout, " installed {installed} new items");
232 }
233 }
234 if updated > 0 {
235 if dry {
236 let _ = writeln!(stdout, " would update {updated} items");
237 } else {
238 let _ = writeln!(stdout, " updated {updated} items");
239 }
240 }
241 if removed > 0 {
242 if dry {
243 let _ = writeln!(stdout, " would remove {removed} orphans");
244 } else {
245 let _ = writeln!(stdout, " removed {removed} orphans");
246 }
247 }
248 if kept > 0 {
249 let _ = writeln!(stdout, " kept {kept} locally modified");
250 }
251 if conflicts > 0 {
252 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
253 let _ = writeln!(
254 stdout,
255 " conflicts {conflicts} files (run `mars resolve` after fixing)"
256 );
257 let _ = stdout.reset();
258 }
259
260 if installed == 0 && updated == 0 && removed == 0 && conflicts == 0 && kept == 0 {
261 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
262 let _ = writeln!(stdout, " already up to date");
263 let _ = stdout.reset();
264 }
265
266 let mut stderr = StandardStream::stderr(color_choice());
268 for diag in &report.diagnostics {
269 let color = match diag.level {
270 crate::diagnostic::DiagnosticLevel::Warning => Color::Yellow,
271 crate::diagnostic::DiagnosticLevel::Info => Color::Cyan,
272 };
273 let _ = stderr.set_color(ColorSpec::new().set_fg(Some(color)));
274 let _ = writeln!(stderr, " {diag}");
275 let _ = stderr.reset();
276 }
277}
278
279fn print_action_line(
280 stdout: &mut StandardStream,
281 prefix: &str,
282 color: Color,
283 outcome: &ActionOutcome,
284) {
285 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(color)));
286 let _ = write!(stdout, " {prefix} ");
287 let _ = stdout.reset();
288 let _ = writeln!(
289 stdout,
290 "{} ({})",
291 outcome.dest_path.display(),
292 outcome.item_id.kind
293 );
294}
295
296pub fn print_list(entries: &[ListEntry], json: bool) {
298 if json {
299 println!("{}", serde_json::to_string(entries).unwrap_or_default());
300 } else {
301 print_list_human(entries);
302 }
303}
304
305fn print_list_human(entries: &[ListEntry]) {
306 if entries.is_empty() {
307 println!(" no managed items");
308 return;
309 }
310
311 let source_w = entries
313 .iter()
314 .map(|e| e.source.len())
315 .max()
316 .unwrap_or(6)
317 .max(6);
318 let item_w = entries
319 .iter()
320 .map(|e| e.item.len())
321 .max()
322 .unwrap_or(4)
323 .max(4);
324 let version_w = entries
325 .iter()
326 .map(|e| e.version.len())
327 .max()
328 .unwrap_or(7)
329 .max(7);
330
331 println!(
333 "{:<source_w$} {:<item_w$} {:<version_w$} STATUS",
334 "SOURCE", "ITEM", "VERSION"
335 );
336
337 let mut stdout = StandardStream::stdout(color_choice());
338 for entry in entries {
339 let _ = write!(
340 stdout,
341 "{:<source_w$} {:<item_w$} {:<version_w$} ",
342 entry.source, entry.item, entry.version
343 );
344 let color = match entry.status.as_str() {
345 "ok" => Color::Green,
346 "modified" => Color::Yellow,
347 "conflicted" => Color::Red,
348 _ => Color::White,
349 };
350 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(color)));
351 let _ = writeln!(stdout, "{}", entry.status);
352 let _ = stdout.reset();
353 }
354}
355
356pub fn print_doctor(errors: &[String], warnings: &[String], json: bool) {
358 if json {
359 #[derive(Serialize)]
360 struct DoctorReport {
361 ok: bool,
362 errors: Vec<String>,
363 warnings: Vec<String>,
364 }
365 let report = DoctorReport {
366 ok: errors.is_empty(),
367 errors: errors.to_vec(),
368 warnings: warnings.to_vec(),
369 };
370 println!("{}", serde_json::to_string(&report).unwrap_or_default());
371 } else {
372 let mut stdout = StandardStream::stdout(color_choice());
373 if errors.is_empty() && warnings.is_empty() {
374 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
375 let _ = writeln!(stdout, " all checks passed");
376 let _ = stdout.reset();
377 } else {
378 for warning in warnings {
379 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)));
380 let _ = write!(stdout, " ⚠ ");
381 let _ = stdout.reset();
382 let _ = writeln!(stdout, "{warning}");
383 }
384
385 for error in errors {
386 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
387 let _ = write!(stdout, " ✗ ");
388 let _ = stdout.reset();
389 let _ = writeln!(stdout, "{error}");
390 }
391 let _ = writeln!(stdout);
392 if !warnings.is_empty() {
393 let _ = writeln!(stdout, " {} warning(s)", warnings.len());
394 }
395 if !errors.is_empty() {
396 let _ = writeln!(stdout, " {} error(s)", errors.len());
397 }
398 }
399 }
400}
401
402pub fn print_json<T: Serialize>(value: &T) {
404 println!("{}", serde_json::to_string(value).unwrap_or_default());
405}
406
407pub fn print_success(msg: &str) {
409 let mut stdout = StandardStream::stdout(color_choice());
410 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
411 let _ = write!(stdout, " ✓ ");
412 let _ = stdout.reset();
413 let _ = writeln!(stdout, "{msg}");
414}
415
416pub fn print_warn(msg: &str) {
418 let mut stdout = StandardStream::stdout(color_choice());
419 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)));
420 let _ = write!(stdout, " ⚠ ");
421 let _ = stdout.reset();
422 let _ = writeln!(stdout, "{msg}");
423}
424
425pub fn print_error(msg: &str) {
427 let mut stdout = StandardStream::stdout(color_choice());
428 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
429 let _ = write!(stdout, " ✗ ");
430 let _ = stdout.reset();
431 let _ = writeln!(stdout, "{msg}");
432}
433
434pub fn print_info(msg: &str) {
436 println!(" {msg}");
437}