1use serde::{Deserialize, Serialize};
13
14use super::manifest::{DetectConfig, ExportConfig, Manifest, PluginKind};
15use crate::scheduler::content_pack::AddedContent;
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct InstalledPlugin {
20 pub id: String,
21 pub name: String,
22 #[serde(default)]
23 pub author: String,
24 pub version: String,
25 pub kind: PluginKind,
26 pub public_key: String,
29 #[serde(default)]
32 pub added: AddedContent,
33 #[serde(default)]
37 pub capabilities: Vec<String>,
38 #[serde(default)]
41 pub detect: Option<DetectConfig>,
42 #[serde(default)]
45 pub export: Option<ExportConfig>,
46}
47
48impl InstalledPlugin {
49 pub fn from_manifest(manifest: &Manifest, added: AddedContent) -> Self {
52 Self {
53 id: manifest.id.clone(),
54 name: manifest.name.clone(),
55 author: manifest.author.clone(),
56 version: manifest.version.clone(),
57 kind: manifest.kind,
58 public_key: manifest.signature.public_key.clone(),
59 added,
60 capabilities: Vec::new(),
61 detect: None,
62 export: None,
63 }
64 }
65
66 pub fn from_detector(manifest: &Manifest) -> Self {
70 Self {
71 id: manifest.id.clone(),
72 name: manifest.name.clone(),
73 author: manifest.author.clone(),
74 version: manifest.version.clone(),
75 kind: manifest.kind,
76 public_key: manifest.signature.public_key.clone(),
77 added: AddedContent::default(),
78 capabilities: manifest.imports.clone(),
79 detect: manifest.detect.clone(),
80 export: None,
81 }
82 }
83
84 pub fn from_export(manifest: &Manifest) -> Self {
87 Self {
88 id: manifest.id.clone(),
89 name: manifest.name.clone(),
90 author: manifest.author.clone(),
91 version: manifest.version.clone(),
92 kind: manifest.kind,
93 public_key: manifest.signature.public_key.clone(),
94 added: AddedContent::default(),
95 capabilities: Vec::new(),
96 detect: None,
97 export: manifest.export.clone(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
104pub struct PluginRegistry {
105 #[serde(default)]
106 pub plugins: Vec<InstalledPlugin>,
107}
108
109impl PluginRegistry {
110 pub fn contains(&self, id: &str) -> bool {
112 self.plugins.iter().any(|p| p.id == id)
113 }
114
115 pub fn insert(&mut self, plugin: InstalledPlugin) {
118 self.plugins.push(plugin);
119 }
120
121 pub fn remove(&mut self, id: &str) -> Option<InstalledPlugin> {
123 let idx = self.plugins.iter().position(|p| p.id == id)?;
124 Some(self.plugins.remove(idx))
125 }
126
127 pub fn summaries(&self) -> Vec<PluginSummary> {
129 self.plugins.iter().map(PluginSummary::from).collect()
130 }
131
132 pub fn export_configs_for(&self, event: crate::hooks::HookEvent) -> Vec<ExportConfig> {
135 self.plugins
136 .iter()
137 .filter(|p| p.kind == PluginKind::Export)
138 .filter_map(|p| p.export.clone())
139 .filter(|cfg| cfg.on.contains(&event))
140 .collect()
141 }
142
143 pub fn detector_snapshots(&self) -> Vec<super::eval::DetectorSnapshot> {
148 self.plugins
149 .iter()
150 .filter(|p| p.kind == PluginKind::Detector)
151 .map(|p| super::eval::DetectorSnapshot {
152 id: p.id.clone(),
153 capabilities: p
154 .capabilities
155 .iter()
156 .filter_map(|c| super::manifest::Capability::parse(c).ok())
157 .collect(),
158 process_pattern: p.detect.as_ref().and_then(|d| d.process_name.clone()),
159 })
160 .collect()
161 }
162}
163
164#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
167pub struct PluginSummary {
168 pub id: String,
169 pub name: String,
170 pub author: String,
171 pub version: String,
172 pub kind: PluginKind,
173 pub hints_added: usize,
174 pub routines_added: usize,
175}
176
177impl From<&InstalledPlugin> for PluginSummary {
178 fn from(p: &InstalledPlugin) -> Self {
179 let a = &p.added;
180 let hints_added = a.micro_physical.len()
181 + a.micro_psychological.len()
182 + a.long_solo.len()
183 + a.long_social.len()
184 + a.sleep.len();
185 Self {
186 id: p.id.clone(),
187 name: p.name.clone(),
188 author: p.author.clone(),
189 version: p.version.clone(),
190 kind: p.kind,
191 hints_added,
192 routines_added: a.routine_ids.len(),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn record(id: &str) -> InstalledPlugin {
202 InstalledPlugin {
203 id: id.to_string(),
204 name: "Pack".to_string(),
205 author: "Me".to_string(),
206 version: "1.0.0".to_string(),
207 kind: PluginKind::Content,
208 public_key: "AA==".to_string(),
209 added: AddedContent {
210 micro_physical: vec!["a".to_string(), "b".to_string()],
211 routine_ids: vec!["r1".to_string()],
212 ..AddedContent::default()
213 },
214 capabilities: Vec::new(),
215 detect: None,
216 export: None,
217 }
218 }
219
220 fn detector_record(id: &str) -> InstalledPlugin {
221 InstalledPlugin {
222 id: id.to_string(),
223 name: "Detector".to_string(),
224 author: "Me".to_string(),
225 version: "1.0.0".to_string(),
226 kind: PluginKind::Detector,
227 public_key: "AA==".to_string(),
228 added: AddedContent::default(),
229 capabilities: vec!["detect:processes".to_string(), "garbage".to_string()],
231 detect: Some(DetectConfig {
232 process_name: Some("zoom".to_string()),
233 }),
234 export: None,
235 }
236 }
237
238 #[test]
239 fn detector_snapshots_parses_caps_and_excludes_content() {
240 use super::super::manifest::Capability;
241 let mut reg = PluginRegistry::default();
242 reg.insert(record("com.x.content")); reg.insert(detector_record("com.x.det"));
244
245 let snaps = reg.detector_snapshots();
246 assert_eq!(snaps.len(), 1);
247 assert_eq!(snaps[0].id, "com.x.det");
248 assert_eq!(snaps[0].capabilities, vec![Capability::DetectProcesses]);
250 assert_eq!(snaps[0].process_pattern, Some("zoom".to_string()));
251 }
252
253 #[test]
254 fn insert_contains_remove() {
255 let mut reg = PluginRegistry::default();
256 assert!(!reg.contains("com.x.pack"));
257 reg.insert(record("com.x.pack"));
258 assert!(reg.contains("com.x.pack"));
259
260 let removed = reg.remove("com.x.pack").unwrap();
261 assert_eq!(removed.id, "com.x.pack");
262 assert!(!reg.contains("com.x.pack"));
263 assert!(reg.remove("com.x.pack").is_none());
264 }
265
266 #[test]
267 fn summaries_count_hints_and_routines() {
268 let mut reg = PluginRegistry::default();
269 reg.insert(record("com.x.pack"));
270 let s = reg.summaries();
271 assert_eq!(s.len(), 1);
272 assert_eq!(s[0].hints_added, 2);
273 assert_eq!(s[0].routines_added, 1);
274 assert_eq!(s[0].kind, PluginKind::Content);
275 }
276
277 #[test]
278 fn registry_serde_round_trips() {
279 let mut reg = PluginRegistry::default();
280 reg.insert(record("com.x.pack"));
281 let json = serde_json::to_string(®).unwrap();
282 let back: PluginRegistry = serde_json::from_str(&json).unwrap();
283 assert_eq!(back, reg);
284 }
285}