intelli_shell/process/
search.rs

1use std::time::Duration;
2
3use indicatif::{ProgressBar, ProgressStyle};
4use itertools::Itertools;
5use tracing::instrument;
6
7use super::{InteractiveProcess, Process, ProcessOutput};
8use crate::{
9    cli::SearchCommandsProcess,
10    component::{Component, search::SearchCommandsComponent},
11    config::Config,
12    errors::{AppError, UserFacingError},
13    format_error,
14    service::IntelliShellService,
15    widgets::SPINNER_CHARS,
16};
17
18impl Process for SearchCommandsProcess {
19    #[instrument(skip_all)]
20    async fn execute(self, config: Config, service: IntelliShellService) -> color_eyre::Result<ProcessOutput> {
21        // Different behaviors based on ai flag
22        if self.ai {
23            // Validate we've a query
24            let prompt = self.query.as_deref().unwrap_or_default();
25            if prompt.trim().is_empty() {
26                return Ok(ProcessOutput::fail().stderr(format_error!(
27                    config.theme,
28                    "{}",
29                    UserFacingError::AiEmptyCommand
30                )));
31            }
32
33            // Setup the progress bar
34            let pb = ProgressBar::new_spinner();
35            pb.set_style(
36                ProgressStyle::with_template("{spinner:.blue} {wide_msg}")
37                    .unwrap()
38                    .tick_strings(&SPINNER_CHARS),
39            );
40            pb.enable_steady_tick(Duration::from_millis(100));
41            pb.set_message("Thinking ...");
42
43            // Suggest commands using AI
44            let res = service.suggest_commands(prompt).await;
45
46            // Clear the spinner
47            pb.finish_and_clear();
48
49            // Handle the result
50            match res {
51                Ok(commands) => Ok(ProcessOutput::success().stdout(commands.into_iter().map(|c| c.cmd).join("\n"))),
52                Err(AppError::UserFacing(err)) => {
53                    Ok(ProcessOutput::fail().stderr(format_error!(config.theme, "{err}")))
54                }
55                Err(AppError::Unexpected(report)) => Err(report),
56            }
57        } else {
58            // Merge config with args
59            let (config, query) = merge_config(self, config);
60
61            // Search for commands and handle result
62            match service
63                .search_commands(config.search.mode, config.search.user_only, &query)
64                .await
65            {
66                Ok((commands, _)) => {
67                    Ok(ProcessOutput::success().stdout(commands.into_iter().map(|c| c.cmd).join("\n")))
68                }
69                Err(AppError::UserFacing(err)) => {
70                    Ok(ProcessOutput::fail().stderr(format_error!(config.theme, "{err}")))
71                }
72                Err(AppError::Unexpected(report)) => Err(report),
73            }
74        }
75    }
76}
77
78impl InteractiveProcess for SearchCommandsProcess {
79    #[instrument(skip_all)]
80    fn into_component(
81        self,
82        config: Config,
83        service: IntelliShellService,
84        inline: bool,
85    ) -> color_eyre::Result<Box<dyn Component>> {
86        let ai = self.ai;
87        let (config, query) = merge_config(self, config);
88        Ok(Box::new(SearchCommandsComponent::new(
89            service, config, inline, query, ai,
90        )))
91    }
92}
93
94fn merge_config(p: SearchCommandsProcess, mut config: Config) -> (Config, String) {
95    let SearchCommandsProcess {
96        query,
97        mode,
98        user_only,
99        ai: _,
100    } = p;
101    config.search.mode = mode.unwrap_or(config.search.mode);
102    config.search.user_only = user_only || config.search.user_only;
103    (config, query.unwrap_or_default())
104}