lipgloss_table/
lib.rs

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
9// Re-export the main types and functions
10pub use resizing::{Resizer, ResizerColumn};
11pub use rows::{data_to_matrix, Data, Filter, StringData};
12
13/// HeaderRow denotes the header's row index used when rendering headers.
14/// Use this value when looking to customize header styles in StyleFunc.
15pub const HEADER_ROW: i32 = -1;
16
17/// StyleFunc is the style function that determines the style of a Cell.
18///
19/// It takes the row and column of the cell as an input and determines the
20/// lipgloss Style to use for that cell position.
21///
22/// Example:
23///
24/// ```rust
25/// use lipgloss::{Style, Color};
26/// use lipgloss_table::{Table, HEADER_ROW};
27///
28/// let style_func = |row: i32, col: usize| {
29///     match row {
30///         HEADER_ROW => Style::new().bold(true),
31///         _ if row % 2 == 0 => Style::new().foreground(Color::from("#888888")),
32///         _ => Style::new(),
33///     }
34/// };
35/// ```
36pub type StyleFunc = fn(row: i32, col: usize) -> Style;
37
38/// DefaultStyles is a StyleFunc that returns a new Style with no attributes.
39pub fn default_styles(_row: i32, _col: usize) -> Style {
40    Style::new()
41}
42
43/// HeaderRowStyle applies bold styling to header rows and default styling to data rows.
44pub 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
51/// ZebraStyle alternates background colors between rows for better readability.
52pub 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
65/// MinimalStyle provides subtle styling with header emphasis and muted alternating rows.
66pub 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
87/// ColumnStyleFunc creates a style function that applies specific styles to columns.
88/// Useful for highlighting specific columns like status, priority, or key fields.
89pub fn column_style_func(column_styles: Vec<(usize, Style)>) -> impl Fn(i32, usize) -> Style {
90    move |row: i32, col: usize| {
91        // Apply header styling
92        let mut base_style = if row == HEADER_ROW {
93            Style::new().bold(true)
94        } else {
95            Style::new()
96        };
97
98        // Apply column-specific styling
99        for &(target_col, ref style) in &column_styles {
100            if col == target_col {
101                // Inherit from the column style
102                base_style = base_style.inherit(style.clone());
103                break;
104            }
105        }
106
107        base_style
108    }
109}
110
111/// BoxedStyleFunc is a trait object for more flexible style functions.
112pub type BoxedStyleFunc = Box<dyn Fn(i32, usize) -> Style + Send + Sync>;
113
114/// Table is a type for rendering tables.
115pub 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 tracks the width of each column.
139    widths: Vec<usize>,
140
141    // heights tracks the height of each row.
142    heights: Vec<usize>,
143}
144
145impl Table {
146    /// Creates a new Table that can be modified through different attributes.
147    ///
148    /// By default, a table has rounded borders, basic styling, and no rows.
149    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    /// Clears the table rows.
175    pub fn clear_rows(mut self) -> Self {
176        self.data = Box::new(StringData::empty());
177        self
178    }
179
180    /// Sets the style for a cell based on its position (row, column).
181    pub fn style_func(mut self, style: StyleFunc) -> Self {
182        self.style_func = style;
183        self.boxed_style_func = None; // Clear any boxed style func
184        self
185    }
186
187    /// Sets a more flexible style function using a boxed closure.
188    /// This allows for more complex styling logic including captured variables.
189    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    /// Sets the table border.
198    pub fn border(mut self, border: Border) -> Self {
199        self.border = border;
200        self
201    }
202
203    /// Sets the style for the table border.
204    pub fn border_style(mut self, style: Style) -> Self {
205        self.border_style = style;
206        self
207    }
208
209    /// Sets whether or not the top border is rendered.
210    pub fn border_top(mut self, v: bool) -> Self {
211        self.border_top = v;
212        self
213    }
214
215    /// Sets whether or not the bottom border is rendered.
216    pub fn border_bottom(mut self, v: bool) -> Self {
217        self.border_bottom = v;
218        self
219    }
220
221    /// Sets whether or not the left border is rendered.
222    pub fn border_left(mut self, v: bool) -> Self {
223        self.border_left = v;
224        self
225    }
226
227    /// Sets whether or not the right border is rendered.
228    pub fn border_right(mut self, v: bool) -> Self {
229        self.border_right = v;
230        self
231    }
232
233    /// Sets whether or not the header separator is rendered.
234    pub fn border_header(mut self, v: bool) -> Self {
235        self.border_header = v;
236        self
237    }
238
239    /// Sets whether or not column separators are rendered.
240    pub fn border_column(mut self, v: bool) -> Self {
241        self.border_column = v;
242        self
243    }
244
245    /// Sets whether or not row separators are rendered.
246    pub fn border_row(mut self, v: bool) -> Self {
247        self.border_row = v;
248        self
249    }
250
251    /// Sets the table headers.
252    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    /// Adds a single row to the table.
262    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        // Convert current data to StringData - always create a new one from the matrix
270        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    /// Adds multiple rows to the table.
278    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    /// Sets the data source for the table.
291    pub fn data<D: Data + 'static>(mut self, data: D) -> Self {
292        self.data = Box::new(data);
293        self
294    }
295
296    /// Sets a fixed width for the table.
297    pub fn width(mut self, w: i32) -> Self {
298        self.width = w;
299        self
300    }
301
302    /// Sets a fixed height for the table.
303    pub fn height(mut self, h: i32) -> Self {
304        self.height = h;
305        self.use_manual_height = h > 0;
306        self
307    }
308
309    /// Sets the row offset for the table (for scrolling).
310    pub fn offset(mut self, o: usize) -> Self {
311        self.offset = o;
312        self
313    }
314
315    /// Sets whether text wrapping is enabled.
316    pub fn wrap(mut self, w: bool) -> Self {
317        self.wrap = w;
318        self
319    }
320
321    /// Renders the table to a string.
322    pub fn render(&mut self) -> String {
323        self.resize();
324        self.construct_table()
325    }
326
327    // Private methods for internal rendering
328
329    /// Get the appropriate style for a cell, using either the function pointer or boxed function.
330    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        // Calculate style-based padding for each cell
347        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                // Making sure we're passing the right index to the style function.
363                // The header row should be `-1` and the others should start from `0`.
364                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                // Extract margin and padding values
368                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        // Auto-detect table width if not specified
404        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        // Construct top border
423        if self.border_top {
424            result.push_str(&self.construct_top_border());
425            result.push('\n');
426        }
427
428        // Construct headers
429        if has_headers {
430            result.push_str(&self.construct_headers());
431            result.push('\n');
432
433            // Header separator
434            if self.border_header {
435                result.push_str(&self.construct_header_separator());
436                result.push('\n');
437            }
438        }
439
440        // Construct data rows
441        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        // Construct bottom border
458        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                // Add overflow indicator if we have more data
546                if i < data_rows {
547                    result.push_str(&self.construct_overflow_row());
548                }
549                break;
550            }
551
552            // Get row data
553            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            // Add row separator if needed
565            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            // Apply cell styling and fit to width
595            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        // Handle content wrapping if needed
658        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        // Apply the lipgloss style to the content
665        // The style should handle its own width constraints, so we apply it directly
666        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            // Truncate with ellipsis, handling ANSI sequences properly
674            if width == 0 {
675                return String::new();
676            } else if width == 1 {
677                return "…".to_string();
678            }
679
680            // For ANSI-aware truncation, we need to be more careful
681            // For now, use a simple approach that may not be perfect with ANSI sequences
682            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                    // +1 for ellipsis
692                    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        // Handle existing line breaks
714        for line in content.lines() {
715            if line.is_empty() {
716                wrapped_lines.push(String::new());
717                continue;
718            }
719
720            // Use lipgloss width which handles ANSI sequences
721            let line_width = lipgloss::width(line);
722            if line_width <= width {
723                wrapped_lines.push(line.to_string());
724            } else {
725                // Need to wrap this line - use ANSI-aware wrapping
726                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        // For now, use a simple word-based wrapping that preserves ANSI sequences
735        // This could be enhanced to use lipgloss's word wrapping utilities if available
736        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 adding this word would exceed width, start a new line
745            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        // Need to create a mutable copy for rendering since fmt doesn't allow mutable self
774        let mut table_copy = Table {
775            style_func: self.style_func,
776            boxed_style_func: None, // Cannot clone boxed closures easily
777            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        // Should contain the header and data
889        assert!(output.contains("Name"));
890        assert!(output.contains("Alice"));
891        assert!(output.contains("Bob"));
892
893        // Should have borders by default
894        assert!(output.contains("┌") || output.contains("╭")); // Top-left corner
895    }
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        // Should not contain border characters
914        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); // Force narrow width
925
926        let output = table.render();
927        assert!(!output.is_empty());
928
929        // Each line should respect the width constraint (using display width, not character count)
930        for line in output.lines() {
931            // Use lipgloss width which handles ANSI sequences properly
932            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        // Test different border styles
960        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        // Test width constraint
975        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        // Empty table should produce minimal output
999        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")); // Headers should be present
1024        assert!(output.contains("Alice")); // Data should be present
1025
1026        // Since we're applying styles, there should be ANSI escape sequences
1027        assert!(output.contains("\\x1b[") || output.len() > 50); // Either ANSI codes or substantial content
1028    }
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        // With wrapping enabled and constrained width, we should get multiple lines
1042        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); // Disable wrapping to force truncation
1060
1061        let output = table.render();
1062        assert!(!output.is_empty());
1063
1064        // Should contain ellipsis indicating truncation
1065        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        // Create content with ANSI sequences
1076        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        // The table should handle ANSI sequences correctly in width calculations
1090        // The visual width should be respected, not the character count
1091        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        // Test header_row_style
1105        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        // Test zebra_style
1115        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        // Test minimal_style
1127        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        // Create a closure that captures variables
1145        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                    // Style the status column based on content
1158                    // Note: In a real implementation, you'd have access to the cell content
1159                    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}