use crossterm::style::Color;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use tracing::debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Theme {
Light,
Dark,
Neutral,
}
#[derive(Debug, Clone)]
pub struct ColorPalette {
pub error: Color,
pub warning: Color,
pub info: Color,
pub debug: Color,
pub trace: Color,
pub reasoning: Color,
pub user: Color,
pub server: Color,
pub client: Color,
pub connection: Color,
pub separator: Color,
pub dimmed: Color,
pub normal: Color,
pub success: Color,
pub failure: Color,
pub ask: Color,
}
impl ColorPalette {
pub fn dark() -> Self {
Self {
error: Color::Red,
warning: Color::Yellow,
info: Color::AnsiValue(75),
debug: Color::Cyan,
trace: Color::DarkGrey,
reasoning: Color::Magenta,
user: Color::Green,
server: Color::Cyan,
client: Color::Magenta,
connection: Color::Cyan,
separator: Color::DarkGreen,
dimmed: Color::DarkGrey,
normal: Color::White,
success: Color::Green,
failure: Color::Red,
ask: Color::Yellow,
}
}
pub fn light() -> Self {
Self {
error: Color::DarkRed,
warning: Color::DarkYellow,
info: Color::DarkBlue,
debug: Color::DarkCyan,
trace: Color::DarkGrey,
reasoning: Color::DarkMagenta,
user: Color::DarkGreen,
server: Color::DarkCyan,
client: Color::DarkMagenta,
connection: Color::DarkCyan,
separator: Color::DarkGreen,
dimmed: Color::Grey,
normal: Color::Black,
success: Color::DarkGreen,
failure: Color::DarkRed,
ask: Color::DarkYellow,
}
}
pub fn neutral() -> Self {
Self {
error: Color::Red,
warning: Color::DarkYellow,
info: Color::AnsiValue(32),
debug: Color::DarkCyan,
trace: Color::Grey,
reasoning: Color::Magenta,
user: Color::DarkGreen,
server: Color::DarkCyan,
client: Color::DarkMagenta,
connection: Color::DarkCyan,
separator: Color::DarkGreen,
dimmed: Color::Grey,
normal: Color::Reset, success: Color::DarkGreen,
failure: Color::Red,
ask: Color::DarkYellow,
}
}
pub fn from_theme(theme: Theme) -> Self {
match theme {
Theme::Dark => Self::dark(),
Theme::Light => Self::light(),
Theme::Neutral => Self::neutral(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn detect_theme() -> Option<Theme> {
use std::panic::catch_unwind;
if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
if term_program == "Apple_Terminal" {
debug!("Skipping theme detection on Apple Terminal (known issues)");
return theme_from_colorfgbg();
}
}
let timeout = Duration::from_millis(100);
let result = catch_unwind(|| termbg::theme(timeout));
flush_stdin_nonblocking();
match result {
Ok(Ok(termbg::Theme::Light)) => Some(Theme::Light),
Ok(Ok(termbg::Theme::Dark)) => Some(Theme::Dark),
Ok(Err(e)) => {
debug!("Theme detection failed: {:?}", e);
theme_from_colorfgbg()
}
Err(_) => {
debug!("Theme detection panicked");
theme_from_colorfgbg()
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn theme_from_colorfgbg() -> Option<Theme> {
let value = std::env::var("COLORFGBG").ok()?;
let bg = value.rsplit(';').next()?.trim().parse::<u8>().ok()?;
let theme = if bg == 7 || bg == 15 {
Theme::Light
} else {
Theme::Dark
};
debug!("Theme from COLORFGBG={value}: {theme:?}");
Some(theme)
}
#[cfg(not(target_arch = "wasm32"))]
fn flush_stdin_nonblocking() {
use std::io::Read;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let stdin = std::io::stdin();
let fd = stdin.as_raw_fd();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 {
return;
}
if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
return;
}
let mut buf = [0u8; 256];
let mut stdin_lock = stdin.lock();
while stdin_lock.read(&mut buf).unwrap_or(0) > 0 {}
unsafe { libc::fcntl(fd, libc::F_SETFL, flags) };
}
#[cfg(not(unix))]
{}
}
#[cfg(target_arch = "wasm32")]
pub fn detect_theme() -> Option<Theme> {
None
}
pub fn parse_theme(s: &str) -> anyhow::Result<Option<Theme>> {
match s.to_lowercase().as_str() {
"auto" => Ok(None), "light" => Ok(Some(Theme::Light)),
"dark" => Ok(Some(Theme::Dark)),
"neutral" => Ok(Some(Theme::Neutral)),
_ => anyhow::bail!(
"Invalid theme '{}'. Valid options: auto, light, dark, neutral",
s
),
}
}