intelli_shell/service/
command.rs

1use tracing::instrument;
2use uuid::Uuid;
3
4use super::IntelliShellService;
5use crate::{
6    errors::{Result, UserFacingError},
7    model::{CATEGORY_USER, CATEGORY_WORKSPACE, Command, SearchCommandsFilter, SearchMode},
8    utils::{extract_tags_and_cleaned_text, extract_tags_with_editing_and_cleaned_text, get_working_dir},
9};
10
11/// A Tag consist on the text, the amount of times it has been used and whether it was an exact match from the query
12type Tag = (String, u64, bool);
13
14impl IntelliShellService {
15    /// Returns whether the commands storage is empty
16    #[instrument(skip_all)]
17    pub async fn is_storage_empty(&self) -> Result<bool> {
18        self.storage.is_empty().await
19    }
20
21    /// Bookmarks a new command
22    #[instrument(skip_all)]
23    pub async fn insert_command(&self, command: Command) -> Result<Command> {
24        // Validate
25        if command.cmd.is_empty() {
26            return Err(UserFacingError::EmptyCommand.into());
27        }
28
29        // Insert it
30        tracing::info!("Bookmarking command: {}", command.cmd);
31        self.storage.insert_command(command).await
32    }
33
34    /// Updates an existing command
35    #[instrument(skip_all)]
36    pub async fn update_command(&self, command: Command) -> Result<Command> {
37        // Validate
38        if command.cmd.is_empty() {
39            return Err(UserFacingError::EmptyCommand.into());
40        }
41
42        // Update it
43        tracing::info!("Updating command '{}': {}", command.id, command.cmd);
44        self.storage.update_command(command).await
45    }
46
47    /// Increases the usage of a command, returning the new usage count
48    #[instrument(skip_all)]
49    pub async fn increment_command_usage(&self, command_id: Uuid) -> Result<i32> {
50        tracing::info!("Increasing usage for command '{command_id}'");
51        self.storage
52            .increment_command_usage(command_id, get_working_dir())
53            .await
54    }
55
56    /// Deletes an existing command
57    #[instrument(skip_all)]
58    pub async fn delete_command(&self, id: Uuid) -> Result<()> {
59        tracing::info!("Deleting command: {}", id);
60        self.storage.delete_command(id).await
61    }
62
63    /// Searches for tags based on a query string
64    #[instrument(skip_all)]
65    pub async fn search_tags(
66        &self,
67        mode: SearchMode,
68        user_only: bool,
69        query: &str,
70        cursor_pos: usize,
71    ) -> Result<Option<Vec<Tag>>> {
72        let Some((editing_tag, other_tags, cleaned_text)) =
73            extract_tags_with_editing_and_cleaned_text(query, cursor_pos)
74        else {
75            return Ok(None);
76        };
77
78        tracing::info!(
79            "Searching for tags{} [{mode:?}]: {query}",
80            if user_only { " (user only)" } else { "" }
81        );
82        tracing::trace!("Editing: {editing_tag} Other: {other_tags:?}");
83
84        let filter = SearchCommandsFilter {
85            category: user_only.then(|| vec![CATEGORY_USER.to_string()]),
86            source: None,
87            tags: Some(other_tags),
88            search_mode: mode,
89            search_term: Some(cleaned_text),
90        };
91
92        Ok(Some(
93            self.storage
94                .find_tags(filter, Some(editing_tag), &self.tuning.commands)
95                .await?,
96        ))
97    }
98
99    /// Searches for commands based on a query string, returning both the command and whether it was an alias match
100    #[instrument(skip_all)]
101    pub async fn search_commands(
102        &self,
103        mode: SearchMode,
104        user_only: bool,
105        query: &str,
106    ) -> Result<(Vec<Command>, bool)> {
107        tracing::info!(
108            "Searching for commands{} [{mode:?}]: {query}",
109            if user_only { " (user only)" } else { "" }
110        );
111
112        let query = query.trim();
113        let filter = if query.is_empty() {
114            // If there are no query, just display user commands
115            SearchCommandsFilter {
116                category: Some(if user_only {
117                    vec![CATEGORY_USER.to_string()]
118                } else {
119                    vec![CATEGORY_USER.to_string(), CATEGORY_WORKSPACE.to_string()]
120                }),
121                search_mode: mode,
122                ..Default::default()
123            }
124        } else {
125            // Else, parse user query into tags and search term
126            let (tags, search_term) = match extract_tags_and_cleaned_text(query) {
127                Some((tags, cleaned_query)) => (Some(tags), Some(cleaned_query)),
128                None => (None, Some(query.to_string())),
129            };
130
131            // Build the filter
132            SearchCommandsFilter {
133                category: user_only.then(|| vec![CATEGORY_USER.to_string()]),
134                source: None,
135                tags,
136                search_mode: mode,
137                search_term,
138            }
139        };
140
141        // Query the storage
142        self.storage
143            .find_commands(filter, get_working_dir(), &self.tuning.commands)
144            .await
145    }
146}