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: 120, // Reduced from 150 for better terminal compatibility
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        // Add reasonable buffer for content
100        let content_width_with_buffer = max_content_width + 4; // More 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
106        let optimal_width = title_width.max(needed_width).max(self.min_width);
107        optimal_width.min(self.max_width)
108    }
109    
110    /// Calculate the actual rendered width of a line as it will appear
111    fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
112        let label_width = visual_width(&line.label);
113        let value_width = visual_width(&line.value);
114        
115        if !line.label.is_empty() && !line.value.is_empty() {
116            // Label + value: need space between them
117            // For colored labels, ensure minimum spacing
118            let min_label_space = if line.label_colored { 25 } else { label_width };
119            min_label_space + 2 + value_width // 2 spaces minimum between label and value
120        } else if !line.value.is_empty() {
121            // Value only
122            value_width
123        } else if !line.label.is_empty() {
124            // Label only
125            label_width
126        } else {
127            // Empty line
128            0
129        }
130    }
131    
132    /// Draw the complete box
133    fn draw(&self) -> String {
134        let box_width = self.calculate_optimal_width();
135        let content_width = box_width - 4; // Available space for content
136        
137        let mut output = Vec::new();
138        
139        // Top border
140        output.push(self.draw_top(box_width));
141        
142        // Content lines
143        for line in &self.lines {
144            if line.label == "SEPARATOR" {
145                output.push(self.draw_separator(box_width));
146            } else if line.label.is_empty() && line.value.is_empty() {
147                output.push(self.draw_empty_line(box_width));
148            } else {
149                output.push(self.draw_content_line(line, content_width));
150            }
151        }
152        
153        // Bottom border
154        output.push(self.draw_bottom(box_width));
155        
156        output.join("\n")
157    }
158    
159    fn draw_top(&self, width: usize) -> String {
160        let title_colored = self.title.bright_cyan();
161        let title_len = visual_width(&self.title);
162        
163        // "┌─ " + title + " " + remaining dashes + "┐"
164        let prefix_len = 3; // "┌─ "
165        let suffix_len = 1; // "┐"
166        let title_space = 1; // space after title
167        
168        let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
169        
170        format!("┌─ {} {}┐", 
171            title_colored,
172            "─".repeat(remaining_space)
173        )
174    }
175    
176    fn draw_bottom(&self, width: usize) -> String {
177        format!("└{}┘", "─".repeat(width - 2))
178    }
179    
180    fn draw_separator(&self, width: usize) -> String {
181        format!("│ {} │", "─".repeat(width - 4).dimmed())
182    }
183    
184    fn draw_empty_line(&self, width: usize) -> String {
185        format!("│ {} │", " ".repeat(width - 4))
186    }
187    
188    fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String {
189        // Format the label with color if needed
190        let formatted_label = if line.label_colored && !line.label.is_empty() {
191            line.label.bright_white().to_string()
192        } else {
193            line.label.clone()
194        };
195        
196        // Calculate actual display widths (use original label for width)
197        let label_display_width = visual_width(&line.label);
198        let value_display_width = visual_width(&line.value);
199        
200        // Build the content
201        let content = if !line.label.is_empty() && !line.value.is_empty() {
202            // Both label and value - ensure proper spacing
203            let min_label_space = if line.label_colored { 25 } else { label_display_width };
204            let label_padding = min_label_space.saturating_sub(label_display_width);
205            let remaining_space = content_width.saturating_sub(min_label_space + 2); // 2 for spacing
206            
207            if value_display_width <= remaining_space {
208                // Value fits - right align it
209                let value_padding = remaining_space.saturating_sub(value_display_width);
210                format!("{}{:<width$}  {}{}", 
211                    formatted_label, 
212                    "",
213                    " ".repeat(value_padding),
214                    line.value,
215                    width = label_padding
216                )
217            } else {
218                // Value too long - truncate it
219                let truncated_value = truncate_to_width(&line.value, remaining_space.saturating_sub(3));
220                format!("{}{:<width$}  {}", 
221                    formatted_label, 
222                    "",
223                    truncated_value,
224                    width = label_padding
225                )
226            }
227        } else if !line.value.is_empty() {
228            // Value only - left align
229            if value_display_width <= content_width {
230                format!("{:<width$}", line.value, width = content_width)
231            } else {
232                truncate_to_width(&line.value, content_width)
233            }
234        } else if !line.label.is_empty() {
235            // Label only - left align
236            if label_display_width <= content_width {
237                format!("{:<width$}", formatted_label, width = content_width)
238            } else {
239                truncate_to_width(&formatted_label, content_width)
240            }
241        } else {
242            // Empty line
243            " ".repeat(content_width)
244        };
245        
246        // Ensure final content is exactly the right width
247        let actual_width = visual_width(&content);
248        let final_content = if actual_width < content_width {
249            format!("{}{}", content, " ".repeat(content_width - actual_width))
250        } else if actual_width > content_width {
251            truncate_to_width(&content, content_width)
252        } else {
253            content
254        };
255        
256        format!("│ {} │", final_content)
257    }
258}
259
260/// Calculate visual width of a string, handling ANSI color codes
261fn visual_width(s: &str) -> usize {
262    let mut width = 0;
263    let mut chars = s.chars().peekable();
264    
265    while let Some(ch) = chars.next() {
266        if ch == '\x1b' {
267            // Skip ANSI escape sequence
268            if chars.peek() == Some(&'[') {
269                chars.next(); // consume '['
270                while let Some(c) = chars.next() {
271                    if c.is_ascii_alphabetic() {
272                        break; // End of escape sequence
273                    }
274                }
275            }
276        } else {
277            // Simple width calculation for common cases
278            // Most characters are width 1, some are width 0 or 2
279            width += char_width(ch);
280        }
281    }
282    
283    width
284}
285
286/// Simple character width calculation without external dependencies
287fn char_width(ch: char) -> usize {
288    match ch {
289        // Control characters have width 0
290        '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
291        // Combining marks have width 0
292        '\u{0300}'..='\u{036F}' => 0,
293        // Emoji and symbols (width 2)
294        '\u{2600}'..='\u{26FF}' |    // Miscellaneous Symbols
295        '\u{2700}'..='\u{27BF}' |    // Dingbats
296        '\u{1F000}'..='\u{1F02F}' |  // Mahjong Tiles
297        '\u{1F030}'..='\u{1F09F}' |  // Domino Tiles
298        '\u{1F0A0}'..='\u{1F0FF}' |  // Playing Cards
299        '\u{1F100}'..='\u{1F1FF}' |  // Enclosed Alphanumeric Supplement
300        '\u{1F200}'..='\u{1F2FF}' |  // Enclosed Ideographic Supplement
301        '\u{1F300}'..='\u{1F5FF}' |  // Miscellaneous Symbols and Pictographs
302        '\u{1F600}'..='\u{1F64F}' |  // Emoticons
303        '\u{1F650}'..='\u{1F67F}' |  // Ornamental Dingbats
304        '\u{1F680}'..='\u{1F6FF}' |  // Transport and Map Symbols
305        '\u{1F700}'..='\u{1F77F}' |  // Alchemical Symbols
306        '\u{1F780}'..='\u{1F7FF}' |  // Geometric Shapes Extended
307        '\u{1F800}'..='\u{1F8FF}' |  // Supplemental Arrows-C
308        '\u{1F900}'..='\u{1F9FF}' |  // Supplemental Symbols and Pictographs
309        // Full-width characters (common CJK ranges)
310        '\u{1100}'..='\u{115F}' |  // Hangul Jamo
311        '\u{2E80}'..='\u{2EFF}' |  // CJK Radicals
312        '\u{2F00}'..='\u{2FDF}' |  // Kangxi Radicals
313        '\u{2FF0}'..='\u{2FFF}' |  // Ideographic Description
314        '\u{3000}'..='\u{303E}' |  // CJK Symbols and Punctuation
315        '\u{3041}'..='\u{3096}' |  // Hiragana
316        '\u{30A1}'..='\u{30FA}' |  // Katakana
317        '\u{3105}'..='\u{312D}' |  // Bopomofo
318        '\u{3131}'..='\u{318E}' |  // Hangul Compatibility Jamo
319        '\u{3190}'..='\u{31BA}' |  // Kanbun
320        '\u{31C0}'..='\u{31E3}' |  // CJK Strokes
321        '\u{31F0}'..='\u{31FF}' |  // Katakana Phonetic Extensions
322        '\u{3200}'..='\u{32FF}' |  // Enclosed CJK Letters and Months
323        '\u{3300}'..='\u{33FF}' |  // CJK Compatibility
324        '\u{3400}'..='\u{4DBF}' |  // CJK Extension A
325        '\u{4E00}'..='\u{9FFF}' |  // CJK Unified Ideographs
326        '\u{A000}'..='\u{A48C}' |  // Yi Syllables
327        '\u{A490}'..='\u{A4C6}' |  // Yi Radicals
328        '\u{AC00}'..='\u{D7AF}' |  // Hangul Syllables
329        '\u{F900}'..='\u{FAFF}' |  // CJK Compatibility Ideographs
330        '\u{FE10}'..='\u{FE19}' |  // Vertical Forms
331        '\u{FE30}'..='\u{FE6F}' |  // CJK Compatibility Forms
332        '\u{FF00}'..='\u{FF60}' |  // Fullwidth Forms
333        '\u{FFE0}'..='\u{FFE6}' => 2,
334        // Most other printable characters have width 1
335        _ => 1,
336    }
337}
338
339/// Truncate string to specified visual width, preserving color codes
340fn truncate_to_width(s: &str, max_width: usize) -> String {
341    let current_visual_width = visual_width(s);
342    if current_visual_width <= max_width {
343        return s.to_string();
344    }
345    
346    // For strings with ANSI codes, we need to be more careful
347    if s.contains('\x1b') {
348        // Simple approach: strip ANSI codes, truncate, then re-apply if needed
349        let stripped = strip_ansi_codes(s);
350        if visual_width(&stripped) <= max_width {
351            return s.to_string();
352        }
353        
354        // Truncate the stripped version
355        let mut result = String::new();
356        let mut width = 0;
357        for ch in stripped.chars() {
358            let ch_width = char_width(ch);
359            if width + ch_width > max_width.saturating_sub(3) {
360                result.push_str("...");
361                break;
362            }
363            result.push(ch);
364            width += ch_width;
365        }
366        return result;
367    }
368    
369    // No ANSI codes - simple truncation
370    let mut result = String::new();
371    let mut width = 0;
372    
373    for ch in s.chars() {
374        let ch_width = char_width(ch);
375        if width + ch_width > max_width.saturating_sub(3) {
376            result.push_str("...");
377            break;
378        }
379        result.push(ch);
380        width += ch_width;
381    }
382    
383    result
384}
385
386/// Strip ANSI escape codes from a string
387fn strip_ansi_codes(s: &str) -> String {
388    let mut result = String::new();
389    let mut chars = s.chars().peekable();
390    
391    while let Some(ch) = chars.next() {
392        if ch == '\x1b' {
393            // Skip ANSI escape sequence
394            if chars.peek() == Some(&'[') {
395                chars.next(); // consume '['
396                while let Some(c) = chars.next() {
397                    if c.is_ascii_alphabetic() {
398                        break; // End of escape sequence
399                    }
400                }
401            }
402        } else {
403            result.push(ch);
404        }
405    }
406    
407    result
408}
409
410/// Display mode for analysis output
411#[derive(Debug, Clone, Copy, PartialEq)]
412pub enum DisplayMode {
413    /// Compact matrix view (default)
414    Matrix,
415    /// Detailed vertical view (legacy)
416    Detailed,
417    /// Summary only
418    Summary,
419    /// JSON output
420    Json,
421}
422
423/// Main display function that routes to appropriate formatter
424pub fn display_analysis(analysis: &MonorepoAnalysis, mode: DisplayMode) {
425    match mode {
426        DisplayMode::Matrix => display_matrix_view(analysis),
427        DisplayMode::Detailed => display_detailed_view(analysis),
428        DisplayMode::Summary => display_summary_view(analysis),
429        DisplayMode::Json => display_json_view(analysis),
430    }
431}
432
433/// Display analysis in a compact matrix/dashboard format
434pub fn display_matrix_view(analysis: &MonorepoAnalysis) {
435    // Header
436    println!("\n{}", "═".repeat(100).bright_blue());
437    println!("{}", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold());
438    println!("{}", "═".repeat(100).bright_blue());
439    
440    // Architecture Overview Box
441    display_architecture_box(analysis);
442    
443    // Technology Stack Box
444    display_technology_stack_box(analysis);
445    
446    // Projects Matrix
447    if analysis.projects.len() > 1 {
448        display_projects_matrix(analysis);
449    } else {
450        display_single_project_matrix(analysis);
451    }
452    
453    // Docker Infrastructure Overview
454    if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
455        display_docker_overview_matrix(analysis);
456    }
457    
458    // Analysis Metrics Box
459    display_metrics_box(analysis);
460    
461    // Footer
462    println!("\n{}", "═".repeat(100).bright_blue());
463}
464
465/// Display architecture overview in a box
466fn display_architecture_box(analysis: &MonorepoAnalysis) {
467    let mut box_drawer = BoxDrawer::new("Architecture Overview");
468    
469    let arch_type = if analysis.is_monorepo {
470        format!("Monorepo ({} projects)", analysis.projects.len())
471    } else {
472        "Single Project".to_string()
473    };
474    
475    box_drawer.add_line("Type:", &arch_type.yellow(), true);
476    box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true);
477    
478    // Pattern description
479    let pattern_desc = match &analysis.technology_summary.architecture_pattern {
480        ArchitecturePattern::Monolithic => "Single, self-contained application",
481        ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation",
482        ArchitecturePattern::Microservices => "Multiple independent microservices",
483        ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces",
484        ArchitecturePattern::EventDriven => "Event-driven with decoupled components",
485        ArchitecturePattern::Mixed => "Mixed architecture patterns",
486    };
487    box_drawer.add_value_only(&pattern_desc.dimmed());
488    
489    println!("\n{}", box_drawer.draw());
490}
491
492/// Display technology stack overview
493fn display_technology_stack_box(analysis: &MonorepoAnalysis) {
494    let mut box_drawer = BoxDrawer::new("Technology Stack");
495    
496    let mut has_content = false;
497    
498    // Languages
499    if !analysis.technology_summary.languages.is_empty() {
500        let languages = analysis.technology_summary.languages.join(", ");
501        box_drawer.add_line("Languages:", &languages.blue(), true);
502        has_content = true;
503    }
504    
505    // Frameworks
506    if !analysis.technology_summary.frameworks.is_empty() {
507        let frameworks = analysis.technology_summary.frameworks.join(", ");
508        box_drawer.add_line("Frameworks:", &frameworks.magenta(), true);
509        has_content = true;
510    }
511    
512    // Databases
513    if !analysis.technology_summary.databases.is_empty() {
514        let databases = analysis.technology_summary.databases.join(", ");
515        box_drawer.add_line("Databases:", &databases.cyan(), true);
516        has_content = true;
517    }
518    
519    if !has_content {
520        box_drawer.add_value_only("No technologies detected");
521    }
522    
523    println!("\n{}", box_drawer.draw());
524}
525
526/// Display projects in a matrix table format
527fn display_projects_matrix(analysis: &MonorepoAnalysis) {
528    let mut box_drawer = BoxDrawer::new("Projects Matrix");
529    
530    // Collect all data first to calculate optimal column widths
531    let mut project_data = Vec::new();
532    for project in &analysis.projects {
533        let name = project.name.clone(); // Remove emoji to avoid width calculation issues
534        let proj_type = format_project_category(&project.project_category);
535        
536        let languages = project.analysis.languages.iter()
537            .map(|l| l.name.clone())
538            .collect::<Vec<_>>()
539            .join(", ");
540        
541        let main_tech = get_main_technologies(&project.analysis.technologies);
542        
543        let ports = if project.analysis.ports.is_empty() {
544            "-".to_string()
545        } else {
546            project.analysis.ports.iter()
547                .map(|p| p.number.to_string())
548                .collect::<Vec<_>>()
549                .join(", ")
550        };
551        
552        let docker = if project.analysis.docker_analysis.is_some() {
553            "Yes"
554        } else {
555            "No"
556        };
557        
558        let deps_count = project.analysis.dependencies.len().to_string();
559        
560        project_data.push((name, proj_type.to_string(), languages, main_tech, ports, docker.to_string(), deps_count));
561    }
562    
563    // Calculate column widths based on content
564    let headers = vec!["Project", "Type", "Languages", "Main Tech", "Ports", "Docker", "Deps"];
565    let mut col_widths = headers.iter().map(|h| visual_width(h)).collect::<Vec<_>>();
566    
567    for (name, proj_type, languages, main_tech, ports, docker, deps_count) in &project_data {
568        col_widths[0] = col_widths[0].max(visual_width(name));
569        col_widths[1] = col_widths[1].max(visual_width(proj_type));
570        col_widths[2] = col_widths[2].max(visual_width(languages));
571        col_widths[3] = col_widths[3].max(visual_width(main_tech));
572        col_widths[4] = col_widths[4].max(visual_width(ports));
573        col_widths[5] = col_widths[5].max(visual_width(docker));
574        col_widths[6] = col_widths[6].max(visual_width(deps_count));
575    }
576    
577
578    // Create header row
579    let header_parts: Vec<String> = headers.iter().zip(&col_widths)
580        .map(|(h, &w)| format!("{:<width$}", h, width = w))
581        .collect();
582    let header_line = header_parts.join(" │ ");
583    box_drawer.add_value_only(&header_line);
584    
585    // Add separator
586    let separator_parts: Vec<String> = col_widths.iter()
587        .map(|&w| "─".repeat(w))
588        .collect();
589    let separator_line = separator_parts.join("─┼─");
590    box_drawer.add_value_only(&separator_line);
591    
592    // Add data rows
593    for (name, proj_type, languages, main_tech, ports, docker, deps_count) in project_data {
594        let row_parts = vec![
595            format!("{:<width$}", name, width = col_widths[0]),
596            format!("{:<width$}", proj_type, width = col_widths[1]),
597            format!("{:<width$}", languages, width = col_widths[2]),
598            format!("{:<width$}", main_tech, width = col_widths[3]),
599            format!("{:<width$}", ports, width = col_widths[4]),
600            format!("{:<width$}", docker, width = col_widths[5]),
601            format!("{:<width$}", deps_count, width = col_widths[6]),
602        ];
603        let row_line = row_parts.join(" │ ");
604        box_drawer.add_value_only(&row_line);
605    }
606    
607    println!("\n{}", box_drawer.draw());
608}
609
610/// Display single project in matrix format
611fn display_single_project_matrix(analysis: &MonorepoAnalysis) {
612    if let Some(project) = analysis.projects.first() {
613        let mut box_drawer = BoxDrawer::new("Project Overview");
614        
615        // Basic info
616        box_drawer.add_line("Name:", &project.name.yellow(), true);
617        box_drawer.add_line("Type:", &format_project_category(&project.project_category).green(), true);
618        
619        // Languages 
620        if !project.analysis.languages.is_empty() {
621            let lang_info = project.analysis.languages.iter()
622                .map(|l| l.name.clone())
623                .collect::<Vec<_>>()
624                .join(", ");
625            box_drawer.add_line("Languages:", &lang_info.blue(), true);
626        }
627        
628        // Technologies by category
629        add_technologies_to_drawer(&project.analysis.technologies, &mut box_drawer);
630        
631        // Key metrics
632        box_drawer.add_separator();
633        box_drawer.add_line("Key Metrics:", "", true);
634        
635        // Display metrics on two lines to fit properly
636        box_drawer.add_value_only(&format!("Entry Points: {} │ Exposed Ports: {} │ Env Variables: {}", 
637            project.analysis.entry_points.len(),
638            project.analysis.ports.len(),
639            project.analysis.environment_variables.len()
640        ).cyan());
641        
642        box_drawer.add_value_only(&format!("Build Scripts: {} │ Dependencies: {}", 
643            project.analysis.build_scripts.len(),
644            project.analysis.dependencies.len()
645        ).cyan());
646        
647        // Confidence score with progress bar
648        add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer);
649        
650        println!("\n{}", box_drawer.draw());
651    }
652}
653
654/// Add technologies organized by category to the box drawer
655fn add_technologies_to_drawer(technologies: &[DetectedTechnology], box_drawer: &mut BoxDrawer) {
656    let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
657    
658    for tech in technologies {
659        by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
660    }
661    
662    // Display primary technology first
663    if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
664        let primary_info = primary.name.bright_yellow().bold().to_string();
665        box_drawer.add_line("Primary Stack:", &primary_info, true);
666    }
667    
668    // Display other categories
669    let categories = [
670        (TechnologyCategory::FrontendFramework, "Frameworks"),
671        (TechnologyCategory::BuildTool, "Build Tools"),
672        (TechnologyCategory::Database, "Databases"),
673        (TechnologyCategory::Testing, "Testing"),
674    ];
675    
676    for (category, label) in &categories {
677        if let Some(techs) = by_category.get(category) {
678            let tech_names = techs.iter()
679                .map(|t| t.name.clone())
680                .collect::<Vec<_>>()
681                .join(", ");
682            
683            if !tech_names.is_empty() {
684                let label_with_colon = format!("{}:", label);
685                box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true);
686            }
687        }
688    }
689    
690    // Handle Library category separately since it's parameterized - use vertical layout for many items
691    let mut all_libraries: Vec<&DetectedTechnology> = Vec::new();
692    for (cat, techs) in &by_category {
693        if matches!(cat, TechnologyCategory::Library(_)) {
694            all_libraries.extend(techs.iter().copied());
695        }
696    }
697    
698    if !all_libraries.is_empty() {
699        // Sort libraries by confidence for better display
700        all_libraries.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
701        
702        if all_libraries.len() <= 3 {
703            // For few libraries, keep horizontal layout
704            let tech_names = all_libraries.iter()
705                .map(|t| t.name.clone())
706                .collect::<Vec<_>>()
707                .join(", ");
708            box_drawer.add_line("Libraries:", &tech_names.magenta(), true);
709        } else {
710            // For many libraries, use vertical layout with multiple rows
711            box_drawer.add_line("Libraries:", "", true);
712            
713            // Group libraries into rows of 3-4 items each
714            let items_per_row = 3;
715            for chunk in all_libraries.chunks(items_per_row) {
716                let row_items = chunk.iter()
717                    .map(|t| t.name.clone())
718                    .collect::<Vec<_>>()
719                    .join(", ");
720                
721                // Add indented row
722                let indented_row = format!("  {}", row_items);
723                box_drawer.add_value_only(&indented_row.magenta());
724            }
725        }
726    }
727}
728
729/// Display Docker infrastructure overview in matrix format
730fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) {
731    let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
732    
733    let mut total_dockerfiles = 0;
734    let mut total_compose_files = 0;
735    let mut total_services = 0;
736    let mut orchestration_patterns = std::collections::HashSet::new();
737    
738    for project in &analysis.projects {
739        if let Some(docker) = &project.analysis.docker_analysis {
740            total_dockerfiles += docker.dockerfiles.len();
741            total_compose_files += docker.compose_files.len();
742            total_services += docker.services.len();
743            orchestration_patterns.insert(&docker.orchestration_pattern);
744        }
745    }
746    
747    box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
748    box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
749    box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
750    
751    let patterns = orchestration_patterns.iter()
752        .map(|p| format!("{:?}", p))
753        .collect::<Vec<_>>()
754        .join(", ");
755    box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true);
756    
757    // Service connectivity summary
758    let mut has_services = false;
759    for project in &analysis.projects {
760        if let Some(docker) = &project.analysis.docker_analysis {
761            for service in &docker.services {
762                if !service.ports.is_empty() || !service.depends_on.is_empty() {
763                    has_services = true;
764                    break;
765                }
766            }
767        }
768    }
769    
770    if has_services {
771        box_drawer.add_separator();
772        box_drawer.add_line("Service Connectivity:", "", true);
773        
774        for project in &analysis.projects {
775            if let Some(docker) = &project.analysis.docker_analysis {
776                for service in &docker.services {
777                    if !service.ports.is_empty() || !service.depends_on.is_empty() {
778                        let port_info = service.ports.iter()
779                            .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port)))
780                            .collect::<Vec<_>>()
781                            .join(", ");
782                        
783                        let deps_info = if service.depends_on.is_empty() {
784                            String::new()
785                        } else {
786                            format!(" → {}", service.depends_on.join(", "))
787                        };
788                        
789                        let info = format!("  {}: {}{}", service.name, port_info, deps_info);
790                        box_drawer.add_value_only(&info.cyan());
791                    }
792                }
793            }
794        }
795    }
796    
797    println!("\n{}", box_drawer.draw());
798}
799
800/// Display analysis metrics
801fn display_metrics_box(analysis: &MonorepoAnalysis) {
802    let mut box_drawer = BoxDrawer::new("Analysis Metrics");
803    
804    // Performance metrics
805    let duration_ms = analysis.metadata.analysis_duration_ms;
806    let duration_str = if duration_ms < 1000 {
807        format!("{}ms", duration_ms)
808    } else {
809        format!("{:.1}s", duration_ms as f64 / 1000.0)
810    };
811    
812    // Create metrics line without emojis first to avoid width calculation issues
813    let metrics_line = format!(
814        "Duration: {} | Files: {} | Score: {}% | Version: {}",
815        duration_str,
816        analysis.metadata.files_analyzed,
817        format!("{:.0}", analysis.metadata.confidence_score * 100.0),
818        analysis.metadata.analyzer_version
819    );
820    
821    // Apply single color to the entire line for consistency
822    let colored_metrics = metrics_line.cyan();
823    box_drawer.add_value_only(&colored_metrics.to_string());
824    
825    println!("\n{}", box_drawer.draw());
826}
827
828/// Add confidence score as a progress bar to the box drawer
829fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) {
830    let percentage = (score * 100.0) as u8;
831    let bar_width = 20;
832    let filled = ((score * bar_width as f32) as usize).min(bar_width);
833    
834    let bar = format!("{}{}",
835        "█".repeat(filled).green(),
836        "░".repeat(bar_width - filled).dimmed()
837    );
838    
839    let color = if percentage >= 80 {
840        "green"
841    } else if percentage >= 60 {
842        "yellow"
843    } else {
844        "red"
845    };
846    
847    let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color));
848    box_drawer.add_line("Confidence:", &confidence_info, true);
849}
850
851/// Get main technologies for display
852fn get_main_technologies(technologies: &[DetectedTechnology]) -> String {
853    let primary = technologies.iter().find(|t| t.is_primary);
854    let frameworks: Vec<_> = technologies.iter()
855        .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework))
856        .take(2)
857        .collect();
858    
859    let mut result = Vec::new();
860    
861    if let Some(p) = primary {
862        result.push(p.name.clone());
863    }
864    
865    for f in frameworks {
866        if Some(&f.name) != primary.map(|p| &p.name) {
867            result.push(f.name.clone());
868        }
869    }
870    
871    if result.is_empty() {
872        "-".to_string()
873    } else {
874        result.join(", ")
875    }
876}
877
878/// Display in detailed vertical format (legacy)
879pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
880    // Use the legacy detailed display format
881    println!("{}", "=".repeat(80));
882    println!("\n📊 PROJECT ANALYSIS RESULTS");
883    println!("{}", "=".repeat(80));
884    
885    // Overall project information
886    if analysis.is_monorepo {
887        println!("\n🏗️  Architecture: Monorepo with {} projects", analysis.projects.len());
888        println!("   Pattern: {:?}", analysis.technology_summary.architecture_pattern);
889        
890        display_architecture_description(&analysis.technology_summary.architecture_pattern);
891    } else {
892        println!("\n🏗️  Architecture: Single Project");
893    }
894    
895    // Technology Summary
896    println!("\n🌐 Technology Summary:");
897    if !analysis.technology_summary.languages.is_empty() {
898        println!("   Languages: {}", analysis.technology_summary.languages.join(", "));
899    }
900    if !analysis.technology_summary.frameworks.is_empty() {
901        println!("   Frameworks: {}", analysis.technology_summary.frameworks.join(", "));
902    }
903    if !analysis.technology_summary.databases.is_empty() {
904        println!("   Databases: {}", analysis.technology_summary.databases.join(", "));
905    }
906    
907    // Individual project details
908    println!("\n📁 Project Details:");
909    println!("{}", "=".repeat(80));
910    
911    for (i, project) in analysis.projects.iter().enumerate() {
912        println!("\n{} {}. {} ({})", 
913            get_category_emoji(&project.project_category),
914            i + 1, 
915            project.name,
916            format_project_category(&project.project_category)
917        );
918        
919        if analysis.is_monorepo {
920            println!("   📂 Path: {}", project.path.display());
921        }
922        
923        // Languages for this project
924        if !project.analysis.languages.is_empty() {
925            println!("   🌐 Languages:");
926            for lang in &project.analysis.languages {
927                print!("      • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0);
928                if let Some(version) = &lang.version {
929                    print!(" - Version: {}", version);
930                }
931                println!();
932            }
933        }
934        
935        // Technologies for this project
936        if !project.analysis.technologies.is_empty() {
937            println!("   🚀 Technologies:");
938            display_technologies_detailed_legacy(&project.analysis.technologies);
939        }
940        
941        // Entry Points
942        if !project.analysis.entry_points.is_empty() {
943            println!("   📍 Entry Points ({}):", project.analysis.entry_points.len());
944            for (j, entry) in project.analysis.entry_points.iter().enumerate() {
945                println!("      {}. File: {}", j + 1, entry.file.display());
946                if let Some(func) = &entry.function {
947                    println!("         Function: {}", func);
948                }
949                if let Some(cmd) = &entry.command {
950                    println!("         Command: {}", cmd);
951                }
952            }
953        }
954        
955        // Ports
956        if !project.analysis.ports.is_empty() {
957            println!("   🔌 Exposed Ports ({}):", project.analysis.ports.len());
958            for port in &project.analysis.ports {
959                println!("      • Port {}: {:?}", port.number, port.protocol);
960                if let Some(desc) = &port.description {
961                    println!("        {}", desc);
962                }
963            }
964        }
965        
966        // Environment Variables
967        if !project.analysis.environment_variables.is_empty() {
968            println!("   🔐 Environment Variables ({}):", project.analysis.environment_variables.len());
969            let required_vars: Vec<_> = project.analysis.environment_variables.iter()
970                .filter(|ev| ev.required)
971                .collect();
972            let optional_vars: Vec<_> = project.analysis.environment_variables.iter()
973                .filter(|ev| !ev.required)
974                .collect();
975            
976            if !required_vars.is_empty() {
977                println!("      Required:");
978                for var in required_vars {
979                    println!("        • {} {}", 
980                        var.name,
981                        if let Some(desc) = &var.description { 
982                            format!("({})", desc) 
983                        } else { 
984                            String::new() 
985                        }
986                    );
987                }
988            }
989            
990            if !optional_vars.is_empty() {
991                println!("      Optional:");
992                for var in optional_vars {
993                    println!("        • {} = {:?}", 
994                        var.name, 
995                        var.default_value.as_deref().unwrap_or("no default")
996                    );
997                }
998            }
999        }
1000        
1001        // Build Scripts
1002        if !project.analysis.build_scripts.is_empty() {
1003            println!("   🔨 Build Scripts ({}):", project.analysis.build_scripts.len());
1004            let default_scripts: Vec<_> = project.analysis.build_scripts.iter()
1005                .filter(|bs| bs.is_default)
1006                .collect();
1007            let other_scripts: Vec<_> = project.analysis.build_scripts.iter()
1008                .filter(|bs| !bs.is_default)
1009                .collect();
1010            
1011            if !default_scripts.is_empty() {
1012                println!("      Default scripts:");
1013                for script in default_scripts {
1014                    println!("        • {}: {}", script.name, script.command);
1015                    if let Some(desc) = &script.description {
1016                        println!("          {}", desc);
1017                    }
1018                }
1019            }
1020            
1021            if !other_scripts.is_empty() {
1022                println!("      Other scripts:");
1023                for script in other_scripts {
1024                    println!("        • {}: {}", script.name, script.command);
1025                    if let Some(desc) = &script.description {
1026                        println!("          {}", desc);
1027                    }
1028                }
1029            }
1030        }
1031        
1032        // Dependencies (sample)
1033        if !project.analysis.dependencies.is_empty() {
1034            println!("   📦 Dependencies ({}):", project.analysis.dependencies.len());
1035            if project.analysis.dependencies.len() <= 5 {
1036                for (name, version) in &project.analysis.dependencies {
1037                    println!("      • {} v{}", name, version);
1038                }
1039            } else {
1040                // Show first 5
1041                for (name, version) in project.analysis.dependencies.iter().take(5) {
1042                    println!("      • {} v{}", name, version);
1043                }
1044                println!("      ... and {} more", project.analysis.dependencies.len() - 5);
1045            }
1046        }
1047        
1048        // Docker Infrastructure Analysis
1049        if let Some(docker_analysis) = &project.analysis.docker_analysis {
1050            display_docker_analysis_detailed_legacy(docker_analysis);
1051        }
1052        
1053        // Project type
1054        println!("   🎯 Project Type: {:?}", project.analysis.project_type);
1055        
1056        if i < analysis.projects.len() - 1 {
1057            println!("{}", "-".repeat(40));
1058        }
1059    }
1060    
1061    // Summary
1062    println!("\n📋 ANALYSIS SUMMARY");
1063    println!("{}", "=".repeat(80));
1064    println!("✅ Project Analysis Complete!");
1065    
1066    if analysis.is_monorepo {
1067        println!("\n🏗️  Monorepo Architecture:");
1068        println!("   • Total projects: {}", analysis.projects.len());
1069        println!("   • Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern);
1070        
1071        let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count();
1072        let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count();
1073        let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count();
1074        let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count();
1075        
1076        if frontend_count > 0 { println!("   • Frontend projects: {}", frontend_count); }
1077        if backend_count > 0 { println!("   • Backend/API projects: {}", backend_count); }
1078        if service_count > 0 { println!("   • Service projects: {}", service_count); }
1079        if lib_count > 0 { println!("   • Library projects: {}", lib_count); }
1080    }
1081    
1082    println!("\n📈 Analysis Metadata:");
1083    println!("   • Duration: {}ms", analysis.metadata.analysis_duration_ms);
1084    println!("   • Files analyzed: {}", analysis.metadata.files_analyzed);
1085    println!("   • Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0);
1086    println!("   • Analyzer version: {}", analysis.metadata.analyzer_version);
1087}
1088
1089/// Helper function for legacy detailed technology display
1090fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1091    // Group technologies by category
1092    let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1093    
1094    for tech in technologies {
1095        by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1096    }
1097    
1098    // Find and display primary technology
1099    if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1100        println!("\n🛠️  Technology Stack:");
1101        println!("   🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
1102        println!("      Architecture driver for this project");
1103    }
1104    
1105    // Display categories in order
1106    let categories = [
1107        (TechnologyCategory::MetaFramework, "🏗️  Meta-Frameworks"),
1108        (TechnologyCategory::BackendFramework, "🖥️  Backend Frameworks"),
1109        (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"),
1110        (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"),
1111        (TechnologyCategory::Library(LibraryType::Utility), "📚 Core Libraries"),
1112        (TechnologyCategory::BuildTool, "🔨 Build Tools"),
1113        (TechnologyCategory::PackageManager, "📦 Package Managers"),
1114        (TechnologyCategory::Database, "🗃️  Database & ORM"),
1115        (TechnologyCategory::Runtime, "⚡ Runtimes"),
1116        (TechnologyCategory::Testing, "🧪 Testing"),
1117    ];
1118    
1119    for (category, label) in &categories {
1120        if let Some(techs) = by_category.get(category) {
1121            if !techs.is_empty() {
1122                println!("\n   {}:", label);
1123                for tech in techs {
1124                    println!("      • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1125                    if let Some(version) = &tech.version {
1126                        println!("        Version: {}", version);
1127                    }
1128                }
1129            }
1130        }
1131    }
1132    
1133    // Handle other Library types separately
1134    for (cat, techs) in &by_category {
1135        match cat {
1136            TechnologyCategory::Library(lib_type) => {
1137                let label = match lib_type {
1138                    LibraryType::StateManagement => "🔄 State Management",
1139                    LibraryType::DataFetching => "🔃 Data Fetching",
1140                    LibraryType::Routing => "🗺️  Routing",
1141                    LibraryType::Styling => "🎨 Styling",
1142                    LibraryType::HttpClient => "🌐 HTTP Clients",
1143                    LibraryType::Authentication => "🔐 Authentication",
1144                    LibraryType::Other(_) => "📦 Other Libraries",
1145                    _ => continue, // Skip already handled UI and Utility
1146                };
1147                
1148                // Only print if not already handled above
1149                if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() {
1150                    println!("\n   {}:", label);
1151                    for tech in techs {
1152                        println!("      • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1153                        if let Some(version) = &tech.version {
1154                            println!("        Version: {}", version);
1155                        }
1156                    }
1157                }
1158            }
1159            _ => {} // Other categories already handled in the array
1160        }
1161    }
1162}
1163
1164/// Helper function for legacy Docker analysis display
1165fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1166    println!("\n   🐳 Docker Infrastructure Analysis:");
1167    
1168    // Dockerfiles
1169    if !docker_analysis.dockerfiles.is_empty() {
1170        println!("      📄 Dockerfiles ({}):", docker_analysis.dockerfiles.len());
1171        for dockerfile in &docker_analysis.dockerfiles {
1172            println!("         • {}", dockerfile.path.display());
1173            if let Some(env) = &dockerfile.environment {
1174                println!("           Environment: {}", env);
1175            }
1176            if let Some(base_image) = &dockerfile.base_image {
1177                println!("           Base image: {}", base_image);
1178            }
1179            if !dockerfile.exposed_ports.is_empty() {
1180                println!("           Exposed ports: {}", 
1181                    dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "));
1182            }
1183            if dockerfile.is_multistage {
1184                println!("           Multi-stage build: {} stages", dockerfile.build_stages.len());
1185            }
1186            println!("           Instructions: {}", dockerfile.instruction_count);
1187        }
1188    }
1189    
1190    // Compose files
1191    if !docker_analysis.compose_files.is_empty() {
1192        println!("      📋 Compose Files ({}):", docker_analysis.compose_files.len());
1193        for compose_file in &docker_analysis.compose_files {
1194            println!("         • {}", compose_file.path.display());
1195            if let Some(env) = &compose_file.environment {
1196                println!("           Environment: {}", env);
1197            }
1198            if let Some(version) = &compose_file.version {
1199                println!("           Version: {}", version);
1200            }
1201            if !compose_file.service_names.is_empty() {
1202                println!("           Services: {}", compose_file.service_names.join(", "));
1203            }
1204            if !compose_file.networks.is_empty() {
1205                println!("           Networks: {}", compose_file.networks.join(", "));
1206            }
1207            if !compose_file.volumes.is_empty() {
1208                println!("           Volumes: {}", compose_file.volumes.join(", "));
1209            }
1210        }
1211    }
1212    
1213    // Rest of the detailed Docker display...
1214    println!("      🏗️  Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern);
1215    match docker_analysis.orchestration_pattern {
1216        OrchestrationPattern::SingleContainer => {
1217            println!("         Simple containerized application");
1218        }
1219        OrchestrationPattern::DockerCompose => {
1220            println!("         Multi-service Docker Compose setup");
1221        }
1222        OrchestrationPattern::Microservices => {
1223            println!("         Microservices architecture with service discovery");
1224        }
1225        OrchestrationPattern::EventDriven => {
1226            println!("         Event-driven architecture with message queues");
1227        }
1228        OrchestrationPattern::ServiceMesh => {
1229            println!("         Service mesh for advanced service communication");
1230        }
1231        OrchestrationPattern::Mixed => {
1232            println!("         Mixed/complex orchestration pattern");
1233        }
1234    }
1235}
1236
1237/// Display architecture description
1238fn display_architecture_description(pattern: &ArchitecturePattern) {
1239    match pattern {
1240        ArchitecturePattern::Monolithic => {
1241            println!("   📦 This is a single, self-contained application");
1242        }
1243        ArchitecturePattern::Fullstack => {
1244            println!("   🌐 This is a full-stack application with separate frontend and backend");
1245        }
1246        ArchitecturePattern::Microservices => {
1247            println!("   🔗 This is a microservices architecture with multiple independent services");
1248        }
1249        ArchitecturePattern::ApiFirst => {
1250            println!("   🔌 This is an API-first architecture focused on service interfaces");
1251        }
1252        ArchitecturePattern::EventDriven => {
1253            println!("   📡 This is an event-driven architecture with decoupled components");
1254        }
1255        ArchitecturePattern::Mixed => {
1256            println!("   🔀 This is a mixed architecture combining multiple patterns");
1257        }
1258    }
1259}
1260
1261/// Display summary view only
1262pub fn display_summary_view(analysis: &MonorepoAnalysis) {
1263    println!("\n{} {}", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold());
1264    println!("{}", "─".repeat(50).dimmed());
1265    
1266    println!("{} Architecture: {}", "│".dimmed(), 
1267        if analysis.is_monorepo {
1268            format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1269        } else {
1270            "Single Project".to_string().yellow()
1271        }
1272    );
1273    
1274    println!("{} Pattern: {}", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green());
1275    println!("{} Stack: {}", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue());
1276    
1277    if !analysis.technology_summary.frameworks.is_empty() {
1278        println!("{} Frameworks: {}", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta());
1279    }
1280    
1281    println!("{} Analysis Time: {}ms", "│".dimmed(), analysis.metadata.analysis_duration_ms);
1282    println!("{} Confidence: {:.0}%", "│".dimmed(), analysis.metadata.confidence_score * 100.0);
1283    
1284    println!("{}", "─".repeat(50).dimmed());
1285}
1286
1287/// Display JSON output
1288pub fn display_json_view(analysis: &MonorepoAnalysis) {
1289    match serde_json::to_string_pretty(analysis) {
1290        Ok(json) => println!("{}", json),
1291        Err(e) => eprintln!("Error serializing to JSON: {}", e),
1292    }
1293}
1294
1295/// Get emoji for project category
1296fn get_category_emoji(category: &ProjectCategory) -> &'static str {
1297    match category {
1298        ProjectCategory::Frontend => "🌐",
1299        ProjectCategory::Backend => "⚙️",
1300        ProjectCategory::Api => "🔌",
1301        ProjectCategory::Service => "🚀",
1302        ProjectCategory::Library => "📚",
1303        ProjectCategory::Tool => "🔧",
1304        ProjectCategory::Documentation => "📖",
1305        ProjectCategory::Infrastructure => "🏗️",
1306        ProjectCategory::Unknown => "❓",
1307    }
1308}
1309
1310/// Format project category name
1311fn format_project_category(category: &ProjectCategory) -> &'static str {
1312    match category {
1313        ProjectCategory::Frontend => "Frontend",
1314        ProjectCategory::Backend => "Backend",
1315        ProjectCategory::Api => "API",
1316        ProjectCategory::Service => "Service",
1317        ProjectCategory::Library => "Library",
1318        ProjectCategory::Tool => "Tool",
1319        ProjectCategory::Documentation => "Documentation",
1320        ProjectCategory::Infrastructure => "Infrastructure",
1321        ProjectCategory::Unknown => "Unknown",
1322    }
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327    use super::*;
1328    
1329    #[test]
1330    fn test_display_modes() {
1331        // Test that display modes are properly defined
1332        assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1333        assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1334    }
1335}