1use std::{
2 sync::{Arc, Mutex},
3 time::Duration,
4};
5
6use async_trait::async_trait;
7use color_eyre::Result;
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
9use enum_cycling::EnumCycle;
10use parking_lot::RwLock;
11use ratatui::{
12 Frame,
13 layout::{Constraint, Layout, Rect},
14};
15use tokio_util::sync::CancellationToken;
16use tracing::instrument;
17use tui_textarea::CursorMove;
18
19use super::Component;
20use crate::{
21 app::Action,
22 component::{
23 edit::{EditCommandComponent, EditCommandComponentMode},
24 variable::VariableReplacementComponent,
25 },
26 config::{Config, KeyBindingsConfig, SearchConfig, Theme},
27 errors::AppError,
28 format_msg,
29 model::{Command, DynamicCommand, SOURCE_WORKSPACE, SearchMode},
30 process::ProcessOutput,
31 service::IntelliShellService,
32 widgets::{
33 CommandWidget, CustomList, CustomTextArea, ErrorPopup, HighlightSymbolMode, NewVersionBanner, TagWidget,
34 },
35};
36
37const EMPTY_STORAGE_MESSAGE: &str = r#"There are no stored commands yet!
38 - Try to bookmark some command with 'Ctrl + B'
39 - Or execute 'intelli-shell tldr fetch' to download a bunch of tldr's useful commands"#;
40
41#[derive(Clone)]
43pub struct SearchCommandsComponent {
44 theme: Theme,
46 inline: bool,
48 exec_on_alias_match: bool,
50 service: IntelliShellService,
52 layout: Layout,
54 search_delay: Duration,
56 refresh_token: Arc<Mutex<Option<CancellationToken>>>,
58 state: Arc<RwLock<SearchCommandsComponentState<'static>>>,
60}
61struct SearchCommandsComponentState<'a> {
62 initialize_with_ai: bool,
64 mode: SearchMode,
66 user_only: bool,
68 query: CustomTextArea<'a>,
70 ai_mode: bool,
72 tags: Option<CustomList<'a, TagWidget>>,
74 alias_match: bool,
76 commands: CustomList<'a, CommandWidget>,
78 error: ErrorPopup<'a>,
80}
81
82impl SearchCommandsComponent {
83 pub fn new(
85 service: IntelliShellService,
86 config: Config,
87 inline: bool,
88 query: impl Into<String>,
89 initialize_with_ai: bool,
90 ) -> Self {
91 let query = CustomTextArea::new(config.theme.primary, inline, false, query.into()).focused();
92
93 let commands = CustomList::new(config.theme.primary, inline, Vec::new())
94 .highlight_symbol(config.theme.highlight_symbol.clone())
95 .highlight_symbol_mode(HighlightSymbolMode::Last)
96 .highlight_symbol_style(config.theme.highlight_primary_full().into());
97
98 let error = ErrorPopup::empty(&config.theme);
99
100 let layout = if inline {
101 Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
102 } else {
103 Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
104 };
105
106 let SearchConfig {
107 delay,
108 mode,
109 user_only,
110 exec_on_alias_match,
111 } = config.search;
112
113 let ret = Self {
114 theme: config.theme,
115 inline,
116 exec_on_alias_match,
117 service,
118 layout,
119 search_delay: Duration::from_millis(delay),
120 refresh_token: Arc::new(Mutex::new(None)),
121 state: Arc::new(RwLock::new(SearchCommandsComponentState {
122 initialize_with_ai,
123 mode,
124 user_only,
125 query,
126 ai_mode: false,
127 tags: None,
128 alias_match: false,
129 commands,
130 error,
131 })),
132 };
133
134 ret.update_config(None, None, None);
135
136 ret
137 }
138
139 fn update_config(&self, search_mode: Option<SearchMode>, user_only: Option<bool>, ai_mode: Option<bool>) {
141 let mut state = self.state.write();
142 if let Some(search_mode) = search_mode {
143 state.mode = search_mode;
144 }
145 if let Some(user_only) = user_only {
146 state.user_only = user_only;
147 }
148 if let Some(ai_mode) = ai_mode {
149 state.ai_mode = ai_mode;
150 }
151
152 let search_mode = state.mode;
153 let title = match (state.ai_mode, self.inline, state.user_only) {
154 (true, true, _) => String::from("(ai)"),
155 (false, true, true) => format!("({search_mode},user)"),
156 (false, true, false) => format!("({search_mode})"),
157 (true, false, _) => String::from(" Query (ai) "),
158 (false, false, true) => format!(" Query ({search_mode},user) "),
159 (false, false, false) => format!(" Query ({search_mode}) "),
160 };
161
162 state.query.set_title(title);
163 }
164}
165
166#[async_trait]
167impl Component for SearchCommandsComponent {
168 fn name(&self) -> &'static str {
169 "SearchCommandsComponent"
170 }
171
172 fn min_inline_height(&self) -> u16 {
173 1 + 10
175 }
176
177 #[instrument(skip_all)]
178 async fn init_and_peek(&mut self) -> Result<Action> {
179 let initialize_with_ai = self.state.read().initialize_with_ai;
181 if initialize_with_ai {
182 let res = self.prompt_ai().await;
183 self.state.write().initialize_with_ai = false;
184 return res;
185 }
186 if self.service.is_storage_empty().await.map_err(AppError::into_report)? {
188 Ok(Action::Quit(
189 ProcessOutput::success().stderr(format_msg!(self.theme, "{EMPTY_STORAGE_MESSAGE}")),
190 ))
191 } else {
192 let tags = {
194 let state = self.state.read();
195 state.query.lines_as_string() == "#"
196 };
197 if tags {
198 self.refresh_tags().await?;
199 } else {
200 self.refresh_commands().await?;
201 let command = {
203 let state = self.state.read();
204 if state.alias_match && state.commands.len() == 1 {
205 state.commands.selected().cloned().map(Command::from)
206 } else {
207 None
208 }
209 };
210 if let Some(command) = command {
211 tracing::info!("Found a single alias command: {}", command.cmd);
212 return self.confirm_command(command, self.exec_on_alias_match, false).await;
213 }
214 }
215 Ok(Action::NoOp)
216 }
217 }
218
219 #[instrument(skip_all)]
220 fn render(&mut self, frame: &mut Frame, area: Rect) {
221 let [query_area, suggestions_area] = self.layout.areas(area);
223
224 let mut state = self.state.write();
225
226 frame.render_widget(&state.query, query_area);
228
229 if let Some(ref mut tags) = state.tags {
231 frame.render_widget(tags, suggestions_area);
232 } else {
233 frame.render_widget(&mut state.commands, suggestions_area);
234 }
235
236 if let Some(new_version) = self.service.check_new_version() {
238 NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
239 }
240 state.error.render_in(frame, area);
241 }
242
243 fn tick(&mut self) -> Result<Action> {
244 let mut state = self.state.write();
245 state.query.tick();
246 state.error.tick();
247 Ok(Action::NoOp)
248 }
249
250 fn exit(&mut self) -> Result<Action> {
251 let (ai_mode, tags) = {
252 let state = self.state.read();
253 (state.ai_mode, state.tags.is_some())
254 };
255 if ai_mode {
256 tracing::debug!("Closing ai mode: user request");
257 self.update_config(None, None, Some(false));
258 self.schedule_debounced_command_refresh();
259 Ok(Action::NoOp)
260 } else if tags {
261 tracing::debug!("Closing tag mode: user request");
262 let mut state = self.state.write();
263 state.tags = None;
264 state.commands.set_focus(true);
265 self.schedule_debounced_command_refresh();
266 Ok(Action::NoOp)
267 } else {
268 tracing::info!("User requested to exit");
269 let state = self.state.read();
270 let query = state.query.lines_as_string();
271 Ok(Action::Quit(if query.trim().is_empty() {
272 ProcessOutput::success()
273 } else {
274 ProcessOutput::success().fileout(query)
275 }))
276 }
277 }
278
279 async fn process_key_event(&mut self, keybindings: &KeyBindingsConfig, key: KeyEvent) -> Result<Action> {
280 if key.code == KeyCode::Char(' ') && key.modifiers == KeyModifiers::CONTROL {
282 self.debounced_refresh_tags();
283 Ok(Action::NoOp)
284 } else {
285 Ok(self
287 .default_process_key_event(keybindings, key)
288 .await?
289 .unwrap_or_default())
290 }
291 }
292
293 fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
294 match mouse.kind {
295 MouseEventKind::ScrollDown => Ok(self.move_down()?),
296 MouseEventKind::ScrollUp => Ok(self.move_up()?),
297 _ => Ok(Action::NoOp),
298 }
299 }
300
301 fn move_up(&mut self) -> Result<Action> {
302 let mut state = self.state.write();
303 if !state.query.is_ai_loading() {
304 if let Some(ref mut tags) = state.tags {
305 tags.select_prev();
306 } else {
307 state.commands.select_prev();
308 }
309 }
310 Ok(Action::NoOp)
311 }
312
313 fn move_down(&mut self) -> Result<Action> {
314 let mut state = self.state.write();
315 if !state.query.is_ai_loading() {
316 if let Some(ref mut tags) = state.tags {
317 tags.select_next();
318 } else {
319 state.commands.select_next();
320 }
321 }
322 Ok(Action::NoOp)
323 }
324
325 fn move_left(&mut self, word: bool) -> Result<Action> {
326 let mut state = self.state.write();
327 if state.tags.is_none() {
328 state.query.move_cursor_left(word);
329 }
330 Ok(Action::NoOp)
331 }
332
333 fn move_right(&mut self, word: bool) -> Result<Action> {
334 let mut state = self.state.write();
335 if state.tags.is_none() {
336 state.query.move_cursor_right(word);
337 }
338 Ok(Action::NoOp)
339 }
340
341 fn move_prev(&mut self) -> Result<Action> {
342 self.move_up()
343 }
344
345 fn move_next(&mut self) -> Result<Action> {
346 self.move_down()
347 }
348
349 fn move_home(&mut self, absolute: bool) -> Result<Action> {
350 let mut state = self.state.write();
351 if !state.query.is_ai_loading() {
352 if let Some(ref mut tags) = state.tags {
353 tags.select_first();
354 } else if absolute {
355 state.commands.select_first();
356 } else {
357 state.query.move_home(false);
358 }
359 }
360 Ok(Action::NoOp)
361 }
362
363 fn move_end(&mut self, absolute: bool) -> Result<Action> {
364 let mut state = self.state.write();
365 if !state.query.is_ai_loading() {
366 if let Some(ref mut tags) = state.tags {
367 tags.select_last();
368 } else if absolute {
369 state.commands.select_last();
370 } else {
371 state.query.move_end(false);
372 }
373 }
374 Ok(Action::NoOp)
375 }
376
377 fn undo(&mut self) -> Result<Action> {
378 let mut state = self.state.write();
379 if !state.query.is_ai_loading() {
380 state.query.undo();
381 if state.tags.is_some() {
382 self.debounced_refresh_tags();
383 } else {
384 self.schedule_debounced_command_refresh();
385 }
386 }
387 Ok(Action::NoOp)
388 }
389
390 fn redo(&mut self) -> Result<Action> {
391 let mut state = self.state.write();
392 if !state.query.is_ai_loading() {
393 state.query.redo();
394 if state.tags.is_some() {
395 self.debounced_refresh_tags();
396 } else {
397 self.schedule_debounced_command_refresh();
398 }
399 }
400 Ok(Action::NoOp)
401 }
402
403 fn insert_text(&mut self, text: String) -> Result<Action> {
404 let mut state = self.state.write();
405 state.query.insert_str(text);
406 if state.tags.is_some() {
407 self.debounced_refresh_tags();
408 } else {
409 self.schedule_debounced_command_refresh();
410 }
411 Ok(Action::NoOp)
412 }
413
414 fn insert_char(&mut self, c: char) -> Result<Action> {
415 let mut state = self.state.write();
416 state.query.insert_char(c);
417 if c == '#' || state.tags.is_some() {
418 self.debounced_refresh_tags();
419 } else {
420 self.schedule_debounced_command_refresh();
421 }
422 Ok(Action::NoOp)
423 }
424
425 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
426 let mut state = self.state.write();
427 state.query.delete(backspace, word);
428 if state.tags.is_some() {
429 self.debounced_refresh_tags();
430 } else {
431 self.schedule_debounced_command_refresh();
432 }
433 Ok(Action::NoOp)
434 }
435
436 fn toggle_search_mode(&mut self) -> Result<Action> {
437 let (search_mode, ai_mode, tags) = {
438 let state = self.state.read();
439 if state.query.is_ai_loading() {
440 return Ok(Action::NoOp);
441 }
442 (state.mode, state.ai_mode, state.tags.is_some())
443 };
444 if ai_mode {
445 tracing::debug!("Closing ai mode: user toggled search mode");
446 self.update_config(None, None, Some(false));
447 } else {
448 self.update_config(Some(search_mode.down()), None, None);
449 }
450 if tags {
451 self.debounced_refresh_tags();
452 } else {
453 self.schedule_debounced_command_refresh();
454 }
455 Ok(Action::NoOp)
456 }
457
458 fn toggle_search_user_only(&mut self) -> Result<Action> {
459 let (user_only, ai_mode, tags) = {
460 let state = self.state.read();
461 (state.user_only, state.ai_mode, state.tags.is_some())
462 };
463 if !ai_mode {
464 self.update_config(None, Some(!user_only), None);
465 if tags {
466 self.debounced_refresh_tags();
467 } else {
468 self.schedule_debounced_command_refresh();
469 }
470 }
471 Ok(Action::NoOp)
472 }
473
474 #[instrument(skip_all)]
475 async fn selection_delete(&mut self) -> Result<Action> {
476 let command = {
477 let mut state = self.state.write();
478 if !state.ai_mode
479 && let Some(selected) = state.commands.selected()
480 {
481 if selected.source != SOURCE_WORKSPACE {
482 state.commands.delete_selected()
483 } else {
484 None
485 }
486 } else {
487 None
488 }
489 };
490
491 if let Some(command) = command {
492 self.service
493 .delete_command(command.id)
494 .await
495 .map_err(AppError::into_report)?;
496 }
497
498 Ok(Action::NoOp)
499 }
500
501 #[instrument(skip_all)]
502 async fn selection_update(&mut self) -> Result<Action> {
503 let command = {
504 let state = self.state.read();
505 if state.ai_mode {
506 return Ok(Action::NoOp);
507 }
508 state.commands.selected().cloned().map(Command::from)
509 };
510 if let Some(command) = command
511 && command.source != SOURCE_WORKSPACE
512 {
513 tracing::info!("Entering command update for: {}", command.cmd);
514 Ok(Action::SwitchComponent(Box::new(EditCommandComponent::new(
515 self.service.clone(),
516 self.theme.clone(),
517 self.inline,
518 command,
519 EditCommandComponentMode::Edit {
520 parent: Box::new(self.clone()),
521 },
522 ))))
523 } else {
524 Ok(Action::NoOp)
525 }
526 }
527
528 #[instrument(skip_all)]
529 async fn selection_confirm(&mut self) -> Result<Action> {
530 let (selected_tag, cursor_pos, query, command, ai_mode) = {
531 let state = self.state.read();
532 if state.query.is_ai_loading() {
533 return Ok(Action::NoOp);
534 }
535 let selected_tag = state.tags.as_ref().and_then(|s| s.selected().map(TagWidget::text));
536 (
537 selected_tag.map(String::from),
538 state.query.cursor().1,
539 state.query.lines_as_string(),
540 state.commands.selected().cloned().map(Command::from),
541 state.ai_mode,
542 )
543 };
544
545 if let Some(tag) = selected_tag {
546 tracing::debug!("Selected tag: {tag}");
547 self.confirm_tag(tag, query, cursor_pos).await
548 } else if let Some(command) = command {
549 tracing::info!("Selected command: {}", command.cmd);
550 self.confirm_command(command, false, ai_mode).await
551 } else {
552 Ok(Action::NoOp)
553 }
554 }
555
556 #[instrument(skip_all)]
557 async fn selection_execute(&mut self) -> Result<Action> {
558 let (command, ai_mode) = {
559 let state = self.state.read();
560 if state.query.is_ai_loading() {
561 return Ok(Action::NoOp);
562 }
563 (state.commands.selected().cloned().map(Command::from), state.ai_mode)
564 };
565 if let Some(command) = command {
566 tracing::info!("Selected command to execute: {}", command.cmd);
567 self.confirm_command(command, true, ai_mode).await
568 } else {
569 Ok(Action::NoOp)
570 }
571 }
572
573 async fn prompt_ai(&mut self) -> Result<Action> {
574 let mut state = self.state.write();
575 if state.tags.is_some() || state.query.is_ai_loading() {
576 return Ok(Action::NoOp);
577 }
578 let query = state.query.lines_as_string();
579 if !query.is_empty() {
580 state.query.set_ai_loading(true);
581 drop(state);
582 self.update_config(None, None, Some(true));
583 let this = self.clone();
584 tokio::spawn(async move {
585 let res = this.service.suggest_commands(&query).await;
586 let mut state = this.state.write();
587 let command_widgets = match res {
588 Ok(suggestions) => {
589 if !suggestions.is_empty() {
590 state.error.clear_message();
591 state.alias_match = false;
592 suggestions
593 .into_iter()
594 .map(|command| CommandWidget::new(&this.theme, this.inline, command))
595 .collect()
596 } else {
597 state
598 .error
599 .set_temp_message("AI did not return any suggestion".to_string());
600 Vec::new()
601 }
602 }
603 Err(AppError::UserFacing(err)) => {
604 tracing::warn!("{err}");
605 state.error.set_temp_message(err.to_string());
606 Vec::new()
607 }
608 Err(AppError::Unexpected(err)) => panic!("Error prompting for command suggestions: {err:?}"),
609 };
610 state.commands.update_items(command_widgets);
611 state.query.set_ai_loading(false);
612 });
613 }
614 Ok(Action::NoOp)
615 }
616}
617
618impl SearchCommandsComponent {
619 fn schedule_debounced_command_refresh(&self) {
621 let cancellation_token = {
622 let mut token_guard = self.refresh_token.lock().unwrap();
624 if let Some(token) = token_guard.take() {
625 token.cancel();
626 }
627 let new_token = CancellationToken::new();
629 *token_guard = Some(new_token.clone());
630 new_token
631 };
632
633 let this = self.clone();
635 tokio::spawn(async move {
636 tokio::select! {
637 biased;
638 _ = cancellation_token.cancelled() => {}
640 _ = tokio::time::sleep(this.search_delay) => {
642 if let Err(err) = this.refresh_commands().await {
643 panic!("Error refreshing commands: {err:?}");
644 }
645 }
646 }
647 });
648 }
649
650 #[instrument(skip_all)]
652 async fn refresh_commands(&self) -> Result<()> {
653 let (mode, user_only, ai_mode, query) = {
655 let state = self.state.read();
656 (
657 state.mode,
658 state.user_only,
659 state.ai_mode,
660 state.query.lines_as_string(),
661 )
662 };
663
664 if ai_mode {
666 return Ok(());
667 }
668
669 let res = self.service.search_commands(mode, user_only, &query).await;
671
672 let mut state = self.state.write();
674 let command_widgets = match res {
675 Ok((commands, alias_match)) => {
676 state.error.clear_message();
677 state.alias_match = alias_match;
678 commands
679 .into_iter()
680 .map(|c| CommandWidget::new(&self.theme, self.inline, c))
681 .collect()
682 }
683 Err(AppError::UserFacing(err)) => {
684 tracing::warn!("{err}");
685 state.error.set_perm_message(err.to_string());
686 Vec::new()
687 }
688 Err(AppError::Unexpected(err)) => return Err(err),
689 };
690 state.commands.update_items(command_widgets);
691
692 Ok(())
693 }
694
695 fn debounced_refresh_tags(&self) {
697 let this = self.clone();
698 tokio::spawn(async move {
699 if let Err(err) = this.refresh_tags().await {
700 panic!("Error refreshing tags: {err:?}");
701 }
702 });
703 }
704
705 #[instrument(skip_all)]
707 async fn refresh_tags(&self) -> Result<()> {
708 let (mode, user_only, ai_mode, query, cursor_pos) = {
710 let state = self.state.read();
711 (
712 state.mode,
713 state.user_only,
714 state.ai_mode,
715 state.query.lines_as_string(),
716 state.query.cursor().1,
717 )
718 };
719
720 if ai_mode {
722 return Ok(());
723 }
724
725 let res = self.service.search_tags(mode, user_only, &query, cursor_pos).await;
727
728 let mut state = self.state.write();
730 match res {
731 Ok(None) => {
732 tracing::trace!("No editing tags");
733 if state.tags.is_some() {
734 tracing::debug!("Closing tag mode: no editing tag");
735 state.tags = None;
736 state.commands.set_focus(true);
737 }
738 self.schedule_debounced_command_refresh();
739 Ok(())
740 }
741 Ok(Some(tags)) if tags.is_empty() => {
742 tracing::trace!("No tags found");
743 if state.tags.is_some() {
744 tracing::debug!("Closing tag mode: no tags found");
745 state.tags = None;
746 state.commands.set_focus(true);
747 }
748 self.schedule_debounced_command_refresh();
749 Ok(())
750 }
751 Ok(Some(tags)) => {
752 state.error.clear_message();
753 if tags.len() == 1 && tags.iter().all(|(_, _, exact_match)| *exact_match) {
754 tracing::trace!("Exact tag found only");
755 if state.tags.is_some() {
756 tracing::debug!("Closing tag mode: exact tag found");
757 state.tags = None;
758 state.commands.set_focus(true);
759 }
760 self.schedule_debounced_command_refresh();
761 } else {
762 tracing::trace!("Found {} tags", tags.len());
763 let tag_widgets = tags
764 .into_iter()
765 .map(|(tag, _, _)| TagWidget::new(&self.theme, tag))
766 .collect();
767 let tags_list = if let Some(ref mut list) = state.tags {
768 list
769 } else {
770 tracing::debug!("Entering tag mode");
771 state.tags.insert(
772 CustomList::new(self.theme.primary, self.inline, Vec::new())
773 .highlight_symbol(self.theme.highlight_symbol.clone())
774 .highlight_symbol_mode(HighlightSymbolMode::Last)
775 .highlight_symbol_style(self.theme.highlight_primary_full().into()),
776 )
777 };
778 tags_list.update_items(tag_widgets);
779 state.commands.set_focus(false);
780 }
781
782 Ok(())
783 }
784 Err(AppError::UserFacing(err)) => {
785 tracing::warn!("{err}");
786 state.error.set_perm_message(err.to_string());
787 if state.tags.is_some() {
788 tracing::debug!("Closing tag mode");
789 state.tags = None;
790 state.commands.set_focus(true);
791 }
792 Ok(())
793 }
794 Err(AppError::Unexpected(err)) => Err(err),
795 }
796 }
797
798 #[instrument(skip_all)]
800 async fn confirm_tag(&mut self, tag: String, query: String, cursor_pos: usize) -> Result<Action> {
801 let mut tag_start = cursor_pos.wrapping_sub(1);
803 let chars: Vec<_> = query.chars().collect();
804 while tag_start > 0 && chars[tag_start] != '#' {
805 tag_start -= 1;
806 }
807 let mut tag_end = cursor_pos;
808 while tag_end < chars.len() && chars[tag_end] != ' ' {
809 tag_end += 1;
810 }
811 let mut state = self.state.write();
812 if chars[tag_start] == '#' {
813 state.query.select_all();
815 state.query.cut();
816 state
817 .query
818 .insert_str(format!("{}{} {}", &query[..tag_start], tag, &query[tag_end..]));
819 state
820 .query
821 .move_cursor(CursorMove::Jump(0, (tag_start + tag.len() + 1) as u16));
822 }
823 state.tags = None;
824 state.commands.set_focus(true);
825 self.schedule_debounced_command_refresh();
826 Ok(Action::NoOp)
827 }
828
829 #[instrument(skip_all)]
832 async fn confirm_command(&mut self, command: Command, execute: bool, ai_command: bool) -> Result<Action> {
833 if !ai_command && command.source != SOURCE_WORKSPACE {
835 self.service
836 .increment_command_usage(command.id)
837 .await
838 .map_err(AppError::into_report)?;
839 }
840 let dynamic = DynamicCommand::parse(&command.cmd, false);
842 if dynamic.has_pending_variable() {
843 Ok(Action::SwitchComponent(Box::new(VariableReplacementComponent::new(
845 self.service.clone(),
846 self.theme.clone(),
847 self.inline,
848 execute,
849 false,
850 dynamic,
851 ))))
852 } else if execute {
853 Ok(Action::Quit(ProcessOutput::execute(command.cmd)))
855 } else {
856 Ok(Action::Quit(
858 ProcessOutput::success().stdout(&command.cmd).fileout(command.cmd),
859 ))
860 }
861 }
862}