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
8pub struct SnippetCard {
10 name: String,
12
13 version: String,
15
16 description: String,
18
19 config: SnippetCardConfig,
21
22 data_dir: PathBuf,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct SnippetCardConfig {
29 pub auto_summarize: bool,
31
32 pub max_summary_length: usize,
34
35 pub search_in_summaries: bool,
37
38 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 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 pub fn add_from_clipboard(&self,
67 user_summary: Option<&str>,
68 backpack: Option<&str>) -> Result<String> {
69 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 let content_type = crate::utils::detect_content_type(None, Some(&content));
79
80 let title = content.lines().next()
82 .unwrap_or(&content[..std::cmp::min(50, content.len())])
83 .to_string();
84
85 let mut entry = Entry::new(title, content_type, None, vec![]);
87
88 let summary = if let Some(manual_summary) = user_summary {
90 SummaryMetadata::new(manual_summary.to_string(), false)
92 } else if self.config.auto_summarize {
93 let summary = summarize_text(&content)
95 .unwrap_or_else(|_| {
96 content.lines().next()
98 .unwrap_or(&content[..std::cmp::min(100, content.len())])
99 .to_string()
100 });
101
102 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 SummaryMetadata::new("".to_string(), true)
113 };
114
115 entry.add_metadata("summary", &summary.to_json());
117
118 let storage = StorageManager::new()?;
120 storage.save_entry(&entry, &content, backpack)?;
121
122 Ok(entry.id)
123 }
124
125 pub fn search(&self, query: &str, limit: usize, backpack: Option<&str>) -> Result<Vec<(Entry, String, Option<SummaryMetadata>)>> {
127 let storage = StorageManager::new()?;
128
129 let mut results = Vec::new();
131 let entries = storage.search_entries(query, backpack, limit)?;
132
133 for (entry, content) in entries {
134 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 self.config.search_in_summaries {
149 let all_entries = storage.list_entries(backpack)?;
150
151 for entry in all_entries {
152 if results.iter().any(|(e, _, _)| e.id == entry.id) {
154 continue;
155 }
156
157 if let Some(summary_json) = entry.get_metadata("summary") {
159 if let Ok(summary) = SummaryMetadata::from_json(summary_json) {
160 if summary.summary.to_lowercase().contains(&query.to_lowercase()) {
162 if let Ok((entry, content)) = storage.load_entry(&entry.id, backpack) {
164 results.push((entry, content, Some(summary)));
165
166 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 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 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 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 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 if let Some(summary) = summary {
286 println!(" Summary: {}", summary.summary);
287 }
288
289 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 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}