syncable_cli/analyzer/display/
box_drawer.rs1use colored::*;
4use crate::analyzer::display::utils::{visual_width, truncate_to_width};
5
6#[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
32pub 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, }
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 fn calculate_optimal_width(&self) -> usize {
64 let title_width = visual_width(&self.title) + 6; let mut max_content_width = 0;
66
67 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 let content_width_with_buffer = max_content_width + 4; let needed_width = content_width_with_buffer + 4;
82
83 let optimal_width = title_width.max(needed_width).max(self.min_width);
85 optimal_width.min(self.max_width)
86 }
87
88 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 let min_label_space = if line.label_colored { 25 } else { label_width };
97 min_label_space + 2 + value_width } else if !line.value.is_empty() {
99 value_width
101 } else if !line.label.is_empty() {
102 label_width
104 } else {
105 0
107 }
108 }
109
110 pub fn draw(&self) -> String {
112 let box_width = self.calculate_optimal_width();
113 let content_width = box_width - 4; let mut output = Vec::new();
116
117 output.push(self.draw_top(box_width));
119
120 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 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 let prefix_len = 3; let suffix_len = 1; let title_space = 1; 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 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 let label_display_width = visual_width(&line.label);
176 let value_display_width = visual_width(&line.value);
177
178 let content = if !line.label.is_empty() && !line.value.is_empty() {
180 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); if value_display_width <= remaining_space {
186 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 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 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 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 " ".repeat(content_width)
222 };
223
224 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}