repline: Goodbye, old friend. You have been promoted.

This commit is contained in:
2025-10-24 03:44:11 -04:00
parent 8d641d0060
commit 4e4c61ee4f
14 changed files with 3 additions and 870 deletions

View File

@@ -9,7 +9,6 @@ members = [
"compiler/cl-ast",
"compiler/cl-parser",
"compiler/cl-lexer",
"repline",
]
resolver = "2"

View File

@@ -15,4 +15,4 @@ cl-lexer = { path = "../cl-lexer" }
cl-parser = { path = "../cl-parser" }
[dev-dependencies]
repline = { path = "../../repline" }
repline = { version = "*", registry = "soft-fish" }

View File

@@ -18,5 +18,5 @@ cl-typeck = { path = "../cl-typeck" }
cl-interpret = { path = "../cl-interpret" }
cl-structures = { path = "../cl-structures" }
cl-arena = { version = "0", registry = "soft-fish" }
repline = { path = "../../repline" }
repline = { version = "*", registry = "soft-fish" }
argwerk = "0.20.4"

View File

@@ -12,6 +12,6 @@ cl-ast = { path = "../cl-ast" }
cl-structures = { path = "../cl-structures" }
[dev-dependencies]
repline = { path = "../../repline" }
repline = { version = "*", registry = "soft-fish" }
cl-lexer = { path = "../cl-lexer" }
cl-parser = { path = "../cl-parser" }

View File

@@ -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 }

View File

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

View File

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

View File

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

View File

@@ -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)
}
}

View File

@@ -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()?
}
}
}

View File

@@ -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;

View File

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

View File

@@ -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");
}
}

View File

@@ -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();
}
}
}