pub const OSC11_QUERY_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120);
#[cfg_attr(not(unix), allow(dead_code))]
const OSC11_QUERY: &[u8] = b"\x1b]11;?\x1b\\";
#[cfg_attr(not(unix), allow(dead_code))]
#[must_use]
pub fn parse_osc11_reply(reply: &str) -> Option<(u8, u8, u8)> {
if let Some(idx) = reply.find("rgb:") {
return parse_slash_separated(&reply[idx + 4..]);
}
if let Some(idx) = reply.find('#') {
return parse_hash_hex(&reply[idx + 1..]);
}
None
}
#[cfg_attr(not(unix), allow(dead_code))]
fn parse_slash_separated(spec: &str) -> Option<(u8, u8, u8)> {
let spec: String = spec
.chars()
.take_while(|c| c.is_ascii_hexdigit() || *c == '/')
.collect();
let mut parts = spec.split('/');
let r = scale_hex_channel(parts.next()?)?;
let g = scale_hex_channel(parts.next()?)?;
let b = scale_hex_channel(parts.next()?)?;
if parts.next().is_some() {
return None;
}
Some((r, g, b))
}
#[cfg_attr(not(unix), allow(dead_code))]
fn parse_hash_hex(spec: &str) -> Option<(u8, u8, u8)> {
let digits: String = spec.chars().take_while(char::is_ascii_hexdigit).collect();
if !digits.len().is_multiple_of(3) || digits.is_empty() || digits.len() > 12 {
return None;
}
let width = digits.len() / 3;
let r = scale_hex_channel(&digits[..width])?;
let g = scale_hex_channel(&digits[width..width * 2])?;
let b = scale_hex_channel(&digits[width * 2..])?;
Some((r, g, b))
}
#[cfg_attr(not(unix), allow(dead_code))]
fn scale_hex_channel(digits: &str) -> Option<u8> {
if digits.is_empty() || digits.len() > 4 || !digits.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let value = u32::from_str_radix(digits, 16).ok()?;
let max = (1u32 << (4 * digits.len() as u32)) - 1;
Some(((value * 255 + max / 2) / max) as u8)
}
#[must_use]
pub fn query_terminal_background(timeout: std::time::Duration) -> Option<(u8, u8, u8)> {
query_impl(timeout)
}
#[cfg(unix)]
fn query_impl(timeout: std::time::Duration) -> Option<(u8, u8, u8)> {
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::time::Instant;
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let in_fd = stdin.as_raw_fd();
let out_fd = stdout.as_raw_fd();
let both_tty = unsafe { libc::isatty(in_fd) == 1 && libc::isatty(out_fd) == 1 };
if !both_tty {
return None;
}
{
let mut out = stdout.lock();
out.write_all(OSC11_QUERY).ok()?;
out.flush().ok()?;
}
let deadline = Instant::now() + timeout;
let mut reply = Vec::with_capacity(32);
let mut stdin = stdin.lock();
let mut byte = [0u8; 1];
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return None;
}
if !wait_readable(in_fd, remaining) {
return None;
}
match stdin.read(&mut byte) {
Ok(1) => {}
_ => return None,
}
if byte[0] == 0x07 || (byte[0] == 0x1b && !reply.is_empty()) {
break;
}
reply.push(byte[0]);
if reply.len() >= 128 {
return None;
}
}
parse_osc11_reply(&String::from_utf8_lossy(&reply))
}
#[cfg(unix)]
fn wait_readable(fd: std::os::fd::RawFd, timeout: std::time::Duration) -> bool {
let mut pollfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let millis = i32::try_from(timeout.as_millis())
.unwrap_or(i32::MAX)
.max(1);
let rc = unsafe { libc::poll(std::ptr::addr_of_mut!(pollfd), 1, millis) };
rc > 0 && (pollfd.revents & libc::POLLIN) != 0
}
#[cfg(not(unix))]
fn query_impl(_timeout: std::time::Duration) -> Option<(u8, u8, u8)> {
None
}