From 6ba62ac1c47888ae94363598ae62be82203eec7d Mon Sep 17 00:00:00 2001 From: John Date: Tue, 17 Jun 2025 00:43:02 -0400 Subject: [PATCH] repline: Refactor for fewer redraws and more user control. Definitely has bugs, but eh! --- repline/examples/continue.rs | 42 +++++ repline/src/editor.rs | 329 ++++++++++++++++++----------------- repline/src/repline.rs | 75 +++++--- 3 files changed, 262 insertions(+), 184 deletions(-) create mode 100644 repline/examples/continue.rs diff --git a/repline/examples/continue.rs b/repline/examples/continue.rs new file mode 100644 index 0000000..809042c --- /dev/null +++ b/repline/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/repline/src/editor.rs b/repline/src/editor.rs index f2e173b..280a03f 100644 --- a/repline/src/editor.rs +++ b/repline/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,84 +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, - ), - w, - )?; - Ok(()) - } + 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 }; - pub fn print_err(&self, w: &mut W, err: impl Display) -> ReplResult<()> { queue!( w, - SavePosition, - Clear(ClearType::UntilNewLine), - Print(err), - RestorePosition + MoveToColumn(0), + Print(color), + Print(prompt), + ResetColor, + Print(' '), )?; + + write_chars(head.iter().skip(nl.unwrap_or(0)), w)?; Ok(()) } @@ -132,53 +111,53 @@ 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); + 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) } @@ -196,9 +175,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. @@ -207,24 +191,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<()> { @@ -237,15 +203,25 @@ impl<'a> Editor<'a> { self.head.len() + self.tail.len() } - /// Returns true if the cursor is at the beginning + /// 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 + /// 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.at_start() && self.at_end() @@ -266,59 +242,102 @@ 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(()) } } diff --git a/repline/src/repline.rs b/repline/src/repline.rs index df19416..662b476 100644 --- a/repline/src/repline.rs +++ b/repline/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::{ @@ -28,7 +29,6 @@ impl<'a> Repline<'a, std::io::Stdin> { impl<'a, R: Read> Repline<'a, R> { /// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts. pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self { - #[allow(clippy::unbuffered_bytes)] Self { input: Chars(Flatten(input.bytes())), history: Default::default(), @@ -36,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 @@ -56,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()?; @@ -75,24 +82,19 @@ 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' => { if self.ed.at_end() { self.ed.push('\n', stdout)?; } else { - self.ed.end(stdout)?; - writeln!(stdout)?; + self.ed.cursor_line_end(stdout)?; } 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 @@ -123,8 +125,12 @@ impl<'a, R: Read> Repline<'a, R> { self.print_err(&mut stdout, value) } /// Prints a message (ideally an error) without moving the cursor - fn print_err(&mut self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> { - self.ed.print_err(w, value) + 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<()> { @@ -138,26 +144,39 @@ 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)?; + self.ed.cursor_start(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() => { + self.restore_history(w)?; + self.hindex += 1; } - '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())?; } } } @@ -166,11 +185,9 @@ 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!(" History {hindex} restored!"), w)?; } Ok(()) }