1use crate::agent::ui::colors::icons;
7use crate::agent::ui::response::brand;
8use std::io::{self, Write};
9
10const BOX_WIDTH: usize = 72;
12
13pub struct HelmlintDisplay;
15
16impl HelmlintDisplay {
17 pub fn print_result(json_result: &str) {
19 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
20 Self::print_formatted(&parsed);
21 } else {
22 println!("{}", json_result);
24 }
25 }
26
27 fn print_formatted(result: &serde_json::Value) {
29 let stdout = io::stdout();
30 let mut handle = stdout.lock();
31
32 let chart = result["chart"].as_str().unwrap_or("helm chart");
34
35 let _ = writeln!(handle);
37 let _ = writeln!(
38 handle,
39 "{}{}╭─ {} Helmlint {}{}╮{}",
40 brand::PURPLE,
41 brand::BOLD,
42 icons::HELM,
43 "─".repeat(BOX_WIDTH - 15),
44 brand::DIM,
45 brand::RESET
46 );
47
48 let _ = writeln!(
50 handle,
51 "{}│ {}{}{}{}",
52 brand::DIM,
53 brand::CYAN,
54 chart,
55 " ".repeat((BOX_WIDTH - 4 - chart.len()).max(0)),
56 brand::RESET
57 );
58
59 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
61
62 if let Some(context) = result["decision_context"].as_str() {
64 let context_color = if context.contains("Critical") {
65 brand::CORAL
66 } else if context.contains("High") || context.contains("high") {
67 brand::PEACH
68 } else if context.contains("Good") || context.contains("No issues") {
69 brand::SUCCESS
70 } else {
71 brand::PEACH
72 };
73
74 let display_context = if context.len() > BOX_WIDTH - 6 {
76 &context[..BOX_WIDTH - 9]
77 } else {
78 context
79 };
80
81 let _ = writeln!(
82 handle,
83 "{}│ {}{}{}{}",
84 brand::DIM,
85 context_color,
86 display_context,
87 " ".repeat((BOX_WIDTH - 4 - display_context.len()).max(0)),
88 brand::RESET
89 );
90 }
91
92 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
94
95 if let Some(summary) = result.get("summary") {
97 let total = summary["total"].as_u64().unwrap_or(0);
98
99 if total == 0 {
100 let _ = writeln!(
101 handle,
102 "{}│ {}{} All checks passed! No issues found.{}{}",
103 brand::DIM,
104 brand::SUCCESS,
105 icons::SUCCESS,
106 " ".repeat(BOX_WIDTH - 42),
107 brand::RESET
108 );
109
110 let files = summary["files_checked"].as_u64().unwrap_or(0);
112 let stats = format!("{} files checked", files);
113 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
114 let _ = writeln!(
115 handle,
116 "{}│ {}{}{}{}",
117 brand::DIM,
118 brand::DIM,
119 stats,
120 " ".repeat((BOX_WIDTH - 4 - stats.len()).max(0)),
121 brand::RESET
122 );
123 } else {
124 if let Some(by_priority) = summary.get("by_priority") {
126 let critical = by_priority["critical"].as_u64().unwrap_or(0);
127 let high = by_priority["high"].as_u64().unwrap_or(0);
128 let medium = by_priority["medium"].as_u64().unwrap_or(0);
129 let low = by_priority["low"].as_u64().unwrap_or(0);
130
131 let mut counts = String::new();
132 if critical > 0 {
133 counts.push_str(&format!("{} {} critical ", icons::CRITICAL, critical));
134 }
135 if high > 0 {
136 counts.push_str(&format!("{} {} high ", icons::HIGH, high));
137 }
138 if medium > 0 {
139 counts.push_str(&format!("{} {} medium ", icons::MEDIUM, medium));
140 }
141 if low > 0 {
142 counts.push_str(&format!("{} {} low", icons::LOW, low));
143 }
144
145 let padding = if counts.len() < BOX_WIDTH - 4 {
146 (BOX_WIDTH - 4 - counts.chars().count()).max(0)
147 } else {
148 0
149 };
150 let _ = writeln!(
151 handle,
152 "{}│ {}{}{}",
153 brand::DIM,
154 counts,
155 " ".repeat(padding),
156 brand::RESET
157 );
158 }
159 }
160 }
161
162 if let Some(quick_fixes) = result.get("quick_fixes").and_then(|f| f.as_array())
164 && !quick_fixes.is_empty()
165 {
166 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
167 let _ = writeln!(
168 handle,
169 "{}│ {}{} Quick Fixes:{}{}",
170 brand::DIM,
171 brand::PURPLE,
172 icons::FIX,
173 " ".repeat(BOX_WIDTH - 18),
174 brand::RESET
175 );
176
177 for fix in quick_fixes.iter().take(5) {
178 if let Some(fix_str) = fix.as_str() {
179 let (issue, remediation) = if let Some(pos) = fix_str.find(" - ") {
181 (&fix_str[..pos], &fix_str[pos + 3..])
182 } else {
183 (fix_str, "")
184 };
185
186 let issue_display = if issue.len() > BOX_WIDTH - 10 {
187 format!("{}...", &issue[..BOX_WIDTH - 13])
188 } else {
189 issue.to_string()
190 };
191
192 let _ = writeln!(
193 handle,
194 "{}│ {}→ {}{}{}{}",
195 brand::DIM,
196 brand::CYAN,
197 issue_display,
198 " ".repeat((BOX_WIDTH - 8 - issue_display.len()).max(0)),
199 brand::RESET,
200 brand::RESET
201 );
202
203 if !remediation.is_empty() {
204 let rem_display = if remediation.len() > BOX_WIDTH - 10 {
205 format!("{}...", &remediation[..BOX_WIDTH - 13])
206 } else {
207 remediation.to_string()
208 };
209 let _ = writeln!(
210 handle,
211 "{}│ {}{}{}{}",
212 brand::DIM,
213 brand::DIM,
214 rem_display,
215 " ".repeat((BOX_WIDTH - 8 - rem_display.len()).max(0)),
216 brand::RESET
217 );
218 }
219 }
220 }
221 }
222
223 Self::print_priority_section(
225 &mut handle,
226 result,
227 "critical",
228 "Critical Issues",
229 brand::CORAL,
230 );
231 Self::print_priority_section(&mut handle, result, "high", "High Priority", brand::PEACH);
232
233 let medium_count = result["action_plan"]["medium"]
235 .as_array()
236 .map(|a| a.len())
237 .unwrap_or(0);
238 let low_count = result["action_plan"]["low"]
239 .as_array()
240 .map(|a| a.len())
241 .unwrap_or(0);
242 let other_count = medium_count + low_count;
243
244 if other_count > 0 {
245 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
246 let msg = format!(
247 "{} {} priority issue{} (use --verbose to see all)",
248 other_count,
249 if medium_count > 0 {
250 "medium/low"
251 } else {
252 "low"
253 },
254 if other_count == 1 { "" } else { "s" }
255 );
256 let _ = writeln!(
257 handle,
258 "{}│ {}{}{}{}",
259 brand::DIM,
260 brand::DIM,
261 msg,
262 " ".repeat((BOX_WIDTH - 4 - msg.len()).max(0)),
263 brand::RESET
264 );
265 }
266
267 let _ = writeln!(
269 handle,
270 "{}╰{}╯{}",
271 brand::DIM,
272 "─".repeat(BOX_WIDTH - 2),
273 brand::RESET
274 );
275 let _ = writeln!(handle);
276
277 let _ = handle.flush();
278 }
279
280 fn print_priority_section(
282 handle: &mut io::StdoutLock,
283 result: &serde_json::Value,
284 priority: &str,
285 title: &str,
286 color: &str,
287 ) {
288 if let Some(issues) = result["action_plan"][priority].as_array() {
289 if issues.is_empty() {
290 return;
291 }
292
293 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
294 let _ = writeln!(
295 handle,
296 "{}│ {}{}:{}{}",
297 brand::DIM,
298 color,
299 title,
300 " ".repeat((BOX_WIDTH - 4 - title.len() - 1).max(0)),
301 brand::RESET
302 );
303
304 for issue in issues.iter().take(5) {
305 let code = issue["code"].as_str().unwrap_or("???");
306 let file = issue["file"].as_str().unwrap_or("");
307 let line = issue["line"].as_u64().unwrap_or(0);
308 let message = issue["message"].as_str().unwrap_or("");
309 let category = issue["category"].as_str().unwrap_or("");
310
311 let badge = Self::get_category_badge(category);
313
314 let file_short = if file.len() > 30 {
316 format!("...{}", &file[file.len() - 27..])
317 } else {
318 file.to_string()
319 };
320
321 let header = format!("{}:{} {} {}", file_short, line, code, badge);
323 let header_len = header.chars().count();
324 let _ = writeln!(
325 handle,
326 "{}│ {}{}{}{}",
327 brand::DIM,
328 brand::CYAN,
329 header,
330 " ".repeat((BOX_WIDTH - 6 - header_len).max(0)),
331 brand::RESET
332 );
333
334 let msg_display = if message.len() > BOX_WIDTH - 8 {
336 format!("{}...", &message[..BOX_WIDTH - 11])
337 } else {
338 message.to_string()
339 };
340 let _ = writeln!(
341 handle,
342 "{}│ {}{}{}",
343 brand::DIM,
344 msg_display,
345 " ".repeat((BOX_WIDTH - 6 - msg_display.len()).max(0)),
346 brand::RESET
347 );
348
349 if let Some(fix) = issue["fix"].as_str() {
351 let fix_display = if fix.len() > BOX_WIDTH - 12 {
352 format!("{}...", &fix[..BOX_WIDTH - 15])
353 } else {
354 fix.to_string()
355 };
356 let _ = writeln!(
357 handle,
358 "{}│ {}→ {}{}{}",
359 brand::DIM,
360 brand::CYAN,
361 fix_display,
362 " ".repeat((BOX_WIDTH - 8 - fix_display.len()).max(0)),
363 brand::RESET
364 );
365 }
366 }
367
368 if issues.len() > 5 {
369 let more_msg = format!("... and {} more", issues.len() - 5);
370 let _ = writeln!(
371 handle,
372 "{}│ {}{}{}{}",
373 brand::DIM,
374 brand::DIM,
375 more_msg,
376 " ".repeat((BOX_WIDTH - 6 - more_msg.len()).max(0)),
377 brand::RESET
378 );
379 }
380 }
381 }
382
383 fn get_category_badge(category: &str) -> String {
385 match category {
386 "Security" | "security" => format!("{}[SEC]{}", brand::CORAL, brand::RESET),
387 "Structure" | "structure" => format!("{}[STRUCT]{}", brand::DIM, brand::RESET),
388 "Values" | "values" => format!("{}[VAL]{}", brand::PEACH, brand::RESET),
389 "Template" | "template" => format!("{}[TPL]{}", brand::PEACH, brand::RESET),
390 "Best Practice" | "best-practice" => format!("{}[BP]{}", brand::CYAN, brand::RESET),
391 _ => String::new(),
392 }
393 }
394
395 pub fn format_summary(json_result: &str) -> String {
397 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
398 let success = parsed["success"].as_bool().unwrap_or(false);
399 let total = parsed["summary"]["total"].as_u64().unwrap_or(0);
400
401 if success && total == 0 {
402 format!(
403 "{}{} {} Helm chart OK - no issues{}",
404 brand::SUCCESS,
405 icons::SUCCESS,
406 icons::HELM,
407 brand::RESET
408 )
409 } else {
410 let critical = parsed["summary"]["by_priority"]["critical"]
411 .as_u64()
412 .unwrap_or(0);
413 let high = parsed["summary"]["by_priority"]["high"]
414 .as_u64()
415 .unwrap_or(0);
416
417 if critical > 0 {
418 format!(
419 "{}{} {} {} critical, {} high priority issues{}",
420 brand::CORAL,
421 icons::CRITICAL,
422 icons::HELM,
423 critical,
424 high,
425 brand::RESET
426 )
427 } else if high > 0 {
428 format!(
429 "{}{} {} {} high priority issues{}",
430 brand::PEACH,
431 icons::HIGH,
432 icons::HELM,
433 high,
434 brand::RESET
435 )
436 } else {
437 format!(
438 "{}{} {} {} issues (medium/low){}",
439 brand::PEACH,
440 icons::MEDIUM,
441 icons::HELM,
442 total,
443 brand::RESET
444 )
445 }
446 }
447 } else {
448 format!("{} Helmlint analysis complete", icons::HELM)
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_format_summary_success() {
459 let json = r#"{"success": true, "summary": {"total": 0, "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}}}"#;
460 let summary = HelmlintDisplay::format_summary(json);
461 assert!(summary.contains("OK"));
462 }
463
464 #[test]
465 fn test_format_summary_high() {
466 let json = r#"{"success": false, "summary": {"total": 3, "by_priority": {"critical": 0, "high": 2, "medium": 1, "low": 0}}}"#;
467 let summary = HelmlintDisplay::format_summary(json);
468 assert!(summary.contains("high"));
469 }
470
471 #[test]
472 fn test_category_badge() {
473 let badge = HelmlintDisplay::get_category_badge("Template");
474 assert!(badge.contains("TPL"));
475 }
476
477 #[test]
478 fn test_print_result_with_issues() {
479 let json = r#"{
481 "chart": "test-chart",
482 "success": false,
483 "decision_context": "High priority issues found. Fix template syntax.",
484 "summary": {
485 "total": 3,
486 "files_checked": 5,
487 "by_priority": {"critical": 0, "high": 2, "medium": 1, "low": 0}
488 },
489 "action_plan": {
490 "critical": [],
491 "high": [{
492 "code": "HL3001",
493 "file": "templates/deployment.yaml",
494 "line": 15,
495 "category": "Template",
496 "message": "Unclosed template block",
497 "fix": "Add {{- end }} to close the block"
498 }, {
499 "code": "HL1007",
500 "file": "Chart.yaml",
501 "line": 1,
502 "category": "Structure",
503 "message": "Missing maintainers field",
504 "fix": "Add maintainers list with name and email"
505 }],
506 "medium": [{
507 "code": "HL2003",
508 "file": "values.yaml",
509 "line": 8,
510 "category": "Values",
511 "message": "Unused value defined",
512 "fix": "Remove unused value or reference it in templates"
513 }],
514 "low": []
515 },
516 "quick_fixes": ["templates/deployment.yaml:15 HL3001 - Add {{- end }}", "Chart.yaml:1 HL1007 - Add maintainers list"]
517 }"#;
518
519 HelmlintDisplay::print_result(json);
521 }
522
523 #[test]
524 fn test_print_result_success() {
525 let json = r#"{
526 "chart": "good-chart",
527 "success": true,
528 "decision_context": "No issues found.",
529 "summary": {
530 "total": 0,
531 "files_checked": 8,
532 "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}
533 },
534 "action_plan": {"critical": [], "high": [], "medium": [], "low": []}
535 }"#;
536
537 HelmlintDisplay::print_result(json);
539 }
540}