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    /// Initialize the interactive session (call before the first `run_interactively_once`).
187    pub async fn start(&mut self) -> Result<(), ShellError> {
188        let mut shell = self.shell.lock().await;
189        shell.start_interactive_session()?;
190        Ok(())
191    }
192
193    /// Finalize the interactive session (call after the loop ends).
194    pub async fn finish(&mut self) -> Result<(), ShellError> {
195        let mut shell = self.shell.lock().await;
196
197        shell.end_interactive_session()?;
198
199        if let Err(e) = shell.save_history() {
200            tracing::debug!("couldn't save history: {e}");
201        }
202
203        shell.on_exit().await?;
204
205        Ok(())
206    }
207
208    /// Runs the interactive shell loop once, reading a single command from standard input.
209    pub async fn run_interactively_once(
210        &mut self,
211    ) -> Result<InteractiveExecutionResult, ShellError> {
212        let mut shell = self.shell.lock().await;
213
214        // Run any pre-prompt actions.
215        Self::run_pre_prompt_actions(&mut shell, &self.options).await?;
216
217        // Compose the prompt.
218        let prompt = Self::compose_prompt(&mut shell, self.terminal_integration.as_ref()).await?;
219
220        drop(shell);
221
222        // Read input.
223        match self.input.read_line(&self.shell, prompt)? {
224            ReadResult::Input(read_result) => {
225                // We got a line of input -- execute it.
226                self.execute_line(read_result, true /* user input */).await
227            }
228            ReadResult::BoundCommand(read_result) => {
229                // We got a line that was bound to keybindings; execute it.
230                self.execute_line(read_result, false /* user input */).await
231            }
232            ReadResult::Eof => {
233                // We're done!
234                Ok(InteractiveExecutionResult::Eof)
235            }
236            ReadResult::Interrupted => {
237                // We were interrupted; report that appropriately.
238                let result: brush_core::ExecutionResult =
239                    brush_core::ExecutionExitCode::Interrupted.into();
240                self.shell
241                    .lock()
242                    .await
243                    .set_last_exit_status(result.exit_code.into());
244                Ok(InteractiveExecutionResult::Executed(result))
245            }
246        }
247    }
248
249    async fn compose_prompt(
250        shell: &mut brush_core::Shell<SE>,
251        terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
252    ) -> Result<InteractivePrompt, ShellError> {
253        // Now that we've done that, compose the prompt.
254        let mut prompt = InteractivePrompt {
255            prompt: shell.compose_prompt().await?,
256            alt_side_prompt: shell.compose_alt_side_prompt().await?,
257            continuation_prompt: shell.compose_continuation_prompt().await?,
258        };
259
260        if let Some(terminal_integration) = terminal_integration {
261            let pre_prompt = terminal_integration.pre_prompt();
262            let working_dir = terminal_integration.report_cwd(shell.working_dir());
263            let post_prompt = terminal_integration.post_prompt();
264
265            prompt.prompt = [
266                pre_prompt.as_ref(),
267                working_dir.as_ref(),
268                prompt.prompt.as_str(),
269                post_prompt.as_ref(),
270            ]
271            .concat();
272        }
273
274        Ok(prompt)
275    }
276
277    /// Executes the given line of input.
278    ///
279    /// # Arguments
280    ///
281    /// * `read_result` - The line of input to execute.
282    /// * `user_input` - Whether the line came from direct user input (as opposed to a key binding,
283    ///   say).
284    async fn execute_line(
285        &mut self,
286        read_result: String,
287        user_input: bool,
288    ) -> Result<InteractiveExecutionResult, ShellError> {
289        let mut shell = self.shell.lock().await;
290
291        // See if the user interface has a non-empty read buffer.
292        let buffer_info = self.input.get_read_buffer();
293
294        // If the user interface did, in fact, have a non-empty read buffer,
295        // then reflect it to the shell in case any shell code wants to
296        // process and/or transform the buffer.
297        let nonempty_buffer = if let Some((buffer, cursor)) = buffer_info {
298            if !buffer.is_empty() {
299                shell.set_edit_buffer(buffer, cursor)?;
300                true
301            } else {
302                false
303            }
304        } else {
305            false
306        };
307
308        // If the line came from direct user input (as opposed to a key binding, say), then we
309        // need to do a few more things before executing it.
310        if user_input {
311            Self::run_pre_exec_actions(
312                &mut shell,
313                read_result.as_str(),
314                &self.options,
315                self.terminal_integration.as_ref(),
316            )
317            .await?;
318        }
319
320        // Count the command's lines.
321        let line_count = read_result.lines().count().max(1);
322
323        // Execute the command.
324        let params = shell.default_exec_params();
325        let source_info = brush_core::SourceInfo::from("main");
326        let result = match shell.run_string(read_result, &source_info, &params).await {
327            Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
328            Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
329        };
330
331        // Update cumulative line counter based on actual lines in the command.
332        shell.increment_interactive_line_offset(line_count);
333
334        // See if the shell has input buffer state that we need to reflect back to
335        // the user interface. It may be state that originally came from the user
336        // interface, or it may be state that was programmatically generated by
337        // the command we just executed.
338        let mut buffer_and_cursor = shell.pop_edit_buffer()?;
339
340        drop(shell);
341
342        if buffer_and_cursor.is_none() && nonempty_buffer {
343            buffer_and_cursor = Some((String::new(), 0));
344        }
345
346        if let Some((updated_buffer, updated_cursor)) = buffer_and_cursor {
347            self.input.set_read_buffer(updated_buffer, updated_cursor);
348        }
349
350        // Invoke terminal integration.
351        if let Some(terminal_integration) = &self.terminal_integration {
352            let exit_code = result.as_ref().map_or(1, i32::from);
353            print!(
354                "{}",
355                terminal_integration.post_exec_command(exit_code).as_ref()
356            );
357            std::io::stdout().flush()?;
358        }
359
360        result
361    }
362
363    async fn run_pre_prompt_actions(
364        shell: &mut brush_core::Shell<SE>,
365        options: &InteractiveOptions,
366    ) -> Result<(), ShellError> {
367        // Check for any completed jobs.
368        shell.check_for_completed_jobs()?;
369
370        // If there's a variable called PROMPT_COMMAND, then run it first.
371        if options.run_prompt_command
372            && let Some(prompt_cmd_var) = shell.env_var("PROMPT_COMMAND")
373        {
374            match prompt_cmd_var.value() {
375                brush_core::ShellValue::String(cmd_str) => {
376                    Self::run_pre_prompt_command(shell, cmd_str.to_owned()).await?;
377                }
378                brush_core::ShellValue::IndexedArray(values) => {
379                    let owned_values: Vec<_> = values.values().cloned().collect();
380                    for cmd_str in owned_values {
381                        Self::run_pre_prompt_command(shell, cmd_str).await?;
382                    }
383                }
384                // Other types are ignored.
385                _ => (),
386            }
387        }
388
389        // Next, run any zsh-style `precmd_functions`.
390        // TODO(precmd_functions): verify if we need to save/restore exit results.
391        if options.run_cmd_exec_funcs {
392            // If there's a variable called precmd_functions, then call them.
393            if let Some(brush_core::ShellValue::IndexedArray(precmd_funcs)) = shell
394                .env_var("precmd_functions")
395                .map(|var| var.value())
396                .cloned()
397            {
398                for func_name in precmd_funcs.values() {
399                    let _ = shell
400                        .invoke_function(
401                            func_name,
402                            std::iter::empty::<&str>(),
403                            &shell.default_exec_params(),
404                        )
405                        .await;
406                }
407            }
408        }
409
410        Ok(())
411    }
412
413    async fn run_pre_exec_actions(
414        shell: &mut brush_core::Shell<SE>,
415        command_line: &str,
416        options: &InteractiveOptions,
417        terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
418    ) -> Result<(), ShellError> {
419        // Display the pre-command prompt (if there is one).
420        let precmd_prompt = shell.compose_precmd_prompt().await?;
421        if !precmd_prompt.is_empty() {
422            print!("{precmd_prompt}");
423        }
424
425        // Update history (if applicable).
426        shell.add_to_history(command_line.trim_end_matches('\n'))?;
427
428        // Next, run any zsh-style `preexec_functions`.
429        // TODO(preexec_functions): verify if we need to save/restore exit results.
430        if options.run_cmd_exec_funcs {
431            // If there's a variable called preexec_functions, then call them.
432            if let Some(brush_core::ShellValue::IndexedArray(preexec_funcs)) = shell
433                .env_var("preexec_functions")
434                .map(|var| var.value())
435                .cloned()
436            {
437                for func_name in preexec_funcs.values() {
438                    let _ = shell
439                        .invoke_function(func_name, &[command_line], &shell.default_exec_params())
440                        .await;
441                }
442            }
443        }
444
445        // Invoke terminal integration.
446        if let Some(terminal_integration) = terminal_integration {
447            print!(
448                "{}",
449                terminal_integration.pre_exec_command(command_line).as_ref()
450            );
451            std::io::stdout().flush()?;
452        }
453
454        Ok(())
455    }
456
457    async fn run_pre_prompt_command(
458        shell: &mut brush_core::Shell<SE>,
459        prompt_cmd: String,
460    ) -> Result<(), ShellError> {
461        // Save (and later restore) the last exit status.
462        let prev_last_result = shell.last_exit_status();
463        let prev_last_pipeline_statuses = shell.last_pipeline_statuses().to_vec();
464
465        // Run the command.
466        let params = shell.default_exec_params();
467        let source_info = brush_core::SourceInfo::from("PROMPT_COMMAND");
468        shell.run_string(prompt_cmd, &source_info, &params).await?;
469
470        // Restore the last exit status.
471        *shell.last_pipeline_statuses_mut() = prev_last_pipeline_statuses;
472        shell.set_last_exit_status(prev_last_result);
473
474        Ok(())
475    }
476}
477
478/// Represents the host environment; used for terminal detection in conjunction
479/// with the `TerminalEnvironment` trait.
480struct HostEnvironment;
481
482impl crate::term_detection::TerminalEnvironment for HostEnvironment {
483    /// Gets the value of the given environment variable from the host process's
484    /// OS environment variables. Returns `None` if the variable is not set.
485    ///
486    /// # Arguments
487    ///
488    /// * `name` - The name of the environment variable to get.
489    fn get_env_var(&self, name: &str) -> Option<String> {
490        std::env::var(name).ok()
491    }
492}