intelli_shell/model/
command.rs

1use std::{collections::HashSet, fmt::Display};
2
3use chrono::{DateTime, Utc};
4use clap::ValueEnum;
5use enum_cycling::EnumCycle;
6use serde::Deserialize;
7use uuid::Uuid;
8
9use crate::utils::{extract_tags_from_description, flatten_str};
10
11/// Category for user defined commands
12pub const CATEGORY_USER: &str = "user";
13
14/// Category for workspace defined commands
15pub const CATEGORY_WORKSPACE: &str = "workspace";
16
17/// Source for user defined commands
18pub const SOURCE_USER: &str = "user";
19
20/// Source for tldr fetched commands
21pub const SOURCE_TLDR: &str = "tldr";
22
23/// Source for imported commands
24pub const SOURCE_IMPORT: &str = "import";
25
26/// Source for workspace-level commands
27pub const SOURCE_WORKSPACE: &str = "workspace";
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, ValueEnum, EnumCycle, strum::Display)]
30#[cfg_attr(test, derive(strum::EnumIter))]
31#[serde(rename_all = "snake_case")]
32#[strum(serialize_all = "snake_case")]
33/// Determines the strategy used for searching commands
34pub enum SearchMode {
35    /// An internal algorithm will be used to understand human search patterns and decide the best search strategy
36    #[default]
37    Auto,
38    /// Employs a set of predefined rules to perform a fuzzy search
39    Fuzzy,
40    /// Treats the input query as a regular expression, allowing for complex pattern matching
41    Regex,
42    /// Return commands that precisely match the entire input query only
43    Exact,
44    /// Attempts to find the maximum number of potentially relevant commands.
45    ///
46    /// It uses a broader set of matching criteria and may include partial matches, matches within descriptions, or
47    /// commands that share keywords.
48    Relaxed,
49}
50
51/// Represents the filtering criteria for searching for commands
52#[derive(Default, Clone)]
53#[cfg_attr(debug_assertions, derive(Debug))]
54pub struct SearchCommandsFilter {
55    /// Filter commands by a specific category (`user`, `workspace` or tldr's category)
56    pub category: Option<Vec<String>>,
57    /// Filter commands by their original source (`user`, `tldr`, `import`, `workspace`)
58    pub source: Option<String>,
59    /// Filter commands by a list of tags, only commands matching all of the provided tags will be included
60    pub tags: Option<Vec<String>>,
61    /// Specifies the search strategy to be used for matching the `search_term`
62    pub search_mode: SearchMode,
63    /// The actual term or query string to search for.
64    ///
65    /// This term will be matched against command names, aliases, or descriptions according to the specified
66    /// `search_mode`.
67    pub search_term: Option<String>,
68}
69impl SearchCommandsFilter {
70    /// Returns a cleaned version of self, trimming and removing empty or duplicated filters
71    pub fn cleaned(self) -> Self {
72        let SearchCommandsFilter {
73            category,
74            source,
75            tags,
76            search_mode,
77            search_term,
78        } = self;
79        Self {
80            category: category
81                .map(|v| {
82                    let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
83                    let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
84                    for t in &v {
85                        let t = t.trim();
86                        if !t.is_empty() && seen.insert(t) {
87                            final_vec.push(t.to_string());
88                        }
89                    }
90                    final_vec
91                })
92                .filter(|t| !t.is_empty()),
93            source: source.map(|t| t.trim().to_string()).filter(|s| !s.is_empty()),
94            tags: tags
95                .map(|v| {
96                    let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
97                    let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
98                    for t in &v {
99                        let t = t.trim();
100                        if !t.is_empty() && seen.insert(t) {
101                            final_vec.push(t.to_string());
102                        }
103                    }
104                    final_vec
105                })
106                .filter(|t| !t.is_empty()),
107            search_mode,
108            search_term: search_term.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
109        }
110    }
111}
112
113#[derive(Clone)]
114#[cfg_attr(debug_assertions, derive(Debug))]
115#[cfg_attr(test, derive(Default))]
116pub struct Command {
117    /// Unique identifier for the command
118    pub id: Uuid,
119    /// Category of the command (`user`, `workspace` or tldr's category)
120    pub category: String,
121    /// Category of the command (`user`, `tldr`, `import`, `workspace`)
122    pub source: String,
123    /// Optional alias for easier recall
124    pub alias: Option<String>,
125    /// The actual command string, potentially with `{{placeholders}}`
126    pub cmd: String,
127    /// Flattened version of `cmd`
128    pub flat_cmd: String,
129    /// Optional user-provided description
130    pub description: Option<String>,
131    /// Flattened version of `description`
132    pub flat_description: Option<String>,
133    /// Tags associated with the command (including the hashtag `#`)
134    pub tags: Option<Vec<String>>,
135    /// The date and time when the command was created
136    pub created_at: DateTime<Utc>,
137    /// The date and time when the command was last updated
138    pub updated_at: Option<DateTime<Utc>>,
139}
140
141impl Command {
142    /// Creates a new command, with zero usage
143    pub fn new(category: impl Into<String>, source: impl Into<String>, cmd: impl Into<String>) -> Self {
144        let cmd = cmd.into();
145        Self {
146            id: Uuid::now_v7(),
147            category: category.into(),
148            source: source.into(),
149            alias: None,
150            flat_cmd: flatten_str(&cmd),
151            cmd,
152            description: None,
153            flat_description: None,
154            tags: None,
155            created_at: Utc::now(),
156            updated_at: None,
157        }
158    }
159
160    /// Updates the alias of the command
161    pub fn with_alias(mut self, alias: Option<String>) -> Self {
162        self.alias = alias.filter(|a| !a.trim().is_empty());
163        self
164    }
165
166    /// Updates the cmd of the command
167    pub fn with_cmd(mut self, cmd: String) -> Self {
168        self.flat_cmd = flatten_str(&cmd);
169        self.cmd = cmd;
170        self
171    }
172
173    /// Updates the description (and tags) of the command
174    pub fn with_description(mut self, description: Option<String>) -> Self {
175        let description = description.filter(|d| !d.trim().is_empty());
176        self.tags = extract_tags_from_description(description.as_deref());
177        self.flat_description = description.as_ref().map(flatten_str);
178        self.description = description;
179        self
180    }
181
182    #[cfg(test)]
183    /// Updates the tags of the command
184    pub fn with_tags(mut self, tags: Option<Vec<String>>) -> Self {
185        self.tags = tags.filter(|t| !t.is_empty());
186        self
187    }
188}
189
190impl Display for Command {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "{}", self.cmd)
193    }
194}