repline: Goodbye, old friend. You have been promoted.
This commit is contained in:
@@ -9,7 +9,6 @@ members = [
|
|||||||
"compiler/cl-ast",
|
"compiler/cl-ast",
|
||||||
"compiler/cl-parser",
|
"compiler/cl-parser",
|
||||||
"compiler/cl-lexer",
|
"compiler/cl-lexer",
|
||||||
"repline",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ cl-lexer = { path = "../cl-lexer" }
|
|||||||
cl-parser = { path = "../cl-parser" }
|
cl-parser = { path = "../cl-parser" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
repline = { path = "../../repline" }
|
repline = { version = "*", registry = "soft-fish" }
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ cl-typeck = { path = "../cl-typeck" }
|
|||||||
cl-interpret = { path = "../cl-interpret" }
|
cl-interpret = { path = "../cl-interpret" }
|
||||||
cl-structures = { path = "../cl-structures" }
|
cl-structures = { path = "../cl-structures" }
|
||||||
cl-arena = { version = "0", registry = "soft-fish" }
|
cl-arena = { version = "0", registry = "soft-fish" }
|
||||||
repline = { path = "../../repline" }
|
repline = { version = "*", registry = "soft-fish" }
|
||||||
argwerk = "0.20.4"
|
argwerk = "0.20.4"
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ cl-ast = { path = "../cl-ast" }
|
|||||||
cl-structures = { path = "../cl-structures" }
|
cl-structures = { path = "../cl-structures" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
repline = { path = "../../repline" }
|
repline = { version = "*", registry = "soft-fish" }
|
||||||
cl-lexer = { path = "../cl-lexer" }
|
cl-lexer = { path = "../cl-lexer" }
|
||||||
cl-parser = { path = "../cl-parser" }
|
cl-parser = { path = "../cl-parser" }
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "repline"
|
|
||||||
repository.workspace = true
|
|
||||||
version.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
crossterm = { version = "0.29.0", default-features = false }
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
//! 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(())
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//! 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::{prebaked::read_and, Response};
|
|
||||||
use std::{error::Error, str::FromStr};
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
read_and("\x1b[33m", " >", " ?>", |line| {
|
|
||||||
println!("-> {:?}", f64::from_str(line.trim())?);
|
|
||||||
Ok(Response::Accept)
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
//! The [Editor] is a multi-line buffer of [`char`]s which operates on an ANSI-compatible terminal.
|
|
||||||
|
|
||||||
use crossterm::{cursor::*, queue, style::*, terminal::*};
|
|
||||||
use std::{collections::VecDeque, fmt::Display, io::Write};
|
|
||||||
|
|
||||||
use super::error::ReplResult;
|
|
||||||
|
|
||||||
fn is_newline(c: &char) -> bool {
|
|
||||||
*c == '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_chars<'a, W: Write>(
|
|
||||||
c: impl IntoIterator<Item = &'a char>,
|
|
||||||
w: &mut W,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
for c in c {
|
|
||||||
queue!(w, Print(c))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A multi-line editor which operates on an un-cleared ANSI terminal.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Editor<'a> {
|
|
||||||
head: VecDeque<char>,
|
|
||||||
tail: VecDeque<char>,
|
|
||||||
|
|
||||||
pub color: &'a str,
|
|
||||||
pub begin: &'a str,
|
|
||||||
pub again: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Editor<'a> {
|
|
||||||
/// Constructs a new Editor with the provided prompt color, begin prompt, and again prompt.
|
|
||||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
|
||||||
Self { head: Default::default(), tail: Default::default(), color, begin, again }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator over characters in the editor.
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
|
||||||
let Self { head, tail, .. } = self;
|
|
||||||
head.iter().chain(tail.iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn putchar<W: Write>(&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)),
|
|
||||||
}?;
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints the characters before the cursor on the current line.
|
|
||||||
pub fn print_head<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints the characters after the cursor on the current line.
|
|
||||||
pub fn print_tail<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
|
||||||
let Self { tail, .. } = self;
|
|
||||||
queue!(w, SavePosition, Clear(ClearType::UntilNewLine))?;
|
|
||||||
write_chars(tail.iter().take_while(|&c| !is_newline(c)), w)?;
|
|
||||||
queue!(w, RestorePosition)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_err<W: Write>(&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<W: Write>(&mut self, c: char, w: &mut W) -> ReplResult<()> {
|
|
||||||
self.head.push_back(c);
|
|
||||||
queue!(w, Clear(ClearType::UntilNewLine))?;
|
|
||||||
self.putchar(c, w)?;
|
|
||||||
match c {
|
|
||||||
'\n' => self.redraw_tail(w),
|
|
||||||
_ => self.print_tail(w),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>> {
|
|
||||||
let c = self.head.pop_back();
|
|
||||||
|
|
||||||
match c {
|
|
||||||
None => return Ok(None),
|
|
||||||
Some('\n') => {
|
|
||||||
queue!(w, MoveToPreviousLine(1))?;
|
|
||||||
self.print_head(w)?;
|
|
||||||
self.redraw_tail(w)?;
|
|
||||||
}
|
|
||||||
Some(_) => {
|
|
||||||
queue!(w, MoveLeft(1), Clear(ClearType::UntilNewLine))?;
|
|
||||||
self.print_tail(w)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes characters into the editor at the location of the cursor.
|
|
||||||
pub fn extend<T: IntoIterator<Item = char>, W: Write>(
|
|
||||||
&mut self,
|
|
||||||
iter: T,
|
|
||||||
w: &mut W,
|
|
||||||
) -> ReplResult<()> {
|
|
||||||
for c in iter {
|
|
||||||
self.push(c, w)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the editor to the contents of a string, placing the cursor at the end.
|
|
||||||
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.print_head(w)?;
|
|
||||||
self.extend(s.chars(), w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the editor, removing all characters.
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.head.clear();
|
|
||||||
self.tail.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Erases a word from the buffer, where a word is any non-whitespace characters
|
|
||||||
/// preceded by a single whitespace character
|
|
||||||
pub fn erase_word<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
while self.pop(w)?.filter(|c| !c.is_whitespace()).is_some() {}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of characters in the buffer
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
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.at_start() && self.at_end()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the buffer ends with a given pattern
|
|
||||||
pub fn ends_with(&self, iter: impl DoubleEndedIterator<Item = char>) -> bool {
|
|
||||||
let mut iter = iter.rev();
|
|
||||||
let mut head = self.head.iter().rev();
|
|
||||||
loop {
|
|
||||||
match (iter.next(), head.next()) {
|
|
||||||
(None, _) => break true,
|
|
||||||
(Some(_), None) => break false,
|
|
||||||
(Some(a), Some(b)) if a != *b => break false,
|
|
||||||
(Some(_), Some(_)) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor back `steps` steps
|
|
||||||
pub fn cursor_back<W: Write>(&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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor forward `steps` steps
|
|
||||||
pub fn cursor_forward<W: Write>(&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<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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor to the beginning of the current line
|
|
||||||
pub fn cursor_line_start<W: Write>(&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 cursor_line_end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
while !self.at_line_end() {
|
|
||||||
self.cursor_forward(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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'e> IntoIterator for &'e Editor<'_> {
|
|
||||||
type Item = &'e char;
|
|
||||||
type IntoIter = std::iter::Chain<
|
|
||||||
std::collections::vec_deque::Iter<'e, char>,
|
|
||||||
std::collections::vec_deque::Iter<'e, char>,
|
|
||||||
>;
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.head.iter().chain(self.tail.iter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Editor<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
use std::fmt::Write;
|
|
||||||
for c in self.iter() {
|
|
||||||
f.write_char(*c)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::iter::chars::BadUnicode;
|
|
||||||
|
|
||||||
/// Result type for Repline
|
|
||||||
pub type ReplResult<T> = std::result::Result<T, Error>;
|
|
||||||
/// Borrowed error (does not implement [Error](std::error::Error)!)
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// User broke with Ctrl+C
|
|
||||||
CtrlC(String),
|
|
||||||
/// User broke with Ctrl+D
|
|
||||||
CtrlD(String),
|
|
||||||
/// Invalid unicode codepoint
|
|
||||||
BadUnicode(u32),
|
|
||||||
/// Error came from [std::io]
|
|
||||||
IoFailure(std::io::Error),
|
|
||||||
/// End of input
|
|
||||||
EndOfInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::CtrlC(_) => write!(f, "Ctrl+C"),
|
|
||||||
Error::CtrlD(_) => write!(f, "Ctrl+D"),
|
|
||||||
Error::BadUnicode(u) => write!(f, "\\u{{{u:x}}} is not a valid unicode codepoint"),
|
|
||||||
Error::IoFailure(s) => write!(f, "{s}"),
|
|
||||||
Error::EndOfInput => write!(f, "End of input"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(value: std::io::Error) -> Self {
|
|
||||||
Self::IoFailure(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<BadUnicode> for Error {
|
|
||||||
fn from(value: BadUnicode) -> Self {
|
|
||||||
let BadUnicode(code) = value;
|
|
||||||
Self::BadUnicode(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
//! Shmancy iterator adapters
|
|
||||||
|
|
||||||
pub use chars::Chars;
|
|
||||||
pub use flatten::Flatten;
|
|
||||||
|
|
||||||
pub mod chars {
|
|
||||||
//! Converts an <code>[Iterator]<Item = [u8]></code> into an
|
|
||||||
//! <code>[Iterator]<Item = [Result]<[char], [BadUnicode]>></code>
|
|
||||||
|
|
||||||
/// Invalid unicode codepoint found when iterating over [Chars]
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct BadUnicode(pub u32);
|
|
||||||
impl std::error::Error for BadUnicode {}
|
|
||||||
impl std::fmt::Display for BadUnicode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let Self(code) = self;
|
|
||||||
write!(f, "Bad unicode: {code}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an <code>[Iterator]<Item = [u8]></code> into an
|
|
||||||
/// <code>[Iterator]<Item = [char]></code>
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Chars<I: Iterator<Item = u8>>(pub I);
|
|
||||||
impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
|
|
||||||
type Item = Result<char, BadUnicode>;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let Self(bytes) = self;
|
|
||||||
let start = bytes.next()? as u32;
|
|
||||||
let (mut out, count) = match start {
|
|
||||||
start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range
|
|
||||||
start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte
|
|
||||||
start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes
|
|
||||||
start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
for _ in 0..count {
|
|
||||||
let cont = bytes.next()? as u32;
|
|
||||||
if cont & 0xc0 != 0x80 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
out = (out << 6) | (cont & 0x3f);
|
|
||||||
}
|
|
||||||
Some(char::from_u32(out).ok_or(BadUnicode(out)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub mod flatten {
|
|
||||||
//! Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
|
||||||
//! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
|
||||||
|
|
||||||
/// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
|
||||||
/// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
|
|
||||||
impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
|
|
||||||
type Item = T;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.0.next()?.ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
|
|
||||||
type Item = T;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.0.next()?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//! A small pseudo-multiline editing library
|
|
||||||
|
|
||||||
mod editor;
|
|
||||||
mod iter;
|
|
||||||
mod raw;
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod prebaked;
|
|
||||||
pub mod repline;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use prebaked::{read_and, Response};
|
|
||||||
pub use repline::Repline;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
//! Here's a menu I prepared earlier!
|
|
||||||
//!
|
|
||||||
//! Constructs a [Repline] and repeatedly runs the provided closure on the input strings,
|
|
||||||
//! obeying the closure's [Response].
|
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use crate::{error::Error as RlError, repline::Repline};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
||||||
/// Control codes for the [prebaked menu](read_and)
|
|
||||||
pub enum Response {
|
|
||||||
/// Accept the line, and save it to history
|
|
||||||
Accept,
|
|
||||||
/// Reject the line, and clear the buffer
|
|
||||||
Deny,
|
|
||||||
/// End the loop
|
|
||||||
Break,
|
|
||||||
/// Gather more input and try again
|
|
||||||
Continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implements a basic menu loop using an embedded [Repline].
|
|
||||||
///
|
|
||||||
/// Repeatedly runs the provided closure on the input strings,
|
|
||||||
/// obeying the closure's [Response].
|
|
||||||
///
|
|
||||||
/// Captures and displays all user [Error]s.
|
|
||||||
///
|
|
||||||
/// # Keybinds
|
|
||||||
/// - `Ctrl+C` exits the loop
|
|
||||||
/// - `Ctrl+D` clears the input, but *runs the closure* with the old input
|
|
||||||
pub fn read_and<F>(color: &str, begin: &str, again: &str, mut f: F) -> Result<(), RlError>
|
|
||||||
where F: FnMut(&str) -> Result<Response, Box<dyn Error>> {
|
|
||||||
let mut rl = Repline::new(color, begin, again);
|
|
||||||
loop {
|
|
||||||
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) => rl.print_inline(format_args!("\x1b[40G\x1b[91m{e}\x1b[0m"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime.
|
|
||||||
|
|
||||||
/// Sets the terminal to raw mode for the duration of the returned object's lifetime.
|
|
||||||
pub fn raw() -> impl Drop {
|
|
||||||
Raw::default()
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
Raw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for Raw {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
crossterm::terminal::disable_raw_mode()
|
|
||||||
.expect("should be able to transition out of raw mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
//! 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::{
|
|
||||||
collections::VecDeque,
|
|
||||||
io::{Bytes, Read, Result, Write, stdout},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Prompts the user, reads the lines. Not much more to it than that.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Repline<'a, R: Read> {
|
|
||||||
input: Chars<Flatten<Result<u8>, Bytes<R>>>,
|
|
||||||
|
|
||||||
history_cap: usize,
|
|
||||||
history: VecDeque<String>, // previous lines
|
|
||||||
hindex: usize, // current index into the history buffer
|
|
||||||
|
|
||||||
ed: Editor<'a>, // the current line buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Repline<'a, std::io::Stdin> {
|
|
||||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
|
||||||
Self::with_input(std::io::stdin(), color, begin, again)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Self {
|
|
||||||
input: Chars(Flatten(input.bytes())),
|
|
||||||
history_cap: 200,
|
|
||||||
history: Default::default(),
|
|
||||||
hindex: 0,
|
|
||||||
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_cap: self.history_cap,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the entire terminal prompt sequence
|
|
||||||
pub fn set_prompt(&mut self, color: &'a str, begin: &'a str, again: &'a str) {
|
|
||||||
self.ed.color = color;
|
|
||||||
self.ed.begin = begin;
|
|
||||||
self.ed.again = again;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append line to history and clear it
|
|
||||||
pub fn accept(&mut self) {
|
|
||||||
self.history_append(self.ed.to_string());
|
|
||||||
self.ed.clear();
|
|
||||||
self.hindex = self.history.len();
|
|
||||||
}
|
|
||||||
/// Clear the line
|
|
||||||
pub fn deny(&mut self) {
|
|
||||||
self.ed.clear()
|
|
||||||
}
|
|
||||||
/// Reads in a line, and returns it for validation
|
|
||||||
pub fn read(&mut self) -> ReplResult<String> {
|
|
||||||
const INDENT: &str = " ";
|
|
||||||
let mut stdout = stdout().lock();
|
|
||||||
let stdout = &mut stdout;
|
|
||||||
let _make_raw = raw();
|
|
||||||
|
|
||||||
self.ed.print_head(stdout)?;
|
|
||||||
loop {
|
|
||||||
stdout.flush()?;
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
// Ctrl+C: End of Text. Immediately exits.
|
|
||||||
'\x03' => {
|
|
||||||
drop(_make_raw);
|
|
||||||
writeln!(stdout)?;
|
|
||||||
return Err(Error::CtrlC(self.ed.to_string()));
|
|
||||||
}
|
|
||||||
// Ctrl+D: End of Transmission. Ends the current line.
|
|
||||||
'\x04' => {
|
|
||||||
drop(_make_raw);
|
|
||||||
writeln!(stdout)?;
|
|
||||||
return Err(Error::CtrlD(self.ed.to_string()));
|
|
||||||
}
|
|
||||||
// Tab: extend line by 4 spaces
|
|
||||||
'\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)?;
|
|
||||||
if self.ed.at_end() {
|
|
||||||
return Ok(self.ed.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ctrl+Backspace in my terminal
|
|
||||||
'\x17' => self.ed.erase_word(stdout)?,
|
|
||||||
// Escape sequence
|
|
||||||
'\x1b' => self.escape(stdout)?,
|
|
||||||
// backspace
|
|
||||||
'\x08' | '\x7f' => {
|
|
||||||
let ed = &mut self.ed;
|
|
||||||
if ed.ends_with(INDENT.chars()) {
|
|
||||||
for _ in 0..INDENT.len() {
|
|
||||||
ed.pop(stdout)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ed.pop(stdout)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c if c.is_ascii_control() => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.print_err(
|
|
||||||
stdout,
|
|
||||||
format_args!("\t\x1b[30mUnhandled ASCII C0 {c:?}\x1b[0m"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c => {
|
|
||||||
self.ed.push(c, stdout)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// 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<W: Write>(&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<D: std::fmt::Display, W: Write>(&mut self, disp: D, w: &mut W) -> ReplResult<()> {
|
|
||||||
self.ed.extend(format!("{disp}").chars(), w)
|
|
||||||
}
|
|
||||||
/// Handle ANSI Escape
|
|
||||||
fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
'\r' => Err(Error::EndOfInput)?,
|
|
||||||
'[' => self.csi(w)?,
|
|
||||||
'O' => todo!("Process alternate character mode"),
|
|
||||||
other => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.print_err(w, format_args!("\t\x1b[30mANSI escape: {other:?}\x1b[0m"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Handle ANSI Control Sequence Introducer
|
|
||||||
fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
'A' if self.ed.at_start() && self.hindex > 0 => {
|
|
||||||
self.hindex -= 1;
|
|
||||||
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)?;
|
|
||||||
}
|
|
||||||
'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)?? {
|
|
||||||
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.print_err(
|
|
||||||
w,
|
|
||||||
format_args!("\t\x1b[30mUnhandled control sequence: {other:?}\x1b[0m"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Restores the currently selected history
|
|
||||||
fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
let Self { history, hindex, ed, .. } = self;
|
|
||||||
if let Some(history) = history.get(*hindex) {
|
|
||||||
ed.restore(history, w)?;
|
|
||||||
ed.print_err(
|
|
||||||
format_args!("\t\x1b[30mHistory {hindex} restored!\x1b[0m"),
|
|
||||||
w,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append line to history
|
|
||||||
fn history_append(&mut self, mut buf: String) {
|
|
||||||
while buf.ends_with(char::is_whitespace) {
|
|
||||||
buf.pop();
|
|
||||||
}
|
|
||||||
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() > self.history_cap {
|
|
||||||
self.history.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user