use libc::{termios, winsize}; use std::error::Error; use std::io::{stdout, BufRead, BufReader, Read, Stdin, Write}; const ESC: u8 = 27; pub fn size() -> Result> { unsafe { let mut size = std::mem::zeroed(); if libc::ioctl(1, libc::TIOCGWINSZ, &mut size) < 0 { return Err(Box::from("ioctl failed")); } Ok(size) } } pub struct RawHandle { old_attr: termios, } impl Drop for RawHandle { fn drop(&mut self) { unsafe { libc::tcsetattr(1, libc::TCSANOW, &self.old_attr); } let _ = write!(stdout(), "\x1b[?7h\x1b[2J"); let _ = stdout().flush(); } } pub fn raw_mode() -> Result> { unsafe { let mut attr = std::mem::zeroed(); if libc::tcgetattr(1, &mut attr) < 0 { return Err(Box::from("tcgetattr failed")); } let mut raw = std::mem::zeroed(); libc::cfmakeraw(&mut raw); raw.c_cc[libc::VTIME] = 1; raw.c_cc[libc::VMIN] = 0; if libc::tcsetattr(1, libc::TCSANOW, &raw) < 0 { return Err(Box::from("tcsetattr failed")); } write!(stdout(), "\x1b[?7l")?; stdout().flush()?; Ok(RawHandle { old_attr: attr }) } } fn parse_status_report(buf: &[u8]) -> Result<(u16, u16), Box> { let mut semicolon = buf.len(); let mut r = buf.len(); for (i, &c) in buf.iter().enumerate() { if c == b';' { semicolon = i; } if c == b'R' { r = i; break; } } if buf.len() < 6 || buf[0] != ESC || buf[1] != b'[' || semicolon < 2 || semicolon + 1 > r || r != buf.len() - 1 { return Err(Box::from("invalid response")); } let row = std::str::from_utf8(&buf[2..semicolon])?.parse::()?; let col = std::str::from_utf8(&buf[semicolon + 1..r])?.parse::()?; Ok((row - 1, col - 1)) } pub enum Key { Timeout, Up, Down, Left, Right, Home, End, PgDn, PgUp, Char(char), } pub struct Reader { stdin: BufReader, // expected_status_reports counts the number of unread status reports expected to come in // on stdin. expected_status_reports: i64, } impl Reader { pub fn new(stdin: Stdin) -> Reader { Reader { stdin: BufReader::new(stdin), expected_status_reports: 0, } } pub fn read_key(&mut self) -> Result> { loop { let buf = self.stdin.fill_buf()?; if buf.is_empty() { // No buffered input, so let's give up on any status reports we were expecting. self.expected_status_reports = 0; return Ok(Key::Timeout); } if buf[0] != ESC { let mut n = usize::try_from(buf[0].leading_ones()) .expect("buf[0] is a u8 so it can only have 8 leading ones"); if n == 0 { n = 1; } let mut char = [0; 4]; self.stdin.read_exact(&mut char[..n])?; let Ok(s) = std::str::from_utf8(&char) else { continue; }; return Ok(Key::Char( s.chars() .nth(0) .expect("s should be exactly one codepoint long"), )); } if buf.len() < 3 || buf[1] != b'[' { // Unknown escape sequence, eat the escape and one more byte. self.stdin.consume(2); continue; } // An escape sequence usually starts with [, then has one or two numbers separated by // semicolon, and ends with some terminating character. To try and munch the whole // sequence, skip over any numbers and semicolon here. let mut n = 2; while let b'0'..=b'9' | b';' = buf[n] { n += 1; if n == buf.len() - 1 { break; } } // Skip the terminating character. n += 1; let seq = buf[..n].to_vec(); self.stdin.consume(n); if seq[seq.len() - 1] == b'R' && parse_status_report(&seq).is_ok() { if self.expected_status_reports > 0 { self.expected_status_reports -= 1; } continue; } let Ok(s) = String::from_utf8(seq) else { continue; }; match &s[2..] { "A" => { return Ok(Key::Up); } "B" => { return Ok(Key::Down); } "C" => { return Ok(Key::Right); } "D" => { return Ok(Key::Left); } "H" | "1~" => { return Ok(Key::Home); } "F" | "4~" | "8~" => { return Ok(Key::End); } "5~" => { return Ok(Key::PgUp); } "6~" => { return Ok(Key::PgDn); } "3~" => { // Backspace return Ok(Key::Char(char::from(b'H' - b'@'))); } _ => { continue; } } } } // cursor_pos *WILL* fail if the user is typing on the keyboard. Be sure to handle // errors appropriately. pub fn cursor_pos(&mut self) -> Result<(u16, u16), Box> { if !self.stdin.buffer().is_empty() || self.expected_status_reports > 0 { // If there is any buffered input, the below call to fill_buf will just return the // buffered data and not actually read the terminal response. Also abort if we are // expecting any status reports from previous failed calls to cursor_pos, since we // would read an old response and return an incorrect answer. return Err(Box::from("interrupted")); } write!(stdout(), "\x1b[6n")?; self.expected_status_reports += 1; stdout().flush()?; let buf = self.stdin.fill_buf()?; let (row, col) = parse_status_report(buf)?; let n = buf.len(); self.stdin.consume(n); self.expected_status_reports -= 1; Ok((row, col)) } }