intelli_shell/model/
command.rs1use std::{
2 collections::HashSet,
3 fmt::{self, Display},
4};
5
6use chrono::{DateTime, Utc};
7use clap::ValueEnum;
8use enum_cycling::EnumCycle;
9use regex::Regex;
10use serde::Deserialize;
11use uuid::Uuid;
12
13use crate::utils::{extract_tags_from_description, flatten_str, remove_newlines};
14
15pub const CATEGORY_USER: &str = "user";
17
18pub const CATEGORY_WORKSPACE: &str = "workspace";
20
21pub const SOURCE_USER: &str = "user";
23
24pub const SOURCE_AI: &str = "ai";
26
27pub const SOURCE_TLDR: &str = "tldr";
29
30pub const SOURCE_IMPORT: &str = "import";
32
33pub const SOURCE_WORKSPACE: &str = "workspace";
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, ValueEnum, EnumCycle, strum::Display)]
37#[cfg_attr(test, derive(strum::EnumIter))]
38#[serde(rename_all = "snake_case")]
39#[strum(serialize_all = "snake_case")]
40pub enum SearchMode {
42 #[default]
44 Auto,
45 Fuzzy,
47 Regex,
49 Exact,
51 Relaxed,
56}
57
58#[derive(Default, Clone)]
60#[cfg_attr(debug_assertions, derive(Debug))]
61pub struct SearchCommandsFilter {
62 pub category: Option<Vec<String>>,
64 pub source: Option<String>,
66 pub tags: Option<Vec<String>>,
68 pub search_mode: SearchMode,
70 pub search_term: Option<String>,
75}
76impl SearchCommandsFilter {
77 pub fn cleaned(self) -> Self {
79 let SearchCommandsFilter {
80 category,
81 source,
82 tags,
83 search_mode,
84 search_term,
85 } = self;
86 Self {
87 category: category
88 .map(|v| {
89 let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
90 let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
91 for t in &v {
92 let t = t.trim();
93 if !t.is_empty() && seen.insert(t) {
94 final_vec.push(t.to_string());
95 }
96 }
97 final_vec
98 })
99 .filter(|t| !t.is_empty()),
100 source: source.map(|t| t.trim().to_string()).filter(|s| !s.is_empty()),
101 tags: tags
102 .map(|v| {
103 let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
104 let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
105 for t in &v {
106 let t = t.trim();
107 if !t.is_empty() && seen.insert(t) {
108 final_vec.push(t.to_string());
109 }
110 }
111 final_vec
112 })
113 .filter(|t| !t.is_empty()),
114 search_mode,
115 search_term: search_term.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
116 }
117 }
118}
119
120#[derive(Clone)]
121#[cfg_attr(debug_assertions, derive(Debug))]
122#[cfg_attr(test, derive(Default))]
123pub struct Command {
124 pub id: Uuid,
126 pub category: String,
128 pub source: String,
130 pub alias: Option<String>,
132 pub cmd: String,
134 pub flat_cmd: String,
136 pub description: Option<String>,
138 pub flat_description: Option<String>,
140 pub tags: Option<Vec<String>>,
142 pub created_at: DateTime<Utc>,
144 pub updated_at: Option<DateTime<Utc>>,
146}
147
148impl Command {
149 pub fn new(category: impl Into<String>, source: impl Into<String>, cmd: impl Into<String>) -> Self {
151 let cmd = remove_newlines(cmd.into());
152 Self {
153 id: Uuid::now_v7(),
154 category: category.into(),
155 source: source.into(),
156 alias: None,
157 flat_cmd: flatten_str(&cmd),
158 cmd,
159 description: None,
160 flat_description: None,
161 tags: None,
162 created_at: Utc::now(),
163 updated_at: None,
164 }
165 }
166
167 pub fn with_alias(mut self, alias: Option<String>) -> Self {
169 self.alias = alias.filter(|a| !a.trim().is_empty());
170 self
171 }
172
173 pub fn with_cmd(mut self, cmd: String) -> Self {
175 self.flat_cmd = flatten_str(&cmd);
176 self.cmd = cmd;
177 self
178 }
179
180 pub fn with_description(mut self, description: Option<String>) -> Self {
182 let description = description.filter(|d| !d.trim().is_empty());
183 self.tags = extract_tags_from_description(description.as_deref());
184 self.flat_description = description.as_ref().map(flatten_str);
185 self.description = description;
186 self
187 }
188
189 #[cfg(test)]
190 pub fn with_tags(mut self, tags: Option<Vec<String>>) -> Self {
192 self.tags = tags.filter(|t| !t.is_empty());
193 self
194 }
195
196 pub fn matches(&self, regex: &Regex) -> bool {
198 regex.is_match(&self.cmd) || self.description.as_ref().is_some_and(|d| regex.is_match(d))
199 }
200}
201
202impl Display for Command {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 let cmd = &self.cmd;
206 let desc = self.description.as_deref().filter(|s| !s.is_empty());
207 let alias = self.alias.as_deref();
208
209 match (desc, alias) {
210 (None, None) => return writeln!(f, "#\n{cmd}"),
212 (Some(d), Some(a)) => {
214 if d.contains('\n') {
215 writeln!(f, "# [alias:{a}]")?;
217 for line in d.lines() {
218 writeln!(f, "# {line}")?;
219 }
220 } else {
221 writeln!(f, "# [alias:{a}] {d}")?;
223 }
224 }
225 (Some(d), None) => {
227 for line in d.lines() {
228 writeln!(f, "# {line}")?;
229 }
230 }
231 (None, Some(a)) => {
233 writeln!(f, "# [alias:{a}]")?;
234 }
235 };
236
237 writeln!(f, "{cmd}")
239 }
240}