1use std::path::{Path, PathBuf};
2
3use crossterm::event::{KeyEvent, MouseEvent};
4
5use crate::{
6 app::{commands::command_info::CommandInfo, log::NotificationLevel},
7 headers::custom_header::CustomHeader,
8};
9
10use super::{
11 app_context::AppContext,
12 event::{Event, Events},
13 plugin::Plugin,
14 popup_context::PopupContext,
15 ui_location::ui_location::UiLocation,
16};
17
18#[derive(Default, Debug)]
19pub struct PluginManager {
20 plugins: Vec<Plugin>,
21 on_open: Vec<usize>,
22 on_save: Vec<usize>,
23 on_edit: Vec<usize>,
24 on_key: Vec<usize>,
25 on_mouse: Vec<usize>,
26}
27
28impl PluginManager {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn load(path: Option<&Path>, app_context: &mut AppContext) -> std::io::Result<Self> {
34 let mut plugin_manager = Self {
35 plugins: Self::load_plugins(path, app_context)?,
36 ..Default::default()
37 };
38
39 for (i, plugin) in plugin_manager.plugins.iter().enumerate() {
40 let handlers = plugin.get_event_handlers();
41 if handlers.contains(Events::ON_OPEN) {
42 plugin_manager.on_open.push(i);
43 }
44 if handlers.contains(Events::ON_SAVE) {
45 plugin_manager.on_save.push(i);
46 }
47 if handlers.contains(Events::ON_EDIT) {
48 plugin_manager.on_edit.push(i);
49 }
50 if handlers.contains(Events::ON_KEY) {
51 plugin_manager.on_key.push(i);
52 }
53 if handlers.contains(Events::ON_MOUSE) {
54 plugin_manager.on_mouse.push(i);
55 }
56 }
57 Ok(plugin_manager)
58 }
59
60 fn get_default_plugin_path() -> Option<PathBuf> {
61 let config = dirs::config_dir()?;
62 Some(config.join("HexPatch").join("plugins"))
63 }
64
65 fn load_plugins(
66 path: Option<&Path>,
67 app_context: &mut AppContext,
68 ) -> std::io::Result<Vec<Plugin>> {
69 let mut plugins = Vec::new();
70 let path = match path {
71 Some(path) => path.to_path_buf(),
72 None => Self::get_default_plugin_path()
73 .ok_or(std::io::Error::other(t!("errors.get_default_plugin_path")))?,
74 };
75 std::fs::create_dir_all(&path)?;
76 for entry in std::fs::read_dir(path)? {
77 let entry = entry?;
78 let path = entry.path();
79 if path.is_file() && path.extension().unwrap_or_default() == "lua" {
80 match Plugin::new_from_file(&path.to_string_lossy(), app_context) {
81 Ok(plugin) => {
82 plugins.push(plugin);
83 }
84 Err(e) => app_context.logger.log(
85 NotificationLevel::Error,
86 t!(
87 "app.messages.plugin_load_error",
88 path = path.to_string_lossy(),
89 error = e
90 ),
91 ),
92 }
93 }
94 }
95 Ok(plugins)
96 }
97
98 pub fn on_open(&mut self, app_context: &mut AppContext) {
99 for i in self.on_open.iter() {
100 app_context.plugin_index = Some(*i);
101 let event = Event::Open;
102 self.plugins[*i].handle(event, app_context);
103 }
104 }
105
106 pub fn on_save(&mut self, app_context: &mut AppContext) {
107 for i in self.on_save.iter() {
108 app_context.plugin_index = Some(*i);
109 let event = Event::Save;
110 self.plugins[*i].handle(event, app_context);
111 }
112 }
113
114 pub fn on_edit(&mut self, new_bytes: &mut Vec<u8>, app_context: &mut AppContext) {
115 for i in self.on_edit.iter() {
116 app_context.plugin_index = Some(*i);
117 let event = Event::Edit { new_bytes };
118 self.plugins[*i].handle(event, app_context);
119 }
120 }
121
122 pub fn on_key(&mut self, event: KeyEvent, app_context: &mut AppContext) {
123 for i in self.on_key.iter() {
124 app_context.plugin_index = Some(*i);
125 let event = Event::Key { event };
126 self.plugins[*i].handle(event, app_context);
127 }
128 }
129
130 pub fn on_mouse(
131 &mut self,
132 event: MouseEvent,
133 location: Option<UiLocation>,
134 app_context: &mut AppContext,
135 ) {
136 for i in self.on_mouse.iter() {
137 app_context.plugin_index = Some(*i);
138 let event = Event::Mouse {
139 event,
140 location: location.clone(),
141 };
142 self.plugins[*i].handle(event, app_context);
143 }
144 }
145
146 pub fn on_focus(&mut self, app_context: &mut AppContext) {
147 for i in self.on_open.iter() {
148 app_context.plugin_index = Some(*i);
149 let event = Event::Focus;
150 self.plugins[*i].handle(event, app_context);
151 }
152 }
153
154 pub fn on_blur(&mut self, app_context: &mut AppContext) {
155 for i in self.on_open.iter() {
156 app_context.plugin_index = Some(*i);
157 let event = Event::Blur;
158 self.plugins[*i].handle(event, app_context);
159 }
160 }
161
162 pub fn on_paste(&mut self, text: impl AsRef<str>, app_context: &mut AppContext) {
163 for i in self.on_open.iter() {
164 app_context.plugin_index = Some(*i);
165 let event = Event::Paste {
166 text: text.as_ref().to_string(),
167 };
168 self.plugins[*i].handle(event, app_context);
169 }
170 }
171
172 pub fn on_resize(&mut self, width: u16, height: u16, app_context: &mut AppContext) {
173 for i in self.on_open.iter() {
174 app_context.plugin_index = Some(*i);
175 let event = Event::Resize { width, height };
176 self.plugins[*i].handle(event, app_context);
177 }
178 }
179
180 pub fn get_commands(&self) -> Vec<&CommandInfo> {
181 let mut commands = Vec::new();
182 let command_count = self.plugins.iter().map(|p| p.get_commands().len()).sum();
183 commands.reserve(command_count);
184 for plugin in self.plugins.iter() {
185 commands.extend(plugin.get_commands());
186 }
187 commands
188 }
189
190 pub fn run_command(&mut self, command: &str, app_context: &mut AppContext) -> mlua::Result<()> {
191 let mut found = false;
192 for (i, plugin) in self.plugins.iter_mut().enumerate() {
193 if let Some(_command_info) = plugin.get_commands().iter().find(|c| c.command == command)
194 {
195 app_context.plugin_index = Some(i);
196 plugin.run_command(command, app_context)?;
197 found = true;
198 break;
199 }
200 }
201 if !found {
202 Err(mlua::Error::external(format!(
203 "Command \"{command}\" not found"
204 )))
205 } else {
206 Ok(())
207 }
208 }
209
210 pub fn fill_popup(
211 &mut self,
212 plugin_index: usize,
213 callback: impl AsRef<str>,
214 popup_context: PopupContext,
215 app_context: AppContext,
216 ) -> mlua::Result<()> {
217 self.plugins[plugin_index].fill_popup(callback, popup_context, app_context)
218 }
219
220 pub fn try_parse_header(&mut self, app_context: &mut AppContext) -> Option<CustomHeader> {
221 for (i, plugin) in self.plugins.iter_mut().enumerate() {
222 app_context.plugin_index = Some(i);
223 if let Some(header) = plugin.try_parse_header(app_context) {
224 return Some(header);
225 }
226 }
227 None
228 }
229}
230
231#[cfg(test)]
232mod test {
233 use crate::{
234 app::{log::NotificationLevel, App},
235 get_app_context,
236 };
237
238 use super::*;
239
240 #[test]
241 fn test_load_plugins() {
242 let mut app = App::mockup(vec![0; 0x100]);
243 app.logger.clear();
244 let mut app_context = get_app_context!(app);
245 let test_plugins_path = Path::new("test").join("plugins");
246 app.plugin_manager =
247 PluginManager::load(Some(&test_plugins_path), &mut app_context).unwrap();
248 assert_eq!(app.plugin_manager.plugins.len(), 2);
249
250 app.plugin_manager
251 .run_command("p1c1", &mut app_context)
252 .unwrap();
253 app.plugin_manager
254 .run_command("p1c2", &mut app_context)
255 .unwrap();
256 app.plugin_manager
257 .run_command("p2c1", &mut app_context)
258 .unwrap();
259 app.plugin_manager
260 .run_command("p2c2", &mut app_context)
261 .unwrap();
262
263 app.plugin_manager.on_open(&mut app_context);
264 assert_ne!(
266 app_context.logger.get_notification_level(),
267 NotificationLevel::Error
268 );
269
270 let messages: Vec<_> = app_context.logger.iter().collect();
271 assert_eq!(messages.len(), 5, "{messages:?}");
272 assert_eq!(messages[0].message, "Plugin 1 Command 1 called");
273 assert_eq!(messages[1].message, "Plugin 1 Command 2 called");
274 assert_eq!(messages[2].message, "Plugin 2 Command 1 called");
275 assert_eq!(messages[3].message, "Plugin 2 Command 2 called");
276 assert_eq!(messages[4].message, "Plugin 1 on_open called");
277 }
278}