1use crossterm::style::Color;
2#[cfg(not(target_arch = "wasm32"))]
3use std::time::Duration;
4use tracing::debug;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Theme {
9 Light,
10 Dark,
11 Neutral,
12}
13
14#[derive(Debug, Clone)]
16pub struct ColorPalette {
17 pub error: Color,
19 pub warning: Color,
20 pub info: Color,
21 pub debug: Color,
22 pub trace: Color,
23 pub reasoning: Color,
25 pub user: Color,
26
27 pub server: Color,
29 pub client: Color,
30 pub connection: Color,
31
32 pub separator: Color,
34 pub dimmed: Color,
35 pub normal: Color,
36
37 pub success: Color,
39 pub failure: Color,
40 pub ask: Color,
41}
42
43impl ColorPalette {
44 pub fn dark() -> Self {
46 Self {
47 error: Color::Red,
48 warning: Color::Yellow,
49 info: Color::AnsiValue(75),
55 debug: Color::Cyan,
56 trace: Color::DarkGrey,
57 reasoning: Color::Magenta,
58 user: Color::Green,
59 server: Color::Cyan,
60 client: Color::Magenta,
61 connection: Color::Cyan,
62 separator: Color::DarkGreen,
63 dimmed: Color::DarkGrey,
64 normal: Color::White,
65 success: Color::Green,
66 failure: Color::Red,
67 ask: Color::Yellow,
68 }
69 }
70
71 pub fn light() -> Self {
73 Self {
74 error: Color::DarkRed,
75 warning: Color::DarkYellow,
76 info: Color::DarkBlue,
77 debug: Color::DarkCyan,
78 trace: Color::DarkGrey,
79 reasoning: Color::DarkMagenta,
80 user: Color::DarkGreen,
81 server: Color::DarkCyan,
82 client: Color::DarkMagenta,
83 connection: Color::DarkCyan,
84 separator: Color::DarkGreen,
85 dimmed: Color::Grey,
86 normal: Color::Black,
87 success: Color::DarkGreen,
88 failure: Color::DarkRed,
89 ask: Color::DarkYellow,
90 }
91 }
92
93 pub fn neutral() -> Self {
96 Self {
97 error: Color::Red,
98 warning: Color::DarkYellow,
99 info: Color::AnsiValue(32),
104 debug: Color::DarkCyan,
105 trace: Color::Grey,
106 reasoning: Color::Magenta,
107 user: Color::DarkGreen,
108 server: Color::DarkCyan,
109 client: Color::DarkMagenta,
110 connection: Color::DarkCyan,
111 separator: Color::DarkGreen,
112 dimmed: Color::Grey,
113 normal: Color::Reset, success: Color::DarkGreen,
115 failure: Color::Red,
116 ask: Color::DarkYellow,
117 }
118 }
119
120 pub fn from_theme(theme: Theme) -> Self {
122 match theme {
123 Theme::Dark => Self::dark(),
124 Theme::Light => Self::light(),
125 Theme::Neutral => Self::neutral(),
126 }
127 }
128}
129
130#[cfg(not(target_arch = "wasm32"))]
137pub fn detect_theme() -> Option<Theme> {
138 use std::panic::catch_unwind;
139
140 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
143 if term_program == "Apple_Terminal" {
145 debug!("Skipping theme detection on Apple Terminal (known issues)");
146 return theme_from_colorfgbg();
147 }
148 }
149
150 let timeout = Duration::from_millis(100);
152
153 let result = catch_unwind(|| termbg::theme(timeout));
155
156 flush_stdin_nonblocking();
160
161 match result {
162 Ok(Ok(termbg::Theme::Light)) => Some(Theme::Light),
163 Ok(Ok(termbg::Theme::Dark)) => Some(Theme::Dark),
164 Ok(Err(e)) => {
165 debug!("Theme detection failed: {:?}", e);
166 theme_from_colorfgbg()
167 }
168 Err(_) => {
169 debug!("Theme detection panicked");
170 theme_from_colorfgbg()
171 }
172 }
173}
174
175#[cfg(not(target_arch = "wasm32"))]
182fn theme_from_colorfgbg() -> Option<Theme> {
183 let value = std::env::var("COLORFGBG").ok()?;
184 let bg = value.rsplit(';').next()?.trim().parse::<u8>().ok()?;
185 let theme = if bg == 7 || bg == 15 {
187 Theme::Light
188 } else {
189 Theme::Dark
190 };
191 debug!("Theme from COLORFGBG={value}: {theme:?}");
192 Some(theme)
193}
194
195#[cfg(not(target_arch = "wasm32"))]
198fn flush_stdin_nonblocking() {
199 use std::io::Read;
200
201 #[cfg(unix)]
203 {
204 use std::os::unix::io::AsRawFd;
205
206 let stdin = std::io::stdin();
207 let fd = stdin.as_raw_fd();
208
209 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
211 if flags < 0 {
212 return;
213 }
214
215 if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
217 return;
218 }
219
220 let mut buf = [0u8; 256];
222 let mut stdin_lock = stdin.lock();
223 while stdin_lock.read(&mut buf).unwrap_or(0) > 0 {}
224
225 unsafe { libc::fcntl(fd, libc::F_SETFL, flags) };
227 }
228
229 #[cfg(not(unix))]
232 {}
233}
234
235#[cfg(target_arch = "wasm32")]
237pub fn detect_theme() -> Option<Theme> {
238 None
239}
240
241pub fn parse_theme(s: &str) -> anyhow::Result<Option<Theme>> {
243 match s.to_lowercase().as_str() {
244 "auto" => Ok(None), "light" => Ok(Some(Theme::Light)),
246 "dark" => Ok(Some(Theme::Dark)),
247 "neutral" => Ok(Some(Theme::Neutral)),
248 _ => anyhow::bail!(
249 "Invalid theme '{}'. Valid options: auto, light, dark, neutral",
250 s
251 ),
252 }
253}