diff options
| author | sigoden <sigoden@gmail.com> | 2023-10-29 17:22:23 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-29 17:22:23 +0800 |
| commit | 1ced15a2098f1469b2dd7d59636fdcdc710efc22 (patch) | |
| tree | 159b76ea877291741903f97f873e28bcb259a8e3 /src/utils/termbg.rs | |
| parent | 10b1d4b4de521a48747c73d29e9fe3f06db48a32 (diff) | |
| download | aichat-1ced15a2098f1469b2dd7d59636fdcdc710efc22.tar.gz | |
feat: autodetect light theme on unix-like os (#168)
Diffstat (limited to 'src/utils/termbg.rs')
| -rw-r--r-- | src/utils/termbg.rs | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/src/utils/termbg.rs b/src/utils/termbg.rs new file mode 100644 index 0000000..2322227 --- /dev/null +++ b/src/utils/termbg.rs @@ -0,0 +1,257 @@ +//! Terminal background color detection +/// Fork from https://github.com/dalance/termbg/blob/v0.4.3/src/lib.rs +use anyhow::{anyhow, Error, Result}; +use crossterm::terminal; +use std::env; +use std::io::{self, Read, Write}; +use std::time::Duration; + +/// Terminal +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Terminal { + Screen, + Tmux, + XtermCompatible, + VSCode, + Emacs, +} + +/// 16bit RGB color +#[derive(Copy, Clone, Debug)] +pub struct Rgb { + pub r: u16, + pub g: u16, + pub b: u16, +} + +/// Background theme +#[derive(Copy, Clone, Debug)] +pub enum Theme { + Light, + Dark, +} + +/// get detected termnial +pub fn terminal() -> Terminal { + if let Ok(term_program) = env::var("TERM_PROGRAM") { + if term_program == "vscode" { + return Terminal::VSCode; + } + } + + if env::var("INSIDE_EMACS").is_ok() { + return Terminal::Emacs; + } + + if env::var("TMUX").is_ok() { + Terminal::Tmux + } else { + let is_screen = if let Ok(term) = env::var("TERM") { + term.starts_with("screen") + } else { + false + }; + if is_screen { + Terminal::Screen + } else { + Terminal::XtermCompatible + } + } +} + +/// get background color by `RGB` +pub fn rgb(timeout: Duration) -> Result<Rgb> { + let term = terminal(); + let rgb = match term { + Terminal::VSCode => Err(unsupported_err()), + Terminal::Emacs => Err(unsupported_err()), + _ => from_xterm(term, timeout), + }; + check_rgb(rgb) +} + +/// get background color by `Theme` +pub fn theme(timeout: Duration) -> Result<Theme> { + let rgb = rgb(timeout)?; + + // ITU-R BT.601 + let y = rgb.r as f64 * 0.299 + rgb.g as f64 * 0.587 + rgb.b as f64 * 0.114; + + if y > 32768.0 { + Ok(Theme::Light) + } else { + Ok(Theme::Dark) + } +} + +fn from_xterm(term: Terminal, timeout: Duration) -> Result<Rgb> { + // Query by XTerm control sequence + let query = if term == Terminal::Tmux { + "\x1bPtmux;\x1b\x1b]11;?\x07\x1b\\\x03" + } else if term == Terminal::Screen { + "\x1bP\x1b]11;?\x07\x1b\\\x03" + } else { + "\x1b]11;?\x1b\\" + }; + + let mut stderr = io::stderr(); + terminal::enable_raw_mode()?; + write!(stderr, "{}", query)?; + stderr.flush()?; + + let mut stdin = io::stdin(); + + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + let mut buffer = Vec::new(); + let mut buf = [0; 1]; + let mut start = false; + loop { + let _ = stdin.read_exact(&mut buf); + // response terminated by BEL(0x7) + if start && (buf[0] == 0x7) { + break; + } + // response terminated by ST(0x1b 0x5c) + if start && (buf[0] == 0x1b) { + // consume last 0x5c + let _ = stdin.read_exact(&mut buf); + debug_assert_eq!(buf[0], 0x5c); + break; + } + if start { + buffer.push(buf[0]); + } + if buf[0] == b':' { + start = true; + } + } + // Ignore send error because timeout may be occured + let _ = tx.send(buffer); + }); + + let buffer = rx.recv_timeout(timeout); + + terminal::disable_raw_mode()?; + + let buffer = buffer?; + + let s = String::from_utf8_lossy(&buffer); + let (r, g, b) = decode_x11_color(&s)?; + Ok(Rgb { r, g, b }) +} + +fn decode_x11_color(s: &str) -> Result<(u16, u16, u16)> { + fn decode_hex(s: &str) -> Result<u16> { + let len = s.len() as u32; + let mut ret = u16::from_str_radix(s, 16).map_err(|_| parse_err(s))?; + ret <<= (4 - len) * 4; + Ok(ret) + } + + let rgb: Vec<_> = s.split('/').collect(); + + let r = rgb.first().ok_or_else(|| parse_err(s))?; + let g = rgb.get(1).ok_or_else(|| parse_err(s))?; + let b = rgb.get(2).ok_or_else(|| parse_err(s))?; + let r = decode_hex(r)?; + let g = decode_hex(g)?; + let b = decode_hex(b)?; + + Ok((r, g, b)) +} + +fn check_rgb(prev: Result<Rgb>) -> Result<Rgb> { + if prev.is_ok() { + return prev; + } + let fallback = from_env_colorfgbg(); + if fallback.is_ok() { + return fallback; + } + prev +} + +fn from_env_colorfgbg() -> Result<Rgb> { + let var = env::var("COLORFGBG").map_err(|_| unsupported_err())?; + let fgbg: Vec<_> = var.split(';').collect(); + let bg = fgbg.get(1).ok_or(unsupported_err())?; + let bg = bg.parse::<u8>().map_err(|_| parse_err(&var))?; + + // rxvt default color table + let (r, g, b) = match bg { + // black + 0 => (0, 0, 0), + // red + 1 => (205, 0, 0), + // green + 2 => (0, 205, 0), + // yellow + 3 => (205, 205, 0), + // blue + 4 => (0, 0, 238), + // magenta + 5 => (205, 0, 205), + + // cyan + 6 => (0, 205, 205), + // white + 7 => (229, 229, 229), + // bright black + 8 => (127, 127, 127), + // bright red + 9 => (255, 0, 0), + // bright green + 10 => (0, 255, 0), + // bright yellow + 11 => (255, 255, 0), + // bright blue + 12 => (92, 92, 255), + // bright magenta + 13 => (255, 0, 255), + // bright cyan + 14 => (0, 255, 255), + + // bright white + 15 => (255, 255, 255), + _ => (0, 0, 0), + }; + + Ok(Rgb { + r: r * 256, + g: g * 256, + b: b * 256, + }) +} + +fn unsupported_err() -> Error { + anyhow!("Unsupported terminal") +} + +fn parse_err(value: &str) -> Error { + anyhow!("Failed to parse {value}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_x11_color() { + let s = "0000/0000/0000"; + assert_eq!((0, 0, 0), decode_x11_color(s).unwrap()); + + let s = "1111/2222/3333"; + assert_eq!((0x1111, 0x2222, 0x3333), decode_x11_color(s).unwrap()); + + let s = "111/222/333"; + assert_eq!((0x1110, 0x2220, 0x3330), decode_x11_color(s).unwrap()); + + let s = "11/22/33"; + assert_eq!((0x1100, 0x2200, 0x3300), decode_x11_color(s).unwrap()); + + let s = "1/2/3"; + assert_eq!((0x1000, 0x2000, 0x3000), decode_x11_color(s).unwrap()); + } +} |
