1pub mod resizing;
2pub mod rows;
3pub mod util;
4
5use lipgloss::security::{safe_repeat, safe_str_repeat};
6use lipgloss::{Border, Style};
7use std::fmt;
8
9pub use resizing::{Resizer, ResizerColumn};
11pub use rows::{data_to_matrix, Data, Filter, StringData};
12
13pub const HEADER_ROW: i32 = -1;
16
17pub type StyleFunc = fn(row: i32, col: usize) -> Style;
37
38pub fn default_styles(_row: i32, _col: usize) -> Style {
40 Style::new()
41}
42
43pub fn header_row_style(row: i32, _col: usize) -> Style {
45 match row {
46 HEADER_ROW => Style::new().bold(true),
47 _ => Style::new(),
48 }
49}
50
51pub fn zebra_style(row: i32, _col: usize) -> Style {
53 use lipgloss::color::AdaptiveColor;
54 let table_row_even_bg = AdaptiveColor {
55 Light: "#F9FAFB",
56 Dark: "#1F1F1F",
57 };
58 match row {
59 HEADER_ROW => Style::new().bold(true),
60 _ if row % 2 == 0 => Style::new().background(table_row_even_bg),
61 _ => Style::new(),
62 }
63}
64
65pub fn minimal_style(row: i32, _col: usize) -> Style {
67 use lipgloss::color::AdaptiveColor;
68 let table_header_text = AdaptiveColor {
69 Light: "#171717",
70 Dark: "#F5F5F5",
71 };
72 let table_row_text = AdaptiveColor {
73 Light: "#262626",
74 Dark: "#FAFAFA",
75 };
76 let text_muted = AdaptiveColor {
77 Light: "#737373",
78 Dark: "#A3A3A3",
79 };
80 match row {
81 HEADER_ROW => Style::new().bold(true).foreground(table_header_text),
82 _ if row % 2 == 0 => Style::new().foreground(text_muted),
83 _ => Style::new().foreground(table_row_text),
84 }
85}
86
87pub fn column_style_func(column_styles: Vec<(usize, Style)>) -> impl Fn(i32, usize) -> Style {
90 move |row: i32, col: usize| {
91 let mut base_style = if row == HEADER_ROW {
93 Style::new().bold(true)
94 } else {
95 Style::new()
96 };
97
98 for &(target_col, ref style) in &column_styles {
100 if col == target_col {
101 base_style = base_style.inherit(style.clone());
103 break;
104 }
105 }
106
107 base_style
108 }
109}
110
111pub type BoxedStyleFunc = Box<dyn Fn(i32, usize) -> Style + Send + Sync>;
113
114pub struct Table {
116 style_func: StyleFunc,
117 boxed_style_func: Option<BoxedStyleFunc>,
118 border: Border,
119
120 border_top: bool,
121 border_bottom: bool,
122 border_left: bool,
123 border_right: bool,
124 border_header: bool,
125 border_column: bool,
126 border_row: bool,
127
128 border_style: Style,
129 headers: Vec<String>,
130 data: Box<dyn Data>,
131
132 width: i32,
133 height: i32,
134 use_manual_height: bool,
135 offset: usize,
136 wrap: bool,
137
138 widths: Vec<usize>,
140
141 heights: Vec<usize>,
143}
144
145impl Table {
146 pub fn new() -> Self {
150 Self {
151 style_func: default_styles,
152 boxed_style_func: None,
153 border: lipgloss::rounded_border(),
154 border_bottom: true,
155 border_column: true,
156 border_header: true,
157 border_left: true,
158 border_right: true,
159 border_top: true,
160 border_row: false,
161 border_style: Style::new(),
162 headers: Vec::new(),
163 data: Box::new(StringData::empty()),
164 width: 0,
165 height: 0,
166 use_manual_height: false,
167 offset: 0,
168 wrap: true,
169 widths: Vec::new(),
170 heights: Vec::new(),
171 }
172 }
173
174 pub fn clear_rows(mut self) -> Self {
176 self.data = Box::new(StringData::empty());
177 self
178 }
179
180 pub fn style_func(mut self, style: StyleFunc) -> Self {
182 self.style_func = style;
183 self.boxed_style_func = None; self
185 }
186
187 pub fn style_func_boxed<F>(mut self, style: F) -> Self
190 where
191 F: Fn(i32, usize) -> Style + Send + Sync + 'static,
192 {
193 self.boxed_style_func = Some(Box::new(style));
194 self
195 }
196
197 pub fn border(mut self, border: Border) -> Self {
199 self.border = border;
200 self
201 }
202
203 pub fn border_style(mut self, style: Style) -> Self {
205 self.border_style = style;
206 self
207 }
208
209 pub fn border_top(mut self, v: bool) -> Self {
211 self.border_top = v;
212 self
213 }
214
215 pub fn border_bottom(mut self, v: bool) -> Self {
217 self.border_bottom = v;
218 self
219 }
220
221 pub fn border_left(mut self, v: bool) -> Self {
223 self.border_left = v;
224 self
225 }
226
227 pub fn border_right(mut self, v: bool) -> Self {
229 self.border_right = v;
230 self
231 }
232
233 pub fn border_header(mut self, v: bool) -> Self {
235 self.border_header = v;
236 self
237 }
238
239 pub fn border_column(mut self, v: bool) -> Self {
241 self.border_column = v;
242 self
243 }
244
245 pub fn border_row(mut self, v: bool) -> Self {
247 self.border_row = v;
248 self
249 }
250
251 pub fn headers<I, S>(mut self, headers: I) -> Self
253 where
254 I: IntoIterator<Item = S>,
255 S: Into<String>,
256 {
257 self.headers = headers.into_iter().map(|s| s.into()).collect();
258 self
259 }
260
261 pub fn row<I, S>(mut self, row: I) -> Self
263 where
264 I: IntoIterator<Item = S>,
265 S: Into<String>,
266 {
267 let row_data: Vec<String> = row.into_iter().map(|s| s.into()).collect();
268
269 let matrix = data_to_matrix(self.data.as_ref());
271 let mut string_data = StringData::new(matrix);
272 string_data.append(row_data);
273 self.data = Box::new(string_data);
274 self
275 }
276
277 pub fn rows<I, J, S>(mut self, rows: I) -> Self
279 where
280 I: IntoIterator<Item = J>,
281 J: IntoIterator<Item = S>,
282 S: Into<String>,
283 {
284 for row in rows {
285 self = self.row(row);
286 }
287 self
288 }
289
290 pub fn data<D: Data + 'static>(mut self, data: D) -> Self {
292 self.data = Box::new(data);
293 self
294 }
295
296 pub fn width(mut self, w: i32) -> Self {
298 self.width = w;
299 self
300 }
301
302 pub fn height(mut self, h: i32) -> Self {
304 self.height = h;
305 self.use_manual_height = h > 0;
306 self
307 }
308
309 pub fn offset(mut self, o: usize) -> Self {
311 self.offset = o;
312 self
313 }
314
315 pub fn wrap(mut self, w: bool) -> Self {
317 self.wrap = w;
318 self
319 }
320
321 pub fn render(&mut self) -> String {
323 self.resize();
324 self.construct_table()
325 }
326
327 fn get_cell_style(&self, row: i32, col: usize) -> Style {
331 if let Some(ref boxed_func) = self.boxed_style_func {
332 boxed_func(row, col)
333 } else {
334 (self.style_func)(row, col)
335 }
336 }
337
338 fn resize(&mut self) {
339 let has_headers = !self.headers.is_empty();
340 let rows = data_to_matrix(self.data.as_ref());
341 let mut resizer = Resizer::new(self.width, self.height, self.headers.clone(), rows);
342 resizer.wrap = self.wrap;
343 resizer.border_column = self.border_column;
344 resizer.y_paddings = vec![vec![0; resizer.columns.len()]; resizer.all_rows.len()];
345
346 resizer.row_heights = resizer.default_row_heights();
348
349 for (i, row) in resizer.all_rows.iter().enumerate() {
350 if i >= resizer.y_paddings.len() {
351 resizer.y_paddings.push(vec![0; row.len()]);
352 }
353 if resizer.y_paddings[i].len() < row.len() {
354 resizer.y_paddings[i].resize(row.len(), 0);
355 }
356
357 for j in 0..row.len() {
358 if j >= resizer.columns.len() {
359 continue;
360 }
361
362 let row_index = if has_headers { i as i32 - 1 } else { i as i32 };
365 let style = self.get_cell_style(row_index, j);
366
367 let (top_margin, right_margin, bottom_margin, left_margin) = (
369 style.get_margin_top().max(0) as usize,
370 style.get_margin_right().max(0) as usize,
371 style.get_margin_bottom().max(0) as usize,
372 style.get_margin_left().max(0) as usize,
373 );
374 let (top_padding, right_padding, bottom_padding, left_padding) = (
375 style.get_padding_top().max(0) as usize,
376 style.get_padding_right().max(0) as usize,
377 style.get_padding_bottom().max(0) as usize,
378 style.get_padding_left().max(0) as usize,
379 );
380
381 let total_horizontal_padding =
382 left_margin + right_margin + left_padding + right_padding;
383 resizer.columns[j].x_padding =
384 resizer.columns[j].x_padding.max(total_horizontal_padding);
385
386 let width = style.get_width();
387 if width > 0 {
388 resizer.columns[j].fixed_width =
389 resizer.columns[j].fixed_width.max(width as usize);
390 }
391
392 let height = style.get_height();
393 if height > 0 {
394 resizer.row_heights[i] = resizer.row_heights[i].max(height as usize);
395 }
396
397 let total_vertical_padding =
398 top_margin + bottom_margin + top_padding + bottom_padding;
399 resizer.y_paddings[i][j] = total_vertical_padding;
400 }
401 }
402
403 if resizer.table_width <= 0 {
405 resizer.table_width = resizer.detect_table_width();
406 }
407
408 let (widths, heights) = resizer.optimized_widths();
409 self.widths = widths;
410 self.heights = heights;
411 }
412
413 fn construct_table(&self) -> String {
414 let mut result = String::new();
415 let has_headers = !self.headers.is_empty();
416 let _data_rows = self.data.rows();
417
418 if self.widths.is_empty() {
419 return result;
420 }
421
422 if self.border_top {
424 result.push_str(&self.construct_top_border());
425 result.push('\n');
426 }
427
428 if has_headers {
430 result.push_str(&self.construct_headers());
431 result.push('\n');
432
433 if self.border_header {
435 result.push_str(&self.construct_header_separator());
436 result.push('\n');
437 }
438 }
439
440 let available_lines = if self.use_manual_height && self.height > 0 {
442 let used_lines = if self.border_top { 1 } else { 0 }
443 + if has_headers { 1 } else { 0 }
444 + if has_headers && self.border_header {
445 1
446 } else {
447 0
448 }
449 + if self.border_bottom { 1 } else { 0 };
450 (self.height as usize).saturating_sub(used_lines)
451 } else {
452 usize::MAX
453 };
454
455 result.push_str(&self.construct_rows(available_lines));
456
457 if self.border_bottom {
459 if !result.is_empty() && !result.ends_with('\n') {
460 result.push('\n');
461 }
462 result.push_str(&self.construct_bottom_border());
463 }
464
465 result
466 }
467
468 fn construct_top_border(&self) -> String {
469 let mut border_parts = Vec::new();
470
471 if self.border_left {
472 border_parts.push(self.border.top_left.to_string());
473 }
474
475 for (i, &width) in self.widths.iter().enumerate() {
476 border_parts.push(safe_str_repeat(self.border.top, width));
477
478 if i < self.widths.len() - 1 && self.border_column {
479 border_parts.push(self.border.middle_top.to_string());
480 }
481 }
482
483 if self.border_right {
484 border_parts.push(self.border.top_right.to_string());
485 }
486
487 self.border_style.render(&border_parts.join(""))
488 }
489
490 fn construct_bottom_border(&self) -> String {
491 let mut border_parts = Vec::new();
492
493 if self.border_left {
494 border_parts.push(self.border.bottom_left.to_string());
495 }
496
497 for (i, &width) in self.widths.iter().enumerate() {
498 border_parts.push(safe_str_repeat(self.border.bottom, width));
499
500 if i < self.widths.len() - 1 && self.border_column {
501 border_parts.push(self.border.middle_bottom.to_string());
502 }
503 }
504
505 if self.border_right {
506 border_parts.push(self.border.bottom_right.to_string());
507 }
508
509 self.border_style.render(&border_parts.join(""))
510 }
511
512 fn construct_header_separator(&self) -> String {
513 let mut border_parts = Vec::new();
514
515 if self.border_left {
516 border_parts.push(self.border.middle_left.to_string());
517 }
518
519 for (i, &width) in self.widths.iter().enumerate() {
520 border_parts.push(safe_str_repeat(self.border.top, width));
521
522 if i < self.widths.len() - 1 && self.border_column {
523 border_parts.push(self.border.middle.to_string());
524 }
525 }
526
527 if self.border_right {
528 border_parts.push(self.border.middle_right.to_string());
529 }
530
531 self.border_style.render(&border_parts.join(""))
532 }
533
534 fn construct_headers(&self) -> String {
535 self.construct_row_content(&self.headers, HEADER_ROW)
536 }
537
538 fn construct_rows(&self, available_lines: usize) -> String {
539 let mut result = String::new();
540 let mut lines_used = 0;
541 let data_rows = self.data.rows();
542
543 for i in self.offset..data_rows {
544 if lines_used >= available_lines {
545 if i < data_rows {
547 result.push_str(&self.construct_overflow_row());
548 }
549 break;
550 }
551
552 let mut row_data = Vec::new();
554 for j in 0..self.data.columns() {
555 row_data.push(self.data.at(i, j));
556 }
557
558 result.push_str(&self.construct_row_content(&row_data, i as i32));
559 lines_used += self
560 .heights
561 .get(i + if !self.headers.is_empty() { 1 } else { 0 })
562 .unwrap_or(&1);
563
564 if self.border_row && i < data_rows - 1 && lines_used < available_lines {
566 result.push('\n');
567 result.push_str(&self.construct_row_separator());
568 lines_used += 1;
569 }
570
571 if i < data_rows - 1 {
572 result.push('\n');
573 }
574 }
575
576 result
577 }
578
579 fn construct_row_content(&self, row_data: &[String], row_index: i32) -> String {
580 let mut cell_parts = Vec::new();
581
582 if self.border_left {
583 cell_parts.push(self.border.left.to_string());
584 }
585
586 for (j, cell_content) in row_data.iter().enumerate() {
587 if j >= self.widths.len() {
588 break;
589 }
590
591 let cell_width = self.widths[j];
592 let style = self.get_cell_style(row_index, j);
593
594 let styled_content = self.style_cell_content(cell_content, cell_width, style);
596 cell_parts.push(styled_content);
597
598 if self.border_column && j < row_data.len() - 1 {
599 cell_parts.push(self.border.left.to_string());
600 }
601 }
602
603 if self.border_right {
604 cell_parts.push(self.border.right.to_string());
605 }
606
607 cell_parts.join("")
608 }
609
610 fn construct_row_separator(&self) -> String {
611 let mut border_parts = Vec::new();
612
613 if self.border_left {
614 border_parts.push(self.border.middle_left.to_string());
615 }
616
617 for (i, &width) in self.widths.iter().enumerate() {
618 border_parts.push(safe_str_repeat(self.border.top, width));
619
620 if i < self.widths.len() - 1 && self.border_column {
621 border_parts.push(self.border.middle.to_string());
622 }
623 }
624
625 if self.border_right {
626 border_parts.push(self.border.middle_right.to_string());
627 }
628
629 self.border_style.render(&border_parts.join(""))
630 }
631
632 fn construct_overflow_row(&self) -> String {
633 let mut cell_parts = Vec::new();
634
635 if self.border_left {
636 cell_parts.push(self.border.left.to_string());
637 }
638
639 for (i, &width) in self.widths.iter().enumerate() {
640 let ellipsis = "…".to_string();
641 let padding = safe_repeat(' ', width.saturating_sub(ellipsis.len()));
642 cell_parts.push(format!("{}{}", ellipsis, padding));
643
644 if self.border_column && i < self.widths.len() - 1 {
645 cell_parts.push(self.border.left.to_string());
646 }
647 }
648
649 if self.border_right {
650 cell_parts.push(self.border.right.to_string());
651 }
652
653 cell_parts.join("")
654 }
655
656 fn style_cell_content(&self, content: &str, width: usize, style: Style) -> String {
657 let fitted_content = if self.wrap {
659 self.wrap_cell_content(content, width)
660 } else {
661 self.truncate_cell_content(content, width)
662 };
663
664 style.width(width as i32).render(&fitted_content)
667 }
668
669 fn truncate_cell_content(&self, content: &str, width: usize) -> String {
670 let content_width = lipgloss::width(content);
671
672 if content_width > width {
673 if width == 0 {
675 return String::new();
676 } else if width == 1 {
677 return "…".to_string();
678 }
679
680 let chars: Vec<char> = content.chars().collect();
683 let mut result = String::new();
684 let mut current_width = 0;
685
686 for ch in chars {
687 let char_str = ch.to_string();
688 let char_width = lipgloss::width(&char_str);
689
690 if current_width + char_width + 1 > width {
691 break;
693 }
694
695 result.push(ch);
696 current_width += char_width;
697 }
698
699 result.push('…');
700 result
701 } else {
702 content.to_string()
703 }
704 }
705
706 fn wrap_cell_content(&self, content: &str, width: usize) -> String {
707 if width == 0 {
708 return String::new();
709 }
710
711 let mut wrapped_lines = Vec::new();
712
713 for line in content.lines() {
715 if line.is_empty() {
716 wrapped_lines.push(String::new());
717 continue;
718 }
719
720 let line_width = lipgloss::width(line);
722 if line_width <= width {
723 wrapped_lines.push(line.to_string());
724 } else {
725 wrapped_lines.extend(self.wrap_line_ansi_aware(line, width));
727 }
728 }
729
730 wrapped_lines.join("\n")
731 }
732
733 fn wrap_line_ansi_aware(&self, line: &str, width: usize) -> Vec<String> {
734 let words: Vec<&str> = line.split_whitespace().collect();
737 let mut lines = Vec::new();
738 let mut current_line = String::new();
739 let mut current_width = 0;
740
741 for word in words {
742 let word_width = lipgloss::width(word);
743
744 if !current_line.is_empty() && current_width + 1 + word_width > width {
746 lines.push(current_line);
747 current_line = word.to_string();
748 current_width = word_width;
749 } else if current_line.is_empty() {
750 current_line = word.to_string();
751 current_width = word_width;
752 } else {
753 current_line.push(' ');
754 current_line.push_str(word);
755 current_width += 1 + word_width;
756 }
757 }
758
759 if !current_line.is_empty() {
760 lines.push(current_line);
761 }
762
763 if lines.is_empty() {
764 lines.push(String::new());
765 }
766
767 lines
768 }
769}
770
771impl fmt::Display for Table {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 let mut table_copy = Table {
775 style_func: self.style_func,
776 boxed_style_func: None, border: self.border,
778 border_top: self.border_top,
779 border_bottom: self.border_bottom,
780 border_left: self.border_left,
781 border_right: self.border_right,
782 border_header: self.border_header,
783 border_column: self.border_column,
784 border_row: self.border_row,
785 border_style: self.border_style.clone(),
786 headers: self.headers.clone(),
787 data: Box::new(StringData::new(data_to_matrix(self.data.as_ref()))),
788 width: self.width,
789 height: self.height,
790 use_manual_height: self.use_manual_height,
791 offset: self.offset,
792 wrap: self.wrap,
793 widths: self.widths.clone(),
794 heights: self.heights.clone(),
795 };
796
797 write!(f, "{}", table_copy.render())
798 }
799}
800
801impl Default for Table {
802 fn default() -> Self {
803 Self::new()
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 #[test]
812 fn test_table_new() {
813 let table = Table::new();
814 assert_eq!(table.headers.len(), 0);
815 assert_eq!(table.data.rows(), 0);
816 assert_eq!(table.data.columns(), 0);
817 assert!(table.border_top);
818 assert!(table.border_bottom);
819 assert!(table.border_left);
820 assert!(table.border_right);
821 assert!(table.border_header);
822 assert!(table.border_column);
823 assert!(!table.border_row);
824 assert!(table.wrap);
825 }
826
827 #[test]
828 fn test_table_headers() {
829 let table = Table::new().headers(vec!["Name", "Age", "Location"]);
830 assert_eq!(table.headers.len(), 3);
831 assert_eq!(table.headers[0], "Name");
832 assert_eq!(table.headers[1], "Age");
833 assert_eq!(table.headers[2], "Location");
834 }
835
836 #[test]
837 fn test_table_rows() {
838 let table = Table::new()
839 .headers(vec!["Name", "Age"])
840 .row(vec!["Alice", "30"])
841 .row(vec!["Bob", "25"]);
842
843 assert_eq!(table.data.rows(), 2);
844 assert_eq!(table.data.columns(), 2);
845 assert_eq!(table.data.at(0, 0), "Alice");
846 assert_eq!(table.data.at(0, 1), "30");
847 assert_eq!(table.data.at(1, 0), "Bob");
848 assert_eq!(table.data.at(1, 1), "25");
849 }
850
851 #[test]
852 fn test_table_builder_pattern() {
853 let table = Table::new()
854 .border_top(false)
855 .border_bottom(false)
856 .width(80)
857 .height(10)
858 .wrap(false);
859
860 assert!(!table.border_top);
861 assert!(!table.border_bottom);
862 assert_eq!(table.width, 80);
863 assert_eq!(table.height, 10);
864 assert!(!table.wrap);
865 }
866
867 #[test]
868 fn test_table_clear_rows() {
869 let table = Table::new()
870 .row(vec!["A", "B"])
871 .row(vec!["C", "D"])
872 .clear_rows();
873
874 assert_eq!(table.data.rows(), 0);
875 assert_eq!(table.data.columns(), 0);
876 }
877
878 #[test]
879 fn test_table_rendering() {
880 let mut table = Table::new()
881 .headers(vec!["Name", "Age", "City"])
882 .row(vec!["Alice", "30", "New York"])
883 .row(vec!["Bob", "25", "London"]);
884
885 let output = table.render();
886 assert!(!output.is_empty());
887
888 assert!(output.contains("Name"));
890 assert!(output.contains("Alice"));
891 assert!(output.contains("Bob"));
892
893 assert!(output.contains("┌") || output.contains("╭")); }
896
897 #[test]
898 fn test_table_no_borders() {
899 let mut table = Table::new()
900 .headers(vec!["Name", "Age"])
901 .row(vec!["Alice", "30"])
902 .border_top(false)
903 .border_bottom(false)
904 .border_left(false)
905 .border_right(false)
906 .border_column(false);
907
908 let output = table.render();
909 assert!(!output.is_empty());
910 assert!(output.contains("Name"));
911 assert!(output.contains("Alice"));
912
913 assert!(!output.contains("┌"));
915 assert!(!output.contains("│"));
916 }
917
918 #[test]
919 fn test_table_width_constraint() {
920 let mut table = Table::new()
921 .headers(vec!["Name", "Age", "City"])
922 .row(vec!["Alice Johnson", "28", "New York"])
923 .row(vec!["Bob Smith", "35", "London"])
924 .width(25); let output = table.render();
927 assert!(!output.is_empty());
928
929 for line in output.lines() {
931 let line_width = lipgloss::width(line);
933 assert!(
934 line_width <= 25,
935 "Line '{}' has display width {} > 25",
936 line,
937 line_width
938 );
939 }
940 }
941
942 #[test]
943 fn test_comprehensive_table_demo() {
944 let mut table = Table::new()
945 .headers(vec!["Name", "Age", "City", "Occupation"])
946 .row(vec!["Alice Johnson", "28", "New York", "Software Engineer"])
947 .row(vec!["Bob Smith", "35", "London", "Product Manager"])
948 .row(vec!["Charlie Brown", "42", "Tokyo", "UX Designer"])
949 .row(vec!["Diana Prince", "30", "Paris", "Data Scientist"]);
950
951 let output = table.render();
952 println!("\n=== Comprehensive Table Demo ===");
953 println!("{}", output);
954
955 assert!(!output.is_empty());
956 assert!(output.contains("Alice Johnson"));
957 assert!(output.contains("Software Engineer"));
958
959 println!("\n=== No Borders Demo ===");
961 let mut no_border_table = Table::new()
962 .headers(vec!["Item", "Price"])
963 .row(vec!["Coffee", "$3.50"])
964 .row(vec!["Tea", "$2.25"])
965 .border_top(false)
966 .border_bottom(false)
967 .border_left(false)
968 .border_right(false)
969 .border_column(false)
970 .border_header(false);
971
972 println!("{}", no_border_table.render());
973
974 println!("\n=== Width Constrained Table ===");
976 let mut narrow_table = Table::new()
977 .headers(vec!["Product", "Description", "Price"])
978 .row(vec![
979 "MacBook Pro",
980 "Powerful laptop for developers",
981 "$2399",
982 ])
983 .row(vec![
984 "iPhone",
985 "Latest smartphone with amazing camera",
986 "$999",
987 ])
988 .width(40);
989
990 println!("{}", narrow_table.render());
991 }
992
993 #[test]
994 fn test_empty_table() {
995 let mut table = Table::new();
996 let output = table.render();
997
998 assert!(output.is_empty() || output.trim().is_empty());
1000 }
1001
1002 #[test]
1003 fn test_cell_styling_with_lipgloss() {
1004 use lipgloss::{
1005 color::{STATUS_ERROR, TEXT_MUTED},
1006 Style,
1007 };
1008
1009 let style_func = |row: i32, _col: usize| match row {
1010 HEADER_ROW => Style::new().bold(true).foreground(STATUS_ERROR),
1011 _ if row % 2 == 0 => Style::new().foreground(TEXT_MUTED),
1012 _ => Style::new().italic(true),
1013 };
1014
1015 let mut table = Table::new()
1016 .headers(vec!["Name", "Age", "City"])
1017 .row(vec!["Alice", "30", "New York"])
1018 .row(vec!["Bob", "25", "London"])
1019 .style_func(style_func);
1020
1021 let output = table.render();
1022 assert!(!output.is_empty());
1023 assert!(output.contains("Name")); assert!(output.contains("Alice")); assert!(output.contains("\\x1b[") || output.len() > 50); }
1029
1030 #[test]
1031 fn test_text_wrapping_functionality() {
1032 let mut table = Table::new()
1033 .headers(vec!["Short", "VeryLongContentThatShouldWrap"])
1034 .row(vec!["A", "This is a very long piece of content that should wrap across multiple lines when the table width is constrained"])
1035 .width(30)
1036 .wrap(true);
1037
1038 let output = table.render();
1039 assert!(!output.is_empty());
1040
1041 let line_count = output.lines().count();
1043 assert!(
1044 line_count > 3,
1045 "Expected more than 3 lines due to wrapping, got {}",
1046 line_count
1047 );
1048 }
1049
1050 #[test]
1051 fn test_text_truncation_functionality() {
1052 let mut table = Table::new()
1053 .headers(vec!["Short", "Long"])
1054 .row(vec![
1055 "A",
1056 "This is a very long piece of content that should be truncated",
1057 ])
1058 .width(25)
1059 .wrap(false); let output = table.render();
1062 assert!(!output.is_empty());
1063
1064 assert!(
1066 output.contains("…"),
1067 "Expected ellipsis for truncated content"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_ansi_aware_width_calculation() {
1073 use lipgloss::{Color, Style};
1074
1075 let styled_content = Style::new()
1077 .foreground(Color::from("#FF0000"))
1078 .bold(true)
1079 .render("Test");
1080
1081 let mut table = Table::new()
1082 .headers(vec!["Styled"])
1083 .row(vec![&styled_content])
1084 .width(10);
1085
1086 let output = table.render();
1087 assert!(!output.is_empty());
1088
1089 for line in output.lines() {
1092 let visual_width = lipgloss::width(line);
1093 assert!(
1094 visual_width <= 10,
1095 "Line has visual width {} > 10: '{}'",
1096 visual_width,
1097 line
1098 );
1099 }
1100 }
1101
1102 #[test]
1103 fn test_predefined_style_functions() {
1104 let mut table1 = Table::new()
1106 .headers(vec!["Name", "Age"])
1107 .row(vec!["Alice", "30"])
1108 .style_func(header_row_style);
1109
1110 let output1 = table1.render();
1111 assert!(!output1.is_empty());
1112 assert!(output1.contains("Name"));
1113
1114 let mut table2 = Table::new()
1116 .headers(vec!["Item", "Count"])
1117 .row(vec!["Apple", "5"])
1118 .row(vec!["Banana", "3"])
1119 .row(vec!["Cherry", "8"])
1120 .style_func(zebra_style);
1121
1122 let output2 = table2.render();
1123 assert!(!output2.is_empty());
1124 assert!(output2.contains("Item"));
1125
1126 let mut table3 = Table::new()
1128 .headers(vec!["Name"])
1129 .row(vec!["Test"])
1130 .style_func(minimal_style);
1131
1132 let output3 = table3.render();
1133 assert!(!output3.is_empty());
1134 assert!(output3.contains("Name"));
1135 }
1136
1137 #[test]
1138 fn test_boxed_style_function() {
1139 use lipgloss::{
1140 color::{STATUS_ERROR, STATUS_WARNING},
1141 Style,
1142 };
1143
1144 let error_color = STATUS_ERROR;
1146 let warning_color = STATUS_WARNING;
1147
1148 let mut table = Table::new()
1149 .headers(vec!["Status", "Message"])
1150 .row(vec!["ERROR", "Something went wrong"])
1151 .row(vec!["WARNING", "This is a warning"])
1152 .row(vec!["INFO", "Everything is fine"])
1153 .style_func_boxed(move |row: i32, col: usize| {
1154 if row == HEADER_ROW {
1155 Style::new().bold(true)
1156 } else if col == 0 {
1157 match row {
1160 0 => Style::new().foreground(error_color.clone()),
1161 1 => Style::new().foreground(warning_color.clone()),
1162 _ => Style::new(),
1163 }
1164 } else {
1165 Style::new()
1166 }
1167 });
1168
1169 let output = table.render();
1170 assert!(!output.is_empty());
1171 assert!(output.contains("Status"));
1172 assert!(output.contains("ERROR"));
1173 }
1174}