hex_patch/app/plugins/
plugin_manager.rs

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        // If there was an error, the logger will have a message
265        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}