1use crate::analyzer::{
7 MonorepoAnalysis, ProjectCategory, ArchitecturePattern,
8 DetectedTechnology, TechnologyCategory, LibraryType,
9 DockerAnalysis, OrchestrationPattern,
10};
11use colored::*;
12
13#[derive(Debug, Clone)]
15struct ContentLine {
16 label: String,
17 value: String,
18 label_colored: bool,
19}
20
21impl ContentLine {
22 fn new(label: &str, value: &str, label_colored: bool) -> Self {
23 Self {
24 label: label.to_string(),
25 value: value.to_string(),
26 label_colored,
27 }
28 }
29
30
31 fn separator() -> Self {
32 Self {
33 label: "SEPARATOR".to_string(),
34 value: String::new(),
35 label_colored: false,
36 }
37 }
38
39
40}
41
42pub struct BoxDrawer {
44 title: String,
45 lines: Vec<ContentLine>,
46 min_width: usize,
47 max_width: usize,
48}
49
50impl BoxDrawer {
51 pub fn new(title: &str) -> Self {
52 Self {
53 title: title.to_string(),
54 lines: Vec::new(),
55 min_width: 60,
56 max_width: 120, }
58 }
59
60 pub fn add_line(&mut self, label: &str, value: &str, label_colored: bool) {
61 self.lines.push(ContentLine::new(label, value, label_colored));
62 }
63
64 pub fn add_value_only(&mut self, value: &str) {
65 self.lines.push(ContentLine::new("", value, false));
66 }
67
68 pub fn add_separator(&mut self) {
69 self.lines.push(ContentLine::separator());
70 }
71
72 fn calculate_optimal_width(&self) -> usize {
74 let title_width = visual_width(&self.title) + 6; let mut max_content_width = 0;
76
77 for line in &self.lines {
79 if line.label == "SEPARATOR" {
80 continue;
81 }
82
83 let rendered_width = self.calculate_rendered_line_width(line);
84 max_content_width = max_content_width.max(rendered_width);
85 }
86
87 let content_width_with_buffer = max_content_width + 4; let needed_width = content_width_with_buffer + 4;
92
93 let optimal_width = title_width.max(needed_width).max(self.min_width);
95 optimal_width.min(self.max_width)
96 }
97
98 fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
100 let label_width = visual_width(&line.label);
101 let value_width = visual_width(&line.value);
102
103 if !line.label.is_empty() && !line.value.is_empty() {
104 let min_label_space = if line.label_colored { 25 } else { label_width };
107 min_label_space + 2 + value_width } else if !line.value.is_empty() {
109 value_width
111 } else if !line.label.is_empty() {
112 label_width
114 } else {
115 0
117 }
118 }
119
120 pub fn draw(&self) -> String {
122 let box_width = self.calculate_optimal_width();
123 let content_width = box_width - 4; let mut output = Vec::new();
126
127 output.push(self.draw_top(box_width));
129
130 for line in &self.lines {
132 if line.label == "SEPARATOR" {
133 output.push(self.draw_separator(box_width));
134 } else if line.label.is_empty() && line.value.is_empty() {
135 output.push(self.draw_empty_line(box_width));
136 } else {
137 output.push(self.draw_content_line(line, content_width));
138 }
139 }
140
141 output.push(self.draw_bottom(box_width));
143
144 output.join("\n")
145 }
146
147 fn draw_top(&self, width: usize) -> String {
148 let title_colored = self.title.bright_cyan();
149 let title_len = visual_width(&self.title);
150
151 let prefix_len = 3; let suffix_len = 1; let title_space = 1; let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
157
158 format!("┌─ {} {}┐",
159 title_colored,
160 "─".repeat(remaining_space)
161 )
162 }
163
164 fn draw_bottom(&self, width: usize) -> String {
165 format!("└{}┘", "─".repeat(width - 2))
166 }
167
168 fn draw_separator(&self, width: usize) -> String {
169 format!("│ {} │", "─".repeat(width - 4).dimmed())
170 }
171
172 fn draw_empty_line(&self, width: usize) -> String {
173 format!("│ {} │", " ".repeat(width - 4))
174 }
175
176 fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String {
177 let formatted_label = if line.label_colored && !line.label.is_empty() {
179 line.label.bright_white().to_string()
180 } else {
181 line.label.clone()
182 };
183
184 let label_display_width = visual_width(&line.label);
186 let value_display_width = visual_width(&line.value);
187
188 let content = if !line.label.is_empty() && !line.value.is_empty() {
190 let min_label_space = if line.label_colored { 25 } else { label_display_width };
192 let label_padding = min_label_space.saturating_sub(label_display_width);
193 let remaining_space = content_width.saturating_sub(min_label_space + 2); if value_display_width <= remaining_space {
196 let value_padding = remaining_space.saturating_sub(value_display_width);
198 format!("{}{:<width$} {}{}",
199 formatted_label,
200 "",
201 " ".repeat(value_padding),
202 line.value,
203 width = label_padding
204 )
205 } else {
206 let truncated_value = truncate_to_width(&line.value, remaining_space.saturating_sub(3));
208 format!("{}{:<width$} {}",
209 formatted_label,
210 "",
211 truncated_value,
212 width = label_padding
213 )
214 }
215 } else if !line.value.is_empty() {
216 if value_display_width <= content_width {
218 format!("{:<width$}", line.value, width = content_width)
219 } else {
220 truncate_to_width(&line.value, content_width)
221 }
222 } else if !line.label.is_empty() {
223 if label_display_width <= content_width {
225 format!("{:<width$}", formatted_label, width = content_width)
226 } else {
227 truncate_to_width(&formatted_label, content_width)
228 }
229 } else {
230 " ".repeat(content_width)
232 };
233
234 let actual_width = visual_width(&content);
236 let final_content = if actual_width < content_width {
237 format!("{}{}", content, " ".repeat(content_width - actual_width))
238 } else if actual_width > content_width {
239 truncate_to_width(&content, content_width)
240 } else {
241 content
242 };
243
244 format!("│ {} │", final_content)
245 }
246}
247
248fn visual_width(s: &str) -> usize {
250 let mut width = 0;
251 let mut chars = s.chars().peekable();
252
253 while let Some(ch) = chars.next() {
254 if ch == '\x1b' {
255 if chars.peek() == Some(&'[') {
257 chars.next(); while let Some(c) = chars.next() {
259 if c.is_ascii_alphabetic() {
260 break; }
262 }
263 }
264 } else {
265 width += char_width(ch);
268 }
269 }
270
271 width
272}
273
274fn char_width(ch: char) -> usize {
276 match ch {
277 '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
279 '\u{0300}'..='\u{036F}' => 0,
281 '\u{2600}'..='\u{26FF}' | '\u{2700}'..='\u{27BF}' | '\u{1F000}'..='\u{1F02F}' | '\u{1F030}'..='\u{1F09F}' | '\u{1F0A0}'..='\u{1F0FF}' | '\u{1F100}'..='\u{1F1FF}' | '\u{1F200}'..='\u{1F2FF}' | '\u{1F300}'..='\u{1F5FF}' | '\u{1F600}'..='\u{1F64F}' | '\u{1F650}'..='\u{1F67F}' | '\u{1F680}'..='\u{1F6FF}' | '\u{1F700}'..='\u{1F77F}' | '\u{1F780}'..='\u{1F7FF}' | '\u{1F800}'..='\u{1F8FF}' | '\u{1F900}'..='\u{1F9FF}' | '\u{1100}'..='\u{115F}' | '\u{2E80}'..='\u{2EFF}' | '\u{2F00}'..='\u{2FDF}' | '\u{2FF0}'..='\u{2FFF}' | '\u{3000}'..='\u{303E}' | '\u{3041}'..='\u{3096}' | '\u{30A1}'..='\u{30FA}' | '\u{3105}'..='\u{312D}' | '\u{3131}'..='\u{318E}' | '\u{3190}'..='\u{31BA}' | '\u{31C0}'..='\u{31E3}' | '\u{31F0}'..='\u{31FF}' | '\u{3200}'..='\u{32FF}' | '\u{3300}'..='\u{33FF}' | '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{A000}'..='\u{A48C}' | '\u{A490}'..='\u{A4C6}' | '\u{AC00}'..='\u{D7AF}' | '\u{F900}'..='\u{FAFF}' | '\u{FE10}'..='\u{FE19}' | '\u{FE30}'..='\u{FE6F}' | '\u{FF00}'..='\u{FF60}' | '\u{FFE0}'..='\u{FFE6}' => 2,
322 _ => 1,
324 }
325}
326
327fn truncate_to_width(s: &str, max_width: usize) -> String {
329 let current_visual_width = visual_width(s);
330 if current_visual_width <= max_width {
331 return s.to_string();
332 }
333
334 if s.contains('\x1b') {
336 let stripped = strip_ansi_codes(s);
338 if visual_width(&stripped) <= max_width {
339 return s.to_string();
340 }
341
342 let mut result = String::new();
344 let mut width = 0;
345 for ch in stripped.chars() {
346 let ch_width = char_width(ch);
347 if width + ch_width > max_width.saturating_sub(3) {
348 result.push_str("...");
349 break;
350 }
351 result.push(ch);
352 width += ch_width;
353 }
354 return result;
355 }
356
357 let mut result = String::new();
359 let mut width = 0;
360
361 for ch in s.chars() {
362 let ch_width = char_width(ch);
363 if width + ch_width > max_width.saturating_sub(3) {
364 result.push_str("...");
365 break;
366 }
367 result.push(ch);
368 width += ch_width;
369 }
370
371 result
372}
373
374fn strip_ansi_codes(s: &str) -> String {
376 let mut result = String::new();
377 let mut chars = s.chars().peekable();
378
379 while let Some(ch) = chars.next() {
380 if ch == '\x1b' {
381 if chars.peek() == Some(&'[') {
383 chars.next(); while let Some(c) = chars.next() {
385 if c.is_ascii_alphabetic() {
386 break; }
388 }
389 }
390 } else {
391 result.push(ch);
392 }
393 }
394
395 result
396}
397
398#[derive(Debug, Clone, Copy, PartialEq)]
400pub enum DisplayMode {
401 Matrix,
403 Detailed,
405 Summary,
407 Json,
409}
410
411pub fn display_analysis(analysis: &MonorepoAnalysis, mode: DisplayMode) {
413 match mode {
414 DisplayMode::Matrix => display_matrix_view(analysis),
415 DisplayMode::Detailed => display_detailed_view(analysis),
416 DisplayMode::Summary => display_summary_view(analysis),
417 DisplayMode::Json => display_json_view(analysis),
418 }
419}
420
421pub fn display_matrix_view(analysis: &MonorepoAnalysis) {
423 println!("\n{}", "═".repeat(100).bright_blue());
425 println!("{}", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold());
426 println!("{}", "═".repeat(100).bright_blue());
427
428 display_architecture_box(analysis);
430
431 display_technology_stack_box(analysis);
433
434 if analysis.projects.len() > 1 {
436 display_projects_matrix(analysis);
437 } else {
438 display_single_project_matrix(analysis);
439 }
440
441 if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
443 display_docker_overview_matrix(analysis);
444 }
445
446 display_metrics_box(analysis);
448
449 println!("\n{}", "═".repeat(100).bright_blue());
451}
452
453fn display_architecture_box(analysis: &MonorepoAnalysis) {
455 let mut box_drawer = BoxDrawer::new("Architecture Overview");
456
457 let arch_type = if analysis.is_monorepo {
458 format!("Monorepo ({} projects)", analysis.projects.len())
459 } else {
460 "Single Project".to_string()
461 };
462
463 box_drawer.add_line("Type:", &arch_type.yellow(), true);
464 box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true);
465
466 let pattern_desc = match &analysis.technology_summary.architecture_pattern {
468 ArchitecturePattern::Monolithic => "Single, self-contained application",
469 ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation",
470 ArchitecturePattern::Microservices => "Multiple independent microservices",
471 ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces",
472 ArchitecturePattern::EventDriven => "Event-driven with decoupled components",
473 ArchitecturePattern::Mixed => "Mixed architecture patterns",
474 };
475 box_drawer.add_value_only(&pattern_desc.dimmed());
476
477 println!("\n{}", box_drawer.draw());
478}
479
480fn display_technology_stack_box(analysis: &MonorepoAnalysis) {
482 let mut box_drawer = BoxDrawer::new("Technology Stack");
483
484 let mut has_content = false;
485
486 if !analysis.technology_summary.languages.is_empty() {
488 let languages = analysis.technology_summary.languages.join(", ");
489 box_drawer.add_line("Languages:", &languages.blue(), true);
490 has_content = true;
491 }
492
493 if !analysis.technology_summary.frameworks.is_empty() {
495 let frameworks = analysis.technology_summary.frameworks.join(", ");
496 box_drawer.add_line("Frameworks:", &frameworks.magenta(), true);
497 has_content = true;
498 }
499
500 if !analysis.technology_summary.databases.is_empty() {
502 let databases = analysis.technology_summary.databases.join(", ");
503 box_drawer.add_line("Databases:", &databases.cyan(), true);
504 has_content = true;
505 }
506
507 if !has_content {
508 box_drawer.add_value_only("No technologies detected");
509 }
510
511 println!("\n{}", box_drawer.draw());
512}
513
514fn display_projects_matrix(analysis: &MonorepoAnalysis) {
516 let mut box_drawer = BoxDrawer::new("Projects Matrix");
517
518 let mut project_data = Vec::new();
520 for project in &analysis.projects {
521 let name = project.name.clone(); let proj_type = format_project_category(&project.project_category);
523
524 let languages = project.analysis.languages.iter()
525 .map(|l| l.name.clone())
526 .collect::<Vec<_>>()
527 .join(", ");
528
529 let main_tech = get_main_technologies(&project.analysis.technologies);
530
531 let ports = if project.analysis.ports.is_empty() {
532 "-".to_string()
533 } else {
534 project.analysis.ports.iter()
535 .map(|p| p.number.to_string())
536 .collect::<Vec<_>>()
537 .join(", ")
538 };
539
540 let docker = if project.analysis.docker_analysis.is_some() {
541 "Yes"
542 } else {
543 "No"
544 };
545
546 let deps_count = project.analysis.dependencies.len().to_string();
547
548 project_data.push((name, proj_type.to_string(), languages, main_tech, ports, docker.to_string(), deps_count));
549 }
550
551 let headers = vec!["Project", "Type", "Languages", "Main Tech", "Ports", "Docker", "Deps"];
553 let mut col_widths = headers.iter().map(|h| visual_width(h)).collect::<Vec<_>>();
554
555 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in &project_data {
556 col_widths[0] = col_widths[0].max(visual_width(name));
557 col_widths[1] = col_widths[1].max(visual_width(proj_type));
558 col_widths[2] = col_widths[2].max(visual_width(languages));
559 col_widths[3] = col_widths[3].max(visual_width(main_tech));
560 col_widths[4] = col_widths[4].max(visual_width(ports));
561 col_widths[5] = col_widths[5].max(visual_width(docker));
562 col_widths[6] = col_widths[6].max(visual_width(deps_count));
563 }
564
565
566 let header_parts: Vec<String> = headers.iter().zip(&col_widths)
568 .map(|(h, &w)| format!("{:<width$}", h, width = w))
569 .collect();
570 let header_line = header_parts.join(" │ ");
571 box_drawer.add_value_only(&header_line);
572
573 let separator_parts: Vec<String> = col_widths.iter()
575 .map(|&w| "─".repeat(w))
576 .collect();
577 let separator_line = separator_parts.join("─┼─");
578 box_drawer.add_value_only(&separator_line);
579
580 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in project_data {
582 let row_parts = vec![
583 format!("{:<width$}", name, width = col_widths[0]),
584 format!("{:<width$}", proj_type, width = col_widths[1]),
585 format!("{:<width$}", languages, width = col_widths[2]),
586 format!("{:<width$}", main_tech, width = col_widths[3]),
587 format!("{:<width$}", ports, width = col_widths[4]),
588 format!("{:<width$}", docker, width = col_widths[5]),
589 format!("{:<width$}", deps_count, width = col_widths[6]),
590 ];
591 let row_line = row_parts.join(" │ ");
592 box_drawer.add_value_only(&row_line);
593 }
594
595 println!("\n{}", box_drawer.draw());
596}
597
598fn display_single_project_matrix(analysis: &MonorepoAnalysis) {
600 if let Some(project) = analysis.projects.first() {
601 let mut box_drawer = BoxDrawer::new("Project Overview");
602
603 box_drawer.add_line("Name:", &project.name.yellow(), true);
605 box_drawer.add_line("Type:", &format_project_category(&project.project_category).green(), true);
606
607 if !project.analysis.languages.is_empty() {
609 let lang_info = project.analysis.languages.iter()
610 .map(|l| l.name.clone())
611 .collect::<Vec<_>>()
612 .join(", ");
613 box_drawer.add_line("Languages:", &lang_info.blue(), true);
614 }
615
616 add_technologies_to_drawer(&project.analysis.technologies, &mut box_drawer);
618
619 box_drawer.add_separator();
621 box_drawer.add_line("Key Metrics:", "", true);
622
623 box_drawer.add_value_only(&format!("Entry Points: {} │ Exposed Ports: {} │ Env Variables: {}",
625 project.analysis.entry_points.len(),
626 project.analysis.ports.len(),
627 project.analysis.environment_variables.len()
628 ).cyan());
629
630 box_drawer.add_value_only(&format!("Build Scripts: {} │ Dependencies: {}",
631 project.analysis.build_scripts.len(),
632 project.analysis.dependencies.len()
633 ).cyan());
634
635 add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer);
637
638 println!("\n{}", box_drawer.draw());
639 }
640}
641
642fn add_technologies_to_drawer(technologies: &[DetectedTechnology], box_drawer: &mut BoxDrawer) {
644 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
645
646 for tech in technologies {
647 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
648 }
649
650 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
652 let primary_info = primary.name.bright_yellow().bold().to_string();
653 box_drawer.add_line("Primary Stack:", &primary_info, true);
654 }
655
656 let categories = [
658 (TechnologyCategory::FrontendFramework, "Frameworks"),
659 (TechnologyCategory::BuildTool, "Build Tools"),
660 (TechnologyCategory::Database, "Databases"),
661 (TechnologyCategory::Testing, "Testing"),
662 ];
663
664 for (category, label) in &categories {
665 if let Some(techs) = by_category.get(category) {
666 let tech_names = techs.iter()
667 .map(|t| t.name.clone())
668 .collect::<Vec<_>>()
669 .join(", ");
670
671 if !tech_names.is_empty() {
672 let label_with_colon = format!("{}:", label);
673 box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true);
674 }
675 }
676 }
677
678 let mut all_libraries: Vec<&DetectedTechnology> = Vec::new();
680 for (cat, techs) in &by_category {
681 if matches!(cat, TechnologyCategory::Library(_)) {
682 all_libraries.extend(techs.iter().copied());
683 }
684 }
685
686 if !all_libraries.is_empty() {
687 all_libraries.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
689
690 if all_libraries.len() <= 3 {
691 let tech_names = all_libraries.iter()
693 .map(|t| t.name.clone())
694 .collect::<Vec<_>>()
695 .join(", ");
696 box_drawer.add_line("Libraries:", &tech_names.magenta(), true);
697 } else {
698 box_drawer.add_line("Libraries:", "", true);
700
701 let items_per_row = 3;
703 for chunk in all_libraries.chunks(items_per_row) {
704 let row_items = chunk.iter()
705 .map(|t| t.name.clone())
706 .collect::<Vec<_>>()
707 .join(", ");
708
709 let indented_row = format!(" {}", row_items);
711 box_drawer.add_value_only(&indented_row.magenta());
712 }
713 }
714 }
715}
716
717fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) {
719 let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
720
721 let mut total_dockerfiles = 0;
722 let mut total_compose_files = 0;
723 let mut total_services = 0;
724 let mut orchestration_patterns = std::collections::HashSet::new();
725
726 for project in &analysis.projects {
727 if let Some(docker) = &project.analysis.docker_analysis {
728 total_dockerfiles += docker.dockerfiles.len();
729 total_compose_files += docker.compose_files.len();
730 total_services += docker.services.len();
731 orchestration_patterns.insert(&docker.orchestration_pattern);
732 }
733 }
734
735 box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
736 box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
737 box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
738
739 let patterns = orchestration_patterns.iter()
740 .map(|p| format!("{:?}", p))
741 .collect::<Vec<_>>()
742 .join(", ");
743 box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true);
744
745 let mut has_services = false;
747 for project in &analysis.projects {
748 if let Some(docker) = &project.analysis.docker_analysis {
749 for service in &docker.services {
750 if !service.ports.is_empty() || !service.depends_on.is_empty() {
751 has_services = true;
752 break;
753 }
754 }
755 }
756 }
757
758 if has_services {
759 box_drawer.add_separator();
760 box_drawer.add_line("Service Connectivity:", "", true);
761
762 for project in &analysis.projects {
763 if let Some(docker) = &project.analysis.docker_analysis {
764 for service in &docker.services {
765 if !service.ports.is_empty() || !service.depends_on.is_empty() {
766 let port_info = service.ports.iter()
767 .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port)))
768 .collect::<Vec<_>>()
769 .join(", ");
770
771 let deps_info = if service.depends_on.is_empty() {
772 String::new()
773 } else {
774 format!(" → {}", service.depends_on.join(", "))
775 };
776
777 let info = format!(" {}: {}{}", service.name, port_info, deps_info);
778 box_drawer.add_value_only(&info.cyan());
779 }
780 }
781 }
782 }
783 }
784
785 println!("\n{}", box_drawer.draw());
786}
787
788fn display_metrics_box(analysis: &MonorepoAnalysis) {
790 let mut box_drawer = BoxDrawer::new("Analysis Metrics");
791
792 let duration_ms = analysis.metadata.analysis_duration_ms;
794 let duration_str = if duration_ms < 1000 {
795 format!("{}ms", duration_ms)
796 } else {
797 format!("{:.1}s", duration_ms as f64 / 1000.0)
798 };
799
800 let metrics_line = format!(
802 "Duration: {} | Files: {} | Score: {}% | Version: {}",
803 duration_str,
804 analysis.metadata.files_analyzed,
805 format!("{:.0}", analysis.metadata.confidence_score * 100.0),
806 analysis.metadata.analyzer_version
807 );
808
809 let colored_metrics = metrics_line.cyan();
811 box_drawer.add_value_only(&colored_metrics.to_string());
812
813 println!("\n{}", box_drawer.draw());
814}
815
816fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) {
818 let percentage = (score * 100.0) as u8;
819 let bar_width = 20;
820 let filled = ((score * bar_width as f32) as usize).min(bar_width);
821
822 let bar = format!("{}{}",
823 "█".repeat(filled).green(),
824 "░".repeat(bar_width - filled).dimmed()
825 );
826
827 let color = if percentage >= 80 {
828 "green"
829 } else if percentage >= 60 {
830 "yellow"
831 } else {
832 "red"
833 };
834
835 let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color));
836 box_drawer.add_line("Confidence:", &confidence_info, true);
837}
838
839fn get_main_technologies(technologies: &[DetectedTechnology]) -> String {
841 let primary = technologies.iter().find(|t| t.is_primary);
842 let frameworks: Vec<_> = technologies.iter()
843 .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework))
844 .take(2)
845 .collect();
846
847 let mut result = Vec::new();
848
849 if let Some(p) = primary {
850 result.push(p.name.clone());
851 }
852
853 for f in frameworks {
854 if Some(&f.name) != primary.map(|p| &p.name) {
855 result.push(f.name.clone());
856 }
857 }
858
859 if result.is_empty() {
860 "-".to_string()
861 } else {
862 result.join(", ")
863 }
864}
865
866pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
868 println!("{}", "=".repeat(80));
870 println!("\n📊 PROJECT ANALYSIS RESULTS");
871 println!("{}", "=".repeat(80));
872
873 if analysis.is_monorepo {
875 println!("\n🏗️ Architecture: Monorepo with {} projects", analysis.projects.len());
876 println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern);
877
878 display_architecture_description(&analysis.technology_summary.architecture_pattern);
879 } else {
880 println!("\n🏗️ Architecture: Single Project");
881 }
882
883 println!("\n🌐 Technology Summary:");
885 if !analysis.technology_summary.languages.is_empty() {
886 println!(" Languages: {}", analysis.technology_summary.languages.join(", "));
887 }
888 if !analysis.technology_summary.frameworks.is_empty() {
889 println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", "));
890 }
891 if !analysis.technology_summary.databases.is_empty() {
892 println!(" Databases: {}", analysis.technology_summary.databases.join(", "));
893 }
894
895 println!("\n📁 Project Details:");
897 println!("{}", "=".repeat(80));
898
899 for (i, project) in analysis.projects.iter().enumerate() {
900 println!("\n{} {}. {} ({})",
901 get_category_emoji(&project.project_category),
902 i + 1,
903 project.name,
904 format_project_category(&project.project_category)
905 );
906
907 if analysis.is_monorepo {
908 println!(" 📂 Path: {}", project.path.display());
909 }
910
911 if !project.analysis.languages.is_empty() {
913 println!(" 🌐 Languages:");
914 for lang in &project.analysis.languages {
915 print!(" • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0);
916 if let Some(version) = &lang.version {
917 print!(" - Version: {}", version);
918 }
919 println!();
920 }
921 }
922
923 if !project.analysis.technologies.is_empty() {
925 println!(" 🚀 Technologies:");
926 display_technologies_detailed_legacy(&project.analysis.technologies);
927 }
928
929 if !project.analysis.entry_points.is_empty() {
931 println!(" 📍 Entry Points ({}):", project.analysis.entry_points.len());
932 for (j, entry) in project.analysis.entry_points.iter().enumerate() {
933 println!(" {}. File: {}", j + 1, entry.file.display());
934 if let Some(func) = &entry.function {
935 println!(" Function: {}", func);
936 }
937 if let Some(cmd) = &entry.command {
938 println!(" Command: {}", cmd);
939 }
940 }
941 }
942
943 if !project.analysis.ports.is_empty() {
945 println!(" 🔌 Exposed Ports ({}):", project.analysis.ports.len());
946 for port in &project.analysis.ports {
947 println!(" • Port {}: {:?}", port.number, port.protocol);
948 if let Some(desc) = &port.description {
949 println!(" {}", desc);
950 }
951 }
952 }
953
954 if !project.analysis.environment_variables.is_empty() {
956 println!(" 🔐 Environment Variables ({}):", project.analysis.environment_variables.len());
957 let required_vars: Vec<_> = project.analysis.environment_variables.iter()
958 .filter(|ev| ev.required)
959 .collect();
960 let optional_vars: Vec<_> = project.analysis.environment_variables.iter()
961 .filter(|ev| !ev.required)
962 .collect();
963
964 if !required_vars.is_empty() {
965 println!(" Required:");
966 for var in required_vars {
967 println!(" • {} {}",
968 var.name,
969 if let Some(desc) = &var.description {
970 format!("({})", desc)
971 } else {
972 String::new()
973 }
974 );
975 }
976 }
977
978 if !optional_vars.is_empty() {
979 println!(" Optional:");
980 for var in optional_vars {
981 println!(" • {} = {:?}",
982 var.name,
983 var.default_value.as_deref().unwrap_or("no default")
984 );
985 }
986 }
987 }
988
989 if !project.analysis.build_scripts.is_empty() {
991 println!(" 🔨 Build Scripts ({}):", project.analysis.build_scripts.len());
992 let default_scripts: Vec<_> = project.analysis.build_scripts.iter()
993 .filter(|bs| bs.is_default)
994 .collect();
995 let other_scripts: Vec<_> = project.analysis.build_scripts.iter()
996 .filter(|bs| !bs.is_default)
997 .collect();
998
999 if !default_scripts.is_empty() {
1000 println!(" Default scripts:");
1001 for script in default_scripts {
1002 println!(" • {}: {}", script.name, script.command);
1003 if let Some(desc) = &script.description {
1004 println!(" {}", desc);
1005 }
1006 }
1007 }
1008
1009 if !other_scripts.is_empty() {
1010 println!(" Other scripts:");
1011 for script in other_scripts {
1012 println!(" • {}: {}", script.name, script.command);
1013 if let Some(desc) = &script.description {
1014 println!(" {}", desc);
1015 }
1016 }
1017 }
1018 }
1019
1020 if !project.analysis.dependencies.is_empty() {
1022 println!(" 📦 Dependencies ({}):", project.analysis.dependencies.len());
1023 if project.analysis.dependencies.len() <= 5 {
1024 for (name, version) in &project.analysis.dependencies {
1025 println!(" • {} v{}", name, version);
1026 }
1027 } else {
1028 for (name, version) in project.analysis.dependencies.iter().take(5) {
1030 println!(" • {} v{}", name, version);
1031 }
1032 println!(" ... and {} more", project.analysis.dependencies.len() - 5);
1033 }
1034 }
1035
1036 if let Some(docker_analysis) = &project.analysis.docker_analysis {
1038 display_docker_analysis_detailed_legacy(docker_analysis);
1039 }
1040
1041 println!(" 🎯 Project Type: {:?}", project.analysis.project_type);
1043
1044 if i < analysis.projects.len() - 1 {
1045 println!("{}", "-".repeat(40));
1046 }
1047 }
1048
1049 println!("\n📋 ANALYSIS SUMMARY");
1051 println!("{}", "=".repeat(80));
1052 println!("✅ Project Analysis Complete!");
1053
1054 if analysis.is_monorepo {
1055 println!("\n🏗️ Monorepo Architecture:");
1056 println!(" • Total projects: {}", analysis.projects.len());
1057 println!(" • Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern);
1058
1059 let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count();
1060 let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count();
1061 let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count();
1062 let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count();
1063
1064 if frontend_count > 0 { println!(" • Frontend projects: {}", frontend_count); }
1065 if backend_count > 0 { println!(" • Backend/API projects: {}", backend_count); }
1066 if service_count > 0 { println!(" • Service projects: {}", service_count); }
1067 if lib_count > 0 { println!(" • Library projects: {}", lib_count); }
1068 }
1069
1070 println!("\n📈 Analysis Metadata:");
1071 println!(" • Duration: {}ms", analysis.metadata.analysis_duration_ms);
1072 println!(" • Files analyzed: {}", analysis.metadata.files_analyzed);
1073 println!(" • Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0);
1074 println!(" • Analyzer version: {}", analysis.metadata.analyzer_version);
1075}
1076
1077fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1079 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1081
1082 for tech in technologies {
1083 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1084 }
1085
1086 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1088 println!("\n🛠️ Technology Stack:");
1089 println!(" 🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
1090 println!(" Architecture driver for this project");
1091 }
1092
1093 let categories = [
1095 (TechnologyCategory::MetaFramework, "🏗️ Meta-Frameworks"),
1096 (TechnologyCategory::BackendFramework, "🖥️ Backend Frameworks"),
1097 (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"),
1098 (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"),
1099 (TechnologyCategory::Library(LibraryType::Utility), "📚 Core Libraries"),
1100 (TechnologyCategory::BuildTool, "🔨 Build Tools"),
1101 (TechnologyCategory::PackageManager, "📦 Package Managers"),
1102 (TechnologyCategory::Database, "🗃️ Database & ORM"),
1103 (TechnologyCategory::Runtime, "⚡ Runtimes"),
1104 (TechnologyCategory::Testing, "🧪 Testing"),
1105 ];
1106
1107 for (category, label) in &categories {
1108 if let Some(techs) = by_category.get(category) {
1109 if !techs.is_empty() {
1110 println!("\n {}:", label);
1111 for tech in techs {
1112 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1113 if let Some(version) = &tech.version {
1114 println!(" Version: {}", version);
1115 }
1116 }
1117 }
1118 }
1119 }
1120
1121 for (cat, techs) in &by_category {
1123 match cat {
1124 TechnologyCategory::Library(lib_type) => {
1125 let label = match lib_type {
1126 LibraryType::StateManagement => "🔄 State Management",
1127 LibraryType::DataFetching => "🔃 Data Fetching",
1128 LibraryType::Routing => "🗺️ Routing",
1129 LibraryType::Styling => "🎨 Styling",
1130 LibraryType::HttpClient => "🌐 HTTP Clients",
1131 LibraryType::Authentication => "🔐 Authentication",
1132 LibraryType::Other(_) => "📦 Other Libraries",
1133 _ => continue, };
1135
1136 if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() {
1138 println!("\n {}:", label);
1139 for tech in techs {
1140 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1141 if let Some(version) = &tech.version {
1142 println!(" Version: {}", version);
1143 }
1144 }
1145 }
1146 }
1147 _ => {} }
1149 }
1150}
1151
1152fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1154 println!("\n 🐳 Docker Infrastructure Analysis:");
1155
1156 if !docker_analysis.dockerfiles.is_empty() {
1158 println!(" 📄 Dockerfiles ({}):", docker_analysis.dockerfiles.len());
1159 for dockerfile in &docker_analysis.dockerfiles {
1160 println!(" • {}", dockerfile.path.display());
1161 if let Some(env) = &dockerfile.environment {
1162 println!(" Environment: {}", env);
1163 }
1164 if let Some(base_image) = &dockerfile.base_image {
1165 println!(" Base image: {}", base_image);
1166 }
1167 if !dockerfile.exposed_ports.is_empty() {
1168 println!(" Exposed ports: {}",
1169 dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "));
1170 }
1171 if dockerfile.is_multistage {
1172 println!(" Multi-stage build: {} stages", dockerfile.build_stages.len());
1173 }
1174 println!(" Instructions: {}", dockerfile.instruction_count);
1175 }
1176 }
1177
1178 if !docker_analysis.compose_files.is_empty() {
1180 println!(" 📋 Compose Files ({}):", docker_analysis.compose_files.len());
1181 for compose_file in &docker_analysis.compose_files {
1182 println!(" • {}", compose_file.path.display());
1183 if let Some(env) = &compose_file.environment {
1184 println!(" Environment: {}", env);
1185 }
1186 if let Some(version) = &compose_file.version {
1187 println!(" Version: {}", version);
1188 }
1189 if !compose_file.service_names.is_empty() {
1190 println!(" Services: {}", compose_file.service_names.join(", "));
1191 }
1192 if !compose_file.networks.is_empty() {
1193 println!(" Networks: {}", compose_file.networks.join(", "));
1194 }
1195 if !compose_file.volumes.is_empty() {
1196 println!(" Volumes: {}", compose_file.volumes.join(", "));
1197 }
1198 }
1199 }
1200
1201 println!(" 🏗️ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern);
1203 match docker_analysis.orchestration_pattern {
1204 OrchestrationPattern::SingleContainer => {
1205 println!(" Simple containerized application");
1206 }
1207 OrchestrationPattern::DockerCompose => {
1208 println!(" Multi-service Docker Compose setup");
1209 }
1210 OrchestrationPattern::Microservices => {
1211 println!(" Microservices architecture with service discovery");
1212 }
1213 OrchestrationPattern::EventDriven => {
1214 println!(" Event-driven architecture with message queues");
1215 }
1216 OrchestrationPattern::ServiceMesh => {
1217 println!(" Service mesh for advanced service communication");
1218 }
1219 OrchestrationPattern::Mixed => {
1220 println!(" Mixed/complex orchestration pattern");
1221 }
1222 }
1223}
1224
1225fn display_architecture_description(pattern: &ArchitecturePattern) {
1227 match pattern {
1228 ArchitecturePattern::Monolithic => {
1229 println!(" 📦 This is a single, self-contained application");
1230 }
1231 ArchitecturePattern::Fullstack => {
1232 println!(" 🌐 This is a full-stack application with separate frontend and backend");
1233 }
1234 ArchitecturePattern::Microservices => {
1235 println!(" 🔗 This is a microservices architecture with multiple independent services");
1236 }
1237 ArchitecturePattern::ApiFirst => {
1238 println!(" 🔌 This is an API-first architecture focused on service interfaces");
1239 }
1240 ArchitecturePattern::EventDriven => {
1241 println!(" 📡 This is an event-driven architecture with decoupled components");
1242 }
1243 ArchitecturePattern::Mixed => {
1244 println!(" 🔀 This is a mixed architecture combining multiple patterns");
1245 }
1246 }
1247}
1248
1249pub fn display_summary_view(analysis: &MonorepoAnalysis) {
1251 println!("\n{} {}", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold());
1252 println!("{}", "─".repeat(50).dimmed());
1253
1254 println!("{} Architecture: {}", "│".dimmed(),
1255 if analysis.is_monorepo {
1256 format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1257 } else {
1258 "Single Project".to_string().yellow()
1259 }
1260 );
1261
1262 println!("{} Pattern: {}", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green());
1263 println!("{} Stack: {}", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue());
1264
1265 if !analysis.technology_summary.frameworks.is_empty() {
1266 println!("{} Frameworks: {}", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta());
1267 }
1268
1269 println!("{} Analysis Time: {}ms", "│".dimmed(), analysis.metadata.analysis_duration_ms);
1270 println!("{} Confidence: {:.0}%", "│".dimmed(), analysis.metadata.confidence_score * 100.0);
1271
1272 println!("{}", "─".repeat(50).dimmed());
1273}
1274
1275pub fn display_json_view(analysis: &MonorepoAnalysis) {
1277 match serde_json::to_string_pretty(analysis) {
1278 Ok(json) => println!("{}", json),
1279 Err(e) => eprintln!("Error serializing to JSON: {}", e),
1280 }
1281}
1282
1283fn get_category_emoji(category: &ProjectCategory) -> &'static str {
1285 match category {
1286 ProjectCategory::Frontend => "🌐",
1287 ProjectCategory::Backend => "⚙️",
1288 ProjectCategory::Api => "🔌",
1289 ProjectCategory::Service => "🚀",
1290 ProjectCategory::Library => "📚",
1291 ProjectCategory::Tool => "🔧",
1292 ProjectCategory::Documentation => "📖",
1293 ProjectCategory::Infrastructure => "🏗️",
1294 ProjectCategory::Unknown => "❓",
1295 }
1296}
1297
1298fn format_project_category(category: &ProjectCategory) -> &'static str {
1300 match category {
1301 ProjectCategory::Frontend => "Frontend",
1302 ProjectCategory::Backend => "Backend",
1303 ProjectCategory::Api => "API",
1304 ProjectCategory::Service => "Service",
1305 ProjectCategory::Library => "Library",
1306 ProjectCategory::Tool => "Tool",
1307 ProjectCategory::Documentation => "Documentation",
1308 ProjectCategory::Infrastructure => "Infrastructure",
1309 ProjectCategory::Unknown => "Unknown",
1310 }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315 use super::*;
1316
1317 #[test]
1318 fn test_display_modes() {
1319 assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1321 assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1322 }
1323}