use crate::term::theme_notify::{OscColorKind, parse_color_scheme_report, parse_osc_color_reply};
use crate::theme::Appearance;
use crate::ui::key::Key;
#[derive(Clone, PartialEq, Debug)]
pub enum TapEvent {
Key(Key),
ThemeNotification(Appearance),
OscColor(OscColorKind, xterm_color::Color),
}
const MAX_ESCAPE_LEN: usize = 128;
pub struct TapScanner {
buf: Vec<u8>,
}
impl TapScanner {
pub fn new() -> TapScanner {
TapScanner { buf: Vec::new() }
}
pub fn feed(&mut self, chunk: &[u8]) -> Vec<TapEvent> {
let mut events = Vec::new();
for &byte in chunk {
if self.buf.is_empty() {
if byte == 0x1b {
self.buf.push(byte);
} else if let Some(key) = decode_key(byte) {
events.push(TapEvent::Key(key));
}
continue;
}
self.buf.push(byte);
if self.buf.len() == 2 {
if !matches!(self.buf[1], b'[' | b']') {
self.buf.clear();
if let Some(key) = decode_key(byte) {
events.push(TapEvent::Key(key));
}
}
continue;
}
if self.buf.len() > MAX_ESCAPE_LEN {
self.buf.clear();
continue;
}
if self.buf[1] == b'[' {
if (0x40..=0x7e).contains(&byte) {
if let Some(appearance) = parse_color_scheme_report(&self.buf) {
events.push(TapEvent::ThemeNotification(appearance));
}
self.buf.clear();
}
} else {
debug_assert_eq!(self.buf[1], b']');
if self.buf.ends_with(b"\x07") || self.buf.ends_with(b"\x1b\\") {
if let Some((kind, color)) = parse_osc_color_reply(&self.buf) {
events.push(TapEvent::OscColor(kind, color));
}
self.buf.clear();
}
}
}
events
}
}
impl Default for TapScanner {
fn default() -> Self {
TapScanner::new()
}
}
pub fn decode_key(byte: u8) -> Option<Key> {
match byte {
0x03 => Some(Key::CtrlC),
b'\r' | b'\n' => Some(Key::Enter),
0x20..=0x7e => Some(Key::Char(byte as char)),
_ => None,
}
}
#[cfg(unix)]
const READ_SLICE: std::time::Duration = std::time::Duration::from_millis(50);
#[cfg(unix)]
const PARK_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(150);
#[cfg(unix)]
#[derive(Default)]
struct TapControl {
pause: std::sync::atomic::AtomicBool,
parked: std::sync::atomic::AtomicBool,
shutdown: std::sync::atomic::AtomicBool,
}
#[cfg(unix)]
pub struct TtyTap {
rx: std::sync::mpsc::Receiver<Vec<u8>>,
control: std::sync::Arc<TapControl>,
reader: Option<std::thread::JoinHandle<()>>,
}
#[cfg(unix)]
impl TtyTap {
pub fn spawn() -> std::io::Result<TtyTap> {
let tty = std::fs::File::open("/dev/tty")?;
let (tx, rx) = std::sync::mpsc::channel();
let control = std::sync::Arc::new(TapControl::default());
let reader_control = std::sync::Arc::clone(&control);
let reader = std::thread::Builder::new()
.name("rat-tty-tap".to_string())
.spawn(move || read_loop(&tty, &tx, &reader_control))?;
Ok(TtyTap {
rx,
control,
reader: Some(reader),
})
}
pub fn recv_timeout(&self, timeout: std::time::Duration) -> Option<Vec<u8>> {
use std::sync::mpsc::RecvTimeoutError;
match self.rx.recv_timeout(timeout) {
Ok(chunk) => Some(chunk),
Err(RecvTimeoutError::Timeout) => None,
Err(RecvTimeoutError::Disconnected) => {
std::thread::sleep(timeout);
None
}
}
}
pub fn pause(&self) -> bool {
use std::sync::atomic::Ordering;
self.control.pause.store(true, Ordering::SeqCst);
let deadline = std::time::Instant::now() + PARK_ACK_TIMEOUT;
loop {
if self.control.parked.load(Ordering::SeqCst) {
return true;
}
if self
.reader
.as_ref()
.is_none_or(|reader| reader.is_finished())
{
return true;
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
}
pub fn resume(&self) {
use std::sync::atomic::Ordering;
self.control.parked.store(false, Ordering::SeqCst);
self.control.pause.store(false, Ordering::SeqCst);
}
}
#[cfg(unix)]
impl Drop for TtyTap {
fn drop(&mut self) {
use std::sync::atomic::Ordering;
self.control.shutdown.store(true, Ordering::SeqCst);
self.control.pause.store(false, Ordering::SeqCst);
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
}
}
#[cfg(unix)]
fn read_loop(tty: &std::fs::File, tx: &std::sync::mpsc::Sender<Vec<u8>>, control: &TapControl) {
use std::os::unix::io::AsRawFd;
use std::sync::atomic::Ordering;
let fd = tty.as_raw_fd();
let mut buf = [0u8; 256];
loop {
if control.shutdown.load(Ordering::SeqCst) {
return;
}
if control.pause.load(Ordering::SeqCst) {
control.parked.store(true, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(2));
continue;
}
let mut read_set: libc::fd_set = unsafe { std::mem::zeroed() };
unsafe {
libc::FD_ZERO(&mut read_set);
libc::FD_SET(fd, &mut read_set);
}
let mut timeout = libc::timeval {
tv_sec: 0,
tv_usec: READ_SLICE.subsec_micros() as libc::suseconds_t,
};
let ready = unsafe {
libc::select(
fd + 1,
&mut read_set,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut timeout,
)
};
if ready < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return;
}
if ready == 0 {
continue;
}
if control.pause.load(Ordering::SeqCst) {
continue;
}
let read = unsafe { libc::read(fd, buf.as_mut_ptr().cast::<libc::c_void>(), buf.len()) };
if read <= 0 {
return; }
if tx.send(buf[..read as usize].to_vec()).is_err() {
return; }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_split_report_reassembles_across_feeds() {
let mut scanner = TapScanner::new();
assert_eq!(scanner.feed(b"\x1b[?997"), vec![]);
assert_eq!(
scanner.feed(b";2n"),
vec![TapEvent::ThemeNotification(Appearance::Light)]
);
}
#[test]
fn a_report_sandwiched_between_keys_yields_all_three_in_order() {
let mut scanner = TapScanner::new();
let events = scanner.feed(b"a\x1b[?997;2nb");
assert_eq!(
events,
vec![
TapEvent::Key(Key::Char('a')),
TapEvent::ThemeNotification(Appearance::Light),
TapEvent::Key(Key::Char('b')),
]
);
}
#[test]
fn an_unrecognized_private_csi_is_dropped_without_wedging() {
let mut scanner = TapScanner::new();
assert_eq!(scanner.feed(b"\x1b[?123;4x"), vec![]);
assert_eq!(scanner.feed(b"z"), vec![TapEvent::Key(Key::Char('z'))]);
}
#[test]
fn an_unfinished_sequence_past_the_cap_is_discarded_wholesale() {
let mut scanner = TapScanner::new();
let mut long_run = b"\x1b[".to_vec();
long_run.resize(long_run.len() + 200, 0u8);
assert_eq!(scanner.feed(&long_run), vec![]);
assert_eq!(scanner.feed(b"z"), vec![TapEvent::Key(Key::Char('z'))]);
}
#[test]
fn a_complete_arrow_key_is_dropped_silently() {
let mut scanner = TapScanner::new();
assert_eq!(scanner.feed(b"\x1b[A"), vec![]);
}
#[test]
fn an_osc_color_reply_reassembles_across_feeds() {
let mut scanner = TapScanner::new();
assert_eq!(scanner.feed(b"\x1b]11;rgb:1e1e/1e1e/"), vec![]);
assert_eq!(
scanner.feed(b"2e2e\x07"),
vec![TapEvent::OscColor(
OscColorKind::Background,
xterm_color::Color::rgb(0x1e1e, 0x1e1e, 0x2e2e)
)]
);
}
#[test]
fn a_lone_escape_with_no_recognized_introducer_does_not_eat_the_next_byte() {
let mut scanner = TapScanner::new();
assert_eq!(scanner.feed(b"\x1b"), vec![]);
assert_eq!(scanner.feed(b"q"), vec![TapEvent::Key(Key::Char('q'))]);
}
#[test]
fn decode_key_maps_the_five_recognized_bytes() {
assert_eq!(decode_key(0x03), Some(Key::CtrlC));
assert_eq!(decode_key(b'\r'), Some(Key::Enter));
assert_eq!(decode_key(b'\n'), Some(Key::Enter));
assert_eq!(decode_key(b'q'), Some(Key::Char('q')));
assert_eq!(decode_key(b'v'), Some(Key::Char('v')));
}
#[test]
fn decode_key_has_no_verdict_for_escape_or_delete() {
assert_eq!(decode_key(0x1b), None);
assert_eq!(decode_key(0x7f), None);
}
}