1use std::str::FromStr;
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
6#[cfg_attr(feature = "cli", clap(rename_all = "lowercase"))]
7pub enum Theme {
8 Light,
9 Dark,
10 #[default]
12 Auto,
13}
14
15impl Theme {
16 pub fn detect_terminal_theme() -> Self {
20 #[cfg(unix)]
23 {
24 use std::io::{Read, Write};
25 use std::os::unix::io::AsRawFd;
26
27 let stdin_fd = std::io::stdin().as_raw_fd();
29
30 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
32 if unsafe { libc::tcgetattr(stdin_fd, &mut termios) } != 0 {
33 return Theme::Dark;
34 }
35
36 let original_termios = termios;
37
38 unsafe {
40 libc::cfmakeraw(&mut termios);
41 termios.c_cc[libc::VMIN] = 0;
42 termios.c_cc[libc::VTIME] = 1; if libc::tcsetattr(stdin_fd, libc::TCSANOW, &termios) != 0 {
44 return Theme::Dark;
45 }
46 }
47
48 let query = b"\x1b]11;?\x1b\\";
50 let mut stdout = std::io::stdout();
51 let result = (|| -> Option<Theme> {
52 stdout.write_all(query).ok()?;
53 stdout.flush().ok()?;
54
55 let mut stdin = std::io::stdin();
57 let mut buf = [0u8; 256];
58 let n = stdin.read(&mut buf).ok()?;
59
60 if n == 0 {
61 return None;
62 }
63
64 let response = String::from_utf8_lossy(&buf[..n]);
65
66 if let Some(rgb_start) = response.find("rgb:") {
68 let rgb_part = &response[rgb_start + 4..];
69 if let Some(slash1) = rgb_part.find('/') {
70 let r_str = &rgb_part[..slash1];
71 if let Ok(r) = u16::from_str_radix(&r_str[..r_str.len().min(4)], 16) {
72 let brightness = (r as f32 / 65535.0) * 100.0;
75 return Some(if brightness > 50.0 {
76 Theme::Light
77 } else {
78 Theme::Dark
79 });
80 }
81 }
82 }
83
84 None
85 })();
86
87 unsafe {
89 libc::tcsetattr(stdin_fd, libc::TCSANOW, &original_termios);
90 }
91
92 result.unwrap_or(Theme::Dark)
93 }
94
95 #[cfg(not(unix))]
96 {
97 Theme::Dark
99 }
100 }
101}
102
103impl FromStr for Theme {
104 type Err = String;
105
106 fn from_str(s: &str) -> Result<Self, Self::Err> {
107 match s.to_lowercase().as_str() {
108 "light" => Ok(Theme::Light),
109 "dark" => Ok(Theme::Dark),
110 "auto" => Ok(Theme::Auto),
111 _ => Err(format!(
112 "invalid theme: '{}', must be 'light', 'dark', or 'auto'",
113 s
114 )),
115 }
116 }
117}
118
119impl Theme {
120 pub fn resolve(&self) -> Theme {
124 match self {
125 Theme::Auto => Self::detect_terminal_theme(),
126 other => *other,
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_theme_parsing() {
137 assert_eq!("light".parse::<Theme>().unwrap(), Theme::Light);
138 assert_eq!("Light".parse::<Theme>().unwrap(), Theme::Light);
139 assert_eq!("LIGHT".parse::<Theme>().unwrap(), Theme::Light);
140 assert_eq!("dark".parse::<Theme>().unwrap(), Theme::Dark);
141 assert_eq!("Dark".parse::<Theme>().unwrap(), Theme::Dark);
142 assert_eq!("auto".parse::<Theme>().unwrap(), Theme::Auto);
143 assert!("invalid".parse::<Theme>().is_err());
144 }
145
146 #[test]
147 fn test_theme_resolve() {
148 assert_eq!(Theme::Light.resolve(), Theme::Light);
149 assert_eq!(Theme::Dark.resolve(), Theme::Dark);
150 let resolved = Theme::Auto.resolve();
152 assert!(resolved == Theme::Light || resolved == Theme::Dark);
153 }
154}