Skip to main content

zeph_tui/
theme.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use ratatui::style::{Color, Modifier, Style};
5
6/// Ratatui [`Style`] mappings for tree-sitter syntax-highlight capture groups.
7///
8/// Each field corresponds to a tree-sitter capture name (e.g. `"keyword"`,
9/// `"string"`, `"comment"`). The [`crate::highlight::SyntaxHighlighter`] uses
10/// this struct to map highlight events to terminal styles.
11///
12/// The [`Default`] implementation provides a dark One Dark-inspired palette.
13///
14/// # Examples
15///
16/// ```rust
17/// use zeph_tui::theme::SyntaxTheme;
18///
19/// let theme = SyntaxTheme::default();
20/// // Keywords are rendered bold.
21/// use ratatui::style::Modifier;
22/// assert!(theme.keyword.add_modifier.contains(Modifier::BOLD));
23/// ```
24pub struct SyntaxTheme {
25    /// Style for language keywords (e.g. `fn`, `let`, `if`).
26    pub keyword: Style,
27    /// Style for string literals.
28    pub string: Style,
29    /// Style for comments.
30    pub comment: Style,
31    /// Style for function names.
32    pub function: Style,
33    /// Style for type names and constructors.
34    pub r#type: Style,
35    /// Style for numeric literals.
36    pub number: Style,
37    /// Style for operators.
38    pub operator: Style,
39    /// Style for variable names and parameters.
40    pub variable: Style,
41    /// Style for attributes and annotations.
42    pub attribute: Style,
43    /// Style for punctuation tokens.
44    pub punctuation: Style,
45    /// Style for constants and built-in values.
46    pub constant: Style,
47    /// Fallback style for unstyled source text.
48    pub default: Style,
49}
50
51/// Visual theme for the TUI dashboard widgets.
52///
53/// Contains [`Style`] values for every distinct UI element — message roles,
54/// input fields, borders, diff gutters, hyperlinks, and status elements.
55/// The [`Default`] implementation provides a dark blue colour scheme.
56///
57/// # Examples
58///
59/// ```rust
60/// use zeph_tui::theme::Theme;
61///
62/// let theme = Theme::default();
63/// // User and assistant messages use distinct colours.
64/// assert_ne!(theme.user_message, theme.assistant_message);
65/// ```
66pub struct Theme {
67    pub user_message: Style,
68    pub assistant_message: Style,
69    pub system_message: Style,
70    pub input_text: Style,
71    pub input_cursor: Style,
72    pub status_bar: Style,
73    pub header: Style,
74    pub panel_border: Style,
75    pub panel_title: Style,
76    pub highlight: Style,
77    pub error: Style,
78    pub thinking_message: Style,
79    pub code_inline: Style,
80    pub code_block: Style,
81    pub streaming_cursor: Style,
82    pub tool_command: Style,
83    pub assistant_accent: Style,
84    pub tool_accent: Style,
85    pub diff_added_bg: Color,
86    pub diff_removed_bg: Color,
87    pub diff_word_added_bg: Color,
88    pub diff_word_removed_bg: Color,
89    pub diff_gutter_add: Style,
90    pub diff_gutter_remove: Style,
91    pub diff_header: Style,
92    pub link: Style,
93    pub table_border: Style,
94}
95
96impl Default for Theme {
97    fn default() -> Self {
98        Self {
99            user_message: Style::default().fg(Color::Cyan),
100            assistant_message: Style::default().fg(Color::Rgb(200, 200, 210)),
101            system_message: Style::default().fg(Color::DarkGray),
102            input_text: Style::default().fg(Color::Cyan),
103            input_cursor: Style::default()
104                .fg(Color::Yellow)
105                .add_modifier(Modifier::BOLD),
106            status_bar: Style::default().fg(Color::White).bg(Color::DarkGray),
107            header: Style::default()
108                .fg(Color::Rgb(200, 220, 255))
109                .bg(Color::Rgb(20, 40, 80))
110                .add_modifier(Modifier::BOLD),
111            panel_border: Style::default().fg(Color::Gray),
112            panel_title: Style::default()
113                .fg(Color::White)
114                .add_modifier(Modifier::BOLD),
115            highlight: Style::default().fg(Color::Rgb(215, 150, 60)),
116            error: Style::default().fg(Color::Red),
117            thinking_message: Style::default().fg(Color::DarkGray),
118            code_inline: Style::default()
119                .fg(Color::Rgb(100, 180, 255))
120                .bg(Color::Rgb(15, 30, 55))
121                .add_modifier(Modifier::BOLD),
122            code_block: Style::default().fg(Color::Rgb(190, 175, 145)),
123            streaming_cursor: Style::default().fg(Color::DarkGray),
124            tool_command: Style::default()
125                .fg(Color::Yellow)
126                .add_modifier(Modifier::BOLD),
127            assistant_accent: Style::default().fg(Color::Rgb(185, 85, 25)),
128            tool_accent: Style::default().fg(Color::Rgb(140, 120, 50)),
129            diff_added_bg: Color::Rgb(0, 40, 0),
130            diff_removed_bg: Color::Rgb(40, 0, 0),
131            diff_word_added_bg: Color::Rgb(0, 80, 0),
132            diff_word_removed_bg: Color::Rgb(80, 0, 0),
133            diff_gutter_add: Style::default().fg(Color::Green),
134            diff_gutter_remove: Style::default().fg(Color::Red),
135            diff_header: Style::default().fg(Color::DarkGray),
136            link: Style::default()
137                .fg(Color::Cyan)
138                .add_modifier(Modifier::UNDERLINED),
139            table_border: Style::default().fg(Color::DarkGray),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn default_theme_has_distinct_message_styles() {
150        let theme = Theme::default();
151        assert_ne!(theme.user_message, theme.assistant_message);
152        assert_ne!(theme.assistant_message, theme.system_message);
153    }
154
155    #[test]
156    fn default_theme_status_bar_has_background() {
157        let theme = Theme::default();
158        assert_eq!(theme.status_bar.bg, Some(Color::DarkGray));
159    }
160}