pocket_cli/cards/
core.rs

1use crate::cards::{Card, CardConfig, CardCommand};
2use crate::models::{Entry, Backpack};
3use crate::storage::StorageManager;
4use crate::utils;
5use anyhow::{Result, Context, anyhow};
6use colored::Colorize;
7use std::path::PathBuf;
8use std::fs;
9use chrono::{DateTime, Utc};
10
11/// Card for core commands (search, insert, etc.)
12pub struct CoreCard {
13    /// Name of the card
14    name: String,
15    
16    /// Version of the card
17    version: String,
18    
19    /// Description of the card
20    description: String,
21    
22    /// Configuration for the card
23    config: CoreCardConfig,
24    
25    /// Path to the Pocket data directory
26    data_dir: PathBuf,
27}
28
29/// Configuration for the core card
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub struct CoreCardConfig {
32    /// Maximum number of search results
33    pub max_search_results: usize,
34    
35    /// Default delimiter for inserting content
36    pub default_delimiter: String,
37}
38
39impl Default for CoreCardConfig {
40    fn default() -> Self {
41        Self {
42            max_search_results: 10,
43            default_delimiter: "// --- Pocket CLI Insert ---".to_string(),
44        }
45    }
46}
47
48impl CoreCard {
49    /// Creates a new core card
50    pub fn new(data_dir: impl AsRef<std::path::Path>) -> Self {
51        Self {
52            name: "core".to_string(),
53            version: env!("CARGO_PKG_VERSION").to_string(),
54            description: "Core functionality for Pocket CLI".to_string(),
55            config: CoreCardConfig::default(),
56            data_dir: data_dir.as_ref().to_path_buf(),
57        }
58    }
59    
60    /// Search for entries
61    pub fn search(&self, query: &str, limit: usize, backpack: Option<&str>, exact: bool) -> Result<Vec<Entry>> {
62        let storage = StorageManager::new()?;
63        
64        // For now, we'll use the built-in search, as the API doesn't have exact/semantic differentiation
65        let search_results = storage.search_entries(query, backpack, limit)?;
66        
67        // Return just the entries without content
68        Ok(search_results.into_iter().map(|(entry, _)| entry).collect())
69    }
70    
71    /// Insert an entry into a file
72    pub fn insert(&self, entry_id: &str, file_path: &str, delimiter: Option<&str>, no_confirm: bool) -> Result<()> {
73        let storage = StorageManager::new()?;
74        
75        // Load the entry and its content
76        let (entry, content) = storage.load_entry(entry_id, None)?;
77        
78        let delim = delimiter.unwrap_or(&self.config.default_delimiter);
79        
80        // Read the file content
81        let file_content = fs::read_to_string(file_path)
82            .with_context(|| format!("Failed to read file {}", file_path))?;
83        
84        // Get cursor position or end of file
85        let cursor_pos = utils::get_cursor_position(&file_content)
86            .unwrap_or(file_content.len());
87        
88        // Insert the content at cursor position
89        let new_content = format!(
90            "{}\n{}\n{}\n{}",
91            &file_content[..cursor_pos],
92            delim,
93            content,
94            &file_content[cursor_pos..]
95        );
96        
97        // Confirm with user if needed
98        if !no_confirm {
99            println!("Inserting entry {} into {}", entry_id.bold(), file_path.bold());
100            let confirm = utils::confirm("Continue?", true)?;
101            if !confirm {
102                println!("Operation cancelled");
103                return Ok(());
104            }
105        }
106        
107        // Write the new content
108        fs::write(file_path, new_content)
109            .with_context(|| format!("Failed to write to file {}", file_path))?;
110        
111        println!("Successfully inserted entry {} into {}", entry_id.bold(), file_path.bold());
112        Ok(())
113    }
114    
115    /// List all entries
116    pub fn list(&self, include_backpacks: bool, backpack: Option<&str>, json: bool) -> Result<()> {
117        let storage = StorageManager::new()?;
118        let entries = storage.list_entries(backpack)?;
119        
120        if json {
121            println!("{}", serde_json::to_string_pretty(&entries)?);
122            return Ok(());
123        }
124        
125        if entries.is_empty() {
126            println!("No entries found");
127            return Ok(());
128        }
129        
130        for entry in entries {
131            let backpack_name = if include_backpacks {
132                match &entry.source {
133                    Some(source) if source.starts_with("backpack:") => {
134                        let bp_name = source.strip_prefix("backpack:").unwrap_or("unknown");
135                        format!(" [{}]", bp_name.bold())
136                    },
137                    _ => "".to_string(),
138                }
139            } else {
140                "".to_string()
141            };
142            
143            println!("{}{} - {}", entry.id.bold(), backpack_name, entry.title);
144        }
145        
146        Ok(())
147    }
148    
149    /// Create a new backpack
150    pub fn create_backpack(&self, name: &str, description: Option<&str>) -> Result<()> {
151        let storage = StorageManager::new()?;
152        
153        // Create a backpack structure
154        let backpack = Backpack {
155            name: name.to_string(),
156            description: description.map(|s| s.to_string()),
157            created_at: chrono::Utc::now(),
158        };
159        
160        // Save the backpack
161        storage.create_backpack(&backpack)?;
162        println!("Created backpack: {}", name.bold());
163        Ok(())
164    }
165    
166    /// Remove an entry
167    pub fn remove(&self, id: &str, force: bool, backpack: Option<&str>) -> Result<()> {
168        let storage = StorageManager::new()?;
169        
170        // Check if entry exists
171        let (entry, _) = storage.load_entry(id, backpack)?;
172        
173        // Confirm with user if not forced
174        if !force {
175            println!("You are about to remove: {}", id.bold());
176            println!("Title: {}", entry.title);
177            
178            let confirm = utils::confirm("Are you sure?", false)?;
179            if !confirm {
180                println!("Operation cancelled");
181                return Ok(());
182            }
183        }
184        
185        // Remove the entry
186        storage.remove_entry(id, backpack)?;
187        println!("Removed entry: {}", id.bold());
188        
189        Ok(())
190    }
191}
192
193impl Card for CoreCard {
194    fn name(&self) -> &str {
195        &self.name
196    }
197    
198    fn version(&self) -> &str {
199        &self.version
200    }
201    
202    fn description(&self) -> &str {
203        &self.description
204    }
205    
206    fn initialize(&mut self, config: &CardConfig) -> Result<()> {
207        // If there are options in the card config, try to parse them
208        if let Some(options_value) = config.options.get("core") {
209            if let Ok(options) = serde_json::from_value::<CoreCardConfig>(options_value.clone()) {
210                self.config = options;
211            }
212        }
213        
214        Ok(())
215    }
216    
217    fn execute(&self, command: &str, args: &[String]) -> Result<()> {
218        match command {
219            "search" => {
220                if args.is_empty() {
221                    return Err(anyhow!("Missing search query"));
222                }
223                
224                let query = &args[0];
225                let mut limit = self.config.max_search_results;
226                let mut backpack = None;
227                let mut exact = false;
228                
229                // Parse optional arguments
230                let mut i = 1;
231                while i < args.len() {
232                    match args[i].as_str() {
233                        "--limit" => {
234                            if i + 1 < args.len() {
235                                limit = args[i + 1].parse()?;
236                                i += 1;
237                            }
238                        }
239                        "--backpack" => {
240                            if i + 1 < args.len() {
241                                backpack = Some(args[i + 1].as_str());
242                                i += 1;
243                            }
244                        }
245                        "--exact" => {
246                            exact = true;
247                        }
248                        _ => { /* Ignore unknown args */ }
249                    }
250                    i += 1;
251                }
252                
253                let results = self.search(query, limit, backpack, exact)?;
254                
255                if results.is_empty() {
256                    println!("No results found for query: {}", query.bold());
257                    return Ok(());
258                }
259                
260                println!("Search results for: {}", query.bold());
261                for (i, entry) in results.iter().enumerate() {
262                    println!("{}. {} - {}", i + 1, entry.id.bold(), entry.title);
263                }
264            }
265            "insert" => {
266                if args.len() < 2 {
267                    return Err(anyhow!("Missing entry ID or file path"));
268                }
269                
270                let entry_id = &args[0];
271                let file_path = &args[1];
272                
273                let mut delimiter = None;
274                let mut no_confirm = false;
275                
276                // Parse optional arguments
277                let mut i = 2;
278                while i < args.len() {
279                    match args[i].as_str() {
280                        "--delimiter" => {
281                            if i + 1 < args.len() {
282                                delimiter = Some(args[i + 1].as_str());
283                                i += 1;
284                            }
285                        }
286                        "--no-confirm" => {
287                            no_confirm = true;
288                        }
289                        _ => { /* Ignore unknown args */ }
290                    }
291                    i += 1;
292                }
293                
294                self.insert(entry_id, file_path, delimiter, no_confirm)?;
295            }
296            "list" => {
297                let mut include_backpacks = false;
298                let mut backpack = None;
299                let mut json = false;
300                
301                // Parse optional arguments
302                let mut i = 0;
303                while i < args.len() {
304                    match args[i].as_str() {
305                        "--include-backpacks" => {
306                            include_backpacks = true;
307                        }
308                        "--backpack" => {
309                            if i + 1 < args.len() {
310                                backpack = Some(args[i + 1].as_str());
311                                i += 1;
312                            }
313                        }
314                        "--json" => {
315                            json = true;
316                        }
317                        _ => { /* Ignore unknown args */ }
318                    }
319                    i += 1;
320                }
321                
322                self.list(include_backpacks, backpack, json)?;
323            }
324            "create-backpack" => {
325                if args.is_empty() {
326                    return Err(anyhow!("Missing backpack name"));
327                }
328                
329                let name = &args[0];
330                let mut description = None;
331                
332                // Parse optional arguments
333                let mut i = 1;
334                while i < args.len() {
335                    match args[i].as_str() {
336                        "--description" => {
337                            if i + 1 < args.len() {
338                                description = Some(args[i + 1].as_str());
339                                i += 1;
340                            }
341                        }
342                        _ => { /* Ignore unknown args */ }
343                    }
344                    i += 1;
345                }
346                
347                self.create_backpack(name, description)?;
348            }
349            "remove" => {
350                if args.is_empty() {
351                    return Err(anyhow!("Missing entry ID"));
352                }
353                
354                let id = &args[0];
355                let mut force = false;
356                let mut backpack = None;
357                
358                // Parse optional arguments
359                let mut i = 1;
360                while i < args.len() {
361                    match args[i].as_str() {
362                        "--force" => {
363                            force = true;
364                        }
365                        "--backpack" => {
366                            if i + 1 < args.len() {
367                                backpack = Some(args[i + 1].as_str());
368                                i += 1;
369                            }
370                        }
371                        _ => { /* Ignore unknown args */ }
372                    }
373                    i += 1;
374                }
375                
376                self.remove(id, force, backpack)?;
377            }
378            _ => {
379                return Err(anyhow!("Unknown command: {}", command));
380            }
381        }
382        
383        Ok(())
384    }
385    
386    fn commands(&self) -> Vec<CardCommand> {
387        vec![
388            CardCommand {
389                name: "search".to_string(),
390                description: "Search for entries".to_string(),
391                usage: "search <query> [--limit N] [--backpack NAME] [--exact]".to_string(),
392            },
393            CardCommand {
394                name: "insert".to_string(),
395                description: "Insert an entry into a file".to_string(),
396                usage: "insert <entry_id> <file_path> [--delimiter TEXT] [--no-confirm]".to_string(),
397            },
398            CardCommand {
399                name: "list".to_string(),
400                description: "List all entries".to_string(),
401                usage: "list [--include-backpacks] [--backpack NAME] [--json]".to_string(),
402            },
403            CardCommand {
404                name: "create-backpack".to_string(),
405                description: "Create a new backpack".to_string(),
406                usage: "create-backpack <name> [--description TEXT]".to_string(),
407            },
408            CardCommand {
409                name: "remove".to_string(),
410                description: "Remove an entry".to_string(),
411                usage: "remove <id> [--force] [--backpack NAME]".to_string(),
412            },
413        ]
414    }
415    
416    fn cleanup(&mut self) -> Result<()> {
417        Ok(())
418    }
419}