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
11pub struct CoreCard {
13 name: String,
15
16 version: String,
18
19 description: String,
21
22 config: CoreCardConfig,
24
25 data_dir: PathBuf,
27}
28
29#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub struct CoreCardConfig {
32 pub max_search_results: usize,
34
35 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 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 pub fn search(&self, query: &str, limit: usize, backpack: Option<&str>, exact: bool) -> Result<Vec<Entry>> {
62 let storage = StorageManager::new()?;
63
64 let search_results = storage.search_entries(query, backpack, limit)?;
66
67 Ok(search_results.into_iter().map(|(entry, _)| entry).collect())
69 }
70
71 pub fn insert(&self, entry_id: &str, file_path: &str, delimiter: Option<&str>, no_confirm: bool) -> Result<()> {
73 let storage = StorageManager::new()?;
74
75 let (entry, content) = storage.load_entry(entry_id, None)?;
77
78 let delim = delimiter.unwrap_or(&self.config.default_delimiter);
79
80 let file_content = fs::read_to_string(file_path)
82 .with_context(|| format!("Failed to read file {}", file_path))?;
83
84 let cursor_pos = utils::get_cursor_position(&file_content)
86 .unwrap_or(file_content.len());
87
88 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 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 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 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 pub fn create_backpack(&self, name: &str, description: Option<&str>) -> Result<()> {
151 let storage = StorageManager::new()?;
152
153 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 storage.create_backpack(&backpack)?;
162 println!("Created backpack: {}", name.bold());
163 Ok(())
164 }
165
166 pub fn remove(&self, id: &str, force: bool, backpack: Option<&str>) -> Result<()> {
168 let storage = StorageManager::new()?;
169
170 let (entry, _) = storage.load_entry(id, backpack)?;
172
173 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 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 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 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 _ => { }
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 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 _ => { }
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 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 _ => { }
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 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 _ => { }
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 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 _ => { }
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}