Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rustyline Migration #235

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -25,6 +25,7 @@ completion = []

[dependencies]
console = "0.15.0"
rustyline = "10.0.0"
tempfile = { version = "3", optional = true }
zeroize = { version = "1.1.1", optional = true }
fuzzy-matcher = { version = "0.3.7", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion examples/completion.rs
@@ -1,6 +1,6 @@
use dialoguer::{theme::ColorfulTheme, Completion, Input};

fn main() -> Result<(), std::io::Error> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Use the Right arrow or Tab to complete your command");
let completion = MyCompletion::default();
Input::<String>::with_theme(&ColorfulTheme::default())
Expand Down
319 changes: 67 additions & 252 deletions src/prompts/input.rs
@@ -1,4 +1,6 @@
use std::{fmt::Debug, io, iter, str::FromStr};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use std::{fmt::{Debug, Display, Formatter, self}, io, str::FromStr, error::Error};

#[cfg(feature = "completion")]
use crate::completion::Completion;
Expand All @@ -9,7 +11,7 @@ use crate::{
validate::Validator,
};

use console::{Key, Term};
use console:: Term;

/// Renders an input prompt.
///
Expand Down Expand Up @@ -251,6 +253,49 @@ where
}
}

// create an error type that has both IO and readline
#[derive(Debug)]
pub enum InteractError {
Io(io::Error),
Readline(ReadlineError),
}

impl From<io::Error> for InteractError {
fn from(err: io::Error) -> Self {
InteractError::Io(err)
}
}

impl From<ReadlineError> for InteractError {
fn from(err: ReadlineError) -> Self {
InteractError::Readline(err)
}
}

impl Display for InteractError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
InteractError::Io(err) => std::fmt::Display::fmt(&err, f),
InteractError::Readline(err) => std::fmt::Display::fmt(&err, f),
}
}
}

impl Error for InteractError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
InteractError::Io(err) => Some(err),
InteractError::Readline(err) => Some(err),
}
}
fn cause(&self) -> Option<&dyn Error> {
match self {
InteractError::Io(err) => Some(err),
InteractError::Readline(err) => Some(err),
}
}
}

