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