From e9d4730099ba2ee78c8de476f17c8ea92d14b281 Mon Sep 17 00:00:00 2001 From: Yuto <39439017+utouto97@users.noreply.github.com> Date: Sun, 3 Sep 2023 04:09:26 +0900 Subject: [PATCH] refactor input command (#10150) close #8074 I attempted to refactor the "input" command. The reason for this is that the current implementation of the "input" command lacks consistency for different options. For instance, some parts use `std::io::stdin` while others use `crossterm::event::read`. In this pull request, I have made changes to use crossterm consistently: - Detection of the -u option is now done using `crossterm`'s `KeyCode::Char`. - The current input is displayed when using `crossterm` for input (it won't be displayed when -s is present). - Ctrl-C triggers SIGINT. # User-Facing Changes Users can interrupt "input" with ctrl-c. --- .../nu-command/src/platform/input/input_.rs | 197 +++++++----------- 1 file changed, 81 insertions(+), 116 deletions(-) diff --git a/crates/nu-command/src/platform/input/input_.rs b/crates/nu-command/src/platform/input/input_.rs index 170ecf34b..8a8ef50ef 100644 --- a/crates/nu-command/src/platform/input/input_.rs +++ b/crates/nu-command/src/platform/input/input_.rs @@ -1,4 +1,10 @@ -use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; +use crossterm::{ + cursor, + event::{Event, KeyCode, KeyEventKind, KeyModifiers}, + execute, + style::Print, + terminal::{self, ClearType}, +}; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; @@ -6,7 +12,7 @@ use nu_protocol::{ Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, }; -use std::io::{Read, Write}; +use std::io::Write; use std::time::Duration; #[derive(Clone)] @@ -60,7 +66,6 @@ impl Command for Input { let bytes_until: Option = call.get_flag(engine_state, stack, "bytes-until")?; let suppress_output = call.has_flag("suppress-output"); let numchar: Option> = call.get_flag(engine_state, stack, "numchar")?; - let numchar_exists = numchar.is_some(); let numchar: Spanned = numchar.unwrap_or(Spanned { item: i64::MAX, span: call.head, @@ -75,139 +80,99 @@ impl Command for Input { )); } - if let Some(bytes_until) = bytes_until { - let _ = crossterm::terminal::enable_raw_mode(); - - if let Some(prompt) = prompt { - print!("{prompt}"); - let _ = std::io::stdout().flush(); - } + let byte_until = if let Some(bytes_until) = bytes_until { if let Some(c) = bytes_until.bytes().next() { - let mut buf = [0u8; 1]; - let mut buffer = vec![]; - - let mut stdin = std::io::stdin(); - - loop { - if let Err(err) = stdin.read_exact(&mut buf) { - let _ = crossterm::terminal::disable_raw_mode(); - return Err(ShellError::IOError(err.to_string())); - } - buffer.push(buf[0]); - - if i64::try_from(buffer.len()).unwrap_or(0) >= numchar.item { - let _ = crossterm::terminal::disable_raw_mode(); - break; - } - - // 03 symbolizes SIGINT/Ctrl+C - if buf.contains(&3) { - let _ = crossterm::terminal::disable_raw_mode(); - return Err(ShellError::IOError("SIGINT".to_string())); - } - - if buf[0] == c { - let _ = crossterm::terminal::disable_raw_mode(); - break; - } - } - - Ok(Value::Binary { - val: buffer, - span: call.head, - } - .into_pipeline_data()) + Some(c) } else { let _ = crossterm::terminal::disable_raw_mode(); - Err(ShellError::IOError( + return Err(ShellError::IOError( "input can't stop on this byte".to_string(), - )) + )); } } else { - if let Some(prompt) = prompt { - print!("{prompt}"); - let _ = std::io::stdout().flush(); + None + }; + + if let Some(prompt) = &prompt { + print!("{prompt}"); + let _ = std::io::stdout().flush(); + } + + let mut buf = String::new(); + + crossterm::terminal::enable_raw_mode()?; + // clear terminal events + while crossterm::event::poll(Duration::from_secs(0))? { + // If there's an event, read it to remove it from the queue + let _ = crossterm::event::read()?; + } + + loop { + if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item { + break; } - - let mut buf = String::new(); - - if suppress_output || numchar_exists { - crossterm::terminal::enable_raw_mode()?; - // clear terminal events - while crossterm::event::poll(Duration::from_secs(0))? { - // If there's an event, read it to remove it from the queue - let _ = crossterm::event::read()?; - } - - loop { - if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item { - let _ = crossterm::terminal::disable_raw_mode(); - 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()?; - return Err(ShellError::IOError( - "SIGINT".to_string(), - )); - } - continue; - } - - buf.push(c); + 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()?; + return Err(ShellError::IOError("SIGINT".to_string())); } - KeyCode::Backspace => { - let _ = buf.pop(); - } - KeyCode::Enter => { + continue; + } + + if let Some(byte_until) = byte_until { + if c == byte_until as char { break; } - _ => continue, } + buf.push(c); + } + KeyCode::Backspace => { + let _ = buf.pop(); + } + KeyCode::Enter => { + break; } _ => continue, - }, - Ok(_) => continue, - Err(event_error) => { - crossterm::terminal::disable_raw_mode()?; - return Err(event_error.into()); } } - } - crossterm::terminal::disable_raw_mode()?; - return Ok(Value::String { - val: buf, - span: call.head, - } - .into_pipeline_data()); - } - - // Just read a normal line of text, and trim the newline at the end - let input = std::io::stdin().read_line(&mut buf); - if buf.ends_with('\n') { - buf.pop(); - if buf.ends_with('\r') { - buf.pop(); + _ => continue, + }, + Ok(_) => continue, + Err(event_error) => { + crossterm::terminal::disable_raw_mode()?; + return Err(event_error.into()); } } - - match input { - Ok(_) => Ok(Value::String { - val: buf, - span: call.head, + if !suppress_output { + // clear the current line and print the current buffer + execute!( + std::io::stdout(), + terminal::Clear(ClearType::CurrentLine), + cursor::MoveToColumn(0), + )?; + if let Some(prompt) = &prompt { + execute!(std::io::stdout(), Print(prompt.to_string()))?; } - .into_pipeline_data()), - Err(err) => Err(ShellError::IOError(err.to_string())), + execute!(std::io::stdout(), Print(buf.to_string()))?; } } + crossterm::terminal::disable_raw_mode()?; + if !suppress_output { + std::io::stdout().write_all(b"\n")?; + } + Ok(Value::String { + val: buf, + span: call.head, + } + .into_pipeline_data()) } fn examples(&self) -> Vec {