From 95c8e4763eddb2336d6ae7acdb752da94be43f5b Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sun, 7 Jan 2024 14:14:58 -0800 Subject: Support pasting text into the editor. The editor only repaints when there is a small delay in the input, which only happens when the user is typing. Otherwise the bytes go straight into the buffer. --- src/main.rs | 187 ++++++++++++++++++++++++++++++------------------------------ src/term.rs | 92 +++++++++++++++++++++++++----- 2 files changed, 171 insertions(+), 108 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index f0a38d8..8999771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,69 +4,53 @@ mod term; use rope::Rope; use std::error::Error; use std::ffi::{OsStr, OsString}; -use std::io::{stdin, stdout, Read, Write}; +use std::io::{stdout, BufReader, Stdin, Write}; use std::ops::{Deref, DerefMut}; use std::path::Path; use term::Key; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Mode { Insert, + Paste, Normal, } #[derive(Debug, Clone)] struct State { buf: Rope, - mode: Mode, row_start: usize, - cursor_row: usize, - cursor_col: usize, line_offset: usize, - line_cols: usize, + cursor_row: u16, + cursor_col: u16, + line_cols: u16, + mode: Mode, } -fn parse_status_report(buf: &[u8]) -> Result<(usize, usize), Box> { - let mut semicolon = 0; - for (i, &c) in buf.iter().enumerate() { - if c == b';' { - semicolon = i; - break; - } - } - if semicolon < 2 || semicolon == buf.len() - 1 { - return Err(Box::from("invalid response")); +#[derive(Debug)] +struct StateHistory { + stdin: BufReader, + + i: usize, + history: Vec, +} + +impl Deref for StateHistory { + type Target = State; + + fn deref(&self) -> &Self::Target { + return &self.history[self.i]; } - let row_str = std::str::from_utf8(&buf[2..semicolon])?; - let col_str = std::str::from_utf8(&buf[semicolon + 1..buf.len() - 1])?; - let row = row_str.parse::()?; - let col = col_str.parse::()?; - // Yuck... 1 indexing - return Ok((row - 1, col - 1)); } -fn cursor_pos() -> Result<(usize, usize), Box> { - write!(stdout(), "\x1b[6n") - .map_err(|err| format!("cursor position: status report: {}", err))?; - stdout() - .flush() - .map_err(|err| format!("cursor position: status report: {}", err))?; - let mut buf = vec![0; 20]; - let n = stdin() - .read(&mut buf) - .map_err(|err| format!("cursor position: read status report: {}", err))?; - buf.truncate(n); - let pos = parse_status_report(&buf).map_err(|_| { - format!( - "cursor position: invalid response: {}", - String::from_utf8_lossy(&buf) - ) - })?; - return Ok(pos); +impl DerefMut for StateHistory { + fn deref_mut(&mut self) -> &mut Self::Target { + return &mut self.history[self.i]; + } } -impl State { - fn repaint_all(&self) -> Result<(), Box> { +impl StateHistory { + fn repaint_all(&mut self) -> Result<(), Box> { let size = term::size()?; let mut stdout = stdout().lock(); @@ -79,9 +63,10 @@ impl State { break; } self.buf.line(i).print(&mut stdout)?; - let (_, col) = cursor_pos()?; - if col == usize::from(size.ws_col - 1) { - write!(stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", size.ws_col - 1)?; + if let Ok((_, col)) = term::cursor_pos(&mut self.stdin) { + if col == size.ws_col - 1 { + let _ = write!(stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", size.ws_col - 1); + } } } return Ok(()); @@ -92,13 +77,19 @@ impl State { let mut stdout = stdout().lock(); write!(stdout, "\x1b[{}H\x1b[K", self.cursor_row + 1)?; - let line = self.buf.line(self.row_start + self.cursor_row); + let line = self.buf.line(self.row_start + usize::from(self.cursor_row)); line.print(&mut stdout)?; - let (_, col) = cursor_pos()?; + let (_, col) = match term::cursor_pos(&mut self.stdin) { + Ok(x) => x, + Err(err) => { + let _ = write!(stdout, "\x1b[{}G", self.cursor_col + 1); + return Err(err); + } + }; self.line_cols = col; let mut truncated = false; - if col == usize::from(term_size.ws_col - 1) { + if col == term_size.ws_col - 1 { truncated = true; self.line_cols = col - 1; } @@ -122,7 +113,13 @@ impl State { let x = line.floor_char_boundary(lo + (hi - lo) / 2); write!(stdout, "\x1b[G")?; line.slice(0, x).print(&mut stdout)?; - let (_, col) = cursor_pos()?; + let (_, col) = match term::cursor_pos(&mut self.stdin) { + Ok(x) => x, + Err(err) => { + let _ = write!(stdout, "\x1b[{}G", self.cursor_col + 1); + return Err(err); + } + }; if col <= self.cursor_col { lo = line.ceil_char_boundary(x + 1); } else { @@ -158,11 +155,11 @@ impl State { } fn down(&mut self) -> Result<(), Box> { - if self.row_start + self.cursor_row == self.buf.lines() { + if self.row_start + usize::from(self.cursor_row) == self.buf.lines() { return Ok(()); } let size = term::size()?; - if self.cursor_row == usize::from(size.ws_row - 1) { + if self.cursor_row == size.ws_row - 1 { self.row_start += 1; self.repaint_all()?; } else { @@ -190,7 +187,8 @@ impl State { } fn right(&mut self) -> Result<(), Box> { - if self.cursor_col >= self.line_cols && self.row_start + self.cursor_row == self.buf.lines() + if self.cursor_col >= self.line_cols + && self.row_start + usize::from(self.cursor_row) == self.buf.lines() { return Ok(()); } @@ -203,49 +201,32 @@ impl State { self.repaint_line_full()?; return Ok(()); } -} - -#[derive(Debug)] -struct StateHistory { - i: usize, - history: Vec, -} -impl StateHistory { fn set_mode(&mut self, mode: Mode) { match (self.mode, mode) { - (Mode::Insert, Mode::Normal) => { - if let Ok((_, col)) = cursor_pos() { - self.cursor_col = col; - } - let _ = self.repaint_line_full(); - } - (Mode::Normal, Mode::Insert) => { + (Mode::Normal, Mode::Normal) => (), + (Mode::Normal, _) => { let curr = self.clone(); self.i += 1; self.history.truncate(self.i); self.history.push(curr); } + (Mode::Paste, Mode::Paste) => (), + (Mode::Paste, _) => { + let Ok((_, col)) = term::cursor_pos(&mut self.stdin) else { + return; + }; + self.cursor_col = col; + let _ = self.repaint_all(); + let _ = self.repaint_line_full(); + let _ = stdout().flush(); + } _ => (), } self.mode = mode; } } -impl Deref for StateHistory { - type Target = State; - - fn deref(&self) -> &Self::Target { - return &self.history[self.i]; - } -} - -impl DerefMut for StateHistory { - fn deref_mut(&mut self) -> &mut Self::Target { - return &mut self.history[self.i]; - } -} - fn edit(file: &OsStr) -> Result<(), Box> { let _raw_handle = term::raw().map_err(|err| format!("cannot put terminal in raw mode: {}", err))?; @@ -263,14 +244,21 @@ fn edit(file: &OsStr) -> Result<(), Box> { line_offset: 0, line_cols: 0, }], + stdin: BufReader::new(std::io::stdin()), }; let _ = state.repaint_all(); let _ = write!(stdout(), "\x1b[H"); let _ = stdout().flush(); loop { - let c = term::read_key().map_err(|err| format!("read key: {}", err))?; + let c = term::read_key(&mut state.stdin)?; match c { + Key::Timeout => { + if state.mode != Mode::Paste { + continue; + } + state.set_mode(Mode::Insert); + } Key::CtrlQ => { break; } @@ -323,7 +311,9 @@ fn edit(file: &OsStr) -> Result<(), Box> { if state.row_start == 0 && state.cursor_row == 0 && state.line_offset == 0 { continue; } - let line_start = state.buf.line_idx(state.row_start + state.cursor_row); + let line_start = state + .buf + .line_idx(state.row_start + usize::from(state.cursor_row)); let gap_end = line_start + state.line_offset; let gap_start = state.buf.floor_char_boundary(gap_end - 1); if state.line_offset == 0 { @@ -331,8 +321,14 @@ fn edit(file: &OsStr) -> Result<(), Box> { } else { let _ = write!(stdout(), "\x1b[G"); let _ = state.buf.slice(line_start, gap_start).print(&mut stdout()); - let (_, col) = cursor_pos()?; - state.cursor_col = col; + match term::cursor_pos(&mut state.stdin) { + Ok((_, col)) => { + state.cursor_col = col; + } + Err(_) => { + state.cursor_col -= 1; + } + } } state.buf = state .buf @@ -343,26 +339,31 @@ fn edit(file: &OsStr) -> Result<(), Box> { let _ = stdout().flush(); } Key::Char(b'\r') => { - state.set_mode(Mode::Insert); + state.set_mode(Mode::Paste); state.buf = state.buf.insert( - state.buf.line_idx(state.row_start + state.cursor_row) + state.line_offset, + state + .buf + .line_idx(state.row_start + usize::from(state.cursor_row)) + + state.line_offset, b'\n', ); - if state.cursor_row == usize::from(size.ws_row - 1) { + if state.cursor_row == size.ws_row - 1 { state.row_start += 1; } else { state.cursor_row += 1; } state.cursor_col = 0; - let _ = state.repaint_all(); - let _ = state.repaint_line_full(); + state.line_offset = 0; + let _ = write!(stdout(), "\r\n"); let _ = stdout().flush(); } Key::Char(c) => { - state.set_mode(Mode::Insert); - state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols); + state.set_mode(Mode::Paste); state.buf = state.buf.insert( - state.buf.line_idx(state.row_start + state.cursor_row) + state.line_offset, + state + .buf + .line_idx(state.row_start + usize::from(state.cursor_row)) + + state.line_offset, c, ); state.line_offset += 1; diff --git a/src/term.rs b/src/term.rs index 988786d..daef715 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,6 +1,13 @@ use libc::{termios, winsize}; use std::error::Error; -use std::io::{stdin, stdout, Read, Write}; +use std::io::{stdout, BufRead, Write}; + +const CTRL_Q: u8 = 17; +const CTRL_S: u8 = 19; +const CTRL_Y: u8 = 25; +const CTRL_Z: u8 = 26; +const BACKSPACE: u8 = 127; +const ESC: u8 = 27; pub fn size() -> Result> { unsafe { @@ -41,6 +48,8 @@ fn make_raw() -> termios { unsafe { let mut attr: termios = std::mem::zeroed(); libc::cfmakeraw(&mut attr); + attr.c_cc[libc::VMIN] = 0; // Return zero bytes on timeout. + attr.c_cc[libc::VTIME] = 1; // 100 ms timeout. return attr; } } @@ -51,7 +60,7 @@ pub struct RawHandle { impl Drop for RawHandle { fn drop(&mut self) { - let _ = write!(stdout(), "\x1b[?7h"); + let _ = write!(stdout(), "\x1b[?7h\x1b[H\x1b[J"); let _ = stdout().flush(); let _ = set_attr(&self.old); } @@ -76,19 +85,16 @@ pub enum Key { Right, Backspace, Char(u8), + Timeout, } -pub fn read_key() -> Result> { - const CTRL_Q: u8 = 17; - const CTRL_S: u8 = 19; - const CTRL_Y: u8 = 25; - const CTRL_Z: u8 = 26; - const BACKSPACE: u8 = 127; - const ESC: u8 = 27; - +pub fn read_key(stdin: &mut dyn BufRead) -> Result> { loop { let mut buf = vec![0; 1]; - stdin().read_exact(&mut buf)?; + let n = stdin.read(&mut buf)?; + if n == 0 { + return Ok(Key::Timeout); + } if buf[0] == CTRL_Q { return Ok(Key::CtrlQ); } @@ -108,13 +114,34 @@ pub fn read_key() -> Result> { return Ok(Key::Char(buf[0])); } // Try to handle an escape sequence. - let mut buf = vec![0; 2]; - stdin().read_exact(&mut buf)?; + let buf = stdin.fill_buf()?; + if buf.len() == 0 { + // Just an escape. Ignore it I guess. + continue; + } if buf[0] != b'[' { // Unknown escape sequence, just read another key. + stdin.consume(1); + continue; + } + let mut n = buf.len(); + for (i, &c) in buf.iter().enumerate() { + match c { + // Escape sequences usually end with a letter. + b'A'..=b'Z' | b'a'..=b'z' => { + n = i + 1; + break; + } + _ => (), + } + } + let mut escape_sequence = Vec::new(); + escape_sequence.extend_from_slice(&buf[1..n]); + stdin.consume(n); + if escape_sequence.len() < 1 { continue; } - match buf[1] { + match escape_sequence[0] { b'A' => { return Ok(Key::Up); } @@ -128,9 +155,44 @@ pub fn read_key() -> Result> { return Ok(Key::Left); } _ => { - // Unknown key, just ignore it. + // Unknown escape, just skip it. continue; } } } } + +fn read_status_report(stdin: &mut dyn BufRead) -> Result<(u16, u16), Box> { + 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; + } + if c == b'R' { + r = i; + break; + } + } + if buf.len() < 6 || buf[0] != ESC || buf[1] != b'[' || semicolon + 1 > r || r == buf.len() { + return Err(Box::from(format!( + "invalid response: {:?}", + String::from_utf8_lossy(&buf[0..r]) + ))); + } + let row_str = std::str::from_utf8(&buf[2..semicolon])?; + let col_str = std::str::from_utf8(&buf[semicolon + 1..r])?; + let row = row_str.parse::()?; + let col = col_str.parse::()?; + stdin.consume(r + 1); + // Yuck... 1 indexing + return Ok((row - 1, col - 1)); +} + +pub fn cursor_pos(stdin: &mut dyn BufRead) -> Result<(u16, u16), Box> { + write!(stdout(), "\x1b[6n")?; + stdout().flush()?; + let pos = read_status_report(stdin)?; + return Ok(pos); +} -- cgit v1.3.1