impl<T> Input<'_, T>
where
T: Clone + ToString + FromStr,
Expand All @@ -262,18 +307,18 @@ where
/// while [`interact`](#method.interact) allows virtually any character to be used e.g arrow keys.
///
/// The dialog is rendered on stderr.
pub fn interact_text(&mut self) -> io::Result<T> {
pub fn interact_text(&mut self) -> Result<T, InteractError> {
self.interact_text_on(&Term::stderr())
}

/// Like [`interact_text`](#method.interact_text) but allows a specific terminal to be set.
pub fn interact_text_on(&mut self, term: &Term) -> io::Result<T> {
pub fn interact_text_on(&mut self, term: &Term) -> Result<T, InteractError> {
let mut render = TermThemeRenderer::new(term, self.theme);

loop {
let default_string = self.default.as_ref().map(ToString::to_string);

let prompt_len = render.input_prompt(
let prompt = render.get_input_prompt(
&self.prompt,
if self.show_default {
default_string.as_deref()
Expand All @@ -287,265 +332,35 @@ where
return Ok("".to_owned().parse::<T>().unwrap());
}

let mut chars: Vec<char> = Vec::new();
let mut position = 0;
#[cfg(feature = "history")]
let mut hist_pos = 0;
let mut chars = "".to_string();

if let Some(initial) = self.initial_text.as_ref() {
term.write_str(initial)?;
chars = initial.chars().collect();
position = chars.len();
chars = initial.chars().collect::<String>();
}
term.flush()?;

loop {
match term.read_key()? {
Key::Backspace if position > 0 => {
position -= 1;
chars.remove(position);
let line_size = term.size().1 as usize;
// Case we want to delete last char of a line so the cursor is at the beginning of the next line
if (position + prompt_len) % (line_size - 1) == 0 {
term.clear_line()?;
term.move_cursor_up(1)?;
term.move_cursor_right(line_size + 1)?;
} else {
term.clear_chars(1)?;
}

let tail: String = chars[position..].iter().collect();
let mut rl = Editor::<()>::new()?;

if !tail.is_empty() {
term.write_str(&tail)?;

let total = position + prompt_len + tail.len();
let total_line = total / line_size;
let line_cursor = (position + prompt_len) / line_size;
term.move_cursor_up(total_line - line_cursor)?;

term.move_cursor_left(line_size)?;
term.move_cursor_right((position + prompt_len) % line_size)?;
}

term.flush()?;
}
Key::Char(chr) if !chr.is_ascii_control() => {
chars.insert(position, chr);
position += 1;
let tail: String =
iter::once(&chr).chain(chars[position..].iter()).collect();
term.write_str(&tail)?;
term.move_cursor_left(tail.len() - 1)?;
term.flush()?;
}
Key::ArrowLeft if position > 0 => {
if (position + prompt_len) % term.size().1 as usize == 0 {
term.move_cursor_up(1)?;
term.move_cursor_right(term.size().1 as usize)?;
} else {
term.move_cursor_left(1)?;
}
position -= 1;
term.flush()?;
}
Key::ArrowRight if position < chars.len() => {
if (position + prompt_len) % (term.size().1 as usize - 1) == 0 {
term.move_cursor_down(1)?;
term.move_cursor_left(term.size().1 as usize)?;
} else {
term.move_cursor_right(1)?;
}
position += 1;
term.flush()?;
}
Key::UnknownEscSeq(seq) if seq == vec!['b'] => {
let line_size = term.size().1 as usize;
let nb_space = chars[..position]
.iter()
.rev()
.take_while(|c| c.is_whitespace())
.count();
let find_last_space = chars[..position - nb_space]
.iter()
.rposition(|c| c.is_whitespace());

// If we find a space we set the cursor to the next char else we set it to the beginning of the input
if let Some(mut last_space) = find_last_space {
if last_space < position {
last_space += 1;
let new_line = (prompt_len + last_space) / line_size;
let old_line = (prompt_len + position) / line_size;
let diff_line = old_line - new_line;
if diff_line != 0 {
term.move_cursor_up(old_line - new_line)?;
}

let new_pos_x = (prompt_len + last_space) % line_size;
let old_pos_x = (prompt_len + position) % line_size;
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
//println!("new_pos_x = {}, old_pos_x = {}, diff = {}", new_pos_x, old_pos_x, diff_pos_x);
if diff_pos_x < 0 {
term.move_cursor_left((diff_pos_x * -1) as usize)?;
} else {
term.move_cursor_right((diff_pos_x) as usize)?;
}
position = last_space;
}
} else {
term.move_cursor_left(position)?;
position = 0;
}

term.flush()?;
}
Key::UnknownEscSeq(seq) if seq == vec!['f'] => {
let line_size = term.size().1 as usize;
let find_next_space =
chars[position..].iter().position(|c| c.is_whitespace());

// If we find a space we set the cursor to the next char else we set it to the beginning of the input
if let Some(mut next_space) = find_next_space {
let nb_space = chars[position + next_space..]
.iter()
.take_while(|c| c.is_whitespace())
.count();
next_space += nb_space;
let new_line = (prompt_len + position + next_space) / line_size;
let old_line = (prompt_len + position) / line_size;
term.move_cursor_down(new_line - old_line)?;

let new_pos_x = (prompt_len + position + next_space) % line_size;
let old_pos_x = (prompt_len + position) % line_size;
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
if diff_pos_x < 0 {
term.move_cursor_left((diff_pos_x * -1) as usize)?;
} else {
term.move_cursor_right((diff_pos_x) as usize)?;
}
position += next_space;
} else {
let new_line = (prompt_len + chars.len()) / line_size;
let old_line = (prompt_len + position) / line_size;
term.move_cursor_down(new_line - old_line)?;

let new_pos_x = (prompt_len + chars.len()) % line_size;
let old_pos_x = (prompt_len + position) % line_size;
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
if diff_pos_x < 0 {
term.move_cursor_left((diff_pos_x * -1 - 1) as usize)?;
} else if diff_pos_x > 0 {
term.move_cursor_right((diff_pos_x) as usize)?;
}
position = chars.len();
}

term.flush()?;
}
#[cfg(feature = "completion")]
Key::ArrowRight | Key::Tab => {
if let Some(completion) = &self.completion {
let input: String = chars.clone().into_iter().collect();
if let Some(x) = completion.get(&input) {
term.clear_chars(chars.len())?;
chars.clear();
position = 0;
for ch in x.chars() {
chars.insert(position, ch);
position += 1;
}
term.write_str(&x)?;
term.flush()?;
}
}
}
#[cfg(feature = "history")]
Key::ArrowUp => {
let line_size = term.size().1 as usize;
if let Some(history) = &self.history {
if let Some(previous) = history.read(hist_pos) {
hist_pos += 1;
let mut chars_len = chars.len();
while ((prompt_len + chars_len) / line_size) > 0 {
term.clear_chars(chars_len)?;
if (prompt_len + chars_len) % line_size == 0 {
chars_len -= std::cmp::min(chars_len, line_size);
} else {
chars_len -= std::cmp::min(
chars_len,
(prompt_len + chars_len + 1) % line_size,
);
}
if chars_len > 0 {
term.move_cursor_up(1)?;
term.move_cursor_right(line_size)?;
}
}
term.clear_chars(chars_len)?;
chars.clear();
position = 0;
for ch in previous.chars() {
chars.insert(position, ch);
position += 1;
}
term.write_str(&previous)?;
term.flush()?;
}
}
}
#[cfg(feature = "history")]
Key::ArrowDown => {
let line_size = term.size().1 as usize;
if let Some(history) = &self.history {
let mut chars_len = chars.len();
while ((prompt_len + chars_len) / line_size) > 0 {
term.clear_chars(chars_len)?;
if (prompt_len + chars_len) % line_size == 0 {
chars_len -= std::cmp::min(chars_len, line_size);
} else {
chars_len -= std::cmp::min(
chars_len,
(prompt_len + chars_len + 1) % line_size,
);
}
if chars_len > 0 {
term.move_cursor_up(1)?;
term.move_cursor_right(line_size)?;
}
}
term.clear_chars(chars_len)?;
chars.clear();
position = 0;
// Move the history position back one in case we have up arrowed into it
// and the position is sitting on the next to read
if let Some(pos) = hist_pos.checked_sub(1) {
hist_pos = pos;
// Move it back again to get the previous history entry
if let Some(pos) = pos.checked_sub(1) {
if let Some(previous) = history.read(pos) {
for ch in previous.chars() {
chars.insert(position, ch);
position += 1;
}
term.write_str(&previous)?;
}
}
}
term.flush()?;
}
loop {
let readline = rl.readline(&prompt);
match readline {
Ok(line) => {
rl.add_history_entry(line.as_str());
chars = line.clone();
break;
}
Key::Enter => break,
Key::Unknown => {
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"Not a terminal",
))
Err(ReadlineError::Interrupted) => break,
Err(ReadlineError::Eof) => break,
Err(err) => {
println!("Error: {:?}", err);
break;
}
_ => (),
}
}
let input = chars.iter().collect::<String>();
let input = chars.clone();

term.move_cursor_up(1)?;
term.clear_line()?;
render.clear()?;

Expand Down