repline: Refactor for fewer redraws and more user control.

Definitely has bugs, but eh!
This commit is contained in:
John 2025-06-17 00:43:02 -04:00
parent ae026420f1
commit 6ba62ac1c4
3 changed files with 262 additions and 184 deletions

View File

@ -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<dyn Error>> {
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(())
}

View File

@ -1,9 +1,9 @@
//! The [Editor] is a multi-line buffer of [`char`]s which operates on an ANSI-compatible terminal. //! 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 std::{collections::VecDeque, fmt::Display, io::Write};
use super::error::{Error, ReplResult}; use super::error::ReplResult;
fn is_newline(c: &char) -> bool { fn is_newline(c: &char) -> bool {
*c == '\n' *c == '\n'
@ -14,7 +14,7 @@ fn write_chars<'a, W: Write>(
w: &mut W, w: &mut W,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
for c in c { for c in c {
write!(w, "{c}")?; queue!(w, Print(c))?;
} }
Ok(()) Ok(())
} }
@ -42,84 +42,63 @@ impl<'a> Editor<'a> {
head.iter().chain(tail.iter()) head.iter().chain(tail.iter())
} }
/// Moves up to the first line of the editor, and clears the screen. fn putchar<W: Write>(&self, c: char, w: &mut W) -> ReplResult<()> {
/// let Self { color, again, .. } = self;
/// This assumes the screen hasn't moved since the last draw.
pub fn undraw<W: Write>(&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),
}?;
queue!(w, Clear(ClearType::FromCursorDown))?;
// write!(w, "\x1b[0J")?;
Ok(())
}
/// Redraws the entire editor
pub fn redraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { head, tail, color, begin, again } = self;
write!(w, "{color}{begin}\x1b[0m ")?;
// draw head
for c in head {
match c { match c {
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "), '\n' => queue!(
_ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()),
}?
}
// 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<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { head, color, begin, again, .. } = self;
queue!(
w, w,
Print('\n'),
MoveToColumn(0), MoveToColumn(0),
Print(color), Print(color),
Print(if head.is_empty() { begin } else { again }), Print(again),
ResetColor, Print(ResetColor),
Print(' '), Print(' ')
)?; ),
c => queue!(w, Print(c)),
}?;
Ok(())
}
pub fn redraw_head<W: Write>(&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 {
self.putchar(*c, w)?;
}
Ok(())
}
pub fn redraw_tail<W: Write>(&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(()) Ok(())
} }
/// Prints the characters before the cursor on the current line. /// Prints the characters before the cursor on the current line.
pub fn print_head<W: Write>(&self, w: &mut W) -> ReplResult<()> { pub fn print_head<W: Write>(&self, w: &mut W) -> ReplResult<()> {
self.prompt(w)?; let Self { head, color, begin, again, .. } = self;
write_chars( let nl = self.head.iter().rposition(is_newline).map(|n| n + 1);
self.head.iter().skip( let prompt = if nl.is_some() { again } else { begin };
self.head
.iter()
.rposition(is_newline)
.unwrap_or(self.head.len())
+ 1,
),
w,
)?;
Ok(())
}
pub fn print_err<W: Write>(&self, w: &mut W, err: impl Display) -> ReplResult<()> {
queue!( queue!(
w, w,
SavePosition, MoveToColumn(0),
Clear(ClearType::UntilNewLine), Print(color),
Print(err), Print(prompt),
RestorePosition ResetColor,
Print(' '),
)?; )?;
write_chars(head.iter().skip(nl.unwrap_or(0)), w)?;
Ok(()) Ok(())
} }
@ -132,53 +111,53 @@ impl<'a> Editor<'a> {
Ok(()) Ok(())
} }
/// Writes a character at the cursor, shifting the text around as necessary. pub fn print_err<W: Write>(&self, err: impl Display, w: &mut W) -> ReplResult<()> {
pub fn push<W: Write>(&mut self, c: char, w: &mut W) -> ReplResult<()> { queue!(
// Tail optimization: if the tail is empty, w,
//we don't have to undraw and redraw on newline SavePosition,
if self.tail.is_empty() { Clear(ClearType::UntilNewLine),
self.head.push_back(c); Print(err),
match c { RestorePosition
'\n' => { )?;
write!(w, "\r\n")?; Ok(())
self.print_head(w)?;
}
c => {
queue!(w, Print(c))?;
}
};
return Ok(());
} }
if '\n' == c { /// Writes a character at the cursor, shifting the text around as necessary.
self.undraw(w)?; pub fn push<W: Write>(&mut self, c: char, w: &mut W) -> ReplResult<()> {
}
self.head.push_back(c); self.head.push_back(c);
self.putchar(c, w)?;
match c { match c {
'\n' => self.redraw(w)?, '\n' => self.redraw_tail(w),
_ => { _ => self.print_tail(w),
write!(w, "{c}")?;
self.print_tail(w)?;
} }
} }
Ok(())
}
/// Erases a character at the cursor, shifting the text around as necessary. /// Erases a character at the cursor, shifting the text around as necessary.
pub fn pop<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> { pub fn pop<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
if let Some('\n') = self.head.back() {
self.undraw(w)?;
}
let c = self.head.pop_back(); let c = self.head.pop_back();
// if the character was a newline, we need to go back a line
match c { 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(_) => { Some(_) => {
// go back a char queue!(w, MoveLeft(1), Clear(ClearType::UntilNewLine))?;
queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?;
self.print_tail(w)?; self.print_tail(w)?;
} }
None => {} }
Ok(c)
}
/// Pops the character after the cursor, redrawing if necessary
pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
let c = self.tail.pop_front();
match c {
Some('\n') => self.redraw_tail(w)?,
_ => self.print_tail(w)?,
} }
Ok(c) 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. /// 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<W: Write>(&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.clear();
self.head.extend(s.chars()) self.print_head(w)?;
self.extend(s.chars(), w)
} }
/// Clears the editor, removing all characters. /// Clears the editor, removing all characters.
@ -207,24 +191,6 @@ impl<'a> Editor<'a> {
self.tail.clear(); self.tail.clear();
} }
/// Pops the character after the cursor, redrawing if necessary
pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
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 /// Erases a word from the buffer, where a word is any non-whitespace characters
/// preceded by a single whitespace character /// preceded by a single whitespace character
pub fn erase_word<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { pub fn erase_word<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
@ -237,15 +203,25 @@ impl<'a> Editor<'a> {
self.head.len() + self.tail.len() 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 { pub fn at_start(&self) -> bool {
self.head.is_empty() 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 { pub fn at_end(&self) -> bool {
self.tail.is_empty() 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. /// Returns true if the buffer is empty.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.at_start() && self.at_end() self.at_start() && self.at_end()
@ -266,59 +242,102 @@ impl<'a> Editor<'a> {
} }
/// Moves the cursor back `steps` steps /// Moves the cursor back `steps` steps
pub fn cursor_back<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { pub fn cursor_back<W: Write>(&mut self, 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 { let Some(c) = self.head.pop_back() else {
return Ok(()); return Ok(());
}; };
self.tail.push_front(c); self.tail.push_front(c);
match c { match c {
'\n' => self.redraw(w)?, '\n' => {
_ => queue!(w, MoveLeft(1))?, queue!(w, MoveToPreviousLine(1))?;
self.print_head(w)
} }
_ => queue!(w, MoveLeft(1)).map_err(Into::into),
} }
Ok(())
} }
/// Moves the cursor forward `steps` steps /// Moves the cursor forward `steps` steps
pub fn cursor_forward<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { pub fn cursor_forward<W: Write>(&mut self, 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 { let Some(c) = self.tail.pop_front() else {
return Ok(()); return Ok(());
}; };
self.head.push_back(c); self.head.push_back(c);
match c { match c {
'\n' => self.redraw(w)?, '\n' => {
_ => queue!(w, MoveRight(1))?, 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<W: Write>(&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<W: Write>(&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(()) Ok(())
} }
/// Moves the cursor to the beginning of the current line /// Moves the cursor to the beginning of the current line
pub fn home<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { pub fn cursor_line_start<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
loop { while !self.at_line_start() {
match self.head.back() { self.cursor_back(w)?
Some('\n') | None => break Ok(()),
Some(_) => self.cursor_back(1, w)?,
}
} }
Ok(())
} }
/// Moves the cursor to the end of the current line /// Moves the cursor to the end of the current line
pub fn end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { pub fn cursor_line_end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
loop { while !self.at_line_end() {
match self.tail.front() { self.cursor_forward(w)?
Some('\n') | None => break Ok(()),
Some(_) => self.cursor_forward(1, w)?,
} }
Ok(())
} }
pub fn cursor_start<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
while !self.at_start() {
self.cursor_back(w)?
}
Ok(())
}
pub fn cursor_end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
while !self.at_end() {
self.cursor_forward(w)?
}
Ok(())
} }
} }

View File

@ -1,6 +1,7 @@
//! Prompts the user, reads the lines. Not much more to it than that. //! 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. //! 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 crate::{editor::Editor, error::*, iter::*, raw::raw};
use std::{ use std::{
@ -28,7 +29,6 @@ impl<'a> Repline<'a, std::io::Stdin> {
impl<'a, R: Read> Repline<'a, R> { impl<'a, R: Read> Repline<'a, R> {
/// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts. /// 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 { pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self {
#[allow(clippy::unbuffered_bytes)]
Self { Self {
input: Chars(Flatten(input.bytes())), input: Chars(Flatten(input.bytes())),
history: Default::default(), history: Default::default(),
@ -36,6 +36,14 @@ impl<'a, R: Read> Repline<'a, R> {
ed: Editor::new(color, begin, again), ed: Editor::new(color, begin, again),
} }
} }
pub fn swap_input<S: Read>(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 /// Set the terminal prompt color
pub fn set_color(&mut self, color: &'a str) { pub fn set_color(&mut self, color: &'a str) {
self.ed.color = color self.ed.color = color
@ -56,8 +64,7 @@ impl<'a, R: Read> Repline<'a, R> {
let mut stdout = stdout().lock(); let mut stdout = stdout().lock();
let stdout = &mut stdout; let stdout = &mut stdout;
let _make_raw = raw(); let _make_raw = raw();
// self.ed.begin_frame(stdout)?;
// self.ed.redraw_frame(stdout)?;
self.ed.print_head(stdout)?; self.ed.print_head(stdout)?;
loop { loop {
stdout.flush()?; stdout.flush()?;
@ -75,24 +82,19 @@ impl<'a, R: Read> Repline<'a, R> {
return Err(Error::CtrlD(self.ed.to_string())); return Err(Error::CtrlD(self.ed.to_string()));
} }
// Tab: extend line by 4 spaces // Tab: extend line by 4 spaces
'\t' => { '\t' => self.ed.extend(INDENT.chars(), stdout)?,
self.ed.extend(INDENT.chars(), stdout)?;
}
// ignore newlines, process line feeds. Not sure how cross-platform this is. // ignore newlines, process line feeds. Not sure how cross-platform this is.
'\n' => {} '\n' => {}
'\r' => { '\r' => {
if self.ed.at_end() { if self.ed.at_end() {
self.ed.push('\n', stdout)?; self.ed.push('\n', stdout)?;
} else { } else {
self.ed.end(stdout)?; self.ed.cursor_line_end(stdout)?;
writeln!(stdout)?;
} }
return Ok(self.ed.to_string()); return Ok(self.ed.to_string());
} }
// Ctrl+Backspace in my terminal // Ctrl+Backspace in my terminal
'\x17' => { '\x17' => self.ed.erase_word(stdout)?,
self.ed.erase_word(stdout)?;
}
// Escape sequence // Escape sequence
'\x1b' => self.escape(stdout)?, '\x1b' => self.escape(stdout)?,
// backspace // backspace
@ -123,8 +125,12 @@ impl<'a, R: Read> Repline<'a, R> {
self.print_err(&mut stdout, value) self.print_err(&mut stdout, value)
} }
/// Prints a message (ideally an error) without moving the cursor /// Prints a message (ideally an error) without moving the cursor
fn print_err<W: Write>(&mut self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> { fn print_err<W: Write>(&self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> {
self.ed.print_err(w, value) self.ed.print_err(value, w)
}
// Prints some debug info into the editor's buffer and the provided writer
pub fn put<D: std::fmt::Display, W: Write>(&mut self, disp: D, w: &mut W) -> ReplResult<()> {
self.ed.extend(format!("{disp}").chars(), w)
} }
/// Handle ANSI Escape /// Handle ANSI Escape
fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
@ -138,26 +144,39 @@ impl<'a, R: Read> Repline<'a, R> {
/// Handle ANSI Control Sequence Introducer /// Handle ANSI Control Sequence Introducer
fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
match self.input.next().ok_or(Error::EndOfInput)?? { match self.input.next().ok_or(Error::EndOfInput)?? {
'A' => { 'A' if self.ed.at_start() && self.hindex > 0 => {
self.hindex = self.hindex.saturating_sub(1); self.hindex -= 1;
self.restore_history(w)? self.restore_history(w)?;
self.ed.cursor_start(w)?;
} }
'B' => { 'A' => self.ed.cursor_up(w)?,
self.hindex = self.hindex.saturating_add(1).min(self.history.len()); 'B' if self.ed.at_end() && self.hindex < self.history.len() => {
self.restore_history(w)? self.restore_history(w)?;
self.hindex += 1;
} }
'C' => self.ed.cursor_forward(1, w)?, 'B' => self.ed.cursor_down(w)?,
'D' => self.ed.cursor_back(1, w)?, 'C' => self.ed.cursor_forward(w)?,
'H' => self.ed.home(w)?, 'D' => self.ed.cursor_back(w)?,
'F' => self.ed.end(w)?, 'H' => self.ed.cursor_line_start(w)?,
'F' => self.ed.cursor_line_end(w)?,
'3' => { '3' => {
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { 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 => { other => {
if cfg!(debug_assertions) { 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 /// Restores the currently selected history
fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> { fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
let Self { history, hindex, ed, .. } = self; let Self { history, hindex, ed, .. } = self;
ed.undraw(w)?;
ed.clear();
ed.print_head(w)?;
if let Some(history) = history.get(*hindex) { 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(()) Ok(())
} }