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