oxur_cli/table/
helpers.rs

1//! Helper functions for advanced table styling
2//!
3//! This module provides utilities for applying cell-specific colors while
4//! preserving the theme's background colors. Use these when you need more
5//! control than the simple `OxurTable::new(data).render()` pattern.
6//!
7//! # Example: Cell-Specific Coloring
8//!
9//! ```no_run
10//! use oxur_cli::table::{helpers, TableStyleConfig, Builder, TabledColor, Tabled};
11//!
12//! #[derive(Tabled)]
13//! struct Row {
14//!     id: String,
15//!     name: String,
16//!     status: String,
17//! }
18//!
19//! // Build table manually
20//! let mut builder = Builder::default();
21//! builder.push_record(["ID", "Name", "Status"]);
22//! builder.push_record(["001", "Alice", "Active"]);
23//! builder.push_record(["002", "Bob", "Inactive"]);
24//!
25//! let mut table = builder.build();
26//!
27//! // Apply theme
28//! let theme = TableStyleConfig::default();
29//! theme.apply_to_table::<Row>(&mut table);
30//!
31//! // Get row background colors from theme
32//! let row_bg_colors = helpers::parse_row_bg_colors(&theme);
33//!
34//! // Apply cell-specific colors to status column
35//! let fg_color = TabledColor::FG_GREEN;
36//! let bg_color = helpers::get_data_row_bg_color(0, &row_bg_colors);
37//! helpers::apply_cell_color(&mut table, 2, 2, fg_color, bg_color);
38//! ```
39
40use tabled::settings::{object::Cell, Color as TabledColor};
41use tabled::Table;
42
43use super::config::{parse_bg_color, TableStyleConfig};
44
45/// Parse row background colors from theme configuration.
46///
47/// Returns a vector of background colors for alternating rows as defined
48/// in the theme's `rows.colors` configuration.
49///
50/// # Example
51///
52/// ```no_run
53/// use oxur_cli::table::{helpers, TableStyleConfig};
54///
55/// let theme = TableStyleConfig::default();
56/// let row_bg_colors = helpers::parse_row_bg_colors(&theme);
57/// assert_eq!(row_bg_colors.len(), 2); // Default theme has 2 alternating colors
58/// ```
59pub fn parse_row_bg_colors(theme: &TableStyleConfig) -> Vec<TabledColor> {
60    theme.rows.colors.iter().map(|rc| parse_bg_color(&rc.bg)).collect()
61}
62
63/// Get the background color for a data row using alternating colors.
64///
65/// # Arguments
66///
67/// * `data_row_index` - The 0-indexed position within the data section (not the absolute table row)
68/// * `row_bg_colors` - Vector of background colors (typically from `parse_row_bg_colors()`)
69///
70/// # Returns
71///
72/// The background color for this row, selected using modulo for alternating colors.
73///
74/// # Example
75///
76/// ```no_run
77/// use oxur_cli::table::{helpers, TableStyleConfig};
78///
79/// let theme = TableStyleConfig::default();
80/// let row_bg_colors = helpers::parse_row_bg_colors(&theme);
81///
82/// // First data row gets first color
83/// let bg0 = helpers::get_data_row_bg_color(0, &row_bg_colors);
84///
85/// // Second data row gets second color
86/// let bg1 = helpers::get_data_row_bg_color(1, &row_bg_colors);
87///
88/// // Third data row wraps back to first color
89/// let bg2 = helpers::get_data_row_bg_color(2, &row_bg_colors);
90/// ```
91pub fn get_data_row_bg_color(data_row_index: usize, row_bg_colors: &[TabledColor]) -> TabledColor {
92    let color_idx = data_row_index % row_bg_colors.len();
93    row_bg_colors[color_idx].clone()
94}
95
96/// Apply a foreground color to a specific cell while preserving its background.
97///
98/// This is the core pattern for cell-specific coloring. The `fg_color | bg_color`
99/// combination ensures the background from the theme is preserved while applying
100/// a custom foreground color.
101///
102/// # Arguments
103///
104/// * `table` - The table to modify
105/// * `row_idx` - Absolute row index in the table (0 = title if enabled, 1 = header, 2+ = data)
106/// * `col_idx` - Column index (0-based)
107/// * `fg_color` - Foreground color to apply
108/// * `bg_color` - Background color to preserve (typically from `get_data_row_bg_color()`)
109///
110/// # Example
111///
112/// ```no_run
113/// use oxur_cli::table::{helpers, TableStyleConfig, Builder, TabledColor, Tabled};
114///
115/// #[derive(Tabled)]
116/// struct Row { col: String }
117///
118/// let mut builder = Builder::default();
119/// builder.push_record(["Title"]);
120/// builder.push_record(["Header"]);
121/// builder.push_record(["Data"]);
122///
123/// let mut table = builder.build();
124///
125/// let theme = TableStyleConfig::default();
126/// theme.apply_to_table::<Row>(&mut table);
127///
128/// let row_bg_colors = helpers::parse_row_bg_colors(&theme);
129/// let bg = helpers::get_data_row_bg_color(0, &row_bg_colors);
130///
131/// // Apply green foreground to data cell (row 2, col 0)
132/// helpers::apply_cell_color(&mut table, 2, 0, TabledColor::FG_GREEN, bg);
133/// ```
134pub fn apply_cell_color(
135    table: &mut Table,
136    row_idx: usize,
137    col_idx: usize,
138    fg_color: TabledColor,
139    bg_color: TabledColor,
140) {
141    let combined = fg_color | bg_color;
142    table.modify(Cell::new(row_idx, col_idx), combined);
143}
144
145/// Map a state string to a foreground color.
146///
147/// This is a domain-specific helper for design document states. You can use this
148/// as an example and create your own mapping functions for your domain.
149///
150/// # Supported States
151///
152/// * `"draft"` → Yellow
153/// * `"under review"` / `"under-review"` → Cyan
154/// * `"revised"` → Blue
155/// * `"accepted"` → Green
156/// * `"active"` → Bright Green
157/// * `"final"` → Green
158/// * `"deferred"` → Magenta
159/// * `"rejected"` → Red
160/// * `"withdrawn"` → Red
161/// * `"superseded"` → Red
162///
163/// # Returns
164///
165/// `Some(color)` if the state is recognized, `None` otherwise.
166///
167/// # Example
168///
169/// ```no_run
170/// use oxur_cli::table::helpers;
171///
172/// let color = helpers::state_to_fg_color("active");
173/// assert!(color.is_some());
174///
175/// let unknown = helpers::state_to_fg_color("invalid");
176/// assert!(unknown.is_none());
177/// ```
178pub fn state_to_fg_color(state: &str) -> Option<TabledColor> {
179    match state.to_lowercase().as_str() {
180        "draft" => Some(TabledColor::FG_YELLOW),
181        "under review" | "under-review" => Some(TabledColor::FG_CYAN),
182        "revised" => Some(TabledColor::FG_BLUE),
183        "accepted" => Some(TabledColor::FG_GREEN),
184        "active" => Some(TabledColor::FG_BRIGHT_GREEN),
185        "final" => Some(TabledColor::FG_GREEN),
186        "deferred" => Some(TabledColor::FG_MAGENTA),
187        "rejected" => Some(TabledColor::FG_RED),
188        "withdrawn" => Some(TabledColor::FG_RED),
189        "superseded" => Some(TabledColor::FG_RED),
190        _ => None,
191    }
192}
193
194/// Map a deleted boolean to a foreground color.
195///
196/// This is a domain-specific helper for showing deletion status.
197///
198/// # Returns
199///
200/// * `true` (deleted) → Red
201/// * `false` (not deleted) → Green
202///
203/// # Example
204///
205/// ```no_run
206/// use oxur_cli::table::helpers;
207/// use tabled::settings::Color as TabledColor;
208///
209/// let deleted = helpers::deleted_to_fg_color(true);
210/// assert_eq!(deleted, TabledColor::FG_RED);
211///
212/// let not_deleted = helpers::deleted_to_fg_color(false);
213/// assert_eq!(not_deleted, TabledColor::FG_GREEN);
214/// ```
215pub fn deleted_to_fg_color(deleted: bool) -> TabledColor {
216    if deleted {
217        TabledColor::FG_RED
218    } else {
219        TabledColor::FG_GREEN
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_parse_row_bg_colors() {
229        let theme = TableStyleConfig::default();
230        let colors = parse_row_bg_colors(&theme);
231
232        // Default Oxur theme has 2 alternating row colors
233        assert_eq!(colors.len(), 2);
234    }
235
236    #[test]
237    fn test_get_data_row_bg_color_alternating() {
238        let theme = TableStyleConfig::default();
239        let colors = parse_row_bg_colors(&theme);
240
241        // Test alternating pattern
242        let bg0 = get_data_row_bg_color(0, &colors);
243        let bg1 = get_data_row_bg_color(1, &colors);
244        let bg2 = get_data_row_bg_color(2, &colors);
245        let bg3 = get_data_row_bg_color(3, &colors);
246
247        // Should alternate (comparing via Debug since Color doesn't implement Eq)
248        assert_eq!(format!("{:?}", bg0), format!("{:?}", bg2));
249        assert_eq!(format!("{:?}", bg1), format!("{:?}", bg3));
250    }
251
252    #[test]
253    fn test_state_to_fg_color_recognized() {
254        assert!(state_to_fg_color("active").is_some());
255        assert!(state_to_fg_color("Active").is_some()); // Case insensitive
256        assert!(state_to_fg_color("ACTIVE").is_some());
257        assert!(state_to_fg_color("draft").is_some());
258        assert!(state_to_fg_color("under review").is_some());
259        assert!(state_to_fg_color("under-review").is_some());
260        assert!(state_to_fg_color("accepted").is_some());
261        assert!(state_to_fg_color("final").is_some());
262        assert!(state_to_fg_color("rejected").is_some());
263    }
264
265    #[test]
266    fn test_state_to_fg_color_unrecognized() {
267        assert!(state_to_fg_color("invalid").is_none());
268        assert!(state_to_fg_color("").is_none());
269        assert!(state_to_fg_color("unknown-state").is_none());
270    }
271
272    #[test]
273    fn test_deleted_to_fg_color() {
274        let deleted = deleted_to_fg_color(true);
275        let not_deleted = deleted_to_fg_color(false);
276
277        assert_eq!(deleted, TabledColor::FG_RED);
278        assert_eq!(not_deleted, TabledColor::FG_GREEN);
279    }
280
281    #[test]
282    fn test_apply_cell_color_does_not_panic() {
283        use tabled::builder::Builder;
284        use tabled::Tabled;
285
286        #[derive(Tabled)]
287        struct TestRow {
288            col: String,
289        }
290
291        let mut builder = Builder::default();
292        builder.push_record(["Title"]);
293        builder.push_record(["Header"]);
294        builder.push_record(["Data"]);
295
296        let mut table = builder.build();
297
298        let theme = TableStyleConfig::default();
299        theme.apply_to_table::<TestRow>(&mut table);
300
301        let row_bg_colors = parse_row_bg_colors(&theme);
302        let bg = get_data_row_bg_color(0, &row_bg_colors);
303
304        // Should not panic
305        apply_cell_color(&mut table, 2, 0, TabledColor::FG_GREEN, bg);
306    }
307}