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.saturating_sub(label_width).saturating_sub(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.saturating_sub(label_width).saturating_sub(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.saturating_sub(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.saturating_sub(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.saturating_sub(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.saturating_sub(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.saturating_sub(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| l.name.clone())
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 = primary.name.bright_yellow().bold().to_string();
673 box_drawer.add_line("Primary Stack:", &primary_info, true);
674 }
675
676 let categories = [
678 (TechnologyCategory::FrontendFramework, "Frameworks"),
679 (TechnologyCategory::BuildTool, "Build Tools"),
680 (TechnologyCategory::Database, "Databases"),
681 (TechnologyCategory::Testing, "Testing"),
682 ];
683
684 for (category, label) in &categories {
685 if let Some(techs) = by_category.get(category) {
686 let tech_names = techs.iter()
687 .map(|t| t.name.clone())
688 .collect::<Vec<_>>()
689 .join(", ");
690
691 if !tech_names.is_empty() {
692 let label_with_colon = format!("{}:", label);
693 box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true);
694 }
695 }
696 }
697
698 let mut all_libraries: Vec<&DetectedTechnology> = Vec::new();
700 for (cat, techs) in &by_category {
701 if matches!(cat, TechnologyCategory::Library(_)) {
702 all_libraries.extend(techs.iter().copied());
703 }
704 }
705
706 if !all_libraries.is_empty() {
707 all_libraries.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
709
710 if all_libraries.len() <= 3 {
711 let tech_names = all_libraries.iter()
713 .map(|t| t.name.clone())
714 .collect::<Vec<_>>()
715 .join(", ");
716 box_drawer.add_line("Libraries:", &tech_names.magenta(), true);
717 } else {
718 box_drawer.add_line("Libraries:", "", true);
720
721 let items_per_row = 3;
723 for chunk in all_libraries.chunks(items_per_row) {
724 let row_items = chunk.iter()
725 .map(|t| t.name.clone())
726 .collect::<Vec<_>>()
727 .join(", ");
728
729 let indented_row = format!(" {}", row_items);
731 box_drawer.add_value_only(&indented_row.magenta());
732 }
733 }
734 }
735}
736
737fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) {
739 let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
740
741 let mut total_dockerfiles = 0;
742 let mut total_compose_files = 0;
743 let mut total_services = 0;
744 let mut orchestration_patterns = std::collections::HashSet::new();
745
746 for project in &analysis.projects {
747 if let Some(docker) = &project.analysis.docker_analysis {
748 total_dockerfiles += docker.dockerfiles.len();
749 total_compose_files += docker.compose_files.len();
750 total_services += docker.services.len();
751 orchestration_patterns.insert(&docker.orchestration_pattern);
752 }
753 }
754
755 box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
756 box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
757 box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
758
759 let patterns = orchestration_patterns.iter()
760 .map(|p| format!("{:?}", p))
761 .collect::<Vec<_>>()
762 .join(", ");
763 box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true);
764
765 let mut has_services = false;
767 for project in &analysis.projects {
768 if let Some(docker) = &project.analysis.docker_analysis {
769 for service in &docker.services {
770 if !service.ports.is_empty() || !service.depends_on.is_empty() {
771 has_services = true;
772 break;
773 }
774 }
775 }
776 }
777
778 if has_services {
779 box_drawer.add_separator();
780 box_drawer.add_line("Service Connectivity:", "", true);
781
782 for project in &analysis.projects {
783 if let Some(docker) = &project.analysis.docker_analysis {
784 for service in &docker.services {
785 if !service.ports.is_empty() || !service.depends_on.is_empty() {
786 let port_info = service.ports.iter()
787 .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port)))
788 .collect::<Vec<_>>()
789 .join(", ");
790
791 let deps_info = if service.depends_on.is_empty() {
792 String::new()
793 } else {
794 format!(" → {}", service.depends_on.join(", "))
795 };
796
797 let info = format!(" {}: {}{}", service.name, port_info, deps_info);
798 box_drawer.add_value_only(&info.cyan());
799 }
800 }
801 }
802 }
803 }
804
805 println!("\n{}", box_drawer.draw());
806}
807
808fn display_metrics_box(analysis: &MonorepoAnalysis) {
810 let mut box_drawer = BoxDrawer::new("Analysis Metrics");
811
812 let duration_ms = analysis.metadata.analysis_duration_ms;
814 let duration_str = if duration_ms < 1000 {
815 format!("{}ms", duration_ms)
816 } else {
817 format!("{:.1}s", duration_ms as f64 / 1000.0)
818 };
819
820 let metrics_line = format!(
822 "Duration: {} | Files: {} | Score: {}% | Version: {}",
823 duration_str,
824 analysis.metadata.files_analyzed,
825 format!("{:.0}", analysis.metadata.confidence_score * 100.0),
826 analysis.metadata.analyzer_version
827 );
828
829 let colored_metrics = metrics_line.cyan();
831 box_drawer.add_value_only(&colored_metrics.to_string());
832
833 println!("\n{}", box_drawer.draw());
834}
835
836fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) {
838 let percentage = (score * 100.0) as u8;
839 let bar_width = 20;
840 let filled = ((score * bar_width as f32) as usize).min(bar_width);
841
842 let bar = format!("{}{}",
843 "█".repeat(filled).green(),
844 "░".repeat(bar_width - filled).dimmed()
845 );
846
847 let color = if percentage >= 80 {
848 "green"
849 } else if percentage >= 60 {
850 "yellow"
851 } else {
852 "red"
853 };
854
855 let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color));
856 box_drawer.add_line("Confidence:", &confidence_info, true);
857}
858
859fn get_main_technologies(technologies: &[DetectedTechnology]) -> String {
861 let primary = technologies.iter().find(|t| t.is_primary);
862 let frameworks: Vec<_> = technologies.iter()
863 .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework))
864 .take(2)
865 .collect();
866
867 let mut result = Vec::new();
868
869 if let Some(p) = primary {
870 result.push(p.name.clone());
871 }
872
873 for f in frameworks {
874 if Some(&f.name) != primary.map(|p| &p.name) {
875 result.push(f.name.clone());
876 }
877 }
878
879 if result.is_empty() {
880 "-".to_string()
881 } else {
882 result.join(", ")
883 }
884}
885
886pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
888 println!("{}", "=".repeat(80));
890 println!("\n📊 PROJECT ANALYSIS RESULTS");
891 println!("{}", "=".repeat(80));
892
893 if analysis.is_monorepo {
895 println!("\n🏗️ Architecture: Monorepo with {} projects", analysis.projects.len());
896 println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern);
897
898 display_architecture_description(&analysis.technology_summary.architecture_pattern);
899 } else {
900 println!("\n🏗️ Architecture: Single Project");
901 }
902
903 println!("\n🌐 Technology Summary:");
905 if !analysis.technology_summary.languages.is_empty() {
906 println!(" Languages: {}", analysis.technology_summary.languages.join(", "));
907 }
908 if !analysis.technology_summary.frameworks.is_empty() {
909 println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", "));
910 }
911 if !analysis.technology_summary.databases.is_empty() {
912 println!(" Databases: {}", analysis.technology_summary.databases.join(", "));
913 }
914
915 println!("\n📁 Project Details:");
917 println!("{}", "=".repeat(80));
918
919 for (i, project) in analysis.projects.iter().enumerate() {
920 println!("\n{} {}. {} ({})",
921 get_category_emoji(&project.project_category),
922 i + 1,
923 project.name,
924 format_project_category(&project.project_category)
925 );
926
927 if analysis.is_monorepo {
928 println!(" 📂 Path: {}", project.path.display());
929 }
930
931 if !project.analysis.languages.is_empty() {
933 println!(" 🌐 Languages:");
934 for lang in &project.analysis.languages {
935 print!(" • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0);
936 if let Some(version) = &lang.version {
937 print!(" - Version: {}", version);
938 }
939 println!();
940 }
941 }
942
943 if !project.analysis.technologies.is_empty() {
945 println!(" 🚀 Technologies:");
946 display_technologies_detailed_legacy(&project.analysis.technologies);
947 }
948
949 if !project.analysis.entry_points.is_empty() {
951 println!(" 📍 Entry Points ({}):", project.analysis.entry_points.len());
952 for (j, entry) in project.analysis.entry_points.iter().enumerate() {
953 println!(" {}. File: {}", j + 1, entry.file.display());
954 if let Some(func) = &entry.function {
955 println!(" Function: {}", func);
956 }
957 if let Some(cmd) = &entry.command {
958 println!(" Command: {}", cmd);
959 }
960 }
961 }
962
963 if !project.analysis.ports.is_empty() {
965 println!(" 🔌 Exposed Ports ({}):", project.analysis.ports.len());
966 for port in &project.analysis.ports {
967 println!(" • Port {}: {:?}", port.number, port.protocol);
968 if let Some(desc) = &port.description {
969 println!(" {}", desc);
970 }
971 }
972 }
973
974 if !project.analysis.environment_variables.is_empty() {
976 println!(" 🔐 Environment Variables ({}):", project.analysis.environment_variables.len());
977 let required_vars: Vec<_> = project.analysis.environment_variables.iter()
978 .filter(|ev| ev.required)
979 .collect();
980 let optional_vars: Vec<_> = project.analysis.environment_variables.iter()
981 .filter(|ev| !ev.required)
982 .collect();
983
984 if !required_vars.is_empty() {
985 println!(" Required:");
986 for var in required_vars {
987 println!(" • {} {}",
988 var.name,
989 if let Some(desc) = &var.description {
990 format!("({})", desc)
991 } else {
992 String::new()
993 }
994 );
995 }
996 }
997
998 if !optional_vars.is_empty() {
999 println!(" Optional:");
1000 for var in optional_vars {
1001 println!(" • {} = {:?}",
1002 var.name,
1003 var.default_value.as_deref().unwrap_or("no default")
1004 );
1005 }
1006 }
1007 }
1008
1009 if !project.analysis.build_scripts.is_empty() {
1011 println!(" 🔨 Build Scripts ({}):", project.analysis.build_scripts.len());
1012 let default_scripts: Vec<_> = project.analysis.build_scripts.iter()
1013 .filter(|bs| bs.is_default)
1014 .collect();
1015 let other_scripts: Vec<_> = project.analysis.build_scripts.iter()
1016 .filter(|bs| !bs.is_default)
1017 .collect();
1018
1019 if !default_scripts.is_empty() {
1020 println!(" Default scripts:");
1021 for script in default_scripts {
1022 println!(" • {}: {}", script.name, script.command);
1023 if let Some(desc) = &script.description {
1024 println!(" {}", desc);
1025 }
1026 }
1027 }
1028
1029 if !other_scripts.is_empty() {
1030 println!(" Other scripts:");
1031 for script in other_scripts {
1032 println!(" • {}: {}", script.name, script.command);
1033 if let Some(desc) = &script.description {
1034 println!(" {}", desc);
1035 }
1036 }
1037 }
1038 }
1039
1040 if !project.analysis.dependencies.is_empty() {
1042 println!(" 📦 Dependencies ({}):", project.analysis.dependencies.len());
1043 if project.analysis.dependencies.len() <= 5 {
1044 for (name, version) in &project.analysis.dependencies {
1045 println!(" • {} v{}", name, version);
1046 }
1047 } else {
1048 for (name, version) in project.analysis.dependencies.iter().take(5) {
1050 println!(" • {} v{}", name, version);
1051 }
1052 println!(" ... and {} more", project.analysis.dependencies.len() - 5);
1053 }
1054 }
1055
1056 if let Some(docker_analysis) = &project.analysis.docker_analysis {
1058 display_docker_analysis_detailed_legacy(docker_analysis);
1059 }
1060
1061 println!(" 🎯 Project Type: {:?}", project.analysis.project_type);
1063
1064 if i < analysis.projects.len() - 1 {
1065 println!("{}", "-".repeat(40));
1066 }
1067 }
1068
1069 println!("\n📋 ANALYSIS SUMMARY");
1071 println!("{}", "=".repeat(80));
1072 println!("✅ Project Analysis Complete!");
1073
1074 if analysis.is_monorepo {
1075 println!("\n🏗️ Monorepo Architecture:");
1076 println!(" • Total projects: {}", analysis.projects.len());
1077 println!(" • Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern);
1078
1079 let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count();
1080 let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count();
1081 let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count();
1082 let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count();
1083
1084 if frontend_count > 0 { println!(" • Frontend projects: {}", frontend_count); }
1085 if backend_count > 0 { println!(" • Backend/API projects: {}", backend_count); }
1086 if service_count > 0 { println!(" • Service projects: {}", service_count); }
1087 if lib_count > 0 { println!(" • Library projects: {}", lib_count); }
1088 }
1089
1090 println!("\n📈 Analysis Metadata:");
1091 println!(" • Duration: {}ms", analysis.metadata.analysis_duration_ms);
1092 println!(" • Files analyzed: {}", analysis.metadata.files_analyzed);
1093 println!(" • Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0);
1094 println!(" • Analyzer version: {}", analysis.metadata.analyzer_version);
1095}
1096
1097fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1099 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1101
1102 for tech in technologies {
1103 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1104 }
1105
1106 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1108 println!("\n🛠️ Technology Stack:");
1109 println!(" 🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
1110 println!(" Architecture driver for this project");
1111 }
1112
1113 let categories = [
1115 (TechnologyCategory::MetaFramework, "🏗️ Meta-Frameworks"),
1116 (TechnologyCategory::BackendFramework, "🖥️ Backend Frameworks"),
1117 (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"),
1118 (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"),
1119 (TechnologyCategory::Library(LibraryType::Utility), "📚 Core Libraries"),
1120 (TechnologyCategory::BuildTool, "🔨 Build Tools"),
1121 (TechnologyCategory::PackageManager, "📦 Package Managers"),
1122 (TechnologyCategory::Database, "🗃️ Database & ORM"),
1123 (TechnologyCategory::Runtime, "⚡ Runtimes"),
1124 (TechnologyCategory::Testing, "🧪 Testing"),
1125 ];
1126
1127 for (category, label) in &categories {
1128 if let Some(techs) = by_category.get(category) {
1129 if !techs.is_empty() {
1130 println!("\n {}:", label);
1131 for tech in techs {
1132 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1133 if let Some(version) = &tech.version {
1134 println!(" Version: {}", version);
1135 }
1136 }
1137 }
1138 }
1139 }
1140
1141 for (cat, techs) in &by_category {
1143 match cat {
1144 TechnologyCategory::Library(lib_type) => {
1145 let label = match lib_type {
1146 LibraryType::StateManagement => "🔄 State Management",
1147 LibraryType::DataFetching => "🔃 Data Fetching",
1148 LibraryType::Routing => "🗺️ Routing",
1149 LibraryType::Styling => "🎨 Styling",
1150 LibraryType::HttpClient => "🌐 HTTP Clients",
1151 LibraryType::Authentication => "🔐 Authentication",
1152 LibraryType::Other(_) => "📦 Other Libraries",
1153 _ => continue, };
1155
1156 if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() {
1158 println!("\n {}:", label);
1159 for tech in techs {
1160 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1161 if let Some(version) = &tech.version {
1162 println!(" Version: {}", version);
1163 }
1164 }
1165 }
1166 }
1167 _ => {} }
1169 }
1170}
1171
1172fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1174 println!("\n 🐳 Docker Infrastructure Analysis:");
1175
1176 if !docker_analysis.dockerfiles.is_empty() {
1178 println!(" 📄 Dockerfiles ({}):", docker_analysis.dockerfiles.len());
1179 for dockerfile in &docker_analysis.dockerfiles {
1180 println!(" • {}", dockerfile.path.display());
1181 if let Some(env) = &dockerfile.environment {
1182 println!(" Environment: {}", env);
1183 }
1184 if let Some(base_image) = &dockerfile.base_image {
1185 println!(" Base image: {}", base_image);
1186 }
1187 if !dockerfile.exposed_ports.is_empty() {
1188 println!(" Exposed ports: {}",
1189 dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "));
1190 }
1191 if dockerfile.is_multistage {
1192 println!(" Multi-stage build: {} stages", dockerfile.build_stages.len());
1193 }
1194 println!(" Instructions: {}", dockerfile.instruction_count);
1195 }
1196 }
1197
1198 if !docker_analysis.compose_files.is_empty() {
1200 println!(" 📋 Compose Files ({}):", docker_analysis.compose_files.len());
1201 for compose_file in &docker_analysis.compose_files {
1202 println!(" • {}", compose_file.path.display());
1203 if let Some(env) = &compose_file.environment {
1204 println!(" Environment: {}", env);
1205 }
1206 if let Some(version) = &compose_file.version {
1207 println!(" Version: {}", version);
1208 }
1209 if !compose_file.service_names.is_empty() {
1210 println!(" Services: {}", compose_file.service_names.join(", "));
1211 }
1212 if !compose_file.networks.is_empty() {
1213 println!(" Networks: {}", compose_file.networks.join(", "));
1214 }
1215 if !compose_file.volumes.is_empty() {
1216 println!(" Volumes: {}", compose_file.volumes.join(", "));
1217 }
1218 }
1219 }
1220
1221 println!(" 🏗️ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern);
1223 match docker_analysis.orchestration_pattern {
1224 OrchestrationPattern::SingleContainer => {
1225 println!(" Simple containerized application");
1226 }
1227 OrchestrationPattern::DockerCompose => {
1228 println!(" Multi-service Docker Compose setup");
1229 }
1230 OrchestrationPattern::Microservices => {
1231 println!(" Microservices architecture with service discovery");
1232 }
1233 OrchestrationPattern::EventDriven => {
1234 println!(" Event-driven architecture with message queues");
1235 }
1236 OrchestrationPattern::ServiceMesh => {
1237 println!(" Service mesh for advanced service communication");
1238 }
1239 OrchestrationPattern::Mixed => {
1240 println!(" Mixed/complex orchestration pattern");
1241 }
1242 }
1243}
1244
1245fn display_architecture_description(pattern: &ArchitecturePattern) {
1247 match pattern {
1248 ArchitecturePattern::Monolithic => {
1249 println!(" 📦 This is a single, self-contained application");
1250 }
1251 ArchitecturePattern::Fullstack => {
1252 println!(" 🌐 This is a full-stack application with separate frontend and backend");
1253 }
1254 ArchitecturePattern::Microservices => {
1255 println!(" 🔗 This is a microservices architecture with multiple independent services");
1256 }
1257 ArchitecturePattern::ApiFirst => {
1258 println!(" 🔌 This is an API-first architecture focused on service interfaces");
1259 }
1260 ArchitecturePattern::EventDriven => {
1261 println!(" 📡 This is an event-driven architecture with decoupled components");
1262 }
1263 ArchitecturePattern::Mixed => {
1264 println!(" 🔀 This is a mixed architecture combining multiple patterns");
1265 }
1266 }
1267}
1268
1269pub fn display_summary_view(analysis: &MonorepoAnalysis) {
1271 println!("\n{} {}", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold());
1272 println!("{}", "─".repeat(50).dimmed());
1273
1274 println!("{} Architecture: {}", "│".dimmed(),
1275 if analysis.is_monorepo {
1276 format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1277 } else {
1278 "Single Project".to_string().yellow()
1279 }
1280 );
1281
1282 println!("{} Pattern: {}", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green());
1283 println!("{} Stack: {}", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue());
1284
1285 if !analysis.technology_summary.frameworks.is_empty() {
1286 println!("{} Frameworks: {}", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta());
1287 }
1288
1289 println!("{} Analysis Time: {}ms", "│".dimmed(), analysis.metadata.analysis_duration_ms);
1290 println!("{} Confidence: {:.0}%", "│".dimmed(), analysis.metadata.confidence_score * 100.0);
1291
1292 println!("{}", "─".repeat(50).dimmed());
1293}
1294
1295pub fn display_json_view(analysis: &MonorepoAnalysis) {
1297 match serde_json::to_string_pretty(analysis) {
1298 Ok(json) => println!("{}", json),
1299 Err(e) => eprintln!("Error serializing to JSON: {}", e),
1300 }
1301}
1302
1303fn get_category_emoji(category: &ProjectCategory) -> &'static str {
1305 match category {
1306 ProjectCategory::Frontend => "🌐",
1307 ProjectCategory::Backend => "⚙️",
1308 ProjectCategory::Api => "🔌",
1309 ProjectCategory::Service => "🚀",
1310 ProjectCategory::Library => "📚",
1311 ProjectCategory::Tool => "🔧",
1312 ProjectCategory::Documentation => "📖",
1313 ProjectCategory::Infrastructure => "🏗️",
1314 ProjectCategory::Unknown => "❓",
1315 }
1316}
1317
1318fn format_project_category(category: &ProjectCategory) -> &'static str {
1320 match category {
1321 ProjectCategory::Frontend => "Frontend",
1322 ProjectCategory::Backend => "Backend",
1323 ProjectCategory::Api => "API",
1324 ProjectCategory::Service => "Service",
1325 ProjectCategory::Library => "Library",
1326 ProjectCategory::Tool => "Tool",
1327 ProjectCategory::Documentation => "Documentation",
1328 ProjectCategory::Infrastructure => "Infrastructure",
1329 ProjectCategory::Unknown => "Unknown",
1330 }
1331}
1332
1333#[cfg(test)]
1334mod tests {
1335 use super::*;
1336
1337 #[test]
1338 fn test_display_modes() {
1339 assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1341 assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1342 }
1343}