brush_interactive/
interactive_shell.rs

1use crate::ShellError;
2use std::io::{IsTerminal, Write};
3
4/// Result of a read operation.
5pub enum ReadResult {
6    /// The user entered a line of input.
7    Input(String),
8    /// A bound key sequence yielded a registered command.
9    BoundCommand(String),
10    /// End of input was reached.
11    Eof,
12    /// The user interrupted the input operation.
13    Interrupted,
14}
15
16/// Result of an interactive execution.
17pub enum InteractiveExecutionResult {
18    /// The command was executed and returned the given result.
19    Executed(brush_core::ExecutionResult),
20    /// The command failed to execute.
21    Failed(brush_core::Error),
22    /// End of input was reached.
23    Eof,
24}
25
26/// Represents an interactive prompt.
27pub struct InteractivePrompt {
28    /// Prompt to display.
29    pub prompt: String,
30    /// Alternate-side prompt (typically right) to display.
31    pub alt_side_prompt: String,
32    /// Prompt to display on a continuation line of input.
33    pub continuation_prompt: String,
34}
35
36/// Represents a shell capable of taking commands from standard input.
37pub trait InteractiveShell: Send {
38    /// Returns an immutable reference to the inner shell object.
39    fn shell(&self) -> impl AsRef<brush_core::Shell> + Send;
40
41    /// Returns a mutable reference to the inner shell object.
42    fn shell_mut(&mut self) -> impl AsMut<brush_core::Shell> + Send;
43
44    /// Reads a line of input, using the given prompt.
45    ///
46    /// # Arguments
47    ///
48    /// * `prompt` - The prompt to display to the user.
49    fn read_line(&mut self, prompt: InteractivePrompt) -> Result<ReadResult, ShellError>;
50
51    /// Returns the current contents of the read buffer and the current cursor
52    /// position within the buffer; None is returned if the read buffer is
53    /// empty or cannot be read by this implementation.
54    fn get_read_buffer(&self) -> Option<(String, usize)> {
55        None
56    }
57
58    /// Updates the read buffer with the given string and cursor. Considered a
59    /// no-op if the implementation does not support updating read buffers.
60    fn set_read_buffer(&mut self, _buffer: String, _cursor: usize) {
61        // No-op by default.
62    }
63
64    /// Runs the interactive shell loop, reading commands from standard input and writing
65    /// results to standard output and standard error. Continues until the shell
66    /// normally exits or until a fatal error occurs.
67    // NOTE: we use desugared async here because [async_fn_in_trait] "warning: use of `async fn` in
68    // public traits is discouraged as auto trait bounds cannot be specified"
69    fn run_interactively(
70        &mut self,
71    ) -> impl std::future::Future<Output = Result<(), ShellError>> + Send {
72        async {
73            // Acquire terminal control if stdin is a terminal.
74            if std::io::stdin().is_terminal() {
75                brush_core::terminal::TerminalControl::acquire()?;
76            }
77
78            let mut announce_exit = self.shell().as_ref().options.interactive;
79
80            loop {
81                let result = self.run_interactively_once().await?;
82                match result {
83                    InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
84                        next_control_flow: brush_core::results::ExecutionControlFlow::ExitShell,
85                        ..
86                    }) => {
87                        break;
88                    }
89                    InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
90                        next_control_flow:
91                            brush_core::results::ExecutionControlFlow::ReturnFromFunctionOrScript,
92                        ..
93                    }) => {
94                        tracing::error!("return from non-function/script");
95                    }
96                    InteractiveExecutionResult::Executed(_) => {}
97                    InteractiveExecutionResult::Failed(err) => {
98                        // Report the error, but continue to execute.
99                        let shell = self.shell();
100                        let mut stderr = shell.as_ref().stderr();
101                        let _ = shell.as_ref().display_error(&mut stderr, &err).await;
102                    }
103                    InteractiveExecutionResult::Eof => {
104                        break;
105                    }
106                }
107
108                if self.shell().as_ref().options.exit_after_one_command {
109                    announce_exit = false;
110                    break;
111                }
112            }
113
114            if announce_exit {
115                writeln!(self.shell().as_ref().stderr(), "exit")?;
116            }
117
118            if let Err(e) = self.shell_mut().as_mut().save_history() {
119                // N.B. This seems like the sort of thing that's worth being noisy about,
120                // but bash doesn't do that -- and probably for a reason.
121                tracing::debug!("couldn't save history: {e}");
122            }
123
124            // Give the shell an opportunity to perform any on-exit operations.
125            self.shell_mut().as_mut().on_exit().await?;
126
127            Ok(())
128        }
129    }
130
131    /// Runs the interactive shell loop once, reading a single command from standard input.
132    fn run_interactively_once(
133        &mut self,
134    ) -> impl std::future::Future<Output = Result<InteractiveExecutionResult, ShellError>> + Send
135    {
136        async {
137            let mut shell = self.shell_mut();
138            let shell_mut = shell.as_mut();
139
140            // Check for any completed jobs.
141            shell_mut.check_for_completed_jobs()?;
142
143            // Run any pre-prompt commands.
144            run_pre_prompt_commands(shell_mut).await?;
145
146            // Now that we've done that, compose the prompt.
147            let prompt = InteractivePrompt {
148                prompt: shell_mut.as_mut().compose_prompt().await?,
149                alt_side_prompt: shell_mut.as_mut().compose_alt_side_prompt().await?,
150                continuation_prompt: shell_mut.as_mut().compose_continuation_prompt().await?,
151            };
152
153            drop(shell);
154
155            match self.read_line(prompt)? {
156                ReadResult::Input(read_result) => {
157                    self.execute_line(read_result, true /*user input*/).await
158                }
159                ReadResult::BoundCommand(read_result) => {
160                    self.execute_line(read_result, false /*user input*/).await
161                }
162                ReadResult::Eof => Ok(InteractiveExecutionResult::Eof),
163                ReadResult::Interrupted => {
164                    let mut shell_mut = self.shell_mut();
165                    let result: brush_core::ExecutionResult =
166                        brush_core::ExecutionExitCode::Interrupted.into();
167                    *shell_mut.as_mut().last_exit_status_mut() = result.exit_code.into();
168                    Ok(InteractiveExecutionResult::Executed(result))
169                }
170            }
171        }
172    }
173
174    /// Executes the given line of input.
175    fn execute_line(
176        &mut self,
177        read_result: String,
178        user_input: bool,
179    ) -> impl std::future::Future<Output = Result<InteractiveExecutionResult, ShellError>> + Send
180    {
181        async move {
182            // See if the the user interface has a non-empty read buffer.
183            let buffer_info = self.get_read_buffer();
184
185            let mut shell_mut = self.shell_mut();
186
187            // If the user interface did, in fact, have a non-empty read buffer,
188            // then reflect it to the shell in case any shell code wants to
189            // process and/or transform the buffer.
190            let nonempty_buffer = if let Some((buffer, cursor)) = buffer_info {
191                if !buffer.is_empty() {
192                    shell_mut.as_mut().set_edit_buffer(buffer, cursor)?;
193                    true
194                } else {
195                    false
196                }
197            } else {
198                false
199            };
200
201            // If the line came from direct user input (as opposed to a key binding, say), then we
202            // need to do a few more things before executing it.
203            if user_input {
204                // Display the pre-command prompt (if there is one).
205                let precmd_prompt = shell_mut.as_mut().compose_precmd_prompt().await?;
206                if !precmd_prompt.is_empty() {
207                    print!("{precmd_prompt}");
208                }
209
210                // Update history (if applicable).
211                shell_mut
212                    .as_mut()
213                    .add_to_history(read_result.trim_end_matches('\n'))?;
214            }
215
216            // Execute the command.
217            let params = shell_mut.as_mut().default_exec_params();
218            let result = match shell_mut.as_mut().run_string(read_result, &params).await {
219                Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
220                Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
221            };
222
223            // See if the shell has input buffer state that we need to reflect back to
224            // the user interface. It may be state that originally came from the user
225            // interface, or it may be state that was programmatically generated by
226            // the command we just executed.
227            let mut buffer_and_cursor = shell_mut.as_mut().pop_edit_buffer()?;
228
229            if buffer_and_cursor.is_none() && nonempty_buffer {
230                buffer_and_cursor = Some((String::new(), 0));
231            }
232
233            if let Some((updated_buffer, updated_cursor)) = buffer_and_cursor {
234                drop(shell_mut);
235
236                self.set_read_buffer(updated_buffer, updated_cursor);
237            }
238
239            result
240        }
241    }
242}
243
244async fn run_pre_prompt_commands(shell: &mut brush_core::Shell) -> Result<(), ShellError> {
245    // If there's a variable called PROMPT_COMMAND, then run it first.
246    if let Some(prompt_cmd_var) = shell.env_var("PROMPT_COMMAND") {
247        match prompt_cmd_var.value() {
248            brush_core::ShellValue::String(cmd_str) => {
249                run_pre_prompt_command(shell, cmd_str.to_owned()).await?;
250            }
251            brush_core::ShellValue::IndexedArray(values) => {
252                let owned_values: Vec<_> = values.values().cloned().collect();
253                for cmd_str in owned_values {
254                    run_pre_prompt_command(shell, cmd_str).await?;
255                }
256            }
257            // Other types are ignored.
258            _ => (),
259        }
260    }
261
262    Ok(())
263}
264
265async fn run_pre_prompt_command(
266    shell: &mut brush_core::Shell,
267    prompt_cmd: impl Into<String>,
268) -> Result<(), ShellError> {
269    // Save (and later restore) the last exit status.
270    let prev_last_result = shell.last_result();
271    let prev_last_pipeline_statuses = shell.last_pipeline_statuses.clone();
272
273    // Run the command.
274    let params = shell.default_exec_params();
275    shell.run_string(prompt_cmd, &params).await?;
276
277    // Restore the last exit status.
278    shell.last_pipeline_statuses = prev_last_pipeline_statuses;
279    *shell.last_exit_status_mut() = prev_last_result;
280
281    Ok(())
282}