Skip to main content

sqlmodel_console/
theme.rs

1//! Theme definitions for SQLModel console output.
2//!
3//! This module provides the `Theme` struct that defines colors and styles
4//! for all console output elements. Themes can be customized or use
5//! predefined presets.
6//!
7//! # Example
8//!
9//! ```rust
10//! use sqlmodel_console::Theme;
11//!
12//! // Use the default dark theme
13//! let theme = Theme::default();
14//!
15//! // Or explicitly choose a theme
16//! let dark = Theme::dark();
17//! let light = Theme::light();
18//! ```
19//!
20//! # Color Philosophy
21//!
22//! The dark theme uses colors inspired by the Dracula palette:
23//! - **Green** = Success, strings (positive/data)
24//! - **Red** = Errors, operators (danger/action)
25//! - **Yellow** = Warnings, booleans (caution/special)
26//! - **Cyan** = Info, numbers (neutral data)
27//! - **Magenta** = Dates, SQL keywords (special syntax)
28//! - **Purple** = JSON, SQL numbers (structured data)
29//! - **Gray** = Dim text, comments, borders (secondary)
30
31/// A color that can be rendered differently based on output mode.
32///
33/// Contains both truecolor RGB and ANSI-256 fallback values,
34/// plus an optional plain text marker for non-color output.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct ThemeColor {
37    /// RGB color for truecolor terminals (r, g, b).
38    pub rgb: (u8, u8, u8),
39    /// ANSI 256-color fallback for older terminals.
40    pub ansi256: u8,
41    /// Plain text marker (for plain mode output), e.g., "NULL" for null values.
42    pub plain_marker: Option<&'static str>,
43}
44
45impl ThemeColor {
46    /// Create a theme color with RGB and ANSI-256 fallback.
47    ///
48    /// # Example
49    ///
50    /// ```rust
51    /// use sqlmodel_console::theme::ThemeColor;
52    ///
53    /// let green = ThemeColor::new((80, 250, 123), 84);
54    /// ```
55    #[must_use]
56    pub const fn new(rgb: (u8, u8, u8), ansi256: u8) -> Self {
57        Self {
58            rgb,
59            ansi256,
60            plain_marker: None,
61        }
62    }
63
64    /// Create a theme color with a plain text marker.
65    ///
66    /// The marker is used in plain mode to indicate special values
67    /// like NULL without using colors.
68    ///
69    /// # Example
70    ///
71    /// ```rust
72    /// use sqlmodel_console::theme::ThemeColor;
73    ///
74    /// let null_color = ThemeColor::with_marker((98, 114, 164), 60, "NULL");
75    /// ```
76    #[must_use]
77    pub const fn with_marker(rgb: (u8, u8, u8), ansi256: u8, marker: &'static str) -> Self {
78        Self {
79            rgb,
80            ansi256,
81            plain_marker: Some(marker),
82        }
83    }
84
85    /// Get the RGB components as a tuple.
86    #[must_use]
87    pub const fn rgb(&self) -> (u8, u8, u8) {
88        self.rgb
89    }
90
91    /// Get the ANSI-256 color code.
92    #[must_use]
93    pub const fn ansi256(&self) -> u8 {
94        self.ansi256
95    }
96
97    /// Get the plain text marker, if any.
98    #[must_use]
99    pub const fn plain_marker(&self) -> Option<&'static str> {
100        self.plain_marker
101    }
102
103    /// Get a truecolor ANSI escape sequence for this color.
104    ///
105    /// Returns a string like `\x1b[38;2;R;G;Bm` for foreground color.
106    #[must_use]
107    pub fn color_code(&self) -> String {
108        let (r, g, b) = self.rgb;
109        format!("\x1b[38;2;{r};{g};{b}m")
110    }
111}
112
113/// SQLModel console theme with semantic colors.
114///
115/// Defines all colors used throughout SQLModel console output.
116/// Use [`Theme::dark()`] or [`Theme::light()`] for predefined themes,
117/// or customize individual colors.
118///
119/// # Example
120///
121/// ```rust
122/// use sqlmodel_console::Theme;
123///
124/// let theme = Theme::dark();
125/// assert_eq!(theme.success.rgb(), (80, 250, 123));
126/// ```
127#[derive(Debug, Clone)]
128pub struct Theme {
129    // === Status Colors ===
130    /// Success messages, completion indicators (green).
131    pub success: ThemeColor,
132    /// Error messages, failure indicators (red).
133    pub error: ThemeColor,
134    /// Warning messages, deprecation notices (yellow).
135    pub warning: ThemeColor,
136    /// Informational messages, hints (cyan).
137    pub info: ThemeColor,
138
139    // === SQL Value Type Colors ===
140    /// NULL values (typically dim/italic).
141    pub null_value: ThemeColor,
142    /// Boolean values (true/false).
143    pub bool_value: ThemeColor,
144    /// Numeric values (integers, floats).
145    pub number_value: ThemeColor,
146    /// String/text values.
147    pub string_value: ThemeColor,
148    /// Date/time/timestamp values.
149    pub date_value: ThemeColor,
150    /// Binary/blob values.
151    pub binary_value: ThemeColor,
152    /// JSON values.
153    pub json_value: ThemeColor,
154    /// UUID values.
155    pub uuid_value: ThemeColor,
156
157    // === SQL Syntax Colors ===
158    /// SQL keywords (SELECT, FROM, WHERE).
159    pub sql_keyword: ThemeColor,
160    /// SQL strings ('value').
161    pub sql_string: ThemeColor,
162    /// SQL numbers (42, 3.14).
163    pub sql_number: ThemeColor,
164    /// SQL comments (-- comment).
165    pub sql_comment: ThemeColor,
166    /// SQL operators (=, >, AND).
167    pub sql_operator: ThemeColor,
168    /// SQL identifiers (table names, column names).
169    pub sql_identifier: ThemeColor,
170
171    // === UI Element Colors ===
172    /// Table/panel borders.
173    pub border: ThemeColor,
174    /// Headers and titles.
175    pub header: ThemeColor,
176    /// Dimmed/secondary text.
177    pub dim: ThemeColor,
178    /// Highlighted/emphasized text.
179    pub highlight: ThemeColor,
180}
181
182impl Theme {
183    /// Create the default dark theme (Dracula-inspired).
184    ///
185    /// This theme is optimized for dark terminal backgrounds and uses
186    /// the Dracula color palette for high contrast and visual appeal.
187    ///
188    /// # Example
189    ///
190    /// ```rust
191    /// use sqlmodel_console::Theme;
192    ///
193    /// let theme = Theme::dark();
194    /// ```
195    #[must_use]
196    pub fn dark() -> Self {
197        Self {
198            // Status colors (Dracula palette)
199            success: ThemeColor::new((80, 250, 123), 84), // Green
200            error: ThemeColor::new((255, 85, 85), 203),   // Red
201            warning: ThemeColor::new((241, 250, 140), 228), // Yellow
202            info: ThemeColor::new((139, 233, 253), 117),  // Cyan
203
204            // Value type colors
205            null_value: ThemeColor::with_marker((98, 114, 164), 60, "NULL"),
206            bool_value: ThemeColor::new((241, 250, 140), 228), // Yellow
207            number_value: ThemeColor::new((139, 233, 253), 117), // Cyan
208            string_value: ThemeColor::new((80, 250, 123), 84), // Green
209            date_value: ThemeColor::new((255, 121, 198), 212), // Magenta
210            binary_value: ThemeColor::new((255, 184, 108), 215), // Orange
211            json_value: ThemeColor::new((189, 147, 249), 141), // Purple
212            uuid_value: ThemeColor::new((255, 184, 108), 215), // Orange
213
214            // SQL syntax colors
215            sql_keyword: ThemeColor::new((255, 121, 198), 212), // Magenta
216            sql_string: ThemeColor::new((80, 250, 123), 84),    // Green
217            sql_number: ThemeColor::new((189, 147, 249), 141),  // Purple
218            sql_comment: ThemeColor::new((98, 114, 164), 60),   // Gray
219            sql_operator: ThemeColor::new((255, 85, 85), 203),  // Red
220            sql_identifier: ThemeColor::new((248, 248, 242), 255), // White
221
222            // UI elements
223            border: ThemeColor::new((98, 114, 164), 60), // Gray
224            header: ThemeColor::new((248, 248, 242), 255), // White
225            dim: ThemeColor::new((98, 114, 164), 60),    // Gray
226            highlight: ThemeColor::new((255, 255, 255), 231), // Bright white
227        }
228    }
229
230    /// Create a light theme variant.
231    ///
232    /// This theme is optimized for light terminal backgrounds with
233    /// darker colors for better visibility.
234    ///
235    /// # Example
236    ///
237    /// ```rust
238    /// use sqlmodel_console::Theme;
239    ///
240    /// let theme = Theme::light();
241    /// ```
242    #[must_use]
243    pub fn light() -> Self {
244        Self {
245            // Status colors (adjusted for light background)
246            success: ThemeColor::new((40, 167, 69), 34),
247            error: ThemeColor::new((220, 53, 69), 160),
248            warning: ThemeColor::new((255, 193, 7), 220),
249            info: ThemeColor::new((23, 162, 184), 37),
250
251            // Value colors (darker for visibility on light bg)
252            null_value: ThemeColor::with_marker((108, 117, 125), 244, "NULL"),
253            bool_value: ThemeColor::new((156, 39, 176), 128),
254            number_value: ThemeColor::new((0, 150, 136), 30),
255            string_value: ThemeColor::new((76, 175, 80), 34),
256            date_value: ThemeColor::new((156, 39, 176), 128),
257            binary_value: ThemeColor::new((255, 152, 0), 208),
258            json_value: ThemeColor::new((103, 58, 183), 92),
259            uuid_value: ThemeColor::new((255, 152, 0), 208),
260
261            // SQL syntax (darker)
262            sql_keyword: ThemeColor::new((156, 39, 176), 128),
263            sql_string: ThemeColor::new((76, 175, 80), 34),
264            sql_number: ThemeColor::new((103, 58, 183), 92),
265            sql_comment: ThemeColor::new((108, 117, 125), 244),
266            sql_operator: ThemeColor::new((220, 53, 69), 160),
267            sql_identifier: ThemeColor::new((33, 37, 41), 235),
268
269            // UI elements
270            border: ThemeColor::new((108, 117, 125), 244),
271            header: ThemeColor::new((33, 37, 41), 235),
272            dim: ThemeColor::new((108, 117, 125), 244),
273            highlight: ThemeColor::new((0, 0, 0), 16),
274        }
275    }
276
277    /// Create a new theme by cloning an existing one.
278    ///
279    /// Useful for customizing a preset theme.
280    ///
281    /// # Example
282    ///
283    /// ```rust
284    /// use sqlmodel_console::Theme;
285    /// use sqlmodel_console::theme::ThemeColor;
286    ///
287    /// let mut theme = Theme::dark();
288    /// theme.success = ThemeColor::new((0, 255, 0), 46); // Brighter green
289    /// ```
290    #[must_use]
291    pub fn new() -> Self {
292        Self::default()
293    }
294}
295
296impl Default for Theme {
297    fn default() -> Self {
298        Self::dark()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_theme_color_new() {
308        let color = ThemeColor::new((255, 0, 0), 196);
309        assert_eq!(color.rgb(), (255, 0, 0));
310        assert_eq!(color.ansi256(), 196);
311        assert_eq!(color.plain_marker(), None);
312    }
313
314    #[test]
315    fn test_theme_color_with_marker() {
316        let color = ThemeColor::with_marker((128, 128, 128), 244, "DIM");
317        assert_eq!(color.rgb(), (128, 128, 128));
318        assert_eq!(color.ansi256(), 244);
319        assert_eq!(color.plain_marker(), Some("DIM"));
320    }
321
322    #[test]
323    fn test_dark_theme_success_color() {
324        let theme = Theme::dark();
325        // Dracula green
326        assert_eq!(theme.success.rgb(), (80, 250, 123));
327    }
328
329    #[test]
330    fn test_light_theme_error_color() {
331        let theme = Theme::light();
332        // Bootstrap-style red
333        assert_eq!(theme.error.rgb(), (220, 53, 69));
334    }
335
336    #[test]
337    fn test_default_is_dark() {
338        let default = Theme::default();
339        let dark = Theme::dark();
340        assert_eq!(default.success.rgb(), dark.success.rgb());
341        assert_eq!(default.error.rgb(), dark.error.rgb());
342    }
343
344    #[test]
345    fn test_null_value_has_marker() {
346        let theme = Theme::dark();
347        assert_eq!(theme.null_value.plain_marker(), Some("NULL"));
348    }
349
350    #[test]
351    fn test_theme_clone() {
352        let theme1 = Theme::dark();
353        let theme2 = theme1.clone();
354        assert_eq!(theme1.success.rgb(), theme2.success.rgb());
355    }
356
357    #[test]
358    fn test_theme_color_copy() {
359        let color1 = ThemeColor::new((100, 100, 100), 245);
360        let color2 = color1; // Copy
361        assert_eq!(color1.rgb(), color2.rgb());
362    }
363
364    #[test]
365    fn test_all_dark_theme_colors_have_ansi256() {
366        let theme = Theme::dark();
367        // Verify all theme colors have non-zero ANSI-256 values
368        // (zero is typically only used for black, which is intentional for some colors)
369        let _ = theme.success.ansi256();
370        let _ = theme.error.ansi256();
371        let _ = theme.warning.ansi256();
372        let _ = theme.info.ansi256();
373        let _ = theme.null_value.ansi256();
374        let _ = theme.sql_keyword.ansi256();
375        let _ = theme.border.ansi256();
376        // If we got here, all colors have valid ANSI values
377    }
378
379    #[test]
380    fn test_all_light_theme_colors_have_ansi256() {
381        let theme = Theme::light();
382        // Verify all theme colors have valid ANSI-256 values
383        let _ = theme.success.ansi256();
384        let _ = theme.error.ansi256();
385        let _ = theme.warning.ansi256();
386        let _ = theme.info.ansi256();
387        // If we got here, all colors have valid ANSI values
388    }
389}