lil_tabby/
lib.rs

1//! Macro wrapper over `tabled` crate, allowing to create tables inplace or from `Vec<Vec<String>>` treating first data row
2//! both as data or as header overcoming specific cont trait treatment as originally in tabled.
3//!
4//! # Features
5//!
6//! - **Automatic Column Spanning**: Rows with fewer columns automatically span to fill the table
7//! - **Customizable Styling**: Configure table style, header color, first column color, content color, border color, and bold headers
8//! - **Simple Macro Syntax**: Create tables inline or from existing collections
9//! - **Extensible**: Use the re-exported `tabled` module for advanced customization
10//!
11//! # Examples
12//!
13//! ## Basic Table with Literal Data
14//! ```rust
15//! use lil_tabby::tabby;
16//!
17//! let t = tabby![
18//!     { style: ascii }
19//!     ["Header 1", "Header 2", "Header 3"],
20//!     ["Short", "Row"], // Auto-spans to fill remaining columns
21//!     ["Full", "Width", "Row"]
22//! ];
23//! println!("{}", t);
24//! ```
25//!
26//! **Output**:
27//! ```text
28//! +----------+----------+----------+
29//! | Header 1 | Header 2 | Header 3 |
30//! +----------+----------+----------+
31//! | Short    | Row      |          |
32//! +----------+----------+----------+
33//! | Full     | Width    | Row      |
34//! +----------+----------+----------+
35//! ```
36//!
37//! ## Styled Table with Custom Options
38//! ```rust
39//! use lil_tabby::tabby;
40//!
41//! let t = tabby![
42//!     { style: modern_rounded, header: green, labels: yellow, color: blue, border: red, bold: true }
43//!     ["Header 1", "Header 2", "Header 3"],
44//!     ["Short", "Row"],
45//!     ["Full", "Width", "Row"]
46//! ];
47//! println!("{}", t);
48//! ```
49//!
50//! ## Table from `Vec<Vec<String>>`
51//! ```rust
52//! use lil_tabby::tabby;
53//!
54//! let data: Vec<Vec<String>> = vec![
55//!     vec!["Header 1".to_string(), "Header 2".to_string(), "Header 3".to_string()],
56//!     vec!["Row 1".to_string()],
57//!     vec!["Row 2 Col 1".to_string(), "Row 2 Col 2".to_string()]
58//! ];
59//! let t = tabby!(data);
60//! println!("{}", t);
61//! ```
62//!
63//! ## Post-Creation
64//! Just re-export tabled
65//! ```rust
66//! use lil_tabby::{tabby, tabled};
67//!
68//! let mut table = tabby![
69//!     { style: ascii_rounded, border: bright_green }
70//!     ["a", "b", "c"],
71//!     ["c", "d"],
72//!     ["e"]
73//! ];
74//! table.modify(tabled::settings::object::Columns::new(0..), tabled::settings::Alignment::right());
75//! println!("{}", table);
76//! ```
77//!
78//! **Output:**
79//! ```text
80//! +---+---+---+
81//! | a | b | c |
82//! +---+---+---+
83//! | d |     e |
84//! +---+-------+
85//! |         f |
86//! +-----------+
87//! ```
88//!
89//! # Styling Options
90//!
91//! The `tabby!` macro accepts an optional styling block with these parameters:
92//! - `style`: Table style (e.g., `modern_rounded`, `ascii_rounded`, `markdown`). Defaults to `modern_rounded`. Maps to `tabled::settings::Style`
93//! - `header`: First row color (e.g., `green`, `blue`, `red`). Defaults to `white`. All colors mapped to `tabled::settings::Color::FG_<COLOR>`
94//! - `labels`: First column color (e.g., `yellow`, `cyan`). Defaults to `white`
95//! - `color`: Content color (e.g., `blue`, `magenta`). Defaults to `white`
96//! - `border`: Border color (e.g., `red`, `bright_green`). Defaults to `bright_black`
97//! - `bold`: Whether the header row is bold (`true` or `false`). Defaults to `false`
98//!
99//! # Limitations
100//!
101//! - Table limited to 20 columns, due to const trait implementation, each used size should be covered. So, 20 like 640kb, enough, anyway wont fit the screen.
102
103/// Main macro
104#[macro_export]
105macro_rules! tabby {
106    ($($rest:tt)+) => {{
107        $crate::_tabby_internal!(@start $($rest)+)
108    }};
109}
110
111/// Re-exported `tabled` crate for advanced table customization
112pub mod tabled {
113    pub use ::tabled::*;
114}
115
116/// `paste` re-export for color identifiers
117pub mod paste {
118    pub use ::paste::*;
119}
120
121// Internal utilities module - hidden from public API
122mod internal {
123    #[macro_export]
124    #[doc(hidden)]
125    macro_rules! _tabby_internal {
126        (@start $($rest:tt)+) => {{
127            $crate::_tabby_internal!(@style $($rest)+)
128        }};
129
130        (@style {$($style:ident: $value:ident),+$(,)?} $($rest:tt)+) => {{
131            $crate::_tabby_internal!(@table {$($style: $value),+}; $($rest)+)
132        }};
133
134        (@style $($rest:tt)+) => {{
135            $crate::_tabby_internal!(@table {}; $($rest)+)
136        }};
137
138        // Handle Vec<Vec<String>> or similar collections
139        (@table {$($style:ident: $value:ident),*}; $vec:expr) => {{
140            use $crate::tabled;
141
142            let mut rows = $vec;
143            let (max_columns, spans) = $crate::_process_rows!(rows);
144            let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
145            $crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
146        }};
147
148        // Handle literal array syntax
149        (@table {$($style:ident: $value:ident),*}; $([ $($cells:expr),* $(,)? ]),+ $(,)?) => {{
150            use $crate::tabled;
151
152            let mut rows = vec![
153                $({
154                    vec![$( $cells.to_string() ),*]
155                }),+
156            ];
157
158            let (max_columns, spans) = $crate::_process_rows!(rows);
159            let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
160            $crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
161        }};
162    }
163
164    #[macro_export]
165    #[doc(hidden)]
166    macro_rules! _table_convert {
167        ($max_columns:ident, $rows:ident, $($n:literal),+) => {
168            match $max_columns {
169                $(
170                    $n => {
171                        let data: Vec<[String; $n]> = $rows.drain(..)
172                            .map(|mut r| {
173                                r.resize($n, String::new());
174                                r.try_into().unwrap_or_else(|v: Vec<String>| {
175                                    panic!("Expected vec of length {}, got {}", $n, v.len())
176                                })
177                            })
178                            .collect();
179                        tabled::Table::new(data)
180                    }
181                )+,
182                _ => panic!("Table exceeds maximum supported columns (20)")
183            }
184        };
185    }
186
187    #[macro_export]
188    #[doc(hidden)]
189    macro_rules! _table_style {
190        (_default) => {
191            tabled::settings::Style::modern_rounded()
192        };
193        ($style:ident) => {
194            tabled::settings::Style::$style()
195        };
196    }
197
198    #[macro_export]
199    #[doc(hidden)]
200    macro_rules! _table_border {
201        (_default) => {
202            tabled::settings::Color::FG_BRIGHT_BLACK
203        };
204        ($color:ident) => {
205            $crate::paste::paste! {
206                tabled::settings::Color::[<FG_ $color:upper>]
207            }
208        };
209    }
210
211    #[macro_export]
212    #[doc(hidden)]
213    macro_rules! _table_labels {
214        (_default) => {
215            tabled::settings::Color::FG_WHITE
216        };
217        ($color:ident) => {
218            $crate::paste::paste! {
219                tabled::settings::Color::[<FG_ $color:upper>]
220            }
221        };
222    }
223
224    #[macro_export]
225    #[doc(hidden)]
226    macro_rules! _table_header {
227        (_default) => {
228            tabled::settings::Color::FG_WHITE
229        };
230        ($color:ident) => {
231            $crate::paste::paste! {
232                tabled::settings::Color::[<FG_ $color:upper>]
233            }
234        };
235    }
236
237    #[macro_export]
238    #[doc(hidden)]
239    macro_rules! _table_color {
240        (_default) => {
241            tabled::settings::Color::FG_WHITE
242        };
243        ($color:ident) => {
244            $crate::paste::paste! {
245                tabled::settings::Color::[<FG_ $color:upper>]
246            }
247        };
248    }
249
250    #[macro_export]
251    #[doc(hidden)]
252    macro_rules! _table_bold {
253        (_default) => {
254            false
255        };
256        ($bold:ident) => {
257            $bold
258        };
259    }
260
261    #[macro_export]
262    #[doc(hidden)]
263    macro_rules! _extract_field {
264        ($macro:ident; $field:ident; $($style:ident : $value:ident),* $(,)?) => {
265            $crate::_extract_field_inner!($macro; $field; $($style : $value),*; _default)
266        };
267        ($macro:ident; $field:ident;) => {
268            $crate::$macro!(_default)
269        };
270    }
271
272    #[macro_export]
273    #[doc(hidden)]
274    macro_rules! _extract_field_inner {
275        ($macro:ident; $field:ident; $style:ident : $value:ident $(, $rest_style:ident : $rest_value:ident)*; $default:ident) => {{
276            macro_rules! _check_field {
277                ($field : $v:ident) => {
278                    $crate::$macro!($v)
279                };
280                ($other:ident : $v:ident) => {
281                    $crate::_extract_field_inner!($macro; $field; $($rest_style : $rest_value),*; $default)
282                };
283            }
284            _check_field!($style : $value)
285        }};
286        ($macro:ident; $field:ident; ; $default:ident) => {
287            $crate::$macro!($default)
288        };
289    }
290
291    #[macro_export]
292    #[doc(hidden)]
293    macro_rules! _apply_table_styling {
294        ({$($style:ident: $value:ident),*}, $table:expr, $spans:expr, $max_columns:expr) => {{
295            // Apply column spans
296            for (row, &width) in $spans.iter().enumerate() {
297                if width > 0 && width < $max_columns {
298                    $table.modify(
299                        (row, width - 1),
300                        tabled::settings::Span::column(isize::MAX)
301                    );
302                }
303            }
304
305            $table
306                // Remove the auto-generated header row since we treat first data row as header
307                .with(tabled::settings::Remove::row(tabled::settings::object::Rows::first()))
308                .with($crate::_extract_field!(_table_style; style; $($style: $value),*))
309                .with(tabled::settings::themes::BorderCorrection::span())
310                .modify(
311                    tabled::settings::object::Columns::new(0..),
312                    tabled::settings::style::BorderColor::filled($crate::_extract_field!(_table_border; border; $($style: $value),*))
313                )
314                .modify(
315                    tabled::settings::object::Columns::new(0..),
316                    $crate::_extract_field!(_table_color; color; $($style: $value),*)
317                )
318                .modify(
319                    tabled::settings::object::Columns::first(),
320                    $crate::_extract_field!(_table_labels; labels; $($style: $value),*)
321                )
322                .modify(
323                    tabled::settings::object::Rows::first(),
324                    if $crate::_extract_field!(_table_bold; bold; $($style: $value),*) {
325                        $crate::_extract_field!(_table_header; header; $($style: $value),*) | tabled::settings::Color::BOLD
326                    } else {
327                        $crate::_extract_field!(_table_header; header; $($style: $value),*)
328                    }
329                );
330
331            $table
332        }};
333    }
334
335    #[macro_export]
336    #[doc(hidden)]
337    macro_rules! _process_rows {
338        ($rows:expr) => {{
339            let mut max_columns = 0;
340            let mut spans = Vec::new();
341
342            for row in $rows.iter() {
343                spans.push(row.len());
344                max_columns = std::cmp::max(max_columns, row.len());
345            }
346
347            (max_columns, spans)
348        }};
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    /// Test basic literal syntax with default styling
357    #[test]
358    fn test_literal_syntax() {
359        let t = tabby![["a", "b", "c"], ["c", "d"], ["e", "f", "g"]];
360
361        let output = t.to_string();
362        assert!(output.contains("a"));
363        assert!(output.contains("b"));
364        assert!(output.contains("c"));
365        assert!(output.contains("d"));
366        assert!(output.contains("e"));
367        assert!(output.contains("f"));
368        assert!(output.contains("g"));
369    }
370
371    /// Test Vec<Vec<String>> input
372    #[test]
373    fn test_vec_syntax() {
374        let data = vec![
375            vec!["Header1".to_string(), "Header2".to_string()],
376            vec!["Row1".to_string()],
377            vec!["Row2Col1".to_string(), "Row2Col2".to_string()],
378        ];
379
380        let t = tabby!(data);
381        let output = t.to_string();
382        assert!(output.contains("Header1"));
383        assert!(output.contains("Header2"));
384        assert!(output.contains("Row1"));
385        assert!(output.contains("Row2Col1"));
386        assert!(output.contains("Row2Col2"));
387    }
388
389    /// Test styling options with full configuration
390    #[test]
391    fn test_styling_options() {
392        let t = tabby![
393            {style: modern_rounded, header: green, labels: yellow, color: blue, border: red, bold: true}
394            ["Header 1", "Header 2", "Header 3"],
395            ["Short", "Row"],
396            ["Full", "Width", "Row"]
397        ];
398
399        let output = t.to_string();
400        // Check if the output contains colored elements (ANSI codes)
401        assert!(output.contains("\x1b[31m")); // Red border
402        assert!(output.contains("\x1b[32m")); // Green header
403        assert!(output.contains("\x1b[33m")); // Yellow labels
404        assert!(output.contains("\x1b[34m")); // Blue content
405        assert!(output.contains("Header 1"));
406        assert!(output.contains("Short"));
407    }
408
409    /// Test empty rows handling
410    #[test]
411    fn test_empty_rows() {
412        let t = tabby![["Header 1", "Header 2"], [], ["Row 2 Col 1"]];
413
414        let output = t.to_string();
415        assert!(output.contains("Header 1"));
416        assert!(output.contains("Header 2"));
417        assert!(output.contains("Row 2 Col 1"));
418    }
419
420    /// Test single column table
421    #[test]
422    fn test_single_column() {
423        let t = tabby![["Header"], ["Row 1"], ["Row 2"]];
424
425        let output = t.to_string();
426        assert!(output.contains("Header"));
427        assert!(output.contains("Row 1"));
428        assert!(output.contains("Row 2"));
429    }
430    /// Test maximum column limit (20 columns)
431    #[test]
432    fn test_max_columns() {
433        let row1: Vec<String> = (0..20).map(|i| format!("Col{}", i)).collect();
434        let row2: Vec<String> = vec!["Row 1".to_string()];
435        let t = tabby![vec!(row1, row2)];
436        let output = t.to_string();
437        assert!(output.contains("Col0"));
438        assert!(output.contains("Col19"));
439        assert!(output.contains("Row 1"));
440    }
441}