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            return self.legacy_input(engine_state, stack, call, input);
95        }
96
97        let prompt_str: Option<String> = call.opt(engine_state, stack, 0)?;
98        let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
99        let history_file_val: Option<String> =
100            call.get_flag(engine_state, stack, "history-file")?;
101        let max_history: usize = call
102            .get_flag::<i64>(engine_state, stack, "max-history")?
103            .map(|l| if l < 0 { 0 } else { l as usize })
104            .unwrap_or(HISTORY_SIZE);
105        let max_history_span = call.get_flag_span(stack, "max-history");
106        let history_file_span = call.get_flag_span(stack, "history-file");
107
108        let default_str = match (&prompt_str, &default_val) {
109            (Some(_prompt), Some(val)) => format!("(default: {val}) "),
110            _ => "".to_string(),
111        };
112
113        let history_entries = match input {
114            PipelineData::Value(Value::List { vals, .. }, ..) => Some(vals),
115            _ => None,
116        };
117
118        // If we either have history entries or history file, we create an history
119        let history = match (history_entries.is_some(), history_file_val.is_some()) {
120            (false, false) => None, // Neither are set, no need for history support
121            _ => {
122                let file_history = match history_file_val {
123                    Some(file) => FileBackedHistory::with_file(max_history, file.into()),
124                    None => FileBackedHistory::new(max_history),
125                };
126                let mut history = match file_history {
127                    Ok(h) => h,
128                    Err(e) => match e.0 {
129                        reedline::ReedlineErrorVariants::IOError(err) => {
130                            return Err(ShellError::IncorrectValue {
131                                msg: err.to_string(),
132                                val_span: history_file_span.expect("history-file should be set"),
133                                call_span: call.head,
134                            });
135                        }
136                        reedline::ReedlineErrorVariants::OtherHistoryError(msg) => {
137                            return Err(ShellError::IncorrectValue {
138                                msg: msg.to_string(),
139                                val_span: max_history_span.expect("max-history should be set"),
140                                call_span: call.head,
141                            });
142                        }
143                        _ => {
144                            return Err(ShellError::IncorrectValue {
145                                msg: "unable to create history".to_string(),
146                                val_span: call.head,
147                                call_span: call.head,
148                            });
149                        }
150                    },
151                };
152
153                if let Some(vals) = history_entries {
154                    vals.iter().for_each(|val| {
155                        if let Value::String { val, .. } = val {
156                            let _ = history.save(HistoryItem::from_command_line(val.clone()));
157                        }
158                    });
159                }
160                Some(history)
161            }
162        };
163
164        let prompt = ReedlinePrompt {
165            indicator: default_str,
166            left_prompt: prompt_str.unwrap_or("".to_string()),
167            right_prompt: "".to_string(),
168        };
169
170        let mut line_editor = Reedline::create();
171        line_editor = line_editor.with_ansi_colors(false);
172        line_editor = match history {
173            Some(h) => line_editor.with_history(Box::new(h)),
174            None => line_editor,
175        };
176
177        // In reedline mode, treat `--default` as the initial editable buffer contents.
178        // This keeps options minimal while supporting the "prefilled but editable" UX.
179        if let Some(val) = default_val.as_ref() {
180            prefill_reedline_buffer(&mut line_editor, val);
181        }
182
183        let mut buf = String::new();
184
185        match line_editor.read_line(&prompt) {
186            Ok(Signal::Success(buffer)) => {
187                buf.push_str(&buffer);
188            }
189            Ok(Signal::CtrlC) => {
190                return Err(IoError::new(
191                    shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Interrupted),
192                    call.head,
193                    None,
194                )
195                .into());
196            }
197            Ok(Signal::CtrlD) => {
198                // Do nothing on ctrl-d
199                return Ok(Value::nothing(call.head).into_pipeline_data());
200            }
201            Err(event_error) => {
202                let from_io_error = IoError::factory(call.head, None);
203                return Err(from_io_error(event_error).into());
204            }
205        }
206        match default_val {
207            Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
208            _ => Ok(Value::string(buf, call.head).into_pipeline_data()),
209        }
210    }
211
212    fn examples(&self) -> Vec<Example<'_>> {
213        vec![
214            Example {
215                description: "Get input from the user, and assign to a variable.",
216                example: "let user_input = (input)",
217                result: None,
218            },
219            Example {
220                description: "Get two characters from the user, and assign to a variable.",
221                example: "let user_input = (input --numchar 2)",
222                result: None,
223            },
224            Example {
225                description: "Get input from the user with default value, and assign to a variable.",
226                example: "let user_input = (input --default 10)",
227                result: None,
228            },
229            Example {
230                description: "Get multiple lines of input from the user (newlines can be entered using `Alt` + `Enter` or `Ctrl` + `Enter`), and assign to a variable.",
231                example: "let multiline_input = (input --reedline)",
232                result: None,
233            },
234            Example {
235                description: "Get input from the user with history, and assign to a variable.",
236                example: "let user_input = ([past,command,entries] | input --reedline)",
237                result: None,
238            },
239            Example {
240                description: "Get input from the user with history backed by a file, and assign to a variable.",
241                example: "let user_input = (input --reedline --history-file ./history.txt)",
242                result: None,
243            },
244        ]
245    }
246}
247
248fn prefill_reedline_buffer(line_editor: &mut Reedline, default_val: &str) {
249    if default_val.is_empty() {
250        return;
251    }
252
253    // Start with a clean buffer. This also ensures idempotency if this function is ever called
254    // more than once.
255    line_editor.run_edit_commands(&[EditCommand::Clear]);
256    line_editor.run_edit_commands(&[EditCommand::InsertString(default_val.to_string())]);
257    // Keep cursor at end (insertion point is naturally advanced by InsertString).
258}
259
260#[cfg(test)]
261mod tests {
262    use super::Input;
263    use super::prefill_reedline_buffer;
264    use reedline::Reedline;
265
266    #[test]
267    fn examples_work_as_expected() {
268        use crate::test_examples;
269        test_examples(Input {})
270    }
271
272    #[test]
273    fn reedline_default_prefills_editable_buffer() {
274        let mut line_editor = Reedline::create();
275        prefill_reedline_buffer(&mut line_editor, "foobar.txt");
276
277        assert_eq!(line_editor.current_buffer_contents(), "foobar.txt");
278        assert_eq!(line_editor.current_insertion_point(), "foobar.txt".len());
279    }
280
281    #[test]
282    fn reedline_default_empty_does_not_prefill() {
283        let mut line_editor = Reedline::create();
284        prefill_reedline_buffer(&mut line_editor, "");
285
286        assert_eq!(line_editor.current_buffer_contents(), "");
287        assert_eq!(line_editor.current_insertion_point(), 0);
288    }
289}