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 + 2) to get the right total width
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 + 2;
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 + padding (2 total, added once for the cell)
911 // The border space " | " (3 chars) is added in content formatting, not here
912 let colspan_cell_need = cell_width + 2;
913
914 // Just add up the total widths of the spanned columns: (content_width + 2) for each
915 let column_widths_sum: u16 = (0..colspan)
916 .map(|i| {
917 if col_index + i < max_widths.len() {
918 max_widths[col_index + i] + 2 // content_width + padding = total width
919 } else {
920 0
921 }
922 })
923 .sum();
924
925 // The largest accumulation: use max of colspan_cell_need (with border compensation) and column widths sum
926 let target_total = std::cmp::max(colspan_cell_need, column_widths_sum);
927
928 // For the colspan cell, content_widths sum + 2 padding = target_total
929 // So content_widths should sum to target_total - 2
930 let target_content_sum = target_total.saturating_sub(2);
931 let current_content_sum: u16 = (0..colspan)
932 .map(|i| {
933 if col_index + i < max_widths.len() {
934 max_widths[col_index + i]
935 } else {
936 0
937 }
938 })
939 .sum();
940
941 if current_content_sum < target_content_sum {
942 let diff = target_content_sum - current_content_sum;
943 let per_col_inc = diff / colspan as u16;
944 let remainder_inc = diff % colspan as u16;
945 for i in 0..colspan {
946 if col_index + i < max_widths.len() {
947 let inc = if i < remainder_inc as usize {
948 per_col_inc + 1
949 } else {
950 per_col_inc
951 };
952 max_widths[col_index + i] += inc;
953 }
954 }
955 }
956 }
957 col_index += colspan;
958 }
959 }
960
961 for row in &self.rows {
962 let row_max_widths = row.max_content_widths();
963 let mut col_index = 0;
964 for (cell_index, width) in row_max_widths.iter().enumerate() {
965 let cell = &row.cells[cell_index];
966 let colspan = cell.colspan() as usize;
967 let cell_width = (*width).try_into().unwrap_or(u16::MAX);
968 let cell_width = std::cmp::max(1, cell_width);
969
970 if colspan > 1 {
971 // The largest accumulation counts: use max of colspan cell need and current column sum
972 let colspan_cell_need = cell_width + 2;
973 let current_sum: u16 = (0..colspan)
974 .map(|i| {
975 if col_index + i < max_widths.len() {
976 max_widths[col_index + i]
977 } else {
978 0
979 }
980 })
981 .sum();
982
983 // Use the maximum: if columns already sum to more than the cell needs, keep that
984 // Otherwise, increase to meet the cell's need
985 let target_sum = std::cmp::max(current_sum, colspan_cell_need);
986
987 if current_sum < target_sum {
988 let diff = target_sum - current_sum;
989 let per_col_inc = diff / colspan as u16;
990 let remainder_inc = diff % colspan as u16;
991 for i in 0..colspan {
992 if col_index + i < max_widths.len() {
993 let inc = if i < remainder_inc as usize {
994 per_col_inc + 1
995 } else {
996 per_col_inc
997 };
998 max_widths[col_index + i] += inc;
999 }
1000 }
1001 }
1002 }
1003 col_index += colspan;
1004 }
1005 }
1006
1007 max_widths
1008 }
1009
1010 pub(crate) fn style_or_default(&self, component: TableComponent) -> String {
1011 match self.style.get(&component) {
1012 None => " ".to_string(),
1013 Some(character) => character.to_string(),
1014 }
1015 }
1016
1017 pub(crate) fn style_exists(&self, component: TableComponent) -> bool {
1018 self.style.contains_key(&component)
1019 }
1020
1021 /// Autogenerate new columns, if a row is added with more cells than existing columns.
1022 /// Accounts for colspan when determining the required number of columns.
1023 fn autogenerate_columns(&mut self, row: &Row) {
1024 let effective_cols = row.effective_column_count();
1025 if effective_cols > self.columns.len() {
1026 for index in self.columns.len()..effective_cols {
1027 self.columns.push(Column::new(index));
1028 }
1029 }
1030 }
1031
1032 /// Calling this might be necessary if you add new cells to rows that're already added to the
1033 /// table.
1034 ///
1035 /// If more cells than're currently know to the table are added to that row,
1036 /// the table cannot know about these, since new [Column]s are only
1037 /// automatically detected when a new row is added.
1038 ///
1039 /// To make sure everything works as expected, just call this function if you're adding cells
1040 /// to rows that're already added to the table.
1041 ///
1042 /// Accounts for colspan when determining the required number of columns.
1043 pub fn discover_columns(&mut self) {
1044 // Check header if it exists
1045 if let Some(header) = &self.header {
1046 let effective_cols = header.effective_column_count();
1047 if effective_cols > self.columns.len() {
1048 for index in self.columns.len()..effective_cols {
1049 self.columns.push(Column::new(index));
1050 }
1051 }
1052 }
1053
1054 // Check all rows
1055 for row in self.rows.iter() {
1056 let effective_cols = row.effective_column_count();
1057 if effective_cols > self.columns.len() {
1058 for index in self.columns.len()..effective_cols {
1059 self.columns.push(Column::new(index));
1060 }
1061 }
1062 }
1063 }
1064}
1065
1066/// An iterator over cells of a specific column.
1067/// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in
1068/// `Table::rows`. This type is returned by [Table::column_cells_iter].
1069pub struct ColumnCellIter<'a> {
1070 rows: &'a [Row],
1071 column_index: usize,
1072 row_index: usize,
1073}
1074
1075impl<'a> Iterator for ColumnCellIter<'a> {
1076 type Item = Option<&'a Cell>;
1077 fn next(&mut self) -> Option<Option<&'a Cell>> {
1078 // Check if there's a next row
1079 if let Some(row) = self.rows.get(self.row_index) {
1080 self.row_index += 1;
1081
1082 // Return the cell (if it exists).
1083 return Some(row.cells.get(self.column_index));
1084 }
1085
1086 None
1087 }
1088}
1089
1090/// An iterator over cells of a specific column.
1091/// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in
1092/// `Table::rows`. This type is returned by [Table::column_cells_iter].
1093pub struct ColumnCellsWithHeaderIter<'a> {
1094 header_checked: bool,
1095 header: &'a Option<Row>,
1096 rows: &'a [Row],
1097 column_index: usize,
1098 row_index: usize,
1099}
1100
1101impl<'a> Iterator for ColumnCellsWithHeaderIter<'a> {
1102 type Item = Option<&'a Cell>;
1103 fn next(&mut self) -> Option<Option<&'a Cell>> {
1104 // Get the header as the first cell
1105 if !self.header_checked {
1106 self.header_checked = true;
1107
1108 return match self.header {
1109 Some(header) => {
1110 // Return the cell (if it exists).
1111 Some(header.cells.get(self.column_index))
1112 }
1113 None => Some(None),
1114 };
1115 }
1116
1117 // Check if there's a next row
1118 if let Some(row) = self.rows.get(self.row_index) {
1119 self.row_index += 1;
1120
1121 // Return the cell (if it exists).
1122 return Some(row.cells.get(self.column_index));
1123 }
1124
1125 None
1126 }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132
1133 #[test]
1134 fn test_column_generation() {
1135 let mut table = Table::new();
1136 table.set_header(vec!["thr", "four", "fivef"]);
1137
1138 // When adding a new row, columns are automatically generated
1139 assert_eq!(table.columns.len(), 3);
1140 // The max content width is also correctly set for each column
1141 assert_eq!(table.column_max_content_widths(), vec![3, 4, 5]);
1142
1143 // When adding a new row, the max content width is updated accordingly
1144 table.add_row(vec!["four", "fivef", "very long text with 23"]);
1145 assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]);
1146
1147 // Now add a row that has column lines. The max content width shouldn't change
1148 table.add_row(vec!["", "", "shorter"]);
1149 assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]);
1150
1151 println!("{table}");
1152 }
1153}