Skip to main content

semantic_diff/
theme.rs

1use ratatui::style::Color;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum ThemeMode {
5    Dark,
6    Light,
7    Auto,
8}
9
10#[derive(Debug, Clone)]
11pub struct Theme {
12    // Diff view
13    pub selection_bg: Color,
14    pub file_header_bg: Color,
15    pub added_line_bg: Color,
16    pub removed_line_bg: Color,
17    pub added_emphasis_bg: Color,   // inline diff changed segments
18    pub removed_emphasis_bg: Color, // inline diff changed segments
19    pub file_header_fg: Color,
20    pub context_fg: Color,
21    pub context_bg: Color,
22
23    // Gutter
24    pub gutter_fg: Color,
25
26    // Help overlay
27    pub help_text_fg: Color,
28    pub help_section_fg: Color, // Cyan section headers
29    pub help_key_fg: Color,     // Yellow key names
30    pub help_dismiss_fg: Color, // DarkGray "press any key"
31    pub help_overlay_bg: Color, // Help popup background
32
33    // File tree
34    pub tree_highlight_fg: Color,
35    pub tree_highlight_bg: Color,
36    pub tree_group_fg: Color,
37
38    // Search
39    pub search_match_fg: Color,
40    pub search_match_bg: Color,
41
42    // Markdown preview
43    pub md_inline_code_fg: Color,
44    pub md_heading_h1_fg: Color,
45    pub md_heading_h2_fg: Color,
46    pub md_heading_h3_fg: Color,
47    pub md_heading_h4_fg: Color,
48    pub md_heading_h5_fg: Color,
49    pub md_heading_h6_fg: Color,
50    pub md_list_bullet_fg: Color,
51    pub md_code_block_fg: Color,
52    pub md_code_block_delim_fg: Color,
53    pub md_blockquote_fg: Color,
54    pub md_link_fg: Color,
55    pub md_rule_fg: Color,
56
57    // Syntect theme name
58    pub syntect_theme: &'static str,
59}
60
61impl Theme {
62    pub fn dark() -> Self {
63        Self {
64            selection_bg: Color::Rgb(40, 40, 60),
65            file_header_bg: Color::Rgb(30, 30, 40),
66            added_line_bg: Color::Rgb(0, 40, 0),
67            removed_line_bg: Color::Rgb(40, 0, 0),
68            added_emphasis_bg: Color::Rgb(0, 80, 0),
69            removed_emphasis_bg: Color::Rgb(80, 0, 0),
70            file_header_fg: Color::White,
71            context_fg: Color::Reset,
72            context_bg: Color::Reset,
73            gutter_fg: Color::DarkGray,
74            help_text_fg: Color::White,
75            help_section_fg: Color::Cyan,
76            help_key_fg: Color::Yellow,
77            help_dismiss_fg: Color::DarkGray,
78            help_overlay_bg: Color::Black,
79            tree_highlight_fg: Color::Black,
80            tree_highlight_bg: Color::Cyan,
81            tree_group_fg: Color::Cyan,
82            search_match_fg: Color::Black,
83            search_match_bg: Color::Yellow,
84            md_inline_code_fg: Color::Yellow,
85            md_heading_h1_fg: Color::Magenta,
86            md_heading_h2_fg: Color::Cyan,
87            md_heading_h3_fg: Color::Green,
88            md_heading_h4_fg: Color::Yellow,
89            md_heading_h5_fg: Color::Blue,
90            md_heading_h6_fg: Color::Red,
91            md_list_bullet_fg: Color::Cyan,
92            md_code_block_fg: Color::Green,
93            md_code_block_delim_fg: Color::DarkGray,
94            md_blockquote_fg: Color::DarkGray,
95            md_link_fg: Color::Blue,
96            md_rule_fg: Color::DarkGray,
97            syntect_theme: "base16-ocean.dark",
98        }
99    }
100
101    pub fn light() -> Self {
102        Self {
103            selection_bg: Color::Rgb(210, 210, 230),
104            file_header_bg: Color::Rgb(220, 220, 235),
105            added_line_bg: Color::Rgb(210, 255, 210),
106            removed_line_bg: Color::Rgb(255, 210, 210),
107            added_emphasis_bg: Color::Rgb(170, 240, 170),
108            removed_emphasis_bg: Color::Rgb(240, 170, 170),
109            file_header_fg: Color::Black,
110            context_fg: Color::Reset,
111            context_bg: Color::Reset,
112            gutter_fg: Color::Gray,
113            help_text_fg: Color::Black,
114            help_section_fg: Color::Blue,
115            help_key_fg: Color::Red,
116            help_dismiss_fg: Color::Gray,
117            help_overlay_bg: Color::White,
118            tree_highlight_fg: Color::White,
119            tree_highlight_bg: Color::Blue,
120            tree_group_fg: Color::Blue,
121            search_match_fg: Color::Black,
122            search_match_bg: Color::Yellow,
123            md_inline_code_fg: Color::Rgb(180, 80, 0),
124            md_heading_h1_fg: Color::Rgb(160, 0, 160),
125            md_heading_h2_fg: Color::Rgb(0, 130, 150),
126            md_heading_h3_fg: Color::Rgb(0, 130, 0),
127            md_heading_h4_fg: Color::Rgb(180, 80, 0),
128            md_heading_h5_fg: Color::Blue,
129            md_heading_h6_fg: Color::Red,
130            md_list_bullet_fg: Color::Rgb(0, 130, 150),
131            md_code_block_fg: Color::Rgb(0, 130, 0),
132            md_code_block_delim_fg: Color::Gray,
133            md_blockquote_fg: Color::Gray,
134            md_link_fg: Color::Blue,
135            md_rule_fg: Color::Gray,
136            syntect_theme: "base16-ocean.light",
137        }
138    }
139
140    pub fn from_mode(mode: ThemeMode) -> Self {
141        match mode {
142            ThemeMode::Dark => Self::dark(),
143            ThemeMode::Light => Self::light(),
144            ThemeMode::Auto => {
145                if detect_light_background() {
146                    Self::light()
147                } else {
148                    Self::dark()
149                }
150            }
151        }
152    }
153}
154
155/// Detect terminal background brightness.
156///
157/// Strategy (in order):
158/// 1. OSC 11 query via `terminal-light` — sends an escape sequence to the
159///    terminal and reads back the background RGB. Works on local terminals
160///    but fails through tmux/SSH (tmux intercepts the escape sequence).
161/// 2. `COLORFGBG` env var — set by iTerm2, rxvt, and some other terminals.
162///    Format is "fg;bg" (e.g. "7;0" for dark). Not forwarded by SSH by
163///    default, but available locally and sometimes in tmux's environment.
164///
165/// For SSH/tmux sessions where auto-detection fails, set `"theme": "light"`
166/// in ~/.config/semantic-diff.json or use `--theme=light`.
167fn detect_light_background() -> bool {
168    use std::io::IsTerminal;
169
170    // Skip in non-interactive environments
171    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
172        return false;
173    }
174    if std::env::var("CI").is_ok() || std::env::var("TERM").as_deref() == Ok("dumb") {
175        return false;
176    }
177
178    // 1. Direct terminal query (works locally, fails through tmux/SSH)
179    if let Ok(luma) = terminal_light::luma() {
180        return luma > 0.6;
181    }
182
183    // 2. COLORFGBG (e.g. "7;0" dark, "0;15" light) — set by some terminals
184    if let Ok(val) = std::env::var("COLORFGBG") {
185        if let Some(bg) = val.rsplit(';').next().and_then(|s| s.parse::<u8>().ok()) {
186            // ANSI colors 0-6 are dark, 7+ are light
187            return bg >= 7;
188        }
189    }
190
191    false
192}