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