1use std::io::IsTerminal as _;
2use std::io::Write as _;
3
4use crate::InputBackend;
5use crate::InteractivePrompt;
6use crate::ReadResult;
7use crate::ShellError;
8
9pub enum InteractiveExecutionResult {
11 Executed(brush_core::ExecutionResult),
13 Failed(brush_core::Error),
15 Eof,
17}
18
19impl From<&InteractiveExecutionResult> for i32 {
20 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#[derive(Clone)]
32pub struct InteractiveOptions {
33 pub terminal_shell_integration: bool,
35 pub run_prompt_command: bool,
37 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
52pub struct InteractiveShell<'a, IB: InputBackend, SE: brush_core::ShellExtensions> {
54 shell: crate::ShellRef<SE>,
56 input: &'a mut IB,
58 terminal_integration: Option<crate::term_integration::TerminalIntegration>,
60 options: InteractiveOptions,
62}
63
64impl<'a, IB: InputBackend, SE: brush_core::ShellExtensions> InteractiveShell<'a, IB, SE> {
65 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 if stdin_is_terminal {
81 brush_core::terminal::TerminalControl::acquire()?;
82 }
83
84 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 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 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 tracing::debug!("couldn't save history: {e}");
165 }
166
167 shell.on_exit().await?;
169
170 drop(shell);
171
172 Ok(())
173 }
174
175 async fn run_interactively_once(&mut self) -> Result<InteractiveExecutionResult, ShellError> {
177 let mut shell = self.shell.lock().await;
178
179 Self::run_pre_prompt_actions(&mut shell, &self.options).await?;
181
182 let prompt = Self::compose_prompt(&mut shell, self.terminal_integration.as_ref()).await?;
184
185 drop(shell);
186
187 match self.input.read_line(&self.shell, prompt)? {
189 ReadResult::Input(read_result) => {
190 self.execute_line(read_result, true ).await
192 }
193 ReadResult::BoundCommand(read_result) => {
194 self.execute_line(read_result, false ).await
196 }
197 ReadResult::Eof => {
198 Ok(InteractiveExecutionResult::Eof)
200 }
201 ReadResult::Interrupted => {
202 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 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 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 let buffer_info = self.input.get_read_buffer();
258
259 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 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 let line_count = read_result.lines().count().max(1);
287
288 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, ¶ms).await {
292 Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
293 Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
294 };
295
296 shell.increment_interactive_line_offset(line_count);
298
299 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 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 shell.check_for_completed_jobs()?;
334
335 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 _ => (),
350 }
351 }
352 }
353
354 if options.run_cmd_exec_funcs {
357 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 let precmd_prompt = shell.compose_precmd_prompt().await?;
386 if !precmd_prompt.is_empty() {
387 print!("{precmd_prompt}");
388 }
389
390 shell.add_to_history(command_line.trim_end_matches('\n'))?;
392
393 if options.run_cmd_exec_funcs {
396 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 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 let prev_last_result = shell.last_exit_status();
428 let prev_last_pipeline_statuses = shell.last_pipeline_statuses().to_vec();
429
430 let params = shell.default_exec_params();
432 let source_info = brush_core::SourceInfo::from("PROMPT_COMMAND");
433 shell.run_string(prompt_cmd, &source_info, ¶ms).await?;
434
435 *shell.last_pipeline_statuses_mut() = prev_last_pipeline_statuses;
437 shell.set_last_exit_status(prev_last_result);
438
439 Ok(())
440 }
441}
442
443struct HostEnvironment;
446
447impl crate::term_detection::TerminalEnvironment for HostEnvironment {
448 fn get_env_var(&self, name: &str) -> Option<String> {
455 std::env::var(name).ok()
456 }
457}