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