bestool_psql/
theme.rs

1use std::str::FromStr;
2
3/// Theme selection for syntax highlighting
4#[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	/// Auto-detect terminal theme
11	#[default]
12	Auto,
13}
14
15impl Theme {
16	/// Detect terminal theme by checking background color
17	///
18	/// Falls back to Dark if detection fails or is not supported
19	pub fn detect_terminal_theme() -> Self {
20		// Try to detect terminal background using OSC 11 query
21		// This is best-effort and may not work on all terminals
22		#[cfg(unix)]
23		{
24			use std::io::{Read, Write};
25			use std::os::unix::io::AsRawFd;
26
27			// Try to query terminal background color
28			let stdin_fd = std::io::stdin().as_raw_fd();
29
30			// Save terminal state
31			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			// Set raw mode for reading response
39			unsafe {
40				libc::cfmakeraw(&mut termios);
41				termios.c_cc[libc::VMIN] = 0;
42				termios.c_cc[libc::VTIME] = 1; // 0.1 second timeout
43				if libc::tcsetattr(stdin_fd, libc::TCSANOW, &termios) != 0 {
44					return Theme::Dark;
45				}
46			}
47
48			// Query background color
49			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				// Read response (timeout after 100ms via VTIME)
56				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				// Parse response: ESC ] 11 ; rgb:RRRR/GGGG/BBBB ESC \
67				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							// Calculate brightness (simple average of RGB)
73							// If R component is high, likely light background
74							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			// Restore terminal
88			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			// Windows and other platforms: default to dark
98			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	/// Resolve the theme to a concrete Light or Dark value
121	///
122	/// If the theme is Auto, performs terminal detection
123	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		// Auto resolves to either Light or Dark depending on terminal
151		let resolved = Theme::Auto.resolve();
152		assert!(resolved == Theme::Light || resolved == Theme::Dark);
153	}
154}