Skip to main content

tiny_table/
table.rs

1//! Table rendering primitives and formatting logic.
2//!
3//! The crate root re-exports the main types from this module, so most users
4//! will interact with [`Table`], [`Column`], [`Cell`], [`Trunc`], and
5//! [`Align`].
6//!
7//! The renderer works in three steps:
8//!
9//! 1. Measure content width for headers and rows.
10//! 2. Apply column width rules, truncation, and wrapping.
11//! 3. Emit a bordered table using the active terminal width when available.
12//!
13//! The public API is intentionally small. `Table` owns the rows, `Column`
14//! defines the schema, and `Cell` gives you per-value overrides when needed.
15
16#[cfg(feature = "style")]
17pub use crate::color::{Color, CustomColor};
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use terminal_size::{Width, terminal_size};
21
22/// Style actions and macros for applying colors and formatting to cells and columns.
23#[cfg(feature = "style")]
24pub mod style;
25mod text;
26
27#[cfg(feature = "style")]
28pub use crate::impl_style_methods;
29#[cfg(feature = "style")]
30use style::{StyleAction, apply_style_actions};
31use text::{layout_line, split_lines, strip_ansi, truncate_line, visible_len};
32
33const ANSI_RESET: &str = "\x1b[0m";
34
35/// Characters used to draw table borders and joints.
36///
37/// The renderer currently uses the Unicode preset from [`TableStyle::unicode`].
38/// Section and separator rows can also use their own `TableStyle` snapshot,
39/// which lets you mix multiple visual themes in a single table.
40/// This type exists so the border characters are grouped in one place if you
41/// want to experiment with alternate themes or extend the renderer.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct TableStyle {
44    /// Top-left corner character.
45    pub top_left: &'static str,
46    /// Top-right corner character.
47    pub top_right: &'static str,
48    /// Bottom-left corner character.
49    pub bottom_left: &'static str,
50    /// Bottom-right corner character.
51    pub bottom_right: &'static str,
52    /// Horizontal border character.
53    pub horiz: &'static str,
54    /// Vertical border character.
55    pub vert: &'static str,
56    /// Top border joint between columns.
57    pub top_joint: &'static str,
58    /// Left border character for section separators and middle rules.
59    pub mid_left: &'static str,
60    /// Right border character for section separators and middle rules.
61    pub mid_right: &'static str,
62    /// Joint character for interior separators.
63    pub mid_joint: &'static str,
64    /// Bottom border joint between columns.
65    pub bottom_joint: &'static str,
66}
67
68impl TableStyle {
69    /// Return the default Unicode border style.
70    pub fn unicode() -> Self {
71        TableStyle {
72            top_left: "┌",
73            top_right: "┐",
74            bottom_left: "└",
75            bottom_right: "┘",
76            horiz: "─",
77            vert: "│",
78            top_joint: "┬",
79            mid_left: "├",
80            mid_right: "┤",
81            mid_joint: "┼",
82            bottom_joint: "┴",
83        }
84    }
85
86    /// Convert a [`SectionStyle`] into a [`TableStyle`] by filling in the missing fields
87    pub fn from_section_style(section_style: SectionStyle) -> Self {
88        TableStyle {
89            horiz: section_style.horiz,
90            mid_left: section_style.mid_left,
91            mid_right: section_style.mid_right,
92            mid_joint: section_style.mid_joint,
93            ..TableStyle::unicode()
94        }
95    }
96}
97
98/// Characters used to overwrite section and separator styles.
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub struct SectionStyle {
101    /// Horizontal border character.
102    pub horiz: &'static str,
103    /// Left border character for section separators and middle rules.
104    pub mid_left: &'static str,
105    /// Right border character for section separators and middle rules.
106    pub mid_right: &'static str,
107    /// Joint character for interior separators.
108    pub mid_joint: &'static str,
109}
110
111impl SectionStyle {
112    /// Return the default Unicode border style for sections and separators.
113    pub fn unicode() -> Self {
114        SectionStyle::from_table_style(TableStyle::unicode())
115    }
116
117    /// Convert a [`TableStyle`] into a [`SectionStyle`] by taking the relevant fields
118    pub fn from_table_style(table_style: TableStyle) -> Self {
119        SectionStyle {
120            horiz: table_style.horiz,
121            mid_left: table_style.mid_left,
122            mid_right: table_style.mid_right,
123            mid_joint: table_style.mid_joint,
124        }
125    }
126}
127
128/// How to handle content that does not fit inside the available column width.
129///
130/// Truncation is applied after the column width is resolved. If you need the
131/// full content to remain visible, use [`Trunc::NewLine`] to wrap onto more
132/// lines instead of clipping the text.
133///
134/// # Examples
135///
136/// ```rust
137/// use tiny_table::{Cell, Trunc};
138///
139/// let cell = Cell::new("abcdefghij").truncate(Trunc::Middle);
140/// ```
141#[allow(dead_code)]
142#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
143pub enum Trunc {
144    /// Keep the beginning of the text and add an ellipsis at the end.
145    #[default]
146    End,
147    /// Keep the end of the text and add an ellipsis at the start.
148    Start,
149    /// Keep the start and end of the text with an ellipsis in the middle.
150    Middle,
151    /// Wrap onto multiple lines instead of truncating.
152    NewLine,
153}
154
155/// Alignment options for any content supporting alignment.
156///
157/// # Examples
158///
159/// ```rust
160/// use tiny_table::{Align, Table};
161///
162/// let mut table = Table::new();
163/// table.add_section("Team").align(Align::Right);
164/// ```
165#[allow(dead_code)]
166#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
167pub enum Align {
168    /// Center the content within the available width.
169    #[default]
170    Center,
171    /// Place the content toward the left side of the available width.
172    Left,
173    /// Place the content toward the right side of the available width.
174    Right,
175}
176
177/// Width policy for a table column.
178///
179/// `Auto` keeps the column at content width unless a column-specific width is
180/// applied. `Fixed` uses an exact terminal-cell width. `Fraction` claims a
181/// proportional share of the available width. `Fill` consumes whatever width
182/// remains after fixed, content-based, and fractional columns are resolved.
183///
184/// # Examples
185///
186/// ```rust
187/// use tiny_table::ColumnWidth;
188///
189/// let fixed = ColumnWidth::fixed(12);
190/// let fraction = ColumnWidth::fraction(0.5);
191/// assert_eq!(fixed, ColumnWidth::Fixed(12));
192/// assert_eq!(fraction, ColumnWidth::Fraction(0.5));
193/// ```
194#[derive(Clone, Copy, Debug, PartialEq, Default)]
195pub enum ColumnWidth {
196    /// Size the column from its content and any explicit constraints.
197    #[default]
198    Auto,
199    /// Use an exact width in terminal cells.
200    Fixed(usize),
201    /// Allocate a fraction of the remaining available width.
202    Fraction(f64),
203    /// Fill the remaining available width.
204    Fill,
205}
206
207impl ColumnWidth {
208    /// Create a fixed-width column.
209    pub fn fixed(width: usize) -> Self {
210        Self::Fixed(width)
211    }
212
213    /// Create a fractional-width column.
214    pub fn fraction(fraction: f64) -> Self {
215        Self::Fraction(fraction)
216    }
217
218    /// Create a column that takes the remaining available width.
219    pub fn fill() -> Self {
220        Self::Fill
221    }
222}
223
224macro_rules! impl_column_width_from_int {
225    ($($ty:ty),* $(,)?) => {
226        $(impl From<$ty> for ColumnWidth {
227            fn from(width: $ty) -> Self {
228                Self::Fixed(width.max(0) as usize)
229            }
230        })*
231    };
232}
233
234impl_column_width_from_int!(
235    usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128
236);
237
238impl From<f32> for ColumnWidth {
239    fn from(fraction: f32) -> Self {
240        Self::Fraction(fraction as f64)
241    }
242}
243
244impl From<f64> for ColumnWidth {
245    fn from(fraction: f64) -> Self {
246        Self::Fraction(fraction)
247    }
248}
249
250/// Selector used to apply styling to a column.
251///
252/// [`Index`](ColumnTarget::Index) is zero-based. [`Header`](ColumnTarget::Header)
253/// matches the exact header text supplied to [`Column::new`].
254///
255/// # Examples
256///
257/// ```rust
258/// use tiny_table::ColumnTarget;
259///
260/// assert_eq!(ColumnTarget::from(0usize), ColumnTarget::Index(0));
261/// assert_eq!(ColumnTarget::from("Name"), ColumnTarget::Header("Name".to_string()));
262/// ```
263#[derive(Clone, Debug, Eq, PartialEq, Hash)]
264pub enum ColumnTarget {
265    /// Target a column by zero-based index.
266    Index(usize),
267    /// Target a column by its header text.
268    Header(String),
269}
270
271impl From<usize> for ColumnTarget {
272    fn from(index: usize) -> Self {
273        Self::Index(index)
274    }
275}
276
277impl From<&str> for ColumnTarget {
278    fn from(header: &str) -> Self {
279        Self::Header(header.to_string())
280    }
281}
282
283impl From<String> for ColumnTarget {
284    fn from(header: String) -> Self {
285        Self::Header(header)
286    }
287}
288
289/// A column definition that bundles the header text with default styling.
290///
291/// Use this when you already know your schema and want the table to inherit
292/// width, truncation, and color defaults from the column definition.
293///
294/// # Examples
295///
296/// ```rust
297/// use tiny_table::{Column, Color, Trunc};
298///
299/// let column = Column::new("Status")
300///     .color(Color::BrightGreen)
301///     .width(12)
302///     .truncate(Trunc::End);
303/// ```
304pub struct Column {
305    header: Cell,
306    style: ColumnStyle,
307}
308
309impl Column {
310    /// Create a new column definition from a header value.
311    pub fn new(header: impl Into<Cell>) -> Self {
312        Self {
313            header: header.into(),
314            style: ColumnStyle::default(),
315        }
316    }
317
318    /// Set the preferred width for this column.
319    pub fn width(mut self, width: impl Into<ColumnWidth>) -> Self {
320        self.style.width = Some(width.into());
321        self
322    }
323
324    /// Alias for [`Column::width`].
325    pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
326        self.width(width)
327    }
328
329    /// Set the default truncation strategy used by this column.
330    pub fn truncate(mut self, truncation: Trunc) -> Self {
331        self.style.truncation = Some(truncation);
332        self
333    }
334
335    /// Set the default text alignment for this column.
336    pub fn align(mut self, align: Align) -> Self {
337        self.style.align = Some(align);
338        self
339    }
340}
341
342#[cfg(feature = "style")]
343impl_style_methods!(Column, |mut column: Column, action| {
344    column.style.styles.push(action);
345    column
346});
347
348#[derive(Clone, Debug, Default)]
349struct ColumnStyle {
350    #[cfg(feature = "style")]
351    styles: Vec<StyleAction>,
352    width: Option<ColumnWidth>,
353    truncation: Option<Trunc>,
354    align: Option<Align>,
355}
356
357impl ColumnStyle {
358    fn merge(&mut self, other: &ColumnStyle) {
359        #[cfg(feature = "style")]
360        self.styles.extend_from_slice(&other.styles);
361
362        if other.width.is_some() {
363            self.width = other.width;
364        }
365
366        if other.truncation.is_some() {
367            self.truncation = other.truncation;
368        }
369
370        if other.align.is_some() {
371            self.align = other.align;
372        }
373    }
374}
375
376/// A single table cell with optional styling overrides.
377///
378/// Cells are usually created from strings and then used in [`Table::add_row`].
379/// Any color or truncation set on the cell takes priority over the owning
380/// column's defaults.
381///
382/// # Examples
383///
384/// ```rust
385/// use tiny_table::{Cell, Color, Trunc};
386///
387/// let cell = Cell::new("warning")
388///     .color(Color::BrightRed)
389///     .truncate(Trunc::Middle);
390/// ```
391pub struct Cell {
392    content: String,
393    #[cfg(feature = "style")]
394    styles: Vec<StyleAction>,
395    truncation: Option<Trunc>,
396    align: Option<Align>,
397}
398
399struct PreparedCell {
400    lines: Vec<String>,
401    align: Align,
402}
403
404enum TableRow {
405    Cells(Vec<Cell>),
406    Section(SectionRow),
407}
408
409enum PreparedRow {
410    Cells(Vec<PreparedCell>),
411    Section(SectionRow),
412}
413
414#[derive(Clone, Debug)]
415struct SectionRow {
416    title: String,
417    align: Align,
418    style: TableStyle,
419}
420
421/// Builder returned by [`Table::add_section`] and [`Table::add_separator`].
422pub struct SectionBuilder<'a> {
423    table: &'a mut Table,
424    row_index: usize,
425}
426
427/// Builder returned by [`Table::column`] for per-column styling.
428///
429/// Use this to override color, width, or truncation for one column without
430/// changing the schema-wide defaults defined by [`Column`].
431pub struct ColumnBuilder<'a> {
432    table: &'a mut Table,
433    target: ColumnTarget,
434}
435
436impl Cell {
437    /// Create a new cell from display content.
438    pub fn new(content: impl ToString) -> Self {
439        Self {
440            content: content.to_string(),
441            #[cfg(feature = "style")]
442            styles: Vec::new(),
443            truncation: None,
444            align: None,
445        }
446    }
447
448    /// Set a truncation mode for this cell, overriding any column default.
449    #[must_use]
450    pub fn truncate(mut self, truncation: Trunc) -> Self {
451        self.truncation = Some(truncation);
452        self
453    }
454
455    /// Set the text alignment for this cell, overriding any column default.
456    #[must_use]
457    pub fn align(mut self, align: Align) -> Self {
458        self.align = Some(align);
459        self
460    }
461}
462
463#[cfg(feature = "style")]
464impl_style_methods!(Cell, |mut cell: Cell, action| {
465    cell.styles.push(action);
466    cell
467});
468
469impl From<&str> for Cell {
470    fn from(content: &str) -> Self {
471        Self::new(content)
472    }
473}
474
475impl From<String> for Cell {
476    fn from(content: String) -> Self {
477        Self::new(content)
478    }
479}
480
481/// Main table type for terminal output.
482///
483/// A table owns its headers, rows, and column styling. Content is measured at
484/// render time, so the output can adapt to the current terminal width without
485/// manual layout code from the caller.
486///
487/// # Typical Flow
488///
489/// 1. Build the schema with [`Table::with_columns`] or [`Table::new`].
490/// 2. Add rows with [`Table::add_row`].
491/// 3. Adjust columns with [`Table::column`] if you need per-column overrides.
492/// 4. Call [`Table::render`] or print the table directly.
493///
494/// # Example
495///
496/// ```rust
497/// use tiny_table::{Cell, Column, Align, Table, Trunc};
498///
499/// let mut table = Table::with_columns(vec![
500///     Column::new("Name").width(0.35),
501///     Column::new("Role").truncate(Trunc::Middle),
502///     Column::new("Status"),
503/// ]);
504///
505/// table.add_section("Team").align(Align::Center);
506/// table.add_row(vec![
507///     Cell::new("Ada Lovelace"),
508///     Cell::new("Principal Engineer"),
509///     Cell::new("Active"),
510/// ]);
511///
512/// let rendered = table.render();
513/// assert!(rendered.contains("Name"));
514/// assert!(rendered.contains("Ada Lovelace"));
515/// ```
516pub struct Table {
517    headers: Vec<Cell>,
518    rows: Vec<TableRow>,
519    column_defaults: Vec<ColumnStyle>,
520    column_overrides: HashMap<ColumnTarget, ColumnStyle>,
521    style: TableStyle,
522    section_style: Option<SectionStyle>,
523    separator_style: Option<SectionStyle>,
524}
525
526impl Table {
527    /// Create an empty table using the default Unicode border style.
528    pub fn new() -> Self {
529        Self {
530            headers: Vec::new(),
531            rows: Vec::new(),
532            column_defaults: Vec::new(),
533            column_overrides: HashMap::new(),
534            style: TableStyle::unicode(),
535            section_style: None,
536            separator_style: None,
537        }
538    }
539
540    /// Build a table from an explicit column schema.
541    ///
542    /// Each [`Column`] contributes the header text and the default formatting
543    /// for that column.
544    pub fn with_columns(columns: impl IntoIterator<Item = Column>) -> Self {
545        let mut table = Self::new();
546        table.set_columns(columns);
547        table
548    }
549
550    /// Replace the default border style for this table.
551    pub fn with_style(mut self, style: TableStyle) -> Self {
552        self.style = style;
553        self
554    }
555
556    /// Set the default style used for all section rows added with [`Table::add_section`].
557    ///
558    /// Individual sections can still override this by calling `.style()` on the
559    /// [`SectionBuilder`] returned by [`Table::add_section`].
560    pub fn with_section_style(mut self, style: SectionStyle) -> Self {
561        self.section_style = Some(style);
562        self
563    }
564
565    /// Set the default style used for all separator rows added with [`Table::add_separator`].
566    ///
567    /// Individual separators can still override this by calling `.style()` on the
568    /// [`SectionBuilder`] returned by [`Table::add_separator`].
569    pub fn with_separator_style(mut self, style: SectionStyle) -> Self {
570        self.separator_style = Some(style);
571        self
572    }
573
574    /// Replace the table schema with a new set of columns.
575    ///
576    /// This clears existing headers and column-specific overrides before the
577    /// new schema is applied.
578    pub fn set_columns(&mut self, columns: impl IntoIterator<Item = Column>) {
579        let (headers, column_defaults): (Vec<_>, Vec<_>) = columns
580            .into_iter()
581            .map(|Column { header, style }| (header, style))
582            .unzip();
583
584        self.headers = headers;
585        self.column_defaults = column_defaults;
586        self.column_overrides.clear();
587    }
588
589    /// Add a data row.
590    ///
591    /// Short rows are padded with empty cells. If a row has more cells than the
592    /// header list, the table expands to fit the additional columns.
593    pub fn add_row(&mut self, row: Vec<Cell>) {
594        self.rows.push(TableRow::Cells(row));
595    }
596
597    /// Add a full-width section separator inside the table.
598    ///
599    /// Section rows span the entire table width and are useful for grouping
600    /// related data visually. The returned builder currently lets you choose
601    /// the label alignment.
602    ///
603    /// If a default section style was set with [`Table::with_section_style`], it is
604    /// applied automatically. Call `.style()` on the returned builder to override it.
605    pub fn add_section(&mut self, title: impl ToString) -> SectionBuilder<'_> {
606        let row_index = self.rows.len();
607        let style = match self.section_style {
608            Some(s) => TableStyle::from_section_style(s),
609            None => self.style,
610        };
611        self.rows.push(TableRow::Section(SectionRow {
612            title: title.to_string(),
613            align: Align::Center,
614            style,
615        }));
616
617        SectionBuilder {
618            table: self,
619            row_index,
620        }
621    }
622
623    /// Add a full-width separator row with no label.
624    ///
625    /// If a default separator style was set with [`Table::with_separator_style`], it is
626    /// applied automatically. Call `.style()` on the returned builder to override it.
627    pub fn add_separator(&mut self) -> SectionBuilder<'_> {
628        let row_index = self.rows.len();
629        let style = match self.separator_style {
630            Some(s) => TableStyle::from_section_style(s),
631            None => self.style,
632        };
633        self.rows.push(TableRow::Section(SectionRow {
634            title: String::new(),
635            align: Align::Center,
636            style,
637        }));
638
639        SectionBuilder {
640            table: self,
641            row_index,
642        }
643    }
644
645    /// Configure a column using either its zero-based index or exact header text.
646    pub fn column<T: Into<ColumnTarget>>(&mut self, target: T) -> ColumnBuilder<'_> {
647        ColumnBuilder {
648            table: self,
649            target: target.into(),
650        }
651    }
652
653    /// Print the formatted table to standard output.
654    #[allow(dead_code)]
655    pub fn print(&self) {
656        for line in self.render_lines() {
657            println!("{line}");
658        }
659    }
660
661    /// Render the formatted table as a single string.
662    ///
663    /// The returned string includes ANSI color codes when styling is applied.
664    /// Fractional widths are resolved using the current terminal size when it
665    /// can be detected.
666    pub fn render(&self) -> String {
667        self.render_lines().join("\n")
668    }
669
670    fn column_style_mut(&mut self, target: ColumnTarget) -> &mut ColumnStyle {
671        self.column_overrides.entry(target).or_default()
672    }
673
674    fn column_style(&self, col: usize) -> ColumnStyle {
675        let mut style = self.column_defaults.get(col).cloned().unwrap_or_default();
676
677        if let Some(header) = self.headers.get(col)
678            && let Some(header_style) = self
679                .column_overrides
680                .get(&ColumnTarget::Header(strip_ansi(&header.content)))
681        {
682            style.merge(header_style);
683        }
684
685        if let Some(index_style) = self.column_overrides.get(&ColumnTarget::Index(col)) {
686            style.merge(index_style);
687        }
688
689        style
690    }
691
692    fn prepare_cell(
693        &self,
694        cell: Option<&Cell>,
695        column_style: &ColumnStyle,
696        width: usize,
697        #[cfg_attr(not(feature = "style"), allow(unused_variables))] is_header: bool,
698    ) -> PreparedCell {
699        let raw = cell.map(|c| c.content.as_str()).unwrap_or("");
700        let truncation = cell
701            .and_then(|c| c.truncation)
702            .or(column_style.truncation)
703            .unwrap_or(Trunc::End);
704        let align = cell
705            .and_then(|c| c.align)
706            .or(column_style.align)
707            .unwrap_or(Align::Left);
708
709        #[cfg(feature = "style")]
710        let styled = {
711            // Merge styles: column first, then cell overrides (last color wins in
712            // the flat ANSI prefix built by apply_style_actions).
713            let mut all_styles = column_style.styles.clone();
714            if let Some(cell) = cell {
715                all_styles.extend_from_slice(&cell.styles);
716            }
717            if is_header {
718                all_styles.push(StyleAction::Bold);
719            }
720            apply_style_actions(raw, &all_styles)
721        };
722        #[cfg(not(feature = "style"))]
723        let styled = raw.to_string();
724
725        let lines = split_lines(&styled)
726            .into_iter()
727            .flat_map(|line| layout_line(&line, Some(width), truncation))
728            .collect();
729
730        PreparedCell { lines, align }
731    }
732
733    fn prepare_row(
734        &self,
735        row: &[Cell],
736        col_widths: &[usize],
737        column_styles: &[ColumnStyle],
738        is_header: bool,
739    ) -> Vec<PreparedCell> {
740        col_widths
741            .iter()
742            .zip(column_styles)
743            .enumerate()
744            .map(|(col, (&width, style))| self.prepare_cell(row.get(col), style, width, is_header))
745            .collect()
746    }
747
748    fn collect_content_widths(&self, col_count: usize) -> Vec<usize> {
749        let mut widths = vec![0usize; col_count];
750
751        let all_rows =
752            std::iter::once(self.headers.as_slice()).chain(self.rows.iter().filter_map(|row| {
753                match row {
754                    TableRow::Cells(cells) => Some(cells.as_slice()),
755                    TableRow::Section(_) => None,
756                }
757            }));
758
759        for row in all_rows {
760            for (col, cell) in row.iter().enumerate() {
761                for line in split_lines(&cell.content) {
762                    widths[col] = widths[col].max(visible_len(&line));
763                }
764            }
765        }
766
767        widths
768    }
769
770    fn resolve_column_widths(
771        &self,
772        content_widths: &[usize],
773        column_styles: &[ColumnStyle],
774        terminal_width: Option<usize>,
775    ) -> Vec<usize> {
776        let mut widths = content_widths.to_vec();
777        let mut fraction_columns = Vec::new();
778        let mut fill_columns = Vec::new();
779        let mut reserved_width = 0usize;
780
781        for (col, style) in column_styles.iter().enumerate() {
782            match style.width.unwrap_or_default() {
783                ColumnWidth::Auto => {
784                    reserved_width += widths[col];
785                }
786                ColumnWidth::Fixed(width) => {
787                    widths[col] = width;
788                    reserved_width += width;
789                }
790                ColumnWidth::Fraction(fraction) => {
791                    fraction_columns.push((col, fraction.max(0.0)));
792                }
793                ColumnWidth::Fill => {
794                    fill_columns.push(col);
795                }
796            }
797        }
798
799        let Some(terminal_width) = terminal_width else {
800            return widths;
801        };
802
803        let table_overhead = (3 * widths.len()) + 1;
804        let available_content_width = terminal_width.saturating_sub(table_overhead);
805        let remaining_width = available_content_width.saturating_sub(reserved_width);
806
807        if fraction_columns.is_empty() && fill_columns.is_empty() {
808            return widths;
809        }
810
811        let mut leftover = remaining_width;
812
813        if !fraction_columns.is_empty() {
814            let total_fraction: f64 = fraction_columns.iter().map(|(_, fraction)| *fraction).sum();
815            if total_fraction <= f64::EPSILON {
816                for (col, _) in fraction_columns {
817                    widths[col] = 0;
818                }
819            } else {
820                let fraction_budget = remaining_width;
821
822                let mut remainders = Vec::with_capacity(fraction_columns.len());
823                let mut assigned = 0usize;
824
825                for (col, fraction) in fraction_columns {
826                    let exact = if total_fraction <= 1.0 {
827                        (fraction_budget as f64) * fraction
828                    } else {
829                        (fraction_budget as f64) * fraction / total_fraction
830                    };
831                    let width = exact.floor() as usize;
832                    widths[col] = width;
833                    assigned += width;
834                    remainders.push((col, exact - width as f64));
835                }
836
837                leftover = leftover.saturating_sub(assigned);
838
839                if fill_columns.is_empty() {
840                    remainders.sort_by(|left, right| {
841                        right.1.partial_cmp(&left.1).unwrap_or(Ordering::Equal)
842                    });
843
844                    for (col, _) in remainders {
845                        if leftover == 0 {
846                            break;
847                        }
848
849                        widths[col] += 1;
850                        leftover -= 1;
851                    }
852                }
853            }
854        }
855
856        if !fill_columns.is_empty() {
857            let fill_count = fill_columns.len();
858            let fill_width = leftover / fill_count;
859            let mut fill_remainder = leftover % fill_count;
860
861            for col in fill_columns {
862                widths[col] = fill_width + usize::from(fill_remainder > 0);
863                fill_remainder = fill_remainder.saturating_sub(1);
864            }
865        }
866
867        widths
868    }
869
870    fn column_count(&self) -> usize {
871        let max_row_len = self
872            .rows
873            .iter()
874            .filter_map(|row| match row {
875                TableRow::Cells(cells) => Some(cells.len()),
876                TableRow::Section(_) => None,
877            })
878            .max()
879            .unwrap_or(0);
880
881        self.headers.len().max(max_row_len)
882    }
883
884    fn row_height(cells: &[PreparedCell]) -> usize {
885        cells.iter().map(|cell| cell.lines.len()).max().unwrap_or(1)
886    }
887
888    fn rule_line(
889        &self,
890        style: &TableStyle,
891        left: &str,
892        joint: &str,
893        right: &str,
894        col_widths: &[usize],
895    ) -> String {
896        let h = style.horiz;
897        let join = format!("{}{}{}", h, joint, h);
898        let inner = col_widths
899            .iter()
900            .map(|&width| h.repeat(width))
901            .collect::<Vec<_>>()
902            .join(&join);
903
904        format!("{}{}{}{}{}", left, h, inner, h, right)
905    }
906
907    fn push_row_lines(
908        &self,
909        lines: &mut Vec<String>,
910        cells: &[PreparedCell],
911        col_widths: &[usize],
912    ) {
913        for line_idx in 0..Self::row_height(cells) {
914            lines.push(self.render_row_line(cells, line_idx, col_widths));
915        }
916    }
917
918    fn render_row_line(
919        &self,
920        row: &[PreparedCell],
921        line_idx: usize,
922        col_widths: &[usize],
923    ) -> String {
924        let vertical = self.style.vert;
925        let rendered_cells: Vec<String> = row
926            .iter()
927            .enumerate()
928            .map(|(col, cell)| {
929                let raw = cell.lines.get(line_idx).map(String::as_str).unwrap_or("");
930                let padding = col_widths[col].saturating_sub(visible_len(raw));
931                match cell.align {
932                    Align::Left => format!("{}{}", raw, " ".repeat(padding)),
933                    Align::Right => format!("{}{}", " ".repeat(padding), raw),
934                    Align::Center => {
935                        let left_pad = padding / 2;
936                        let right_pad = padding - left_pad;
937                        format!("{}{}{}", " ".repeat(left_pad), raw, " ".repeat(right_pad))
938                    }
939                }
940            })
941            .collect();
942
943        format!(
944            "{} {} {}",
945            vertical,
946            rendered_cells.join(&format!(" {} ", vertical)),
947            vertical
948        )
949    }
950
951    fn render_section_line(&self, section: &SectionRow, col_widths: &[usize]) -> String {
952        let style = &section.style;
953
954        if section.title.trim().is_empty() {
955            return self.rule_line(
956                style,
957                style.mid_left,
958                style.mid_joint,
959                style.mid_right,
960                col_widths,
961            );
962        }
963
964        let total_inner = col_widths.iter().sum::<usize>() + 3 * col_widths.len() - 1;
965        let label = truncate_line(
966            &format!(" {} ", section.title),
967            Some(total_inner),
968            Trunc::End,
969        );
970        let label_len = label.chars().count();
971        let remaining = total_inner.saturating_sub(label_len);
972
973        let left_fill = match section.align {
974            Align::Left => 1,
975            Align::Center => remaining / 2,
976            Align::Right => remaining.saturating_sub(1),
977        };
978
979        let mut inner: Vec<char> = style.horiz.repeat(total_inner).chars().collect();
980        let joint = style.mid_joint.chars().next().unwrap_or('┼');
981        let mut cursor = 1;
982
983        for &w in col_widths.iter().take(col_widths.len().saturating_sub(1)) {
984            cursor += w + 1;
985            if cursor < inner.len() {
986                inner[cursor] = joint;
987            }
988            cursor += 2;
989        }
990
991        let prefix: String = inner[..left_fill].iter().collect();
992        let suffix: String = inner[left_fill + label_len..].iter().collect();
993
994        #[cfg(feature = "style")]
995        let bold_label = format!("\x1b[1m{}{}", label, ANSI_RESET);
996        #[cfg(not(feature = "style"))]
997        let bold_label = label;
998
999        format!(
1000            "{}{}{}{}{}",
1001            style.mid_left, prefix, bold_label, suffix, style.mid_right
1002        )
1003    }
1004
1005    fn render_lines_with_terminal_width(&self, terminal_width: Option<usize>) -> Vec<String> {
1006        let col_count = self.column_count();
1007        if col_count == 0 {
1008            return Vec::new();
1009        }
1010
1011        let column_styles: Vec<ColumnStyle> =
1012            (0..col_count).map(|col| self.column_style(col)).collect();
1013        let content_widths = self.collect_content_widths(col_count);
1014        let col_widths =
1015            self.resolve_column_widths(&content_widths, &column_styles, terminal_width);
1016
1017        let prepared_header = (!self.headers.is_empty())
1018            .then(|| self.prepare_row(&self.headers, &col_widths, &column_styles, true));
1019        let prepared_rows: Vec<PreparedRow> = self
1020            .rows
1021            .iter()
1022            .map(|row| match row {
1023                TableRow::Cells(cells) => {
1024                    PreparedRow::Cells(self.prepare_row(cells, &col_widths, &column_styles, false))
1025                }
1026                TableRow::Section(section) => PreparedRow::Section(section.clone()),
1027            })
1028            .collect();
1029
1030        let mut lines = Vec::new();
1031
1032        lines.push(self.rule_line(
1033            &self.style,
1034            self.style.top_left,
1035            self.style.top_joint,
1036            self.style.top_right,
1037            &col_widths,
1038        ));
1039
1040        if let Some(header) = prepared_header.as_ref() {
1041            self.push_row_lines(&mut lines, header, &col_widths);
1042
1043            if prepared_rows.is_empty()
1044                || !matches!(prepared_rows.first(), Some(PreparedRow::Section(_)))
1045            {
1046                lines.push(self.rule_line(
1047                    &self.style,
1048                    self.style.mid_left,
1049                    self.style.mid_joint,
1050                    self.style.mid_right,
1051                    &col_widths,
1052                ));
1053            }
1054        }
1055
1056        for row in &prepared_rows {
1057            match row {
1058                PreparedRow::Cells(cells) => self.push_row_lines(&mut lines, cells, &col_widths),
1059                PreparedRow::Section(section) => {
1060                    lines.push(self.render_section_line(section, &col_widths))
1061                }
1062            }
1063        }
1064
1065        lines.push(self.rule_line(
1066            &self.style,
1067            self.style.bottom_left,
1068            self.style.bottom_joint,
1069            self.style.bottom_right,
1070            &col_widths,
1071        ));
1072
1073        lines
1074    }
1075
1076    fn render_lines(&self) -> Vec<String> {
1077        self.render_lines_with_terminal_width(
1078            terminal_size().map(|(Width(width), _)| width as usize),
1079        )
1080    }
1081}
1082
1083impl Default for Table {
1084    fn default() -> Self {
1085        Self::new()
1086    }
1087}
1088
1089impl std::fmt::Display for Table {
1090    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1091        for line in self.render_lines() {
1092            writeln!(f, "{line}")?;
1093        }
1094        Ok(())
1095    }
1096}
1097
1098impl<'a> SectionBuilder<'a> {
1099    /// Set the alignment for the section label.
1100    pub fn align(self, align: Align) -> Self {
1101        if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1102            section.align = align;
1103        }
1104
1105        self
1106    }
1107
1108    /// Set the border style used when rendering this section or separator.
1109    pub fn style(self, style: SectionStyle) -> Self {
1110        if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1111            section.style = TableStyle::from_section_style(style);
1112        }
1113
1114        self
1115    }
1116}
1117
1118impl<'a> ColumnBuilder<'a> {
1119    /// Set the default color for the selected column.
1120    #[cfg(feature = "style")]
1121    pub fn color(self, color: Color) -> Self {
1122        self.table
1123            .column_style_mut(self.target.clone())
1124            .styles
1125            .push(StyleAction::Color(color));
1126        self
1127    }
1128
1129    /// Set the preferred width for the selected column.
1130    pub fn width(self, width: impl Into<ColumnWidth>) -> Self {
1131        self.table.column_style_mut(self.target.clone()).width = Some(width.into());
1132        self
1133    }
1134
1135    /// Alias for [`ColumnBuilder::width`].
1136    pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
1137        self.width(width)
1138    }
1139
1140    /// Set the truncation strategy for the selected column.
1141    pub fn truncate(self, truncation: Trunc) -> Self {
1142        self.table.column_style_mut(self.target.clone()).truncation = Some(truncation);
1143        self
1144    }
1145
1146    /// Set the text alignment for the selected column.
1147    pub fn align(self, align: Align) -> Self {
1148        self.table.column_style_mut(self.target.clone()).align = Some(align);
1149        self
1150    }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::text::strip_ansi;
1156    use super::*;
1157    #[cfg(feature = "style")]
1158    use crate::Color::BrightBlack;
1159
1160    impl Table {
1161        fn render_lines_for_test(&self, terminal_width: Option<usize>) -> Vec<String> {
1162            self.render_lines_with_terminal_width(terminal_width)
1163        }
1164    }
1165
1166    #[cfg(feature = "style")]
1167    #[test]
1168    fn cell_builders_are_chainable() {
1169        let cell = Cell::new("value")
1170            .color(BrightBlack)
1171            .truncate(Trunc::Middle);
1172
1173        assert!(matches!(
1174            cell.styles.as_slice(),
1175            [StyleAction::Color(BrightBlack)]
1176        ));
1177        assert_eq!(cell.truncation, Some(Trunc::Middle));
1178    }
1179
1180    #[cfg(feature = "style")]
1181    #[test]
1182    fn accepts_colorize_values_for_cells_and_headers() {
1183        let mut table = Table::with_columns(vec![
1184            Column::new("Status").bright_green().bold(),
1185            Column::new("Notes"),
1186        ]);
1187
1188        table.column("Status").width(10);
1189        table.add_row(vec![
1190            Cell::new("DefinitelyActive").bright_red().underline(),
1191            Cell::new("Ready"),
1192        ]);
1193
1194        let plain = plain_lines(&table);
1195
1196        assert!(plain[1].contains("Status"));
1197        assert!(plain[3].contains("Definitel…"));
1198    }
1199
1200    #[test]
1201    fn renders_multiline_headers_and_rows() {
1202        let mut table = Table::with_columns(vec![Column::new("Name\nAlias"), Column::new("Value")]);
1203        table.add_row(vec![Cell::new("alpha\nbeta"), Cell::new("1")]);
1204
1205        assert_eq!(
1206            plain_lines(&table),
1207            vec![
1208                "┌───────┬───────┐",
1209                "│ Name  │ Value │",
1210                "│ Alias │       │",
1211                "├───────┼───────┤",
1212                "│ alpha │ 1     │",
1213                "│ beta  │       │",
1214                "└───────┴───────┘",
1215            ]
1216        );
1217    }
1218
1219    #[test]
1220    fn renders_center_aligned_sections_inside_a_single_table() {
1221        assert_eq!(
1222            section_table_lines(Align::Center),
1223            expected_section_lines("├─── Alpha ────┤")
1224        );
1225    }
1226
1227    #[test]
1228    fn renders_left_aligned_sections_inside_a_single_table() {
1229        assert_eq!(
1230            section_table_lines(Align::Left),
1231            expected_section_lines("├─ Alpha ──────┤")
1232        );
1233    }
1234
1235    #[test]
1236    fn renders_right_aligned_sections_inside_a_single_table() {
1237        assert_eq!(
1238            section_table_lines(Align::Right),
1239            expected_section_lines("├────── Alpha ─┤")
1240        );
1241    }
1242
1243    #[test]
1244    fn renders_mid_joints_when_a_section_label_leaves_room() {
1245        let mut table =
1246            Table::with_columns(vec![Column::new("A"), Column::new("B"), Column::new("C")]);
1247        table.add_section("X");
1248        table.add_row(vec![Cell::new("1"), Cell::new("2"), Cell::new("3")]);
1249
1250        assert_eq!(
1251            plain_lines(&table),
1252            vec![
1253                "┌───┬───┬───┐",
1254                "│ A │ B │ C │",
1255                "├───┼ X ┼───┤",
1256                "│ 1 │ 2 │ 3 │",
1257                "└───┴───┴───┘",
1258            ]
1259        );
1260    }
1261
1262    #[test]
1263    fn sections_and_separators_can_use_their_own_styles() {
1264        let table_style = TableStyle {
1265            top_left: "╔",
1266            top_right: "╗",
1267            bottom_left: "╚",
1268            bottom_right: "╝",
1269            horiz: "═",
1270            vert: "║",
1271            top_joint: "╦",
1272            mid_left: "╠",
1273            mid_right: "╣",
1274            mid_joint: "╬",
1275            bottom_joint: "╩",
1276        };
1277        let section_style = SectionStyle::unicode();
1278        let separator_style = SectionStyle {
1279            horiz: "-",
1280            mid_left: "-",
1281            mid_right: "-",
1282            mid_joint: "-",
1283        };
1284
1285        let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")])
1286            .with_style(table_style);
1287
1288        table.add_section("Alpha").style(section_style);
1289        table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1290        table.add_separator().style(separator_style);
1291        table.add_row(vec![Cell::new("b"), Cell::new("2")]);
1292
1293        let plain = plain_lines(&table);
1294
1295        assert!(plain[0].starts_with("╔"));
1296        assert!(plain[0].ends_with("╗"));
1297        assert_eq!(plain[2], "├─── Alpha ────┤");
1298        assert_eq!(plain[4], "----------------");
1299        assert!(plain[6].starts_with("╚"));
1300        assert!(plain[6].ends_with("╝"));
1301    }
1302
1303    #[test]
1304    fn applies_column_and_cell_truncation() {
1305        let mut table = Table::with_columns(vec![Column::new("Value"), Column::new("Other")]);
1306        table.column("Value").max_width(5).truncate(Trunc::Start);
1307        table.add_row(vec![Cell::new("abcdefghij"), Cell::new("z")]);
1308        table.add_row(vec![
1309            Cell::new("abcdefghij").truncate(Trunc::Middle),
1310            Cell::new("z"),
1311        ]);
1312
1313        assert_eq!(
1314            plain_lines(&table),
1315            vec![
1316                "┌───────┬───────┐",
1317                "│ Value │ Other │",
1318                "├───────┼───────┤",
1319                "│ …ghij │ z     │",
1320                "│ ab…ij │ z     │",
1321                "└───────┴───────┘",
1322            ]
1323        );
1324    }
1325
1326    #[cfg(feature = "style")]
1327    #[test]
1328    fn truncation_keeps_ellipsis_tight_and_colored() {
1329        let mut table = Table::with_columns(vec![Column::new("Name")]);
1330        table.column(0).max_width(14);
1331        table.add_row(vec![Cell::new("Cynthia \"CJ\" Lee").bright_red()]);
1332
1333        let rendered = table.render_lines_for_test(Some(40)).join("\n");
1334        let plain = strip_ansi(&rendered);
1335
1336        assert!(plain.contains("Cynthia \"CJ\"…"));
1337        assert!(!plain.contains("Cynthia \"CJ\" …"));
1338        assert!(rendered.contains("\x1b[91mCynthia \"CJ\"…\x1b[0m"));
1339    }
1340
1341    #[test]
1342    fn builds_columns_in_one_step() {
1343        let mut table = Table::with_columns(vec![
1344            Column::new("Name").width(0.3),
1345            Column::new("Age").width(0.15),
1346            Column::new("City").width(0.55),
1347        ]);
1348
1349        table.add_row(vec![
1350            Cell::new("Alice"),
1351            Cell::new("30"),
1352            Cell::new("New York"),
1353        ]);
1354
1355        let plain = table
1356            .render_lines_for_test(Some(40))
1357            .into_iter()
1358            .map(|line| strip_ansi(&line))
1359            .collect::<Vec<_>>();
1360
1361        assert_eq!(plain[0].chars().count(), 40);
1362        assert!(plain[1].contains("Name"));
1363        assert!(plain[1].contains("Age"));
1364        assert!(plain[1].contains("City"));
1365        assert!(plain[3].contains("Alice"));
1366    }
1367
1368    #[test]
1369    fn fill_columns_take_the_remainder_after_fractional_columns() {
1370        let mut table = Table::with_columns(vec![
1371            Column::new("Name").width(ColumnWidth::fill()),
1372            Column::new("Role").width(0.6),
1373            Column::new("Status").width(0.3),
1374        ]);
1375
1376        table.add_row(vec![
1377            Cell::new("Ada Lovelace"),
1378            Cell::new("Principal Engineer"),
1379            Cell::new("Active"),
1380        ]);
1381
1382        let plain = table
1383            .render_lines_for_test(Some(70))
1384            .into_iter()
1385            .map(|line| strip_ansi(&line))
1386            .collect::<Vec<_>>();
1387
1388        assert_eq!(plain[0].chars().count(), 70);
1389        assert_eq!(cell_widths(&plain[1]), vec![6, 36, 18]);
1390        assert_eq!(cell_widths(&plain[3]), vec![6, 36, 18]);
1391        assert!(plain[1].contains("Name"));
1392        assert!(plain[1].contains("Role"));
1393        assert!(plain[1].contains("Status"));
1394    }
1395
1396    fn cell_widths(line: &str) -> Vec<usize> {
1397        line.split('│')
1398            .filter(|segment| !segment.is_empty())
1399            .map(|segment| segment.chars().count().saturating_sub(2))
1400            .collect()
1401    }
1402
1403    #[test]
1404    fn renders_fractional_columns_against_terminal_width() {
1405        let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1406        table.column("Name").max_width(0.5);
1407        table.column("Value").max_width(0.5);
1408        table.add_row(vec![Cell::new("Alice"), Cell::new("123")]);
1409
1410        let lines = table.render_lines_for_test(Some(40));
1411        let plain = lines
1412            .iter()
1413            .map(|line| strip_ansi(line))
1414            .collect::<Vec<_>>();
1415
1416        assert_eq!(plain[0].chars().count(), 40);
1417        assert_eq!(plain.last().unwrap().chars().count(), 40);
1418        assert!(plain[1].contains("Name"));
1419        assert!(plain[3].contains("Alice"));
1420    }
1421
1422    #[test]
1423    fn newline_truncation_wraps_at_spaces_and_hard_breaks_when_needed() {
1424        let mut table = Table::with_columns(vec![Column::new("Value")]);
1425        table.column(0).max_width(8);
1426        table.add_row(vec![Cell::new("one two three").truncate(Trunc::NewLine)]);
1427        table.add_row(vec![Cell::new("abcdefghij").truncate(Trunc::NewLine)]);
1428
1429        assert_eq!(
1430            plain_lines(&table),
1431            vec![
1432                "┌──────────┐",
1433                "│ Value    │",
1434                "├──────────┤",
1435                "│ one two  │",
1436                "│ three    │",
1437                "│ abcdefgh │",
1438                "│ ij       │",
1439                "└──────────┘",
1440            ]
1441        );
1442    }
1443
1444    fn plain_lines(table: &Table) -> Vec<String> {
1445        table
1446            .render_lines()
1447            .into_iter()
1448            .map(|line| strip_ansi(&line))
1449            .collect()
1450    }
1451
1452    fn section_table_lines(align: Align) -> Vec<String> {
1453        let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1454        table.add_section("Alpha").align(align);
1455        table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1456
1457        plain_lines(&table)
1458    }
1459
1460    fn expected_section_lines(section_line: &str) -> Vec<String> {
1461        vec![
1462            "┌──────┬───────┐".to_string(),
1463            "│ Name │ Value │".to_string(),
1464            section_line.to_string(),
1465            "│ a    │ 1     │".to_string(),
1466            "└──────┴───────┘".to_string(),
1467        ]
1468    }
1469}