mirror of
https://github.com/nushell/nushell.git
synced 2025-06-01 23:55:50 +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 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"reedline",
|
||||||
"rmp",
|
"rmp",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"rstest",
|
"rstest",
|
||||||
|
@ -85,6 +85,7 @@ quick-xml = { workspace = true }
|
|||||||
rand = { workspace = true, optional = true }
|
rand = { workspace = true, optional = true }
|
||||||
getrandom = { workspace = true, optional = true }
|
getrandom = { workspace = true, optional = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
|
reedline = { workspace = true }
|
||||||
roxmltree = { workspace = true }
|
roxmltree = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = [
|
rusqlite = { workspace = true, features = [
|
||||||
"bundled",
|
"bundled",
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
use crossterm::{
|
use crate::platform::input::legacy_input::LegacyInput;
|
||||||
cursor,
|
use crate::platform::input::reedline_prompt::ReedlinePrompt;
|
||||||
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
|
|
||||||
execute,
|
|
||||||
style::Print,
|
|
||||||
terminal::{self, ClearType},
|
|
||||||
};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::shell_error::io::IoError;
|
use nu_protocol::shell_error::io::IoError;
|
||||||
|
use reedline::{Reedline, Signal};
|
||||||
|
|
||||||
use std::{io::Write, time::Duration};
|
use std::io::Write;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Input;
|
pub struct Input;
|
||||||
|
|
||||||
|
impl LegacyInput for Input {}
|
||||||
|
|
||||||
impl Command for Input {
|
impl Command for Input {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"input"
|
"input"
|
||||||
@ -61,11 +58,11 @@ impl Command for Input {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> 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 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 suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
|
||||||
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
let numchar_flag: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||||
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
|
let numchar: Spanned<i64> = numchar_flag.unwrap_or(Spanned {
|
||||||
item: i64::MAX,
|
item: i64::MAX,
|
||||||
span: call.head,
|
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")?;
|
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 {
|
match &default_val {
|
||||||
None => print!("{prompt}"),
|
None => print!("{prompt}"),
|
||||||
Some(val) => print!("{prompt} (default: {val})"),
|
Some(val) => print!("{prompt} (default: {val})"),
|
||||||
@ -91,82 +95,35 @@ impl Command for Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
let prompt = ReedlinePrompt {
|
||||||
crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
|
left_prompt: prompt_str.unwrap_or("".to_string()),
|
||||||
// clear terminal events
|
indicator: "".to_string(), // TODO: Add support for custom prompt indicators
|
||||||
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
|
// for now, and backwards compat, we just use the empty
|
||||||
// If there's an event, read it to remove it from the queue
|
// string
|
||||||
let _ = crossterm::event::read().map_err(&from_io_error)?;
|
};
|
||||||
}
|
let mut line_editor = Reedline::create();
|
||||||
|
// TODO handle options
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match crossterm::event::read() {
|
match line_editor.read_line(&prompt) {
|
||||||
Ok(Event::Key(k)) => match k.kind {
|
Ok(Signal::Success(buffer)) => {
|
||||||
KeyEventKind::Press | KeyEventKind::Repeat => {
|
buf.push_str(&buffer);
|
||||||
match k.code {
|
break;
|
||||||
// TODO: maintain keycode parity with existing command
|
}
|
||||||
KeyCode::Char(c) => {
|
Ok(Signal::CtrlC) => {
|
||||||
if k.modifiers == KeyModifiers::ALT
|
return Err(
|
||||||
|| k.modifiers == KeyModifiers::CONTROL
|
IoError::new(std::io::ErrorKind::Interrupted, call.head, None).into(),
|
||||||
{
|
);
|
||||||
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,
|
Ok(_) => continue,
|
||||||
Err(event_error) => {
|
Err(event_error) => {
|
||||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||||
return Err(from_io_error(event_error).into());
|
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 {
|
match default_val {
|
||||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
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_;
|
||||||
mod input_listen;
|
mod input_listen;
|
||||||
|
mod legacy_input;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod reedline_prompt;
|
||||||
|
|
||||||
pub use input_::Input;
|
pub use input_::Input;
|
||||||
pub use input_listen::InputListen;
|
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