nu_command/platform/input/
input_.rs1use crossterm::{
2 cursor,
3 event::{Event, KeyCode, KeyEventKind, KeyModifiers},
4 execute,
5 style::Print,
6 terminal::{self, ClearType},
7};
8use itertools::Itertools;
9use nu_engine::command_prelude::*;
10use nu_protocol::shell_error::io::IoError;
11
12use std::{io::Write, time::Duration};
13
14#[derive(Clone)]
15pub struct Input;
16
17impl Command for Input {
18 fn name(&self) -> &str {
19 "input"
20 }
21
22 fn description(&self) -> &str {
23 "Get input from the user."
24 }
25
26 fn search_terms(&self) -> Vec<&str> {
27 vec!["prompt", "interactive"]
28 }
29
30 fn signature(&self) -> Signature {
31 Signature::build("input")
32 .input_output_types(vec![(Type::Nothing, Type::Any)])
33 .allow_variants_without_examples(true)
34 .optional("prompt", SyntaxShape::String, "Prompt to show the user.")
35 .named(
36 "bytes-until-any",
37 SyntaxShape::String,
38 "read bytes (not text) until any of the given stop bytes is seen",
39 Some('u'),
40 )
41 .named(
42 "numchar",
43 SyntaxShape::Int,
44 "number of characters to read; suppresses output",
45 Some('n'),
46 )
47 .named(
48 "default",
49 SyntaxShape::String,
50 "default value if no input is provided",
51 Some('d'),
52 )
53 .switch("suppress-output", "don't print keystroke values", Some('s'))
54 .category(Category::Platform)
55 }
56
57 fn run(
58 &self,
59 engine_state: &EngineState,
60 stack: &mut Stack,
61 call: &Call,
62 _input: PipelineData,
63 ) -> Result<PipelineData, ShellError> {
64 let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
65 let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
66 let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
67 let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
68 let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
69 item: i64::MAX,
70 span: call.head,
71 });
72
73 let from_io_error = IoError::factory(call.head, None);
74
75 if numchar.item < 1 {
76 return Err(ShellError::UnsupportedInput {
77 msg: "Number of characters to read has to be positive".to_string(),
78 input: "value originated from here".to_string(),
79 msg_span: call.head,
80 input_span: numchar.span,
81 });
82 }
83
84 let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
85 if let Some(prompt) = &prompt {
86 match &default_val {
87 None => print!("{prompt}"),
88 Some(val) => print!("{prompt} (default: {val})"),
89 }
90 let _ = std::io::stdout().flush();
91 }
92
93 let mut buf = String::new();
94
95 crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
96 while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
98 let _ = crossterm::event::read().map_err(&from_io_error)?;
100 }
101
102 loop {
103 if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
104 break;
105 }
106 match crossterm::event::read() {
107 Ok(Event::Key(k)) => match k.kind {
108 KeyEventKind::Press | KeyEventKind::Repeat => {
109 match k.code {
110 KeyCode::Char(c) => {
112 if k.modifiers == KeyModifiers::ALT
113 || k.modifiers == KeyModifiers::CONTROL
114 {
115 if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
116 crossterm::terminal::disable_raw_mode()
117 .map_err(&from_io_error)?;
118 return Err(IoError::new(
119 std::io::ErrorKind::Interrupted,
120 call.head,
121 None,
122 )
123 .into());
124 }
125 continue;
126 }
127
128 if let Some(bytes_until) = bytes_until.as_ref() {
129 if bytes_until.bytes().contains(&(c as u8)) {
130 break;
131 }
132 }
133 buf.push(c);
134 }
135 KeyCode::Backspace => {
136 let _ = buf.pop();
137 }
138 KeyCode::Enter => {
139 break;
140 }
141 _ => continue,
142 }
143 }
144 _ => continue,
145 },
146 Ok(_) => continue,
147 Err(event_error) => {
148 crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
149 return Err(from_io_error(event_error).into());
150 }
151 }
152 if !suppress_output {
153 execute!(
155 std::io::stdout(),
156 terminal::Clear(ClearType::CurrentLine),
157 cursor::MoveToColumn(0),
158 )
159 .map_err(|err| IoError::new(err.kind(), call.head, None))?;
160 if let Some(prompt) = &prompt {
161 execute!(std::io::stdout(), Print(prompt.to_string()))
162 .map_err(&from_io_error)?;
163 }
164 execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
165 }
166 }
167 crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
168 if !suppress_output {
169 std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
170 }
171 match default_val {
172 Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
173 _ => Ok(Value::string(buf, call.head).into_pipeline_data()),
174 }
175 }
176
177 fn examples(&self) -> Vec<Example> {
178 vec![
179 Example {
180 description: "Get input from the user, and assign to a variable",
181 example: "let user_input = (input)",
182 result: None,
183 },
184 Example {
185 description: "Get two characters from the user, and assign to a variable",
186 example: "let user_input = (input --numchar 2)",
187 result: None,
188 },
189 Example {
190 description: "Get input from the user with default value, and assign to a variable",
191 example: "let user_input = (input --default 10)",
192 result: None,
193 },
194 ]
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::Input;
201
202 #[test]
203 fn examples_work_as_expected() {
204 use crate::test_examples;
205 test_examples(Input {})
206 }
207}