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: 150, }
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 + 2; let needed_width = content_width_with_buffer + 4;
104
105 let min_reasonable_width = 50;
107 let optimal_width = title_width.max(needed_width).max(min_reasonable_width);
108 optimal_width.clamp(self.min_width, self.max_width)
109 }
110
111 fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
113 let label_display_width = visual_width(&line.label);
115 let mut value_display_width = visual_width(&line.value);
116
117 if !line.value.is_empty() {
119 if line.label.contains("Files") || line.label.contains("Duration") ||
121 line.label.contains("Dependencies") || line.label.contains("Ports") ||
122 line.label.contains("Services") || line.label.contains("Total") {
123 value_display_width = value_display_width.max(8); }
125 }
126
127 if !line.label.is_empty() && !line.value.is_empty() {
128 let actual_label_width = if line.label_colored {
131 label_display_width.max(20) } else {
133 label_display_width
134 };
135 actual_label_width + 1 + value_display_width
136 } else if !line.value.is_empty() {
137 value_display_width
139 } else if !line.label.is_empty() {
140 label_display_width
142 } else {
143 0
145 }
146 }
147
148 fn draw(&self) -> String {
150 let box_width = self.calculate_optimal_width();
151 let content_width = box_width - 4; let mut output = Vec::new();
154
155 output.push(self.draw_top(box_width));
157
158 for line in &self.lines {
160 if line.label == "SEPARATOR" {
161 output.push(self.draw_separator(box_width));
162 } else if line.label.is_empty() && line.value.is_empty() {
163 output.push(self.draw_empty_line(box_width));
164 } else {
165 output.push(self.draw_content_line(line, content_width));
166 }
167 }
168
169 output.push(self.draw_bottom(box_width));
171
172 output.join("\n")
173 }
174
175 fn draw_top(&self, width: usize) -> String {
176 let title_colored = self.title.bright_cyan();
177 let title_len = visual_width(&self.title);
178
179 let prefix_len = 3; let suffix_len = 1; let title_space = 1; let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
185
186 format!("┌─ {} {}┐",
187 title_colored,
188 "─".repeat(remaining_space)
189 )
190 }
191
192 fn draw_bottom(&self, width: usize) -> String {
193 format!("└{}┘", "─".repeat(width - 2))
194 }
195
196 fn draw_separator(&self, width: usize) -> String {
197 format!("│ {} │", "─".repeat(width - 4).dimmed())
198 }
199
200 fn draw_empty_line(&self, width: usize) -> String {
201 format!("│ {} │", " ".repeat(width - 4))
202 }
203
204 fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String {
205 let formatted_label = if line.label_colored && !line.label.is_empty() {
207 line.label.bright_white().to_string()
208 } else {
209 line.label.clone()
210 };
211 let formatted_value = line.value.clone();
212
213 let label_display_width = visual_width(&line.label); let value_display_width = visual_width(&formatted_value);
216
217 let effective_label_width = if line.label_colored && !line.label.is_empty() {
219 label_display_width.max(20) } else {
221 label_display_width
222 };
223
224 let content = if !line.label.is_empty() && !line.value.is_empty() {
226 let available_space = content_width;
228 let min_space_between = 1; let label_width = visual_width(&formatted_label);
232 let value_width = visual_width(&formatted_value);
233 let total_needed = label_width + min_space_between + value_width;
234
235 if total_needed <= available_space {
236 let padding_needed = available_space - label_width - value_width;
238 format!("{}{}{}", formatted_label, " ".repeat(padding_needed), formatted_value)
239 } else {
240 let max_value_width = available_space.saturating_sub(label_width + min_space_between);
242 let truncated_value = truncate_to_width(&formatted_value, max_value_width);
243 let truncated_value_width = visual_width(&truncated_value);
244 let padding_needed = available_space - label_width - truncated_value_width;
245 format!("{}{}{}", formatted_label, " ".repeat(padding_needed), truncated_value)
246 }
247 } else if !line.value.is_empty() {
248 let value_width = visual_width(&formatted_value);
250 if value_width <= content_width {
251 let padding_needed = content_width - value_width;
252 format!("{}{}", formatted_value, " ".repeat(padding_needed))
253 } else {
254 let truncated = truncate_to_width(&formatted_value, content_width);
256 let actual_width = visual_width(&truncated);
257 let padding_needed = content_width - actual_width;
258 format!("{}{}", truncated, " ".repeat(padding_needed))
259 }
260 } else if !line.label.is_empty() {
261 let label_width = visual_width(&formatted_label);
263 if label_width <= content_width {
264 let padding_needed = content_width - label_width;
265 format!("{}{}", formatted_label, " ".repeat(padding_needed))
266 } else {
267 let truncated = truncate_to_width(&formatted_label, content_width);
269 let actual_width = visual_width(&truncated);
270 let padding_needed = content_width - actual_width;
271 format!("{}{}", truncated, " ".repeat(padding_needed))
272 }
273 } else {
274 " ".repeat(content_width)
276 };
277
278 let actual_content_width = visual_width(&content);
280 let final_content = if actual_content_width == content_width {
281 content
282 } else if actual_content_width < content_width {
283 if content.contains("│") || content.contains("─┼─") {
285 content
286 } else {
287 let padding_needed = content_width - actual_content_width;
289 format!("{}{}", content, " ".repeat(padding_needed))
290 }
291 } else {
292 truncate_to_width(&content, content_width)
294 };
295
296 format!("│ {} │", final_content)
297 }
298}
299
300fn visual_width(s: &str) -> usize {
302 let mut width = 0;
303 let mut chars = s.chars().peekable();
304
305 while let Some(ch) = chars.next() {
306 if ch == '\x1b' {
307 if chars.peek() == Some(&'[') {
309 chars.next(); while let Some(c) = chars.next() {
311 if c.is_ascii_alphabetic() {
312 break; }
314 }
315 }
316 } else {
317 width += char_width(ch);
320 }
321 }
322
323 width
324}
325
326fn char_width(ch: char) -> usize {
328 match ch {
329 '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
331 '\u{0300}'..='\u{036F}' => 0,
333 '\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,
374 _ => 1,
376 }
377}
378
379fn truncate_to_width(s: &str, max_width: usize) -> String {
381 if visual_width(s) <= max_width {
382 return s.to_string();
383 }
384
385 let mut result = String::new();
386 let mut current_width = 0;
387 let mut chars = s.chars().peekable();
388
389 while let Some(ch) = chars.next() {
390 if ch == '\x1b' {
391 result.push(ch);
393 if chars.peek() == Some(&'[') {
394 result.push(chars.next().unwrap()); while let Some(c) = chars.next() {
396 result.push(c);
397 if c.is_ascii_alphabetic() {
398 break; }
400 }
401 }
402 } else {
403 let char_width = char_width(ch);
404 if current_width + char_width > max_width {
405 if max_width >= 3 {
406 result.push_str("...");
407 }
408 break;
409 }
410 result.push(ch);
411 current_width += char_width;
412 }
413 }
414
415 result
416}
417
418#[derive(Debug, Clone, Copy, PartialEq)]
420pub enum DisplayMode {
421 Matrix,
423 Detailed,
425 Summary,
427 Json,
429}
430
431pub fn display_analysis(analysis: &MonorepoAnalysis, mode: DisplayMode) {
433 match mode {
434 DisplayMode::Matrix => display_matrix_view(analysis),
435 DisplayMode::Detailed => display_detailed_view(analysis),
436 DisplayMode::Summary => display_summary_view(analysis),
437 DisplayMode::Json => display_json_view(analysis),
438 }
439}
440
441pub fn display_matrix_view(analysis: &MonorepoAnalysis) {
443 println!("\n{}", "═".repeat(100).bright_blue());
445 println!("{}", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold());
446 println!("{}", "═".repeat(100).bright_blue());
447
448 display_architecture_box(analysis);
450
451 display_technology_stack_box(analysis);
453
454 if analysis.projects.len() > 1 {
456 display_projects_matrix(analysis);
457 } else {
458 display_single_project_matrix(analysis);
459 }
460
461 if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
463 display_docker_overview_matrix(analysis);
464 }
465
466 display_metrics_box(analysis);
468
469 println!("\n{}", "═".repeat(100).bright_blue());
471}
472
473fn display_architecture_box(analysis: &MonorepoAnalysis) {
475 let mut box_drawer = BoxDrawer::new("Architecture Overview");
476
477 let arch_type = if analysis.is_monorepo {
478 format!("Monorepo ({} projects)", analysis.projects.len())
479 } else {
480 "Single Project".to_string()
481 };
482
483 box_drawer.add_line("Type:", &arch_type.yellow(), true);
484 box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true);
485
486 let pattern_desc = match &analysis.technology_summary.architecture_pattern {
488 ArchitecturePattern::Monolithic => "Single, self-contained application",
489 ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation",
490 ArchitecturePattern::Microservices => "Multiple independent microservices",
491 ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces",
492 ArchitecturePattern::EventDriven => "Event-driven with decoupled components",
493 ArchitecturePattern::Mixed => "Mixed architecture patterns",
494 };
495 box_drawer.add_value_only(&pattern_desc.dimmed());
496
497 println!("\n{}", box_drawer.draw());
498}
499
500fn display_technology_stack_box(analysis: &MonorepoAnalysis) {
502 let mut box_drawer = BoxDrawer::new("Technology Stack");
503
504 let mut has_content = false;
505
506 if !analysis.technology_summary.languages.is_empty() {
508 let languages = analysis.technology_summary.languages.join(", ");
509 box_drawer.add_line("Languages:", &languages.blue(), true);
510 has_content = true;
511 }
512
513 if !analysis.technology_summary.frameworks.is_empty() {
515 let frameworks = analysis.technology_summary.frameworks.join(", ");
516 box_drawer.add_line("Frameworks:", &frameworks.magenta(), true);
517 has_content = true;
518 }
519
520 if !analysis.technology_summary.databases.is_empty() {
522 let databases = analysis.technology_summary.databases.join(", ");
523 box_drawer.add_line("Databases:", &databases.cyan(), true);
524 has_content = true;
525 }
526
527 if !has_content {
528 box_drawer.add_value_only("No technologies detected");
529 }
530
531 println!("\n{}", box_drawer.draw());
532}
533
534fn display_projects_matrix(analysis: &MonorepoAnalysis) {
536 let mut box_drawer = BoxDrawer::new("Projects Matrix");
537
538 let mut project_data = Vec::new();
540 for project in &analysis.projects {
541 let name = project.name.clone(); let proj_type = format_project_category(&project.project_category);
543
544 let languages = project.analysis.languages.iter()
545 .map(|l| l.name.clone())
546 .collect::<Vec<_>>()
547 .join(", ");
548
549 let main_tech = get_main_technologies(&project.analysis.technologies);
550
551 let ports = if project.analysis.ports.is_empty() {
552 "-".to_string()
553 } else {
554 project.analysis.ports.iter()
555 .map(|p| p.number.to_string())
556 .collect::<Vec<_>>()
557 .join(", ")
558 };
559
560 let docker = if project.analysis.docker_analysis.is_some() {
561 "Yes"
562 } else {
563 "No"
564 };
565
566 let deps_count = project.analysis.dependencies.len().to_string();
567
568 project_data.push((name, proj_type.to_string(), languages, main_tech, ports, docker.to_string(), deps_count));
569 }
570
571 let headers = vec!["Project", "Type", "Languages", "Main Tech", "Ports", "Docker", "Deps"];
573 let mut col_widths = headers.iter().map(|h| visual_width(h)).collect::<Vec<_>>();
574
575 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in &project_data {
576 col_widths[0] = col_widths[0].max(visual_width(name));
577 col_widths[1] = col_widths[1].max(visual_width(proj_type));
578 col_widths[2] = col_widths[2].max(visual_width(languages));
579 col_widths[3] = col_widths[3].max(visual_width(main_tech));
580 col_widths[4] = col_widths[4].max(visual_width(ports));
581 col_widths[5] = col_widths[5].max(visual_width(docker));
582 col_widths[6] = col_widths[6].max(visual_width(deps_count));
583 }
584
585
586 let header_parts: Vec<String> = headers.iter().zip(&col_widths)
588 .map(|(h, &w)| format!("{:<width$}", h, width = w))
589 .collect();
590 let header_line = header_parts.join(" │ ");
591 box_drawer.add_value_only(&header_line);
592
593 let separator_parts: Vec<String> = col_widths.iter()
595 .map(|&w| "─".repeat(w))
596 .collect();
597 let separator_line = separator_parts.join("─┼─");
598 box_drawer.add_value_only(&separator_line);
599
600 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in project_data {
602 let row_parts = vec![
603 format!("{:<width$}", name, width = col_widths[0]),
604 format!("{:<width$}", proj_type, width = col_widths[1]),
605 format!("{:<width$}", languages, width = col_widths[2]),
606 format!("{:<width$}", main_tech, width = col_widths[3]),
607 format!("{:<width$}", ports, width = col_widths[4]),
608 format!("{:<width$}", docker, width = col_widths[5]),
609 format!("{:<width$}", deps_count, width = col_widths[6]),
610 ];
611 let row_line = row_parts.join(" │ ");
612 box_drawer.add_value_only(&row_line);
613 }
614
615 println!("\n{}", box_drawer.draw());
616}
617
618fn display_single_project_matrix(analysis: &MonorepoAnalysis) {
620 if let Some(project) = analysis.projects.first() {
621 let mut box_drawer = BoxDrawer::new("Project Overview");
622
623 box_drawer.add_line("Name:", &project.name.yellow(), true);
625 box_drawer.add_line("Type:", &format_project_category(&project.project_category).green(), true);
626
627 if !project.analysis.languages.is_empty() {
629 let lang_info = project.analysis.languages.iter()
630 .map(|l| format!("{} ({:.0}%)", l.name, l.confidence * 100.0))
631 .collect::<Vec<_>>()
632 .join(", ");
633 box_drawer.add_line("Languages:", &lang_info.blue(), true);
634 }
635
636 add_technologies_to_drawer(&project.analysis.technologies, &mut box_drawer);
638
639 box_drawer.add_separator();
641 box_drawer.add_line("Key Metrics:", "", true);
642
643 box_drawer.add_value_only(&format!("Entry Points: {} │ Exposed Ports: {} │ Env Variables: {}",
645 project.analysis.entry_points.len(),
646 project.analysis.ports.len(),
647 project.analysis.environment_variables.len()
648 ).cyan());
649
650 box_drawer.add_value_only(&format!("Build Scripts: {} │ Dependencies: {}",
651 project.analysis.build_scripts.len(),
652 project.analysis.dependencies.len()
653 ).cyan());
654
655 add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer);
657
658 println!("\n{}", box_drawer.draw());
659 }
660}
661
662fn add_technologies_to_drawer(technologies: &[DetectedTechnology], box_drawer: &mut BoxDrawer) {
664 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
665
666 for tech in technologies {
667 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
668 }
669
670 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
672 let primary_info = format!("{} {}",
673 primary.name.bright_yellow().bold(),
674 format!("({:.0}%)", primary.confidence * 100.0).dimmed()
675 );
676 box_drawer.add_line("Primary Stack:", &primary_info, true);
677 }
678
679 let categories = [
681 (TechnologyCategory::FrontendFramework, "Frameworks"),
682 (TechnologyCategory::BuildTool, "Build Tools"),
683 (TechnologyCategory::Database, "Databases"),
684 (TechnologyCategory::Testing, "Testing"),
685 ];
686
687 for (category, label) in &categories {
688 if let Some(techs) = by_category.get(category) {
689 let tech_names = techs.iter()
690 .map(|t| format!("{} ({:.0}%)", t.name, t.confidence * 100.0))
691 .collect::<Vec<_>>()
692 .join(", ");
693
694 if !tech_names.is_empty() {
695 let label_with_colon = format!("{}:", label);
696 box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true);
697 }
698 }
699 }
700
701 for (cat, techs) in &by_category {
703 if matches!(cat, TechnologyCategory::Library(_)) {
704 let tech_names = techs.iter()
705 .map(|t| format!("{} ({:.0}%)", t.name, t.confidence * 100.0))
706 .collect::<Vec<_>>()
707 .join(", ");
708
709 if !tech_names.is_empty() {
710 box_drawer.add_line("Libraries:", &tech_names.magenta(), true);
711 }
712 }
713 }
714}
715
716fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) {
718 let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
719
720 let mut total_dockerfiles = 0;
721 let mut total_compose_files = 0;
722 let mut total_services = 0;
723 let mut orchestration_patterns = std::collections::HashSet::new();
724
725 for project in &analysis.projects {
726 if let Some(docker) = &project.analysis.docker_analysis {
727 total_dockerfiles += docker.dockerfiles.len();
728 total_compose_files += docker.compose_files.len();
729 total_services += docker.services.len();
730 orchestration_patterns.insert(&docker.orchestration_pattern);
731 }
732 }
733
734 box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
735 box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
736 box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
737
738 let patterns = orchestration_patterns.iter()
739 .map(|p| format!("{:?}", p))
740 .collect::<Vec<_>>()
741 .join(", ");
742 box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true);
743
744 let mut has_services = false;
746 for project in &analysis.projects {
747 if let Some(docker) = &project.analysis.docker_analysis {
748 for service in &docker.services {
749 if !service.ports.is_empty() || !service.depends_on.is_empty() {
750 has_services = true;
751 break;
752 }
753 }
754 }
755 }
756
757 if has_services {
758 box_drawer.add_separator();
759 box_drawer.add_line("Service Connectivity:", "", true);
760
761 for project in &analysis.projects {
762 if let Some(docker) = &project.analysis.docker_analysis {
763 for service in &docker.services {
764 if !service.ports.is_empty() || !service.depends_on.is_empty() {
765 let port_info = service.ports.iter()
766 .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port)))
767 .collect::<Vec<_>>()
768 .join(", ");
769
770 let deps_info = if service.depends_on.is_empty() {
771 String::new()
772 } else {
773 format!(" → {}", service.depends_on.join(", "))
774 };
775
776 let info = format!(" {}: {}{}", service.name, port_info, deps_info);
777 box_drawer.add_value_only(&info.cyan());
778 }
779 }
780 }
781 }
782 }
783
784 println!("\n{}", box_drawer.draw());
785}
786
787fn display_metrics_box(analysis: &MonorepoAnalysis) {
789 let mut box_drawer = BoxDrawer::new("Analysis Metrics");
790
791 let duration_ms = analysis.metadata.analysis_duration_ms;
793 let duration_str = if duration_ms < 1000 {
794 format!("{}ms", duration_ms)
795 } else {
796 format!("{:.1}s", duration_ms as f64 / 1000.0)
797 };
798
799 let metrics_line = format!(
801 "Duration: {} | Files: {} | Score: {}% | Version: {}",
802 duration_str,
803 analysis.metadata.files_analyzed,
804 format!("{:.0}", analysis.metadata.confidence_score * 100.0),
805 analysis.metadata.analyzer_version
806 );
807
808 let colored_metrics = metrics_line.cyan();
810 box_drawer.add_value_only(&colored_metrics.to_string());
811
812 println!("\n{}", box_drawer.draw());
813}
814
815fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) {
817 let percentage = (score * 100.0) as u8;
818 let bar_width = 20;
819 let filled = ((score * bar_width as f32) as usize).min(bar_width);
820
821 let bar = format!("{}{}",
822 "█".repeat(filled).green(),
823 "░".repeat(bar_width - filled).dimmed()
824 );
825
826 let color = if percentage >= 80 {
827 "green"
828 } else if percentage >= 60 {
829 "yellow"
830 } else {
831 "red"
832 };
833
834 let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color));
835 box_drawer.add_line("Confidence:", &confidence_info, true);
836}
837
838fn get_main_technologies(technologies: &[DetectedTechnology]) -> String {
840 let primary = technologies.iter().find(|t| t.is_primary);
841 let frameworks: Vec<_> = technologies.iter()
842 .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework))
843 .take(2)
844 .collect();
845
846 let mut result = Vec::new();
847
848 if let Some(p) = primary {
849 result.push(p.name.clone());
850 }
851
852 for f in frameworks {
853 if Some(&f.name) != primary.map(|p| &p.name) {
854 result.push(f.name.clone());
855 }
856 }
857
858 if result.is_empty() {
859 "-".to_string()
860 } else {
861 result.join(", ")
862 }
863}
864
865pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
867 println!("{}", "=".repeat(80));
869 println!("\n📊 PROJECT ANALYSIS RESULTS");
870 println!("{}", "=".repeat(80));
871
872 if analysis.is_monorepo {
874 println!("\n🏗️ Architecture: Monorepo with {} projects", analysis.projects.len());
875 println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern);
876
877 display_architecture_description(&analysis.technology_summary.architecture_pattern);
878 } else {
879 println!("\n🏗️ Architecture: Single Project");
880 }
881
882 println!("\n🌐 Technology Summary:");
884 if !analysis.technology_summary.languages.is_empty() {
885 println!(" Languages: {}", analysis.technology_summary.languages.join(", "));
886 }
887 if !analysis.technology_summary.frameworks.is_empty() {
888 println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", "));
889 }
890 if !analysis.technology_summary.databases.is_empty() {
891 println!(" Databases: {}", analysis.technology_summary.databases.join(", "));
892 }
893
894 println!("\n📁 Project Details:");
896 println!("{}", "=".repeat(80));
897
898 for (i, project) in analysis.projects.iter().enumerate() {
899 println!("\n{} {}. {} ({})",
900 get_category_emoji(&project.project_category),
901 i + 1,
902 project.name,
903 format_project_category(&project.project_category)
904 );
905
906 if analysis.is_monorepo {
907 println!(" 📂 Path: {}", project.path.display());
908 }
909
910 if !project.analysis.languages.is_empty() {
912 println!(" 🌐 Languages:");
913 for lang in &project.analysis.languages {
914 print!(" • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0);
915 if let Some(version) = &lang.version {
916 print!(" - Version: {}", version);
917 }
918 println!();
919 }
920 }
921
922 if !project.analysis.technologies.is_empty() {
924 println!(" 🚀 Technologies:");
925 display_technologies_detailed_legacy(&project.analysis.technologies);
926 }
927
928 if !project.analysis.entry_points.is_empty() {
930 println!(" 📍 Entry Points ({}):", project.analysis.entry_points.len());
931 for (j, entry) in project.analysis.entry_points.iter().enumerate() {
932 println!(" {}. File: {}", j + 1, entry.file.display());
933 if let Some(func) = &entry.function {
934 println!(" Function: {}", func);
935 }
936 if let Some(cmd) = &entry.command {
937 println!(" Command: {}", cmd);
938 }
939 }
940 }
941
942 if !project.analysis.ports.is_empty() {
944 println!(" 🔌 Exposed Ports ({}):", project.analysis.ports.len());
945 for port in &project.analysis.ports {
946 println!(" • Port {}: {:?}", port.number, port.protocol);
947 if let Some(desc) = &port.description {
948 println!(" {}", desc);
949 }
950 }
951 }
952
953 if !project.analysis.environment_variables.is_empty() {
955 println!(" 🔐 Environment Variables ({}):", project.analysis.environment_variables.len());
956 let required_vars: Vec<_> = project.analysis.environment_variables.iter()
957 .filter(|ev| ev.required)
958 .collect();
959 let optional_vars: Vec<_> = project.analysis.environment_variables.iter()
960 .filter(|ev| !ev.required)
961 .collect();
962
963 if !required_vars.is_empty() {
964 println!(" Required:");
965 for var in required_vars {
966 println!(" • {} {}",
967 var.name,
968 if let Some(desc) = &var.description {
969 format!("({})", desc)
970 } else {
971 String::new()
972 }
973 );
974 }
975 }
976
977 if !optional_vars.is_empty() {
978 println!(" Optional:");
979 for var in optional_vars {
980 println!(" • {} = {:?}",
981 var.name,
982 var.default_value.as_deref().unwrap_or("no default")
983 );
984 }
985 }
986 }
987
988 if !project.analysis.build_scripts.is_empty() {
990 println!(" 🔨 Build Scripts ({}):", project.analysis.build_scripts.len());
991 let default_scripts: Vec<_> = project.analysis.build_scripts.iter()
992 .filter(|bs| bs.is_default)
993 .collect();
994 let other_scripts: Vec<_> = project.analysis.build_scripts.iter()
995 .filter(|bs| !bs.is_default)
996 .collect();
997
998 if !default_scripts.is_empty() {
999 println!(" Default scripts:");
1000 for script in default_scripts {
1001 println!(" • {}: {}", script.name, script.command);
1002 if let Some(desc) = &script.description {
1003 println!(" {}", desc);
1004 }
1005 }
1006 }
1007
1008 if !other_scripts.is_empty() {
1009 println!(" Other scripts:");
1010 for script in other_scripts {
1011 println!(" • {}: {}", script.name, script.command);
1012 if let Some(desc) = &script.description {
1013 println!(" {}", desc);
1014 }
1015 }
1016 }
1017 }
1018
1019 if !project.analysis.dependencies.is_empty() {
1021 println!(" 📦 Dependencies ({}):", project.analysis.dependencies.len());
1022 if project.analysis.dependencies.len() <= 5 {
1023 for (name, version) in &project.analysis.dependencies {
1024 println!(" • {} v{}", name, version);
1025 }
1026 } else {
1027 for (name, version) in project.analysis.dependencies.iter().take(5) {
1029 println!(" • {} v{}", name, version);
1030 }
1031 println!(" ... and {} more", project.analysis.dependencies.len() - 5);
1032 }
1033 }
1034
1035 if let Some(docker_analysis) = &project.analysis.docker_analysis {
1037 display_docker_analysis_detailed_legacy(docker_analysis);
1038 }
1039
1040 println!(" 🎯 Project Type: {:?}", project.analysis.project_type);
1042
1043 if i < analysis.projects.len() - 1 {
1044 println!("{}", "-".repeat(40));
1045 }
1046 }
1047
1048 println!("\n📋 ANALYSIS SUMMARY");
1050 println!("{}", "=".repeat(80));
1051 println!("✅ Project Analysis Complete!");
1052
1053 if analysis.is_monorepo {
1054 println!("\n🏗️ Monorepo Architecture:");
1055 println!(" • Total projects: {}", analysis.projects.len());
1056 println!(" • Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern);
1057
1058 let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count();
1059 let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count();
1060 let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count();
1061 let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count();
1062
1063 if frontend_count > 0 { println!(" • Frontend projects: {}", frontend_count); }
1064 if backend_count > 0 { println!(" • Backend/API projects: {}", backend_count); }
1065 if service_count > 0 { println!(" • Service projects: {}", service_count); }
1066 if lib_count > 0 { println!(" • Library projects: {}", lib_count); }
1067 }
1068
1069 println!("\n📈 Analysis Metadata:");
1070 println!(" • Duration: {}ms", analysis.metadata.analysis_duration_ms);
1071 println!(" • Files analyzed: {}", analysis.metadata.files_analyzed);
1072 println!(" • Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0);
1073 println!(" • Analyzer version: {}", analysis.metadata.analyzer_version);
1074}
1075
1076fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1078 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1080
1081 for tech in technologies {
1082 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1083 }
1084
1085 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1087 println!("\n🛠️ Technology Stack:");
1088 println!(" 🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
1089 println!(" Architecture driver for this project");
1090 }
1091
1092 let categories = [
1094 (TechnologyCategory::MetaFramework, "🏗️ Meta-Frameworks"),
1095 (TechnologyCategory::BackendFramework, "🖥️ Backend Frameworks"),
1096 (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"),
1097 (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"),
1098 (TechnologyCategory::Library(LibraryType::Utility), "📚 Core Libraries"),
1099 (TechnologyCategory::BuildTool, "🔨 Build Tools"),
1100 (TechnologyCategory::PackageManager, "📦 Package Managers"),
1101 (TechnologyCategory::Database, "🗃️ Database & ORM"),
1102 (TechnologyCategory::Runtime, "⚡ Runtimes"),
1103 (TechnologyCategory::Testing, "🧪 Testing"),
1104 ];
1105
1106 for (category, label) in &categories {
1107 if let Some(techs) = by_category.get(category) {
1108 if !techs.is_empty() {
1109 println!("\n {}:", label);
1110 for tech in techs {
1111 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1112 if let Some(version) = &tech.version {
1113 println!(" Version: {}", version);
1114 }
1115 }
1116 }
1117 }
1118 }
1119
1120 for (cat, techs) in &by_category {
1122 match cat {
1123 TechnologyCategory::Library(lib_type) => {
1124 let label = match lib_type {
1125 LibraryType::StateManagement => "🔄 State Management",
1126 LibraryType::DataFetching => "🔃 Data Fetching",
1127 LibraryType::Routing => "🗺️ Routing",
1128 LibraryType::Styling => "🎨 Styling",
1129 LibraryType::HttpClient => "🌐 HTTP Clients",
1130 LibraryType::Authentication => "🔐 Authentication",
1131 LibraryType::Other(_) => "📦 Other Libraries",
1132 _ => continue, };
1134
1135 if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() {
1137 println!("\n {}:", label);
1138 for tech in techs {
1139 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1140 if let Some(version) = &tech.version {
1141 println!(" Version: {}", version);
1142 }
1143 }
1144 }
1145 }
1146 _ => {} }
1148 }
1149}
1150
1151fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1153 println!("\n 🐳 Docker Infrastructure Analysis:");
1154
1155 if !docker_analysis.dockerfiles.is_empty() {
1157 println!(" 📄 Dockerfiles ({}):", docker_analysis.dockerfiles.len());
1158 for dockerfile in &docker_analysis.dockerfiles {
1159 println!(" • {}", dockerfile.path.display());
1160 if let Some(env) = &dockerfile.environment {
1161 println!(" Environment: {}", env);
1162 }
1163 if let Some(base_image) = &dockerfile.base_image {
1164 println!(" Base image: {}", base_image);
1165 }
1166 if !dockerfile.exposed_ports.is_empty() {
1167 println!(" Exposed ports: {}",
1168 dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "));
1169 }
1170 if dockerfile.is_multistage {
1171 println!(" Multi-stage build: {} stages", dockerfile.build_stages.len());
1172 }
1173 println!(" Instructions: {}", dockerfile.instruction_count);
1174 }
1175 }
1176
1177 if !docker_analysis.compose_files.is_empty() {
1179 println!(" 📋 Compose Files ({}):", docker_analysis.compose_files.len());
1180 for compose_file in &docker_analysis.compose_files {
1181 println!(" • {}", compose_file.path.display());
1182 if let Some(env) = &compose_file.environment {
1183 println!(" Environment: {}", env);
1184 }
1185 if let Some(version) = &compose_file.version {
1186 println!(" Version: {}", version);
1187 }
1188 if !compose_file.service_names.is_empty() {
1189 println!(" Services: {}", compose_file.service_names.join(", "));
1190 }
1191 if !compose_file.networks.is_empty() {
1192 println!(" Networks: {}", compose_file.networks.join(", "));
1193 }
1194 if !compose_file.volumes.is_empty() {
1195 println!(" Volumes: {}", compose_file.volumes.join(", "));
1196 }
1197 }
1198 }
1199
1200 println!(" 🏗️ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern);
1202 match docker_analysis.orchestration_pattern {
1203 OrchestrationPattern::SingleContainer => {
1204 println!(" Simple containerized application");
1205 }
1206 OrchestrationPattern::DockerCompose => {
1207 println!(" Multi-service Docker Compose setup");
1208 }
1209 OrchestrationPattern::Microservices => {
1210 println!(" Microservices architecture with service discovery");
1211 }
1212 OrchestrationPattern::EventDriven => {
1213 println!(" Event-driven architecture with message queues");
1214 }
1215 OrchestrationPattern::ServiceMesh => {
1216 println!(" Service mesh for advanced service communication");
1217 }
1218 OrchestrationPattern::Mixed => {
1219 println!(" Mixed/complex orchestration pattern");
1220 }
1221 }
1222}
1223
1224fn display_architecture_description(pattern: &ArchitecturePattern) {
1226 match pattern {
1227 ArchitecturePattern::Monolithic => {
1228 println!(" 📦 This is a single, self-contained application");
1229 }
1230 ArchitecturePattern::Fullstack => {
1231 println!(" 🌐 This is a full-stack application with separate frontend and backend");
1232 }
1233 ArchitecturePattern::Microservices => {
1234 println!(" 🔗 This is a microservices architecture with multiple independent services");
1235 }
1236 ArchitecturePattern::ApiFirst => {
1237 println!(" 🔌 This is an API-first architecture focused on service interfaces");
1238 }
1239 ArchitecturePattern::EventDriven => {
1240 println!(" 📡 This is an event-driven architecture with decoupled components");
1241 }
1242 ArchitecturePattern::Mixed => {
1243 println!(" 🔀 This is a mixed architecture combining multiple patterns");
1244 }
1245 }
1246}
1247
1248pub fn display_summary_view(analysis: &MonorepoAnalysis) {
1250 println!("\n{} {}", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold());
1251 println!("{}", "─".repeat(50).dimmed());
1252
1253 println!("{} Architecture: {}", "│".dimmed(),
1254 if analysis.is_monorepo {
1255 format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1256 } else {
1257 "Single Project".to_string().yellow()
1258 }
1259 );
1260
1261 println!("{} Pattern: {}", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green());
1262 println!("{} Stack: {}", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue());
1263
1264 if !analysis.technology_summary.frameworks.is_empty() {
1265 println!("{} Frameworks: {}", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta());
1266 }
1267
1268 println!("{} Analysis Time: {}ms", "│".dimmed(), analysis.metadata.analysis_duration_ms);
1269 println!("{} Confidence: {:.0}%", "│".dimmed(), analysis.metadata.confidence_score * 100.0);
1270
1271 println!("{}", "─".repeat(50).dimmed());
1272}
1273
1274pub fn display_json_view(analysis: &MonorepoAnalysis) {
1276 match serde_json::to_string_pretty(analysis) {
1277 Ok(json) => println!("{}", json),
1278 Err(e) => eprintln!("Error serializing to JSON: {}", e),
1279 }
1280}
1281
1282fn get_category_emoji(category: &ProjectCategory) -> &'static str {
1284 match category {
1285 ProjectCategory::Frontend => "🌐",
1286 ProjectCategory::Backend => "⚙️",
1287 ProjectCategory::Api => "🔌",
1288 ProjectCategory::Service => "🚀",
1289 ProjectCategory::Library => "📚",
1290 ProjectCategory::Tool => "🔧",
1291 ProjectCategory::Documentation => "📖",
1292 ProjectCategory::Infrastructure => "🏗️",
1293 ProjectCategory::Unknown => "❓",
1294 }
1295}
1296
1297fn format_project_category(category: &ProjectCategory) -> &'static str {
1299 match category {
1300 ProjectCategory::Frontend => "Frontend",
1301 ProjectCategory::Backend => "Backend",
1302 ProjectCategory::Api => "API",
1303 ProjectCategory::Service => "Service",
1304 ProjectCategory::Library => "Library",
1305 ProjectCategory::Tool => "Tool",
1306 ProjectCategory::Documentation => "Documentation",
1307 ProjectCategory::Infrastructure => "Infrastructure",
1308 ProjectCategory::Unknown => "Unknown",
1309 }
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::*;
1315
1316 #[test]
1317 fn test_display_modes() {
1318 assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1320 assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1321 }
1322}