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;
7use std::fs;
8
9pub struct SnippetCard {
11 name: String,
13
14 version: String,
16
17 description: String,
19
20 config: SnippetCardConfig,
22
23 data_dir: PathBuf,
25}
26
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct SnippetCardConfig {
30 pub auto_summarize: bool,
32
33 pub max_summary_length: usize,
35
36 pub search_in_summaries: bool,
38
39 pub summary_search_weight: f32,
41}
42
43impl Default for SnippetCardConfig {
44 fn default() -> Self {
45 Self {
46 auto_summarize: true,
47 max_summary_length: 150,
48 search_in_summaries: true,
49 summary_search_weight: 0.7,
50 }
51 }
52}
53
54impl SnippetCard {
55 pub fn new(data_dir: impl AsRef<std::path::Path>) -> Self {
57 Self {
58 name: "snippet".to_string(),
59 version: env!("CARGO_PKG_VERSION").to_string(),
60 description: "Enhanced snippet functionality with clipboard and summarization features".to_string(),
61 config: SnippetCardConfig::default(),
62 data_dir: data_dir.as_ref().to_path_buf(),
63 }
64 }
65
66 pub fn add(&self,
68 file: Option<&str>,
69 message: Option<&str>,
70 use_editor: bool,
71 use_clipboard: bool,
72 backpack: Option<&str>,
73 summarize: Option<&str>) -> Result<String> {
74 let content = if let Some(file_path) = file {
76 fs::read_to_string(file_path)
78 .context(format!("Failed to read file: {}", file_path))?
79 } else if use_editor {
80 crate::utils::open_editor(None)
82 .context("Failed to open editor")?
83 } else if use_clipboard {
84 read_clipboard()
86 .context("Failed to read from clipboard")?
87 } else {
88 return Err(anyhow!("No content source provided. Use --file, --editor, or --clipboard options"));
90 };
91
92 if content.trim().is_empty() {
93 return Err(anyhow!("Content is empty"));
94 }
95
96 let content_type = if let Some(file_path) = file {
98 let path = PathBuf::from(file_path);
99 crate::utils::detect_content_type(Some(&path), Some(&content))
100 } else {
101 crate::utils::detect_content_type(None, Some(&content))
102 };
103
104 let title = if let Some(msg) = message {
106 msg.to_string()
107 } else {
108 content.lines().next()
109 .unwrap_or(&content[..std::cmp::min(50, content.len())])
110 .to_string()
111 };
112
113 let mut entry = Entry::new(title, content_type, None, vec![]);
115
116 let summary = if let Some(manual_summary) = summarize {
118 SummaryMetadata::new(manual_summary.to_string(), false)
120 } else if self.config.auto_summarize {
121 let summary = summarize_text(&content)
123 .unwrap_or_else(|_| {
124 content.lines().next()
126 .unwrap_or(&content[..std::cmp::min(100, content.len())])
127 .to_string()
128 });
129
130 let summary = if summary.len() > self.config.max_summary_length {
132 format!("{}...", &summary[..self.config.max_summary_length - 3])
133 } else {
134 summary
135 };
136
137 SummaryMetadata::new(summary, true)
138 } else {
139 SummaryMetadata::new("".to_string(), true)
141 };
142
143 entry.add_metadata("summary", &summary.to_json());
145
146 let storage = StorageManager::new()?;
148 storage.save_entry(&entry, &content, backpack)?;
149
150 Ok(entry.id)
151 }
152
153 pub fn add_from_clipboard(&self,
155 user_summary: Option<&str>,
156 backpack: Option<&str>) -> Result<String> {
157 let content = read_clipboard()
159 .context("Failed to read from clipboard")?;
160
161 if content.trim().is_empty() {
162 return Err(anyhow!("Clipboard is empty"));
163 }
164
165 let content_type = crate::utils::detect_content_type(None, Some(&content));
167
168 let title = content.lines().next()
170 .unwrap_or(&content[..std::cmp::min(50, content.len())])
171 .to_string();
172
173 let mut entry = Entry::new(title, content_type, None, vec![]);
175
176 let summary = if let Some(manual_summary) = user_summary {
178 SummaryMetadata::new(manual_summary.to_string(), false)
180 } else if self.config.auto_summarize {
181 let summary = summarize_text(&content)
183 .unwrap_or_else(|_| {
184 content.lines().next()
186 .unwrap_or(&content[..std::cmp::min(100, content.len())])
187 .to_string()
188 });
189
190 let summary = if summary.len() > self.config.max_summary_length {
192 format!("{}...", &summary[..self.config.max_summary_length - 3])
193 } else {
194 summary
195 };
196
197 SummaryMetadata::new(summary, true)
198 } else {
199 SummaryMetadata::new("".to_string(), true)
201 };
202
203 entry.add_metadata("summary", &summary.to_json());
205
206 let storage = StorageManager::new()?;
208 storage.save_entry(&entry, &content, backpack)?;
209
210 Ok(entry.id)
211 }
212
213 pub fn search(&self, query: &str, limit: usize, backpack: Option<&str>) -> Result<Vec<(Entry, String, Option<SummaryMetadata>)>> {
215 let storage = StorageManager::new()?;
216
217 let mut results = Vec::new();
219 let entries = storage.search_entries(query, backpack, limit)?;
220
221 for (entry, content) in entries {
222 let summary = if let Some(summary_json) = entry.get_metadata("summary") {
224 match SummaryMetadata::from_json(summary_json) {
225 Ok(summary) => Some(summary),
226 Err(_) => None,
227 }
228 } else {
229 None
230 };
231
232 results.push((entry, content, summary));
233 }
234
235 if self.config.search_in_summaries {
237 let all_entries = storage.list_entries(backpack)?;
238
239 for entry in all_entries {
240 if results.iter().any(|(e, _, _)| e.id == entry.id) {
242 continue;
243 }
244
245 if let Some(summary_json) = entry.get_metadata("summary") {
247 if let Ok(summary) = SummaryMetadata::from_json(summary_json) {
248 if summary.summary.to_lowercase().contains(&query.to_lowercase()) {
250 if let Ok((entry, content)) = storage.load_entry(&entry.id, backpack) {
252 results.push((entry, content, Some(summary)));
253
254 if results.len() >= limit {
256 break;
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264
265 Ok(results)
266 }
267}
268
269impl Card for SnippetCard {
270 fn name(&self) -> &str {
271 &self.name
272 }
273
274 fn version(&self) -> &str {
275 &self.version
276 }
277
278 fn description(&self) -> &str {
279 &self.description
280 }
281
282 fn initialize(&mut self, config: &CardConfig) -> Result<()> {
283 if let Some(options) = &config.options.get("config") {
285 if let Ok(card_config) = serde_json::from_value::<SnippetCardConfig>((*options).clone()) {
286 self.config = card_config;
287 }
288 }
289
290 Ok(())
291 }
292
293 fn execute(&self, command: &str, args: &[String]) -> Result<()> {
294 match command {
295 "add" => {
296 let mut file = None;
297 let mut message = None;
298 let mut use_editor = false;
299 let mut use_clipboard = false;
300 let mut backpack = None;
301 let mut summarize = None;
302
303 let mut i = 0;
305 while i < args.len() {
306 if args[i].starts_with("--file=") {
307 file = Some(args[i][7..].to_string());
308 i += 1;
309 } else if args[i] == "--file" {
310 if i + 1 < args.len() {
311 file = Some(args[i + 1].clone());
312 i += 2;
313 } else {
314 return Err(anyhow!("--file requires a file path"));
315 }
316 } else if args[i].starts_with("--message=") {
317 message = Some(args[i][10..].to_string());
318 i += 1;
319 } else if args[i] == "--message" {
320 if i + 1 < args.len() {
321 message = Some(args[i + 1].clone());
322 i += 2;
323 } else {
324 return Err(anyhow!("--message requires a message string"));
325 }
326 } else if args[i] == "--editor" {
327 use_editor = true;
328 i += 1;
329 } else if args[i] == "--clipboard" {
330 use_clipboard = true;
331 i += 1;
332 } else if args[i].starts_with("--backpack=") {
333 backpack = Some(args[i][11..].to_string());
334 i += 1;
335 } else if args[i] == "--backpack" {
336 if i + 1 < args.len() {
337 backpack = Some(args[i + 1].clone());
338 i += 2;
339 } else {
340 return Err(anyhow!("--backpack requires a backpack name"));
341 }
342 } else if args[i].starts_with("--summarize=") {
343 summarize = Some(args[i][12..].to_string());
344 i += 1;
345 } else if args[i] == "--summarize" {
346 if i + 1 < args.len() {
347 summarize = Some(args[i + 1].clone());
348 i += 2;
349 } else {
350 return Err(anyhow!("--summarize requires a summary string"));
351 }
352 } else {
353 i += 1;
354 }
355 }
356
357 let id = self.add(file.as_deref(), message.as_deref(), use_editor, use_clipboard, backpack.as_deref(), summarize.as_deref())?;
359 println!("Added snippet with ID: {}", id);
360 Ok(())
361 },
362 "add-from-clipboard" => {
363 let mut user_summary = None;
364 let mut backpack = None;
365
366 let mut i = 0;
368 while i < args.len() {
369 match args[i].as_str() {
370 "--summarize" => {
371 if i + 1 < args.len() {
372 user_summary = Some(args[i + 1].as_str());
373 i += 2;
374 } else {
375 return Err(anyhow!("--summarize requires a summary string"));
376 }
377 },
378 "--backpack" => {
379 if i + 1 < args.len() {
380 backpack = Some(args[i + 1].as_str());
381 i += 2;
382 } else {
383 return Err(anyhow!("--backpack requires a backpack name"));
384 }
385 },
386 _ => {
387 i += 1;
388 }
389 }
390 }
391
392 let id = self.add_from_clipboard(user_summary, backpack)?;
394 println!("Added snippet from clipboard with ID: {}", id);
395 Ok(())
396 },
397 "search" => {
398 if args.is_empty() {
399 return Err(anyhow!("search requires a query string"));
400 }
401
402 let query = &args[0];
403 let limit = if args.len() > 1 {
404 args[1].parse().unwrap_or(10)
405 } else {
406 10
407 };
408
409 let mut backpack = None;
410 let mut i = 2;
411 while i < args.len() {
412 match args[i].as_str() {
413 "--backpack" => {
414 if i + 1 < args.len() {
415 backpack = Some(args[i + 1].as_str());
416 i += 2;
417 } else {
418 return Err(anyhow!("--backpack requires a backpack name"));
419 }
420 },
421 _ => {
422 i += 1;
423 }
424 }
425 }
426
427 let results = self.search(query, limit, backpack)?;
429
430 if results.is_empty() {
431 println!("No results found");
432 return Ok(());
433 }
434
435 println!("Search results for '{}':", query);
436 for (i, (entry, content, summary)) in results.iter().enumerate() {
437 println!("{}. {} ({})", i + 1, entry.title, entry.id);
438
439 if let Some(summary) = summary {
441 println!(" Summary: {}", summary.summary);
442 }
443
444 let preview = if content.len() > 100 {
446 format!("{}...", &content[..97])
447 } else {
448 content.clone()
449 };
450 println!(" Content: {}", preview.replace('\n', " "));
451 println!();
452 }
453
454 Ok(())
455 },
456 "config" => {
457 println!("Snippet card configuration:");
459 println!(" Auto-summarize: {}", self.config.auto_summarize);
460 println!(" Max summary length: {}", self.config.max_summary_length);
461 println!(" Search in summaries: {}", self.config.search_in_summaries);
462 println!(" Summary search weight: {}", self.config.summary_search_weight);
463 Ok(())
464 },
465 _ => Err(anyhow!("Unknown command: {}", command))
466 }
467 }
468
469 fn commands(&self) -> Vec<CardCommand> {
470 vec![
471 CardCommand {
472 name: "add".to_string(),
473 description: "Add a new snippet from a file or editor".to_string(),
474 usage: "pocket cards execute snippet add [--file=FILE] [--message=MESSAGE] [--editor] [--backpack=BACKPACK] [--summarize=SUMMARY]".to_string(),
475 },
476 CardCommand {
477 name: "add-from-clipboard".to_string(),
478 description: "Add a snippet from clipboard content".to_string(),
479 usage: "pocket cards execute snippet add-from-clipboard [--summarize SUMMARY] [--backpack BACKPACK]".to_string(),
480 },
481 CardCommand {
482 name: "search".to_string(),
483 description: "Search for snippets, including in summaries".to_string(),
484 usage: "pocket cards execute snippet search QUERY [LIMIT] [--backpack BACKPACK]".to_string(),
485 },
486 CardCommand {
487 name: "config".to_string(),
488 description: "Show current snippet card configuration".to_string(),
489 usage: "pocket cards execute snippet config".to_string(),
490 },
491 ]
492 }
493
494 fn cleanup(&mut self) -> Result<()> {
495 Ok(())
496 }
497}