lipgloss_table/lib.rs
1//! # lipgloss-table
2//!
3//! A flexible and powerful table rendering library for terminal applications.
4//!
5//! This crate provides a comprehensive table rendering system with advanced styling,
6//! layout options, and terminal-aware text handling. It's designed to work seamlessly
7//! with the `lipgloss` styling library to create beautiful terminal user interfaces.
8//!
9//! ## Features
10//!
11//! - **Flexible Table Construction**: Build tables using a fluent builder pattern
12//! - **Advanced Styling**: Apply different styles to headers, rows, and individual cells
13//! - **Border Customization**: Control all aspects of table borders and separators
14//! - **Responsive Layout**: Automatic width detection and content wrapping/truncation
15//! - **Height Constraints**: Set maximum heights with automatic scrolling and overflow indicators
16//! - **ANSI-Aware**: Proper handling of ANSI escape sequences in content
17//! - **Memory Safe**: Built-in protections against memory exhaustion from malicious input
18//!
19//! ## Quick Start
20//!
21//! ```rust
22//! use lipgloss_table::{Table, HEADER_ROW, header_row_style};
23//! use lipgloss::{Style, Color};
24//!
25//! // Create a simple table
26//! let mut table = Table::new()
27//! .headers(vec!["Name", "Age", "City"])
28//! .row(vec!["Alice", "30", "New York"])
29//! .row(vec!["Bob", "25", "London"])
30//! .style_func(header_row_style);
31//!
32//! println!("{}", table.render());
33//! ```
34//!
35//! ## Advanced Usage
36//!
37//! ### Custom Styling
38//!
39//! ```rust
40//! use lipgloss_table::{Table, HEADER_ROW};
41//! use lipgloss::{Style, Color};
42//!
43//! let style_func = |row: i32, col: usize| {
44//! match row {
45//! HEADER_ROW => Style::new().bold(true).foreground(Color::from("#FFFFFF")),
46//! _ if row % 2 == 0 => Style::new().background(Color::from("#F0F0F0")),
47//! _ => Style::new(),
48//! }
49//! };
50//!
51//! let mut table = Table::new()
52//! .headers(vec!["Product", "Price", "Stock"])
53//! .rows(vec![
54//! vec!["Widget A", "$10.99", "50"],
55//! vec!["Widget B", "$15.99", "25"],
56//! vec!["Widget C", "$8.99", "100"],
57//! ])
58//! .style_func(style_func)
59//! .width(40);
60//!
61//! println!("{}", table.render());
62//! ```
63//!
64//! ### Height-Constrained Tables with Scrolling
65//!
66//! ```rust
67//! use lipgloss_table::Table;
68//!
69//! let mut table = Table::new()
70//! .headers(vec!["Item", "Description"])
71//! .height(10) // Limit table to 10 lines
72//! .offset(5); // Skip first 5 rows (scrolling)
73//!
74//! // Add many rows...
75//! for i in 1..=100 {
76//! table = table.row(vec![format!("Item {}", i), "Description".to_string()]);
77//! }
78//!
79//! println!("{}", table.render());
80//! println!("Table height: {}", table.compute_height());
81//! ```
82//!
83//! ## Predefined Style Functions
84//!
85//! The crate includes several predefined styling functions:
86//!
87//! - [`default_styles`]: Basic styling with no attributes
88//! - [`header_row_style`]: Bold headers with default data rows
89//! - [`zebra_style`]: Alternating row backgrounds for better readability
90//! - [`minimal_style`]: Subtle styling with muted colors
91//! - [`column_style_func`]: Factory for creating column-specific styles
92//!
93//! ## Integration with lipgloss
94//!
95//! This crate is designed to work seamlessly with the `lipgloss` styling library.
96//! All styling functions receive `lipgloss::Style` objects and can use the full
97//! range of lipgloss features including colors, borders, padding, and alignment.
98
99#![warn(missing_docs)]
100
101/// Internal module for table resizing logic and column width calculations.
102pub mod resizing;
103
104/// Internal module for data handling and row management.
105pub mod rows;
106
107/// Internal utility functions for table operations.
108pub mod util;
109
110use lipgloss::security::{safe_repeat, safe_str_repeat};
111use lipgloss::{Border, Style};
112use std::fmt;
113
114// Re-export the main types and functions
115pub use resizing::{Resizer, ResizerColumn};
116pub use rows::{data_to_matrix, Data, Filter, StringData};
117
118/// HeaderRow denotes the header's row index used when rendering headers.
119/// Use this value when looking to customize header styles in StyleFunc.
120pub const HEADER_ROW: i32 = -1;
121
122/// StyleFunc is the style function that determines the style of a Cell.
123///
124/// It takes the row and column of the cell as an input and determines the
125/// lipgloss Style to use for that cell position.
126///
127/// Example:
128///
129/// ```rust
130/// use lipgloss::{Style, Color};
131/// use lipgloss_table::{Table, HEADER_ROW};
132///
133/// let style_func = |row: i32, col: usize| {
134/// match row {
135/// HEADER_ROW => Style::new().bold(true),
136/// _ if row % 2 == 0 => Style::new().foreground(Color::from("#888888")),
137/// _ => Style::new(),
138/// }
139/// };
140/// ```
141pub type StyleFunc = fn(row: i32, col: usize) -> Style;
142
143/// A basic style function that applies no formatting to any cells.
144///
145/// This function serves as the default styling approach, returning a plain
146/// `Style` with no attributes for all table cells. It's useful as a starting
147/// point or when you want completely unstyled table content.
148///
149/// # Arguments
150///
151/// * `_row` - The row index (unused, but required by the `StyleFunc` signature)
152/// * `_col` - The column index (unused, but required by the `StyleFunc` signature)
153///
154/// # Returns
155///
156/// A new `Style` instance with no formatting applied.
157///
158/// # Examples
159///
160/// ```rust
161/// use lipgloss_table::{Table, default_styles};
162///
163/// let mut table = Table::new()
164/// .headers(vec!["Name", "Age"])
165/// .row(vec!["Alice", "30"])
166/// .style_func(default_styles);
167///
168/// println!("{}", table.render());
169/// ```
170pub fn default_styles(_row: i32, _col: usize) -> Style {
171 Style::new()
172}
173
174/// A style function that makes header rows bold while leaving data rows unstyled.
175///
176/// This function provides a simple but effective styling approach by applying
177/// bold formatting to header rows (identified by `HEADER_ROW`) while leaving
178/// all data rows with default styling. This creates a clear visual distinction
179/// between headers and content.
180///
181/// # Arguments
182///
183/// * `row` - The row index to style (headers use `HEADER_ROW` constant)
184/// * `_col` - The column index (unused, but required by the `StyleFunc` signature)
185///
186/// # Returns
187///
188/// * Bold `Style` for header rows (`HEADER_ROW`)
189/// * Default `Style` for all data rows
190///
191/// # Examples
192///
193/// ```rust
194/// use lipgloss_table::{Table, header_row_style};
195///
196/// let mut table = Table::new()
197/// .headers(vec!["Product", "Price", "Stock"])
198/// .row(vec!["Widget A", "$10.99", "50"])
199/// .row(vec!["Widget B", "$15.99", "25"])
200/// .style_func(header_row_style);
201///
202/// println!("{}", table.render());
203/// ```
204pub fn header_row_style(row: i32, _col: usize) -> Style {
205 match row {
206 HEADER_ROW => Style::new().bold(true),
207 _ => Style::new(),
208 }
209}
210
211/// A style function that creates alternating row backgrounds (zebra striping) for improved readability.
212///
213/// This function applies a "zebra stripe" pattern to table rows, alternating between
214/// a default background and a subtle background color for even-numbered rows. The header
215/// row receives bold styling. The background colors are adaptive, changing based on
216/// whether the terminal has a light or dark theme.
217///
218/// # Row Pattern
219///
220/// * Header row: Bold text
221/// * Even data rows (0, 2, 4...): Subtle background color
222/// * Odd data rows (1, 3, 5...): Default background
223///
224/// # Arguments
225///
226/// * `row` - The row index to style (headers use `HEADER_ROW` constant)
227/// * `_col` - The column index (unused, but required by the `StyleFunc` signature)
228///
229/// # Returns
230///
231/// * Bold `Style` for header rows
232/// * `Style` with subtle background for even data rows
233/// * Default `Style` for odd data rows
234///
235/// # Examples
236///
237/// ```rust
238/// use lipgloss_table::{Table, zebra_style};
239///
240/// let mut table = Table::new()
241/// .headers(vec!["Name", "Score", "Grade"])
242/// .row(vec!["Alice", "95", "A"]) // Even row - background color
243/// .row(vec!["Bob", "87", "B"]) // Odd row - default
244/// .row(vec!["Charlie", "92", "A"]) // Even row - background color
245/// .style_func(zebra_style);
246///
247/// println!("{}", table.render());
248/// ```
249#[allow(unknown_lints, clippy::manual_is_multiple_of)]
250pub fn zebra_style(row: i32, _col: usize) -> Style {
251 use lipgloss::color::AdaptiveColor;
252 let table_row_even_bg = AdaptiveColor {
253 Light: "#F9FAFB",
254 Dark: "#1F1F1F",
255 };
256 match row {
257 HEADER_ROW => Style::new().bold(true),
258 _ if row % 2 == 0 => Style::new().background(table_row_even_bg),
259 _ => Style::new(),
260 }
261}
262
263/// A subtle style function that provides minimal, professional-looking table styling.
264///
265/// This function creates a clean, minimal aesthetic using muted colors and subtle
266/// contrast. Headers are bold with high-contrast text, while data rows alternate
267/// between normal and muted text colors. All colors are adaptive to work well
268/// with both light and dark terminal themes.
269///
270/// # Row Pattern
271///
272/// * Header row: Bold text with high-contrast color
273/// * Even data rows (0, 2, 4...): Muted text color
274/// * Odd data rows (1, 3, 5...): Normal text color
275///
276/// # Arguments
277///
278/// * `row` - The row index to style (headers use `HEADER_ROW` constant)
279/// * `_col` - The column index (unused, but required by the `StyleFunc` signature)
280///
281/// # Returns
282///
283/// * Bold `Style` with high-contrast foreground for headers
284/// * `Style` with muted foreground for even data rows
285/// * `Style` with normal foreground for odd data rows
286///
287/// # Examples
288///
289/// ```rust
290/// use lipgloss_table::{Table, minimal_style};
291///
292/// let mut table = Table::new()
293/// .headers(vec!["Status", "Task", "Priority"])
294/// .row(vec!["Done", "Fix bug #123", "High"])
295/// .row(vec!["In Progress", "Add new feature", "Medium"])
296/// .row(vec!["Todo", "Update docs", "Low"])
297/// .style_func(minimal_style);
298///
299/// println!("{}", table.render());
300/// ```
301#[allow(unknown_lints, clippy::manual_is_multiple_of)]
302pub fn minimal_style(row: i32, _col: usize) -> Style {
303 use lipgloss::color::AdaptiveColor;
304 let table_header_text = AdaptiveColor {
305 Light: "#171717",
306 Dark: "#F5F5F5",
307 };
308 let table_row_text = AdaptiveColor {
309 Light: "#262626",
310 Dark: "#FAFAFA",
311 };
312 let text_muted = AdaptiveColor {
313 Light: "#737373",
314 Dark: "#A3A3A3",
315 };
316 match row {
317 HEADER_ROW => Style::new().bold(true).foreground(table_header_text),
318 _ if row % 2 == 0 => Style::new().foreground(text_muted),
319 _ => Style::new().foreground(table_row_text),
320 }
321}
322
323/// Creates a style function that applies column-specific styling to table cells.
324///
325/// This function factory generates a style function that can apply different styles
326/// to specific columns while maintaining consistent header styling. It's particularly
327/// useful for highlighting important columns like status indicators, priority levels,
328/// or key data fields.
329///
330/// # Arguments
331///
332/// * `column_styles` - A vector of tuples where each tuple contains:
333/// - `usize`: The zero-based column index to style
334/// - `Style`: The lipgloss style to apply to that column
335///
336/// # Returns
337///
338/// A closure that implements the `StyleFunc` signature, applying:
339/// * Bold styling to all header row cells
340/// * Column-specific styles to matching data cells
341/// * Default styling to other cells
342///
343/// # Examples
344///
345/// ```rust
346/// use lipgloss_table::{Table, column_style_func};
347/// use lipgloss::{Style, Color};
348///
349/// // Define styles for specific columns
350/// let column_styles = vec![
351/// (0, Style::new().foreground(Color::from("#00FF00"))), // Green for first column
352/// (2, Style::new().bold(true).foreground(Color::from("#FF0000"))), // Bold red for third column
353/// ];
354///
355/// let mut table = Table::new()
356/// .headers(vec!["Status", "Task", "Priority", "Assignee"])
357/// .row(vec!["Active", "Fix bug", "High", "Alice"])
358/// .row(vec!["Done", "Add feature", "Medium", "Bob"])
359/// .style_func_boxed(Box::new(column_style_func(column_styles)));
360///
361/// println!("{}", table.render());
362/// ```
363pub fn column_style_func(column_styles: Vec<(usize, Style)>) -> impl Fn(i32, usize) -> Style {
364 move |row: i32, col: usize| {
365 // Apply header styling
366 let mut base_style = if row == HEADER_ROW {
367 Style::new().bold(true)
368 } else {
369 Style::new()
370 };
371
372 // Apply column-specific styling
373 for &(target_col, ref style) in &column_styles {
374 if col == target_col {
375 // Inherit from the column style
376 base_style = base_style.inherit(style.clone());
377 break;
378 }
379 }
380
381 base_style
382 }
383}
384
385/// A trait object type for flexible style functions that can capture their environment.
386///
387/// This type allows for more complex styling logic that can capture variables
388/// from the surrounding scope, unlike the simple function pointer `StyleFunc`.
389/// It's particularly useful when you need to reference external data or state
390/// in your styling logic.
391///
392/// # Examples
393///
394/// ```rust
395/// use lipgloss_table::{Table, BoxedStyleFunc, HEADER_ROW};
396/// use lipgloss::{Style, Color};
397///
398/// let error_color = Color::from("#FF0000");
399/// let warning_color = Color::from("#FFAA00");
400///
401/// let boxed_style: BoxedStyleFunc = Box::new(move |row: i32, col: usize| {
402/// match (row, col) {
403/// (HEADER_ROW, _) => Style::new().bold(true),
404/// (_, 0) => Style::new().foreground(error_color.clone()),
405/// (_, 1) => Style::new().foreground(warning_color.clone()),
406/// _ => Style::new(),
407/// }
408/// });
409/// ```
410pub type BoxedStyleFunc = Box<dyn Fn(i32, usize) -> Style + Send + Sync>;
411
412/// A flexible table renderer with advanced styling and layout capabilities.
413///
414/// `Table` provides a comprehensive solution for rendering tabular data in terminal
415/// applications. It supports a wide range of customization options including borders,
416/// styling functions, width/height constraints, text wrapping, and scrolling.
417///
418/// # Features
419///
420/// - **Flexible Content**: Supports headers, multiple data rows, and various data sources
421/// - **Advanced Styling**: Cell-by-cell styling with function-based or closure-based approaches
422/// - **Border Control**: Granular control over all border elements (top, bottom, sides, separators)
423/// - **Layout Management**: Width/height constraints with automatic wrapping and truncation
424/// - **Scrolling Support**: Offset-based scrolling for large datasets
425/// - **ANSI-Aware**: Proper handling of ANSI escape sequences in cell content
426/// - **Memory Safe**: Built-in protections against excessive memory usage
427///
428/// # Builder Pattern
429///
430/// `Table` uses a fluent builder pattern where each method returns `Self`, allowing
431/// for method chaining. Call `render()` to generate the final string representation.
432///
433/// # Examples
434///
435/// ## Basic Table
436///
437/// ```rust
438/// use lipgloss_table::Table;
439///
440/// let mut table = Table::new()
441/// .headers(vec!["Name", "Age", "City"])
442/// .row(vec!["Alice", "30", "New York"])
443/// .row(vec!["Bob", "25", "London"]);
444///
445/// println!("{}", table.render());
446/// ```
447///
448/// ## Styled Table with Width Constraint
449///
450/// ```rust
451/// use lipgloss_table::{Table, zebra_style};
452/// use lipgloss::rounded_border;
453///
454/// let mut table = Table::new()
455/// .headers(vec!["Product", "Description", "Price"])
456/// .rows(vec![
457/// vec!["Widget A", "A useful widget for all your needs", "$19.99"],
458/// vec!["Widget B", "An even more useful widget", "$29.99"],
459/// ])
460/// .width(50)
461/// .border(rounded_border())
462/// .style_func(zebra_style);
463///
464/// println!("{}", table.render());
465/// ```
466///
467/// ## Scrollable Table with Height Limit
468///
469/// ```rust
470/// use lipgloss_table::Table;
471///
472/// let mut large_table = Table::new()
473/// .headers(vec!["ID", "Data"])
474/// .height(10) // Limit to 10 lines total
475/// .offset(20); // Start from row 20 (scrolling)
476///
477/// // Add many rows...
478/// for i in 1..=1000 {
479/// large_table = large_table.row(vec![i.to_string(), format!("Data {}", i)]);
480/// }
481///
482/// println!("{}", large_table.render());
483/// println!("Actual height: {}", large_table.compute_height());
484/// ```
485pub struct Table {
486 style_func: StyleFunc,
487 boxed_style_func: Option<BoxedStyleFunc>,
488 border: Border,
489
490 border_top: bool,
491 border_bottom: bool,
492 border_left: bool,
493 border_right: bool,
494 border_header: bool,
495 border_column: bool,
496 border_row: bool,
497
498 border_style: Style,
499 headers: Vec<String>,
500 data: Box<dyn Data>,
501
502 width: i32,
503 height: i32,
504 use_manual_height: bool,
505 offset: usize,
506 wrap: bool,
507
508 // widths tracks the width of each column.
509 widths: Vec<usize>,
510
511 // heights tracks the height of each row.
512 heights: Vec<usize>,
513}
514
515impl Table {
516 /// Creates a new `Table` with default settings and no content.
517 ///
518 /// The default table configuration includes:
519 /// - Rounded borders (`lipgloss::rounded_border()`)
520 /// - All border sides enabled (top, bottom, left, right, header separator, column separators)
521 /// - Row separators disabled
522 /// - Text wrapping enabled
523 /// - No width or height constraints
524 /// - No content (headers or data rows)
525 /// - Basic styling function (`default_styles`)
526 ///
527 /// # Returns
528 ///
529 /// A new `Table` instance ready for configuration via the builder pattern.
530 ///
531 /// # Examples
532 ///
533 /// ```rust
534 /// use lipgloss_table::Table;
535 ///
536 /// let table = Table::new();
537 /// assert_eq!(table.compute_height(), 2); // Just top and bottom borders
538 /// ```
539 ///
540 /// ```rust
541 /// use lipgloss_table::Table;
542 ///
543 /// let mut table = Table::new()
544 /// .headers(vec!["Column 1", "Column 2"])
545 /// .row(vec!["Data 1", "Data 2"]);
546 ///
547 /// println!("{}", table.render());
548 /// ```
549 pub fn new() -> Self {
550 Self {
551 style_func: default_styles,
552 boxed_style_func: None,
553 border: lipgloss::rounded_border(),
554 border_bottom: true,
555 border_column: true,
556 border_header: true,
557 border_left: true,
558 border_right: true,
559 border_top: true,
560 border_row: false,
561 border_style: Style::new(),
562 headers: Vec::new(),
563 data: Box::new(StringData::empty()),
564 width: 0,
565 height: 0,
566 use_manual_height: false,
567 offset: 0,
568 wrap: true,
569 widths: Vec::new(),
570 heights: Vec::new(),
571 }
572 }
573
574 /// Removes all data rows from the table while preserving headers and settings.
575 ///
576 /// This method clears only the table's data content, leaving headers, styling,
577 /// borders, and other configuration unchanged. It's useful for reusing a
578 /// configured table with different data.
579 ///
580 /// # Returns
581 ///
582 /// The `Table` instance with all data rows removed, enabling method chaining.
583 ///
584 /// # Examples
585 ///
586 /// ```rust
587 /// use lipgloss_table::Table;
588 ///
589 /// let mut table = Table::new()
590 /// .headers(vec!["Name", "Age"])
591 /// .row(vec!["Alice", "30"])
592 /// .row(vec!["Bob", "25"])
593 /// .clear_rows()
594 /// .row(vec!["Charlie", "35"]);
595 ///
596 /// // Table now has headers and only Charlie's row
597 /// println!("{}", table.render());
598 /// ```
599 pub fn clear_rows(mut self) -> Self {
600 self.data = Box::new(StringData::empty());
601 self
602 }
603
604 /// Sets a simple function-based styling function for table cells.
605 ///
606 /// This method accepts a function pointer that determines the style for each
607 /// cell based on its row and column position. The function receives the row
608 /// index (with `HEADER_ROW` for headers) and column index, returning a
609 /// `Style` to apply to that cell.
610 ///
611 /// Using this method will clear any previously set boxed style function.
612 ///
613 /// # Arguments
614 ///
615 /// * `style` - A function that takes `(row: i32, col: usize) -> Style`
616 ///
617 /// # Returns
618 ///
619 /// The `Table` instance with the style function applied, enabling method chaining.
620 ///
621 /// # Examples
622 ///
623 /// ```rust
624 /// use lipgloss_table::{Table, HEADER_ROW, header_row_style};
625 /// use lipgloss::{Style, Color};
626 ///
627 /// // Using a predefined style function
628 /// let table1 = Table::new()
629 /// .headers(vec!["Name", "Age"])
630 /// .style_func(header_row_style);
631 ///
632 /// // Using a custom style function
633 /// let custom_style = |row: i32, col: usize| {
634 /// match (row, col) {
635 /// (HEADER_ROW, _) => Style::new().bold(true),
636 /// (_, 0) => Style::new().foreground(Color::from("#00FF00")),
637 /// _ => Style::new(),
638 /// }
639 /// };
640 ///
641 /// let table2 = Table::new()
642 /// .headers(vec!["Status", "Message"])
643 /// .style_func(custom_style);
644 /// ```
645 pub fn style_func(mut self, style: StyleFunc) -> Self {
646 self.style_func = style;
647 self.boxed_style_func = None; // Clear any boxed style func
648 self
649 }
650
651 /// Sets a flexible closure-based styling function that can capture variables from its environment.
652 ///
653 /// This method allows for more complex styling logic than `style_func` by accepting
654 /// a closure that can capture variables from the surrounding scope. This is useful
655 /// when your styling logic needs to reference external data, configuration, or state.
656 ///
657 /// The closure is boxed and stored, allowing it to outlive the current scope while
658 /// maintaining access to captured variables.
659 ///
660 /// # Type Parameters
661 ///
662 /// * `F` - A closure type that implements `Fn(i32, usize) -> Style + Send + Sync + 'static`
663 ///
664 /// # Arguments
665 ///
666 /// * `style` - A closure that takes `(row: i32, col: usize) -> Style`
667 ///
668 /// # Returns
669 ///
670 /// The `Table` instance with the boxed style function applied, enabling method chaining.
671 ///
672 /// # Examples
673 ///
674 /// ```rust
675 /// use lipgloss_table::{Table, HEADER_ROW};
676 /// use lipgloss::{Style, Color};
677 ///
678 /// // Capture colors from the environment
679 /// let error_color = Color::from("#FF0000");
680 /// let success_color = Color::from("#00FF00");
681 /// let warning_color = Color::from("#FFAA00");
682 ///
683 /// let mut table = Table::new()
684 /// .headers(vec!["Status", "Message", "Code"])
685 /// .row(vec!["Error", "Something failed", "500"])
686 /// .row(vec!["Success", "All good", "200"])
687 /// .row(vec!["Warning", "Be careful", "400"])
688 /// .style_func_boxed(move |row: i32, col: usize| {
689 /// match (row, col) {
690 /// (HEADER_ROW, _) => Style::new().bold(true),
691 /// (_, 0) => {
692 /// // Style status column based on content
693 /// match row {
694 /// 0 => Style::new().foreground(error_color.clone()),
695 /// 1 => Style::new().foreground(success_color.clone()),
696 /// 2 => Style::new().foreground(warning_color.clone()),
697 /// _ => Style::new(),
698 /// }
699 /// }
700 /// _ => Style::new(),
701 /// }
702 /// });
703 ///
704 /// println!("{}", table.render());
705 /// ```
706 pub fn style_func_boxed<F>(mut self, style: F) -> Self
707 where
708 F: Fn(i32, usize) -> Style + Send + Sync + 'static,
709 {
710 self.boxed_style_func = Some(Box::new(style));
711 self
712 }
713
714 /// Sets the table border.
715 pub fn border(mut self, border: Border) -> Self {
716 self.border = border;
717 self
718 }
719
720 /// Sets the style for the table border.
721 pub fn border_style(mut self, style: Style) -> Self {
722 self.border_style = style;
723 self
724 }
725
726 /// Sets whether or not the top border is rendered.
727 pub fn border_top(mut self, v: bool) -> Self {
728 self.border_top = v;
729 self
730 }
731
732 /// Sets whether or not the bottom border is rendered.
733 pub fn border_bottom(mut self, v: bool) -> Self {
734 self.border_bottom = v;
735 self
736 }
737
738 /// Sets whether or not the left border is rendered.
739 pub fn border_left(mut self, v: bool) -> Self {
740 self.border_left = v;
741 self
742 }
743
744 /// Sets whether or not the right border is rendered.
745 pub fn border_right(mut self, v: bool) -> Self {
746 self.border_right = v;
747 self
748 }
749
750 /// Sets whether or not the header separator is rendered.
751 pub fn border_header(mut self, v: bool) -> Self {
752 self.border_header = v;
753 self
754 }
755
756 /// Sets whether or not column separators are rendered.
757 pub fn border_column(mut self, v: bool) -> Self {
758 self.border_column = v;
759 self
760 }
761
762 /// Sets whether or not row separators are rendered.
763 pub fn border_row(mut self, v: bool) -> Self {
764 self.border_row = v;
765 self
766 }
767
768 /// Sets the column headers for the table.
769 ///
770 /// Headers are displayed at the top of the table and are typically styled
771 /// differently from data rows (e.g., bold text). The number of headers
772 /// determines the number of columns in the table.
773 ///
774 /// # Type Parameters
775 ///
776 /// * `I` - An iterator type that yields items convertible to `String`
777 /// * `S` - A type that can be converted into `String`
778 ///
779 /// # Arguments
780 ///
781 /// * `headers` - An iterable collection of header values (strings, string slices, etc.)
782 ///
783 /// # Returns
784 ///
785 /// The `Table` instance with headers set, enabling method chaining.
786 ///
787 /// # Examples
788 ///
789 /// ```rust
790 /// use lipgloss_table::Table;
791 ///
792 /// // Using string slices
793 /// let table1 = Table::new()
794 /// .headers(vec!["Name", "Age", "City"]);
795 ///
796 /// // Using owned strings
797 /// let headers = vec!["ID".to_string(), "Description".to_string()];
798 /// let table2 = Table::new()
799 /// .headers(headers);
800 ///
801 /// // Using an array
802 /// let table3 = Table::new()
803 /// .headers(["Product", "Price", "Stock"]);
804 /// ```
805 pub fn headers<I, S>(mut self, headers: I) -> Self
806 where
807 I: IntoIterator<Item = S>,
808 S: Into<String>,
809 {
810 self.headers = headers.into_iter().map(|s| s.into()).collect();
811 self
812 }
813
814 /// Adds a single row to the table.
815 pub fn row<I, S>(mut self, row: I) -> Self
816 where
817 I: IntoIterator<Item = S>,
818 S: Into<String>,
819 {
820 let row_data: Vec<String> = row.into_iter().map(|s| s.into()).collect();
821
822 // Convert current data to StringData - always create a new one from the matrix
823 let matrix = data_to_matrix(self.data.as_ref());
824 let mut string_data = StringData::new(matrix);
825 string_data.append(row_data);
826 self.data = Box::new(string_data);
827 self
828 }
829
830 /// Adds multiple rows to the table.
831 pub fn rows<I, J, S>(mut self, rows: I) -> Self
832 where
833 I: IntoIterator<Item = J>,
834 J: IntoIterator<Item = S>,
835 S: Into<String>,
836 {
837 for row in rows {
838 self = self.row(row);
839 }
840 self
841 }
842
843 /// Sets the data source for the table.
844 pub fn data<D: Data + 'static>(mut self, data: D) -> Self {
845 self.data = Box::new(data);
846 self
847 }
848
849 /// Sets a fixed width for the table.
850 pub fn width(mut self, w: i32) -> Self {
851 self.width = w;
852 self
853 }
854
855 /// Sets a fixed height for the table.
856 pub fn height(mut self, h: i32) -> Self {
857 self.height = h;
858 self.use_manual_height = h > 0;
859 self
860 }
861
862 /// Sets the row offset for the table (for scrolling).
863 pub fn offset(mut self, o: usize) -> Self {
864 self.offset = o;
865 self
866 }
867
868 /// Sets whether text wrapping is enabled.
869 pub fn wrap(mut self, w: bool) -> Self {
870 self.wrap = w;
871 self
872 }
873
874 /// Renders the table to a complete string representation.
875 ///
876 /// This method performs the final rendering step, calculating layout dimensions,
877 /// applying styles, and constructing the complete table string with borders,
878 /// headers, and data rows. It must be called to generate the visual output.
879 ///
880 /// The rendering process includes:
881 /// - Calculating optimal column widths and row heights
882 /// - Applying cell styles and text wrapping/truncation
883 /// - Constructing borders and separators
884 /// - Handling height constraints and overflow indicators
885 ///
886 /// # Returns
887 ///
888 /// A `String` containing the complete rendered table with ANSI escape sequences
889 /// for styling and proper spacing.
890 ///
891 /// # Examples
892 ///
893 /// ```rust
894 /// use lipgloss_table::{Table, header_row_style};
895 ///
896 /// let mut table = Table::new()
897 /// .headers(vec!["Name", "Score"])
898 /// .row(vec!["Alice", "95"])
899 /// .row(vec!["Bob", "87"])
900 /// .style_func(header_row_style);
901 ///
902 /// let output = table.render();
903 /// println!("{}", output);
904 /// ```
905 ///
906 /// ```rust
907 /// use lipgloss_table::Table;
908 ///
909 /// let mut table = Table::new()
910 /// .headers(vec!["Product", "Description"])
911 /// .row(vec!["Widget", "A very long description that will wrap"])
912 /// .width(30);
913 ///
914 /// let output = table.render();
915 /// // Output will be wrapped to fit within 30 characters width
916 /// println!("{}", output);
917 /// ```
918 pub fn render(&mut self) -> String {
919 self.resize();
920 self.construct_table()
921 }
922
923 /// Computes the total height the table will occupy when rendered.
924 ///
925 /// This method calculates the exact number of terminal lines the table will
926 /// use when rendered, including all borders, headers, data rows, and separators.
927 /// It's useful for layout planning, especially when working with height-constrained
928 /// terminals or when implementing scrolling interfaces.
929 ///
930 /// The calculation includes:
931 /// - Top and bottom borders (if enabled)
932 /// - Header row and header separator (if headers exist)
933 /// - All data rows with their calculated heights
934 /// - Row separators between data rows (if enabled)
935 ///
936 /// # Returns
937 ///
938 /// The total height in terminal lines as a `usize`.
939 ///
940 /// # Examples
941 ///
942 /// ```rust
943 /// use lipgloss_table::Table;
944 ///
945 /// let table = Table::new();
946 /// assert_eq!(table.compute_height(), 2); // Just top and bottom borders
947 ///
948 /// let table_with_content = Table::new()
949 /// .headers(vec!["Name", "Age"])
950 /// .row(vec!["Alice", "30"]);
951 /// // Height = top border + header + header separator + data row + bottom border
952 /// assert_eq!(table_with_content.compute_height(), 5);
953 /// ```
954 ///
955 /// ```rust
956 /// use lipgloss_table::Table;
957 ///
958 /// let mut large_table = Table::new()
959 /// .headers(vec!["ID", "Data"])
960 /// .height(10); // Height constraint
961 ///
962 /// for i in 1..=100 {
963 /// large_table = large_table.row(vec![i.to_string(), format!("Data {}", i)]);
964 /// }
965 ///
966 /// large_table.render(); // Must render first to populate heights
967 /// let height = large_table.compute_height();
968 /// // compute_height() returns the natural height, not constrained height
969 /// // The actual rendered output will be constrained to 10 lines
970 /// assert!(height > 10); // Natural height is larger than constraint
971 /// ```
972 pub fn compute_height(&self) -> usize {
973 let has_headers = !self.headers.is_empty();
974 let data_rows = self.data.rows();
975
976 // If no rows and no headers, just border height
977 if data_rows == 0 && !has_headers {
978 return if self.border_top && self.border_bottom {
979 2
980 } else if self.border_top || self.border_bottom {
981 1
982 } else {
983 0
984 };
985 }
986
987 let mut total_height = 0;
988
989 // Top border
990 if self.border_top {
991 total_height += 1;
992 }
993
994 // Header row
995 if has_headers {
996 total_height += 1;
997
998 // Header separator
999 if self.border_header {
1000 total_height += 1;
1001 }
1002 }
1003
1004 // Data rows
1005 if data_rows > 0 {
1006 // Sum the heights of all data rows
1007 let header_offset = if has_headers { 1 } else { 0 };
1008 for i in 0..data_rows {
1009 let row_height = self.heights.get(i + header_offset).unwrap_or(&1);
1010 total_height += row_height;
1011
1012 // Row separators (between data rows, not after the last one)
1013 if self.border_row && i < data_rows - 1 {
1014 total_height += 1;
1015 }
1016 }
1017 }
1018
1019 // Bottom border
1020 if self.border_bottom {
1021 total_height += 1;
1022 }
1023
1024 total_height
1025 }
1026
1027 // Private methods for internal rendering
1028
1029 /// Get the appropriate style for a cell, using either the function pointer or boxed function.
1030 fn get_cell_style(&self, row: i32, col: usize) -> Style {
1031 if let Some(ref boxed_func) = self.boxed_style_func {
1032 boxed_func(row, col)
1033 } else {
1034 (self.style_func)(row, col)
1035 }
1036 }
1037
1038 fn resize(&mut self) {
1039 let has_headers = !self.headers.is_empty();
1040 let rows = data_to_matrix(self.data.as_ref());
1041 let mut resizer = Resizer::new(self.width, self.height, self.headers.clone(), rows);
1042 resizer.wrap = self.wrap;
1043 resizer.border_column = self.border_column;
1044 resizer.y_paddings = vec![vec![0; resizer.columns.len()]; resizer.all_rows.len()];
1045
1046 // Calculate style-based padding for each cell
1047 resizer.row_heights = resizer.default_row_heights();
1048
1049 for (i, row) in resizer.all_rows.iter().enumerate() {
1050 if i >= resizer.y_paddings.len() {
1051 resizer.y_paddings.push(vec![0; row.len()]);
1052 }
1053 if resizer.y_paddings[i].len() < row.len() {
1054 resizer.y_paddings[i].resize(row.len(), 0);
1055 }
1056
1057 for j in 0..row.len() {
1058 if j >= resizer.columns.len() {
1059 continue;
1060 }
1061
1062 // Making sure we're passing the right index to the style function.
1063 // The header row should be `-1` and the others should start from `0`.
1064 let row_index = if has_headers { i as i32 - 1 } else { i as i32 };
1065 let style = self.get_cell_style(row_index, j);
1066
1067 // Extract margin and padding values
1068 let (top_margin, right_margin, bottom_margin, left_margin) = (
1069 style.get_margin_top().max(0) as usize,
1070 style.get_margin_right().max(0) as usize,
1071 style.get_margin_bottom().max(0) as usize,
1072 style.get_margin_left().max(0) as usize,
1073 );
1074 let (top_padding, right_padding, bottom_padding, left_padding) = (
1075 style.get_padding_top().max(0) as usize,
1076 style.get_padding_right().max(0) as usize,
1077 style.get_padding_bottom().max(0) as usize,
1078 style.get_padding_left().max(0) as usize,
1079 );
1080
1081 let total_horizontal_padding =
1082 left_margin + right_margin + left_padding + right_padding;
1083 resizer.columns[j].x_padding =
1084 resizer.columns[j].x_padding.max(total_horizontal_padding);
1085
1086 let width = style.get_width();
1087 if width > 0 {
1088 resizer.columns[j].fixed_width =
1089 resizer.columns[j].fixed_width.max(width as usize);
1090 }
1091
1092 let height = style.get_height();
1093 if height > 0 {
1094 resizer.row_heights[i] = resizer.row_heights[i].max(height as usize);
1095 }
1096
1097 let total_vertical_padding =
1098 top_margin + bottom_margin + top_padding + bottom_padding;
1099 resizer.y_paddings[i][j] = total_vertical_padding;
1100 }
1101 }
1102
1103 // Auto-detect table width if not specified
1104 if resizer.table_width <= 0 {
1105 resizer.table_width = resizer.detect_table_width();
1106 }
1107
1108 let (widths, heights) = resizer.optimized_widths();
1109 self.widths = widths;
1110 self.heights = heights;
1111 }
1112
1113 fn construct_table(&self) -> String {
1114 let mut result = String::new();
1115 let has_headers = !self.headers.is_empty();
1116 let _data_rows = self.data.rows();
1117
1118 if self.widths.is_empty() {
1119 return result;
1120 }
1121
1122 // Construct top border
1123 if self.border_top {
1124 result.push_str(&self.construct_top_border());
1125 result.push('\n');
1126 }
1127
1128 // Construct headers
1129 if has_headers {
1130 result.push_str(&self.construct_headers());
1131 result.push('\n');
1132
1133 // Header separator
1134 if self.border_header {
1135 result.push_str(&self.construct_header_separator());
1136 result.push('\n');
1137 }
1138 }
1139
1140 // Construct data rows
1141 let available_lines = if self.use_manual_height && self.height > 0 {
1142 let used_lines = if self.border_top { 1 } else { 0 }
1143 + if has_headers { 1 } else { 0 }
1144 + if has_headers && self.border_header {
1145 1
1146 } else {
1147 0
1148 }
1149 + if self.border_bottom { 1 } else { 0 };
1150 (self.height as usize).saturating_sub(used_lines)
1151 } else {
1152 usize::MAX
1153 };
1154
1155 result.push_str(&self.construct_rows(available_lines));
1156
1157 // Construct bottom border
1158 if self.border_bottom {
1159 if !result.is_empty() && !result.ends_with('\n') {
1160 result.push('\n');
1161 }
1162 result.push_str(&self.construct_bottom_border());
1163 }
1164
1165 result
1166 }
1167
1168 fn construct_top_border(&self) -> String {
1169 let mut border_parts = Vec::new();
1170
1171 if self.border_left {
1172 border_parts.push(self.border.top_left.to_string());
1173 }
1174
1175 for (i, &width) in self.widths.iter().enumerate() {
1176 border_parts.push(safe_str_repeat(self.border.top, width));
1177
1178 if i < self.widths.len() - 1 && self.border_column {
1179 border_parts.push(self.border.middle_top.to_string());
1180 }
1181 }
1182
1183 if self.border_right {
1184 border_parts.push(self.border.top_right.to_string());
1185 }
1186
1187 self.border_style.render(&border_parts.join(""))
1188 }
1189
1190 fn construct_bottom_border(&self) -> String {
1191 let mut border_parts = Vec::new();
1192
1193 if self.border_left {
1194 border_parts.push(self.border.bottom_left.to_string());
1195 }
1196
1197 for (i, &width) in self.widths.iter().enumerate() {
1198 border_parts.push(safe_str_repeat(self.border.bottom, width));
1199
1200 if i < self.widths.len() - 1 && self.border_column {
1201 border_parts.push(self.border.middle_bottom.to_string());
1202 }
1203 }
1204
1205 if self.border_right {
1206 border_parts.push(self.border.bottom_right.to_string());
1207 }
1208
1209 self.border_style.render(&border_parts.join(""))
1210 }
1211
1212 fn construct_header_separator(&self) -> String {
1213 let mut border_parts = Vec::new();
1214
1215 if self.border_left {
1216 border_parts.push(self.border.middle_left.to_string());
1217 }
1218
1219 for (i, &width) in self.widths.iter().enumerate() {
1220 border_parts.push(safe_str_repeat(self.border.top, width));
1221
1222 if i < self.widths.len() - 1 && self.border_column {
1223 border_parts.push(self.border.middle.to_string());
1224 }
1225 }
1226
1227 if self.border_right {
1228 border_parts.push(self.border.middle_right.to_string());
1229 }
1230
1231 self.border_style.render(&border_parts.join(""))
1232 }
1233
1234 fn construct_headers(&self) -> String {
1235 self.construct_row_content(&self.headers, HEADER_ROW)
1236 }
1237
1238 fn construct_rows(&self, available_lines: usize) -> String {
1239 let mut result = String::new();
1240 let mut lines_used = 0;
1241 let data_rows = self.data.rows();
1242
1243 for i in self.offset..data_rows {
1244 if lines_used >= available_lines {
1245 // Add overflow indicator if we have more data
1246 if i < data_rows {
1247 result.push_str(&self.construct_overflow_row());
1248 }
1249 break;
1250 }
1251
1252 // Get row data
1253 let mut row_data = Vec::new();
1254 for j in 0..self.data.columns() {
1255 row_data.push(self.data.at(i, j));
1256 }
1257
1258 result.push_str(&self.construct_row_content(&row_data, i as i32));
1259 lines_used += self
1260 .heights
1261 .get(i + if !self.headers.is_empty() { 1 } else { 0 })
1262 .unwrap_or(&1);
1263
1264 // Add row separator if needed
1265 if self.border_row && i < data_rows - 1 && lines_used < available_lines {
1266 result.push('\n');
1267 result.push_str(&self.construct_row_separator());
1268 lines_used += 1;
1269 }
1270
1271 if i < data_rows - 1 {
1272 result.push('\n');
1273 }
1274 }
1275
1276 result
1277 }
1278
1279 fn construct_row_content(&self, row_data: &[String], row_index: i32) -> String {
1280 let mut cell_parts = Vec::new();
1281
1282 if self.border_left {
1283 cell_parts.push(self.border.left.to_string());
1284 }
1285
1286 for (j, cell_content) in row_data.iter().enumerate() {
1287 if j >= self.widths.len() {
1288 break;
1289 }
1290
1291 let cell_width = self.widths[j];
1292 let style = self.get_cell_style(row_index, j);
1293
1294 // Apply cell styling and fit to width
1295 let styled_content = self.style_cell_content(cell_content, cell_width, style);
1296 cell_parts.push(styled_content);
1297
1298 if self.border_column && j < row_data.len() - 1 {
1299 cell_parts.push(self.border.left.to_string());
1300 }
1301 }
1302
1303 if self.border_right {
1304 cell_parts.push(self.border.right.to_string());
1305 }
1306
1307 cell_parts.join("")
1308 }
1309
1310 fn construct_row_separator(&self) -> String {
1311 let mut border_parts = Vec::new();
1312
1313 if self.border_left {
1314 border_parts.push(self.border.middle_left.to_string());
1315 }
1316
1317 for (i, &width) in self.widths.iter().enumerate() {
1318 border_parts.push(safe_str_repeat(self.border.top, width));
1319
1320 if i < self.widths.len() - 1 && self.border_column {
1321 border_parts.push(self.border.middle.to_string());
1322 }
1323 }
1324
1325 if self.border_right {
1326 border_parts.push(self.border.middle_right.to_string());
1327 }
1328
1329 self.border_style.render(&border_parts.join(""))
1330 }
1331
1332 fn construct_overflow_row(&self) -> String {
1333 let mut cell_parts = Vec::new();
1334
1335 if self.border_left {
1336 cell_parts.push(self.border.left.to_string());
1337 }
1338
1339 for (i, &width) in self.widths.iter().enumerate() {
1340 let ellipsis = "…".to_string();
1341 let padding = safe_repeat(' ', width.saturating_sub(ellipsis.len()));
1342 cell_parts.push(format!("{}{}", ellipsis, padding));
1343
1344 if self.border_column && i < self.widths.len() - 1 {
1345 cell_parts.push(self.border.left.to_string());
1346 }
1347 }
1348
1349 if self.border_right {
1350 cell_parts.push(self.border.right.to_string());
1351 }
1352
1353 cell_parts.join("")
1354 }
1355
1356 fn style_cell_content(&self, content: &str, width: usize, style: Style) -> String {
1357 // Handle content wrapping if needed
1358 let fitted_content = if self.wrap {
1359 self.wrap_cell_content(content, width)
1360 } else {
1361 self.truncate_cell_content(content, width)
1362 };
1363
1364 // Apply the lipgloss style to the content
1365 // The style should handle its own width constraints, so we apply it directly
1366 style.width(width as i32).render(&fitted_content)
1367 }
1368
1369 fn truncate_cell_content(&self, content: &str, width: usize) -> String {
1370 let content_width = lipgloss::width(content);
1371
1372 if content_width > width {
1373 // Truncate with ellipsis, handling ANSI sequences properly
1374 if width == 0 {
1375 return String::new();
1376 } else if width == 1 {
1377 return "…".to_string();
1378 }
1379
1380 // For ANSI-aware truncation, we need to be more careful
1381 // For now, use a simple approach that may not be perfect with ANSI sequences
1382 let chars: Vec<char> = content.chars().collect();
1383 let mut result = String::new();
1384 let mut current_width = 0;
1385
1386 for ch in chars {
1387 let char_str = ch.to_string();
1388 let char_width = lipgloss::width(&char_str);
1389
1390 if current_width + char_width + 1 > width {
1391 // +1 for ellipsis
1392 break;
1393 }
1394
1395 result.push(ch);
1396 current_width += char_width;
1397 }
1398
1399 result.push('…');
1400 result
1401 } else {
1402 content.to_string()
1403 }
1404 }
1405
1406 fn wrap_cell_content(&self, content: &str, width: usize) -> String {
1407 if width == 0 {
1408 return String::new();
1409 }
1410
1411 let mut wrapped_lines = Vec::new();
1412
1413 // Handle existing line breaks
1414 for line in content.lines() {
1415 if line.is_empty() {
1416 wrapped_lines.push(String::new());
1417 continue;
1418 }
1419
1420 // Use lipgloss width which handles ANSI sequences
1421 let line_width = lipgloss::width(line);
1422 if line_width <= width {
1423 wrapped_lines.push(line.to_string());
1424 } else {
1425 // Need to wrap this line - use ANSI-aware wrapping
1426 wrapped_lines.extend(self.wrap_line_ansi_aware(line, width));
1427 }
1428 }
1429
1430 wrapped_lines.join("\n")
1431 }
1432
1433 fn wrap_line_ansi_aware(&self, line: &str, width: usize) -> Vec<String> {
1434 // For now, use a simple word-based wrapping that preserves ANSI sequences
1435 // This could be enhanced to use lipgloss's word wrapping utilities if available
1436 let words: Vec<&str> = line.split_whitespace().collect();
1437 let mut lines = Vec::new();
1438 let mut current_line = String::new();
1439 let mut current_width = 0;
1440
1441 for word in words {
1442 let word_width = lipgloss::width(word);
1443
1444 // If adding this word would exceed width, start a new line
1445 if !current_line.is_empty() && current_width + 1 + word_width > width {
1446 lines.push(current_line);
1447 current_line = word.to_string();
1448 current_width = word_width;
1449 } else if current_line.is_empty() {
1450 current_line = word.to_string();
1451 current_width = word_width;
1452 } else {
1453 current_line.push(' ');
1454 current_line.push_str(word);
1455 current_width += 1 + word_width;
1456 }
1457 }
1458
1459 if !current_line.is_empty() {
1460 lines.push(current_line);
1461 }
1462
1463 if lines.is_empty() {
1464 lines.push(String::new());
1465 }
1466
1467 lines
1468 }
1469}
1470
1471impl fmt::Display for Table {
1472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1473 // Need to create a mutable copy for rendering since fmt doesn't allow mutable self
1474 let mut table_copy = Table {
1475 style_func: self.style_func,
1476 boxed_style_func: None, // Cannot clone boxed closures easily
1477 border: self.border,
1478 border_top: self.border_top,
1479 border_bottom: self.border_bottom,
1480 border_left: self.border_left,
1481 border_right: self.border_right,
1482 border_header: self.border_header,
1483 border_column: self.border_column,
1484 border_row: self.border_row,
1485 border_style: self.border_style.clone(),
1486 headers: self.headers.clone(),
1487 data: Box::new(StringData::new(data_to_matrix(self.data.as_ref()))),
1488 width: self.width,
1489 height: self.height,
1490 use_manual_height: self.use_manual_height,
1491 offset: self.offset,
1492 wrap: self.wrap,
1493 widths: self.widths.clone(),
1494 heights: self.heights.clone(),
1495 };
1496
1497 write!(f, "{}", table_copy.render())
1498 }
1499}
1500
1501impl Default for Table {
1502 fn default() -> Self {
1503 Self::new()
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510
1511 #[test]
1512 fn test_table_new() {
1513 let table = Table::new();
1514 assert_eq!(table.headers.len(), 0);
1515 assert_eq!(table.data.rows(), 0);
1516 assert_eq!(table.data.columns(), 0);
1517 assert!(table.border_top);
1518 assert!(table.border_bottom);
1519 assert!(table.border_left);
1520 assert!(table.border_right);
1521 assert!(table.border_header);
1522 assert!(table.border_column);
1523 assert!(!table.border_row);
1524 assert!(table.wrap);
1525 }
1526
1527 #[test]
1528 fn test_table_headers() {
1529 let table = Table::new().headers(vec!["Name", "Age", "Location"]);
1530 assert_eq!(table.headers.len(), 3);
1531 assert_eq!(table.headers[0], "Name");
1532 assert_eq!(table.headers[1], "Age");
1533 assert_eq!(table.headers[2], "Location");
1534 }
1535
1536 #[test]
1537 fn test_table_rows() {
1538 let table = Table::new()
1539 .headers(vec!["Name", "Age"])
1540 .row(vec!["Alice", "30"])
1541 .row(vec!["Bob", "25"]);
1542
1543 assert_eq!(table.data.rows(), 2);
1544 assert_eq!(table.data.columns(), 2);
1545 assert_eq!(table.data.at(0, 0), "Alice");
1546 assert_eq!(table.data.at(0, 1), "30");
1547 assert_eq!(table.data.at(1, 0), "Bob");
1548 assert_eq!(table.data.at(1, 1), "25");
1549 }
1550
1551 #[test]
1552 fn test_table_builder_pattern() {
1553 let table = Table::new()
1554 .border_top(false)
1555 .border_bottom(false)
1556 .width(80)
1557 .height(10)
1558 .wrap(false);
1559
1560 assert!(!table.border_top);
1561 assert!(!table.border_bottom);
1562 assert_eq!(table.width, 80);
1563 assert_eq!(table.height, 10);
1564 assert!(!table.wrap);
1565 }
1566
1567 #[test]
1568 fn test_compute_height_empty_table() {
1569 let table = Table::new();
1570 assert_eq!(table.compute_height(), 2); // top + bottom border
1571
1572 let table_no_borders = Table::new().border_top(false).border_bottom(false);
1573 assert_eq!(table_no_borders.compute_height(), 0);
1574
1575 let table_top_only = Table::new().border_bottom(false);
1576 assert_eq!(table_top_only.compute_height(), 1);
1577
1578 let table_bottom_only = Table::new().border_top(false);
1579 assert_eq!(table_bottom_only.compute_height(), 1);
1580 }
1581
1582 #[test]
1583 fn test_compute_height_headers_only() {
1584 let table = Table::new().headers(vec!["Name", "Age"]);
1585 // top border + header + header separator + bottom border
1586 assert_eq!(table.compute_height(), 4);
1587
1588 let table_no_header_sep = Table::new()
1589 .headers(vec!["Name", "Age"])
1590 .border_header(false);
1591 // top border + header + bottom border
1592 assert_eq!(table_no_header_sep.compute_height(), 3);
1593
1594 let table_no_borders = Table::new()
1595 .headers(vec!["Name", "Age"])
1596 .border_top(false)
1597 .border_bottom(false)
1598 .border_header(false);
1599 // just header
1600 assert_eq!(table_no_borders.compute_height(), 1);
1601 }
1602
1603 #[test]
1604 fn test_compute_height_with_data() {
1605 let mut table = Table::new()
1606 .headers(vec!["Name", "Age"])
1607 .row(vec!["Alice", "30"])
1608 .row(vec!["Bob", "25"]);
1609
1610 // Need to render first to populate heights
1611 table.render();
1612
1613 // top border + header + header separator + 2 data rows + bottom border = 6
1614 assert_eq!(table.compute_height(), 6);
1615 }
1616
1617 #[test]
1618 fn test_compute_height_with_row_borders() {
1619 let mut table = Table::new()
1620 .headers(vec!["Name", "Age"])
1621 .row(vec!["Alice", "30"])
1622 .row(vec!["Bob", "25"])
1623 .border_row(true);
1624
1625 table.render();
1626
1627 // top border + header + header separator + row1 + row separator + row2 + bottom border = 7
1628 assert_eq!(table.compute_height(), 7);
1629 }
1630
1631 #[test]
1632 fn test_compute_height_data_only() {
1633 let mut table = Table::new().row(vec!["Alice", "30"]).row(vec!["Bob", "25"]);
1634
1635 table.render();
1636
1637 // top border + 2 data rows + bottom border = 4
1638 assert_eq!(table.compute_height(), 4);
1639
1640 let mut table_with_row_borders = Table::new()
1641 .row(vec!["Alice", "30"])
1642 .row(vec!["Bob", "25"])
1643 .border_row(true);
1644
1645 table_with_row_borders.render();
1646
1647 // top border + row1 + row separator + row2 + bottom border = 5
1648 assert_eq!(table_with_row_borders.compute_height(), 5);
1649 }
1650
1651 #[test]
1652 fn test_compute_height_single_row() {
1653 let mut table = Table::new().headers(vec!["Name"]).row(vec!["Alice"]);
1654
1655 table.render();
1656
1657 // top border + header + header separator + 1 data row + bottom border = 5
1658 assert_eq!(table.compute_height(), 5);
1659 }
1660
1661 #[test]
1662 fn test_compute_height_minimal_borders() {
1663 let mut table = Table::new()
1664 .headers(vec!["Name", "Age"])
1665 .row(vec!["Alice", "30"])
1666 .border_top(false)
1667 .border_bottom(false)
1668 .border_header(false);
1669
1670 table.render();
1671
1672 // just header + data row = 2
1673 assert_eq!(table.compute_height(), 2);
1674 }
1675
1676 #[test]
1677 fn test_table_clear_rows() {
1678 let table = Table::new()
1679 .row(vec!["A", "B"])
1680 .row(vec!["C", "D"])
1681 .clear_rows();
1682
1683 assert_eq!(table.data.rows(), 0);
1684 assert_eq!(table.data.columns(), 0);
1685 }
1686
1687 #[test]
1688 fn test_table_rendering() {
1689 let mut table = Table::new()
1690 .headers(vec!["Name", "Age", "City"])
1691 .row(vec!["Alice", "30", "New York"])
1692 .row(vec!["Bob", "25", "London"]);
1693
1694 let output = table.render();
1695 assert!(!output.is_empty());
1696
1697 // Should contain the header and data
1698 assert!(output.contains("Name"));
1699 assert!(output.contains("Alice"));
1700 assert!(output.contains("Bob"));
1701
1702 // Should have borders by default
1703 assert!(output.contains("┌") || output.contains("╭")); // Top-left corner
1704 }
1705
1706 #[test]
1707 fn test_table_no_borders() {
1708 let mut table = Table::new()
1709 .headers(vec!["Name", "Age"])
1710 .row(vec!["Alice", "30"])
1711 .border_top(false)
1712 .border_bottom(false)
1713 .border_left(false)
1714 .border_right(false)
1715 .border_column(false);
1716
1717 let output = table.render();
1718 assert!(!output.is_empty());
1719 assert!(output.contains("Name"));
1720 assert!(output.contains("Alice"));
1721
1722 // Should not contain border characters
1723 assert!(!output.contains("┌"));
1724 assert!(!output.contains("│"));
1725 }
1726
1727 #[test]
1728 fn test_table_width_constraint() {
1729 let mut table = Table::new()
1730 .headers(vec!["Name", "Age", "City"])
1731 .row(vec!["Alice Johnson", "28", "New York"])
1732 .row(vec!["Bob Smith", "35", "London"])
1733 .width(25); // Force narrow width
1734
1735 let output = table.render();
1736 assert!(!output.is_empty());
1737
1738 // Each line should respect the width constraint (using display width, not character count)
1739 for line in output.lines() {
1740 // Use lipgloss width which handles ANSI sequences properly
1741 let line_width = lipgloss::width(line);
1742 assert!(
1743 line_width <= 25,
1744 "Line '{}' has display width {} > 25",
1745 line,
1746 line_width
1747 );
1748 }
1749 }
1750
1751 #[test]
1752 fn test_comprehensive_table_demo() {
1753 let mut table = Table::new()
1754 .headers(vec!["Name", "Age", "City", "Occupation"])
1755 .row(vec!["Alice Johnson", "28", "New York", "Software Engineer"])
1756 .row(vec!["Bob Smith", "35", "London", "Product Manager"])
1757 .row(vec!["Charlie Brown", "42", "Tokyo", "UX Designer"])
1758 .row(vec!["Diana Prince", "30", "Paris", "Data Scientist"]);
1759
1760 let output = table.render();
1761 println!("\n=== Comprehensive Table Demo ===");
1762 println!("{}", output);
1763
1764 assert!(!output.is_empty());
1765 assert!(output.contains("Alice Johnson"));
1766 assert!(output.contains("Software Engineer"));
1767
1768 // Test different border styles
1769 println!("\n=== No Borders Demo ===");
1770 let mut no_border_table = Table::new()
1771 .headers(vec!["Item", "Price"])
1772 .row(vec!["Coffee", "$3.50"])
1773 .row(vec!["Tea", "$2.25"])
1774 .border_top(false)
1775 .border_bottom(false)
1776 .border_left(false)
1777 .border_right(false)
1778 .border_column(false)
1779 .border_header(false);
1780
1781 println!("{}", no_border_table.render());
1782
1783 // Test width constraint
1784 println!("\n=== Width Constrained Table ===");
1785 let mut narrow_table = Table::new()
1786 .headers(vec!["Product", "Description", "Price"])
1787 .row(vec![
1788 "MacBook Pro",
1789 "Powerful laptop for developers",
1790 "$2399",
1791 ])
1792 .row(vec![
1793 "iPhone",
1794 "Latest smartphone with amazing camera",
1795 "$999",
1796 ])
1797 .width(40);
1798
1799 println!("{}", narrow_table.render());
1800 }
1801
1802 #[test]
1803 fn test_empty_table() {
1804 let mut table = Table::new();
1805 let output = table.render();
1806
1807 // Empty table should produce minimal output
1808 assert!(output.is_empty() || output.trim().is_empty());
1809 }
1810
1811 #[test]
1812 #[allow(unknown_lints, clippy::manual_is_multiple_of)]
1813 fn test_cell_styling_with_lipgloss() {
1814 use lipgloss::{
1815 color::{STATUS_ERROR, TEXT_MUTED},
1816 Style,
1817 };
1818
1819 let style_func = |row: i32, _col: usize| match row {
1820 HEADER_ROW => Style::new().bold(true).foreground(STATUS_ERROR),
1821 _ if row % 2 == 0 => Style::new().foreground(TEXT_MUTED),
1822 _ => Style::new().italic(true),
1823 };
1824
1825 let mut table = Table::new()
1826 .headers(vec!["Name", "Age", "City"])
1827 .row(vec!["Alice", "30", "New York"])
1828 .row(vec!["Bob", "25", "London"])
1829 .style_func(style_func);
1830
1831 let output = table.render();
1832 assert!(!output.is_empty());
1833 assert!(output.contains("Name")); // Headers should be present
1834 assert!(output.contains("Alice")); // Data should be present
1835
1836 // Since we're applying styles, there should be ANSI escape sequences
1837 assert!(output.contains("\\x1b[") || output.len() > 50); // Either ANSI codes or substantial content
1838 }
1839
1840 #[test]
1841 fn test_text_wrapping_functionality() {
1842 let mut table = Table::new()
1843 .headers(vec!["Short", "VeryLongContentThatShouldWrap"])
1844 .row(vec!["A", "This is a very long piece of content that should wrap across multiple lines when the table width is constrained"])
1845 .width(30)
1846 .wrap(true);
1847
1848 let output = table.render();
1849 assert!(!output.is_empty());
1850
1851 // With wrapping enabled and constrained width, we should get multiple lines
1852 let line_count = output.lines().count();
1853 assert!(
1854 line_count > 3,
1855 "Expected more than 3 lines due to wrapping, got {}",
1856 line_count
1857 );
1858 }
1859
1860 #[test]
1861 fn test_text_truncation_functionality() {
1862 let mut table = Table::new()
1863 .headers(vec!["Short", "Long"])
1864 .row(vec![
1865 "A",
1866 "This is a very long piece of content that should be truncated",
1867 ])
1868 .width(25)
1869 .wrap(false); // Disable wrapping to force truncation
1870
1871 let output = table.render();
1872 assert!(!output.is_empty());
1873
1874 // Should contain ellipsis indicating truncation
1875 assert!(
1876 output.contains("…"),
1877 "Expected ellipsis for truncated content"
1878 );
1879 }
1880
1881 #[test]
1882 fn test_ansi_aware_width_calculation() {
1883 use lipgloss::{Color, Style};
1884
1885 // Create content with ANSI sequences
1886 let styled_content = Style::new()
1887 .foreground(Color::from("#FF0000"))
1888 .bold(true)
1889 .render("Test");
1890
1891 let mut table = Table::new()
1892 .headers(vec!["Styled"])
1893 .row(vec![&styled_content])
1894 .width(10);
1895
1896 let output = table.render();
1897 assert!(!output.is_empty());
1898
1899 // The table should handle ANSI sequences correctly in width calculations
1900 // The visual width should be respected, not the character count
1901 for line in output.lines() {
1902 let visual_width = lipgloss::width(line);
1903 assert!(
1904 visual_width <= 10,
1905 "Line has visual width {} > 10: '{}'",
1906 visual_width,
1907 line
1908 );
1909 }
1910 }
1911
1912 #[test]
1913 fn test_predefined_style_functions() {
1914 // Test header_row_style
1915 let mut table1 = Table::new()
1916 .headers(vec!["Name", "Age"])
1917 .row(vec!["Alice", "30"])
1918 .style_func(header_row_style);
1919
1920 let output1 = table1.render();
1921 assert!(!output1.is_empty());
1922 assert!(output1.contains("Name"));
1923
1924 // Test zebra_style
1925 let mut table2 = Table::new()
1926 .headers(vec!["Item", "Count"])
1927 .row(vec!["Apple", "5"])
1928 .row(vec!["Banana", "3"])
1929 .row(vec!["Cherry", "8"])
1930 .style_func(zebra_style);
1931
1932 let output2 = table2.render();
1933 assert!(!output2.is_empty());
1934 assert!(output2.contains("Item"));
1935
1936 // Test minimal_style
1937 let mut table3 = Table::new()
1938 .headers(vec!["Name"])
1939 .row(vec!["Test"])
1940 .style_func(minimal_style);
1941
1942 let output3 = table3.render();
1943 assert!(!output3.is_empty());
1944 assert!(output3.contains("Name"));
1945 }
1946
1947 #[test]
1948 fn test_boxed_style_function() {
1949 use lipgloss::{
1950 color::{STATUS_ERROR, STATUS_WARNING},
1951 Style,
1952 };
1953
1954 // Create a closure that captures variables
1955 let error_color = STATUS_ERROR;
1956 let warning_color = STATUS_WARNING;
1957
1958 let mut table = Table::new()
1959 .headers(vec!["Status", "Message"])
1960 .row(vec!["ERROR", "Something went wrong"])
1961 .row(vec!["WARNING", "This is a warning"])
1962 .row(vec!["INFO", "Everything is fine"])
1963 .style_func_boxed(move |row: i32, col: usize| {
1964 if row == HEADER_ROW {
1965 Style::new().bold(true)
1966 } else if col == 0 {
1967 // Style the status column based on content
1968 // Note: In a real implementation, you'd have access to the cell content
1969 match row {
1970 0 => Style::new().foreground(error_color.clone()),
1971 1 => Style::new().foreground(warning_color.clone()),
1972 _ => Style::new(),
1973 }
1974 } else {
1975 Style::new()
1976 }
1977 });
1978
1979 let output = table.render();
1980 assert!(!output.is_empty());
1981 assert!(output.contains("Status"));
1982 assert!(output.contains("ERROR"));
1983 }
1984}