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