bel7_cli/
tables.rs

1// Copyright (C) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Table styling utilities for CLI output.
16
17use std::fmt::Display;
18use tabled::{
19    Table,
20    settings::{
21        Format, Modify, Panel, Remove,
22        object::{Rows, Segment},
23        style::Style,
24    },
25};
26
27pub use tabled::settings::Padding;
28
29/// Available table styles for CLI output.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum TableStyle {
32    /// Modern rounded corners (default).
33    #[default]
34    Modern,
35    /// No borders, space-separated.
36    Borderless,
37    /// Self-explanatory.
38    Markdown,
39    /// Sharp corners with box-drawing characters.
40    Sharp,
41    /// ASCII-only characters.
42    Ascii,
43    /// psql-style output.
44    Psql,
45    /// uses dots for borders.
46    Dots,
47}
48
49impl TableStyle {
50    /// Applies this style to a table.
51    pub fn apply(self, table: &mut Table) {
52        match self {
53            TableStyle::Modern => {
54                table.with(Style::rounded());
55            }
56            TableStyle::Borderless => {
57                table.with(Style::blank());
58            }
59            TableStyle::Markdown => {
60                table.with(Style::markdown());
61            }
62            TableStyle::Sharp => {
63                table.with(Style::sharp());
64            }
65            TableStyle::Ascii => {
66                table.with(Style::ascii());
67            }
68            TableStyle::Psql => {
69                table.with(Style::psql());
70            }
71            TableStyle::Dots => {
72                table.with(Style::dots());
73            }
74        }
75    }
76}
77
78/// A builder for styled tables.
79pub struct StyledTable {
80    style: TableStyle,
81    header: Option<String>,
82    remove_header_row: bool,
83    padding: Option<Padding>,
84    newline_replacement: Option<String>,
85}
86
87impl Default for StyledTable {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl StyledTable {
94    /// Creates a new table builder with the default style.
95    pub fn new() -> Self {
96        Self {
97            style: TableStyle::default(),
98            header: None,
99            remove_header_row: false,
100            padding: None,
101            newline_replacement: None,
102        }
103    }
104
105    /// Sets the table style.
106    pub fn style(mut self, style: TableStyle) -> Self {
107        self.style = style;
108        self
109    }
110
111    /// Sets a header panel above the table.
112    pub fn header(mut self, header: impl Into<String>) -> Self {
113        self.header = Some(header.into());
114        self
115    }
116
117    /// Removes the first row (column headers) from the table.
118    ///
119    /// Useful for non-interactive/scriptable output where headers are noise.
120    pub fn remove_header_row(mut self) -> Self {
121        self.remove_header_row = true;
122        self
123    }
124
125    /// Sets custom padding for table cells.
126    ///
127    /// Use `Padding::new(top, right, bottom, left)` to specify padding values.
128    pub fn padding(mut self, padding: Padding) -> Self {
129        self.padding = Some(padding);
130        self
131    }
132
133    /// Replaces newlines in cell content with the specified string.
134    ///
135    /// Useful for non-interactive output where newlines would break parsing.
136    /// Common replacement is `","` to turn multi-line values into comma-separated lists.
137    pub fn replace_newlines(mut self, replacement: impl Into<String>) -> Self {
138        self.newline_replacement = Some(replacement.into());
139        self
140    }
141
142    /// Builds the final table from the provided data.
143    pub fn build<T: tabled::Tabled>(self, data: Vec<T>) -> Table {
144        let mut table = Table::new(data);
145
146        self.style.apply(&mut table);
147
148        if let Some(header) = self.header {
149            table.with(Panel::header(header));
150        }
151
152        if let Some(padding) = self.padding {
153            table.with(padding);
154        }
155
156        if self.remove_header_row {
157            table.with(Remove::row(Rows::first()));
158        }
159
160        if let Some(replacement) = self.newline_replacement {
161            table.with(
162                Modify::new(Segment::all())
163                    .with(Format::content(move |s| s.replace('\n', &replacement))),
164            );
165        }
166
167        table
168    }
169}
170
171/// Formats an optional value for rendering in a table cell.
172///
173/// Returns an empty string for None, otherwise the Display representation.
174pub fn display_option<T: Display>(opt: &Option<T>) -> String {
175    opt.as_ref().map_or_else(String::new, |val| val.to_string())
176}
177
178/// Formats an optional value with a default.
179pub fn display_option_or<T: Display>(opt: &Option<T>, default: &str) -> String {
180    opt.as_ref()
181        .map_or_else(|| default.to_string(), |val| val.to_string())
182}