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}