syncable_cli/analyzer/display/
box_drawer.rs

1//! Box drawing utilities for creating formatted text boxes in the terminal
2
3use colored::*;
4use crate::analyzer::display::utils::{visual_width, truncate_to_width};
5
6/// Content line for measuring and drawing
7#[derive(Debug, Clone)]
8struct ContentLine {
9    label: String,
10    value: String,
11    label_colored: bool,
12}
13
14impl ContentLine {
15    fn new(label: &str, value: &str, label_colored: bool) -> Self {
16        Self {
17            label: label.to_string(),
18            value: value.to_string(),
19            label_colored,
20        }
21    }
22    
23    fn separator() -> Self {
24        Self {
25            label: "SEPARATOR".to_string(),
26            value: String::new(),
27            label_colored: false,
28        }
29    }
30}
31
32/// Box drawer that pre-calculates optimal dimensions
33pub struct BoxDrawer {
34    title: String,
35    lines: Vec<ContentLine>,
36    min_width: usize,
37    max_width: usize,
38}
39
40impl BoxDrawer {
41    pub fn new(title: &str) -> Self {
42        Self {
43            title: title.to_string(),
44            lines: Vec::new(),
45            min_width: 60,
46            max_width: 120, // Reduced from 150 for better terminal compatibility
47        }
48    }
49    
50    pub fn add_line(&mut self, label: &str, value: &str, label_colored: bool) {
51        self.lines.push(ContentLine::new(label, value, label_colored));
52    }
53    
54    pub fn add_value_only(&mut self, value: &str) {
55        self.lines.push(ContentLine::new("", value, false));
56    }
57    
58    pub fn add_separator(&mut self) {
59        self.lines.push(ContentLine::separator());
60    }
61    
62    /// Calculate optimal box width based on content
63    fn calculate_optimal_width(&self) -> usize {
64        let title_width = visual_width(&self.title) + 6; // "┌─ " + title + " " + extra padding
65        let mut max_content_width = 0;
66        
67        // Calculate the actual rendered width for each line
68        for line in &self.lines {
69            if line.label == "SEPARATOR" {
70                continue;
71            }
72            
73            let rendered_width = self.calculate_rendered_line_width(line);
74            max_content_width = max_content_width.max(rendered_width);
75        }
76        
77        // Add reasonable buffer for content
78        let content_width_with_buffer = max_content_width + 4; // More buffer for safety
79        
80        // Box needs padding: "│ " + content + " │" = content + 4
81        let needed_width = content_width_with_buffer + 4;
82        
83        // Use the maximum of title width and content width
84        let optimal_width = title_width.max(needed_width).max(self.min_width);
85        optimal_width.min(self.max_width)
86    }
87    
88    /// Calculate the actual rendered width of a line as it will appear
89    fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
90        let label_width = visual_width(&line.label);
91        let value_width = visual_width(&line.value);
92        
93        if !line.label.is_empty() && !line.value.is_empty() {
94            // Label + value: need space between them
95            // For colored labels, ensure minimum spacing
96            let min_label_space = if line.label_colored { 25 } else { label_width };
97            min_label_space + 2 + value_width // 2 spaces minimum between label and value
98        } else if !line.value.is_empty() {
99            // Value only
100            value_width
101        } else if !line.label.is_empty() {
102            // Label only
103            label_width
104        } else {
105            // Empty line
106            0
107        }
108    }
109    
110    /// Draw the complete box
111    pub fn draw(&self) -> String {
112        let box_width = self.calculate_optimal_width();
113        let content_width = box_width - 4; // Available space for content
114        
115        let mut output = Vec::new();
116        
117        // Top border
118        output.push(self.draw_top(box_width));
119        
120        // Content lines
121        for line in &self.lines {
122            if line.label == "SEPARATOR" {
123                output.push(self.draw_separator(box_width));
124            } else if line.label.is_empty() && line.value.is_empty() {
125                output.push(self.draw_empty_line(box_width));
126            } else {
127                output.push(self.draw_content_line(line, content_width));
128            }
129        }
130        
131        // Bottom border
132        output.push(self.draw_bottom(box_width));
133        
134        output.join("\n")
135    }
136    
137    fn draw_top(&self, width: usize) -> String {
138        let title_colored = self.title.bright_cyan();
139        let title_len = visual_width(&self.title);
140        
141        // "┌─ " + title + " " + remaining dashes + "┐"
142        let prefix_len = 3; // "┌─ "
143        let suffix_len = 1; // "┐"
144        let title_space = 1; // space after title
145        
146        let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
147        
148        format!("┌─ {} {}┐", 
149            title_colored,
150            "─".repeat(remaining_space)
151        )
152    }
153    
154    fn draw_bottom(&self, width: usize) -> String {
155        format!("└{}┘", "─".repeat(width - 2))
156    }
157    
158    fn draw_separator(&self, width: usize) -> String {
159        format!("│ {} │", "─".repeat(width - 4).dimmed())
160    }
161    
162    fn draw_empty_line(&self, width: usize) -> String {
163        format!("│ {} │", " ".repeat(width - 4))
164    }
165    
166    fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String {
167        // Format the label with color if needed
168        let formatted_label = if line.label_colored && !line.label.is_empty() {
169            line.label.bright_white().to_string()
170        } else {
171            line.label.clone()
172        };
173        
174        // Calculate actual display widths (use original label for width)
175        let label_display_width = visual_width(&line.label);
176        let value_display_width = visual_width(&line.value);
177        
178        // Build the content
179        let content = if !line.label.is_empty() && !line.value.is_empty() {
180            // Both label and value - ensure proper spacing
181            let min_label_space = if line.label_colored { 25 } else { label_display_width };
182            let label_padding = min_label_space.saturating_sub(label_display_width);
183            let remaining_space = content_width.saturating_sub(min_label_space + 2); // 2 for spacing
184            
185            if value_display_width <= remaining_space {
186                // Value fits - right align it
187                let value_padding = remaining_space.saturating_sub(value_display_width);
188                format!("{}{:<width$}  {}{}", 
189                    formatted_label, 
190                    "",
191                    " ".repeat(value_padding),
192                    line.value,
193                    width = label_padding
194                )
195            } else {
196                // Value too long - truncate it
197                let truncated_value = truncate_to_width(&line.value, remaining_space.saturating_sub(3));
198                format!("{}{:<width$}  {}", 
199                    formatted_label, 
200                    "",
201                    truncated_value,
202                    width = label_padding
203                )
204            }
205        } else if !line.value.is_empty() {
206            // Value only - left align
207            if value_display_width <= content_width {
208                format!("{:<width$}", line.value, width = content_width)
209            } else {
210                truncate_to_width(&line.value, content_width)
211            }
212        } else if !line.label.is_empty() {
213            // Label only - left align
214            if label_display_width <= content_width {
215                format!("{:<width$}", formatted_label, width = content_width)
216            } else {
217                truncate_to_width(&formatted_label, content_width)
218            }
219        } else {
220            // Empty line
221            " ".repeat(content_width)
222        };
223        
224        // Ensure final content is exactly the right width
225        let actual_width = visual_width(&content);
226        let final_content = if actual_width < content_width {
227            format!("{}{}", content, " ".repeat(content_width - actual_width))
228        } else if actual_width > content_width {
229            truncate_to_width(&content, content_width)
230        } else {
231            content
232        };
233        
234        format!("│ {} │", final_content)
235    }
236}