diff options
Diffstat (limited to 'src/term.rs')
| -rw-r--r-- | src/term.rs | 216 |
1 files changed, 125 insertions, 91 deletions
diff --git a/src/term.rs b/src/term.rs index bf06538..6b892bf 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,6 +1,6 @@ use libc::{termios, winsize}; use std::error::Error; -use std::io::{stdout, BufRead, BufReader, Read, Stdin, Write}; +use std::io::{stdout, BufRead, BufReader, Stdin, Write}; const ESC: u8 = 27; @@ -47,6 +47,32 @@ pub fn raw_mode() -> Result<RawHandle, Box<dyn Error>> { } } +fn parse_status_report(buf: &[u8]) -> Result<(u16, u16), Box<dyn Error>> { + 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::<u16>()?; + let col = std::str::from_utf8(&buf[semicolon + 1..r])?.parse::<u16>()?; + Ok((row - 1, col - 1)) +} + pub enum Key { Timeout, Up, @@ -60,108 +86,116 @@ pub enum Key { Byte(u8), } -pub fn read_key(stdin: &mut BufReader<Stdin>) -> Result<Key, Box<dyn Error>> { - loop { - let mut buf = vec![0; 1]; - let n = stdin.read(&mut buf)?; - if n == 0 { - return Ok(Key::Timeout); - } - if buf[0] != ESC { - return Ok(Key::Byte(buf[0])); - } - let buf = stdin.fill_buf()?; - if buf.is_empty() { - continue; - } - if buf[0] != b'[' || buf.len() == 1 { - stdin.consume(1); - 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 = 1; - while let b'0'..=b'9' | b';' = buf[n] { - n += 1; - if n == buf.len() - 1 { - break; - } - } +pub struct Reader { + stdin: BufReader<Stdin>, + // expected_status_reports counts the number of unread status reports expected to come in + // on stdin. + expected_status_reports: i64, +} - // Skip the terminating character. - n += 1; +impl Reader { + pub fn new(stdin: Stdin) -> Reader { + Reader { + stdin: BufReader::new(stdin), + expected_status_reports: 0, + } + } - let seq = buf[1..n].to_vec(); - stdin.consume(n); - let Ok(s) = String::from_utf8(seq) else { - continue; - }; - match s.as_str() { - "A" => { - return Ok(Key::Up); + pub fn read_key(&mut self) -> Result<Key, Box<dyn Error>> { + 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); } - "B" => { - return Ok(Key::Down); + if buf[0] != ESC { + let b = buf[0]; + self.stdin.consume(1); + return Ok(Key::Byte(b)); } - "C" => { - return Ok(Key::Right); - } - "D" => { - return Ok(Key::Left); - } - "H" | "1~" => { - return Ok(Key::Home); - } - "F" | "8~" => { - return Ok(Key::End); + if buf.len() < 3 || buf[1] != b'[' { + // Unknown escape sequence, eat the escape and one more byte. + self.stdin.consume(2); + continue; } - "5~" => { - return Ok(Key::PgUp); + // 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; + } } - "6~" => { - return Ok(Key::PgDn); + + // 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" | "8~" => { + return Ok(Key::End); + } + "5~" => { + return Ok(Key::PgUp); + } + "6~" => { + return Ok(Key::PgDn); + } + _ => { + continue; + } } } } -} -// cursor_pos *WILL* fail if the user is typing on the keyboard. Be sure to handle -// errors appropriately. -pub fn cursor_pos(stdin: &mut BufReader<Stdin>) -> Result<(u16, u16), Box<dyn Error>> { - if !stdin.buffer().is_empty() { - // 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. - return Err(Box::from("interrupted")); - } - write!(stdout(), "\x1b[6n")?; - stdout().flush()?; - let buf = stdin.fill_buf()?; - let mut semicolon = buf.len(); - let mut r = buf.len(); - for (i, &c) in buf.iter().enumerate() { - if c == b';' { - semicolon = i; + // 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<dyn Error>> { + 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")); } - 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() - { - return Err(Box::from("invalid response")); + 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)) } - let row = std::str::from_utf8(&buf[2..semicolon])?.parse::<u16>()?; - let col = std::str::from_utf8(&buf[semicolon + 1..r])?.parse::<u16>()?; - stdin.consume(r + 1); - Ok((row - 1, col - 1)) } |
