syncable_cli/analyzer/
display.rs

1//! # Display Module
2//! 
3//! Provides improved CLI output formatting with matrix/dashboard views for better readability
4//! and easier parsing by both humans and LLMs.
5
6use crate::analyzer::{
7    MonorepoAnalysis, ProjectCategory, ArchitecturePattern,
8    DetectedTechnology, TechnologyCategory, LibraryType,
9    DockerAnalysis, OrchestrationPattern,
10};
11use colored::*;
12use prettytable::{Table, Cell, Row, format};
13
14/// Content line for measuring and drawing
15#[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
50/// Box drawer that pre-calculates optimal dimensions
51struct 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, // Increased to accommodate longer content
65        }
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    /// Calculate optimal box width based on content
85    fn calculate_optimal_width(&self) -> usize {
86        let title_width = visual_width(&self.title) + 6; // "┌─ " + title + " " + extra padding
87        let mut max_content_width = 0;
88        
89        // Calculate the actual rendered width for each line
90        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        // Use exact content width with minimal buffer for safety
100        let content_width_with_buffer = max_content_width + 2; // Minimal buffer for safety
101        
102        // Box needs padding: "│ " + content + " │" = content + 4
103        let needed_width = content_width_with_buffer + 4;
104        
105        // Use the maximum of title width and content width, with a reasonable minimum
106        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    /// Calculate the actual rendered width of a line as it will appear
112    fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
113        // Calculate actual display widths without formatting
114        let label_display_width = visual_width(&line.label);
115        let mut value_display_width = visual_width(&line.value);
116        
117        // Be more conservative for values that could grow significantly
118        if !line.value.is_empty() {
119            // Add extra space for values that are likely numeric and could grow
120            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); // Reserve space for larger numbers
124            }
125        }
126        
127        if !line.label.is_empty() && !line.value.is_empty() {
128            // Both label and value - they need space between them
129            // For colored labels, ensure minimum spacing but use actual width
130            let actual_label_width = if line.label_colored {
131                label_display_width.max(20) // At least 20, but can be longer
132            } else {
133                label_display_width
134            };
135            actual_label_width + 1 + value_display_width
136        } else if !line.value.is_empty() {
137            // Value only
138            value_display_width
139        } else if !line.label.is_empty() {
140            // Label only
141            label_display_width
142        } else {
143            // Empty line
144            0
145        }
146    }
147    
148    /// Draw the complete box
149    fn draw(&self) -> String {
150        let box_width = self.calculate_optimal_width();
151        let content_width = box_width - 4; // Available space for content
152        
153        let mut output = Vec::new();
154        
155        // Top border
156        output.push(self.draw_top(box_width));
157        
158        // Content lines
159        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        // Bottom border
170        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        // "┌─ " + title + " " + remaining dashes + "┐"
180        let prefix_len = 3; // "┌─ "
181        let suffix_len = 1; // "┐"
182        let title_space = 1; // space after title
183        
184        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        // Format the label with color if needed, but calculate width dynamically
206        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        // Calculate actual display widths
214        let label_display_width = visual_width(&line.label); // Use original label for width calculation
215        let value_display_width = visual_width(&formatted_value);
216        
217        // For colored labels, ensure minimum spacing but allow longer labels
218        let effective_label_width = if line.label_colored && !line.label.is_empty() {
219            label_display_width.max(20) // At least 20, but can be longer if needed
220        } else {
221            label_display_width
222        };
223        
224        // Determine content layout
225        let content = if !line.label.is_empty() && !line.value.is_empty() {
226            // Both label and value - right-align the value
227            let available_space = content_width;
228            let min_space_between = 1; // Minimum space between label and value
229            
230            // Calculate how much space we need and have
231            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                // Everything fits - right-align the value
237                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                // Need to truncate value
241                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            // Value only - left-align it (for descriptions, etc.)
249            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                // Truncate and ensure it fills exactly content_width
255                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            // Label only - left-align it
262            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                // Truncate and ensure it fills exactly content_width
268                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            // Empty line
275            " ".repeat(content_width)
276        };
277        
278        // Verify content is exactly the right width and fix if needed
279        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            // For table content (contains │ or ─), don't add padding as it's already properly formatted
284            if content.contains("│") || content.contains("─┼─") {
285                content
286            } else {
287                // Add padding to reach exact width for non-table content
288                let padding_needed = content_width.saturating_sub(actual_content_width);
289                format!("{}{}", content, " ".repeat(padding_needed))
290            }
291        } else {
292            // Truncate to exact width if somehow too long
293            truncate_to_width(&content, content_width)
294        };
295        
296        format!("│ {} │", final_content)
297    }
298}
299
300/// Calculate visual width of a string, handling ANSI color codes
301fn 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            // Skip ANSI escape sequence
308            if chars.peek() == Some(&'[') {
309                chars.next(); // consume '['
310                while let Some(c) = chars.next() {
311                    if c.is_ascii_alphabetic() {
312                        break; // End of escape sequence
313                    }
314                }
315            }
316        } else {
317            // Simple width calculation for common cases
318            // Most characters are width 1, some are width 0 or 2
319            width += char_width(ch);
320        }
321    }
322    
323    width
324}
325
326/// Simple character width calculation without external dependencies
327fn char_width(ch: char) -> usize {
328    match ch {
329        // Control characters have width 0
330        '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
331        // Combining marks have width 0
332        '\u{0300}'..='\u{036F}' => 0,
333        // Emoji and symbols (width 2)
334        '\u{2600}'..='\u{26FF}' |    // Miscellaneous Symbols
335        '\u{2700}'..='\u{27BF}' |    // Dingbats
336        '\u{1F000}'..='\u{1F02F}' |  // Mahjong Tiles
337        '\u{1F030}'..='\u{1F09F}' |  // Domino Tiles
338        '\u{1F0A0}'..='\u{1F0FF}' |  // Playing Cards
339        '\u{1F100}'..='\u{1F1FF}' |  // Enclosed Alphanumeric Supplement
340        '\u{1F200}'..='\u{1F2FF}' |  // Enclosed Ideographic Supplement
341        '\u{1F300}'..='\u{1F5FF}' |  // Miscellaneous Symbols and Pictographs
342        '\u{1F600}'..='\u{1F64F}' |  // Emoticons
343        '\u{1F650}'..='\u{1F67F}' |  // Ornamental Dingbats
344        '\u{1F680}'..='\u{1F6FF}' |  // Transport and Map Symbols
345        '\u{1F700}'..='\u{1F77F}' |  // Alchemical Symbols
346        '\u{1F780}'..='\u{1F7FF}' |  // Geometric Shapes Extended
347        '\u{1F800}'..='\u{1F8FF}' |  // Supplemental Arrows-C
348        '\u{1F900}'..='\u{1F9FF}' |  // Supplemental Symbols and Pictographs
349        // Full-width characters (common CJK ranges)
350        '\u{1100}'..='\u{115F}' |  // Hangul Jamo
351        '\u{2E80}'..='\u{2EFF}' |  // CJK Radicals
352        '\u{2F00}'..='\u{2FDF}' |  // Kangxi Radicals
353        '\u{2FF0}'..='\u{2FFF}' |  // Ideographic Description
354        '\u{3000}'..='\u{303E}' |  // CJK Symbols and Punctuation
355        '\u{3041}'..='\u{3096}' |  // Hiragana
356        '\u{30A1}'..='\u{30FA}' |  // Katakana
357        '\u{3105}'..='\u{312D}' |  // Bopomofo
358        '\u{3131}'..='\u{318E}' |  // Hangul Compatibility Jamo
359        '\u{3190}'..='\u{31BA}' |  // Kanbun
360        '\u{31C0}'..='\u{31E3}' |  // CJK Strokes
361        '\u{31F0}'..='\u{31FF}' |  // Katakana Phonetic Extensions
362        '\u{3200}'..='\u{32FF}' |  // Enclosed CJK Letters and Months
363        '\u{3300}'..='\u{33FF}' |  // CJK Compatibility
364        '\u{3400}'..='\u{4DBF}' |  // CJK Extension A
365        '\u{4E00}'..='\u{9FFF}' |  // CJK Unified Ideographs
366        '\u{A000}'..='\u{A48C}' |  // Yi Syllables
367        '\u{A490}'..='\u{A4C6}' |  // Yi Radicals
368        '\u{AC00}'..='\u{D7AF}' |  // Hangul Syllables
369        '\u{F900}'..='\u{FAFF}' |  // CJK Compatibility Ideographs
370        '\u{FE10}'..='\u{FE19}' |  // Vertical Forms
371        '\u{FE30}'..='\u{FE6F}' |  // CJK Compatibility Forms
372        '\u{FF00}'..='\u{FF60}' |  // Fullwidth Forms
373        '\u{FFE0}'..='\u{FFE6}' => 2,
374        // Most other printable characters have width 1
375        _ => 1,
376    }
377}
378
379/// Truncate string to specified visual width, preserving color codes when possible
380fn 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            // Preserve ANSI escape sequence
392            result.push(ch);
393            if chars.peek() == Some(&'[') {
394                result.push(chars.next().unwrap()); // consume '['
395                while let Some(c) = chars.next() {
396                    result.push(c);
397                    if c.is_ascii_alphabetic() {
398                        break; // End of escape sequence
399                    }
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/// Display mode for analysis output
419#[derive(Debug, Clone, Copy, PartialEq)]
420pub enum DisplayMode {
421    /// Compact matrix view (default)
422    Matrix,
423    /// Detailed vertical view (legacy)
424    Detailed,
425    /// Summary only
426    Summary,
427    /// JSON output
428    Json,
429}
430
431/// Main display function that routes to appropriate formatter
432pub 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
441/// Display analysis in a compact matrix/dashboard format
442pub fn display_matrix_view(analysis: &MonorepoAnalysis) {
443    // Header
444    println!("\n{}", "═".repeat(100).bright_blue());
445    println!("{}", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold());
446    println!("{}", "═".repeat(100).bright_blue());
447    
448    // Architecture Overview Box
449    display_architecture_box(analysis);
450    
451    // Technology Stack Box
452    display_technology_stack_box(analysis);
453    
454    // Projects Matrix
455    if analysis.projects.len() > 1 {
456        display_projects_matrix(analysis);
457    } else {
458        display_single_project_matrix(analysis);
459    }
460    
461    // Docker Infrastructure Overview
462    if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
463        display_docker_overview_matrix(analysis);
464    }
465    
466    // Analysis Metrics Box
467    display_metrics_box(analysis);
468    
469    // Footer
470    println!("\n{}", "═".repeat(100).bright_blue());
471}
472
473/// Display architecture overview in a box
474fn 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    // Pattern description
487    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
500/// Display technology stack overview
501fn display_technology_stack_box(analysis: &MonorepoAnalysis) {
502    let mut box_drawer = BoxDrawer::new("Technology Stack");
503    
504    let mut has_content = false;
505    
506    // Languages
507    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    // Frameworks
514    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    // Databases
521    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
534/// Display projects in a matrix table format
535fn display_projects_matrix(analysis: &MonorepoAnalysis) {
536    let mut box_drawer = BoxDrawer::new("Projects Matrix");
537    
538    // Collect all data first to calculate optimal column widths
539    let mut project_data = Vec::new();
540    for project in &analysis.projects {
541        let name = project.name.clone(); // Remove emoji to avoid width calculation issues
542        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    // Calculate column widths based on content
572    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    // Create header row
587    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    // Add separator
594    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    // Add data rows
601    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
618/// Display single project in matrix format
619fn 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        // Basic info
624        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        // Languages 
628        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        // Technologies by category
637        add_technologies_to_drawer(&project.analysis.technologies, &mut box_drawer);
638        
639        // Key metrics
640        box_drawer.add_separator();
641        box_drawer.add_line("Key Metrics:", "", true);
642        
643        // Display metrics on two lines to fit properly
644        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        // Confidence score with progress bar
656        add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer);
657        
658        println!("\n{}", box_drawer.draw());
659    }
660}
661
662/// Add technologies organized by category to the box drawer
663fn 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    // Display primary technology first
671    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    // Display other categories
677    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    // Handle Library category separately since it's parameterized - use vertical layout for many items
699    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        // Sort libraries by confidence for better display
708        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            // For few libraries, keep horizontal layout
712            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            // For many libraries, use vertical layout with multiple rows
719            box_drawer.add_line("Libraries:", "", true);
720            
721            // Group libraries into rows of 3-4 items each
722            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                // Add indented row
730                let indented_row = format!("  {}", row_items);
731                box_drawer.add_value_only(&indented_row.magenta());
732            }
733        }
734    }
735}
736
737/// Display Docker infrastructure overview in matrix format
738fn 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    // Service connectivity summary
766    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
808/// Display analysis metrics
809fn display_metrics_box(analysis: &MonorepoAnalysis) {
810    let mut box_drawer = BoxDrawer::new("Analysis Metrics");
811    
812    // Performance metrics
813    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    // Create metrics line without emojis first to avoid width calculation issues
821    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    // Apply single color to the entire line for consistency
830    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
836/// Add confidence score as a progress bar to the box drawer
837fn 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
859/// Get main technologies for display
860fn 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
886/// Display in detailed vertical format (legacy)
887pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
888    // Use the legacy detailed display format
889    println!("{}", "=".repeat(80));
890    println!("\n📊 PROJECT ANALYSIS RESULTS");
891    println!("{}", "=".repeat(80));
892    
893    // Overall project information
894    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    // Technology Summary
904    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    // Individual project details
916    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        // Languages for this project
932        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        // Technologies for this project
944        if !project.analysis.technologies.is_empty() {
945            println!("   🚀 Technologies:");
946            display_technologies_detailed_legacy(&project.analysis.technologies);
947        }
948        
949        // Entry Points
950        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        // Ports
964        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        // Environment Variables
975        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        // Build Scripts
1010        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        // Dependencies (sample)
1041        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                // Show first 5
1049                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        // Docker Infrastructure Analysis
1057        if let Some(docker_analysis) = &project.analysis.docker_analysis {
1058            display_docker_analysis_detailed_legacy(docker_analysis);
1059        }
1060        
1061        // Project type
1062        println!("   🎯 Project Type: {:?}", project.analysis.project_type);
1063        
1064        if i < analysis.projects.len() - 1 {
1065            println!("{}", "-".repeat(40));
1066        }
1067    }
1068    
1069    // Summary
1070    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
1097/// Helper function for legacy detailed technology display
1098fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1099    // Group technologies by category
1100    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    // Find and display primary technology
1107    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    // Display categories in order
1114    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    // Handle other Library types separately
1142    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, // Skip already handled UI and Utility
1154                };
1155                
1156                // Only print if not already handled above
1157                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            _ => {} // Other categories already handled in the array
1168        }
1169    }
1170}
1171
1172/// Helper function for legacy Docker analysis display
1173fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1174    println!("\n   🐳 Docker Infrastructure Analysis:");
1175    
1176    // Dockerfiles
1177    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    // Compose files
1199    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    // Rest of the detailed Docker display...
1222    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
1245/// Display architecture description
1246fn 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
1269/// Display summary view only
1270pub 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
1295/// Display JSON output
1296pub 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
1303/// Get emoji for project category
1304fn 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
1318/// Format project category name
1319fn 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        // Test that display modes are properly defined
1340        assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1341        assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1342    }
1343}