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 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 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 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 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 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 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}