1pub mod backup;
7
8use std::collections::HashMap;
9use std::path::Path;
10use anyhow::{Result, Context};
11use serde::{Serialize, Deserialize};
12
13pub trait Card: Send + Sync {
15 fn name(&self) -> &str;
17
18 fn version(&self) -> &str;
20
21 fn description(&self) -> &str;
23
24 fn initialize(&mut self, config: &CardConfig) -> Result<()>;
26
27 fn execute(&self, command: &str, args: &[String]) -> Result<()>;
29
30 fn commands(&self) -> Vec<CardCommand>;
32
33 fn cleanup(&mut self) -> Result<()>;
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CardConfig {
40 pub name: String,
42
43 pub enabled: bool,
45
46 #[serde(default)]
48 pub options: HashMap<String, serde_json::Value>,
49}
50
51#[derive(Debug, Clone)]
53pub struct CardCommand {
54 pub name: String,
56
57 pub description: String,
59
60 pub usage: String,
62}
63
64pub struct CardManager {
66 cards: Vec<Box<dyn Card>>,
68
69 configs: HashMap<String, CardConfig>,
71
72 card_dir: std::path::PathBuf,
74}
75
76impl CardManager {
77 pub fn new(card_dir: impl AsRef<Path>) -> Self {
79 Self {
80 cards: Vec::new(),
81 configs: HashMap::new(),
82 card_dir: card_dir.as_ref().to_path_buf(),
83 }
84 }
85
86 pub fn load_cards(&mut self) -> Result<()> {
88 if !self.card_dir.exists() {
90 std::fs::create_dir_all(&self.card_dir)
91 .context("Failed to create card directory")?;
92 return Ok(());
93 }
94
95 self.load_configs()?;
97
98 self.register_builtin_cards()?;
100
101 let wallet_dir = self.card_dir.parent().unwrap_or(&self.card_dir).join("wallet");
103 if wallet_dir.exists() {
104 for entry in std::fs::read_dir(wallet_dir)? {
105 let entry = entry?;
106 let path = entry.path();
107
108 if path.is_dir() {
109 let card_name = path.file_name()
110 .and_then(|name| name.to_str())
111 .ok_or_else(|| anyhow::anyhow!("Invalid card directory name"))?;
112
113 if !self.configs.contains_key(card_name) {
115 self.register_card_config(card_name, "local")?;
117 }
118
119 let target_dir = path.join("target").join("release");
121 let lib_name = format!("libpocket_card_{}.dylib", card_name.replace("-", "_"));
122 let lib_path = target_dir.join(lib_name);
123
124 if lib_path.exists() {
125 println!("Found built card at {}", lib_path.display());
128
129 if !self.cards.iter().any(|p| p.name() == card_name) {
131 let card = Box::new(PlaceholderCard::new(card_name.to_string()));
137 self.register_card(card)?;
138 }
139 }
140 }
141 }
142 }
143
144 Ok(())
145 }
146
147 fn load_configs(&mut self) -> Result<()> {
149 let config_path = self.card_dir.join("cards.json");
150
151 if !config_path.exists() {
152 let default_configs: HashMap<String, CardConfig> = HashMap::new();
154 let json = serde_json::to_string_pretty(&default_configs)?;
155 std::fs::write(&config_path, json)?;
156 return Ok(());
157 }
158
159 let json = std::fs::read_to_string(&config_path)?;
161 self.configs = serde_json::from_str(&json)?;
162
163 Ok(())
164 }
165
166 pub fn save_configs(&self) -> Result<()> {
168 let config_path = self.card_dir.join("cards.json");
169 let json = serde_json::to_string_pretty(&self.configs)?;
170 std::fs::write(&config_path, json)?;
171 Ok(())
172 }
173
174 fn register_builtin_cards(&mut self) -> Result<()> {
176 use crate::cards::backup::BackupCard;
178
179 let data_dir = self.card_dir.parent().unwrap_or(&self.card_dir).to_path_buf();
181
182 self.register_card(Box::new(BackupCard::new(data_dir)))?;
184
185 Ok(())
186 }
187
188 pub fn register_card(&mut self, mut card: Box<dyn Card>) -> Result<()> {
190 let name = card.name().to_string();
191
192 let config = self.configs.entry(name.clone()).or_insert_with(|| {
194 CardConfig {
195 name: name.clone(),
196 enabled: true,
197 options: HashMap::new(),
198 }
199 });
200
201 card.initialize(config)?;
203
204 self.cards.push(card);
206
207 Ok(())
208 }
209
210 pub fn list_cards(&self) -> Vec<(&str, &str, bool)> {
212 self.cards.iter()
213 .map(|p| {
214 let name = p.name();
215 let version = p.version();
216 let enabled = self.configs.get(name)
217 .map(|c| c.enabled)
218 .unwrap_or(false);
219 (name, version, enabled)
220 })
221 .collect()
222 }
223
224 pub fn enable_card(&mut self, name: &str) -> Result<()> {
226 if let Some(config) = self.configs.get_mut(name) {
227 config.enabled = true;
228 self.save_configs()?;
229 Ok(())
230 } else {
231 anyhow::bail!("Card '{}' not found", name)
232 }
233 }
234
235 pub fn disable_card(&mut self, name: &str) -> Result<()> {
237 if let Some(config) = self.configs.get_mut(name) {
238 config.enabled = false;
239 self.save_configs()?;
240 Ok(())
241 } else {
242 anyhow::bail!("Card '{}' not found", name)
243 }
244 }
245
246 pub fn execute_command(&self, card_name: &str, command: &str, args: &[String]) -> Result<()> {
248 let card = self.cards.iter().find(|p| p.name() == card_name);
250
251 if let Some(card) = card {
252 let enabled = self.configs.get(card_name)
254 .map(|c| c.enabled)
255 .unwrap_or(false);
256
257 if !enabled {
258 return Err(anyhow::anyhow!("Card '{}' is disabled", card_name));
259 }
260
261 card.execute(command, args)
263 } else {
264 if self.configs.contains_key(card_name) {
266 return Err(anyhow::anyhow!("Card '{}' is registered but not loaded. Try rebuilding the card with: pocket cards build {}", card_name, card_name));
269 }
270
271 Err(anyhow::anyhow!("Card '{}' not found", card_name))
272 }
273 }
274
275 pub fn list_commands(&self) -> Vec<(String, Vec<CardCommand>)> {
277 self.cards.iter()
278 .filter(|p| {
279 self.configs.get(p.name())
280 .map(|c| c.enabled)
281 .unwrap_or(false)
282 })
283 .map(|p| (p.name().to_string(), p.commands()))
284 .collect()
285 }
286
287 pub fn cleanup(&mut self) -> Result<()> {
289 for card in &mut self.cards {
290 card.cleanup()?;
291 }
292 Ok(())
293 }
294
295 pub fn card_exists(&self, name: &str) -> bool {
297 if self.cards.iter().any(|p| p.name() == name) {
299 return true;
300 }
301
302 self.configs.contains_key(name)
304 }
305
306 pub fn register_card_config(&mut self, name: &str, url: &str) -> Result<()> {
308 let config = CardConfig {
310 name: name.to_string(),
311 enabled: true,
312 options: {
313 let mut options = HashMap::new();
314 options.insert("url".to_string(), serde_json::Value::String(url.to_string()));
315 options
316 },
317 };
318
319 self.configs.insert(name.to_string(), config);
321
322 self.save_configs()?;
324
325 Ok(())
326 }
327
328 pub fn remove_card_config(&mut self, name: &str) -> Result<()> {
330 self.configs.remove(name);
332
333 self.save_configs()?;
335
336 Ok(())
337 }
338}
339
340impl Drop for CardManager {
341 fn drop(&mut self) {
342 let _ = self.cleanup();
344 }
345}
346
347struct PlaceholderCard {
349 name: String,
350 version: String,
351 description: String,
352}
353
354impl PlaceholderCard {
355 fn new(name: String) -> Self {
356 Self {
357 name,
358 version: "0.1.0".to_string(),
359 description: "A placeholder card".to_string(),
360 }
361 }
362}
363
364impl Card for PlaceholderCard {
365 fn name(&self) -> &str {
366 &self.name
367 }
368
369 fn version(&self) -> &str {
370 &self.version
371 }
372
373 fn description(&self) -> &str {
374 &self.description
375 }
376
377 fn initialize(&mut self, _config: &CardConfig) -> Result<()> {
378 Ok(())
379 }
380
381 fn execute(&self, command: &str, args: &[String]) -> Result<()> {
382 println!("Executing command {} with args {:?} on placeholder card {}", command, args, self.name);
383 Ok(())
384 }
385
386 fn commands(&self) -> Vec<CardCommand> {
387 vec![
388 CardCommand {
389 name: "hello".to_string(),
390 description: "A simple hello command".to_string(),
391 usage: format!("pocket cards execute {} hello [args...]", self.name),
392 },
393 ]
394 }
395
396 fn cleanup(&mut self) -> Result<()> {
397 Ok(())
398 }
399}