diff --git a/.gitignore b/.gitignore index 6ce46b1..3a94ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ Cargo.lock # Testing examples/tester.rs output +*~ +/.gdb_history diff --git a/src/lib.rs b/src/lib.rs index aa75734..51e4e24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![deny(warnings, missing_docs, clippy::pedantic, clippy::all)] #![warn(rust_2018_idioms)] +#![feature(min_specialization)] //! Rucline, the Rust CLI Line reader, or simply "recline", is a cross-platform, UTF-8 compatible //! line reader that provides hooks for autocompletion and tab suggestion. It supports advanced diff --git a/src/prompt/builder.rs b/src/prompt/builder.rs index 7ac8528..fbd0369 100644 --- a/src/prompt/builder.rs +++ b/src/prompt/builder.rs @@ -1,4 +1,5 @@ use super::Outcome; +use super::{RuclineWriter, Writer}; use crate::actions::{Action, Event, Overrider}; use crate::completion::{Completer, Suggester}; @@ -315,37 +316,51 @@ pub trait ChainedLineReader { /// /// [`Builder`]: trait.Builder.html #[derive(Debug, Clone, Eq, PartialEq)] -pub struct Prompt { - prompt: Option, +pub struct Prompt { buffer: Option, + writer: Option, + prompt: Option, erase_after_read: bool, } -impl Prompt { +impl Prompt { /// Creates a new [`Prompt`] with no prompt text. Equivalent to calling `Prompt::default()` /// /// [`Prompt`]: struct.Prompt.html #[must_use] pub fn new() -> Self { Self { + writer: None, prompt: None, - buffer: None, erase_after_read: false, + buffer: None, } } } -impl Default for Prompt { +impl Default for Prompt { fn default() -> Self { Self::new() } } -impl std::convert::From for Prompt { +impl std::convert::From for Prompt> { fn from(s: S) -> Self { Self { + buffer: None, + writer: None, prompt: Some(s.to_string()), + erase_after_read: false, + } + } +} + +impl std::convert::From for Prompt { + fn from(w: W) -> Self { + Self { buffer: None, + writer: Some(w), + prompt: None, erase_after_read: false, } } @@ -405,7 +420,20 @@ where suggester: &'s S, } -impl Builder for Prompt { +impl Builder for Prompt> { + fn read_line(self) -> Result { + let mut writer = RuclineWriter::new(self.erase_after_read, self.prompt.as_deref()); + super::read_line::>( + self.buffer, + None, + None, + None, + &mut writer, + ) + } +} + +impl Builder for Prompt { fn buffer(mut self, buffer: Buffer) -> Self { self.buffer = Some(buffer); self @@ -418,14 +446,13 @@ impl Builder for Prompt { impl_builder!(extensions); - fn read_line(self) -> Result { - super::read_line::( - self.prompt.as_deref(), + default fn read_line(self) -> Result { + super::read_line::( self.buffer, - self.erase_after_read, None, None, None, + &mut self.writer.unwrap(), ) } } @@ -514,8 +541,30 @@ where } } -impl ChainedLineReader for Prompt { - fn chain_read_line( +impl ChainedLineReader for Prompt { + default fn chain_read_line( + self, + overrider: Option<&O>, + completer: Option<&C>, + suggester: Option<&S>, + ) -> Result + where + O: Overrider + ?Sized, + C: Completer + ?Sized, + S: Suggester + ?Sized, + { + super::read_line( + self.buffer, + overrider, + completer, + suggester, + &mut self.writer.unwrap(), + ) + } +} + +impl ChainedLineReader for Prompt> { + default fn chain_read_line( self, overrider: Option<&O>, completer: Option<&C>, @@ -526,13 +575,13 @@ impl ChainedLineReader for Prompt { C: Completer + ?Sized, S: Suggester + ?Sized, { + let mut writer = RuclineWriter::new(self.erase_after_read, self.prompt.as_deref()); super::read_line( - self.prompt.as_deref(), self.buffer, - self.erase_after_read, overrider, completer, suggester, + &mut writer, ) } } diff --git a/src/prompt/context.rs b/src/prompt/context.rs index eaf7552..5bcd75b 100644 --- a/src/prompt/context.rs +++ b/src/prompt/context.rs @@ -2,12 +2,13 @@ use super::{Buffer, Completer, Direction, Range, Scope, Suggester, Writer}; use crate::Error; -pub(super) struct Context<'c, 's, C, S> +pub(super) struct Context<'c, 's, 'w, C, S, W> where C: Completer + ?Sized, S: Suggester + ?Sized, + W: Writer + ?Sized, { - writer: Writer, + writer: &'w mut W, buffer: Buffer, completer: Option<&'c C>, completion: Option>, @@ -15,20 +16,21 @@ where suggestions: Option>, } -impl<'c, 's, C, S> Context<'c, 's, C, S> +impl<'c, 's, 'w, C, S, W> Context<'c, 's, 'w, C, S, W> where C: Completer + ?Sized, S: Suggester + ?Sized, + W: Writer + ?Sized, { pub(super) fn new( - erase_on_drop: bool, - prompt: Option<&str>, + writer: &'w mut W, buffer: Option, completer: Option<&'c C>, suggester: Option<&'s S>, ) -> Result { + writer.begin()?; Ok(Self { - writer: Writer::new(erase_on_drop, prompt)?, + writer: writer, buffer: buffer.unwrap_or_else(Buffer::new), completer, completion: None, @@ -129,20 +131,22 @@ where } } -impl std::convert::Into for Context<'_, '_, C, S> +impl std::convert::Into for Context<'_, '_, '_, C, S, W> where C: Completer + ?Sized, S: Suggester + ?Sized, + W: Writer + ?Sized, { fn into(self) -> Buffer { self.buffer } } -impl std::ops::Deref for Context<'_, '_, C, S> +impl std::ops::Deref for Context<'_, '_, '_, C, S, W> where C: Completer + ?Sized, S: Suggester + ?Sized, + W: Writer + ?Sized, { type Target = Buffer; fn deref(&self) -> &Self::Target { diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 4d94eba..0829970 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -32,10 +32,11 @@ mod builder; mod context; -mod writer; + +pub mod writer; use context::Context; -use writer::Writer; +use writer::{RuclineWriter, Writer}; use crate::actions::{action_for, Action, Direction, Overrider, Range, Scope}; use crate::completion::{Completer, Suggester}; @@ -145,22 +146,21 @@ impl Outcome { /// [`Outcome`]: enum.Outcome.html /// [`Prompt`]: struct.Prompt.html /// [`buffer`]: ../buffer/struct.Buffer.html -pub fn read_line( - prompt: Option<&str>, +pub fn read_line( buffer: Option, - erase_after_read: bool, overrider: Option<&O>, completer: Option<&C>, suggester: Option<&S>, + writer: &mut W, ) -> Result where O: Overrider + ?Sized, C: Completer + ?Sized, S: Suggester + ?Sized, + W: Writer + ?Sized, { let mut context = Context::new( - erase_after_read, - prompt.as_deref(), + writer, buffer, completer, suggester, diff --git a/src/prompt/writer.rs b/src/prompt/writer.rs index d03fee3..da23bc5 100644 --- a/src/prompt/writer.rs +++ b/src/prompt/writer.rs @@ -1,24 +1,20 @@ +//! Provides a trait for displaying the REPL to the user. use super::Buffer; use crate::Error; // TODO: Keep track of lines // TODO: Deal with colors -pub(super) struct Writer { +/// Displays the REPL on stdout in Rucline’s default style. +pub struct RuclineWriter<'a> { + prompt: Option<&'a str>, erase_on_drop: Option, printed_length: usize, cursor_offset: usize, } -impl Writer { - pub(super) fn new(erase_on_drop: bool, prompt: Option<&str>) -> Result { - crossterm::terminal::enable_raw_mode()?; - if let Some(prompt) = prompt { - use std::io::Write; - - crossterm::queue!(std::io::stdout(), crossterm::style::Print(prompt))?; - } - +impl<'a> RuclineWriter<'a> { + pub(crate) fn new(erase_on_drop: bool, prompt: Option<&'a str>) -> Self { let erase_on_drop = if erase_on_drop { prompt .map(|s| unicode_segmentation::UnicodeSegmentation::graphemes(s, true).count()) @@ -27,14 +23,41 @@ impl Writer { None }; - Ok(Self { + Self { + prompt, erase_on_drop, printed_length: 0, cursor_offset: 0, - }) + } + } +} + +/// Implement this to provide your own display style. +pub trait Writer { + /// Print the prompt, leaving the cursor in position to receive user input. + fn begin(&mut self) -> Result<(), Error>; + /// Print the user input, followed by the completion (if any). + fn print(&mut self, buffer: &Buffer, completion: Option<&str>) -> Result<(), Error>; + /// Print the list of suggestions. + fn print_suggestions( + &mut self, + selected_index: usize, + suggestions: &[std::borrow::Cow<'_, str>], + ) -> Result<(), Error>; +} + +impl<'a> Writer for RuclineWriter<'a> { + fn begin(&mut self) -> Result<(), Error> { + crossterm::terminal::enable_raw_mode()?; + if let Some(prompt) = self.prompt { + use std::io::Write; + + crossterm::queue!(std::io::stdout(), crossterm::style::Print(prompt))?; + } + Ok(()) } - pub(super) fn print(&mut self, buffer: &Buffer, completion: Option<&str>) -> Result<(), Error> { + fn print(&mut self, buffer: &Buffer, completion: Option<&str>) -> Result<(), Error> { use std::io::Write; use unicode_segmentation::UnicodeSegmentation; @@ -63,7 +86,7 @@ impl Writer { crossterm::execute!(&mut stdout) } - pub(super) fn print_suggestions( + fn print_suggestions( &mut self, selected_index: usize, suggestions: &[std::borrow::Cow<'_, str>], @@ -173,7 +196,7 @@ fn fast_forward_cursor(stdout: &mut std::io::Stdout, amount: usize) -> Result<() crossterm::queue!(stdout, crossterm::cursor::MoveRight(remaining as u16)) } -impl std::ops::Drop for Writer { +impl<'a> std::ops::Drop for RuclineWriter<'a> { // Allowed because this is a drop and the previous construction already managed the get through #[allow(unused_must_use)] fn drop(&mut self) {