syncable_cli/analyzer/display/
box_drawer.rs

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