syncable_cli/analyzer/display/
box_drawer.rs1use crate::analyzer::display::{
4 get_color_adapter,
5 utils::{truncate_to_width, visual_width},
6};
7use colored::*;
8
9#[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
35pub 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, }
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 fn calculate_optimal_width(&self) -> usize {
68 let title_width = visual_width(&self.title) + 6; let mut max_content_width = 0;
70
71 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 let content_width_with_buffer = max_content_width + 4; let needed_width = content_width_with_buffer + 4;
86
87 let optimal_width = title_width.max(needed_width).max(self.min_width);
89 optimal_width.min(self.max_width)
90 }
91
92 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 let min_label_space = if line.label_colored { 25 } else { label_width };
101 min_label_space + 2 + value_width } else if !line.value.is_empty() {
103 value_width
105 } else if !line.label.is_empty() {
106 label_width
108 } else {
109 0
111 }
112 }
113
114 pub fn draw(&self) -> String {
116 let box_width = self.calculate_optimal_width();
117 let content_width = box_width - 4; let mut output = Vec::new();
120
121 output.push(self.draw_top(box_width));
123
124 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 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 let prefix_len = 3; let suffix_len = 1; let title_space = 1; 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 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 let label_display_width = visual_width(&line.label);
178 let value_display_width = visual_width(&line.value);
179
180 let content = if !line.label.is_empty() && !line.value.is_empty() {
182 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); if value_display_width <= remaining_space {
192 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 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 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 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 " ".repeat(content_width)
231 };
232
233 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}