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 let messages = app_context.logger.iter().collect::<Vec<_>>();
595 assert_eq!(messages.len(), 6);
596 assert_eq!(messages[3].message, 64.to_string(), "Default bitness is 64");
597 assert_eq!(
598 messages[4].message,
599 format!("{:?}", Architecture::Unknown),
600 "Default architecture is Unknown"
601 );
602 assert_eq!(
603 messages[5].message,
604 0.to_string(),
605 "Default entry point is 0"
606 );
607 }
608
609 #[test]
610 fn test_parse_custom() {
611 let source = std::fs::read_to_string("test/custom_header/plugins/custom.lua").unwrap();
612 let header_32 = "test/custom_header/32.bin";
613 let header_64 = "test/custom_header/64.bin";
614 let mut app = App::default();
615 let mut terminal = Terminal::new(TestBackend::new(80, 25)).unwrap();
616 app.open_file(header_32, &mut terminal).unwrap();
617 app.logger.clear();
618 let mut app_context = get_app_context!(app);
619 let mut plugin = Plugin::new_from_source(&source, &mut app_context).unwrap();
620 assert_eq!(plugin.header_parsers.parsers.len(), 1);
621 let header = match plugin.try_parse_header(&mut app_context) {
622 Some(header) => header,
623 None => {
624 let log = app_context.logger.iter().collect::<Vec<_>>();
625 panic!("Failed to parse header: {:?}", log);
626 }
627 };
628 assert_eq!(header.bitness, Bitness::Bit32);
629 assert_eq!(header.architecture, Architecture::X86_64_X32);
630 assert_eq!(header.entry, 0x40);
631 assert_eq!(
632 header.sections[0],
633 Section {
634 name: ".text".to_string(),
635 virtual_address: 0x40,
636 file_offset: 0x40,
637 size: 0x100 - 0x40
638 }
639 );
640 assert_eq!(header.symbols[&0x40], "_start");
641
642 let mut app = App::default();
643 let mut terminal = Terminal::new(TestBackend::new(80, 25)).unwrap();
644 app.open_file(header_64, &mut terminal).unwrap();
645 app.logger.clear();
646 let mut app_context = get_app_context!(app);
647 let mut plugin = Plugin::new_from_source(&source, &mut app_context).unwrap();
648 assert_eq!(plugin.header_parsers.parsers.len(), 1);
649 let header = match plugin.try_parse_header(&mut app_context) {
650 Some(header) => header,
651 None => {
652 let log = app_context.logger.iter().collect::<Vec<_>>();
653 panic!("Failed to parse header: {:?}", log);
654 }
655 };
656 assert_eq!(header.bitness, Bitness::Bit64);
657 assert_eq!(header.architecture, Architecture::X86_64);
658 assert_eq!(header.entry, 0x50);
659 assert_eq!(
660 header.sections[0],
661 Section {
662 name: ".text".to_string(),
663 virtual_address: 0x40,
664 file_offset: 0x40,
665 size: 0x100 - 0x40
666 }
667 );
668
669 plugin.try_parse_header(&mut app_context).unwrap();
670 assert_eq!(header.bitness, Bitness::Bit64);
671 assert_eq!(header.architecture, Architecture::X86_64);
672 assert_eq!(header.entry, 0x50);
673 assert_eq!(
674 header.sections[0],
675 Section {
676 name: ".text".to_string(),
677 virtual_address: 0x40,
678 file_offset: 0x40,
679 size: 0x100 - 0x40
680 }
681 );
682 assert_eq!(header.symbols[&0x50], "_start");
683 }
684
685 #[test]
686 fn test_custom_popup() {
687 let source = "
688 function init(context)
689 context.open_popup(\"fill_popup\")
690 end
691
692 function fill_popup(popup_context, context)
693 popup_context.text:push_line(\"span1\")
694 popup_context.text:set_style({fg=\"#ff0000\"})
695 popup_context.text:push_line(\"span2\")
696 popup_context.text:reset_style()
697 popup_context.text:push_span(\"span3\")
698 popup_context.text:set_alignment(\"left\")
699 popup_context.text:push_line(\"span4\")
700 popup_context.text:reset_alignment()
701 end
702 ";
703
704 let mut app = App::mockup(vec![0; 0x100]);
705 let mut app_context = get_app_context!(app);
706 app_context.plugin_index = Some(0);
707 let plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
708 let mut popup_text = Text::default();
709 let mut title = String::new();
710 let mut height = 0;
711 let mut width = 0;
712 let popup_context = PopupContext::new(&mut popup_text, &mut title, &mut height, &mut width);
713 plugin
714 .fill_popup("fill_popup", popup_context, app_context)
715 .unwrap();
716 assert_eq!(popup_text.lines.len(), 3);
717 assert_eq!(popup_text.lines[0].spans.len(), 1);
718 assert_eq!(popup_text.lines[1].spans.len(), 2);
719 assert_eq!(popup_text.lines[2].spans.len(), 1);
720 assert_eq!(popup_text.lines[0].spans[0].content, "span1");
721 assert_eq!(
722 popup_text.lines[1].spans[0].style.fg,
723 Some(ratatui::style::Color::Rgb(0xff, 0, 0))
724 );
725 assert_eq!(popup_text.lines[1].spans[0].content, "span2");
726 assert_eq!(popup_text.lines[1].spans[1].content, "span3");
727 assert_eq!(popup_text.lines[2].alignment, Some(Alignment::Left));
728 assert_eq!(popup_text.lines[2].spans[0].content, "span4");
729 }
730
731 #[test]
732 fn test_plugin_instant() {
733 let source = "
734 function init(context)
735 start = context.get_instant_now()
736
737 counter = 0
738 while counter < 100 do
739 counter = counter + 1
740 end
741
742 if start:elapsed() <= 0 then
743 error(\"Timer is not increasing\")
744 end
745 end
746 ";
747
748 let mut app = App::mockup(vec![0; 0x100]);
749 let mut app_context = get_app_context!(app);
750 Plugin::new_from_source(source, &mut app_context).unwrap();
751 }
752
753 #[test]
754 fn test_jump_to() {
755 let source = "
756 function on_key(key_event, context)
757 context.jump_to(0x42)
758 end
759 ";
760
761 let mut app = App::mockup(vec![0; 0x100]);
762 let mut app_context = get_app_context!(app);
763 let mut plugin = Plugin::new_from_source(source, &mut app_context).unwrap();
764 let event = Event::Key {
765 event: KeyEvent::from(KeyCode::Down),
766 };
767 plugin.handle_with_error(event, &mut app_context).unwrap();
768 assert_eq!(app.get_cursor_position().global_byte_index, 0x42);
769 }
770}