1use crossterm::terminal;
2use std::env;
3use std::io::IsTerminal;
4use std::io::{self, Write};
5use std::time::{Duration, Instant};
6use thiserror::Error;
7use tokio::runtime::Runtime;
8#[cfg(target_os = "windows")]
9use winapi::um::wincon;
10
11mod stdin;
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub enum Terminal {
16 Screen,
17 Tmux,
18 XtermCompatible,
19 Windows,
20 Emacs,
21}
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25pub struct Rgb {
26 pub r: u16,
27 pub g: u16,
28 pub b: u16,
29}
30
31#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub enum Theme {
34 Light,
35 Dark,
36}
37
38#[derive(Error, Debug)]
40pub enum Error {
41 #[error("io error")]
42 Io {
43 #[from]
44 source: io::Error,
45 },
46 #[error("parse error")]
47 Parse(String),
48 #[error("unsupported")]
49 Unsupported,
50 #[error("timeout")]
51 Timeout,
52}
53
54#[cfg(not(target_os = "windows"))]
56pub fn terminal() -> Terminal {
57 if env::var("INSIDE_EMACS").is_ok() {
58 return Terminal::Emacs;
59 }
60
61 if env::var("TMUX").is_ok() || env::var("TERM").is_ok_and(|x| x.starts_with("tmux-")) {
62 Terminal::Tmux
63 } else {
64 let is_screen = if let Ok(term) = env::var("TERM") {
65 term.starts_with("screen")
66 } else {
67 false
68 };
69 if is_screen {
70 Terminal::Screen
71 } else {
72 Terminal::XtermCompatible
73 }
74 }
75}
76
77#[cfg(target_os = "windows")]
79pub fn terminal() -> Terminal {
80 if let Ok(term_program) = env::var("TERM_PROGRAM") {
81 if term_program == "vscode" {
82 return Terminal::XtermCompatible;
83 }
84 }
85
86 if env::var("INSIDE_EMACS").is_ok() {
87 return Terminal::Emacs;
88 }
89
90 if env::var("WT_SESSION").is_ok() {
93 Terminal::XtermCompatible
94 } else {
95 Terminal::Windows
96 }
97}
98
99#[cfg(not(target_os = "windows"))]
101pub fn rgb(timeout: Duration) -> Result<Rgb, Error> {
102 let term = terminal();
103 let rgb = match term {
104 Terminal::Emacs => Err(Error::Unsupported),
105 _ => from_xterm(term, timeout),
106 };
107 let fallback = from_env_colorfgbg();
108 if rgb.is_ok() {
109 rgb
110 } else if fallback.is_ok() {
111 fallback
112 } else {
113 rgb
114 }
115}
116
117#[cfg(target_os = "windows")]
119pub fn rgb(timeout: Duration) -> Result<Rgb, Error> {
120 let term = terminal();
121 let rgb = match term {
122 Terminal::Emacs => Err(Error::Unsupported),
123 Terminal::XtermCompatible => from_xterm(term, timeout),
124 _ => from_winapi(),
125 };
126 let fallback = from_env_colorfgbg();
127 if rgb.is_ok() {
128 rgb
129 } else if fallback.is_ok() {
130 fallback
131 } else {
132 rgb
133 }
134}
135
136#[cfg(not(target_os = "windows"))]
138pub fn latency(timeout: Duration) -> Result<Duration, Error> {
139 let term = terminal();
140 match term {
141 Terminal::Emacs => Ok(Duration::from_millis(0)),
142 _ => xterm_latency(timeout),
143 }
144}
145
146#[cfg(target_os = "windows")]
148pub fn latency(timeout: Duration) -> Result<Duration, Error> {
149 let term = terminal();
150 match term {
151 Terminal::Emacs => Ok(Duration::from_millis(0)),
152 Terminal::XtermCompatible => xterm_latency(timeout),
153 _ => Ok(Duration::from_millis(0)),
154 }
155}
156
157pub fn theme(timeout: Duration) -> Result<Theme, Error> {
159 let rgb = rgb(timeout)?;
160
161 let y = rgb.r as f64 * 0.299 + rgb.g as f64 * 0.587 + rgb.b as f64 * 0.114;
163
164 if y > 32768.0 {
165 Ok(Theme::Light)
166 } else {
167 Ok(Theme::Dark)
168 }
169}
170
171fn from_xterm(term: Terminal, timeout: Duration) -> Result<Rgb, Error> {
172 if !std::io::stdin().is_terminal()
173 || !std::io::stdout().is_terminal()
174 || !std::io::stderr().is_terminal()
175 {
176 return Err(Error::Unsupported);
178 }
179
180 let query = if term == Terminal::Tmux {
182 "\x1bPtmux;\x1b\x1b]11;?\x07\x1b\\\x03"
183 } else if term == Terminal::Screen {
184 "\x1bP\x1b]11;?\x07\x1b\\\x03"
185 } else {
186 "\x1b]11;?\x1b\\"
187 };
188
189 let mut stderr = io::stderr();
190 terminal::enable_raw_mode()?;
191 write!(stderr, "{}", query)?;
192 stderr.flush()?;
193
194 let rt = Runtime::new()?;
195 let buffer: Result<_, Error> = rt.block_on(async {
197 use tokio::io::AsyncReadExt;
198 use tokio::time;
199 let mut buffer = Vec::new();
200 let mut stdin = stdin::stdin()?;
201 let mut buf = [0; 1];
202 let mut start = false;
203 loop {
204 if let Err(_) = time::timeout(timeout, stdin.read_exact(&mut buf)).await {
205 return Err(Error::Timeout);
206 }
207 if start && (buf[0] == 0x7) {
209 break;
210 }
211 if start && (buf[0] == 0x1b) {
213 if let Err(_) = time::timeout(timeout, stdin.read_exact(&mut buf)).await {
215 return Err(Error::Timeout);
216 }
217 debug_assert_eq!(buf[0], 0x5c);
218 break;
219 }
220 if start {
221 buffer.push(buf[0]);
222 }
223 if buf[0] == b':' {
224 start = true;
225 }
226 }
227 Ok(buffer)
228 });
229
230 terminal::disable_raw_mode()?;
231
232 let buffer = buffer?;
234
235 let s = String::from_utf8_lossy(&buffer);
236 let (r, g, b) = decode_x11_color(&*s)?;
237 Ok(Rgb { r, g, b })
238}
239
240fn from_env_colorfgbg() -> Result<Rgb, Error> {
241 let var = env::var("COLORFGBG").map_err(|_| Error::Unsupported)?;
242 let fgbg: Vec<_> = var.split(";").collect();
243 let bg = fgbg.get(1).ok_or(Error::Unsupported)?;
244 let bg = u8::from_str_radix(bg, 10).map_err(|_| Error::Parse(String::from(var)))?;
245
246 let (r, g, b) = match bg {
248 0 => (0, 0, 0),
250 1 => (205, 0, 0),
252 2 => (0, 205, 0),
254 3 => (205, 205, 0),
256 4 => (0, 0, 238),
258 5 => (205, 0, 205),
260
261 6 => (0, 205, 205),
263 7 => (229, 229, 229),
265 8 => (127, 127, 127),
267 9 => (255, 0, 0),
269 10 => (0, 255, 0),
271 11 => (255, 255, 0),
273 12 => (92, 92, 255),
275 13 => (255, 0, 255),
277 14 => (0, 255, 255),
279
280 15 => (255, 255, 255),
282 _ => (0, 0, 0),
283 };
284
285 Ok(Rgb {
286 r: r * 256,
287 g: g * 256,
288 b: b * 256,
289 })
290}
291
292fn xterm_latency(timeout: Duration) -> Result<Duration, Error> {
293 let query = "\x1b[5n";
295
296 let mut stderr = io::stderr();
297 terminal::enable_raw_mode()?;
298 write!(stderr, "{}", query)?;
299 stderr.flush()?;
300
301 let start = Instant::now();
302
303 let rt = Runtime::new()?;
304 let ret: Result<_, Error> = rt.block_on(async {
306 use tokio::io::AsyncReadExt;
307 use tokio::time;
308 let mut stdin = stdin::stdin()?;
309 let mut buf = [0; 1];
310 loop {
311 if let Err(_) = time::timeout(timeout, stdin.read_exact(&mut buf)).await {
312 return Err(Error::Timeout);
313 }
314 if buf[0] == b'n' {
316 break;
317 }
318 }
319 Ok(())
320 });
321
322 let end = start.elapsed();
323
324 terminal::disable_raw_mode()?;
325
326 let _ = ret?;
327
328 Ok(end)
329}
330
331fn decode_x11_color(s: &str) -> Result<(u16, u16, u16), Error> {
332 fn decode_hex(s: &str) -> Result<u16, Error> {
333 let len = s.len() as u32;
334 let mut ret = u16::from_str_radix(s, 16).map_err(|_| Error::Parse(String::from(s)))?;
335 ret = ret << ((4 - len) * 4);
336 Ok(ret)
337 }
338
339 let rgb: Vec<_> = s.split("/").collect();
340
341 let r = rgb.get(0).ok_or_else(|| Error::Parse(String::from(s)))?;
342 let g = rgb.get(1).ok_or_else(|| Error::Parse(String::from(s)))?;
343 let b = rgb.get(2).ok_or_else(|| Error::Parse(String::from(s)))?;
344 let r = decode_hex(r)?;
345 let g = decode_hex(g)?;
346 let b = decode_hex(b)?;
347
348 Ok((r, g, b))
349}
350
351#[cfg(target_os = "windows")]
352fn from_winapi() -> Result<Rgb, Error> {
353 let info = unsafe {
354 let handle = winapi::um::processenv::GetStdHandle(winapi::um::winbase::STD_OUTPUT_HANDLE);
355 let mut info: wincon::CONSOLE_SCREEN_BUFFER_INFO = Default::default();
356 wincon::GetConsoleScreenBufferInfo(handle, &mut info);
357 info
358 };
359
360 let r = (wincon::BACKGROUND_RED & info.wAttributes) != 0;
361 let g = (wincon::BACKGROUND_GREEN & info.wAttributes) != 0;
362 let b = (wincon::BACKGROUND_BLUE & info.wAttributes) != 0;
363 let i = (wincon::BACKGROUND_INTENSITY & info.wAttributes) != 0;
364
365 let r: u8 = r as u8;
366 let g: u8 = g as u8;
367 let b: u8 = b as u8;
368 let i: u8 = i as u8;
369
370 let (r, g, b) = match (r, g, b, i) {
371 (0, 0, 0, 0) => (0, 0, 0),
372 (1, 0, 0, 0) => (128, 0, 0),
373 (0, 1, 0, 0) => (0, 128, 0),
374 (1, 1, 0, 0) => (128, 128, 0),
375 (0, 0, 1, 0) => (0, 0, 128),
376 (1, 0, 1, 0) => (128, 0, 128),
377 (0, 1, 1, 0) => (0, 128, 128),
378 (1, 1, 1, 0) => (192, 192, 192),
379 (0, 0, 0, 1) => (128, 128, 128),
380 (1, 0, 0, 1) => (255, 0, 0),
381 (0, 1, 0, 1) => (0, 255, 0),
382 (1, 1, 0, 1) => (255, 255, 0),
383 (0, 0, 1, 1) => (0, 0, 255),
384 (1, 0, 1, 1) => (255, 0, 255),
385 (0, 1, 1, 1) => (0, 255, 255),
386 (1, 1, 1, 1) => (255, 255, 255),
387 _ => unreachable!(),
388 };
389
390 Ok(Rgb {
391 r: r * 256,
392 g: g * 256,
393 b: b * 256,
394 })
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_decode_x11_color() {
403 let s = "0000/0000/0000";
404 assert_eq!((0, 0, 0), decode_x11_color(s).unwrap());
405
406 let s = "1111/2222/3333";
407 assert_eq!((0x1111, 0x2222, 0x3333), decode_x11_color(s).unwrap());
408
409 let s = "111/222/333";
410 assert_eq!((0x1110, 0x2220, 0x3330), decode_x11_color(s).unwrap());
411
412 let s = "11/22/33";
413 assert_eq!((0x1100, 0x2200, 0x3300), decode_x11_color(s).unwrap());
414
415 let s = "1/2/3";
416 assert_eq!((0x1000, 0x2000, 0x3000), decode_x11_color(s).unwrap());
417 }
418}