From 4e4c61ee4f5d1437454ea40b775e4e9d2ed975bf Mon Sep 17 00:00:00 2001 From: John Date: Fri, 24 Oct 2025 03:44:11 -0400 Subject: [PATCH] repline: Goodbye, old friend. You have been promoted. --- Cargo.toml | 1 - compiler/cl-embed/Cargo.toml | 2 +- compiler/cl-repl/Cargo.toml | 2 +- compiler/cl-typeck/Cargo.toml | 2 +- repline/Cargo.toml | 11 - repline/examples/continue.rs | 42 ---- repline/examples/repl_float.rs | 17 -- repline/src/editor.rs | 364 --------------------------------- repline/src/error.rs | 42 ---- repline/src/iter.rs | 68 ------ repline/src/lib.rs | 13 -- repline/src/prebaked.rs | 56 ----- repline/src/raw.rs | 21 -- repline/src/repline.rs | 232 --------------------- 14 files changed, 3 insertions(+), 870 deletions(-) delete mode 100644 repline/Cargo.toml delete mode 100644 repline/examples/continue.rs delete mode 100644 repline/examples/repl_float.rs delete mode 100644 repline/src/editor.rs delete mode 100644 repline/src/error.rs delete mode 100644 repline/src/iter.rs delete mode 100644 repline/src/lib.rs delete mode 100644 repline/src/prebaked.rs delete mode 100644 repline/src/raw.rs delete mode 100644 repline/src/repline.rs diff --git a/Cargo.toml b/Cargo.toml index 09c41b3..18a673d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ "compiler/cl-ast", "compiler/cl-parser", "compiler/cl-lexer", - "repline", ] resolver = "2" diff --git a/compiler/cl-embed/Cargo.toml b/compiler/cl-embed/Cargo.toml index 9e1157b..e97bfb7 100644 --- a/compiler/cl-embed/Cargo.toml +++ b/compiler/cl-embed/Cargo.toml @@ -15,4 +15,4 @@ cl-lexer = { path = "../cl-lexer" } cl-parser = { path = "../cl-parser" } [dev-dependencies] -repline = { path = "../../repline" } +repline = { version = "*", registry = "soft-fish" } diff --git a/compiler/cl-repl/Cargo.toml b/compiler/cl-repl/Cargo.toml index 9a1f5c8..b35b2fc 100644 --- a/compiler/cl-repl/Cargo.toml +++ b/compiler/cl-repl/Cargo.toml @@ -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" diff --git a/compiler/cl-typeck/Cargo.toml b/compiler/cl-typeck/Cargo.toml index b191128..799ef5b 100644 --- a/compiler/cl-typeck/Cargo.toml +++ b/compiler/cl-typeck/Cargo.toml @@ -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" } diff --git a/repline/Cargo.toml b/repline/Cargo.toml deleted file mode 100644 index 0e4f6db..0000000 --- a/repline/Cargo.toml +++ /dev/null @@ -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 } diff --git a/repline/examples/continue.rs b/repline/examples/continue.rs deleted file mode 100644 index 809042c..0000000 --- a/repline/examples/continue.rs +++ /dev/null @@ -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> { - 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/examples/repl_float.rs b/repline/examples/repl_float.rs deleted file mode 100644 index c72ea89..0000000 --- a/repline/examples/repl_float.rs +++ /dev/null @@ -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> { - read_and("\x1b[33m", " >", " ?>", |line| { - println!("-> {:?}", f64::from_str(line.trim())?); - Ok(Response::Accept) - })?; - Ok(()) -} diff --git a/repline/src/editor.rs b/repline/src/editor.rs deleted file mode 100644 index 0bbc1bf..0000000 --- a/repline/src/editor.rs +++ /dev/null @@ -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, - 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, - tail: VecDeque, - - 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 { - let Self { head, tail, .. } = self; - head.iter().chain(tail.iter()) - } - - 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)), - }?; - Ok(()) - } - - 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 { - self.putchar(*c, w)?; - } - Ok(()) - } - - 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<()> { - 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(&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(&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<()> { - 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(&mut self, w: &mut W) -> ReplResult> { - 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(&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) - } - - /// Writes characters into the editor at the location of the cursor. - pub fn extend, 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(&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(&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) -> 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(&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(&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 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 cursor_line_end(&mut self, w: &mut W) -> ReplResult<()> { - while !self.at_line_end() { - self.cursor_forward(w)? - } - Ok(()) - } - - pub fn cursor_start(&mut self, w: &mut W) -> ReplResult<()> { - while !self.at_start() { - self.cursor_back(w)? - } - Ok(()) - } - - pub fn cursor_end(&mut self, w: &mut W) -> ReplResult<()> { - while !self.at_end() { - self.cursor_forward(w)? - } - Ok(()) - } -} - -impl<'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(()) - } -} diff --git a/repline/src/error.rs b/repline/src/error.rs deleted file mode 100644 index 241254e..0000000 --- a/repline/src/error.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::iter::chars::BadUnicode; - -/// Result type for Repline -pub type ReplResult = std::result::Result; -/// 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 for Error { - fn from(value: std::io::Error) -> Self { - Self::IoFailure(value) - } -} -impl From for Error { - fn from(value: BadUnicode) -> Self { - let BadUnicode(code) = value; - Self::BadUnicode(code) - } -} diff --git a/repline/src/iter.rs b/repline/src/iter.rs deleted file mode 100644 index b57af60..0000000 --- a/repline/src/iter.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Shmancy iterator adapters - -pub use chars::Chars; -pub use flatten::Flatten; - -pub mod chars { - //! Converts an [Iterator] into an - //! [Iterator]> - - /// 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 [Iterator] into an - /// [Iterator] - #[derive(Clone, Debug)] - pub struct Chars>(pub I); - impl> Iterator for Chars { - type Item = Result; - fn next(&mut self) -> Option { - 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`](Result) or [`Option`](Option) - //! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` - - /// Flattens an [Iterator] returning [`Result`](Result) or [`Option`](Option) - /// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` - #[derive(Clone, Debug)] - pub struct Flatten>(pub I); - impl>> Iterator for Flatten, I> { - type Item = T; - fn next(&mut self) -> Option { - self.0.next()?.ok() - } - } - impl>> Iterator for Flatten, I> { - type Item = T; - fn next(&mut self) -> Option { - self.0.next()? - } - } -} diff --git a/repline/src/lib.rs b/repline/src/lib.rs deleted file mode 100644 index d779cef..0000000 --- a/repline/src/lib.rs +++ /dev/null @@ -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; diff --git a/repline/src/prebaked.rs b/repline/src/prebaked.rs deleted file mode 100644 index fba802c..0000000 --- a/repline/src/prebaked.rs +++ /dev/null @@ -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(color: &str, begin: &str, again: &str, mut f: F) -> Result<(), RlError> -where F: FnMut(&str) -> Result> { - 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(()) -} diff --git a/repline/src/raw.rs b/repline/src/raw.rs deleted file mode 100644 index 1b66d3f..0000000 --- a/repline/src/raw.rs +++ /dev/null @@ -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"); - } -} diff --git a/repline/src/repline.rs b/repline/src/repline.rs deleted file mode 100644 index 49102a9..0000000 --- a/repline/src/repline.rs +++ /dev/null @@ -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, Bytes>>, - - history_cap: usize, - history: VecDeque, // 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(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 { - 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(&self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> { - self.ed.print_err(value, w) - } - // Prints some debug info into the editor's buffer and the provided writer - pub fn put(&mut self, disp: D, w: &mut W) -> ReplResult<()> { - self.ed.extend(format!("{disp}").chars(), w) - } - /// Handle ANSI Escape - fn escape(&mut self, w: &mut W) -> ReplResult<()> { - match self.input.next().ok_or(Error::EndOfInput)?? { - '\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(&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(&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(); - } - } -}