pocket_cli/cards/
snippet.rs

1use crate::cards::{Card, CardConfig, CardCommand};
2use crate::utils::{read_clipboard, summarize_text, SummaryMetadata};
3use crate::models::{Entry, ContentType};
4use crate::storage::StorageManager;
5use anyhow::{Result, anyhow, Context};
6use std::path::PathBuf;
7
8/// Card for enhanced snippet functionality
9pub struct SnippetCard {
10    /// Name of the card
11    name: String,
12    
13    /// Version of the card
14    version: String,
15    
16    /// Description of the card
17    description: String,
18    
19    /// Configuration for the card
20    config: SnippetCardConfig,
21    
22    /// Path to the Pocket data directory
23    data_dir: PathBuf,
24}
25
26/// Configuration for the snippet card
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct SnippetCardConfig {
29    /// Whether to automatically summarize snippets
30    pub auto_summarize: bool,
31    
32    /// Maximum length for auto-generated summaries
33    pub max_summary_length: usize,
34    
35    /// Whether to include summaries in search results
36    pub search_in_summaries: bool,
37    
38    /// Weight to give summary matches in search results (0.0-1.0)
39    pub summary_search_weight: f32,
40}
41
42impl Default for SnippetCardConfig {
43    fn default() -> Self {
44        Self {
45            auto_summarize: true,
46            max_summary_length: 150,
47            search_in_summaries: true,
48            summary_search_weight: 0.7,
49        }
50    }
51}
52
53impl SnippetCard {
54    /// Creates a new snippet card
55    pub fn new(data_dir: impl AsRef<std::path::Path>) -> Self {
56        Self {
57            name: "snippet".to_string(),
58            version: env!("CARGO_PKG_VERSION").to_string(),
59            description: "Enhanced snippet functionality with clipboard and summarization features".to_string(),
60            config: SnippetCardConfig::default(),
61            data_dir: data_dir.as_ref().to_path_buf(),
62        }
63    }
64    
65    /// Adds a snippet from clipboard content
66    pub fn add_from_clipboard(&self, 
67                              user_summary: Option<&str>, 
68                              backpack: Option<&str>) -> Result<String> {
69        // Read content from clipboard
70        let content = read_clipboard()
71            .context("Failed to read from clipboard")?;
72            
73        if content.trim().is_empty() {
74            return Err(anyhow!("Clipboard is empty"));
75        }
76        
77        // Detect content type
78        let content_type = crate::utils::detect_content_type(None, Some(&content));
79        
80        // Create a title from the first line, or first 50 chars if no lines
81        let title = content.lines().next()
82            .unwrap_or(&content[..std::cmp::min(50, content.len())])
83            .to_string();
84        
85        // Create entry
86        let mut entry = Entry::new(title, content_type, None, vec![]);
87        
88        // Create summary metadata
89        let summary = if let Some(manual_summary) = user_summary {
90            // User provided a summary, use it
91            SummaryMetadata::new(manual_summary.to_string(), false)
92        } else if self.config.auto_summarize {
93            // Auto-generate a summary
94            let summary = summarize_text(&content)
95                .unwrap_or_else(|_| {
96                    // Fallback: use first line or first 100 chars
97                    content.lines().next()
98                        .unwrap_or(&content[..std::cmp::min(100, content.len())])
99                        .to_string()
100                });
101                
102            // Truncate if needed
103            let summary = if summary.len() > self.config.max_summary_length {
104                format!("{}...", &summary[..self.config.max_summary_length - 3])
105            } else {
106                summary
107            };
108            
109            SummaryMetadata::new(summary, true)
110        } else {
111            // No summarization requested
112            SummaryMetadata::new("".to_string(), true)
113        };
114        
115        // Add summary metadata to entry
116        entry.add_metadata("summary", &summary.to_json());
117        
118        // Save the entry
119        let storage = StorageManager::new()?;
120        storage.save_entry(&entry, &content, backpack)?;
121        
122        Ok(entry.id)
123    }
124    
125    /// Searches for snippets, including in summaries if configured
126    pub fn search(&self, query: &str, limit: usize, backpack: Option<&str>) -> Result<Vec<(Entry, String, Option<SummaryMetadata>)>> {
127        let storage = StorageManager::new()?;
128        
129        // Basic search first
130        let mut results = Vec::new();
131        let entries = storage.search_entries(query, backpack, limit)?;
132        
133        for (entry, content) in entries {
134            // Load summary metadata if it exists
135            let summary = if let Some(summary_json) = entry.get_metadata("summary") {
136                match SummaryMetadata::from_json(summary_json) {
137                    Ok(summary) => Some(summary),
138                    Err(_) => None,
139                }
140            } else {
141                None
142            };
143            
144            results.push((entry, content, summary));
145        }
146        
147        // If searching in summaries is enabled, also search in summaries
148        if self.config.search_in_summaries {
149            let all_entries = storage.list_entries(backpack)?;
150            
151            for entry in all_entries {
152                // Skip entries already in results
153                if results.iter().any(|(e, _, _)| e.id == entry.id) {
154                    continue;
155                }
156                
157                // Get summary metadata
158                if let Some(summary_json) = entry.get_metadata("summary") {
159                    if let Ok(summary) = SummaryMetadata::from_json(summary_json) {
160                        // Check if query matches summary
161                        if summary.summary.to_lowercase().contains(&query.to_lowercase()) {
162                            // Load the entry content
163                            if let Ok((entry, content)) = storage.load_entry(&entry.id, backpack) {
164                                results.push((entry, content, Some(summary)));
165                                
166                                // Check if we've reached the limit
167                                if results.len() >= limit {
168                                    break;
169                                }
170                            }
171                        }
172                    }
173                }
174            }
175        }
176        
177        Ok(results)
178    }
179}
180
181impl Card for SnippetCard {
182    fn name(&self) -> &str {
183        &self.name
184    }
185    
186    fn version(&self) -> &str {
187        &self.version
188    }
189    
190    fn description(&self) -> &str {
191        &self.description
192    }
193    
194    fn initialize(&mut self, config: &CardConfig) -> Result<()> {
195        // Load configuration if available
196        if let Some(options) = &config.options.get("config") {
197            if let Ok(card_config) = serde_json::from_value::<SnippetCardConfig>((*options).clone()) {
198                self.config = card_config;
199            }
200        }
201        
202        Ok(())
203    }
204    
205    fn execute(&self, command: &str, args: &[String]) -> Result<()> {
206        match command {
207            "add-from-clipboard" => {
208                let mut user_summary = None;
209                let mut backpack = None;
210                
211                // Parse arguments
212                let mut i = 0;
213                while i < args.len() {
214                    match args[i].as_str() {
215                        "--summarize" => {
216                            if i + 1 < args.len() {
217                                user_summary = Some(args[i + 1].as_str());
218                                i += 2;
219                            } else {
220                                return Err(anyhow!("--summarize requires a summary string"));
221                            }
222                        },
223                        "--backpack" => {
224                            if i + 1 < args.len() {
225                                backpack = Some(args[i + 1].as_str());
226                                i += 2;
227                            } else {
228                                return Err(anyhow!("--backpack requires a backpack name"));
229                            }
230                        },
231                        _ => {
232                            i += 1;
233                        }
234                    }
235                }
236                
237                // Add from clipboard
238                let id = self.add_from_clipboard(user_summary, backpack)?;
239                println!("Added snippet from clipboard with ID: {}", id);
240                Ok(())
241            },
242            "search" => {
243                if args.is_empty() {
244                    return Err(anyhow!("search requires a query string"));
245                }
246                
247                let query = &args[0];
248                let limit = if args.len() > 1 {
249                    args[1].parse().unwrap_or(10)
250                } else {
251                    10
252                };
253                
254                let mut backpack = None;
255                let mut i = 2;
256                while i < args.len() {
257                    match args[i].as_str() {
258                        "--backpack" => {
259                            if i + 1 < args.len() {
260                                backpack = Some(args[i + 1].as_str());
261                                i += 2;
262                            } else {
263                                return Err(anyhow!("--backpack requires a backpack name"));
264                            }
265                        },
266                        _ => {
267                            i += 1;
268                        }
269                    }
270                }
271                
272                // Search
273                let results = self.search(query, limit, backpack)?;
274                
275                if results.is_empty() {
276                    println!("No results found");
277                    return Ok(());
278                }
279                
280                println!("Search results for '{}':", query);
281                for (i, (entry, content, summary)) in results.iter().enumerate() {
282                    println!("{}. {} ({})", i + 1, entry.title, entry.id);
283                    
284                    // Show summary if available
285                    if let Some(summary) = summary {
286                        println!("   Summary: {}", summary.summary);
287                    }
288                    
289                    // Show snippet of content
290                    let preview = if content.len() > 100 {
291                        format!("{}...", &content[..97])
292                    } else {
293                        content.clone()
294                    };
295                    println!("   Content: {}", preview.replace('\n', " "));
296                    println!();
297                }
298                
299                Ok(())
300            },
301            "config" => {
302                // Show current configuration
303                println!("Snippet card configuration:");
304                println!("  Auto-summarize: {}", self.config.auto_summarize);
305                println!("  Max summary length: {}", self.config.max_summary_length);
306                println!("  Search in summaries: {}", self.config.search_in_summaries);
307                println!("  Summary search weight: {}", self.config.summary_search_weight);
308                Ok(())
309            },
310            _ => Err(anyhow!("Unknown command: {}", command))
311        }
312    }
313    
314    fn commands(&self) -> Vec<CardCommand> {
315        vec![
316            CardCommand {
317                name: "add-from-clipboard".to_string(),
318                description: "Add a snippet from clipboard content".to_string(),
319                usage: "pocket cards execute snippet add-from-clipboard [--summarize SUMMARY] [--backpack BACKPACK]".to_string(),
320            },
321            CardCommand {
322                name: "search".to_string(),
323                description: "Search for snippets, including in summaries".to_string(),
324                usage: "pocket cards execute snippet search QUERY [LIMIT] [--backpack BACKPACK]".to_string(),
325            },
326            CardCommand {
327                name: "config".to_string(),
328                description: "Show current snippet card configuration".to_string(),
329                usage: "pocket cards execute snippet config".to_string(),
330            },
331        ]
332    }
333    
334    fn cleanup(&mut self) -> Result<()> {
335        Ok(())
336    }
337}