Skip to main content

mermaid_cli/render/
theme.rs

1use ratatui::style::Color;
2use serde::{Deserialize, Serialize};
3
4/// Theme configuration for the TUI
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Theme {
7    pub name: String,
8    pub colors: ThemeColors,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ThemeColors {
13    // Primary colors
14    pub background: ColorValue,
15    pub foreground: ColorValue,
16
17    // UI elements
18    pub border: ColorValue,
19    pub border_focused: ColorValue,
20    pub header: ColorValue,
21    pub status_bar: ColorValue,
22
23    // Text colors
24    pub text_primary: ColorValue,
25    pub text_secondary: ColorValue,
26    pub text_disabled: ColorValue,
27    pub text_highlight: ColorValue,
28
29    // Message colors
30    pub user_message: ColorValue,
31    pub assistant_message: ColorValue,
32    pub system_message: ColorValue,
33    /// Full-width background band behind the user's submitted prompt
34    /// (Claude-Code style). A clearly-visible neutral gray, a step above the
35    /// main background — not a blue tint.
36    pub user_message_background: ColorValue,
37
38    // Code highlighting
39    pub code_background: ColorValue,
40    pub code_foreground: ColorValue,
41    pub code_keyword: ColorValue,
42    pub code_string: ColorValue,
43    pub code_comment: ColorValue,
44
45    // Mode colors
46    pub mode_normal: ColorValue,
47    pub mode_accept_edits: ColorValue,
48    pub mode_plan: ColorValue,
49    pub mode_bypass_all: ColorValue,
50
51    // Status colors
52    pub success: ColorValue,
53    pub warning: ColorValue,
54    pub error: ColorValue,
55    pub info: ColorValue,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(untagged)]
60pub enum ColorValue {
61    Rgb { r: u8, g: u8, b: u8 },
62    Named(String),
63}
64
65impl ColorValue {
66    pub fn to_color(&self) -> Color {
67        match self {
68            ColorValue::Rgb { r, g, b } => Color::Rgb(*r, *g, *b),
69            ColorValue::Named(name) => match name.as_str() {
70                "black" => Color::Black,
71                "red" => Color::Red,
72                "green" => Color::Green,
73                "yellow" => Color::Yellow,
74                "blue" => Color::Blue,
75                "magenta" => Color::Magenta,
76                "cyan" => Color::Cyan,
77                "white" => Color::White,
78                "gray" | "grey" => Color::Gray,
79                "dark_gray" | "dark_grey" => Color::DarkGray,
80                _ => Color::White,
81            },
82        }
83    }
84}
85
86impl Theme {
87    /// Create a light theme.
88    ///
89    /// Not wired into any config reader yet — the `Deserialize` impl on
90    /// `Theme` is retained so a future patch can load
91    /// `config.ui.theme = "light" | "dark"` from config.toml and select
92    /// the constructor. Until then, call this explicitly to test.
93    pub fn light() -> Self {
94        Self {
95            name: "Light".to_string(),
96            colors: ThemeColors {
97                background: ColorValue::Rgb {
98                    r: 250,
99                    g: 250,
100                    b: 250,
101                },
102                foreground: ColorValue::Rgb {
103                    r: 30,
104                    g: 30,
105                    b: 30,
106                },
107
108                border: ColorValue::Named("gray".to_string()),
109                border_focused: ColorValue::Named("blue".to_string()),
110                header: ColorValue::Named("blue".to_string()),
111                status_bar: ColorValue::Named("white".to_string()),
112
113                text_primary: ColorValue::Named("black".to_string()),
114                text_secondary: ColorValue::Named("dark_gray".to_string()),
115                text_disabled: ColorValue::Named("gray".to_string()),
116                text_highlight: ColorValue::Named("magenta".to_string()),
117
118                user_message: ColorValue::Named("blue".to_string()),
119                assistant_message: ColorValue::Named("green".to_string()),
120                system_message: ColorValue::Named("yellow".to_string()),
121                user_message_background: ColorValue::Rgb {
122                    r: 230,
123                    g: 230,
124                    b: 230,
125                },
126
127                code_background: ColorValue::Rgb {
128                    r: 240,
129                    g: 240,
130                    b: 240,
131                },
132                code_foreground: ColorValue::Named("dark_gray".to_string()),
133                code_keyword: ColorValue::Named("magenta".to_string()),
134                code_string: ColorValue::Named("green".to_string()),
135                code_comment: ColorValue::Named("gray".to_string()),
136
137                mode_normal: ColorValue::Named("green".to_string()),
138                mode_accept_edits: ColorValue::Named("yellow".to_string()),
139                mode_plan: ColorValue::Named("blue".to_string()),
140                mode_bypass_all: ColorValue::Named("red".to_string()),
141
142                success: ColorValue::Named("green".to_string()),
143                warning: ColorValue::Named("yellow".to_string()),
144                error: ColorValue::Named("red".to_string()),
145                info: ColorValue::Named("blue".to_string()),
146            },
147        }
148    }
149
150    /// Create the default dark theme
151    pub fn dark() -> Self {
152        Self {
153            name: "Dark".to_string(),
154            colors: ThemeColors {
155                background: ColorValue::Rgb {
156                    r: 20,
157                    g: 20,
158                    b: 20,
159                },
160                foreground: ColorValue::Rgb {
161                    r: 230,
162                    g: 230,
163                    b: 230,
164                },
165
166                border: ColorValue::Named("dark_gray".to_string()),
167                border_focused: ColorValue::Named("cyan".to_string()),
168                header: ColorValue::Named("cyan".to_string()),
169                status_bar: ColorValue::Named("black".to_string()),
170
171                text_primary: ColorValue::Named("white".to_string()),
172                text_secondary: ColorValue::Named("gray".to_string()),
173                text_disabled: ColorValue::Named("dark_gray".to_string()),
174                text_highlight: ColorValue::Named("yellow".to_string()),
175
176                user_message: ColorValue::Named("blue".to_string()),
177                assistant_message: ColorValue::Named("green".to_string()),
178                system_message: ColorValue::Named("yellow".to_string()),
179                user_message_background: ColorValue::Rgb {
180                    r: 54,
181                    g: 54,
182                    b: 54,
183                },
184
185                code_background: ColorValue::Rgb {
186                    r: 40,
187                    g: 40,
188                    b: 40,
189                },
190                code_foreground: ColorValue::Named("gray".to_string()),
191                code_keyword: ColorValue::Named("magenta".to_string()),
192                code_string: ColorValue::Named("green".to_string()),
193                code_comment: ColorValue::Named("dark_gray".to_string()),
194
195                mode_normal: ColorValue::Named("green".to_string()),
196                mode_accept_edits: ColorValue::Named("yellow".to_string()),
197                mode_plan: ColorValue::Named("blue".to_string()),
198                mode_bypass_all: ColorValue::Named("red".to_string()),
199
200                success: ColorValue::Named("green".to_string()),
201                warning: ColorValue::Named("yellow".to_string()),
202                error: ColorValue::Named("red".to_string()),
203                info: ColorValue::Named("cyan".to_string()),
204            },
205        }
206    }
207}