hex_patch/app/plugins/
plugin.rs

1use std::error::Error;
2
3use mlua::{Function, Lua};
4
5use crate::{
6    app::{
7        commands::command_info::CommandInfo,
8        log::NotificationLevel,
9        settings::register_key_settings_macro::{key_event_to_lua, mouse_event_to_lua},
10    },
11    headers::custom_header::CustomHeader,
12};
13
14use super::{
15    app_context::AppContext,
16    event::{Event, Events},
17    exported_commands::ExportedCommands,
18    exported_header_parsers::ExportedHeaderParsers,
19    header_context::HeaderContext,
20    popup_context::PopupContext,
21    register_userdata::{
22        register_settings, register_string, register_text, register_usize, register_vec_u8,
23    },
24};
25
26#[derive(Debug)]
27pub struct Plugin {
28    lua: Lua,
29    commands: ExportedCommands,
30    header_parsers: ExportedHeaderParsers,
31}
32
33impl Plugin {
34    pub fn new_from_source(
35        source: &str,
36        app_context: &mut AppContext,
37    ) -> Result<Self, Box<dyn Error>> {
38        let lua = Lua::new();
39        lua.load(source).exec()?;
40
41        register_vec_u8(&lua)?;
42        register_settings(&lua)?;
43        register_text(&lua)?;
44        register_string(&lua)?;
45        register_usize(&lua)?;
46
47        app_context.reset_exported_commands();
48        if let Ok(init) = lua.globals().get::<Function>("init") {
49            lua.scope(|scope| {
50                let context = app_context.to_lua(&lua, scope);
51                init.call::<()>(context)
52            })?;
53        }
54
55        Ok(Plugin {
56            lua,
57            commands: app_context.take_exported_commands(),
58            header_parsers: app_context.take_exported_header_parsers(),
59        })
60    }
61
62    pub fn new_from_file(path: &str, app_context: &mut AppContext) -> Result<Self, Box<dyn Error>> {
63        let source = std::fs::read_to_string(path)?;
64        Self::new_from_source(&source, app_context)
65    }
66
67    pub fn get_event_handlers(&self) -> Events {
68        let mut handlers = Events::NONE;
69        if self.lua.globals().get::<Function>("on_open").is_ok() {
70            handlers |= Events::ON_OPEN;
71        }
72        if self.lua.globals().get::<Function>("on_edit").is_ok() {
73            handlers |= Events::ON_EDIT;
74        }
75        if self.lua.globals().get::<Function>("on_save").is_ok() {
76            handlers |= Events::ON_SAVE;
77        }
78        if self.lua.globals().get::<Function>("on_key").is_ok() {
79            handlers |= Events::ON_KEY;
80        }
81        if self.lua.globals().get::<Function>("on_mouse").is_ok() {
82            handlers |= Events::ON_MOUSE;
83        }
84        if self.lua.globals().get::<Function>("on_focus").is_ok() {
85            handlers |= Events::ON_FOCUS;
86        }
87        if self.lua.globals().get::<Function>("on_blur").is_ok() {
88            handlers |= Events::ON_BLUR;
89        }
90        if self.lua.globals().get::<Function>("on_paste").is_ok() {
91            handlers |= Events::ON_PASTE;
92        }
93        if self.lua.globals().get::<Function>("on_resize").is_ok() {
94            handlers |= Events::ON_RESIZE;
95        }
96        handlers
97    }
98
99    /// Handle an event, if an error occurs, return the error
100    /// see [Plugin::handle] for a version that logs the error
101    pub fn handle_with_error(
102        &mut self,
103        event: Event,
104        app_context: &mut AppContext,
105    ) -> mlua::Result<()> {
106        app_context.set_exported_commands(self.commands.take());
107        let ret = match event {
108            Event::Open => {
109                // Call the on_open function
110                let on_open = self.lua.globals().get::<Function>("on_open").unwrap();
111                self.lua.scope(|scope| {
112                    let context = app_context.to_lua(&self.lua, scope);
113                    on_open.call::<()>(context)
114                })
115            }
116            Event::Edit { new_bytes } => {
117                // Call the on_edit function
118                let on_edit = self.lua.globals().get::<Function>("on_edit").unwrap();
119                self.lua.scope(|scope| {
120                    let new_bytes = scope.create_any_userdata_ref_mut(new_bytes)?;
121                    let context = app_context.to_lua(&self.lua, scope);
122                    on_edit.call::<()>((new_bytes, context))
123                })
124            }
125            Event::Save => {
126                // Call the on_save function
127                let on_save = self.lua.globals().get::<Function>("on_save").unwrap();
128                self.lua.scope(|scope| {
129                    let context = app_context.to_lua(&self.lua, scope);
130                    on_save.call::<()>(context)
131                })
132            }
133            Event::Key { event } => {
134                // Call the on_key function
135                let on_key = self.lua.globals().get::<Function>("on_key").unwrap();
136                let event = key_event_to_lua(&self.lua, &event).unwrap();
137                self.lua.scope(|scope| {
138                    let context = app_context.to_lua(&self.lua, scope);
139                    on_key.call::<()>((event, context))
140                })
141            }
142            Event::Mouse { event, location } => {
143                // Call the on_mouse function
144                let on_mouse = self.lua.globals().get::<Function>("on_mouse").unwrap();
145                let event = mouse_event_to_lua(&self.lua, &event, location).unwrap();
146                self.lua.scope(|scope| {
147                    let context = app_context.to_lua(&self.lua, scope);
148                    on_mouse.call::<()>((event, context))
149                })
150            }
151            Event::Focus => {
152                let on_focus = self.lua.globals().get::<Function>("on_focus").unwrap();
153                self.lua.scope(|scope| {
154                    let context = app_context.to_lua(&self.lua, scope);
155                    on_focus.call::<()>(context)
156                })
157            }
158            Event::Blur => {
159                let on_blur = self.lua.globals().get::<Function>("on_blur").unwrap();
160                self.lua.scope(|scope| {
161                    let context = app_context.to_lua(&self.lua, scope);
162                    on_blur.call::<()>(context)
163                })
164            }
165            Event::Paste { text } => {
166                let on_paste = self.lua.globals().get::<Function>("on_paste").unwrap();
167                self.lua.scope(|scope| {
168                    let text = self.lua.create_string(text).unwrap();
169                    let context = app_context.to_lua(&self.lua, scope);
170                    on_paste.call::<()>((text, context))
171                })
172            }
173            Event::Resize { width, height } => {
174                let on_resize = self.lua.globals().get::<Function>("on_resize").unwrap();
175                self.lua.scope(|scope| {
176                    let context = app_context.to_lua(&self.lua, scope);
177                    on_resize.call::<()>((width, height, context))
178                })
179            }
180        };
181        self.commands = app_context.take_exported_commands();
182        ret
183    }
184
185    pub fn handle(&mut self, event: Event, app_context: &mut AppContext) {
186        if let Err(e) = self.handle_with_error(event, app_context) {
187            app_context.logger.log(
188                NotificationLevel::Error,
189                t!("app.messages.plugin_error", e = e),
190            );
191        }
192    }
193
194    pub fn run_command(&mut self, command: &str, app_context: &mut AppContext) -> mlua::Result<()> {
195        let command_fn = self.lua.globals().get::<Function>(command)?;
196        app_context.set_exported_commands(self.commands.take());
197        app_context.set_exported_header_parsers(self.header_parsers.take());
198        let ret = self.lua.scope(|scope| {
199            let context = app_context.to_lua(&self.lua, scope);
200            command_fn.call::<()>(context)
201        });
202        self.commands = app_context.take_exported_commands();
203        self.header_parsers = app_context.take_exported_header_parsers();
204        ret
205    }
206
207    pub fn get_commands(&self) -> &[CommandInfo] {
208        self.commands.get_commands()
209    }
210
211    pub fn fill_popup(
212        &self,
213        callback: impl AsRef<str>,
214        mut popup_context: PopupContext,
215        mut app_context: AppContext,
216    ) -> mlua::Result<()> {
217        let callback = self
218            .lua
219            .globals()
220            .get::<Function>(callback.as_ref())
221            .unwrap();
222        self.lua.scope(|scope| {
223            let popup_context = popup_context.to_lua(&self.lua, scope);
224            let context = app_context.to_lua(&self.lua, scope);
225            callback.call::<()>((popup_context, context))
226        })
227    }
228
229    pub fn try_parse_header(&mut self, app_context: &mut AppContext) -> Option<CustomHeader> {
230        for parser in self.header_parsers.parsers.iter() {
231            let mut header_context = HeaderContext::default();
232            app_context.set_exported_commands(self.commands.take());
233            let parser_fn = self
234                .lua
235                .globals()
236                .get::<Function>(parser.parser.clone())
237                .unwrap();
238            let result = self.lua.scope(|scope| {
239                let context = app_context.to_lua(&self.lua, scope);
240                let header_context = scope.create_userdata_ref_mut(&mut header_context)?;
241                parser_fn.call::<()>((header_context, context))
242            });
243            self.commands = app_context.take_exported_commands();
244            match result {
245                Err(e) => {
246                    app_context.logger.log(
247                        NotificationLevel::Error,
248                        t!("app.messages.plugin_error", e = e),
249                    );
250                }
251                Ok(()) => {
252                    if let Some(header) = header_context.try_into_custom_header() {
253                        return Some(header);
254                    }
255                }
256            }
257        }
258        None
259    }
260}
261
262#[cfg(test)]
263mod test {
264    use crossterm::event::{KeyCode, KeyEvent};
265    use object::Architecture;
266    use ratatui::{backend::TestBackend, layout::Alignment, style::Style, text::Text, Terminal};
267
268    use crate::{
269        app::{log::NotificationLevel, settings::settings_value::SettingsValue, App},
270        get_app_context,
271        headers::{bitness::Bitness, section::Section},
272    };
273
274    use super::*;
275
276    #[test]
277    fn test_init_plugin() {
278        let test_value = 42;
279        let source = format!(
280            "
281            test_value = 0
282            function init(context)
283                test_value = {test_value}
284            end
285        "
286        );
287        let mut app = App::mockup(vec![0; 0x100]);
288        let mut app_context = get_app_context!(app);
289        let plugin = Plugin::new_from_source(&source, &mut app_context).unwrap();
290        assert_eq!(
291            plugin.lua.globals().get::<i32>("test_value").unwrap(),
292            test_value
293        );
294    }
295
296    #[test]
297    fn test_discover_event_handlers() {
298        let source = "
299            function on_open(context) end
300            function on_edit(new_bytes, context) end
301            function on_save(context) end
302            function on_key(key_event, context) end
303            function on_mouse(mouse_event, context) end
304            function on_focus(context) end
305            function on_blur(context) end
306            function on_paste(text, context) end
307            function on_resize(width, height, context) end
308        ";
309        let mut app = App::mockup(vec![0; 0x100]);
310        let mut app_context = get_app_context!(app);
311        let plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
312        let handlers = plugin.get_event_handlers();
313        assert!(handlers.is_all());
314        let source = "
315            function on_open(context) end
316            function on_edit(new_bytes, context) end
317            function on_save(context) end
318        ";
319        let plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
320        let handlers = plugin.get_event_handlers();
321        assert_eq!(
322            handlers,
323            Events::ON_OPEN | Events::ON_EDIT | Events::ON_SAVE
324        );
325    }
326
327    #[test]
328    fn test_edit_open_data() {
329        let source = "
330            function on_open(context)
331                context.data:set(0,42)
332            end
333        ";
334        let mut app = App::mockup(vec![0; 0x100]);
335        let mut app_context = get_app_context!(app);
336        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
337        let event = Event::Open;
338        plugin.handle_with_error(event, &mut app_context).unwrap();
339        assert_eq!(app.data.bytes()[0], 42);
340    }
341
342    #[test]
343    fn test_init_change_settings() {
344        let source = "
345            function init(context)
346                context.settings.color_address_selected = {fg=\"#ff0000\",bg=\"Black\"}
347                context.settings.color_address_default = {fg=2}
348
349                context.settings.key_up = {code=\"Down\",modifiers=0,kind=\"Press\",state=0}
350
351                if context.settings:get_custom(\"test\") ~= \"Hello\" then
352                    error(\"Custom setting not set\")
353                end
354                context.settings:set_custom(\"string\", \"World\")
355                context.settings:set_custom(\"integer\", 42)
356                context.settings:set_custom(\"float\", 3.14)
357                context.settings:set_custom(\"boolean\", true)
358                context.settings:set_custom(\"nil\", nil)
359                context.settings:set_custom(\"style\", {fg=\"#ff0000\",bg=\"#000000\"})
360                context.settings:set_custom(\"key\", {code=\"Up\"})
361            end
362        ";
363        let mut app = App::mockup(vec![0; 0x100]);
364        app.settings
365            .custom
366            .insert("test".to_string(), SettingsValue::from("Hello"));
367        let mut app_context = get_app_context!(app);
368        let _ = Plugin::new_from_source(source, &mut app_context).unwrap();
369        assert_eq!(
370            app.settings.color.address_selected.fg,
371            Some(ratatui::style::Color::Rgb(0xff, 0, 0))
372        );
373        assert_eq!(
374            app.settings.color.address_selected.bg,
375            Some(ratatui::style::Color::Black)
376        );
377        assert_eq!(
378            app.settings.color.address_default.fg,
379            Some(ratatui::style::Color::Indexed(2))
380        );
381        assert_eq!(app.settings.color.address_default.bg, None);
382        assert_eq!(app.settings.key.up, KeyEvent::from(KeyCode::Down));
383        assert_eq!(
384            app.settings.custom.get("string").unwrap(),
385            &SettingsValue::from("World")
386        );
387        assert_eq!(
388            app.settings.custom.get("integer").unwrap(),
389            &SettingsValue::from(42)
390        );
391        #[allow(clippy::approx_constant)]
392        {
393            assert_eq!(
394                app.settings.custom.get("float").unwrap(),
395                &SettingsValue::from(3.14)
396            );
397        }
398        assert_eq!(
399            app.settings.custom.get("boolean").unwrap(),
400            &SettingsValue::from(true)
401        );
402        assert!(!app.settings.custom.contains_key("nil"));
403        assert_eq!(
404            app.settings.custom.get("style").unwrap(),
405            &SettingsValue::from(
406                Style::new()
407                    .fg(ratatui::style::Color::Rgb(0xff, 0, 0))
408                    .bg(ratatui::style::Color::Rgb(0, 0, 0))
409            )
410        );
411        assert_eq!(
412            app.settings.custom.get("key").unwrap(),
413            &SettingsValue::from(KeyEvent::from(KeyCode::Up))
414        );
415    }
416
417    #[test]
418    fn test_on_key_with_init() {
419        let source = "
420            command = nil
421            function init(context)
422                command = context.settings.key_confirm
423            end
424            function on_key(key_event, context)
425                if key_event.code == command.code then
426                    context.data:set(context.offset, 42)
427                end
428            end
429        ";
430        let mut app = App::mockup(vec![0; 0x100]);
431        let mut app_context = get_app_context!(app);
432        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
433
434        let event = Event::Key {
435            event: KeyEvent::from(KeyCode::Down),
436        };
437        plugin.handle_with_error(event, &mut app_context).unwrap();
438        assert_eq!(app_context.data.lock().unwrap().bytes()[0], 0);
439        let event = Event::Key {
440            event: app_context.settings.key.confirm,
441        };
442        plugin.handle_with_error(event, &mut app_context).unwrap();
443        assert_eq!(app.data.bytes()[0], 42);
444    }
445
446    #[test]
447    fn test_log_from_lua() {
448        let source = "
449            function init(context)
450                context.log(1, \"Hello from init\")
451            end
452
453            function on_open(context)
454                context.log(2, \"Hello from on_open\")
455            end
456        ";
457        let mut app = App::mockup(vec![0; 0x100]);
458        app.logger.clear();
459        let mut app_context = get_app_context!(app);
460        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
461
462        {
463            let mut message_iter = app_context.logger.iter();
464            let message = message_iter.next().unwrap();
465            assert_eq!(message.level, NotificationLevel::Debug);
466            assert_eq!(message.message, "Hello from init");
467            assert_eq!(
468                app_context.logger.get_notification_level(),
469                NotificationLevel::Debug
470            );
471        }
472
473        app_context.logger.clear();
474
475        let event = Event::Open;
476        plugin.handle_with_error(event, &mut app_context).unwrap();
477
478        let mut message_iter = app_context.logger.iter();
479        let message = message_iter.next().unwrap();
480        assert_eq!(message.level, NotificationLevel::Info);
481        assert_eq!(message.message, "Hello from on_open");
482        assert_eq!(
483            app_context.logger.get_notification_level(),
484            NotificationLevel::Info
485        );
486        assert!(message_iter.next().is_none());
487    }
488
489    #[test]
490    fn test_export_command() {
491        let source = "
492            function init(context)
493                context.add_command(\"test\", \"Test command\")
494            end
495        ";
496        let mut app = App::mockup(vec![0; 0x100]);
497        app.logger.clear();
498        let mut app_context = get_app_context!(app);
499
500        assert!(
501            Plugin::new_from_source(source, &mut app_context).is_err(),
502            "Should not be able to export a command without defining it first"
503        );
504
505        let source = "
506            function init(context)
507                context.add_command(\"test\", \"Test command\")
508                context.add_command(\"test3\", \"Test command 3\")
509            end
510
511            -- Add and remove commands
512            function test(context)
513                context.add_command(\"test2\", \"Test command 2\")
514                context.remove_command(\"test\")
515            end
516
517            -- Intentional error
518            function test2(context)
519                context.add_command(\"does_not_exist\", \"This command does not exist\")
520            end
521
522            -- No duplicate command should be added
523            function test3(context)
524                context.add_command(\"test\", \"Test command\")
525                context.add_command(\"test\", \"Test command 1\")
526            end
527        ";
528
529        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
530
531        let commands = plugin.commands.get_commands();
532        assert_eq!(commands.len(), 2);
533        assert_eq!(commands[0].command, "test");
534        assert_eq!(commands[0].description, "Test command");
535        assert_eq!(commands[1].command, "test3");
536        assert_eq!(commands[1].description, "Test command 3");
537
538        plugin.run_command("test", &mut app_context).unwrap();
539
540        let commands = plugin.commands.get_commands();
541        assert_eq!(commands.len(), 2);
542        assert_eq!(commands[0].command, "test3");
543        assert_eq!(commands[0].description, "Test command 3");
544        assert_eq!(commands[1].command, "test2");
545        assert_eq!(commands[1].description, "Test command 2");
546
547        assert!(
548            plugin.run_command("test2", &mut app_context).is_err(),
549            "Should not be able to add a command that is not defined"
550        );
551
552        let commands = plugin.commands.get_commands();
553        assert_eq!(
554            commands.len(),
555            2,
556            "No commands should be lost when an error occurs"
557        );
558        assert_eq!(commands[0].command, "test3");
559        assert_eq!(commands[0].description, "Test command 3");
560        assert_eq!(commands[1].command, "test2");
561        assert_eq!(commands[1].description, "Test command 2");
562
563        plugin.run_command("test3", &mut app_context).unwrap();
564
565        let commands = plugin.commands.get_commands();
566        assert_eq!(commands.len(), 3, "No duplicate commands should be added");
567        assert_eq!(commands[0].command, "test3");
568        assert_eq!(commands[0].description, "Test command 3");
569        assert_eq!(commands[1].command, "test2");
570        assert_eq!(commands[1].description, "Test command 2");
571        assert_eq!(commands[2].command, "test");
572        assert_eq!(
573            commands[2].description, "Test command 1",
574            "Should overwrite the description of the command"
575        );
576    }
577
578    #[test]
579    fn test_header() {
580        let source = "
581            function on_open(context)
582                context.log(1, context.header.bitness)
583                context.log(1, context.header.architecture)
584                context.log(1, context.header.entry_point)
585            end
586        ";
587
588        let mut app = App::mockup(vec![0; 0x100]);
589        let mut app_context = get_app_context!(app);
590
591        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
592
593        let event = Event::Open;
594        plugin.handle_with_error(event, &mut app_context).unwrap();
595
596        assert!(app_context
597            .logger
598            .iter()
599            .any(|message| { message.message == 64.to_string() }));
600        assert!(app_context
601            .logger
602            .iter()
603            .any(|message| { message.message == format!("{:?}", Architecture::Unknown) }),);
604        assert!(app_context
605            .logger
606            .iter()
607            .any(|message| { message.message == 0.to_string() }),);
608    }
609
610    #[test]
611    fn test_parse_custom() {
612        let source = std::fs::read_to_string("test/custom_header/plugins/custom.lua").unwrap();
613        let header_32 = "test/custom_header/32.bin";
614        let header_64 = "test/custom_header/64.bin";
615        let mut app = App::default();
616        let mut terminal = Terminal::new(TestBackend::new(80, 25)).unwrap();
617        app.open_file(header_32, &mut terminal).unwrap();
618        app.logger.clear();
619        let mut app_context = get_app_context!(app);
620        let mut plugin = Plugin::new_from_source(&source, &mut app_context).unwrap();
621        assert_eq!(plugin.header_parsers.parsers.len(), 1);
622        let header = match plugin.try_parse_header(&mut app_context) {
623            Some(header) => header,
624            None => {
625                let log = app_context.logger.iter().collect::<Vec<_>>();
626                panic!("Failed to parse header: {:?}", log);
627            }
628        };
629        assert_eq!(header.bitness, Bitness::Bit32);
630        assert_eq!(header.architecture, Architecture::X86_64_X32);
631        assert_eq!(header.entry, 0x40);
632        assert_eq!(
633            header.sections[0],
634            Section {
635                name: ".text".to_string(),
636                virtual_address: 0x40,
637                file_offset: 0x40,
638                size: 0x100 - 0x40
639            }
640        );
641        assert_eq!(header.symbols[&0x40], "_start");
642
643        let mut app = App::default();
644        let mut terminal = Terminal::new(TestBackend::new(80, 25)).unwrap();
645        app.open_file(header_64, &mut terminal).unwrap();
646        app.logger.clear();
647        let mut app_context = get_app_context!(app);
648        let mut plugin = Plugin::new_from_source(&source, &mut app_context).unwrap();
649        assert_eq!(plugin.header_parsers.parsers.len(), 1);
650        let header = match plugin.try_parse_header(&mut app_context) {
651            Some(header) => header,
652            None => {
653                let log = app_context.logger.iter().collect::<Vec<_>>();
654                panic!("Failed to parse header: {:?}", log);
655            }
656        };
657        assert_eq!(header.bitness, Bitness::Bit64);
658        assert_eq!(header.architecture, Architecture::X86_64);
659        assert_eq!(header.entry, 0x50);
660        assert_eq!(
661            header.sections[0],
662            Section {
663                name: ".text".to_string(),
664                virtual_address: 0x40,
665                file_offset: 0x40,
666                size: 0x100 - 0x40
667            }
668        );
669
670        plugin.try_parse_header(&mut app_context).unwrap();
671        assert_eq!(header.bitness, Bitness::Bit64);
672        assert_eq!(header.architecture, Architecture::X86_64);
673        assert_eq!(header.entry, 0x50);
674        assert_eq!(
675            header.sections[0],
676            Section {
677                name: ".text".to_string(),
678                virtual_address: 0x40,
679                file_offset: 0x40,
680                size: 0x100 - 0x40
681            }
682        );
683        assert_eq!(header.symbols[&0x50], "_start");
684    }
685
686    #[test]
687    fn test_custom_popup() {
688        let source = "
689            function init(context)
690                context.open_popup(\"fill_popup\")
691            end
692
693            function fill_popup(popup_context, context)
694                popup_context.text:push_line(\"span1\")
695                popup_context.text:set_style({fg=\"#ff0000\"})
696                popup_context.text:push_line(\"span2\")
697                popup_context.text:reset_style()
698                popup_context.text:push_span(\"span3\")
699                popup_context.text:set_alignment(\"left\")
700                popup_context.text:push_line(\"span4\")
701                popup_context.text:reset_alignment()
702            end
703        ";
704
705        let mut app = App::mockup(vec![0; 0x100]);
706        let mut app_context = get_app_context!(app);
707        app_context.plugin_index = Some(0);
708        let plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
709        let mut popup_text = Text::default();
710        let mut title = String::new();
711        let mut height = 0;
712        let mut width = 0;
713        let popup_context = PopupContext::new(&mut popup_text, &mut title, &mut height, &mut width);
714        plugin
715            .fill_popup("fill_popup", popup_context, app_context)
716            .unwrap();
717        assert_eq!(popup_text.lines.len(), 3);
718        assert_eq!(popup_text.lines[0].spans.len(), 1);
719        assert_eq!(popup_text.lines[1].spans.len(), 2);
720        assert_eq!(popup_text.lines[2].spans.len(), 1);
721        assert_eq!(popup_text.lines[0].spans[0].content, "span1");
722        assert_eq!(
723            popup_text.lines[1].spans[0].style.fg,
724            Some(ratatui::style::Color::Rgb(0xff, 0, 0))
725        );
726        assert_eq!(popup_text.lines[1].spans[0].content, "span2");
727        assert_eq!(popup_text.lines[1].spans[1].content, "span3");
728        assert_eq!(popup_text.lines[2].alignment, Some(Alignment::Left));
729        assert_eq!(popup_text.lines[2].spans[0].content, "span4");
730    }
731
732    #[test]
733    fn test_plugin_instant() {
734        let source = "
735            function init(context)
736                start = context.get_instant_now()
737
738                counter = 0
739                while counter < 100 do
740                    counter = counter + 1
741                end
742
743                if start:elapsed() <= 0 then
744                    error(\"Timer is not increasing\")
745                end
746            end
747        ";
748
749        let mut app = App::mockup(vec![0; 0x100]);
750        let mut app_context = get_app_context!(app);
751        Plugin::new_from_source(source, &mut app_context).unwrap();
752    }
753
754    #[test]
755    fn test_jump_to() {
756        let source = "
757            function on_key(key_event, context)
758                context.jump_to(0x42)
759            end
760        ";
761
762        let mut app = App::mockup(vec![0; 0x100]);
763        let mut app_context = get_app_context!(app);
764        let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
765        let event = Event::Key {
766            event: KeyEvent::from(KeyCode::Down),
767        };
768        plugin.handle_with_error(event, &mut app_context).unwrap();
769        assert_eq!(app.get_cursor_position().global_byte_index, 0x42);
770    }
771}