diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index e68b6dc79b..bd2543b663 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -166,6 +166,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { AnsiGradient, AnsiStrip, Clear, + Input, Kill, Sleep, }; diff --git a/crates/nu-command/src/platform/input.rs b/crates/nu-command/src/platform/input.rs new file mode 100644 index 0000000000..9a36031a69 --- /dev/null +++ b/crates/nu-command/src/platform/input.rs @@ -0,0 +1,119 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; +use std::io::{Read, Write}; + +#[derive(Clone)] +pub struct Input; + +impl Command for Input { + fn name(&self) -> &str { + "input" + } + + fn usage(&self) -> &str { + "Get input from the user." + } + + fn signature(&self) -> Signature { + Signature::build("input") + .optional("prompt", SyntaxShape::String, "prompt to show the user") + .named( + "bytes-until", + SyntaxShape::String, + "read bytes (not text) until a stop byte", + Some('u'), + ) + .category(Category::Platform) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let prompt: Option = call.opt(engine_state, stack, 0)?; + let bytes_until: Option = call.get_flag(engine_state, stack, "bytes-until")?; + + 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(); + } + 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 buf[0] == c { + let _ = crossterm::terminal::disable_raw_mode(); + break; + } + } + + Ok(Value::Binary { + val: buffer, + span: call.head, + } + .into_pipeline_data()) + } else { + let _ = crossterm::terminal::disable_raw_mode(); + 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(); + } + + // Just read a normal line of text + let mut buf = String::new(); + let input = std::io::stdin().read_line(&mut buf); + + match input { + Ok(_) => Ok(Value::String { + val: buf, + span: call.head, + } + .into_pipeline_data()), + Err(err) => Err(ShellError::IOError(err.to_string())), + } + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get input from the user, and assign to a variable", + example: "let user-input = (input)", + result: None, + }] + } +} + +#[cfg(test)] +mod tests { + use super::Input; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(Input {}) + } +} diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index 65836f079d..45d2d87c66 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -1,9 +1,11 @@ mod ansi; mod clear; +mod input; mod kill; mod sleep; pub use ansi::{Ansi, AnsiGradient, AnsiStrip}; pub use clear::Clear; +pub use input::Input; pub use kill::Kill; pub use sleep::Sleep; diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 90bc7d0f43..b454d383b9 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -78,11 +78,15 @@ impl Command for Table { )), PipelineData::Value(Value::Binary { val, .. }, ..) => Ok(PipelineData::StringStream( StringStream::from_stream( - vec![Ok(if val.iter().all(|x| x.is_ascii()) { - format!("{}", String::from_utf8_lossy(&val)) - } else { - format!("{}\n", nu_pretty_hex::pretty_hex(&val)) - })] + vec![Ok( + if val.iter().all(|x| { + *x < 128 && (*x >= b' ' || *x == b'\t' || *x == b'\r' || *x == b'\n') + }) { + format!("{}", String::from_utf8_lossy(&val)) + } else { + format!("{}\n", nu_pretty_hex::pretty_hex(&val)) + }, + )] .into_iter(), ctrlc, ),