mirror of
https://github.com/nushell/nushell.git
synced 2025-05-31 07:08:22 +02:00
feat: Use reedline for basic input implementation
This commit is contained in:
parent
f33a26123c
commit
f0d3489b79
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3704,6 +3704,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"reedline",
|
||||
"rmp",
|
||||
"roxmltree",
|
||||
"rstest",
|
||||
|
@ -85,6 +85,7 @@ quick-xml = { workspace = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
rayon = { workspace = true }
|
||||
reedline = { workspace = true }
|
||||
roxmltree = { workspace = true }
|
||||
rusqlite = { workspace = true, features = [
|
||||
"bundled",
|
||||
|
@ -1,19 +1,16 @@
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, ClearType},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use crate::platform::input::legacy_input::LegacyInput;
|
||||
use crate::platform::input::reedline_prompt::ReedlinePrompt;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use reedline::{Reedline, Signal};
|
||||
|
||||
use std::{io::Write, time::Duration};
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Input;
|
||||
|
||||
impl LegacyInput for Input {}
|
||||
|
||||
impl Command for Input {
|
||||
fn name(&self) -> &str {
|
||||
"input"
|
||||
@ -61,11 +58,11 @@ impl Command for Input {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let prompt_str: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
|
||||
let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
|
||||
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
|
||||
let numchar_flag: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||
let numchar: Spanned<i64> = numchar_flag.unwrap_or(Spanned {
|
||||
item: i64::MAX,
|
||||
span: call.head,
|
||||
});
|
||||
@ -81,8 +78,15 @@ impl Command for Input {
|
||||
});
|
||||
}
|
||||
|
||||
// Those 2 options are not supported by reedline, default to the legacy
|
||||
// implementation
|
||||
if suppress_output || bytes_until.is_some() || numchar_flag.is_some() {
|
||||
return self.legacy_input(engine_state, stack, call, _input);
|
||||
}
|
||||
|
||||
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||
if let Some(prompt) = &prompt {
|
||||
|
||||
if let Some(prompt) = &prompt_str {
|
||||
match &default_val {
|
||||
None => print!("{prompt}"),
|
||||
Some(val) => print!("{prompt} (default: {val})"),
|
||||
@ -91,82 +95,35 @@ impl Command for Input {
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
|
||||
// clear terminal events
|
||||
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
|
||||
// If there's an event, read it to remove it from the queue
|
||||
let _ = crossterm::event::read().map_err(&from_io_error)?;
|
||||
}
|
||||
let prompt = ReedlinePrompt {
|
||||
left_prompt: prompt_str.unwrap_or("".to_string()),
|
||||
indicator: "".to_string(), // TODO: Add support for custom prompt indicators
|
||||
// for now, and backwards compat, we just use the empty
|
||||
// string
|
||||
};
|
||||
let mut line_editor = Reedline::create();
|
||||
// TODO handle options
|
||||
|
||||
loop {
|
||||
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
||||
break;
|
||||
}
|
||||
match crossterm::event::read() {
|
||||
Ok(Event::Key(k)) => match k.kind {
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => {
|
||||
match k.code {
|
||||
// TODO: maintain keycode parity with existing command
|
||||
KeyCode::Char(c) => {
|
||||
if k.modifiers == KeyModifiers::ALT
|
||||
|| k.modifiers == KeyModifiers::CONTROL
|
||||
{
|
||||
if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
|
||||
crossterm::terminal::disable_raw_mode()
|
||||
.map_err(&from_io_error)?;
|
||||
return Err(IoError::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes_until) = bytes_until.as_ref() {
|
||||
if bytes_until.bytes().contains(&(c as u8)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let _ = buf.pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
match line_editor.read_line(&prompt) {
|
||||
Ok(Signal::Success(buffer)) => {
|
||||
buf.push_str(&buffer);
|
||||
break;
|
||||
}
|
||||
Ok(Signal::CtrlC) => {
|
||||
return Err(
|
||||
IoError::new(std::io::ErrorKind::Interrupted, call.head, None).into(),
|
||||
);
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(event_error) => {
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
return Err(from_io_error(event_error).into());
|
||||
}
|
||||
}
|
||||
if !suppress_output {
|
||||
// clear the current line and print the current buffer
|
||||
execute!(
|
||||
std::io::stdout(),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
cursor::MoveToColumn(0),
|
||||
)
|
||||
.map_err(|err| IoError::new(err.kind(), call.head, None))?;
|
||||
if let Some(prompt) = &prompt {
|
||||
execute!(std::io::stdout(), Print(prompt.to_string()))
|
||||
.map_err(&from_io_error)?;
|
||||
}
|
||||
execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
|
||||
}
|
||||
}
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
if !suppress_output {
|
||||
std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
|
||||
}
|
||||
match default_val {
|
||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||
|
134
crates/nu-command/src/platform/input/legacy_input.rs
Normal file
134
crates/nu-command/src/platform/input/legacy_input.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, ClearType},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
|
||||
use std::{io::Write, time::Duration};
|
||||
|
||||
pub trait LegacyInput {
|
||||
fn legacy_input(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
|
||||
let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
|
||||
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
|
||||
item: i64::MAX,
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
let from_io_error = IoError::factory(call.head, None);
|
||||
|
||||
if numchar.item < 1 {
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: "Number of characters to read has to be positive".to_string(),
|
||||
input: "value originated from here".to_string(),
|
||||
msg_span: call.head,
|
||||
input_span: numchar.span,
|
||||
});
|
||||
}
|
||||
|
||||
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||
if let Some(prompt) = &prompt {
|
||||
match &default_val {
|
||||
None => print!("{prompt}"),
|
||||
Some(val) => print!("{prompt} (default: {val})"),
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
|
||||
// clear terminal events
|
||||
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
|
||||
// If there's an event, read it to remove it from the queue
|
||||
let _ = crossterm::event::read().map_err(&from_io_error)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
||||
break;
|
||||
}
|
||||
match crossterm::event::read() {
|
||||
Ok(Event::Key(k)) => match k.kind {
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => {
|
||||
match k.code {
|
||||
// TODO: maintain keycode parity with existing command
|
||||
KeyCode::Char(c) => {
|
||||
if k.modifiers == KeyModifiers::ALT
|
||||
|| k.modifiers == KeyModifiers::CONTROL
|
||||
{
|
||||
if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
|
||||
crossterm::terminal::disable_raw_mode()
|
||||
.map_err(&from_io_error)?;
|
||||
return Err(IoError::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes_until) = bytes_until.as_ref() {
|
||||
if bytes_until.bytes().contains(&(c as u8)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let _ = buf.pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
Ok(_) => continue,
|
||||
Err(event_error) => {
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
return Err(from_io_error(event_error).into());
|
||||
}
|
||||
}
|
||||
if !suppress_output {
|
||||
// clear the current line and print the current buffer
|
||||
execute!(
|
||||
std::io::stdout(),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
cursor::MoveToColumn(0),
|
||||
)
|
||||
.map_err(|err| IoError::new(err.kind(), call.head, None))?;
|
||||
if let Some(prompt) = &prompt {
|
||||
execute!(std::io::stdout(), Print(prompt.to_string()))
|
||||
.map_err(&from_io_error)?;
|
||||
}
|
||||
execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
|
||||
}
|
||||
}
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
if !suppress_output {
|
||||
std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
|
||||
}
|
||||
match default_val {
|
||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
mod input_;
|
||||
mod input_listen;
|
||||
mod legacy_input;
|
||||
mod list;
|
||||
mod reedline_prompt;
|
||||
|
||||
pub use input_::Input;
|
||||
pub use input_listen::InputListen;
|
||||
|
61
crates/nu-command/src/platform/input/reedline_prompt.rs
Normal file
61
crates/nu-command/src/platform/input/reedline_prompt.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use reedline::{
|
||||
Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// The default prompt indicator
|
||||
pub static DEFAULT_VI_INSERT_PROMPT_INDICATOR: &str = ": ";
|
||||
pub static DEFAULT_VI_NORMAL_PROMPT_INDICATOR: &str = "〉";
|
||||
pub static DEFAULT_MULTILINE_INDICATOR: &str = "::: ";
|
||||
|
||||
/// Simple [`Prompt`] displaying a configurable left and a right prompt.
|
||||
/// For more fine-tuned configuration, implement the [`Prompt`] trait.
|
||||
/// For the default configuration, use [`DefaultPrompt::default()`]
|
||||
#[derive(Clone)]
|
||||
pub struct ReedlinePrompt {
|
||||
/// What segment should be rendered in the left (main) prompt
|
||||
pub left_prompt: String,
|
||||
pub indicator: String,
|
||||
}
|
||||
|
||||
impl Prompt for ReedlinePrompt {
|
||||
fn render_prompt_left(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.left_prompt)
|
||||
}
|
||||
|
||||
fn render_prompt_right(&self) -> Cow<str> {
|
||||
Cow::Borrowed("")
|
||||
}
|
||||
|
||||
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
|
||||
match edit_mode {
|
||||
PromptEditMode::Default | PromptEditMode::Emacs => self.indicator.as_str().into(),
|
||||
PromptEditMode::Vi(vi_mode) => match vi_mode {
|
||||
PromptViMode::Normal => DEFAULT_VI_NORMAL_PROMPT_INDICATOR.into(),
|
||||
PromptViMode::Insert => DEFAULT_VI_INSERT_PROMPT_INDICATOR.into(),
|
||||
},
|
||||
PromptEditMode::Custom(str) => format!("({str})").into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
|
||||
Cow::Borrowed(DEFAULT_MULTILINE_INDICATOR)
|
||||
}
|
||||
|
||||
fn render_prompt_history_search_indicator(
|
||||
&self,
|
||||
history_search: PromptHistorySearch,
|
||||
) -> Cow<str> {
|
||||
let prefix = match history_search.status {
|
||||
PromptHistorySearchStatus::Passing => "",
|
||||
PromptHistorySearchStatus::Failing => "failing ",
|
||||
};
|
||||
// NOTE: magic strings, given there is logic on how these compose I am not sure if it
|
||||
// is worth extracting in to static constant
|
||||
Cow::Owned(format!(
|
||||
"({}reverse-search: {}) ",
|
||||
prefix, history_search.term
|
||||
))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user