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