Skip to main content

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		use std::time::Duration;
21		use terminal_colorsaurus::{QueryOptions, ThemeMode};
22
23		let mut options = QueryOptions::default();
24
25		// Use longer timeout for SSH connections or Windows
26		let is_ssh = std::env::var("SSH_CONNECTION").is_ok()
27			|| std::env::var("SSH_CLIENT").is_ok()
28			|| std::env::var("SSH_TTY").is_ok();
29		let timeout = if is_ssh || cfg!(windows) {
30			Duration::from_millis(1000)
31		} else {
32			Duration::from_millis(200)
33		};
34		options.timeout = timeout;
35
36		match terminal_colorsaurus::theme_mode(options) {
37			Ok(ThemeMode::Light) => Theme::Light,
38			Ok(ThemeMode::Dark) => Theme::Dark,
39			Err(_) => Theme::Dark,
40		}
41	}
42}
43
44impl FromStr for Theme {
45	type Err = String;
46
47	fn from_str(s: &str) -> Result<Self, Self::Err> {
48		match s.to_lowercase().as_str() {
49			"light" => Ok(Theme::Light),
50			"dark" => Ok(Theme::Dark),
51			"auto" => Ok(Theme::Auto),
52			_ => Err(format!(
53				"invalid theme: '{}', must be 'light', 'dark', or 'auto'",
54				s
55			)),
56		}
57	}
58}
59
60impl Theme {
61	/// Resolve the theme to a concrete Light or Dark value
62	///
63	/// If the theme is Auto, performs terminal detection
64	pub fn resolve(&self) -> Theme {
65		match self {
66			Theme::Auto => Self::detect_terminal_theme(),
67			other => *other,
68		}
69	}
70}
71
72#[cfg(test)]
73mod tests {
74	use super::*;
75
76	#[test]
77	fn test_theme_parsing() {
78		assert_eq!("light".parse::<Theme>().unwrap(), Theme::Light);
79		assert_eq!("Light".parse::<Theme>().unwrap(), Theme::Light);
80		assert_eq!("LIGHT".parse::<Theme>().unwrap(), Theme::Light);
81		assert_eq!("dark".parse::<Theme>().unwrap(), Theme::Dark);
82		assert_eq!("Dark".parse::<Theme>().unwrap(), Theme::Dark);
83		assert_eq!("auto".parse::<Theme>().unwrap(), Theme::Auto);
84		assert!("invalid".parse::<Theme>().is_err());
85	}
86
87	#[test]
88	fn test_theme_resolve() {
89		assert_eq!(Theme::Light.resolve(), Theme::Light);
90		assert_eq!(Theme::Dark.resolve(), Theme::Dark);
91		// Auto resolves to either Light or Dark depending on terminal
92		let resolved = Theme::Auto.resolve();
93		assert!(resolved == Theme::Light || resolved == Theme::Dark);
94	}
95}