Skip to main content

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