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    // Syntect theme name
43    pub syntect_theme: &'static str,
44}
45
46impl Theme {
47    pub fn dark() -> Self {
48        Self {
49            selection_bg: Color::Rgb(40, 40, 60),
50            file_header_bg: Color::Rgb(30, 30, 40),
51            added_line_bg: Color::Rgb(0, 40, 0),
52            removed_line_bg: Color::Rgb(40, 0, 0),
53            added_emphasis_bg: Color::Rgb(0, 80, 0),
54            removed_emphasis_bg: Color::Rgb(80, 0, 0),
55            file_header_fg: Color::White,
56            context_fg: Color::Reset,
57            context_bg: Color::Reset,
58            gutter_fg: Color::DarkGray,
59            help_text_fg: Color::White,
60            help_section_fg: Color::Cyan,
61            help_key_fg: Color::Yellow,
62            help_dismiss_fg: Color::DarkGray,
63            help_overlay_bg: Color::Black,
64            tree_highlight_fg: Color::Black,
65            tree_highlight_bg: Color::Cyan,
66            tree_group_fg: Color::Cyan,
67            search_match_fg: Color::Black,
68            search_match_bg: Color::Yellow,
69            syntect_theme: "base16-ocean.dark",
70        }
71    }
72
73    pub fn light() -> Self {
74        Self {
75            selection_bg: Color::Rgb(210, 210, 230),
76            file_header_bg: Color::Rgb(220, 220, 235),
77            added_line_bg: Color::Rgb(210, 255, 210),
78            removed_line_bg: Color::Rgb(255, 210, 210),
79            added_emphasis_bg: Color::Rgb(170, 240, 170),
80            removed_emphasis_bg: Color::Rgb(240, 170, 170),
81            file_header_fg: Color::Black,
82            context_fg: Color::Reset,
83            context_bg: Color::Reset,
84            gutter_fg: Color::Gray,
85            help_text_fg: Color::Black,
86            help_section_fg: Color::Blue,
87            help_key_fg: Color::Red,
88            help_dismiss_fg: Color::Gray,
89            help_overlay_bg: Color::White,
90            tree_highlight_fg: Color::White,
91            tree_highlight_bg: Color::Blue,
92            tree_group_fg: Color::Blue,
93            search_match_fg: Color::Black,
94            search_match_bg: Color::Yellow,
95            syntect_theme: "base16-ocean.light",
96        }
97    }
98
99    pub fn from_mode(mode: ThemeMode) -> Self {
100        match mode {
101            ThemeMode::Dark => Self::dark(),
102            ThemeMode::Light => Self::light(),
103            ThemeMode::Auto => {
104                if detect_light_background() {
105                    Self::light()
106                } else {
107                    Self::dark()
108                }
109            }
110        }
111    }
112}
113
114/// Detect terminal background using OSC 11 via the `terminal-light` crate.
115/// Falls back to dark in non-interactive or CI environments.
116fn detect_light_background() -> bool {
117    use std::io::IsTerminal;
118
119    // Skip in non-interactive environments
120    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
121        return false;
122    }
123    if std::env::var("CI").is_ok() || std::env::var("TERM").as_deref() == Ok("dumb") {
124        return false;
125    }
126
127    match terminal_light::luma() {
128        Ok(luma) => luma > 0.6,
129        Err(_) => false,
130    }
131}