diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..c460cf0 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,16 @@ +unstable_features = true +max_width = 100 +wrap_comments = true +comment_width = 100 +struct_lit_width = 100 + +imports_granularity = "Crate" +# Allow structs to fill an entire line +# use_small_heuristics = "Max" +# Allow small functions on single line +# fn_single_line = true + +# Alignment +enum_discrim_align_threshold = 12 +#struct_field_align_threshold = 12 +where_single_line = true diff --git a/examples/continue.rs b/examples/continue.rs new file mode 100644 index 0000000..809042c --- /dev/null +++ b/examples/continue.rs @@ -0,0 +1,42 @@ +//! Demonstrates the use of [read_and()]: +//! +//! The provided closure: +//! 1. Takes a line of input (a [String]) +//! 2. Performs some calculation (using [FromStr]) +//! 3. Returns a [Result] containing a [Response] or an [Err] + +use repline::{error::Error as RlError, Repline, Response}; +use std::error::Error; + +fn main() -> Result<(), Box> { + let mut rl = Repline::with_input( + "fn main {\r\n\tprintln(\"Foo!\")\r\n}\r\n".as_bytes(), + "\x1b[33m", + " >", + " ?>", + ); + while rl.read().is_ok() {} + + let mut rl = rl.swap_input(std::io::stdin()); + loop { + let f = |_line| -> Result<_, RlError> { Ok(Response::Continue) }; + let line = match rl.read() { + Err(RlError::CtrlC(_)) => break, + Err(RlError::CtrlD(line)) => { + rl.deny(); + line + } + Ok(line) => line, + Err(e) => Err(e)?, + }; + print!("\x1b[G\x1b[J"); + match f(&line) { + Ok(Response::Accept) => rl.accept(), + Ok(Response::Deny) => rl.deny(), + Ok(Response::Break) => break, + Ok(Response::Continue) => continue, + Err(e) => print!("\x1b[40G\x1b[A\x1bJ\x1b[91m{e}\x1b[0m\x1b[B"), + } + } + Ok(()) +} diff --git a/examples/error.rs b/examples/error.rs new file mode 100644 index 0000000..33b8741 --- /dev/null +++ b/examples/error.rs @@ -0,0 +1,4 @@ +fn main() { + let mut rl = repline::Repline::with_input([255, b'\r', b'\n'].as_slice(), "", "", ""); + eprintln!("{:?}", rl.read()) +} diff --git a/examples/repl_float.rs b/examples/repl_float.rs index c72ea89..bd93468 100644 --- a/examples/repl_float.rs +++ b/examples/repl_float.rs @@ -1,5 +1,5 @@ //! Demonstrates the use of [read_and()]: -//! +//! //! The provided closure: //! 1. Takes a line of input (a [String]) //! 2. Performs some calculation (using [FromStr]) diff --git a/src/editor.rs b/src/editor.rs index 6cecd9a..903028b 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,9 +1,9 @@ //! The [Editor] is a multi-line buffer of [`char`]s which operates on an ANSI-compatible terminal. -use crossterm::{cursor::*, execute, queue, style::*, terminal::*}; +use crossterm::{cursor::*, queue, style::*, terminal::*}; use std::{collections::VecDeque, fmt::Display, io::Write}; -use super::error::{Error, ReplResult}; +use super::error::ReplResult; fn is_newline(c: &char) -> bool { *c == '\n' @@ -14,7 +14,7 @@ fn write_chars<'a, W: Write>( w: &mut W, ) -> std::io::Result<()> { for c in c { - write!(w, "{c}")?; + queue!(w, Print(c))?; } Ok(()) } @@ -42,73 +42,63 @@ impl<'a> Editor<'a> { head.iter().chain(tail.iter()) } - /// Moves up to the first line of the editor, and clears the screen. - /// - /// This assumes the screen hasn't moved since the last draw. - pub fn undraw(&self, w: &mut W) -> ReplResult<()> { - let Self { head, .. } = self; - match head.iter().copied().filter(is_newline).count() { - 0 => write!(w, "\x1b[0G"), - lines => write!(w, "\x1b[{}F", lines), + fn putchar(&self, c: char, w: &mut W) -> ReplResult<()> { + let Self { color, again, .. } = self; + match c { + '\n' => queue!( + w, + Print('\n'), + MoveToColumn(0), + Print(color), + Print(again), + Print(ResetColor), + Print(' ') + ), + c => queue!(w, Print(c)), }?; - queue!(w, Clear(ClearType::FromCursorDown))?; - // write!(w, "\x1b[0J")?; Ok(()) } - /// Redraws the entire editor - pub fn redraw(&self, w: &mut W) -> ReplResult<()> { - let Self { head, tail, color, begin, again } = self; - write!(w, "{color}{begin}\x1b[0m ")?; - // draw head + pub fn redraw_head(&self, w: &mut W) -> ReplResult<()> { + let Self { head, color, begin, .. } = self; + match head.iter().copied().filter(is_newline).count() { + 0 => queue!(w, MoveToColumn(0)), + n => queue!(w, MoveUp(n as u16)), + }?; + + queue!(w, Print(color), Print(begin), Print(ResetColor), Print(' '))?; for c in head { - match c { - '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), - _ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()), - }? + self.putchar(*c, w)?; } - // save cursor - execute!(w, SavePosition)?; - // draw tail - for c in tail { - match c { - '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), - _ => write!(w, "{c}"), - }? - } - // restore cursor - execute!(w, RestorePosition)?; Ok(()) } - /// Prints a context-sensitive prompt (either `begin` if this is the first line, - /// or `again` for subsequent lines) - pub fn prompt(&self, w: &mut W) -> ReplResult<()> { - let Self { head, color, begin, again, .. } = self; - queue!( - w, - MoveToColumn(0), - Print(color), - Print(if head.is_empty() { begin } else { again }), - ResetColor, - Print(' '), - )?; + pub fn redraw_tail(&self, w: &mut W) -> ReplResult<()> { + let Self { tail, .. } = self; + queue!(w, SavePosition, Clear(ClearType::FromCursorDown))?; + for c in tail { + self.putchar(*c, w)?; + } + queue!(w, RestorePosition)?; Ok(()) } /// Prints the characters before the cursor on the current line. pub fn print_head(&self, w: &mut W) -> ReplResult<()> { - self.prompt(w)?; - write_chars( - self.head.iter().skip( - self.head - .iter() - .rposition(is_newline) - .unwrap_or(self.head.len()) - + 1, - ), + let Self { head, color, begin, again, .. } = self; + let nl = self.head.iter().rposition(is_newline).map(|n| n + 1); + let prompt = if nl.is_some() { again } else { begin }; + + queue!( w, + MoveToColumn(0), + Print(color), + Print(prompt), + ResetColor, + Print(' '), )?; + + write_chars(head.iter().skip(nl.unwrap_or(0)), w)?; Ok(()) } @@ -121,53 +111,54 @@ impl<'a> Editor<'a> { Ok(()) } + pub fn print_err(&self, err: impl Display, w: &mut W) -> ReplResult<()> { + queue!( + w, + SavePosition, + Clear(ClearType::UntilNewLine), + Print(err), + RestorePosition + )?; + Ok(()) + } + /// Writes a character at the cursor, shifting the text around as necessary. pub fn push(&mut self, c: char, w: &mut W) -> ReplResult<()> { - // Tail optimization: if the tail is empty, - //we don't have to undraw and redraw on newline - if self.tail.is_empty() { - self.head.push_back(c); - match c { - '\n' => { - write!(w, "\r\n")?; - self.print_head(w)?; - } - c => { - queue!(w, Print(c))?; - } - }; - return Ok(()); - } - - if '\n' == c { - self.undraw(w)?; - } self.head.push_back(c); + queue!(w, Clear(ClearType::UntilNewLine))?; + self.putchar(c, w)?; match c { - '\n' => self.redraw(w)?, - _ => { - write!(w, "{c}")?; - self.print_tail(w)?; - } + '\n' => self.redraw_tail(w), + _ => self.print_tail(w), } - Ok(()) } /// Erases a character at the cursor, shifting the text around as necessary. pub fn pop(&mut self, w: &mut W) -> ReplResult> { - if let Some('\n') = self.head.back() { - self.undraw(w)?; - } let c = self.head.pop_back(); - // if the character was a newline, we need to go back a line + match c { - Some('\n') => self.redraw(w)?, + None => return Ok(None), + Some('\n') => { + queue!(w, MoveToPreviousLine(1))?; + self.print_head(w)?; + self.redraw_tail(w)?; + } Some(_) => { - // go back a char - queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?; + queue!(w, MoveLeft(1), Clear(ClearType::UntilNewLine))?; self.print_tail(w)?; } - None => {} + } + + Ok(c) + } + + /// Pops the character after the cursor, redrawing if necessary + pub fn delete(&mut self, w: &mut W) -> ReplResult> { + let c = self.tail.pop_front(); + match c { + Some('\n') => self.redraw_tail(w)?, + _ => self.print_tail(w)?, } Ok(c) } @@ -185,9 +176,14 @@ impl<'a> Editor<'a> { } /// Sets the editor to the contents of a string, placing the cursor at the end. - pub fn restore(&mut self, s: &str) { + pub fn restore(&mut self, s: &str, w: &mut W) -> ReplResult<()> { + match self.head.iter().copied().filter(is_newline).count() { + 0 => queue!(w, MoveToColumn(0), Clear(ClearType::FromCursorDown))?, + n => queue!(w, MoveUp(n as u16), Clear(ClearType::FromCursorDown))?, + }; self.clear(); - self.head.extend(s.chars()) + self.print_head(w)?; + self.extend(s.chars(), w) } /// Clears the editor, removing all characters. @@ -196,24 +192,6 @@ impl<'a> Editor<'a> { self.tail.clear(); } - /// Pops the character after the cursor, redrawing if necessary - pub fn delete(&mut self, w: &mut W) -> ReplResult { - match self.tail.front() { - Some('\n') => { - self.undraw(w)?; - let out = self.tail.pop_front(); - self.redraw(w)?; - out - } - _ => { - let out = self.tail.pop_front(); - self.print_tail(w)?; - out - } - } - .ok_or(Error::EndOfInput) - } - /// Erases a word from the buffer, where a word is any non-whitespace characters /// preceded by a single whitespace character pub fn erase_word(&mut self, w: &mut W) -> ReplResult<()> { @@ -226,9 +204,28 @@ impl<'a> Editor<'a> { self.head.len() + self.tail.len() } + /// Returns true if the cursor is at the start of the buffer + pub fn at_start(&self) -> bool { + self.head.is_empty() + } + /// Returns true if the cursor is at the end of the buffer + pub fn at_end(&self) -> bool { + self.tail.is_empty() + } + + /// Returns true if the cursor is at the start of a line + pub fn at_line_start(&self) -> bool { + matches!(self.head.back(), None | Some('\n')) + } + + /// Returns true if the cursor is at the end of a line + pub fn at_line_end(&self) -> bool { + matches!(self.tail.front(), None | Some('\n')) + } + /// Returns true if the buffer is empty. pub fn is_empty(&self) -> bool { - self.head.is_empty() && self.tail.is_empty() + self.at_start() && self.at_end() } /// Returns true if the buffer ends with a given pattern @@ -246,63 +243,106 @@ impl<'a> Editor<'a> { } /// Moves the cursor back `steps` steps - pub fn cursor_back(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { - for _ in 0..steps { - if let Some('\n') = self.head.back() { - self.undraw(w)?; - } - let Some(c) = self.head.pop_back() else { - return Ok(()); - }; - self.tail.push_front(c); - match c { - '\n' => self.redraw(w)?, - _ => queue!(w, MoveLeft(1))?, + pub fn cursor_back(&mut self, w: &mut W) -> ReplResult<()> { + let Some(c) = self.head.pop_back() else { + return Ok(()); + }; + + self.tail.push_front(c); + match c { + '\n' => { + queue!(w, MoveToPreviousLine(1))?; + self.print_head(w) } + _ => queue!(w, MoveLeft(1)).map_err(Into::into), } - Ok(()) } /// Moves the cursor forward `steps` steps - pub fn cursor_forward(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { - for _ in 0..steps { - if let Some('\n') = self.tail.front() { - self.undraw(w)? - } - let Some(c) = self.tail.pop_front() else { - return Ok(()); - }; - self.head.push_back(c); - match c { - '\n' => self.redraw(w)?, - _ => queue!(w, MoveRight(1))?, + pub fn cursor_forward(&mut self, w: &mut W) -> ReplResult<()> { + let Some(c) = self.tail.pop_front() else { + return Ok(()); + }; + + self.head.push_back(c); + match c { + '\n' => { + queue!(w, MoveToNextLine(1))?; + self.print_head(w) } + _ => queue!(w, MoveRight(1)).map_err(Into::into), } + } + + /// Moves the cursor up to the previous line, attempting to preserve relative offset + pub fn cursor_up(&mut self, w: &mut W) -> ReplResult<()> { + // Calculates length of the current line + let mut len = self.head.len(); + self.cursor_line_start(w)?; + len -= self.head.len(); + + if self.at_start() { + return Ok(()); + } + + self.cursor_back(w)?; + self.cursor_line_start(w)?; + + while 0 < len && !self.at_line_end() { + self.cursor_forward(w)?; + len -= 1; + } + + Ok(()) + } + + /// Moves the cursor down to the next line, attempting to preserve relative offset + pub fn cursor_down(&mut self, w: &mut W) -> ReplResult<()> { + let mut len = self.head.iter().rev().take_while(|&&c| c != '\n').count(); + + self.cursor_line_end(w)?; + self.cursor_forward(w)?; + + while 0 < len && !self.at_line_end() { + self.cursor_forward(w)?; + len -= 1; + } + Ok(()) } /// Moves the cursor to the beginning of the current line - pub fn home(&mut self, w: &mut W) -> ReplResult<()> { - loop { - match self.head.back() { - Some('\n') | None => break Ok(()), - Some(_) => self.cursor_back(1, w)?, - } + pub fn cursor_line_start(&mut self, w: &mut W) -> ReplResult<()> { + while !self.at_line_start() { + self.cursor_back(w)? } + Ok(()) } /// Moves the cursor to the end of the current line - pub fn end(&mut self, w: &mut W) -> ReplResult<()> { - loop { - match self.tail.front() { - Some('\n') | None => break Ok(()), - Some(_) => self.cursor_forward(1, w)?, - } + pub fn cursor_line_end(&mut self, w: &mut W) -> ReplResult<()> { + while !self.at_line_end() { + self.cursor_forward(w)? } + Ok(()) + } + + pub fn cursor_start(&mut self, w: &mut W) -> ReplResult<()> { + while !self.at_start() { + self.cursor_back(w)? + } + Ok(()) + } + + pub fn cursor_end(&mut self, w: &mut W) -> ReplResult<()> { + while !self.at_end() { + self.cursor_forward(w)? + } + Ok(()) } } -impl<'a, 'e> IntoIterator for &'e Editor<'a> { +impl<'e> IntoIterator for &'e Editor<'_> { type Item = &'e char; type IntoIter = std::iter::Chain< std::collections::vec_deque::Iter<'e, char>, @@ -313,7 +353,7 @@ impl<'a, 'e> IntoIterator for &'e Editor<'a> { } } -impl<'a> Display for Editor<'a> { +impl Display for Editor<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use std::fmt::Write; for c in self.iter() { diff --git a/src/iter.rs b/src/iter.rs index 4af50bc..b57af60 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -39,7 +39,7 @@ pub mod chars { if cont & 0xc0 != 0x80 { return None; } - out = out << 6 | (cont & 0x3f); + out = (out << 6) | (cont & 0x3f); } Some(char::from_u32(out).ok_or(BadUnicode(out))) } diff --git a/src/prebaked.rs b/src/prebaked.rs index ba2aa15..fba802c 100644 --- a/src/prebaked.rs +++ b/src/prebaked.rs @@ -49,7 +49,7 @@ where F: FnMut(&str) -> Result> { Ok(Response::Deny) => rl.deny(), Ok(Response::Break) => break, Ok(Response::Continue) => continue, - Err(e) => print!("\x1b[40G\x1b[A\x1bJ\x1b[91m{e}\x1b[0m\x1b[B"), + Err(e) => rl.print_inline(format_args!("\x1b[40G\x1b[91m{e}\x1b[0m"))?, } } Ok(()) diff --git a/src/raw.rs b/src/raw.rs index 1b66d3f..40b2a17 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -8,8 +8,7 @@ struct Raw(); impl Default for Raw { fn default() -> Self { std::thread::yield_now(); - crossterm::terminal::enable_raw_mode() - .expect("should be able to transition into raw mode"); + crossterm::terminal::enable_raw_mode().expect("should be able to transition into raw mode"); Raw() } } diff --git a/src/repline.rs b/src/repline.rs index 4dc19e7..822816e 100644 --- a/src/repline.rs +++ b/src/repline.rs @@ -1,6 +1,7 @@ //! Prompts the user, reads the lines. Not much more to it than that. //! //! This module is in charge of parsing keyboard input and interpreting it for the line editor. +#![allow(clippy::unbuffered_bytes)] use crate::{editor::Editor, error::*, iter::*, raw::raw}; use std::{ @@ -35,6 +36,14 @@ impl<'a, R: Read> Repline<'a, R> { ed: Editor::new(color, begin, again), } } + pub fn swap_input(self, new_input: S) -> Repline<'a, S> { + Repline { + input: Chars(Flatten(new_input.bytes())), + history: self.history, + hindex: self.hindex, + ed: self.ed, + } + } /// Set the terminal prompt color pub fn set_color(&mut self, color: &'a str) { self.ed.color = color @@ -55,8 +64,7 @@ impl<'a, R: Read> Repline<'a, R> { let mut stdout = stdout().lock(); let stdout = &mut stdout; let _make_raw = raw(); - // self.ed.begin_frame(stdout)?; - // self.ed.redraw_frame(stdout)?; + self.ed.print_head(stdout)?; loop { stdout.flush()?; @@ -74,19 +82,17 @@ impl<'a, R: Read> Repline<'a, R> { return Err(Error::CtrlD(self.ed.to_string())); } // Tab: extend line by 4 spaces - '\t' => { - self.ed.extend(INDENT.chars(), stdout)?; - } + '\t' => self.ed.extend(INDENT.chars(), stdout)?, // ignore newlines, process line feeds. Not sure how cross-platform this is. '\n' => {} '\r' => { self.ed.push('\n', stdout)?; - return Ok(self.ed.to_string()); + if self.ed.at_end() { + return Ok(self.ed.to_string()); + } } // Ctrl+Backspace in my terminal - '\x17' => { - self.ed.erase_word(stdout)?; - } + '\x17' => self.ed.erase_word(stdout)?, // Escape sequence '\x1b' => self.escape(stdout)?, // backspace @@ -111,6 +117,19 @@ impl<'a, R: Read> Repline<'a, R> { } } } + /// Prints a message without moving the cursor + pub fn print_inline(&mut self, value: impl std::fmt::Display) -> ReplResult<()> { + let mut stdout = stdout().lock(); + self.print_err(&mut stdout, value) + } + /// Prints a message (ideally an error) without moving the cursor + fn print_err(&self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> { + self.ed.print_err(value, w) + } + // Prints some debug info into the editor's buffer and the provided writer + pub fn put(&mut self, disp: D, w: &mut W) -> ReplResult<()> { + self.ed.extend(format!("{disp}").chars(), w) + } /// Handle ANSI Escape fn escape(&mut self, w: &mut W) -> ReplResult<()> { match self.input.next().ok_or(Error::EndOfInput)?? { @@ -123,26 +142,38 @@ impl<'a, R: Read> Repline<'a, R> { /// Handle ANSI Control Sequence Introducer fn csi(&mut self, w: &mut W) -> ReplResult<()> { match self.input.next().ok_or(Error::EndOfInput)?? { - 'A' => { - self.hindex = self.hindex.saturating_sub(1); - self.restore_history(w)? + 'A' if self.ed.at_start() && self.hindex > 0 => { + self.hindex -= 1; + self.restore_history(w)?; } - 'B' => { - self.hindex = self.hindex.saturating_add(1).min(self.history.len()); - self.restore_history(w)? + 'A' => self.ed.cursor_up(w)?, + 'B' if self.ed.at_end() && self.hindex < self.history.len().saturating_sub(1) => { + self.hindex += 1; + self.restore_history(w)?; } - 'C' => self.ed.cursor_forward(1, w)?, - 'D' => self.ed.cursor_back(1, w)?, - 'H' => self.ed.home(w)?, - 'F' => self.ed.end(w)?, + 'B' => self.ed.cursor_down(w)?, + 'C' => self.ed.cursor_forward(w)?, + 'D' => self.ed.cursor_back(w)?, + 'H' => self.ed.cursor_line_start(w)?, + 'F' => self.ed.cursor_line_end(w)?, '3' => { if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { - let _ = self.ed.delete(w); + self.ed.delete(w)?; + } + } + '5' => { + if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { + self.ed.cursor_start(w)? + } + } + '6' => { + if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { + self.ed.cursor_end(w)? } } other => { if cfg!(debug_assertions) { - self.ed.extend(other.escape_debug(), w)?; + self.print_err(w, other.escape_debug())?; } } } @@ -151,11 +182,12 @@ impl<'a, R: Read> Repline<'a, R> { /// Restores the currently selected history fn restore_history(&mut self, w: &mut W) -> ReplResult<()> { let Self { history, hindex, ed, .. } = self; - ed.undraw(w)?; - ed.clear(); - ed.print_head(w)?; if let Some(history) = history.get(*hindex) { - ed.extend(history.chars(), w)? + ed.restore(history, w)?; + ed.print_err( + format_args!("\t\x1b[30mHistory {hindex} restored!\x1b[0m"), + w, + )?; } Ok(()) } @@ -165,9 +197,12 @@ impl<'a, R: Read> Repline<'a, R> { while buf.ends_with(char::is_whitespace) { buf.pop(); } - if !self.history.contains(&buf) { - self.history.push_back(buf) - } + if let Some(idx) = self.history.iter().position(|v| *v == buf) { + self.history + .remove(idx) + .expect("should have just found this"); + }; + self.history.push_back(buf); while self.history.len() > 20 { self.history.pop_front(); }