Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ Cargo.lock
# Testing
examples/tester.rs
output
*~
/.gdb_history
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
79 changes: 64 additions & 15 deletions src/prompt/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::Outcome;
use super::{RuclineWriter, Writer};

use crate::actions::{Action, Event, Overrider};
use crate::completion::{Completer, Suggester};
Expand Down Expand Up @@ -315,37 +316,51 @@ pub trait ChainedLineReader {
///
/// [`Builder`]: trait.Builder.html
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Prompt {
prompt: Option<String>,
pub struct Prompt<W> {
buffer: Option<Buffer>,
writer: Option<W>,
prompt: Option<String>,
erase_after_read: bool,
}

impl Prompt {
impl<W: Writer> Prompt<W> {
/// 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<W: Writer> Default for Prompt<W> {
fn default() -> Self {
Self::new()
}
}

impl<S: ToString> std::convert::From<S> for Prompt {
impl<S: ToString> std::convert::From<S> for Prompt<RuclineWriter<'_>> {
fn from(s: S) -> Self {
Self {
buffer: None,
writer: None,
prompt: Some(s.to_string()),
erase_after_read: false,
}
}
}

impl<W: Writer> std::convert::From<W> for Prompt<W> {
fn from(w: W) -> Self {
Self {
buffer: None,
writer: Some(w),
prompt: None,
erase_after_read: false,
}
}
Expand Down Expand Up @@ -405,7 +420,20 @@ where
suggester: &'s S,
}

impl Builder for Prompt {
impl Builder for Prompt<RuclineWriter<'_>> {
fn read_line(self) -> Result<Outcome, Error> {
let mut writer = RuclineWriter::new(self.erase_after_read, self.prompt.as_deref());
super::read_line::<Dummy, Dummy, Dummy, RuclineWriter<'_>>(
self.buffer,
None,
None,
None,
&mut writer,
)
}
}

impl<W: Writer> Builder for Prompt<W> {
fn buffer(mut self, buffer: Buffer) -> Self {
self.buffer = Some(buffer);
self
Expand All @@ -418,14 +446,13 @@ impl Builder for Prompt {

impl_builder!(extensions);

fn read_line(self) -> Result<Outcome, Error> {
super::read_line::<Dummy, Dummy, Dummy>(
self.prompt.as_deref(),
default fn read_line(self) -> Result<Outcome, Error> {
super::read_line::<Dummy, Dummy, Dummy, W>(
self.buffer,
self.erase_after_read,
None,
None,
None,
&mut self.writer.unwrap(),
)
}
}
Expand Down Expand Up @@ -514,8 +541,30 @@ where
}
}

impl ChainedLineReader for Prompt {
fn chain_read_line<O, C, S>(
impl<W: Writer> ChainedLineReader for Prompt<W> {
default fn chain_read_line<O, C, S>(
self,
overrider: Option<&O>,
completer: Option<&C>,
suggester: Option<&S>,
) -> Result<Outcome, Error>
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<RuclineWriter<'_>> {
default fn chain_read_line<O, C, S>(
self,
overrider: Option<&O>,
completer: Option<&C>,
Expand All @@ -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,
)
}
}
Expand Down
20 changes: 12 additions & 8 deletions src/prompt/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,35 @@ 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<std::borrow::Cow<'c, str>>,
suggester: Option<&'s S>,
suggestions: Option<Suggestions<'s>>,
}

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<Buffer>,
completer: Option<&'c C>,
suggester: Option<&'s S>,
) -> Result<Self, Error> {
writer.begin()?;
Ok(Self {
writer: Writer::new(erase_on_drop, prompt)?,
writer: writer,
buffer: buffer.unwrap_or_else(Buffer::new),
completer,
completion: None,
Expand Down Expand Up @@ -129,20 +131,22 @@ where
}
}

impl<C, S> std::convert::Into<Buffer> for Context<'_, '_, C, S>
impl<C, S, W> std::convert::Into<Buffer> for Context<'_, '_, '_, C, S, W>
where
C: Completer + ?Sized,
S: Suggester + ?Sized,
W: Writer + ?Sized,
{
fn into(self) -> Buffer {
self.buffer
}
}

impl<C, S> std::ops::Deref for Context<'_, '_, C, S>
impl<C, S, W> 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 {
Expand Down
14 changes: 7 additions & 7 deletions src/prompt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -145,22 +146,21 @@ impl Outcome {
/// [`Outcome`]: enum.Outcome.html
/// [`Prompt`]: struct.Prompt.html
/// [`buffer`]: ../buffer/struct.Buffer.html
pub fn read_line<O, C, S>(
prompt: Option<&str>,
pub fn read_line<O, C, S, W>(
buffer: Option<Buffer>,
erase_after_read: bool,
overrider: Option<&O>,
completer: Option<&C>,
suggester: Option<&S>,
writer: &mut W,
) -> Result<Outcome, crate::Error>
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,
Expand Down
53 changes: 38 additions & 15 deletions src/prompt/writer.rs
Original file line number Diff line number Diff line change
@@ -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<usize>,
printed_length: usize,
cursor_offset: usize,
}

impl Writer {
pub(super) fn new(erase_on_drop: bool, prompt: Option<&str>) -> Result<Self, Error> {
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())
Expand All @@ -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;

Expand Down Expand Up @@ -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>],
Expand Down Expand Up @@ -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) {
Expand Down