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