Skip to main content

nu_command/platform/input/
input_.rs

1use crate::platform::input::legacy_input::LegacyInput;
2use crate::platform::input::reedline_prompt::ReedlinePrompt;
3use nu_engine::command_prelude::*;
4use nu_protocol::shell_error::{self, io::IoError};
5use reedline::{
6    EditCommand, FileBackedHistory, HISTORY_SIZE, History, HistoryItem, Reedline, Signal,
7};
8
9#[derive(Clone)]
10pub struct Input;
11
12impl LegacyInput for Input {}
13
14impl Command for Input {
15    fn name(&self) -> &str {
16        "input"
17    }
18
19    fn description(&self) -> &str {
20        "Get input from the user via the terminal."
21    }
22
23    fn search_terms(&self) -> Vec<&str> {
24        vec!["prompt", "interactive"]
25    }
26
27    fn signature(&self) -> Signature {
28        Signature::build("input")
29            .input_output_types(vec![
30                (Type::Nothing, Type::Any),
31                (Type::List(Box::new(Type::String)), Type::Any)])
32            .allow_variants_without_examples(true)
33            .optional("prompt", SyntaxShape::String, "Prompt to show the user.")
34            .named(
35                "bytes-until-any",
36                SyntaxShape::String,
37                "Read bytes (not text) until any of the given stop bytes is seen.",
38                Some('u'),
39            )
40            .named(
41                "numchar",
42                SyntaxShape::Int,
43                "Number of characters to read; suppresses output.",
44                Some('n'),
45            )
46            .named(
47                "default",
48                SyntaxShape::String,
49                "Default value if no input is provided.",
50                Some('d'),
51            )
52            .switch(
53                "reedline",
54                "Use the reedline library, defaults to false.",
55                None
56            )
57            .named(
58                "history-file",
59                SyntaxShape::Filepath,
60                "Path to a file to read and write command history. This is a text file and will be created if it doesn't exist. Will be used as the selection list. Implies `--reedline`.",
61                None,
62            )
63            .named(
64                "max-history",
65                SyntaxShape::Int,
66                "The maximum number of entries to keep in the history, defaults to $env.config.history.max_size. Implies `--reedline`.",
67                None,
68            )
69            .switch("suppress-output", "Don't print keystroke values.", Some('s'))
70            .category(Category::Platform)
71    }
72
73    fn run(
74        &self,
75        engine_state: &EngineState,
76        stack: &mut Stack,
77        call: &Call,
78        input: PipelineData,
79    ) -> Result<PipelineData, ShellError> {
80        // Check if we should use the legacy implementation or the reedline implementation
81        let use_reedline = [
82            // reedline is not set - use legacy implementation
83            call.has_flag(engine_state, stack, "reedline")?,
84            // We have the history-file or max-history flags set to None
85            call.get_flag::<String>(engine_state, stack, "history-file")?
86                .is_some(),
87            call.get_flag::<i64>(engine_state, stack, "max-history")?
88                .is_some(),
89        ]
90        .iter()
91        .any(|x| *x);
92
93        if !use_reedline {
94            // `legacy_input` guards the terminal itself via `RawModeGuard`.
95            return self.legacy_input(engine_state, stack, call, input);
96        }
97
98        // Reedline grabs the terminal itself, so check the precondition here.
99        stack.require_stdin(call.head)?;
100
101        let prompt_str: Option<String> = call.opt(engine_state, stack, 0)?;
102        let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
103        let history_file_val: Option<String> =
104            call.get_flag(engine_state, stack, "history-file")?;
105        let max_history: usize = call
106            .get_flag::<i64>(engine_state, stack, "max-history")?
107            .map(|l| if l < 0 { 0 } else { l as usize })
108            .unwrap_or(HISTORY_SIZE);
109        let max_history_span = call.get_flag_span(stack, "max-history");
110        let history_file_span = call.get_flag_span(stack, "history-file");
111
112        let default_str = match (&prompt_str, &default_val) {
113            (Some(_prompt), Some(val)) => format!("(default: {val}) "),
114            _ => "".to_string(),
115        };
116
117        let history_entries = match input {
118            PipelineData::Value(Value::List { vals, .. }, ..) => Some(vals),
119            _ => None,
120        };
121
122        // If we either have history entries or history file, we create an history
123        let history = match (history_entries.is_some(), history_file_val.is_some()) {
124            (false, false) => None, // Neither are set, no need for history support
125            _ => {
126                let file_history = match history_file_val {
127                    Some(file) => FileBackedHistory::with_file(max_history, file.into()),
128                    None => FileBackedHistory::new(max_history),
129                };
130                let mut history = match file_history {
131                    Ok(h) => h,
132                    Err(e) => match e.0 {
133                        reedline::ReedlineErrorVariants::IOError(err) => {
134                            return Err(ShellError::IncorrectValue {
135                                msg: err.to_string(),
136                                val_span: history_file_span.expect("history-file should be set"),
137                                call_span: call.head,
138                            });
139                        }
140                        reedline::ReedlineErrorVariants::OtherHistoryError(msg) => {
141                            return Err(ShellError::IncorrectValue {
142                                msg: msg.to_string(),
143                                val_span: max_history_span.expect("max-history should be set"),
144                                call_span: call.head,
145                            });
146                        }
147                        _ => {
148                            return Err(ShellError::IncorrectValue {
149                                msg: "unable to create history".to_string(),
150                                val_span: call.head,
151                                call_span: call.head,
152                            });
153                        }
154                    },
155                };
156
157                if let Some(vals) = history_entries {
158                    vals.iter().for_each(|val| {
159                        if let Value::String { val, .. } = val {
160                            let _ = history.save(HistoryItem::from_command_line(val.clone()));
161                        }
162                    });
163                }
164                Some(history)
165            }
166        };
167
168        let prompt = ReedlinePrompt {
169            indicator: default_str,
170            left_prompt: prompt_str.unwrap_or("".to_string()),
171            right_prompt: "".to_string(),
172        };
173
174        let mut line_editor = Reedline::create();
175        line_editor = line_editor.with_ansi_colors(false);
176        line_editor = match history {
177            Some(h) => line_editor.with_history(Box::new(h)),
178            None => line_editor,
179        };
180
181        // In reedline mode, treat `--default` as the initial editable buffer contents.
182        // This keeps options minimal while supporting the "prefilled but editable" UX.
183        if let Some(val) = default_val.as_ref() {
184            prefill_reedline_buffer(&mut line_editor, val);
185        }
186
187        let mut buf = String::new();
188
189        match line_editor.read_line(&prompt) {
190            Ok(Signal::Success(buffer) | Signal::HostCommand(buffer)) => {
191                buf.push_str(&buffer);
192            }
193            Ok(Signal::CtrlC) => {
194                return Err(IoError::new(
195                    shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Interrupted),
196                    call.head,
197                    None,
198                )
199                .into());
200            }
201            Ok(Signal::CtrlD) => {
202                // Do nothing on ctrl-d
203                return Ok(Value::nothing(call.head).into_pipeline_data());
204            }
205            // TODO: handle other signals like Signal::ExternalBreak
206            Ok(_) => {}
207            Err(event_error) => {
208                let from_io_error = IoError::factory(call.head, None);
209                return Err(from_io_error(event_error).into());
210            }
211        }
212        match default_val {
213            Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
214            _ => Ok(Value::string(buf, call.head).into_pipeline_data()),
215        }
216    }
217
218    fn examples(&self) -> Vec<Example<'_>> {
219        vec![
220            Example {
221                description: "Get input from the user, and assign to a variable.",
222                example: "let user_input = (input)",
223                result: None,
224            },
225            Example {
226                description: "Get two characters from the user, and assign to a variable.",
227                example: "let user_input = (input --numchar 2)",
228                result: None,
229            },
230            Example {
231                description: "Get input from the user with default value, and assign to a variable.",
232                example: "let user_input = (input --default 10)",
233                result: None,
234            },
235            Example {
236                description: "Get multiple lines of input from the user (newlines can be entered using `Alt` + `Enter` or `Ctrl` + `Enter`), and assign to a variable.",
237                example: "let multiline_input = (input --reedline)",
238                result: None,
239            },
240            Example {
241                description: "Get input from the user with history, and assign to a variable.",
242                example: "let user_input = ([past,command,entries] | input --reedline)",
243                result: None,
244            },
245            Example {
246                description: "Get input from the user with history backed by a file, and assign to a variable.",
247                example: "let user_input = (input --reedline --history-file ./history.txt)",
248                result: None,
249            },
250        ]
251    }
252}
253
254fn prefill_reedline_buffer(line_editor: &mut Reedline, default_val: &str) {
255    if default_val.is_empty() {
256        return;
257    }
258
259    // Start with a clean buffer. This also ensures idempotency if this function is ever called
260    // more than once.
261    line_editor.run_edit_commands(&[EditCommand::Clear]);
262    line_editor.run_edit_commands(&[EditCommand::InsertString(default_val.to_string())]);
263    // Keep cursor at end (insertion point is naturally advanced by InsertString).
264}
265
266#[cfg(test)]
267mod tests {
268    use super::Input;
269    use super::prefill_reedline_buffer;
270    use reedline::Reedline;
271
272    #[test]
273    fn examples_work_as_expected() -> nu_test_support::Result {
274        nu_test_support::test().examples(Input)
275    }
276
277    #[test]
278    fn reedline_default_prefills_editable_buffer() {
279        let mut line_editor = Reedline::create();
280        prefill_reedline_buffer(&mut line_editor, "foobar.txt");
281
282        assert_eq!(line_editor.current_buffer_contents(), "foobar.txt");
283        assert_eq!(line_editor.current_insertion_point(), "foobar.txt".len());
284    }
285
286    #[test]
287    fn reedline_default_empty_does_not_prefill() {
288        let mut line_editor = Reedline::create();
289        prefill_reedline_buffer(&mut line_editor, "");
290
291        assert_eq!(line_editor.current_buffer_contents(), "");
292        assert_eq!(line_editor.current_insertion_point(), 0);
293    }
294}