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