intelli_shell/service/
command.rs1use std::{env, path::PathBuf};
2
3use color_eyre::Result;
4use tokio::fs::File;
5use tracing::instrument;
6use uuid::Uuid;
7
8use super::IntelliShellService;
9use crate::{
10 errors::{InsertError, SearchError, UpdateError},
11 model::{CATEGORY_USER, CATEGORY_WORKSPACE, Command, SOURCE_WORKSPACE, SearchCommandsFilter, SearchMode},
12 service::import_export::parse_commands,
13 utils::{extract_tags_and_cleaned_text, extract_tags_with_editing_and_cleaned_text, get_working_dir},
14};
15
16type Tag = (String, u64, bool);
18
19impl IntelliShellService {
20 #[instrument(skip_all)]
25 pub async fn load_workspace_commands(&self) -> Result<u64> {
26 if env::var("INTELLI_SKIP_WORKSPACE")
27 .map(|v| v != "1" && v.to_lowercase() != "true")
28 .unwrap_or(true)
29 && let Some(workspace_commands) = find_workspace_commands_file()
30 {
31 tracing::debug!("Found workspace commands at {}", workspace_commands.display());
32
33 self.storage.setup_workspace_storage().await?;
35
36 let file = File::open(&workspace_commands).await?;
38 let commands_stream = parse_commands(file, vec!["#workspace".into()], CATEGORY_WORKSPACE, SOURCE_WORKSPACE);
39
40 let (loaded, _) = self.storage.import_commands(commands_stream, None, false, true).await?;
42
43 tracing::info!(
44 "Loaded {loaded} workspace commands from {}",
45 workspace_commands.display()
46 );
47
48 Ok(loaded)
49 } else {
50 Ok(0)
51 }
52 }
53
54 #[instrument(skip_all)]
56 pub async fn is_storage_empty(&self) -> Result<bool> {
57 self.storage.is_empty().await
58 }
59
60 #[instrument(skip_all)]
62 pub async fn insert_command(&self, command: Command) -> Result<Command, InsertError> {
63 if command.cmd.is_empty() {
65 return Err(InsertError::Invalid("Command cannot be empty"));
66 }
67
68 tracing::info!("Bookmarking command: {command}");
70 self.storage.insert_command(command).await
71 }
72
73 #[instrument(skip_all)]
75 pub async fn update_command(&self, command: Command) -> Result<Command, UpdateError> {
76 if command.cmd.is_empty() {
78 return Err(UpdateError::Invalid("Command cannot be empty"));
79 }
80
81 tracing::info!("Updating command '{}': {}", command.id, command.cmd);
83 self.storage.update_command(command).await
84 }
85
86 #[instrument(skip_all)]
88 pub async fn increment_command_usage(&self, command_id: Uuid) -> Result<i32, UpdateError> {
89 tracing::info!("Increasing usage for command '{command_id}'");
90 self.storage
91 .increment_command_usage(command_id, get_working_dir())
92 .await
93 }
94
95 #[instrument(skip_all)]
97 pub async fn delete_command(&self, id: Uuid) -> Result<()> {
98 tracing::info!("Deleting command: {}", id);
99 self.storage.delete_command(id).await
100 }
101
102 #[instrument(skip_all)]
104 pub async fn search_tags(
105 &self,
106 mode: SearchMode,
107 user_only: bool,
108 query: &str,
109 cursor_pos: usize,
110 ) -> Result<Option<Vec<Tag>>, SearchError> {
111 let Some((editing_tag, other_tags, cleaned_text)) =
112 extract_tags_with_editing_and_cleaned_text(query, cursor_pos)
113 else {
114 return Ok(None);
115 };
116
117 tracing::info!(
118 "Searching for tags{} [{mode:?}]: {query}",
119 if user_only { " (user only)" } else { "" }
120 );
121 tracing::trace!("Editing: {editing_tag} Other: {other_tags:?}");
122
123 let filter = SearchCommandsFilter {
124 category: user_only.then(|| vec![CATEGORY_USER.to_string()]),
125 source: None,
126 tags: Some(other_tags),
127 search_mode: mode,
128 search_term: Some(cleaned_text),
129 };
130
131 Ok(Some(
132 self.storage
133 .find_tags(filter, Some(editing_tag), &self.tuning.commands)
134 .await?,
135 ))
136 }
137
138 #[instrument(skip_all)]
140 pub async fn search_commands(
141 &self,
142 mode: SearchMode,
143 user_only: bool,
144 query: &str,
145 ) -> Result<(Vec<Command>, bool), SearchError> {
146 tracing::info!(
147 "Searching for commands{} [{mode:?}]: {query}",
148 if user_only { " (user only)" } else { "" }
149 );
150
151 let query = query.trim();
152 let filter = if query.is_empty() {
153 SearchCommandsFilter {
155 category: Some(if user_only {
156 vec![CATEGORY_USER.to_string()]
157 } else {
158 vec![CATEGORY_USER.to_string(), CATEGORY_WORKSPACE.to_string()]
159 }),
160 search_mode: mode,
161 ..Default::default()
162 }
163 } else {
164 let (tags, search_term) = match extract_tags_and_cleaned_text(query) {
166 Some((tags, cleaned_query)) => (Some(tags), Some(cleaned_query)),
167 None => (None, Some(query.to_string())),
168 };
169
170 SearchCommandsFilter {
172 category: user_only.then(|| vec![CATEGORY_USER.to_string()]),
173 source: None,
174 tags,
175 search_mode: mode,
176 search_term,
177 }
178 };
179
180 self.storage
182 .find_commands(filter, get_working_dir(), &self.tuning.commands)
183 .await
184 }
185}
186
187fn find_workspace_commands_file() -> Option<PathBuf> {
191 let working_dir = PathBuf::from(get_working_dir());
192 let mut current = Some(working_dir.as_path());
193 while let Some(parent) = current {
194 let candidate = parent.join(".intellishell");
195 if candidate.is_file() {
196 return Some(candidate);
197 }
198
199 if parent.join(".git").is_dir() {
200 return None;
202 }
203
204 current = parent.parent();
205 }
206 None
207}