use std::time::Duration;
pub fn detect_light(timeout: Duration) -> Option<bool> {
#[cfg(unix)]
{
detect_light_unix(timeout)
}
#[cfg(not(unix))]
{
let _ = timeout;
None
}
}
#[cfg(unix)]
fn detect_light_unix(timeout: Duration) -> Option<bool> {
use std::io::Write;
use std::os::unix::io::AsRawFd;
let mut stdout = std::io::stdout();
stdout.write_all(b"\x1b]11;?\x07").ok()?;
stdout.flush().ok()?;
let stdin = std::io::stdin();
let fd = stdin.as_raw_fd();
let start = std::time::Instant::now();
let initial_deadline = start + timeout;
let extended_deadline = initial_deadline + Duration::from_millis(250);
let mut buf: Vec<u8> = Vec::with_capacity(64);
let mut saw_osc_start = false;
loop {
let deadline = if saw_osc_start { extended_deadline } else { initial_deadline };
let mut chunk = [0u8; 128];
let nread = unsafe { poll_read(fd, deadline, &mut chunk) };
if nread == 0 {
break;
}
buf.extend_from_slice(&chunk[..nread]);
if !saw_osc_start {
saw_osc_start = buf.windows(2).any(|w| w == b"\x1b]");
if !saw_osc_start {
break;
}
}
if has_osc_terminator(&buf) {
break;
}
}
if !has_osc_terminator(&buf) {
let tail_deadline = std::time::Instant::now() + Duration::from_millis(80);
let mut committed = saw_osc_start;
loop {
if committed {
let mut chunk = [0u8; 128];
let nread = unsafe { poll_read(fd, tail_deadline, &mut chunk) };
if nread == 0 {
break;
}
buf.extend_from_slice(&chunk[..nread]);
} else {
let mut probe = [0u8; 1];
let nread = unsafe { poll_read(fd, tail_deadline, &mut probe) };
if nread == 0 {
break;
}
if probe[0] != b'\x1b' {
buf.push(probe[0]);
break;
}
buf.push(probe[0]);
let mut probe2 = [0u8; 1];
let nread2 = unsafe { poll_read(fd, tail_deadline, &mut probe2) };
if nread2 == 0 {
break;
}
buf.push(probe2[0]);
if probe2[0] != b']' {
break;
}
committed = true;
}
if has_osc_terminator(&buf) {
break;
}
}
}
parse_osc11_response(&buf)
}
#[cfg(unix)]
fn has_osc_terminator(buf: &[u8]) -> bool {
buf.contains(&b'\x07') || buf.windows(2).any(|w| w == b"\x1b\\")
}
#[cfg(unix)]
unsafe fn poll_read(fd: i32, deadline: std::time::Instant, out: &mut [u8]) -> usize {
let now = std::time::Instant::now();
if now >= deadline {
return 0;
}
let ms = (deadline - now).as_millis().min(i32::MAX as u128) as i32;
let mut pollfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let n = libc::poll(&mut pollfd, 1, ms);
if n <= 0 {
return 0;
}
let nread = libc::read(fd, out.as_mut_ptr() as *mut libc::c_void, out.len());
if nread <= 0 {
0
} else {
nread as usize
}
}
pub(crate) fn parse_osc11_response(bytes: &[u8]) -> Option<bool> {
let needle = b"rgb:";
let rgb_pos = bytes.windows(needle.len()).position(|w| w == needle)?;
let after = std::str::from_utf8(&bytes[rgb_pos + needle.len()..]).ok()?;
let mut parts = after.split('/');
let r_raw = parts.next()?;
let g_raw = parts.next()?;
let b_raw = parts.next()?;
let r = parse_hex_component(r_raw)?;
let g = parse_hex_component(g_raw)?;
let b = parse_hex_component(b_raw)?;
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
Some(lum > 128.0)
}
fn parse_hex_component(s: &str) -> Option<f64> {
let hex: String = s.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
if hex.is_empty() {
return None;
}
let val = u32::from_str_radix(&hex, 16).ok()?;
let max = (1u64 << (4 * hex.len())).saturating_sub(1) as u32;
if max == 0 {
return None;
}
Some((val as f64 * 255.0) / max as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_pure_white_as_light() {
let response = b"\x1b]11;rgb:ffff/ffff/ffff\x07";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn parses_pure_black_as_dark() {
let response = b"\x1b]11;rgb:0000/0000/0000\x07";
assert_eq!(parse_osc11_response(response), Some(false));
}
#[test]
fn parses_8bit_response() {
let response = b"\x1b]11;rgb:ff/ff/ff\x07";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn parses_vscode_dark_plus() {
let response = b"\x1b]11;rgb:1e1e/1e1e/1e1e\x07";
assert_eq!(parse_osc11_response(response), Some(false));
}
#[test]
fn parses_vscode_light_plus() {
let response = b"\x1b]11;rgb:ffff/ffff/ffff\x07";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn parses_st_terminator() {
let response = b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn tolerates_leading_garbage() {
let response = b"q\x1b]11;rgb:ffff/ffff/ffff\x07";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn rejects_no_rgb_prefix() {
assert_eq!(parse_osc11_response(b""), None);
assert_eq!(parse_osc11_response(b"random bytes"), None);
assert_eq!(parse_osc11_response(b"\x1b[A"), None); }
#[test]
fn rejects_truncated_response() {
assert_eq!(parse_osc11_response(b"\x1b]11;rgb:"), None);
assert_eq!(parse_osc11_response(b"\x1b]11;rgb:ff/"), None);
assert_eq!(parse_osc11_response(b"\x1b]11;rgb:ff/ff"), None);
}
#[test]
fn threshold_at_50_percent_grey_is_dark() {
let response = b"\x1b]11;rgb:8080/8080/8080\x07";
assert_eq!(parse_osc11_response(response), Some(false));
}
#[test]
fn threshold_one_above_50_percent_grey_is_light() {
let response = b"\x1b]11;rgb:8181/8181/8181\x07";
assert_eq!(parse_osc11_response(response), Some(true));
}
#[test]
fn luminance_weights_green_more_than_red_or_blue() {
let pure_green = b"\x1b]11;rgb:0000/ffff/0000\x07";
assert_eq!(parse_osc11_response(pure_green), Some(true));
let pure_red = b"\x1b]11;rgb:ffff/0000/0000\x07";
assert_eq!(parse_osc11_response(pure_red), Some(false));
let pure_blue = b"\x1b]11;rgb:0000/0000/ffff\x07";
assert_eq!(parse_osc11_response(pure_blue), Some(false));
}
}