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, 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 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 global_cancellation_token: CancellationToken,
60 state: Arc<RwLock<SearchCommandsComponentState<'static>>>,
62}
63struct SearchCommandsComponentState<'a> {
64 initialize_with_ai: bool,
66 mode: SearchMode,
68 user_only: bool,
70 query: CustomTextArea<'a>,
72 ai_mode: bool,
74 tags: Option<CustomList<'a, CommentString>>,
76 alias_match: bool,
78 commands: CustomList<'a, Command>,
80 error: ErrorPopup<'a>,
82}
83
84impl SearchCommandsComponent {
85 pub fn new(
87 service: IntelliShellService,
88 config: Config,
89 inline: bool,
90 query: impl Into<String>,
91 initialize_with_ai: bool,
92 cancellation_token: CancellationToken,
93 ) -> Self {
94 let query =
95 CustomTextArea::new(Style::from_crossterm(config.theme.primary), inline, false, query.into()).focused();
96
97 let commands = CustomList::new(config.theme.clone(), inline, Vec::new());
98
99 let error = ErrorPopup::empty(&config.theme);
100
101 let layout = if inline {
102 Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
103 } else {
104 Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
105 };
106
107 let SearchConfig {
108 delay,
109 mode,
110 user_only,
111 exec_on_alias_match,
112 } = config.search;
113
114 let ret = Self {
115 theme: config.theme,
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(RwLock::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.write();
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.read().initialize_with_ai;
183 if initialize_with_ai {
184 let res = self.prompt_ai().await;
185 self.state.write().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.read();
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.read();
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.write();
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.write();
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.read();
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.write();
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.read();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.write();
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.read();
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.read();
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.write();
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.read();
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.inline,
520 command,
521 EditCommandComponentMode::Edit {
522 parent: Box::new(self.clone()),
523 },
524 self.global_cancellation_token.clone(),
525 ))))
526 } else {
527 self.state
528 .write()
529 .error
530 .set_temp_message("Workspace commands can't be updated");
531 Ok(Action::NoOp)
532 }
533 } else {
534 Ok(Action::NoOp)
535 }
536 }
537
538 #[instrument(skip_all)]
539 async fn selection_confirm(&mut self) -> Result<Action> {
540 let (selected_tag, cursor_pos, query, command, ai_mode) = {
541 let state = self.state.read();
542 if state.query.is_ai_loading() {
543 return Ok(Action::NoOp);
544 }
545 let selected_tag = state.tags.as_ref().and_then(|s| s.selected().cloned());
546 (
547 selected_tag.map(String::from),
548 state.query.cursor().1,
549 state.query.lines_as_string(),
550 state.commands.selected().cloned(),
551 state.ai_mode,
552 )
553 };
554
555 if let Some(tag) = selected_tag {
556 tracing::debug!("Selected tag: {tag}");
557 self.confirm_tag(tag, query, cursor_pos).await
558 } else if let Some(command) = command {
559 tracing::info!("Selected command: {}", command.cmd);
560 self.confirm_command(command, false, ai_mode).await
561 } else {
562 Ok(Action::NoOp)
563 }
564 }
565
566 #[instrument(skip_all)]
567 async fn selection_execute(&mut self) -> Result<Action> {
568 let (command, ai_mode) = {
569 let state = self.state.read();
570 if state.query.is_ai_loading() {
571 return Ok(Action::NoOp);
572 }
573 (state.commands.selected().cloned(), state.ai_mode)
574 };
575 if let Some(command) = command {
576 tracing::info!("Selected command to execute: {}", command.cmd);
577 self.confirm_command(command, true, ai_mode).await
578 } else {
579 Ok(Action::NoOp)
580 }
581 }
582
583 async fn prompt_ai(&mut self) -> Result<Action> {
584 let mut state = self.state.write();
585 if state.tags.is_some() || state.query.is_ai_loading() {
586 return Ok(Action::NoOp);
587 }
588 let query = state.query.lines_as_string();
589 if !query.is_empty() {
590 state.query.set_ai_loading(true);
591 drop(state);
592 self.update_config(None, None, Some(true));
593 let this = self.clone();
594 tokio::spawn(async move {
595 let res = this
596 .service
597 .suggest_commands(&query, this.global_cancellation_token.clone())
598 .await;
599 let mut state = this.state.write();
600 let commands = match res {
601 Ok(suggestions) => {
602 if !suggestions.is_empty() {
603 state.error.clear_message();
604 state.alias_match = false;
605 suggestions
606 } else {
607 state
608 .error
609 .set_temp_message("AI did not return any suggestion".to_string());
610 Vec::new()
611 }
612 }
613 Err(AppError::UserFacing(err)) => {
614 tracing::warn!("{err}");
615 state.error.set_temp_message(err.to_string());
616 Vec::new()
617 }
618 Err(AppError::Unexpected(err)) => panic!("Error prompting for command suggestions: {err:?}"),
619 };
620 state.commands.update_items(commands, true);
621 state.query.set_ai_loading(false);
622 });
623 }
624 Ok(Action::NoOp)
625 }
626}
627
628impl SearchCommandsComponent {
629 fn schedule_debounced_command_refresh(&self) {
631 let cancellation_token = {
632 let mut token_guard = self.refresh_token.lock().unwrap();
634 if let Some(token) = token_guard.take() {
635 token.cancel();
636 }
637 let new_token = CancellationToken::new();
639 *token_guard = Some(new_token.clone());
640 new_token
641 };
642
643 let this = self.clone();
645 tokio::spawn(async move {
646 tokio::select! {
647 biased;
648 _ = cancellation_token.cancelled() => {}
650 _ = tokio::time::sleep(this.search_delay) => {
652 if let Err(err) = this.refresh_commands().await {
653 panic!("Error refreshing commands: {err:?}");
654 }
655 }
656 }
657 });
658 }
659
660 #[instrument(skip_all)]
662 async fn refresh_commands(&self) -> Result<()> {
663 let (mode, user_only, ai_mode, query) = {
665 let state = self.state.read();
666 (
667 state.mode,
668 state.user_only,
669 state.ai_mode,
670 state.query.lines_as_string(),
671 )
672 };
673
674 if ai_mode {
676 return Ok(());
677 }
678
679 let res = self.service.search_commands(mode, user_only, &query).await;
681
682 let mut state = self.state.write();
684 let commands = match res {
685 Ok((commands, alias_match)) => {
686 state.error.clear_message();
687 state.alias_match = alias_match;
688 commands
689 }
690 Err(AppError::UserFacing(err)) => {
691 tracing::warn!("{err}");
692 state.error.set_perm_message(err.to_string());
693 Vec::new()
694 }
695 Err(AppError::Unexpected(err)) => return Err(err),
696 };
697 state.commands.update_items(commands, true);
698
699 Ok(())
700 }
701
702 fn debounced_refresh_tags(&self) {
704 let this = self.clone();
705 tokio::spawn(async move {
706 if let Err(err) = this.refresh_tags().await {
707 panic!("Error refreshing tags: {err:?}");
708 }
709 });
710 }
711
712 #[instrument(skip_all)]
714 async fn refresh_tags(&self) -> Result<()> {
715 let (mode, user_only, ai_mode, query, cursor_pos) = {
717 let state = self.state.read();
718 (
719 state.mode,
720 state.user_only,
721 state.ai_mode,
722 state.query.lines_as_string(),
723 state.query.cursor().1,
724 )
725 };
726
727 if ai_mode {
729 return Ok(());
730 }
731
732 let res = self.service.search_tags(mode, user_only, &query, cursor_pos).await;
734
735 let mut state = self.state.write();
737 match res {
738 Ok(None) => {
739 tracing::trace!("No editing tags");
740 if state.tags.is_some() {
741 tracing::debug!("Closing tag mode: no editing tag");
742 state.tags = None;
743 state.commands.set_focus(true);
744 }
745 self.schedule_debounced_command_refresh();
746 Ok(())
747 }
748 Ok(Some(tags)) if tags.is_empty() => {
749 tracing::trace!("No tags found");
750 if state.tags.is_some() {
751 tracing::debug!("Closing tag mode: no tags found");
752 state.tags = None;
753 state.commands.set_focus(true);
754 }
755 self.schedule_debounced_command_refresh();
756 Ok(())
757 }
758 Ok(Some(tags)) => {
759 state.error.clear_message();
760 if tags.len() == 1 && tags.iter().all(|(_, _, exact_match)| *exact_match) {
761 tracing::trace!("Exact tag found only");
762 if state.tags.is_some() {
763 tracing::debug!("Closing tag mode: exact tag found");
764 state.tags = None;
765 state.commands.set_focus(true);
766 }
767 self.schedule_debounced_command_refresh();
768 } else {
769 tracing::trace!("Found {} tags", tags.len());
770 let tags = tags.into_iter().map(|(tag, _, _)| CommentString::from(tag)).collect();
771 let tags_list = if let Some(ref mut list) = state.tags {
772 list
773 } else {
774 tracing::debug!("Entering tag mode");
775 state
776 .tags
777 .insert(CustomList::new(self.theme.clone(), self.inline, Vec::new()))
778 };
779 tags_list.update_items(tags, true);
780 state.commands.set_focus(false);
781 }
782
783 Ok(())
784 }
785 Err(AppError::UserFacing(err)) => {
786 tracing::warn!("{err}");
787 state.error.set_perm_message(err.to_string());
788 if state.tags.is_some() {
789 tracing::debug!("Closing tag mode");
790 state.tags = None;
791 state.commands.set_focus(true);
792 }
793 Ok(())
794 }
795 Err(AppError::Unexpected(err)) => Err(err),
796 }
797 }
798
799 #[instrument(skip_all)]
801 async fn confirm_tag(&mut self, tag: String, query: String, cursor_pos: usize) -> Result<Action> {
802 let mut tag_start = cursor_pos.wrapping_sub(1);
804 let chars: Vec<_> = query.chars().collect();
805 while tag_start > 0 && chars[tag_start] != '#' {
806 tag_start -= 1;
807 }
808 let mut tag_end = cursor_pos;
809 while tag_end < chars.len() && chars[tag_end] != ' ' {
810 tag_end += 1;
811 }
812 let mut state = self.state.write();
813 if chars[tag_start] == '#' {
814 state.query.select_all();
816 state.query.cut();
817 state
818 .query
819 .insert_str(format!("{}{} {}", &query[..tag_start], tag, &query[tag_end..]));
820 state
821 .query
822 .move_cursor(CursorMove::Jump(0, (tag_start + tag.len() + 1) as u16));
823 }
824 state.tags = None;
825 state.commands.set_focus(true);
826 self.schedule_debounced_command_refresh();
827 Ok(Action::NoOp)
828 }
829
830 #[instrument(skip_all)]
833 async fn confirm_command(&mut self, command: Command, execute: bool, ai_command: bool) -> Result<Action> {
834 if !ai_command && command.source != SOURCE_WORKSPACE {
836 self.service
837 .increment_command_usage(command.id)
838 .await
839 .map_err(AppError::into_report)?;
840 }
841 let template = CommandTemplate::parse(&command.cmd, false);
843 if template.has_pending_variable() {
844 Ok(Action::SwitchComponent(Box::new(VariableReplacementComponent::new(
846 self.service.clone(),
847 self.theme.clone(),
848 self.inline,
849 execute,
850 false,
851 template,
852 self.global_cancellation_token.clone(),
853 ))))
854 } else if execute {
855 Ok(Action::Quit(ProcessOutput::execute(command.cmd)))
857 } else {
858 Ok(Action::Quit(
860 ProcessOutput::success().stdout(&command.cmd).fileout(command.cmd),
861 ))
862 }
863 }
864}