syncable_cli/analyzer/
display.rs

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