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}