Skip to main content

prompts_cli/core/
mod.rs

1use anyhow::Result;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use fuzzy_matcher::FuzzyMatcher;
4use sha2::{Digest, Sha256};
5
6pub struct Prompts {
7    storage: Box<dyn crate::storage::Storage + Send + Sync>,
8}
9
10impl Prompts {
11    pub fn new(storage: Box<dyn crate::storage::Storage + Send + Sync>) -> Self {
12        Self { storage }
13    }
14
15    pub async fn add_prompt(&self, prompt: &mut crate::storage::Prompt) -> Result<bool> {
16        let prompts = self.storage.load_prompts().await?;
17        if prompts.iter().any(|p| p.hash == prompt.hash) {
18            return Ok(false);
19        }
20        self.storage.save_prompt(prompt).await?;
21        Ok(true)
22    }
23
24    pub async fn list_prompts(&self, tags: Option<Vec<String>>) -> Result<Vec<crate::storage::Prompt>> {
25        let prompts = self.storage.load_prompts().await?;
26        if let Some(tags) = tags {
27            let search_results = search_prompts(&prompts, "", &tags, &[]);
28            Ok(search_results)
29        } else {
30            Ok(prompts)
31        }
32    }
33
34    pub async fn show_prompt(&self, query: &str, tags: Option<Vec<String>>) -> Result<Vec<crate::storage::Prompt>> {
35        let prompts = self.storage.load_prompts().await?;
36        let search_results = search_prompts(&prompts, query, &tags.unwrap_or_default(), &[]);
37        Ok(search_results)
38    }
39
40    pub async fn edit_prompt(
41        &self,
42        hash: &str,
43        new_text: Option<String>,
44        add_tags: Option<Vec<String>>,
45        remove_tags: Option<Vec<String>>,
46        add_categories: Option<Vec<String>>,
47        remove_categories: Option<Vec<String>>,
48    ) -> Result<()> {
49        let mut prompts = self.storage.load_prompts().await?;
50        let prompt_to_edit = prompts.iter_mut().find(|p| p.hash == hash);
51
52        if let Some(prompt) = prompt_to_edit {
53            if let Some(text) = new_text {
54                prompt.content = text;
55                let hash = Sha256::digest(prompt.content.as_bytes());
56                prompt.hash = format!("{:x}", hash);
57            }
58
59            let mut tags = prompt.tags.clone().unwrap_or_default();
60            if let Some(tags_to_add) = add_tags {
61                tags.extend(tags_to_add);
62                tags.sort();
63                tags.dedup();
64            }
65            if let Some(tags_to_remove) = remove_tags {
66                tags.retain(|t| !tags_to_remove.contains(t));
67            }
68            prompt.tags = Some(tags);
69
70            let mut categories = prompt.categories.clone().unwrap_or_default();
71            if let Some(categories_to_add) = add_categories {
72                categories.extend(categories_to_add);
73                categories.sort();
74                categories.dedup();
75            }
76            if let Some(categories_to_remove) = remove_categories {
77                categories.retain(|c| !categories_to_remove.contains(c));
78            }
79            prompt.categories = Some(categories);
80
81            self.storage.delete_prompt(hash).await?;
82            self.storage.save_prompt(prompt).await?;
83        }
84
85        Ok(())
86    }
87
88    pub async fn delete_prompt(&self, hash: &str) -> Result<()> {
89        self.storage.delete_prompt(hash).await
90    }
91}
92
93pub fn search_prompts(prompts: &[crate::storage::Prompt], query: &str, tags: &[String], categories: &[String]) -> Vec<crate::storage::Prompt> {
94    let matcher = SkimMatcherV2::default();
95    prompts.iter().filter(|p| {
96        let content_match = query.is_empty() || matcher.fuzzy_match(&p.content, query).is_some();
97        let tags_match = tags.is_empty() || p.tags.as_ref().map_or(false, |ptags| {
98            tags.iter().all(|tag| ptags.contains(tag))
99        });
100        let categories_match = categories.is_empty() || p.categories.as_ref().map_or(false, |pcats| {
101            categories.iter().all(|cat| pcats.contains(cat))
102        });
103        content_match && tags_match && categories_match
104    }).cloned().collect()
105}