1use crate::components::ComponentDimensions;
8use crate::model::common::Bounds;
9use crate::table::{render_table, TableConfig, TableData, TablePagination};
10use crossterm::style::Color;
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct TableComponentConfig {
15 pub border_color: Color,
17 pub header_bg_color: Option<Color>,
19 pub header_text_color: Color,
21 pub row_text_color: Color,
23 pub highlight_bg_color: Option<Color>,
25 pub zebra_bg_color: Option<Color>,
27 pub page_info_format: String,
29 pub show_navigation: bool,
31 pub cell_padding: usize,
33}
34
35impl Default for TableComponentConfig {
36 fn default() -> Self {
37 Self {
38 border_color: Color::White,
39 header_bg_color: Some(Color::DarkBlue),
40 header_text_color: Color::White,
41 row_text_color: Color::White,
42 highlight_bg_color: Some(Color::DarkYellow),
43 zebra_bg_color: Some(Color::DarkGrey),
44 page_info_format: "Page {current} of {total} ({count} rows)".to_string(),
45 show_navigation: true,
46 cell_padding: 1,
47 }
48 }
49}
50
51pub struct TableComponent {
53 config: TableComponentConfig,
54}
55
56impl TableComponent {
57 pub fn new() -> Self {
59 Self {
60 config: TableComponentConfig::default(),
61 }
62 }
63
64 pub fn with_config(config: TableComponentConfig) -> Self {
66 Self { config }
67 }
68
69 pub fn with_colors(border_color: Color, header_color: Color, row_color: Color) -> Self {
71 Self {
72 config: TableComponentConfig {
73 border_color,
74 header_text_color: header_color,
75 row_text_color: row_color,
76 ..Default::default()
77 },
78 }
79 }
80
81 pub fn render(
83 &self,
84 table_data: &TableData,
85 table_config: &TableConfig,
86 bounds: &Bounds,
87 ) -> Vec<String> {
88 let mut adjusted_config = table_config.clone();
90 adjusted_config.width = ComponentDimensions::new(*bounds).content_bounds().width(); adjusted_config.height = ComponentDimensions::new(*bounds).content_bounds().height(); let table_content = render_table(table_data, &adjusted_config);
95
96 let mut lines: Vec<String> = table_content.lines().map(|s| s.to_string()).collect();
98
99 self.apply_component_styling(&mut lines, &adjusted_config);
101
102 self.fit_to_bounds(&mut lines, bounds);
104
105 lines
106 }
107
108 pub fn render_with_pagination(
110 &self,
111 table_data: &TableData,
112 table_config: &TableConfig,
113 bounds: &Bounds,
114 ) -> Vec<String> {
115 let mut lines = self.render(table_data, table_config, bounds);
116
117 if let Some(pagination) = &table_config.pagination {
119 if pagination.show_page_info {
120 let page_info = self.generate_page_info(table_data, pagination);
121 lines.push(page_info);
122
123 if self.config.show_navigation {
125 let nav_line = self.generate_navigation_line(pagination);
126 lines.push(nav_line);
127 }
128 }
129 }
130
131 lines
132 }
133
134 fn apply_component_styling(&self, lines: &mut [String], config: &TableConfig) {
136 if config.zebra_striping {
138 self.apply_zebra_styling(lines);
139 }
140
141 if let Some(highlight_row) = config.highlight_row {
143 self.apply_row_highlighting(lines, highlight_row);
144 }
145
146 if config.show_headers && !lines.is_empty() {
148 self.apply_header_styling(lines);
149 }
150 }
151
152 fn apply_zebra_styling(&self, lines: &mut [String]) {
154 let data_start = self.find_data_row_start(lines);
156
157 for (index, line) in lines.iter_mut().enumerate().skip(data_start) {
158 if self.is_data_row(line) && (index - data_start) % 2 == 1 {
159 *line = format!("\x1b[48;5;8m{}\x1b[0m", line); }
162 }
163 }
164
165 fn apply_row_highlighting(&self, lines: &mut [String], highlight_row: usize) {
167 let data_start = self.find_data_row_start(lines);
168 let target_index = data_start + highlight_row;
169
170 if let Some(line) = lines.get_mut(target_index) {
171 if self.is_data_row(line) {
172 *line = format!("\x1b[48;5;11m{}\x1b[0m", line); }
175 }
176 }
177
178 fn apply_header_styling(&self, lines: &mut [String]) {
180 for (index, line) in lines.iter_mut().enumerate() {
182 if self.is_header_row(line, index) {
183 *line = format!("\x1b[48;5;4m\x1b[97m{}\x1b[0m", line); break;
186 }
187 }
188 }
189
190 fn find_data_row_start(&self, lines: &[String]) -> usize {
192 for (index, line) in lines.iter().enumerate() {
193 if self.is_data_row(line) {
194 return index;
195 }
196 }
197 lines.len()
198 }
199
200 fn is_data_row(&self, line: &str) -> bool {
202 !line.chars().all(|c| matches!(c, '┌' | '┐' | '└' | '┘' | '├' | '┤' | '┬' | '┴' | '┼' | '─' | '│' | '+' | '-' | '|' | ' '))
203 && line.contains('│') && !line.trim().is_empty()
205 }
206
207 fn is_header_row(&self, line: &str, index: usize) -> bool {
209 index > 0 && self.is_data_row(line)
211 }
212
213 fn generate_page_info(&self, table_data: &TableData, pagination: &TablePagination) -> String {
215 let total_rows = table_data.rows.len();
216 let total_pages = total_rows.div_ceil(pagination.page_size);
217
218 self.config
219 .page_info_format
220 .replace("{current}", &pagination.current_page.to_string())
221 .replace("{total}", &total_pages.to_string())
222 .replace("{count}", &total_rows.to_string())
223 }
224
225 fn generate_navigation_line(&self, pagination: &TablePagination) -> String {
227 let mut nav_line = String::new();
228
229 if pagination.current_page > 1 {
231 nav_line.push_str("◄ Prev ");
232 } else {
233 nav_line.push_str(" ");
234 }
235
236 let start_page = pagination.current_page.saturating_sub(2).max(1);
238 let end_page = (pagination.current_page + 2).min(10); for page in start_page..=end_page {
241 if page == pagination.current_page {
242 nav_line.push_str(&format!("[{}] ", page));
243 } else {
244 nav_line.push_str(&format!("{} ", page));
245 }
246 }
247
248 nav_line.push_str(" Next ►");
250
251 nav_line
252 }
253
254 fn fit_to_bounds(&self, lines: &mut Vec<String>, bounds: &Bounds) {
256 for line in lines.iter_mut() {
258 if line.chars().count() > bounds.width() {
259 let truncated: String = line.chars().take(bounds.width()).collect();
260 *line = truncated;
261 if bounds.width() > 3 {
262 let ellipsis_pos = bounds.width().saturating_sub(3);
263 let mut chars: Vec<char> = line.chars().collect();
264 if chars.len() > ellipsis_pos {
265 chars.truncate(ellipsis_pos);
266 chars.extend("...".chars());
267 *line = chars.into_iter().collect();
268 }
269 }
270 }
271 }
272
273 if lines.len() > bounds.height() {
275 lines.truncate(bounds.height());
276 }
277
278 while lines.len() < bounds.height() {
280 lines.push(" ".repeat(bounds.width()));
281 }
282 }
283
284 pub fn get_config(&self) -> &TableComponentConfig {
286 &self.config
287 }
288
289 pub fn update_config(&mut self, config: TableComponentConfig) {
291 self.config = config;
292 }
293}
294
295impl Default for TableComponent {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::collections::HashMap;
305
306 fn create_test_table_data() -> TableData {
307 TableData {
308 headers: vec!["Name".to_string(), "Age".to_string(), "Status".to_string()],
309 rows: vec![
310 vec!["Alice".to_string(), "25".to_string(), "Active".to_string()],
311 vec!["Bob".to_string(), "30".to_string(), "Inactive".to_string()],
312 vec![
313 "Charlie".to_string(),
314 "35".to_string(),
315 "Active".to_string(),
316 ],
317 ],
318 metadata: HashMap::new(),
319 }
320 }
321
322 fn create_test_bounds() -> Bounds {
323 Bounds::new(0, 0, 50, 10)
324 }
325
326 #[test]
327 fn test_table_component_creation() {
328 let component = TableComponent::new();
329 assert_eq!(component.config.border_color, Color::White);
330 assert_eq!(component.config.header_text_color, Color::White);
331 }
332
333 #[test]
334 fn test_table_component_with_config() {
335 let config = TableComponentConfig {
336 border_color: Color::Red,
337 header_text_color: Color::Yellow,
338 row_text_color: Color::Green,
339 ..Default::default()
340 };
341
342 let component = TableComponent::with_config(config.clone());
343 assert_eq!(component.config.border_color, Color::Red);
344 assert_eq!(component.config.header_text_color, Color::Yellow);
345 assert_eq!(component.config.row_text_color, Color::Green);
346 }
347
348 #[test]
349 fn test_table_component_with_colors() {
350 let component = TableComponent::with_colors(Color::Blue, Color::Cyan, Color::Magenta);
351 assert_eq!(component.config.border_color, Color::Blue);
352 assert_eq!(component.config.header_text_color, Color::Cyan);
353 assert_eq!(component.config.row_text_color, Color::Magenta);
354 }
355
356 #[test]
357 fn test_basic_table_rendering() {
358 let component = TableComponent::new();
359 let table_data = create_test_table_data();
360 let table_config = TableConfig::default();
361 let bounds = create_test_bounds();
362
363 let lines = component.render(&table_data, &table_config, &bounds);
364
365 assert!(!lines.is_empty());
366 assert!(lines.len() <= bounds.height());
367
368 for line in &lines {
370 assert!(line.chars().count() <= bounds.width());
371 }
372 }
373
374 #[test]
375 fn test_table_rendering_with_pagination() {
376 let component = TableComponent::new();
377 let table_data = create_test_table_data();
378 let mut table_config = TableConfig::default();
379 table_config.pagination = Some(TablePagination {
380 page_size: 2,
381 current_page: 1,
382 show_page_info: true,
383 });
384 let bounds = create_test_bounds();
385
386 let lines = component.render_with_pagination(&table_data, &table_config, &bounds);
387
388 assert!(!lines.is_empty());
389
390 let has_page_info = lines.iter().any(|line| line.contains("Page"));
392 assert!(has_page_info);
393 }
394
395 #[test]
396 fn test_zebra_striping_detection() {
397 let component = TableComponent::new();
398
399 assert!(component.is_data_row("│ Alice │ 25 │ Active │"));
401 assert!(!component.is_data_row("├─────────┼─────┼──────────┤"));
402 assert!(!component.is_data_row("┌─────────┬─────┬──────────┐"));
403 }
404
405 #[test]
406 fn test_page_info_generation() {
407 let component = TableComponent::new();
408 let table_data = create_test_table_data();
409 let pagination = TablePagination {
410 page_size: 2,
411 current_page: 2,
412 show_page_info: true,
413 };
414
415 let page_info = component.generate_page_info(&table_data, &pagination);
416
417 assert!(page_info.contains("Page 2"));
418 assert!(page_info.contains("3 rows"));
419 }
420
421 #[test]
422 fn test_navigation_line_generation() {
423 let component = TableComponent::new();
424 let pagination = TablePagination {
425 page_size: 2,
426 current_page: 2,
427 show_page_info: true,
428 };
429
430 let nav_line = component.generate_navigation_line(&pagination);
431
432 assert!(nav_line.contains("◄ Prev"));
433 assert!(nav_line.contains("[2]")); assert!(nav_line.contains("Next ►"));
435 }
436
437 #[test]
438 fn test_bounds_fitting() {
439 let component = TableComponent::new();
440 let table_data = create_test_table_data();
441 let table_config = TableConfig::default();
442 let small_bounds = Bounds::new(0, 0, 20, 5);
443
444 let lines = component.render(&table_data, &table_config, &small_bounds);
445
446 assert_eq!(lines.len(), small_bounds.height());
447 for line in &lines {
448 assert!(line.chars().count() <= small_bounds.width());
449 }
450 }
451
452 #[test]
453 fn test_config_update() {
454 let mut component = TableComponent::new();
455 let original_color = component.config.border_color;
456
457 let mut new_config = component.config.clone();
458 new_config.border_color = Color::Red;
459
460 component.update_config(new_config);
461
462 assert_ne!(component.config.border_color, original_color);
463 assert_eq!(component.config.border_color, Color::Red);
464 }
465}