aboutsummaryrefslogtreecommitdiffstats
path: root/src/term.rs
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2024-01-12 08:31:12 -0800
committerRose Hogenson <rosehogenson@posteo.net>2024-01-12 08:31:12 -0800
commitd333343b15aab5747ccf74c495cb085b4e32b029 (patch)
tree0c523f40929f44cd2f633d9bfe989f9305f29f41 /src/term.rs
parente23b1c7254aa21c0eb2fd18f720df262725c6d81 (diff)
downloadeditor-d333343b15aab5747ccf74c495cb085b4e32b029.tar.zst
Fix a bug where cursor_pos would return old data.
For now I'm just counting the failures, and aborting if cursor_pos immediately failed before. We have to skip the incoming status reports with read_key before cursor_pos will start working again. This should flush any old status reports, and prevent reading them incorrectly.
Diffstat (limited to 'src/term.rs')
-rw-r--r--src/term.rs216
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))
}