oxur_cli/table/
config.rs

1use serde::Deserialize;
2use tabled::{
3    settings::{
4        formatting::Justification,
5        object::{Object, Rows, Segment},
6        style::BorderColor,
7        style::Style,
8        themes::Colorization,
9        Color, Padding,
10    },
11    Table,
12};
13
14#[derive(Debug, Deserialize)]
15pub struct TableStyleConfig {
16    pub table: TableConfig,
17    #[serde(default)]
18    pub title: Option<TitleConfig>,
19    pub header: HeaderConfig,
20    pub rows: RowsConfig,
21    pub style: StyleConfig,
22    #[serde(default)]
23    pub footer: Option<FooterConfig>,
24}
25
26/// Title configuration - styles a title row ABOVE the header
27///
28/// To create a title row, add it as the FIRST data row in your data structure.
29/// Then set `enabled = true` in the TOML config.
30#[derive(Debug, Deserialize, Clone)]
31pub struct TitleConfig {
32    /// Enable title styling on the first data row
33    #[serde(default)]
34    pub enabled: bool,
35    #[serde(default)]
36    pub bg_color: Option<String>,
37    #[serde(default)]
38    pub fg_color: Option<String>,
39    #[serde(default)]
40    pub justification_char: Option<String>,
41    #[serde(default)]
42    pub vertical_char: Option<String>,
43    #[serde(default)]
44    pub vertical_fg_color: Option<String>,
45    #[serde(default)]
46    pub vertical_bg_color: Option<String>,
47}
48
49/// Footer configuration - styles the LAST ROW of your data
50///
51/// To create a visual footer bar, add a footer row as the last element in your data.
52/// Then set `enabled = true` in the TOML config.
53#[derive(Debug, Deserialize, Clone)]
54pub struct FooterConfig {
55    /// Enable footer styling on the last data row
56    #[serde(default)]
57    pub enabled: bool,
58    #[serde(default)]
59    pub bg_color: Option<String>,
60    #[serde(default)]
61    pub fg_color: Option<String>,
62    #[serde(default)]
63    pub justification_char: Option<String>,
64    #[serde(default)]
65    pub vertical_char: Option<String>,
66    #[serde(default)]
67    pub vertical_fg_color: Option<String>,
68    #[serde(default)]
69    pub vertical_bg_color: Option<String>,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct TableConfig {
74    pub padding_left: usize,
75    pub padding_right: usize,
76    pub padding_top: usize,
77    pub padding_bottom: usize,
78}
79
80#[derive(Debug, Deserialize)]
81pub struct HeaderConfig {
82    pub bg_color: String,
83    pub fg_color: String,
84    pub justification_char: String,
85    #[serde(default)]
86    pub vertical_char: Option<String>,
87    #[serde(default)]
88    pub vertical_fg_color: Option<String>,
89    #[serde(default)]
90    pub vertical_bg_color: Option<String>,
91}
92
93#[derive(Debug, Deserialize)]
94pub struct RowsConfig {
95    pub colors: Vec<RowColor>,
96    #[serde(default)]
97    pub justification_char: Option<String>,
98}
99
100#[derive(Debug, Deserialize)]
101pub struct RowColor {
102    pub bg: String,
103    pub fg: String,
104}
105
106#[derive(Debug, Deserialize)]
107pub struct StyleConfig {
108    #[serde(default)]
109    pub vertical_char: Option<String>,
110    #[serde(default)]
111    pub vertical_fg_color: Option<String>,
112    #[serde(default)]
113    pub vertical_bg_color: Option<String>,
114}
115
116impl TableStyleConfig {
117    /// Apply the configuration to a tabled Table
118    pub fn apply_to_table<T>(&self, table: &mut Table)
119    where
120        T: tabled::Tabled,
121    {
122        // Apply base style (empty)
123        table.with(Style::empty());
124
125        // Determine which vertical separator character to use globally
126        // The limitation: tabled only allows ONE vertical char for the whole table
127        // Priority: use header.vertical_char if specified, else style.vertical_char
128        let global_vert_char = self
129            .header
130            .vertical_char
131            .as_deref()
132            .or(self.style.vertical_char.as_deref())
133            .unwrap_or("");
134
135        // Apply global vertical separator if specified
136        if !global_vert_char.is_empty() {
137            let vert_char = global_vert_char.chars().next().unwrap_or(' ');
138            table.with(Style::empty().vertical(vert_char));
139        }
140
141        // Apply padding
142        table.with(Padding::new(
143            self.table.padding_left,
144            self.table.padding_right,
145            self.table.padding_top,
146            self.table.padding_bottom,
147        ));
148
149        // Apply alternating row colors
150        let row_colors: Vec<Color> =
151            self.rows.colors.iter().map(|rc| parse_color(&rc.bg, &rc.fg)).collect();
152        table.with(Colorization::rows(row_colors));
153
154        // Determine if title is enabled (we need this for data row styling)
155        let title_enabled = self.title.as_ref().map(|t| t.enabled).unwrap_or(false);
156
157        // Apply title styling if enabled (title is the first row = row index 0)
158        if let Some(title) = &self.title {
159            if title.enabled {
160                // Determine title colors (default to header colors for consistency)
161                let title_bg_str = title.bg_color.as_deref().unwrap_or(&self.header.bg_color);
162                let title_fg_str = title.fg_color.as_deref().unwrap_or(&self.header.fg_color);
163                let title_color = parse_color(title_bg_str, title_fg_str);
164                let title_bg = parse_bg_color(title_bg_str);
165
166                // Apply title row colors (first row = row index 0)
167                table.modify(Rows::first(), title_color);
168
169                // Apply title justification
170                let title_just_char = title
171                    .justification_char
172                    .as_deref()
173                    .or(Some(&self.header.justification_char))
174                    .and_then(|s| s.chars().next())
175                    .unwrap_or(' ');
176                table.modify(
177                    Rows::first(),
178                    Justification::new(title_just_char).color(title_bg.clone()),
179                );
180            }
181        }
182
183        // Apply header styling (header is the second row = row index 1)
184        let header_color = parse_color(&self.header.bg_color, &self.header.fg_color);
185        table.modify(Rows::new(1..2), header_color);
186
187        // Apply header justification
188        let just_char = self.header.justification_char.chars().next().unwrap_or(' ');
189        let header_bg = parse_bg_color(&self.header.bg_color);
190        table.modify(Rows::new(1..2), Justification::new(just_char).color(header_bg.clone()));
191
192        // Determine if footer is enabled (we need this for data row styling)
193        let footer_enabled = self.footer.as_ref().map(|f| f.enabled).unwrap_or(false);
194
195        // Apply data row justification if specified
196        if let Some(data_just_char) =
197            self.rows.justification_char.as_ref().and_then(|s| s.chars().next())
198        {
199            // Data rows start at row 2 (after title row 0 and header row 1)
200            // Use the first data row background color for justification padding color
201            let data_bg = parse_bg_color(&self.rows.colors[0].bg);
202
203            // Apply justification to all rows except title, header, and footer
204            match footer_enabled {
205                true => {
206                    // Exclude rows 0, 1, and last row
207                    table.modify(
208                        Segment::all().not(Rows::first()).not(Rows::new(1..2)).not(Rows::last()),
209                        Justification::new(data_just_char).color(data_bg),
210                    );
211                }
212                false => {
213                    // Exclude rows 0 and 1 only
214                    table.modify(
215                        Segment::all().not(Rows::first()).not(Rows::new(1..2)),
216                        Justification::new(data_just_char).color(data_bg),
217                    );
218                }
219            }
220        }
221
222        // Color the vertical separators in different sections
223
224        // 1. Color data row separators if colors specified
225        if self.style.vertical_fg_color.is_some() || self.style.vertical_bg_color.is_some() {
226            let fg = self.style.vertical_fg_color.as_deref().unwrap_or("white");
227            let bg = self.style.vertical_bg_color.as_deref().unwrap_or("black");
228            let vert_color = parse_color(bg, fg);
229
230            // Color all vertical borders in data rows (excluding header, title if enabled, and footer if enabled)
231            // We need to handle different combinations due to Rust's type system
232            match (title_enabled, footer_enabled) {
233                (true, true) => {
234                    table.modify(
235                        Segment::all().not(Rows::first()).not(Rows::new(1..2)).not(Rows::last()),
236                        BorderColor::filled(vert_color),
237                    );
238                }
239                (true, false) => {
240                    table.modify(
241                        Segment::all().not(Rows::first()).not(Rows::new(1..2)),
242                        BorderColor::filled(vert_color),
243                    );
244                }
245                (false, true) => {
246                    table.modify(
247                        Segment::all().not(Rows::first()).not(Rows::last()),
248                        BorderColor::filled(vert_color),
249                    );
250                }
251                (false, false) => {
252                    table
253                        .modify(Segment::all().not(Rows::first()), BorderColor::filled(vert_color));
254                }
255            }
256        }
257
258        // 2. Color title separators if enabled (title is now row 0)
259        if let Some(title) = &self.title {
260            if title.enabled {
261                let title_bg_str = title.bg_color.as_deref().unwrap_or(&self.header.bg_color);
262
263                if title.vertical_fg_color.is_some() || title.vertical_bg_color.is_some() {
264                    let fg = title.vertical_fg_color.as_deref().unwrap_or("white");
265                    let bg = title.vertical_bg_color.as_deref().unwrap_or(title_bg_str);
266                    let title_vert_color = parse_color(bg, fg);
267
268                    table.modify(
269                        Segment::all().intersect(Rows::first()),
270                        BorderColor::filled(title_vert_color),
271                    );
272                } else {
273                    // Default: match title background
274                    let title_bg = parse_bg_color(title_bg_str);
275                    table.modify(
276                        Segment::all().intersect(Rows::first()),
277                        BorderColor::filled(title_bg),
278                    );
279                }
280            }
281        }
282
283        // 3. Color header separators (header is now row 1)
284        if self.header.vertical_fg_color.is_some() || self.header.vertical_bg_color.is_some() {
285            let fg = self.header.vertical_fg_color.as_deref().unwrap_or("white");
286            let bg =
287                self.header.vertical_bg_color.as_deref().unwrap_or_else(|| &self.header.bg_color);
288            let header_vert_color = parse_color(bg, fg);
289
290            table.modify(
291                Segment::all().intersect(Rows::new(1..2)),
292                BorderColor::filled(header_vert_color),
293            );
294        } else {
295            // Default: match header background
296            table.modify(Segment::all().intersect(Rows::new(1..2)), BorderColor::filled(header_bg));
297        }
298
299        // 4. Apply footer styling if enabled
300        if let Some(footer) = &self.footer {
301            if footer.enabled {
302                // Determine footer colors (default to header colors)
303                let footer_bg_str = footer.bg_color.as_deref().unwrap_or(&self.header.bg_color);
304                let footer_fg_str = footer.fg_color.as_deref().unwrap_or(&self.header.fg_color);
305                let footer_color = parse_color(footer_bg_str, footer_fg_str);
306                let footer_bg = parse_bg_color(footer_bg_str);
307
308                // Apply footer row colors
309                table.modify(Rows::last(), footer_color);
310
311                // Apply footer justification
312                let footer_just_char = footer
313                    .justification_char
314                    .as_deref()
315                    .or(Some(&self.header.justification_char))
316                    .and_then(|s| s.chars().next())
317                    .unwrap_or(' ');
318                table.modify(
319                    Rows::last(),
320                    Justification::new(footer_just_char).color(footer_bg.clone()),
321                );
322
323                // Color footer vertical separators
324                if footer.vertical_fg_color.is_some() || footer.vertical_bg_color.is_some() {
325                    let fg = footer.vertical_fg_color.as_deref().unwrap_or("white");
326                    let bg = footer.vertical_bg_color.as_deref().unwrap_or(footer_bg_str);
327                    let footer_vert_color = parse_color(bg, fg);
328
329                    table.modify(
330                        Segment::all().intersect(Rows::last()),
331                        BorderColor::filled(footer_vert_color),
332                    );
333                } else {
334                    // Default: match footer background
335                    table.modify(
336                        Segment::all().intersect(Rows::last()),
337                        BorderColor::filled(footer_bg),
338                    );
339                }
340            }
341        }
342    }
343}
344
345/// Parse a color name string into a tabled Color (foreground)
346fn parse_single_color(color: &str) -> Color {
347    // Check if it's a hex color
348    if let Some(rgb) = parse_hex_color(color) {
349        return Color::rgb_fg(rgb.0, rgb.1, rgb.2);
350    }
351
352    match color.to_lowercase().as_str() {
353        // Foreground colors
354        "black" => Color::FG_BLACK,
355        "red" => Color::FG_RED,
356        "green" => Color::FG_GREEN,
357        "yellow" => Color::FG_YELLOW,
358        "blue" => Color::FG_BLUE,
359        "magenta" => Color::FG_MAGENTA,
360        "cyan" => Color::FG_CYAN,
361        "white" => Color::FG_WHITE,
362        "bright_black" | "gray" | "grey" => Color::FG_BRIGHT_BLACK,
363        "bright_red" => Color::FG_BRIGHT_RED,
364        "bright_green" => Color::FG_BRIGHT_GREEN,
365        "bright_yellow" => Color::FG_BRIGHT_YELLOW,
366        "bright_blue" => Color::FG_BRIGHT_BLUE,
367        "bright_magenta" => Color::FG_BRIGHT_MAGENTA,
368        "bright_cyan" => Color::FG_BRIGHT_CYAN,
369        "bright_white" => Color::FG_BRIGHT_WHITE,
370        _ => Color::FG_WHITE, // default
371    }
372}
373
374/// Parse background and foreground colors and combine them
375fn parse_color(bg: &str, fg: &str) -> Color {
376    // Check if bg is a hex color
377    let bg_color = if let Some(rgb) = parse_hex_color(bg) {
378        Color::rgb_bg(rgb.0, rgb.1, rgb.2)
379    } else {
380        match bg.to_lowercase().as_str() {
381            "black" => Color::BG_BLACK,
382            "red" => Color::BG_RED,
383            "green" => Color::BG_GREEN,
384            "yellow" => Color::BG_YELLOW,
385            "blue" => Color::BG_BLUE,
386            "magenta" => Color::BG_MAGENTA,
387            "cyan" => Color::BG_CYAN,
388            "white" => Color::BG_WHITE,
389            "bright_black" | "gray" | "grey" => Color::BG_BRIGHT_BLACK,
390            "bright_red" => Color::BG_BRIGHT_RED,
391            "bright_green" => Color::BG_BRIGHT_GREEN,
392            "bright_yellow" => Color::BG_BRIGHT_YELLOW,
393            "bright_blue" => Color::BG_BRIGHT_BLUE,
394            "bright_magenta" => Color::BG_BRIGHT_MAGENTA,
395            "bright_cyan" => Color::BG_BRIGHT_CYAN,
396            "bright_white" => Color::BG_BRIGHT_WHITE,
397            _ => Color::BG_BLACK, // default
398        }
399    };
400
401    let fg_color = parse_single_color(fg);
402
403    bg_color | fg_color
404}
405
406/// Parse just a background color
407pub fn parse_bg_color(bg: &str) -> Color {
408    // Check if it's a hex color
409    if let Some(rgb) = parse_hex_color(bg) {
410        return Color::rgb_bg(rgb.0, rgb.1, rgb.2);
411    }
412
413    match bg.to_lowercase().as_str() {
414        "black" => Color::BG_BLACK,
415        "red" => Color::BG_RED,
416        "green" => Color::BG_GREEN,
417        "yellow" => Color::BG_YELLOW,
418        "blue" => Color::BG_BLUE,
419        "magenta" => Color::BG_MAGENTA,
420        "cyan" => Color::BG_CYAN,
421        "white" => Color::BG_WHITE,
422        "bright_black" | "gray" | "grey" => Color::BG_BRIGHT_BLACK,
423        "bright_red" => Color::BG_BRIGHT_RED,
424        "bright_green" => Color::BG_BRIGHT_GREEN,
425        "bright_yellow" => Color::BG_BRIGHT_YELLOW,
426        "bright_blue" => Color::BG_BRIGHT_BLUE,
427        "bright_magenta" => Color::BG_BRIGHT_MAGENTA,
428        "bright_cyan" => Color::BG_BRIGHT_CYAN,
429        "bright_white" => Color::BG_BRIGHT_WHITE,
430        _ => Color::BG_BLACK, // default
431    }
432}
433
434/// Parse a hex color string (with or without #) into RGB components
435/// Returns None if the string is not a valid hex color
436fn parse_hex_color(hex: &str) -> Option<(u8, u8, u8)> {
437    let hex = hex.trim_start_matches('#');
438
439    // Support both 3-digit (#RGB) and 6-digit (#RRGGBB) formats
440    if hex.len() == 3 {
441        let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
442        let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
443        let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
444        Some((r, g, b))
445    } else if hex.len() == 6 {
446        let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
447        let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
448        let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
449        Some((r, g, b))
450    } else {
451        None
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    // ===== parse_hex_color tests =====
460
461    #[test]
462    fn test_parse_hex_color_six_digit_with_hash() {
463        assert_eq!(parse_hex_color("#FF0000"), Some((255, 0, 0)));
464        assert_eq!(parse_hex_color("#00FF00"), Some((0, 255, 0)));
465        assert_eq!(parse_hex_color("#0000FF"), Some((0, 0, 255)));
466        assert_eq!(parse_hex_color("#FFFFFF"), Some((255, 255, 255)));
467        assert_eq!(parse_hex_color("#000000"), Some((0, 0, 0)));
468    }
469
470    #[test]
471    fn test_parse_hex_color_six_digit_without_hash() {
472        assert_eq!(parse_hex_color("FF0000"), Some((255, 0, 0)));
473        assert_eq!(parse_hex_color("00FF00"), Some((0, 255, 0)));
474        assert_eq!(parse_hex_color("0000FF"), Some((0, 0, 255)));
475    }
476
477    #[test]
478    fn test_parse_hex_color_three_digit_with_hash() {
479        assert_eq!(parse_hex_color("#F00"), Some((255, 0, 0)));
480        assert_eq!(parse_hex_color("#0F0"), Some((0, 255, 0)));
481        assert_eq!(parse_hex_color("#00F"), Some((0, 0, 255)));
482        assert_eq!(parse_hex_color("#FFF"), Some((255, 255, 255)));
483        assert_eq!(parse_hex_color("#000"), Some((0, 0, 0)));
484    }
485
486    #[test]
487    fn test_parse_hex_color_three_digit_without_hash() {
488        assert_eq!(parse_hex_color("F00"), Some((255, 0, 0)));
489        assert_eq!(parse_hex_color("0F0"), Some((0, 255, 0)));
490        assert_eq!(parse_hex_color("00F"), Some((0, 0, 255)));
491    }
492
493    #[test]
494    fn test_parse_hex_color_lowercase() {
495        assert_eq!(parse_hex_color("#ff0000"), Some((255, 0, 0)));
496        assert_eq!(parse_hex_color("#f00"), Some((255, 0, 0)));
497        assert_eq!(parse_hex_color("abcdef"), Some((171, 205, 239)));
498    }
499
500    #[test]
501    fn test_parse_hex_color_mixed_case() {
502        assert_eq!(parse_hex_color("#Ff0000"), Some((255, 0, 0)));
503        assert_eq!(parse_hex_color("#AbCdEf"), Some((171, 205, 239)));
504    }
505
506    #[test]
507    fn test_parse_hex_color_invalid_length() {
508        assert_eq!(parse_hex_color("#FF"), None);
509        assert_eq!(parse_hex_color("#FFFF"), None);
510        assert_eq!(parse_hex_color("#FFFFFFF"), None);
511        assert_eq!(parse_hex_color(""), None);
512    }
513
514    #[test]
515    fn test_parse_hex_color_invalid_characters() {
516        assert_eq!(parse_hex_color("#GGGGGG"), None);
517        assert_eq!(parse_hex_color("#ZZZZZZ"), None);
518        assert_eq!(parse_hex_color("#12345G"), None);
519        assert_eq!(parse_hex_color("notahex"), None);
520    }
521
522    #[test]
523    fn test_parse_hex_color_real_world_colors() {
524        // Oxur theme colors
525        assert_eq!(parse_hex_color("#F97316"), Some((249, 115, 22)));
526        assert_eq!(parse_hex_color("#D45500"), Some((212, 85, 0)));
527        assert_eq!(parse_hex_color("#451A03"), Some((69, 26, 3)));
528        assert_eq!(parse_hex_color("#FED7AA"), Some((254, 215, 170)));
529        assert_eq!(parse_hex_color("#FDBA74"), Some((253, 186, 116)));
530        assert_eq!(parse_hex_color("#803300"), Some((128, 51, 0)));
531    }
532
533    // ===== parse_single_color tests =====
534
535    #[test]
536    fn test_parse_single_color_basic_colors() {
537        // Just verify these don't panic - we can't easily test Color equality
538        parse_single_color("black");
539        parse_single_color("red");
540        parse_single_color("green");
541        parse_single_color("yellow");
542        parse_single_color("blue");
543        parse_single_color("magenta");
544        parse_single_color("cyan");
545        parse_single_color("white");
546    }
547
548    #[test]
549    fn test_parse_single_color_bright_colors() {
550        parse_single_color("bright_black");
551        parse_single_color("bright_red");
552        parse_single_color("bright_green");
553        parse_single_color("bright_yellow");
554        parse_single_color("bright_blue");
555        parse_single_color("bright_magenta");
556        parse_single_color("bright_cyan");
557        parse_single_color("bright_white");
558    }
559
560    #[test]
561    fn test_parse_single_color_aliases() {
562        parse_single_color("gray");
563        parse_single_color("grey");
564    }
565
566    #[test]
567    fn test_parse_single_color_case_insensitive() {
568        parse_single_color("RED");
569        parse_single_color("Green");
570        parse_single_color("BLUE");
571        parse_single_color("bright_RED");
572    }
573
574    #[test]
575    fn test_parse_single_color_hex() {
576        // Should handle hex colors
577        parse_single_color("#FF0000");
578        parse_single_color("#F00");
579    }
580
581    #[test]
582    fn test_parse_single_color_unknown_defaults_to_white() {
583        // Unknown colors should default to white
584        parse_single_color("unknown");
585        parse_single_color("notacolor");
586    }
587
588    // ===== parse_color tests =====
589
590    #[test]
591    fn test_parse_color_basic_combinations() {
592        // Test combining background and foreground colors
593        parse_color("black", "white");
594        parse_color("red", "white");
595        parse_color("blue", "yellow");
596    }
597
598    #[test]
599    fn test_parse_color_hex_background() {
600        parse_color("#FF0000", "white");
601        parse_color("#F00", "black");
602    }
603
604    #[test]
605    fn test_parse_color_hex_foreground() {
606        parse_color("black", "#FFFFFF");
607        parse_color("red", "#FFF");
608    }
609
610    #[test]
611    fn test_parse_color_both_hex() {
612        parse_color("#FF0000", "#FFFFFF");
613        parse_color("#F00", "#FFF");
614    }
615
616    #[test]
617    fn test_parse_color_case_insensitive() {
618        parse_color("RED", "WHITE");
619        parse_color("Blue", "Yellow");
620    }
621
622    // ===== parse_bg_color tests =====
623
624    #[test]
625    fn test_parse_bg_color_basic_colors() {
626        parse_bg_color("black");
627        parse_bg_color("red");
628        parse_bg_color("green");
629        parse_bg_color("yellow");
630        parse_bg_color("blue");
631        parse_bg_color("magenta");
632        parse_bg_color("cyan");
633        parse_bg_color("white");
634    }
635
636    #[test]
637    fn test_parse_bg_color_bright_colors() {
638        parse_bg_color("bright_black");
639        parse_bg_color("bright_red");
640        parse_bg_color("bright_green");
641        parse_bg_color("bright_yellow");
642        parse_bg_color("bright_blue");
643        parse_bg_color("bright_magenta");
644        parse_bg_color("bright_cyan");
645        parse_bg_color("bright_white");
646    }
647
648    #[test]
649    fn test_parse_bg_color_aliases() {
650        parse_bg_color("gray");
651        parse_bg_color("grey");
652    }
653
654    #[test]
655    fn test_parse_bg_color_case_insensitive() {
656        parse_bg_color("RED");
657        parse_bg_color("Green");
658        parse_bg_color("BLUE");
659    }
660
661    #[test]
662    fn test_parse_bg_color_hex() {
663        parse_bg_color("#FF0000");
664        parse_bg_color("#F00");
665    }
666
667    #[test]
668    fn test_parse_bg_color_unknown_defaults_to_black() {
669        // Unknown colors should default to black
670        parse_bg_color("unknown");
671        parse_bg_color("notacolor");
672    }
673
674    // ===== TableStyleConfig deserialization tests =====
675
676    #[test]
677    fn test_deserialize_minimal_config() {
678        let toml = r##"
679            [table]
680            padding_left = 1
681            padding_right = 1
682            padding_top = 0
683            padding_bottom = 0
684
685            [header]
686            bg_color = "black"
687            fg_color = "white"
688            justification_char = " "
689
690            [rows]
691            colors = [
692                { bg = "black", fg = "white" }
693            ]
694
695            [style]
696        "##;
697
698        let config: TableStyleConfig = toml::from_str(toml).unwrap();
699        assert_eq!(config.table.padding_left, 1);
700        assert_eq!(config.table.padding_right, 1);
701        assert_eq!(config.header.bg_color, "black");
702        assert_eq!(config.rows.colors.len(), 1);
703    }
704
705    #[test]
706    fn test_deserialize_with_title_and_footer() {
707        let toml = r##"
708            [table]
709            padding_left = 0
710            padding_right = 0
711            padding_top = 0
712            padding_bottom = 0
713
714            [title]
715            enabled = true
716            bg_color = "#FF0000"
717            fg_color = "#FFFFFF"
718
719            [header]
720            bg_color = "blue"
721            fg_color = "white"
722            justification_char = " "
723
724            [rows]
725            colors = [
726                { bg = "black", fg = "white" },
727                { bg = "gray", fg = "white" }
728            ]
729
730            [style]
731
732            [footer]
733            enabled = true
734            bg_color = "red"
735            fg_color = "white"
736        "##;
737
738        let config: TableStyleConfig = toml::from_str(toml).unwrap();
739        assert!(config.title.is_some());
740        assert!(config.footer.is_some());
741
742        let title = config.title.unwrap();
743        assert!(title.enabled);
744        assert_eq!(title.bg_color.as_deref(), Some("#FF0000"));
745
746        let footer = config.footer.unwrap();
747        assert!(footer.enabled);
748        assert_eq!(footer.bg_color.as_deref(), Some("red"));
749    }
750
751    #[test]
752    fn test_deserialize_optional_fields() {
753        let toml = r##"
754            [table]
755            padding_left = 0
756            padding_right = 0
757            padding_top = 0
758            padding_bottom = 0
759
760            [header]
761            bg_color = "black"
762            fg_color = "white"
763            justification_char = " "
764            vertical_char = "|"
765            vertical_fg_color = "red"
766            vertical_bg_color = "blue"
767
768            [rows]
769            colors = [{ bg = "black", fg = "white" }]
770            justification_char = " "
771
772            [style]
773            vertical_char = "|"
774            vertical_fg_color = "green"
775            vertical_bg_color = "yellow"
776        "##;
777
778        let config: TableStyleConfig = toml::from_str(toml).unwrap();
779        assert_eq!(config.header.vertical_char.as_deref(), Some("|"));
780        assert_eq!(config.header.vertical_fg_color.as_deref(), Some("red"));
781        assert_eq!(config.rows.justification_char.as_deref(), Some(" "));
782        assert_eq!(config.style.vertical_char.as_deref(), Some("|"));
783    }
784
785    #[test]
786    fn test_deserialize_without_optional_sections() {
787        let toml = r##"
788            [table]
789            padding_left = 0
790            padding_right = 0
791            padding_top = 0
792            padding_bottom = 0
793
794            [header]
795            bg_color = "black"
796            fg_color = "white"
797            justification_char = " "
798
799            [rows]
800            colors = [{ bg = "black", fg = "white" }]
801
802            [style]
803        "##;
804
805        let config: TableStyleConfig = toml::from_str(toml).unwrap();
806        assert!(config.title.is_none());
807        assert!(config.footer.is_none());
808    }
809
810    // ===== apply_to_table tests =====
811
812    use tabled::{Table, Tabled};
813
814    #[derive(Tabled)]
815    struct TestRow {
816        #[tabled(rename = "Col1")]
817        col1: String,
818        #[tabled(rename = "Col2")]
819        col2: String,
820    }
821
822    #[test]
823    fn test_apply_to_table_basic() {
824        let config = TableStyleConfig::default();
825        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
826        let mut table = Table::new(&data);
827
828        config.apply_to_table::<TestRow>(&mut table);
829
830        let output = table.to_string();
831        assert!(!output.is_empty());
832    }
833
834    #[test]
835    fn test_apply_to_table_no_title_or_footer() {
836        let toml = r##"
837            [table]
838            padding_left = 1
839            padding_right = 1
840            padding_top = 0
841            padding_bottom = 0
842
843            [header]
844            bg_color = "#FF0000"
845            fg_color = "#FFFFFF"
846            justification_char = " "
847
848            [rows]
849            colors = [
850                { bg = "#000000", fg = "#FFFFFF" }
851            ]
852
853            [style]
854            vertical_fg_color = "#00FF00"
855            vertical_bg_color = "#0000FF"
856        "##;
857
858        let config: TableStyleConfig = toml::from_str(toml).unwrap();
859        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
860        let mut table = Table::new(&data);
861
862        config.apply_to_table::<TestRow>(&mut table);
863
864        let output = table.to_string();
865        assert!(!output.is_empty());
866    }
867
868    #[test]
869    fn test_apply_to_table_with_title_no_footer() {
870        let toml = r##"
871            [table]
872            padding_left = 0
873            padding_right = 0
874            padding_top = 0
875            padding_bottom = 0
876
877            [title]
878            enabled = true
879            bg_color = "#FF0000"
880            fg_color = "#FFFFFF"
881            justification_char = " "
882            vertical_fg_color = "#00FF00"
883            vertical_bg_color = "#0000FF"
884
885            [header]
886            bg_color = "blue"
887            fg_color = "white"
888            justification_char = " "
889
890            [rows]
891            colors = [{ bg = "black", fg = "white" }]
892
893            [style]
894            vertical_fg_color = "red"
895            vertical_bg_color = "green"
896        "##;
897
898        let config: TableStyleConfig = toml::from_str(toml).unwrap();
899        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
900        let mut table = Table::new(&data);
901
902        config.apply_to_table::<TestRow>(&mut table);
903
904        let output = table.to_string();
905        assert!(!output.is_empty());
906    }
907
908    #[test]
909    fn test_apply_to_table_with_footer_no_title() {
910        let toml = r##"
911            [table]
912            padding_left = 0
913            padding_right = 0
914            padding_top = 0
915            padding_bottom = 0
916
917            [header]
918            bg_color = "blue"
919            fg_color = "white"
920            justification_char = " "
921
922            [rows]
923            colors = [{ bg = "black", fg = "white" }]
924
925            [style]
926            vertical_fg_color = "red"
927            vertical_bg_color = "green"
928
929            [footer]
930            enabled = true
931            bg_color = "#FF0000"
932            fg_color = "#FFFFFF"
933            justification_char = " "
934            vertical_fg_color = "#00FF00"
935            vertical_bg_color = "#0000FF"
936        "##;
937
938        let config: TableStyleConfig = toml::from_str(toml).unwrap();
939        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
940        let mut table = Table::new(&data);
941
942        config.apply_to_table::<TestRow>(&mut table);
943
944        let output = table.to_string();
945        assert!(!output.is_empty());
946    }
947
948    #[test]
949    fn test_apply_to_table_with_both_title_and_footer() {
950        let toml = r##"
951            [table]
952            padding_left = 0
953            padding_right = 0
954            padding_top = 0
955            padding_bottom = 0
956
957            [title]
958            enabled = true
959            bg_color = "#AA0000"
960            fg_color = "#FFFFFF"
961
962            [header]
963            bg_color = "blue"
964            fg_color = "white"
965            justification_char = " "
966
967            [rows]
968            colors = [{ bg = "black", fg = "white" }]
969
970            [style]
971            vertical_fg_color = "red"
972            vertical_bg_color = "green"
973
974            [footer]
975            enabled = true
976            bg_color = "#00AA00"
977            fg_color = "#FFFFFF"
978        "##;
979
980        let config: TableStyleConfig = toml::from_str(toml).unwrap();
981        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
982        let mut table = Table::new(&data);
983
984        config.apply_to_table::<TestRow>(&mut table);
985
986        let output = table.to_string();
987        assert!(!output.is_empty());
988    }
989
990    #[test]
991    fn test_apply_to_table_title_disabled() {
992        let toml = r##"
993            [table]
994            padding_left = 0
995            padding_right = 0
996            padding_top = 0
997            padding_bottom = 0
998
999            [title]
1000            enabled = false
1001            bg_color = "#FF0000"
1002            fg_color = "#FFFFFF"
1003
1004            [header]
1005            bg_color = "blue"
1006            fg_color = "white"
1007            justification_char = " "
1008
1009            [rows]
1010            colors = [{ bg = "black", fg = "white" }]
1011
1012            [style]
1013        "##;
1014
1015        let config: TableStyleConfig = toml::from_str(toml).unwrap();
1016        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
1017        let mut table = Table::new(&data);
1018
1019        config.apply_to_table::<TestRow>(&mut table);
1020
1021        let output = table.to_string();
1022        assert!(!output.is_empty());
1023    }
1024
1025    #[test]
1026    fn test_apply_to_table_footer_disabled() {
1027        let toml = r##"
1028            [table]
1029            padding_left = 0
1030            padding_right = 0
1031            padding_top = 0
1032            padding_bottom = 0
1033
1034            [header]
1035            bg_color = "blue"
1036            fg_color = "white"
1037            justification_char = " "
1038
1039            [rows]
1040            colors = [{ bg = "black", fg = "white" }]
1041
1042            [style]
1043
1044            [footer]
1045            enabled = false
1046            bg_color = "#FF0000"
1047            fg_color = "#FFFFFF"
1048        "##;
1049
1050        let config: TableStyleConfig = toml::from_str(toml).unwrap();
1051        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
1052        let mut table = Table::new(&data);
1053
1054        config.apply_to_table::<TestRow>(&mut table);
1055
1056        let output = table.to_string();
1057        assert!(!output.is_empty());
1058    }
1059
1060    #[test]
1061    fn test_apply_to_table_with_empty_vertical_char() {
1062        let toml = r##"
1063            [table]
1064            padding_left = 0
1065            padding_right = 0
1066            padding_top = 0
1067            padding_bottom = 0
1068
1069            [header]
1070            bg_color = "blue"
1071            fg_color = "white"
1072            justification_char = " "
1073            vertical_char = ""
1074
1075            [rows]
1076            colors = [{ bg = "black", fg = "white" }]
1077
1078            [style]
1079            vertical_char = ""
1080        "##;
1081
1082        let config: TableStyleConfig = toml::from_str(toml).unwrap();
1083        let data = vec![TestRow { col1: "A".into(), col2: "B".into() }];
1084        let mut table = Table::new(&data);
1085
1086        config.apply_to_table::<TestRow>(&mut table);
1087
1088        let output = table.to_string();
1089        assert!(!output.is_empty());
1090    }
1091}