aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2024-01-07 14:14:58 -0800
committerRose Hogenson <rosehogenson@posteo.net>2024-01-07 14:14:58 -0800
commit95c8e4763eddb2336d6ae7acdb752da94be43f5b (patch)
tree08770b419b7097b65d8c395e3e9f508130daea3e
parent6f085692209f06c3427c475322786be06e45160f (diff)
downloadeditor-95c8e4763eddb2336d6ae7acdb752da94be43f5b.tar.zst
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.
-rw-r--r--src/main.rs187
-rw-r--r--src/term.rs92
2 files changed, 171 insertions, 108 deletions
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<dyn Error>> {
- 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<Stdin>,
+
+ i: usize,
+ history: Vec<State>,
+}
+
+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::<usize>()?;
- let col = col_str.parse::<usize>()?;
- // Yuck... 1 indexing
- return Ok((row - 1, col - 1));
}
-fn cursor_pos() -> Result<(usize, usize), Box<dyn Error>> {
- 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<dyn Error>> {
+impl StateHistory {
+ fn repaint_all(&mut self) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
- 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<dyn Error>> {
- 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<State>,
-}
-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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
} 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<dyn Error>> {
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<winsize, Box<dyn Error>> {
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<Key, Box<dyn Error>> {
- 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<Key, Box<dyn Error>> {
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<Key, Box<dyn Error>> {
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<Key, Box<dyn Error>> {
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<dyn Error>> {
+ 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::<u16>()?;
+ let col = col_str.parse::<u16>()?;
+ 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<dyn Error>> {
+ write!(stdout(), "\x1b[6n")?;
+ stdout().flush()?;
+ let pos = read_status_report(stdin)?;
+ return Ok(pos);
+}