super_table/
table.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::iter::IntoIterator;
4use std::slice::{Iter, IterMut};
5#[cfg(feature = "tty")]
6use std::sync::OnceLock;
7
8use crate::cell::Cell;
9use crate::column::Column;
10use crate::row::Row;
11use crate::style::presets::ASCII_FULL;
12use crate::style::{ColumnConstraint, ContentArrangement, TableComponent};
13use crate::utils::build_table;
14
15/// This is the main interface for building a table.
16/// Each table consists of [Rows](Row), which in turn contain [Cells](crate::cell::Cell).
17///
18/// There also exists a representation of a [Column].
19/// Columns are automatically created when adding rows to a table.
20#[derive(Debug, Clone)]
21pub struct Table {
22    pub(crate) columns: Vec<Column>,
23    style: HashMap<TableComponent, char>,
24    pub(crate) header: Option<Row>,
25    pub(crate) rows: Vec<Row>,
26    pub(crate) arrangement: ContentArrangement,
27    pub(crate) delimiter: Option<char>,
28    pub(crate) truncation_indicator: String,
29    #[cfg(feature = "tty")]
30    no_tty: bool,
31    #[cfg(feature = "tty")]
32    is_tty_cache: OnceLock<bool>,
33    #[cfg(feature = "tty")]
34    use_stderr: bool,
35    width: Option<u16>,
36    #[cfg(feature = "tty")]
37    enforce_styling: bool,
38    /// Define whether everything in a cells should be styled, including whitespaces
39    /// or whether only the text should be styled.
40    #[cfg(feature = "tty")]
41    pub(crate) style_text_only: bool,
42}
43
44impl fmt::Display for Table {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}", self.lines().collect::<Vec<_>>().join("\n"))
47    }
48}
49
50impl Default for Table {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Table {
57    /// Create a new table with default ASCII styling.
58    pub fn new() -> Self {
59        let mut table = Self {
60            columns: Vec::new(),
61            header: None,
62            rows: Vec::new(),
63            arrangement: ContentArrangement::Disabled,
64            delimiter: None,
65            truncation_indicator: "...".to_string(),
66            #[cfg(feature = "tty")]
67            no_tty: false,
68            #[cfg(feature = "tty")]
69            is_tty_cache: OnceLock::new(),
70            #[cfg(feature = "tty")]
71            use_stderr: false,
72            width: None,
73            style: HashMap::new(),
74            #[cfg(feature = "tty")]
75            enforce_styling: false,
76            #[cfg(feature = "tty")]
77            style_text_only: false,
78        };
79
80        table.load_preset(ASCII_FULL);
81
82        table
83    }
84
85    /// This is an alternative `fmt` function, which simply removes any trailing whitespaces.
86    /// Trailing whitespaces often occur, when using tables without a right border.
87    pub fn trim_fmt(&self) -> String {
88        self.lines()
89            .map(|line| line.trim_end().to_string())
90            .collect::<Vec<_>>()
91            .join("\n")
92    }
93
94    /// This is an alternative to `fmt`, but rather returns an iterator to each line, rather than
95    /// one String separated by newlines.
96    pub fn lines(&self) -> impl Iterator<Item = String> {
97        build_table(self)
98    }
99
100    /// Set the header row of the table. This is usually the title of each column.\
101    /// There'll be no header unless you explicitly set it with this function.
102    ///
103    /// ```
104    /// use super_table::{Table, Row};
105    ///
106    /// let mut table = Table::new();
107    /// let header = Row::from(vec!["Header One", "Header Two"]);
108    /// table.set_header(header);
109    /// ```
110    pub fn set_header<T: Into<Row>>(&mut self, row: T) -> &mut Self {
111        let row = row.into();
112        self.autogenerate_columns(&row);
113        self.header = Some(row);
114
115        self
116    }
117
118    pub fn header(&self) -> Option<&Row> {
119        self.header.as_ref()
120    }
121
122    /// Returns the number of currently present columns.
123    ///
124    /// ```
125    /// use super_table::Table;
126    ///
127    /// let mut table = Table::new();
128    /// table.set_header(vec!["Col 1", "Col 2", "Col 3"]);
129    ///
130    /// assert_eq!(table.column_count(), 3);
131    /// ```
132    pub fn column_count(&mut self) -> usize {
133        self.discover_columns();
134        self.columns.len()
135    }
136
137    /// Add a new row to the table.
138    ///
139    /// ```
140    /// use super_table::{Table, Row};
141    ///
142    /// let mut table = Table::new();
143    /// table.add_row(vec!["One", "Two"]);
144    /// ```
145    pub fn add_row<T: Into<Row>>(&mut self, row: T) -> &mut Self {
146        let mut row = row.into();
147        self.autogenerate_columns(&row);
148        row.index = Some(self.rows.len());
149        self.rows.push(row);
150
151        self
152    }
153
154    /// Add a new row to the table if the predicate evaluates to `true`.
155    ///
156    /// ```
157    /// use super_table::{Table, Row};
158    ///
159    /// let mut table = Table::new();
160    /// table.add_row_if(|index, row| true, vec!["One", "Two"]);
161    /// ```
162    pub fn add_row_if<P, T>(&mut self, predicate: P, row: T) -> &mut Self
163    where
164        P: Fn(usize, &T) -> bool,
165        T: Into<Row>,
166    {
167        if predicate(self.rows.len(), &row) {
168            return self.add_row(row);
169        }
170
171        self
172    }
173
174    /// Add multiple rows to the table.
175    ///
176    /// ```
177    /// use super_table::{Table, Row};
178    ///
179    /// let mut table = Table::new();
180    /// let rows = vec![
181    ///     vec!["One", "Two"],
182    ///     vec!["Three", "Four"]
183    /// ];
184    /// table.add_rows(rows);
185    /// ```
186    pub fn add_rows<I>(&mut self, rows: I) -> &mut Self
187    where
188        I: IntoIterator,
189        I::Item: Into<Row>,
190    {
191        for row in rows.into_iter() {
192            let mut row = row.into();
193            self.autogenerate_columns(&row);
194            row.index = Some(self.rows.len());
195            self.rows.push(row);
196        }
197
198        self
199    }
200
201    /// Add multiple rows to the table if the predicate evaluates to `true`.
202    ///
203    /// ```
204    /// use super_table::{Table, Row};
205    ///
206    /// let mut table = Table::new();
207    /// let rows = vec![
208    ///     vec!["One", "Two"],
209    ///     vec!["Three", "Four"]
210    /// ];
211    /// table.add_rows_if(|index, rows| true, rows);
212    /// ```
213    pub fn add_rows_if<P, I>(&mut self, predicate: P, rows: I) -> &mut Self
214    where
215        P: Fn(usize, &I) -> bool,
216        I: IntoIterator,
217        I::Item: Into<Row>,
218    {
219        if predicate(self.rows.len(), &rows) {
220            return self.add_rows(rows);
221        }
222
223        self
224    }
225
226    /// Returns the number of currently present rows.
227    ///
228    /// ```
229    /// use super_table::Table;
230    ///
231    /// let mut table = Table::new();
232    /// table.add_row(vec!["One", "Two"]);
233    ///
234    /// assert_eq!(table.row_count(), 1);
235    /// ```
236    pub fn row_count(&self) -> usize {
237        self.rows.len()
238    }
239
240    /// Returns if the table is empty (contains no data rows).
241    ///
242    /// ```
243    /// use super_table::Table;
244    ///
245    /// let mut table = Table::new();
246    /// assert!(table.is_empty());
247    ///
248    /// table.add_row(vec!["One", "Two"]);
249    /// assert!(!table.is_empty());
250    /// ```
251    pub fn is_empty(&self) -> bool {
252        self.rows.is_empty()
253    }
254
255    /// Enforce a max width that should be used in combination with [dynamic content arrangement](ContentArrangement::Dynamic).\
256    /// This is usually not necessary, if you plan to output your table to a tty,
257    /// since the terminal width can be automatically determined.
258    pub fn set_width(&mut self, width: u16) -> &mut Self {
259        self.width = Some(width);
260
261        self
262    }
263
264    /// Get the expected width of the table.
265    ///
266    /// This will be `Some(width)`, if the terminal width can be detected or if the table width is set via [set_width](Table::set_width).
267    ///
268    /// If neither is not possible, `None` will be returned.\
269    /// This implies that both the [Dynamic](ContentArrangement::Dynamic) mode and the [Percentage](crate::style::Width::Percentage) constraint won't work.
270    #[cfg(feature = "tty")]
271    pub fn width(&self) -> Option<u16> {
272        if let Some(width) = self.width {
273            Some(width)
274        } else if self.is_tty() {
275            if let Ok((width, _)) = crossterm::terminal::size() {
276                Some(width)
277            } else {
278                None
279            }
280        } else {
281            None
282        }
283    }
284
285    #[cfg(not(feature = "tty"))]
286    pub fn width(&self) -> Option<u16> {
287        self.width
288    }
289
290    /// Specify how Comfy Table should arrange the content in your table.
291    ///
292    /// ```
293    /// use super_table::{Table, ContentArrangement};
294    ///
295    /// let mut table = Table::new();
296    /// table.set_content_arrangement(ContentArrangement::Dynamic);
297    /// ```
298    pub fn set_content_arrangement(&mut self, arrangement: ContentArrangement) -> &mut Self {
299        self.arrangement = arrangement;
300
301        self
302    }
303
304    /// Get the current content arrangement of the table.
305    pub fn content_arrangement(&self) -> ContentArrangement {
306        self.arrangement.clone()
307    }
308
309    /// Set the delimiter used to split text in all cells.
310    ///
311    /// A custom delimiter on a cell in will overwrite the column's delimiter.\
312    /// Normal text uses spaces (` `) as delimiters. This is necessary to help super-table
313    /// understand the concept of _words_.
314    pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self {
315        self.delimiter = Some(delimiter);
316
317        self
318    }
319
320    /// Set the truncation indicator for cells that are too long to be displayed.
321    ///
322    /// Set it to "…" for example to use an ellipsis that only takes up one character.
323    pub fn set_truncation_indicator(&mut self, indicator: &str) -> &mut Self {
324        self.truncation_indicator = indicator.to_string();
325
326        self
327    }
328
329    /// In case you are sure you don't want export tables to a tty or you experience
330    /// problems with tty specific code, you can enforce a non_tty mode.
331    ///
332    /// This disables:
333    ///
334    /// - width lookup from the current tty
335    /// - Styling and attributes on cells (unless you use [Table::enforce_styling])
336    ///
337    /// If you use the [dynamic content arrangement](ContentArrangement::Dynamic),
338    /// you need to set the width of your desired table manually with [set_width](Table::set_width).
339    #[cfg(feature = "tty")]
340    pub fn force_no_tty(&mut self) -> &mut Self {
341        self.no_tty = true;
342
343        self
344    }
345
346    /// Use this function to check whether `stderr` is a tty.
347    ///
348    /// The default is `stdout`.
349    #[cfg(feature = "tty")]
350    pub fn use_stderr(&mut self) -> &mut Self {
351        self.use_stderr = true;
352
353        self
354    }
355
356    /// Returns whether the table will be handled as if it's printed to a tty.
357    ///
358    /// By default, super-table looks at `stdout` and checks whether it's a tty.
359    /// This behavior can be changed via [Table::force_no_tty] and [Table::use_stderr].
360    #[cfg(feature = "tty")]
361    pub fn is_tty(&self) -> bool {
362        use std::io::IsTerminal;
363
364        if self.no_tty {
365            return false;
366        }
367
368        *self.is_tty_cache.get_or_init(|| {
369            if self.use_stderr {
370                std::io::stderr().is_terminal()
371            } else {
372                std::io::stdout().is_terminal()
373            }
374        })
375    }
376
377    /// Enforce terminal styling.
378    ///
379    /// Only useful if you forcefully disabled tty, but still want those fancy terminal styles.
380    ///
381    /// ```
382    /// use super_table::Table;
383    ///
384    /// let mut table = Table::new();
385    /// table.force_no_tty()
386    ///     .enforce_styling();
387    /// ```
388    #[cfg(feature = "tty")]
389    pub fn enforce_styling(&mut self) -> &mut Self {
390        self.enforce_styling = true;
391
392        self
393    }
394
395    /// Returns whether the content of this table should be styled with the current settings and
396    /// environment.
397    #[cfg(feature = "tty")]
398    pub fn should_style(&self) -> bool {
399        if self.enforce_styling {
400            return true;
401        }
402        self.is_tty()
403    }
404
405    /// By default, the whole content of a cells will be styled.
406    /// Calling this function disables this behavior for all cells, resulting in
407    /// only the text of cells being styled.
408    #[cfg(feature = "tty")]
409    pub fn style_text_only(&mut self) {
410        self.style_text_only = true;
411    }
412
413    /// Convenience method to set a [ColumnConstraint] for all columns at once.
414    /// Constraints are used to influence the way the columns will be arranged.
415    /// Check out their docs for more information.
416    ///
417    /// **Attention:**
418    /// This function should be called after at least one row (or the headers) has been added to the table.
419    /// Before that, the columns won't initialized.
420    ///
421    /// If more constraints are passed than there are columns, any superfluous constraints will be ignored.
422    /// ```
423    /// use super_table::{Width::*, CellAlignment, ColumnConstraint::*, ContentArrangement, Table};
424    ///
425    /// let mut table = Table::new();
426    /// table.add_row(&vec!["one", "two", "three"])
427    ///     .set_content_arrangement(ContentArrangement::Dynamic)
428    ///     .set_constraints(vec![
429    ///         UpperBoundary(Fixed(15)),
430    ///         LowerBoundary(Fixed(20)),
431    /// ]);
432    /// ```
433    pub fn set_constraints<T: IntoIterator<Item = ColumnConstraint>>(
434        &mut self,
435        constraints: T,
436    ) -> &mut Self {
437        let mut constraints = constraints.into_iter();
438        for column in self.column_iter_mut() {
439            if let Some(constraint) = constraints.next() {
440                column.set_constraint(constraint);
441            } else {
442                break;
443            }
444        }
445
446        self
447    }
448
449    /// This function creates a TableStyle from a given preset string.\
450    /// Preset strings can be found in `styling::presets::*`.
451    ///
452    /// You can also write your own preset strings and use them with this function.
453    /// There's the convenience method [Table::current_style_as_preset], which prints you a preset
454    /// string from your current style configuration. \
455    /// The function expects the to-be-drawn characters to be in the same order as in the [TableComponent] enum.
456    ///
457    /// If the string isn't long enough, the default [ASCII_FULL] style will be used for all remaining components.
458    ///
459    /// If the string is too long, remaining charaacters will be simply ignored.
460    pub fn load_preset(&mut self, preset: &str) -> &mut Self {
461        let mut components = TableComponent::iter();
462
463        for character in preset.chars() {
464            if let Some(component) = components.next() {
465                // White spaces mean "don't draw this" in presets
466                // If we want to override the default preset, we need to remove
467                // this component from the HashMap in case we find a whitespace.
468                if character == ' ' {
469                    self.remove_style(component);
470                    continue;
471                }
472
473                self.set_style(component, character);
474            } else {
475                break;
476            }
477        }
478
479        self
480    }
481
482    /// Returns the current style as a preset string.
483    ///
484    /// A pure convenience method, so you're not force to fiddle with those preset strings yourself.
485    ///
486    /// ```
487    /// use super_table::Table;
488    /// use super_table::presets::UTF8_FULL;
489    ///
490    /// let mut table = Table::new();
491    /// table.load_preset(UTF8_FULL);
492    ///
493    /// assert_eq!(UTF8_FULL, table.current_style_as_preset())
494    /// ```
495    pub fn current_style_as_preset(&mut self) -> String {
496        let components = TableComponent::iter();
497        let mut preset_string = String::new();
498
499        for component in components {
500            match self.style(component) {
501                None => preset_string.push(' '),
502                Some(character) => preset_string.push(character),
503            }
504        }
505
506        preset_string
507    }
508
509    /// Modify a preset with a modifier string from [modifiers](crate::style::modifiers).
510    ///
511    /// For instance, the [UTF8_ROUND_CORNERS](crate::style::modifiers::UTF8_ROUND_CORNERS) modifies all corners to be round UTF8 box corners.
512    ///
513    /// ```
514    /// use super_table::Table;
515    /// use super_table::presets::UTF8_FULL;
516    /// use super_table::modifiers::UTF8_ROUND_CORNERS;
517    ///
518    /// let mut table = Table::new();
519    /// table.load_preset(UTF8_FULL);
520    /// table.apply_modifier(UTF8_ROUND_CORNERS);
521    /// ```
522    pub fn apply_modifier(&mut self, modifier: &str) -> &mut Self {
523        let mut components = TableComponent::iter();
524
525        for character in modifier.chars() {
526            // Skip spaces while applying modifiers.
527            if character == ' ' {
528                components.next();
529                continue;
530            }
531            if let Some(component) = components.next() {
532                self.set_style(component, character);
533            } else {
534                break;
535            }
536        }
537
538        self
539    }
540
541    /// Define the char that will be used to draw a specific component.\
542    /// Look at [TableComponent] to see all stylable components
543    ///
544    /// If `None` is supplied, the element won't be displayed.\
545    /// In case of a e.g. *BorderIntersection a whitespace will be used as placeholder,
546    /// unless related borders and and corners are set to `None` as well.
547    ///
548    /// For example, if `TopBorderIntersections` is `None` the first row would look like this:
549    ///
550    /// ```text
551    /// +------ ------+
552    /// | this | test |
553    /// ```
554    ///
555    /// If in addition `TopLeftCorner`,`TopBorder` and `TopRightCorner` would be `None` as well,
556    /// the first line wouldn't be displayed at all.
557    ///
558    /// ```
559    /// use super_table::Table;
560    /// use super_table::presets::UTF8_FULL;
561    /// use super_table::TableComponent::*;
562    ///
563    /// let mut table = Table::new();
564    /// // Load the UTF8_FULL preset
565    /// table.load_preset(UTF8_FULL);
566    /// // Set all outer corners to round UTF8 corners
567    /// // This is basically the same as the UTF8_ROUND_CORNERS modifier
568    /// table.set_style(TopLeftCorner, '╭');
569    /// table.set_style(TopRightCorner, '╮');
570    /// table.set_style(BottomLeftCorner, '╰');
571    /// table.set_style(BottomRightCorner, '╯');
572    /// ```
573    pub fn set_style(&mut self, component: TableComponent, character: char) -> &mut Self {
574        self.style.insert(component, character);
575
576        self
577    }
578
579    /// Get a copy of the char that's currently used for drawing this component.
580    /// ```
581    /// use super_table::Table;
582    /// use super_table::TableComponent::*;
583    ///
584    /// let mut table = Table::new();
585    /// assert_eq!(table.style(TopLeftCorner), Some('+'));
586    /// ```
587    pub fn style(&mut self, component: TableComponent) -> Option<char> {
588        self.style.get(&component).copied()
589    }
590
591    /// Remove the style for a specific component of the table.\
592    /// By default, a space will be used as a placeholder instead.\
593    /// Though, if for instance all components of the left border are removed, the left border won't be displayed.
594    pub fn remove_style(&mut self, component: TableComponent) -> &mut Self {
595        self.style.remove(&component);
596
597        self
598    }
599
600    /// Get a reference to a specific column.
601    pub fn column(&self, index: usize) -> Option<&Column> {
602        self.columns.get(index)
603    }
604
605    /// Get a mutable reference to a specific column.
606    pub fn column_mut(&mut self, index: usize) -> Option<&mut Column> {
607        self.columns.get_mut(index)
608    }
609
610    /// Iterator over all columns
611    pub fn column_iter(&self) -> Iter<'_, Column> {
612        self.columns.iter()
613    }
614
615    /// Get a mutable iterator over all columns.
616    ///
617    /// ```
618    /// use super_table::{Width::*, ColumnConstraint::*, Table};
619    ///
620    /// let mut table = Table::new();
621    /// table.add_row(&vec!["First", "Second", "Third"]);
622    ///
623    /// // Add a ColumnConstraint to each column (left->right)
624    /// // first -> min width of 10
625    /// // second -> max width of 8
626    /// // third -> fixed width of 10
627    /// let constraints = vec![
628    ///     LowerBoundary(Fixed(10)),
629    ///     UpperBoundary(Fixed(8)),
630    ///     Absolute(Fixed(10)),
631    /// ];
632    ///
633    /// // Add the constraints to their respective column
634    /// for (column_index, column) in table.column_iter_mut().enumerate() {
635    ///     let constraint = constraints.get(column_index).unwrap();
636    ///     column.set_constraint(*constraint);
637    /// }
638    /// ```
639    pub fn column_iter_mut(&mut self) -> IterMut<'_, Column> {
640        self.columns.iter_mut()
641    }
642
643    /// Get a mutable iterator over cells of a column.
644    /// The iterator returns a nested `Option<Option<Cell>>`, since there might be
645    /// rows that are missing this specific Cell.
646    ///
647    /// ```
648    /// use super_table::Table;
649    /// let mut table = Table::new();
650    /// table.add_row(&vec!["First", "Second"]);
651    /// table.add_row(&vec!["Third"]);
652    /// table.add_row(&vec!["Fourth", "Fifth"]);
653    ///
654    /// // Create an iterator over the second column
655    /// let mut cell_iter = table.column_cells_iter(1);
656    /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Second");
657    /// assert!(cell_iter.next().unwrap().is_none());
658    /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Fifth");
659    /// assert!(cell_iter.next().is_none());
660    /// ```
661    pub fn column_cells_iter(&self, column_index: usize) -> ColumnCellIter<'_> {
662        ColumnCellIter {
663            rows: &self.rows,
664            column_index,
665            row_index: 0,
666        }
667    }
668
669    /// Get a mutable iterator over cells of a column, including the header cell.
670    /// The header cell will be the very first cell returned.
671    /// The iterator returns a nested `Option<Option<Cell>>`, since there might be
672    /// rows that are missing this specific Cell.
673    ///
674    /// ```
675    /// use super_table::Table;
676    /// let mut table = Table::new();
677    /// table.set_header(&vec!["A", "B"]);
678    /// table.add_row(&vec!["First", "Second"]);
679    /// table.add_row(&vec!["Third"]);
680    /// table.add_row(&vec!["Fourth", "Fifth"]);
681    ///
682    /// // Create an iterator over the second column
683    /// let mut cell_iter = table.column_cells_with_header_iter(1);
684    /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "B");
685    /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Second");
686    /// assert!(cell_iter.next().unwrap().is_none());
687    /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Fifth");
688    /// assert!(cell_iter.next().is_none());
689    /// ```
690    pub fn column_cells_with_header_iter(
691        &self,
692        column_index: usize,
693    ) -> ColumnCellsWithHeaderIter<'_> {
694        ColumnCellsWithHeaderIter {
695            header_checked: false,
696            header: &self.header,
697            rows: &self.rows,
698            column_index,
699            row_index: 0,
700        }
701    }
702
703    /// Reference to a specific row
704    pub fn row(&self, index: usize) -> Option<&Row> {
705        self.rows.get(index)
706    }
707
708    /// Mutable reference to a specific row
709    pub fn row_mut(&mut self, index: usize) -> Option<&mut Row> {
710        self.rows.get_mut(index)
711    }
712
713    /// Iterator over all rows
714    pub fn row_iter(&self) -> Iter<'_, Row> {
715        self.rows.iter()
716    }
717
718    /// Get a mutable iterator over all rows.
719    ///
720    /// ```
721    /// use super_table::Table;
722    /// let mut table = Table::new();
723    /// table.add_row(&vec!["First", "Second", "Third"]);
724    ///
725    /// // Add the constraints to their respective row
726    /// for row in table.row_iter_mut() {
727    ///     row.max_height(5);
728    /// }
729    /// assert!(table.row_iter_mut().len() == 1);
730    /// ```
731    pub fn row_iter_mut(&mut self) -> IterMut<'_, Row> {
732        self.rows.iter_mut()
733    }
734
735    /// Return a vector representing the maximum amount of characters in any line of this column.\
736    ///
737    /// **Attention** This scans the whole current content of the table.
738    /// Accounts for colspan and rowspan when calculating column widths.
739    pub fn column_max_content_widths(&self) -> Vec<u16> {
740        // The vector that'll contain the max widths per column.
741        let mut max_widths = vec![0; self.columns.len()];
742
743        // Track active rowspans: (start_row, start_col) -> (remaining_rows, colspan)
744        use std::collections::HashMap;
745        let mut active_rowspans: HashMap<(usize, usize), (u16, u16)> = HashMap::new();
746
747        // Helper function to check if a column is occupied by a rowspan
748        fn is_col_occupied_by_rowspan(
749            active_rowspans: &HashMap<(usize, usize), (u16, u16)>,
750            row_index: usize,
751            col_index: usize,
752        ) -> bool {
753            for ((start_row, start_col), (remaining_rows, colspan)) in active_rowspans.iter() {
754                if *start_row < row_index
755                    && *remaining_rows > 0
756                    && *start_col <= col_index
757                    && col_index < *start_col + *colspan as usize
758                {
759                    return true;
760                }
761            }
762            false
763        }
764
765        // Helper function to update max widths for a row, accounting for colspan and rowspan
766        fn set_max_content_widths(
767            max_widths: &mut [u16],
768            row: &Row,
769            row_index: usize,
770            active_rowspans: &mut HashMap<(usize, usize), (u16, u16)>,
771        ) {
772            // Get the max width for each cell of the row
773            let row_max_widths = row.max_content_widths();
774            let mut col_index = 0;
775
776            for (cell_index, width) in row_max_widths.iter().enumerate() {
777                // Skip column positions that are occupied by rowspan from above
778                while col_index < max_widths.len()
779                    && is_col_occupied_by_rowspan(active_rowspans, row_index, col_index)
780                {
781                    col_index += 1;
782                }
783
784                if col_index >= max_widths.len() {
785                    break;
786                }
787
788                let cell = &row.cells[cell_index];
789                let colspan = cell.colspan() as usize;
790                let rowspan = cell.rowspan();
791                let mut cell_width = (*width).try_into().unwrap_or(u16::MAX);
792                // A column's content is at least 1 char wide.
793                cell_width = std::cmp::max(1, cell_width);
794
795                if colspan == 1 {
796                    // Simple case: no colspan, just update the column directly
797                    // For rowspan cells, the full width applies to the column
798                    let current_max = max_widths[col_index];
799                    if current_max < cell_width {
800                        max_widths[col_index] = cell_width;
801                    }
802                    col_index += 1;
803                } else {
804                    // Colspan case: ensure the sum of the spanned columns can accommodate the cell
805                    // The border/content formatting code sums content_widths and adds padding once
806                    // So we need content_widths to sum to cell_width (padding is added separately)
807                    // But we also need to ensure the columns are wide enough for other cells
808                    // So we calculate the minimum needed, then check if we need to increase it
809                    let total_content_needed = cell_width;
810                    let min_width_per_col = total_content_needed / colspan as u16;
811                    let remainder = total_content_needed % colspan as u16;
812
813                    // First, set minimum widths
814                    for i in 0..colspan {
815                        if col_index + i >= max_widths.len() {
816                            break;
817                        }
818                        let width_for_col = if i < remainder as usize {
819                            min_width_per_col + 1
820                        } else {
821                            min_width_per_col
822                        };
823                        let current_max = max_widths[col_index + i];
824                        if current_max < width_for_col {
825                            max_widths[col_index + i] = width_for_col;
826                        }
827                    }
828
829                    // Then, check if the sum is sufficient (the largest accumulation counts)
830                    // If other cells in these columns need more space, they'll increase the widths
831                    // and we'll need to ensure the colspan cell still fits
832                    let current_sum: u16 = (0..colspan)
833                        .map(|i| {
834                            if col_index + i < max_widths.len() {
835                                max_widths[col_index + i]
836                            } else {
837                                0
838                            }
839                        })
840                        .sum();
841
842                    // If current sum is less than needed, increase columns proportionally
843                    if current_sum < total_content_needed {
844                        let diff = total_content_needed - current_sum;
845                        let per_col_inc = diff / colspan as u16;
846                        let remainder_inc = diff % colspan as u16;
847                        for i in 0..colspan {
848                            if col_index + i < max_widths.len() {
849                                let inc = if i < remainder_inc as usize {
850                                    per_col_inc + 1
851                                } else {
852                                    per_col_inc
853                                };
854                                max_widths[col_index + i] += inc;
855                            }
856                        }
857                    }
858
859                    col_index += colspan;
860                }
861
862                // Register rowspan if needed
863                if rowspan > 1 {
864                    active_rowspans.insert(
865                        (row_index, col_index - colspan),
866                        (rowspan - 1, colspan as u16),
867                    );
868                }
869            }
870        }
871
872        // Process header if it exists
873        if let Some(header) = &self.header {
874            set_max_content_widths(&mut max_widths, header, 0, &mut active_rowspans);
875        }
876
877        // Iterate through all rows of the table
878        for (row_idx, row) in self.rows.iter().enumerate() {
879            let actual_row_index = if self.header.is_some() {
880                row_idx + 1
881            } else {
882                row_idx
883            };
884            set_max_content_widths(&mut max_widths, row, actual_row_index, &mut active_rowspans);
885            // Advance rowspans after processing this row
886            // First decrement remaining_rows for all active spans that have been displayed
887            for ((start_row, _), (remaining_rows, _)) in active_rowspans.iter_mut() {
888                if *start_row < actual_row_index && *remaining_rows > 0 {
889                    *remaining_rows -= 1;
890                }
891            }
892            // Then remove expired spans (remaining_rows == 0 means it was just displayed in its last row)
893            active_rowspans.retain(|_, (remaining_rows, _)| *remaining_rows > 0);
894        }
895
896        // Second pass: ensure colspan cells have enough space (the largest accumulation counts)
897        // After all cells have been processed, check if any colspan cell needs more space
898        // We compare total widths (content + padding), not just content widths
899        active_rowspans.clear();
900        if let Some(header) = &self.header {
901            let row_max_widths = header.max_content_widths();
902            let mut col_index = 0;
903            for (cell_index, width) in row_max_widths.iter().enumerate() {
904                let cell = &header.cells[cell_index];
905                let colspan = cell.colspan() as usize;
906                let cell_width = (*width).try_into().unwrap_or(u16::MAX);
907                let cell_width = std::cmp::max(1, cell_width);
908
909                if colspan > 1 {
910                    // Colspan cell needs: content_widths should sum to cell_width
911                    // Padding is added once during formatting, not per column
912                    let colspan_cell_need = cell_width;
913
914                    // Sum the current content widths of the spanned columns
915                    let current_content_sum: u16 = (0..colspan)
916                        .map(|i| {
917                            if col_index + i < max_widths.len() {
918                                max_widths[col_index + i]
919                            } else {
920                                0
921                            }
922                        })
923                        .sum();
924
925                    // Ensure the sum of content widths is at least cell_width
926                    if current_content_sum < colspan_cell_need {
927                        let diff = colspan_cell_need - current_content_sum;
928                        let per_col_inc = diff / colspan as u16;
929                        let remainder_inc = diff % colspan as u16;
930                        for i in 0..colspan {
931                            if col_index + i < max_widths.len() {
932                                let inc = if i < remainder_inc as usize {
933                                    per_col_inc + 1
934                                } else {
935                                    per_col_inc
936                                };
937                                max_widths[col_index + i] += inc;
938                            }
939                        }
940                    }
941                }
942                col_index += colspan;
943            }
944        }
945
946        for row in &self.rows {
947            let row_max_widths = row.max_content_widths();
948            let mut col_index = 0;
949            for (cell_index, width) in row_max_widths.iter().enumerate() {
950                let cell = &row.cells[cell_index];
951                let colspan = cell.colspan() as usize;
952                let cell_width = (*width).try_into().unwrap_or(u16::MAX);
953                let cell_width = std::cmp::max(1, cell_width);
954
955                if colspan > 1 {
956                    // Colspan cell needs: content_widths should sum to cell_width
957                    // Padding is added once during formatting, not per column
958                    let colspan_cell_need = cell_width;
959                    let current_sum: u16 = (0..colspan)
960                        .map(|i| {
961                            if col_index + i < max_widths.len() {
962                                max_widths[col_index + i]
963                            } else {
964                                0
965                            }
966                        })
967                        .sum();
968
969                    // Ensure the sum of content widths is at least cell_width
970                    if current_sum < colspan_cell_need {
971                        let diff = colspan_cell_need - current_sum;
972                        let per_col_inc = diff / colspan as u16;
973                        let remainder_inc = diff % colspan as u16;
974                        for i in 0..colspan {
975                            if col_index + i < max_widths.len() {
976                                let inc = if i < remainder_inc as usize {
977                                    per_col_inc + 1
978                                } else {
979                                    per_col_inc
980                                };
981                                max_widths[col_index + i] += inc;
982                            }
983                        }
984                    }
985                }
986                col_index += colspan;
987            }
988        }
989
990        max_widths
991    }
992
993    pub(crate) fn style_or_default(&self, component: TableComponent) -> String {
994        match self.style.get(&component) {
995            None => " ".to_string(),
996            Some(character) => character.to_string(),
997        }
998    }
999
1000    pub(crate) fn style_exists(&self, component: TableComponent) -> bool {
1001        self.style.contains_key(&component)
1002    }
1003
1004    /// Autogenerate new columns, if a row is added with more cells than existing columns.
1005    /// Accounts for colspan when determining the required number of columns.
1006    fn autogenerate_columns(&mut self, row: &Row) {
1007        let effective_cols = row.effective_column_count();
1008        if effective_cols > self.columns.len() {
1009            for index in self.columns.len()..effective_cols {
1010                self.columns.push(Column::new(index));
1011            }
1012        }
1013    }
1014
1015    /// Calling this might be necessary if you add new cells to rows that're already added to the
1016    /// table.
1017    ///
1018    /// If more cells than're currently know to the table are added to that row,
1019    /// the table cannot know about these, since new [Column]s are only
1020    /// automatically detected when a new row is added.
1021    ///
1022    /// To make sure everything works as expected, just call this function if you're adding cells
1023    /// to rows that're already added to the table.
1024    ///
1025    /// Accounts for colspan when determining the required number of columns.
1026    pub fn discover_columns(&mut self) {
1027        // Check header if it exists
1028        if let Some(header) = &self.header {
1029            let effective_cols = header.effective_column_count();
1030            if effective_cols > self.columns.len() {
1031                for index in self.columns.len()..effective_cols {
1032                    self.columns.push(Column::new(index));
1033                }
1034            }
1035        }
1036
1037        // Check all rows
1038        for row in self.rows.iter() {
1039            let effective_cols = row.effective_column_count();
1040            if effective_cols > self.columns.len() {
1041                for index in self.columns.len()..effective_cols {
1042                    self.columns.push(Column::new(index));
1043                }
1044            }
1045        }
1046    }
1047}
1048
1049/// An iterator over cells of a specific column.
1050/// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in
1051/// `Table::rows`. This type is returned by [Table::column_cells_iter].
1052pub struct ColumnCellIter<'a> {
1053    rows: &'a [Row],
1054    column_index: usize,
1055    row_index: usize,
1056}
1057
1058impl<'a> Iterator for ColumnCellIter<'a> {
1059    type Item = Option<&'a Cell>;
1060    fn next(&mut self) -> Option<Option<&'a Cell>> {
1061        // Check if there's a next row
1062        if let Some(row) = self.rows.get(self.row_index) {
1063            self.row_index += 1;
1064
1065            // Return the cell (if it exists).
1066            return Some(row.cells.get(self.column_index));
1067        }
1068
1069        None
1070    }
1071}
1072
1073/// An iterator over cells of a specific column.
1074/// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in
1075/// `Table::rows`. This type is returned by [Table::column_cells_iter].
1076pub struct ColumnCellsWithHeaderIter<'a> {
1077    header_checked: bool,
1078    header: &'a Option<Row>,
1079    rows: &'a [Row],
1080    column_index: usize,
1081    row_index: usize,
1082}
1083
1084impl<'a> Iterator for ColumnCellsWithHeaderIter<'a> {
1085    type Item = Option<&'a Cell>;
1086    fn next(&mut self) -> Option<Option<&'a Cell>> {
1087        // Get the header as the first cell
1088        if !self.header_checked {
1089            self.header_checked = true;
1090
1091            return match self.header {
1092                Some(header) => {
1093                    // Return the cell (if it exists).
1094                    Some(header.cells.get(self.column_index))
1095                }
1096                None => Some(None),
1097            };
1098        }
1099
1100        // Check if there's a next row
1101        if let Some(row) = self.rows.get(self.row_index) {
1102            self.row_index += 1;
1103
1104            // Return the cell (if it exists).
1105            return Some(row.cells.get(self.column_index));
1106        }
1107
1108        None
1109    }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115
1116    #[test]
1117    fn test_column_generation() {
1118        let mut table = Table::new();
1119        table.set_header(vec!["thr", "four", "fivef"]);
1120
1121        // When adding a new row, columns are automatically generated
1122        assert_eq!(table.columns.len(), 3);
1123        // The max content width is also correctly set for each column
1124        assert_eq!(table.column_max_content_widths(), vec![3, 4, 5]);
1125
1126        // When adding a new row, the max content width is updated accordingly
1127        table.add_row(vec!["four", "fivef", "very long text with 23"]);
1128        assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]);
1129
1130        // Now add a row that has column lines. The max content width shouldn't change
1131        table.add_row(vec!["", "", "shorter"]);
1132        assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]);
1133
1134        println!("{table}");
1135    }
1136}