Skip to main content

brush_interactive/
interactive_shell.rs

1use std::io::IsTerminal as _;
2use std::io::Write as _;
3
4use crate::InputBackend;
5use crate::InteractivePrompt;
6use crate::ReadResult;
7use crate::ShellError;
8
9/// Result of an interactive execution.
10pub enum InteractiveExecutionResult {
11    /// The command was executed and returned the given result.
12    Executed(brush_core::ExecutionResult),
13    /// The command failed to execute.
14    Failed(brush_core::Error),
15    /// End of input was reached.
16    Eof,
17}
18
19impl From<&InteractiveExecutionResult> for i32 {
20    /// Converts an `InteractiveExecutionResult` into a signed, 32-bit exit code.
21    fn from(value: &InteractiveExecutionResult) -> Self {
22        match value {
23            InteractiveExecutionResult::Executed(result) => u8::from(result.exit_code).into(),
24            InteractiveExecutionResult::Failed(_) => 1,
25            InteractiveExecutionResult::Eof => 0,
26        }
27    }
28}
29
30/// Options for interactive shells.
31#[derive(Clone)]
32pub struct InteractiveOptions {
33    /// Whether terminal shell integration is enabled.
34    pub terminal_shell_integration: bool,
35    /// Whether or not to run `PROMPT_COMMAND` before each prompt.
36    pub run_prompt_command: bool,
37    /// Whether or not to run zsh-style exec/cmd functions (e.g., `preexec_functions`,
38    /// `precmd_functions`).
39    pub run_cmd_exec_funcs: bool,
40}
41
42impl Default for InteractiveOptions {
43    fn default() -> Self {
44        Self {
45            terminal_shell_integration: false,
46            run_prompt_command: true,
47            run_cmd_exec_funcs: false,
48        }
49    }
50}
51
52/// Represents an interactive shell that displays prompts, interactively reads user input, etc.
53pub struct InteractiveShell<'a, IB: InputBackend, SE: brush_core::ShellExtensions> {
54    /// The underlying shell instance.
55    shell: crate::ShellRef<SE>,
56    /// The input backend to use.
57    input: &'a mut IB,
58    /// Terminal integration utility, if any.
59    terminal_integration: Option<crate::term_integration::TerminalIntegration>,
60    /// Options.
61    options: InteractiveOptions,
62}
63
64impl<'a, IB: InputBackend, SE: brush_core::ShellExtensions> InteractiveShell<'a, IB, SE> {
65    /// Creates a new `InteractiveShell` wrapping the given shell instance.
66    ///
67    /// # Arguments
68    ///
69    /// * `shell` - The shell instance to wrap.
70    /// * `input` - The input backend to use.
71    /// * `options` - The user interface options to use.
72    pub fn new(
73        shell: &crate::ShellRef<SE>,
74        input: &'a mut IB,
75        options: &InteractiveOptions,
76    ) -> Result<Self, ShellError> {
77        let stdin_is_terminal = std::io::stdin().is_terminal();
78
79        // Acquire terminal control if stdin is a terminal.
80        if stdin_is_terminal {
81            brush_core::terminal::TerminalControl::acquire()?;
82        }
83
84        // Set up terminal integration if enabled *and* if stdin is a terminal.
85        let terminal_integration = if options.terminal_shell_integration && stdin_is_terminal {
86            let terminfo = crate::term_detection::get_terminal_info(&HostEnvironment);
87            let terminal_integration = crate::term_integration::TerminalIntegration::new(terminfo);
88
89            print!("{}", terminal_integration.initialize().as_ref());
90            std::io::stdout().flush()?;
91
92            Some(terminal_integration)
93        } else {
94            None
95        };
96
97        Ok(Self {
98            shell: shell.clone(),
99            input,
100            terminal_integration,
101            options: options.clone(),
102        })
103    }
104
105    /// Runs the interactive shell loop, reading commands from standard input and writing
106    /// results to standard output and standard error. Continues until the shell
107    /// normally exits or until a fatal error occurs.
108    pub async fn run_interactively(&mut self) -> Result<(), ShellError> {
109        let mut shell = self.shell.lock().await;
110
111        let mut announce_exit = shell.options().interactive;
112
113        shell.start_interactive_session()?;
114
115        drop(shell);
116
117        loop {
118            let result = self.run_interactively_once().await?;
119            match result {
120                InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
121                    next_control_flow: brush_core::results::ExecutionControlFlow::ExitShell,
122                    ..
123                }) => {
124                    break;
125                }
126                InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
127                    next_control_flow:
128                        brush_core::results::ExecutionControlFlow::ReturnFromFunctionOrScript,
129                    ..
130                }) => {
131                    tracing::error!("return from non-function/script");
132                }
133                InteractiveExecutionResult::Executed(_) => {}
134                InteractiveExecutionResult::Failed(err) => {
135                    // Report the error, but continue to execute.
136                    let shell = self.shell.lock().await;
137                    let mut stderr = shell.stderr();
138                    let _ = shell.display_error(&mut stderr, &err);
139
140                    drop(shell);
141                }
142                InteractiveExecutionResult::Eof => {
143                    break;
144                }
145            }
146
147            if self.shell.lock().await.options().exit_after_one_command {
148                announce_exit = false;
149                break;
150            }
151        }
152
153        let mut shell = self.shell.lock().await;
154
155        shell.end_interactive_session()?;
156
157        if announce_exit {
158            writeln!(shell.stderr(), "exit")?;
159        }
160
161        if let Err(e) = shell.save_history() {
162            // N.B. This seems like the sort of thing that's worth being noisy about,
163            // but bash doesn't do that -- and probably for a reason.
164            tracing::debug!("couldn't save history: {e}");
165        }
166
167        // Give the shell an opportunity to perform any on-exit operations.
168        shell.on_exit().await?;
169
170        drop(shell);
171
172        Ok(())
173    }
174
175    /// Runs the interactive shell loop once, reading a single command from standard input.
176    async fn run_interactively_once(&mut self) -> Result<InteractiveExecutionResult, ShellError> {
177        let mut shell = self.shell.lock().await;
178
179        // Run any pre-prompt actions.
180        Self::run_pre_prompt_actions(&mut shell, &self.options).await?;
181
182        // Compose the prompt.
183        let prompt = Self::compose_prompt(&mut shell, self.terminal_integration.as_ref()).await?;
184
185        drop(shell);
186
187        // Read input.
188        match self.input.read_line(&self.shell, prompt)? {
189            ReadResult::Input(read_result) => {
190                // We got a line of input -- execute it.
191                self.execute_line(read_result, true /* user input */).await
192            }
193            ReadResult::BoundCommand(read_result) => {
194                // We got a line that was bound to keybindings; execute it.
195                self.execute_line(read_result, false /* user input */).await
196            }
197            ReadResult::Eof => {
198                // We're done!
199                Ok(InteractiveExecutionResult::Eof)
200            }
201            ReadResult::Interrupted => {
202                // We were interrupted; report that appropriately.
203                let result: brush_core::ExecutionResult =
204                    brush_core::ExecutionExitCode::Interrupted.into();
205                self.shell
206                    .lock()
207                    .await
208                    .set_last_exit_status(result.exit_code.into());
209                Ok(InteractiveExecutionResult::Executed(result))
210            }
211        }
212    }
213
214    async fn compose_prompt(
215        shell: &mut brush_core::Shell<SE>,
216        terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
217    ) -> Result<InteractivePrompt, ShellError> {
218        // Now that we've done that, compose the prompt.
219        let mut prompt = InteractivePrompt {
220            prompt: shell.compose_prompt().await?,
221            alt_side_prompt: shell.compose_alt_side_prompt().await?,
222            continuation_prompt: shell.compose_continuation_prompt().await?,
223        };
224
225        if let Some(terminal_integration) = terminal_integration {
226            let pre_prompt = terminal_integration.pre_prompt();
227            let working_dir = terminal_integration.report_cwd(shell.working_dir());
228            let post_prompt = terminal_integration.post_prompt();
229
230            prompt.prompt = [
231                pre_prompt.as_ref(),
232                working_dir.as_ref(),
233                prompt.prompt.as_str(),
234                post_prompt.as_ref(),
235            ]
236            .concat();
237        }
238
239        Ok(prompt)
240    }
241
242    /// Executes the given line of input.
243    ///
244    /// # Arguments
245    ///
246    /// * `read_result` - The line of input to execute.
247    /// * `user_input` - Whether the line came from direct user input (as opposed to a key binding,
248    ///   say).
249    async fn execute_line(
250        &mut self,
251        read_result: String,
252        user_input: bool,
253    ) -> Result<InteractiveExecutionResult, ShellError> {
254        let mut shell = self.shell.lock().await;
255
256        // See if the the user interface has a non-empty read buffer.
257        let buffer_info = self.input.get_read_buffer();
258
259        // If the user interface did, in fact, have a non-empty read buffer,
260        // then reflect it to the shell in case any shell code wants to
261        // process and/or transform the buffer.
262        let nonempty_buffer = if let Some((buffer, cursor)) = buffer_info {
263            if !buffer.is_empty() {
264                shell.set_edit_buffer(buffer, cursor)?;
265                true
266            } else {
267                false
268            }
269        } else {
270            false
271        };
272
273        // If the line came from direct user input (as opposed to a key binding, say), then we
274        // need to do a few more things before executing it.
275        if user_input {
276            Self::run_pre_exec_actions(
277                &mut shell,
278                read_result.as_str(),
279                &self.options,
280                self.terminal_integration.as_ref(),
281            )
282            .await?;
283        }
284
285        // Count the command's lines.
286        let line_count = read_result.lines().count().max(1);
287
288        // Execute the command.
289        let params = shell.default_exec_params();
290        let source_info = brush_core::SourceInfo::from("main");
291        let result = match shell.run_string(read_result, &source_info, &params).await {
292            Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
293            Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
294        };
295
296        // Update cumulative line counter based on actual lines in the command.
297        shell.increment_interactive_line_offset(line_count);
298
299        // See if the shell has input buffer state that we need to reflect back to
300        // the user interface. It may be state that originally came from the user
301        // interface, or it may be state that was programmatically generated by
302        // the command we just executed.
303        let mut buffer_and_cursor = shell.pop_edit_buffer()?;
304
305        drop(shell);
306
307        if buffer_and_cursor.is_none() && nonempty_buffer {
308            buffer_and_cursor = Some((String::new(), 0));
309        }
310
311        if let Some((updated_buffer, updated_cursor)) = buffer_and_cursor {
312            self.input.set_read_buffer(updated_buffer, updated_cursor);
313        }
314
315        // Invoke terminal integration.
316        if let Some(terminal_integration) = &self.terminal_integration {
317            let exit_code = result.as_ref().map_or(1, i32::from);
318            print!(
319                "{}",
320                terminal_integration.post_exec_command(exit_code).as_ref()
321            );
322            std::io::stdout().flush()?;
323        }
324
325        result
326    }
327
328    async fn run_pre_prompt_actions(
329        shell: &mut brush_core::Shell<SE>,
330        options: &InteractiveOptions,
331    ) -> Result<(), ShellError> {
332        // Check for any completed jobs.
333        shell.check_for_completed_jobs()?;
334
335        // If there's a variable called PROMPT_COMMAND, then run it first.
336        if options.run_prompt_command {
337            if let Some(prompt_cmd_var) = shell.env_var("PROMPT_COMMAND") {
338                match prompt_cmd_var.value() {
339                    brush_core::ShellValue::String(cmd_str) => {
340                        Self::run_pre_prompt_command(shell, cmd_str.to_owned()).await?;
341                    }
342                    brush_core::ShellValue::IndexedArray(values) => {
343                        let owned_values: Vec<_> = values.values().cloned().collect();
344                        for cmd_str in owned_values {
345                            Self::run_pre_prompt_command(shell, cmd_str).await?;
346                        }
347                    }
348                    // Other types are ignored.
349                    _ => (),
350                }
351            }
352        }
353
354        // Next, run any zsh-style `precmd_functions`.
355        // TODO(precmd_functions): verify if we need to save/restore exit results.
356        if options.run_cmd_exec_funcs {
357            // If there's a variable called precmd_functions, then call them.
358            if let Some(brush_core::ShellValue::IndexedArray(precmd_funcs)) = shell
359                .env_var("precmd_functions")
360                .map(|var| var.value())
361                .cloned()
362            {
363                for func_name in precmd_funcs.values() {
364                    let _ = shell
365                        .invoke_function(
366                            func_name,
367                            std::iter::empty::<&str>(),
368                            &shell.default_exec_params(),
369                        )
370                        .await;
371                }
372            }
373        }
374
375        Ok(())
376    }
377
378    async fn run_pre_exec_actions(
379        shell: &mut brush_core::Shell<SE>,
380        command_line: &str,
381        options: &InteractiveOptions,
382        terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
383    ) -> Result<(), ShellError> {
384        // Display the pre-command prompt (if there is one).
385        let precmd_prompt = shell.compose_precmd_prompt().await?;
386        if !precmd_prompt.is_empty() {
387            print!("{precmd_prompt}");
388        }
389
390        // Update history (if applicable).
391        shell.add_to_history(command_line.trim_end_matches('\n'))?;
392
393        // Next, run any zsh-style `preexec_functions`.
394        // TODO(preexec_functions): verify if we need to save/restore exit results.
395        if options.run_cmd_exec_funcs {
396            // If there's a variable called preexec_functions, then call them.
397            if let Some(brush_core::ShellValue::IndexedArray(preexec_funcs)) = shell
398                .env_var("preexec_functions")
399                .map(|var| var.value())
400                .cloned()
401            {
402                for func_name in preexec_funcs.values() {
403                    let _ = shell
404                        .invoke_function(func_name, &[command_line], &shell.default_exec_params())
405                        .await;
406                }
407            }
408        }
409
410        // Invoke terminal integration.
411        if let Some(terminal_integration) = terminal_integration {
412            print!(
413                "{}",
414                terminal_integration.pre_exec_command(command_line).as_ref()
415            );
416            std::io::stdout().flush()?;
417        }
418
419        Ok(())
420    }
421
422    async fn run_pre_prompt_command(
423        shell: &mut brush_core::Shell<SE>,
424        prompt_cmd: String,
425    ) -> Result<(), ShellError> {
426        // Save (and later restore) the last exit status.
427        let prev_last_result = shell.last_exit_status();
428        let prev_last_pipeline_statuses = shell.last_pipeline_statuses().to_vec();
429
430        // Run the command.
431        let params = shell.default_exec_params();
432        let source_info = brush_core::SourceInfo::from("PROMPT_COMMAND");
433        shell.run_string(prompt_cmd, &source_info, &params).await?;
434
435        // Restore the last exit status.
436        *shell.last_pipeline_statuses_mut() = prev_last_pipeline_statuses;
437        shell.set_last_exit_status(prev_last_result);
438
439        Ok(())
440    }
441}
442
443/// Represents the host environment; used for terminal detection in conjunction
444/// with the `TerminalEnvironment` trait.
445struct HostEnvironment;
446
447impl crate::term_detection::TerminalEnvironment for HostEnvironment {
448    /// Gets the value of the given environment variable from the host process's
449    /// OS environment variables. Returns `None` if the variable is not set.
450    ///
451    /// # Arguments
452    ///
453    /// * `name` - The name of the environment variable to get.
454    fn get_env_var(&self, name: &str) -> Option<String> {
455        std::env::var(name).ok()
456    }
457}