1use std::error::Error;
2
3use ratatui::text::{Line, Span, Text};
4
5use crate::get_app_context;
6
7use crate::app::{
8 asm::assembly_line::AssemblyLine,
9 commands::command_info::CommandInfo,
10 files::{path, path_result::PathResult},
11 plugins::popup_context::PopupContext,
12 settings::color_settings::ColorSettings,
13 App,
14};
15
16use super::binary_choice::BinaryChoice;
17use super::simple_choice::SimpleChoice;
18
19#[derive(Clone, Debug)]
20pub enum PopupState {
21 Open {
22 currently_open_path: String,
23 path: String,
24 cursor: usize,
25 results: Vec<PathResult>,
26 scroll: usize,
27 },
28 Run {
29 command: String,
30 cursor: usize,
31 results: Vec<CommandInfo>,
32 scroll: usize,
33 },
34 FindText {
35 text: String,
36 cursor: usize,
37 },
38 FindSymbol {
39 filter: String,
40 cursor: usize,
41 symbols: Vec<(u64, String)>,
42 scroll: usize,
43 },
44 Log(usize),
45 InsertText {
46 text: String,
47 cursor: usize,
48 },
49 Patch {
50 assembly: String,
51 preview: Result<Vec<u8>, String>,
52 cursor: usize,
53 },
54 JumpToAddress {
55 location: String,
56 cursor: usize,
57 },
58 EditComment {
59 comment: String,
60 cursor: usize,
61 },
62 FindComment {
63 filter: String,
64 cursor: usize,
65 comments: Vec<(u64, String)>,
66 scroll: usize,
67 },
68 QuitDirtySave(SimpleChoice),
69 SaveAndQuit(BinaryChoice),
70 SaveAs {
71 path: String,
72 cursor: usize,
73 },
74 Save(BinaryChoice),
75 Help(usize),
76 Custom {
77 plugin_index: usize,
78 callback: String,
79 },
80}
81
82impl App {
83 pub(in crate::app) fn get_scrollable_popup_line_count(&self) -> usize {
84 let screen_height = self.screen_size.1 as isize;
85 let lines = match &self.popup {
86 Some(PopupState::Open { .. }) => screen_height - 7 - 2,
87 Some(PopupState::Run { .. }) => screen_height - 6 - 2,
88 Some(PopupState::FindSymbol { .. }) => screen_height - 6 - 2,
89 Some(PopupState::Log(_)) => screen_height - 4 - 2,
90 Some(PopupState::Help(_)) => screen_height - 4 - 2,
91 Some(PopupState::Patch { .. }) => screen_height - 6 - 2,
92 Some(PopupState::InsertText { .. }) => screen_height - 5 - 2,
93 Some(PopupState::FindComment { .. }) => screen_height - 6 - 2,
94 _ => unimplemented!("Popup is not supposed to have scrollable lines"),
95 };
96
97 if lines <= 0 {
98 1
99 } else {
100 lines as usize
101 }
102 }
103
104 pub(super) fn get_patch_preview(
105 &self,
106 color_settings: &ColorSettings,
107 preview: &Result<Vec<u8>, String>,
108 ) -> Line<'static> {
109 let mut preview_string = Line::raw(" ");
110 match preview {
111 Ok(preview) => {
112 let old_instruction = self.get_current_instruction();
113 if let Some(old_instruction) = old_instruction {
114 if let AssemblyLine::Instruction(instruction) = old_instruction {
115 let old_bytes_offset = instruction.file_address as usize;
116 let old_bytes_len = instruction.instruction.len();
117 let patch_len = preview.len();
118 let max_instruction_length =
119 std::cmp::min(16, self.data.len() - old_bytes_offset);
120 let old_bytes_with_max_possible_length = &self.data.bytes()
121 [old_bytes_offset..old_bytes_offset + max_instruction_length];
122 for (i, byte) in old_bytes_with_max_possible_length.iter().enumerate() {
123 if i < patch_len {
124 let style = if i >= old_bytes_len {
125 color_settings.patch_patched_greater
126 } else {
127 color_settings.patch_patched_less_or_equal
128 };
129 preview_string
130 .spans
131 .push(Span::styled(format!("{:02X} ", preview[i]), style));
132 } else if i < old_bytes_len {
133 let style = color_settings.patch_old_instruction;
134 preview_string
135 .spans
136 .push(Span::styled(format!("{:02X} ", byte), style));
137 } else {
138 let style = color_settings.patch_old_rest;
139 preview_string
140 .spans
141 .push(Span::styled(format!("{:02X} ", byte), style));
142 };
143 }
144 } else if preview.is_empty() {
145 preview_string
146 .spans
147 .push(Span::styled("Preview", color_settings.placeholder));
148 } else {
149 for byte in preview.iter() {
150 let style = Self::get_style_for_byte(color_settings, *byte);
151 preview_string
152 .spans
153 .push(Span::styled(format!("{:02X} ", byte), style));
154 }
155 }
156 }
157 }
158 Err(e) => {
159 preview_string
160 .spans
161 .push(Span::styled(e.clone(), color_settings.log_error));
162 }
163 }
164 preview_string
165 }
166
167 pub(in crate::app) fn resize_popup_if_needed(popup: &mut Option<PopupState>) {
168 match popup {
169 Some(PopupState::FindSymbol { scroll, .. })
170 | Some(PopupState::Log(scroll))
171 | Some(PopupState::Help(scroll)) => {
172 *scroll = 0;
173 }
174 _ => {}
175 }
176 }
177
178 fn get_line_from_string_and_cursor(
179 color_settings: &ColorSettings,
180 s: &str,
181 cursor: usize,
182 placeholder: &str,
183 available_width: usize,
184 show_cursor: bool,
185 ) -> Line<'static> {
186 let string = s.to_string();
187 if string.is_empty() {
188 return Line::from(vec![
189 Span::raw(" "),
190 Span::styled(placeholder.to_string(), color_settings.placeholder),
191 Span::raw(" "),
192 ]);
193 }
194 let mut spans = vec![];
195
196 let available_width = available_width.saturating_sub(2);
197
198 let skip = 0.max(cursor as isize - (available_width as isize - 1) / 2) as usize;
199 let skip = skip.min(string.len().saturating_sub(available_width));
200
201 if skip > 0 {
202 spans.push(Span::styled("<", color_settings.menu_text_selected));
203 } else {
204 spans.push(Span::raw(" "));
205 }
206
207 for (i, c) in string.chars().enumerate().skip(skip).take(available_width) {
208 if i == cursor && show_cursor {
209 spans.push(Span::styled(
210 c.to_string(),
211 color_settings.menu_text_selected,
212 ));
213 } else {
214 spans.push(Span::raw(c.to_string()));
215 }
216 }
217
218 if s.len() as isize - skip as isize > available_width as isize {
219 spans.push(Span::styled(">", color_settings.menu_text_selected));
220 } else {
221 spans.push(Span::styled(
222 " ",
223 if cursor == string.len() {
224 color_settings.menu_text_selected
225 } else {
226 color_settings.menu_text
227 },
228 ));
229 }
230
231 Line::from(spans)
232 }
233
234 fn get_line_number_string(line_number: usize, char_for_line_count: usize) -> String {
235 format!("{:width$}", line_number, width = char_for_line_count)
236 }
237
238 fn get_multiline_from_string_and_cursor(
239 color_settings: &ColorSettings,
240 s: &str,
241 cursor: usize,
242 placeholder: &str,
243 available_width: usize,
244 ) -> (Vec<Line<'static>>, usize) {
245 let string = s.to_string();
246 let line_count = &string.chars().filter(|c| *c == '\n').count() + 1;
247 let char_for_line_count = line_count.to_string().len();
248 if string.is_empty() {
249 return (
250 vec![Line::from(vec![
251 Span::styled(
252 Self::get_line_number_string(1, char_for_line_count),
253 color_settings.patch_line_number,
254 ),
255 Span::raw(" "),
256 Span::styled(placeholder.to_string(), color_settings.placeholder),
257 ])
258 .left_aligned()],
259 0,
260 );
261 }
262 let mut lines = Vec::new();
263 let mut selected_line = 0;
264 let mut current_line = String::new();
265 let mut start_of_line_index = 0;
266 for (i, c) in string.chars().enumerate() {
267 if i == cursor {
268 selected_line = lines.len();
269 }
270 if c == '\n' {
271 let line_number = Span::styled(
272 Self::get_line_number_string(lines.len() + 1, char_for_line_count),
273 color_settings.patch_line_number,
274 );
275 let mut line_cursor = cursor as isize - start_of_line_index as isize;
276 let mut show_cursor = true;
277 if line_cursor > current_line.len() as isize || line_cursor < 0 {
278 show_cursor = false;
279 line_cursor = 0;
280 } else {
281 current_line.push(' ');
282 }
283 start_of_line_index = i + 1;
284 let used_width = line_number.content.len();
285 let mut line = Self::get_line_from_string_and_cursor(
286 color_settings,
287 ¤t_line,
288 line_cursor as usize,
289 "",
290 available_width - used_width,
291 show_cursor,
292 );
293 line.spans.insert(0, line_number);
294 lines.push(line.left_aligned());
295 current_line.clear();
296 } else {
297 current_line.push(c);
298 }
299 }
300 if cursor == string.len() {
301 if current_line.is_empty() {
302 current_line.push(' ');
303 }
304 selected_line = lines.len();
305 }
306 let line_number = Span::styled(
307 Self::get_line_number_string(lines.len() + 1, char_for_line_count),
308 color_settings.patch_line_number,
309 );
310 let mut line_cursor = cursor as isize - start_of_line_index as isize;
311 let mut show_cursor = true;
312 if line_cursor > current_line.len() as isize || line_cursor < 0 {
313 show_cursor = false;
314 line_cursor = 0;
315 }
316 let used_width = line_number.content.len();
317 let mut line = Self::get_line_from_string_and_cursor(
318 color_settings,
319 ¤t_line,
320 line_cursor as usize,
321 "",
322 available_width - used_width,
323 show_cursor,
324 );
325 line.spans.insert(0, line_number);
326 lines.push(line.left_aligned());
327 (lines, selected_line)
328 }
329
330 pub(in crate::app) fn fill_popup(
331 &mut self,
332 popup_title: &mut String,
333 popup_text: &mut Text<'static>,
334 height: &mut usize,
335 width: &mut usize,
336 ) -> Result<(), Box<dyn Error>> {
337 match &self.popup {
338 Some(PopupState::Open {
339 currently_open_path,
340 path,
341 cursor,
342 results,
343 scroll,
344 }) => {
345 *popup_title = "Open".into();
346 let available_width = width.saturating_sub(2);
347 let max_results = self.get_scrollable_popup_line_count();
348 *height = max_results + 2 + 5;
349
350 let editable_string = Self::get_line_from_string_and_cursor(
351 &self.settings.color,
352 path,
353 *cursor,
354 "Path",
355 available_width,
356 true,
357 );
358
359 let (prefix, currently_open_path_text) =
360 if let Some(parent) = path::parent(currently_open_path) {
361 (".../", path::diff(currently_open_path, parent))
362 } else {
363 ("", currently_open_path.as_str())
364 };
365
366 popup_text.lines.extend(vec![
367 Line::styled(
368 format!(" {}{}", prefix, currently_open_path_text),
369 self.settings.color.path_dir,
370 )
371 .left_aligned(),
372 editable_string.left_aligned(),
373 Line::raw("─".repeat(*width)),
374 ]);
375 let skip = 0.max(*scroll as isize - max_results as isize / 2) as usize;
376 let skip = skip.min(results.len().saturating_sub(max_results));
377 let relative_scroll = *scroll - skip;
378 let results_iter =
379 results
380 .iter()
381 .skip(skip)
382 .take(max_results)
383 .enumerate()
384 .map(|(i, p)| {
385 p.to_line(
386 &self.settings.color,
387 relative_scroll == i,
388 currently_open_path,
389 )
390 });
391 if skip > 0 {
392 popup_text.lines.push(Line::from(vec![Span::styled(
393 "▲",
394 self.settings.color.menu_text,
395 )]));
396 } else {
397 popup_text.lines.push(Line::raw(""));
398 }
399 popup_text.lines.extend(results_iter);
400 if results.len() as isize - skip as isize > max_results as isize {
401 popup_text.lines.push(Line::from(vec![Span::styled(
402 "▼",
403 self.settings.color.menu_text,
404 )]));
405 } else {
406 popup_text.lines.push(Line::raw(""));
407 }
408 }
409 Some(PopupState::Run {
410 command,
411 cursor,
412 results,
413 scroll,
414 }) => {
415 *popup_title = "Run".into();
416 let available_width = width.saturating_sub(2);
417 let max_results = self.get_scrollable_popup_line_count();
418 *height = max_results + 2 + 4;
419 let mut editable_string = Self::get_line_from_string_and_cursor(
420 &self.settings.color,
421 command,
422 *cursor,
423 "Command",
424 available_width,
425 true,
426 );
427 editable_string
428 .spans
429 .insert(0, Span::styled(" >", self.settings.color.menu_text));
430 popup_text.lines.extend(vec![
431 editable_string.left_aligned(),
432 Line::raw("─".repeat(*width)),
433 ]);
434 let skip = 0.max(*scroll as isize - max_results as isize / 2) as usize;
435 let skip = skip.min(results.len().saturating_sub(max_results));
436 let relative_scroll = *scroll - skip;
437 let results_iter = results
438 .iter()
439 .skip(skip)
440 .take(max_results)
441 .enumerate()
442 .map(|(i, c)| c.to_line(&self.settings.color, relative_scroll == i));
443 if skip > 0 {
444 popup_text.lines.push(Line::from(vec![Span::styled(
445 "▲",
446 self.settings.color.menu_text,
447 )]));
448 } else {
449 popup_text.lines.push(Line::raw(""));
450 }
451 popup_text.lines.extend(results_iter);
452 if results.len() as isize - skip as isize > max_results as isize {
453 popup_text.lines.push(Line::from(vec![Span::styled(
454 "▼",
455 self.settings.color.menu_text,
456 )]));
457 } else {
458 popup_text.lines.push(Line::raw(""));
459 }
460 }
461 Some(PopupState::FindText { text, cursor }) => {
462 *popup_title = "Find Text".into();
463 let available_width = width.saturating_sub(2);
464 *height = 3;
465 let editable_string = Self::get_line_from_string_and_cursor(
466 &self.settings.color,
467 text,
468 *cursor,
469 "Text",
470 available_width,
471 true,
472 );
473 popup_text
474 .lines
475 .extend(vec![editable_string.left_aligned()]);
476 }
477 Some(PopupState::FindSymbol {
478 filter,
479 symbols,
480 cursor,
481 scroll,
482 }) => {
483 *popup_title = "Find Symbol".into();
484 let available_width = width.saturating_sub(2);
485 let max_symbols = self.get_scrollable_popup_line_count();
486 *height = max_symbols + 2 + 4;
487 let mut selection = *scroll;
488 let symbols_len = if !symbols.is_empty() {
489 symbols.len()
490 } else if let Some(symbol_table) = self.header.get_symbols() {
491 symbol_table.len()
492 } else {
493 0
494 };
495 let scroll = if *scroll as isize > symbols_len as isize - (max_symbols as isize) / 2
496 {
497 symbols_len.saturating_sub(max_symbols)
498 } else if *scroll < max_symbols / 2 {
499 0
500 } else {
501 scroll.saturating_sub(max_symbols / 2)
502 };
503 selection = selection.saturating_sub(scroll);
504
505 let editable_string = Self::get_line_from_string_and_cursor(
506 &self.settings.color,
507 filter,
508 *cursor,
509 "Filter",
510 available_width,
511 true,
512 );
513 if self.header.get_symbols().is_some() {
514 let symbols_as_lines = if !symbols.is_empty() || filter.is_empty() {
515 let additional_vector = if filter.is_empty() {
516 if let Some(symbol_table) = self.header.get_symbols() {
517 symbol_table
518 .iter()
519 .skip(scroll)
520 .take(max_symbols + 1)
521 .map(|(k, v)| (*k, v.clone()))
522 .collect()
523 } else {
524 Vec::new()
525 }
526 } else {
527 Vec::new()
528 };
529
530 let symbol_to_line_lambda =
531 |(i, (address, name)): (usize, &(u64, String))| {
532 let short_name = name
533 .chars()
534 .take(width.saturating_sub(19))
535 .collect::<String>();
536 let space_count = (width.saturating_sub(short_name.len() + 19) + 1)
537 .clamp(0, *width);
538 let (style_sym, style_empty, style_addr) = if i == selection {
539 (
540 self.settings.color.assembly_selected,
541 self.settings.color.assembly_selected,
542 self.settings.color.assembly_selected,
543 )
544 } else {
545 (
546 self.settings.color.assembly_symbol,
547 self.settings.color.assembly_symbol,
548 self.settings.color.assembly_address,
549 )
550 };
551 Line::from(vec![
552 Span::styled(short_name, style_sym),
553 Span::styled(" ".repeat(space_count), style_empty),
554 Span::styled(format!("{:16X}", address), style_addr),
555 ])
556 .left_aligned()
557 };
558 let symbol_line_iter = symbols
559 .iter()
560 .skip(scroll)
561 .take(max_symbols)
562 .enumerate()
563 .map(symbol_to_line_lambda);
564 let mut symbols_as_lines = if scroll > 0 {
565 vec![Line::from(vec![Span::styled(
566 "▲",
567 self.settings.color.menu_text,
568 )])]
569 } else {
570 vec![Line::raw("")]
571 };
572
573 symbols_as_lines.extend(symbol_line_iter);
574 symbols_as_lines.extend(
575 additional_vector
576 .iter()
577 .take(max_symbols)
578 .enumerate()
579 .map(symbol_to_line_lambda),
580 );
581 if symbols_as_lines.len() < max_symbols {
582 symbols_as_lines
583 .extend(vec![Line::raw(""); max_symbols - symbols_as_lines.len()]);
584 }
585
586 if symbols.len() as isize - scroll as isize > max_symbols as isize
587 || additional_vector.len() > max_symbols
588 {
589 symbols_as_lines.push(Line::from(vec![Span::styled(
590 "▼",
591 self.settings.color.menu_text,
592 )]));
593 } else {
594 symbols_as_lines.push(Line::raw(""));
595 }
596
597 symbols_as_lines
598 } else {
599 let mut lines = vec![Line::raw("No symbols found.").left_aligned()];
600 lines.extend(vec![Line::raw(""); 7]);
601 lines
602 };
603 popup_text.lines.extend(vec![
604 editable_string.left_aligned(),
605 Line::raw("─".repeat(*width)),
606 ]);
607 popup_text.lines.extend(symbols_as_lines);
608 } else {
609 popup_text
610 .lines
611 .extend(vec![Line::raw("No symbol table found.").left_aligned()]);
612 }
613 }
614 Some(PopupState::Log(scroll)) => {
615 *popup_title = "Log".into();
616 let max_lines = self.get_scrollable_popup_line_count();
617 *height = max_lines + 4;
618 if !self.logger.is_empty() {
619 if self.logger.len() as isize - *scroll as isize > max_lines as isize {
620 popup_text.lines.push(Line::from(vec![Span::styled(
621 "▲",
622 self.settings.color.menu_text,
623 )]));
624 } else {
625 popup_text.lines.push(Line::raw(""));
626 }
627 for line in self.logger.iter().rev().skip(*scroll).take(max_lines).rev() {
629 popup_text.lines.push(line.to_line(&self.settings.color));
630 }
631 if *scroll > 0 {
632 popup_text.lines.push(Line::from(vec![Span::styled(
633 "▼",
634 self.settings.color.menu_text,
635 )]));
636 } else {
637 popup_text.lines.push(Line::raw(""));
638 }
639 }
640 }
641 Some(PopupState::InsertText { text, cursor }) => {
642 *popup_title = "Text".into();
643 let available_editable_text_lines = self.get_scrollable_popup_line_count();
644 *height = 3 + 2 + available_editable_text_lines;
645 let available_width = width.saturating_sub(2);
646 let (editable_lines, selected_line) = Self::get_multiline_from_string_and_cursor(
647 &self.settings.color,
648 text,
649 *cursor,
650 "Text",
651 available_width,
652 );
653 let skip_lines = 0
654 .max(selected_line as isize - (available_editable_text_lines as isize - 1) / 2)
655 as usize;
656 let skip_lines = skip_lines.min(
657 editable_lines
658 .len()
659 .saturating_sub(available_editable_text_lines),
660 );
661 if skip_lines == 0 {
662 popup_text.lines.push(Line::raw(""));
663 } else {
664 popup_text.lines.push(Line::from(vec![Span::styled(
665 "▲",
666 self.settings.color.menu_text,
667 )]));
668 }
669 let editable_lines_count = editable_lines.len();
670 popup_text.lines.extend(
671 editable_lines
672 .into_iter()
673 .skip(skip_lines)
674 .take(available_editable_text_lines),
675 );
676 for _ in 0..(available_editable_text_lines as isize - editable_lines_count as isize)
677 {
678 popup_text.lines.push(Line::raw(""));
679 }
680 if editable_lines_count as isize - skip_lines as isize
681 > available_editable_text_lines as isize
682 {
683 popup_text.lines.push(Line::from(vec![Span::styled(
684 "▼",
685 self.settings.color.menu_text,
686 )]));
687 } else {
688 popup_text.lines.push(Line::raw(""));
689 }
690 let status = format!("{}B", text.len());
691 let padding = width.saturating_sub(status.len());
692 popup_text.lines.push(
693 Line::styled(
694 format!("{}{}", status, " ".repeat(padding)),
695 self.settings.color.insert_text_status,
696 )
697 .left_aligned(),
698 )
699 }
700 Some(PopupState::Patch {
701 assembly,
702 preview,
703 cursor,
704 }) => {
705 *popup_title = "Patch".into();
706 let available_editable_text_lines = self.get_scrollable_popup_line_count();
707 *height = 6 + available_editable_text_lines;
708 let available_width = width.saturating_sub(2);
709 let (editable_lines, selected_line) = Self::get_multiline_from_string_and_cursor(
710 &self.settings.color,
711 assembly,
712 *cursor,
713 "Assembly",
714 available_width,
715 );
716 let preview_line = self.get_patch_preview(&self.settings.color, preview);
717 popup_text.lines.extend(vec![
718 preview_line.left_aligned(),
719 Line::raw("─".repeat(*width)),
720 ]);
721 let skip_lines = 0
722 .max(selected_line as isize - (available_editable_text_lines as isize - 1) / 2)
723 as usize;
724 let skip_lines = skip_lines.min(
725 editable_lines
726 .len()
727 .saturating_sub(available_editable_text_lines),
728 );
729 if skip_lines == 0 {
730 popup_text.lines.push(Line::raw(""));
731 } else {
732 popup_text.lines.push(Line::from(vec![Span::styled(
733 "▲",
734 self.settings.color.menu_text,
735 )]));
736 }
737 let editable_lines_count = editable_lines.len();
738 popup_text.lines.extend(
739 editable_lines
740 .into_iter()
741 .skip(skip_lines)
742 .take(available_editable_text_lines),
743 );
744 if editable_lines_count as isize - skip_lines as isize
745 > available_editable_text_lines as isize
746 {
747 popup_text.lines.push(Line::from(vec![Span::styled(
748 "▼",
749 self.settings.color.menu_text,
750 )]));
751 } else {
752 popup_text.lines.push(Line::raw(""));
753 }
754 }
755 Some(PopupState::JumpToAddress {
756 location: address,
757 cursor,
758 }) => {
759 *popup_title = "Jump".into();
760 let available_width = width.saturating_sub(2);
761 *height = 3;
762 let editable_string = Self::get_line_from_string_and_cursor(
763 &self.settings.color,
764 address,
765 *cursor,
766 "Location",
767 available_width,
768 true,
769 );
770 popup_text
771 .lines
772 .extend(vec![editable_string.left_aligned()]);
773 }
774 Some(PopupState::EditComment { comment, cursor }) => {
775 *popup_title = "Edit Comment".into();
776 let available_width = width.saturating_sub(2);
777 *height = 3;
778 let editable_string = Self::get_line_from_string_and_cursor(
779 &self.settings.color,
780 comment,
781 *cursor,
782 "Comment",
783 available_width,
784 true,
785 );
786 popup_text
787 .lines
788 .extend(vec![editable_string.left_aligned()]);
789 }
790 Some(PopupState::FindComment {
791 filter,
792 comments,
793 cursor,
794 scroll,
795 }) => {
796 *popup_title = "Find Comment".into();
797 let available_width = width.saturating_sub(2);
798 let max_comments = self.get_scrollable_popup_line_count();
799 *height = max_comments + 2 + 4;
800 let mut selection = *scroll;
801 let comments_len = if !comments.is_empty() {
802 comments.len()
803 } else {
804 self.comments.len()
805 };
806 let scroll =
807 if *scroll as isize > comments_len as isize - (max_comments as isize) / 2 {
808 comments_len.saturating_sub(max_comments)
809 } else if *scroll < max_comments / 2 {
810 0
811 } else {
812 scroll.saturating_sub(max_comments / 2)
813 };
814 selection = selection.saturating_sub(scroll);
815 let editable_string = Self::get_line_from_string_and_cursor(
816 &self.settings.color,
817 filter,
818 *cursor,
819 "Filter",
820 available_width,
821 true,
822 );
823
824 let comments_as_lines = if !comments.is_empty() || filter.is_empty() {
825 let additional_vector = if filter.is_empty() {
826 self.comments
827 .iter()
828 .skip(scroll)
829 .take(max_comments + 1)
830 .map(|(k, v)| (*k, v.clone()))
831 .collect()
832 } else {
833 Vec::new()
834 };
835
836 let comment_to_line_lambda =
837 |(i, (address, comment)): (usize, &(u64, String))| {
838 let short_comment = comment
839 .chars()
840 .take(width.saturating_sub(19))
841 .collect::<String>();
842 let space_count = (width.saturating_sub(short_comment.len() + 19) + 1)
843 .clamp(0, *width);
844 let (style_comment, style_empty, style_addr) = if i == selection {
845 (
846 self.settings.color.assembly_selected,
847 self.settings.color.assembly_selected,
848 self.settings.color.assembly_selected,
849 )
850 } else {
851 (
852 self.settings.color.assembly_comment,
853 self.settings.color.assembly_comment,
854 self.settings.color.assembly_address,
855 )
856 };
857 Line::from(vec![
858 Span::styled(short_comment, style_comment),
859 Span::styled(" ".repeat(space_count), style_empty),
860 Span::styled(format!("{:16X}", address), style_addr),
861 ])
862 .left_aligned()
863 };
864 let comment_line_iter = comments
865 .iter()
866 .skip(scroll)
867 .take(max_comments)
868 .enumerate()
869 .map(comment_to_line_lambda);
870 let mut comments_as_lines = if scroll > 0 {
871 vec![Line::from(vec![Span::styled(
872 "▲",
873 self.settings.color.menu_text,
874 )])]
875 } else {
876 vec![Line::raw("")]
877 };
878
879 comments_as_lines.extend(comment_line_iter);
880 comments_as_lines.extend(
881 additional_vector
882 .iter()
883 .take(max_comments)
884 .enumerate()
885 .map(comment_to_line_lambda),
886 );
887 if comments_as_lines.len() < max_comments {
888 comments_as_lines
889 .extend(vec![Line::raw(""); max_comments - comments_as_lines.len()]);
890 }
891
892 if comments.len() as isize - scroll as isize > max_comments as isize
893 || additional_vector.len() > max_comments
894 {
895 comments_as_lines.push(Line::from(vec![Span::styled(
896 "▼",
897 self.settings.color.menu_text,
898 )]));
899 } else {
900 comments_as_lines.push(Line::raw(""));
901 }
902
903 comments_as_lines
904 } else {
905 let mut lines = vec![Line::raw("No comments found.").left_aligned()];
906 lines.extend(vec![Line::raw(""); 7]);
907 lines
908 };
909 popup_text.lines.extend(vec![
910 editable_string.left_aligned(),
911 Line::raw("─".repeat(*width)),
912 ]);
913 popup_text.lines.extend(comments_as_lines);
914 }
915 Some(PopupState::SaveAndQuit(choice)) => {
916 *popup_title = "Save and Quit".into();
917 popup_text.lines.extend(vec![
918 Line::raw("The file will be saved and the program will quit."),
919 Line::raw("Are you sure?"),
920 choice.to_line(&self.settings.color),
921 ]);
922 }
923 Some(PopupState::SaveAs { path, cursor }) => {
924 *popup_title = "Save As".into();
925 let available_width = width.saturating_sub(2);
926 *height = 3;
927 let editable_string = Self::get_line_from_string_and_cursor(
928 &self.settings.color,
929 path,
930 *cursor,
931 "Path",
932 available_width,
933 true,
934 );
935 popup_text
936 .lines
937 .extend(vec![editable_string.left_aligned()]);
938 }
939 Some(PopupState::Save(choice)) => {
940 *popup_title = "Save".into();
941 popup_text.lines.extend(vec![
942 Line::raw("The file will be saved."),
943 Line::raw("Are you sure?"),
944 choice.to_line(&self.settings.color),
945 ]);
946 }
947 Some(PopupState::QuitDirtySave(choice)) => {
948 *popup_title = "Quit".into();
949 popup_text.lines.extend(vec![
950 Line::raw("The file has been modified."),
951 Line::raw("Do you want to save before quitting?"),
952 choice.to_line(&self.settings.color),
953 ]);
954 }
955 Some(PopupState::Help(scroll)) => {
956 let max_lines = self.get_scrollable_popup_line_count();
957 *height = max_lines + 4;
958 *popup_title = "Help".into();
959 if *scroll > 0 {
960 popup_text.lines.push(Line::from(vec![Span::styled(
961 "▲",
962 self.settings.color.menu_text,
963 )]));
964 } else {
965 popup_text.lines.push(Line::raw(""));
966 }
967 popup_text.lines.extend(
968 self.help_list
969 .iter()
970 .skip(*scroll)
971 .take(max_lines)
972 .map(|h| h.to_line(&self.settings.color)),
973 );
974 if self.help_list.len() as isize - *scroll as isize > max_lines as isize {
975 popup_text.lines.push(Line::from(vec![Span::styled(
976 "▼",
977 self.settings.color.menu_text,
978 )]));
979 } else {
980 popup_text.lines.push(Line::raw(""));
981 }
982 }
983 Some(PopupState::Custom {
984 plugin_index,
985 callback,
986 }) => {
987 self.plugin_manager.fill_popup(
988 *plugin_index,
989 callback.clone(),
990 PopupContext::new(popup_text, popup_title, height, width),
991 get_app_context!(self),
992 )?;
993 }
994 None => {}
995 }
996 Ok(())
997 }
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn test_get_line_from_string_and_cursor() {
1006 let color_settings = ColorSettings::get_default_dark_theme();
1007 let s = "Hello, World!";
1008 let line =
1009 App::get_line_from_string_and_cursor(&color_settings, s, 0, "Placeholder", 8, true);
1010 let text = line
1011 .spans
1012 .iter()
1013 .flat_map(|s| s.content.chars())
1014 .collect::<String>();
1015 assert!(text.contains("Hello"), "text: {}", text);
1016
1017 let line =
1018 App::get_line_from_string_and_cursor(&color_settings, s, 0, "Placeholder", 40, true);
1019 let text = line
1020 .spans
1021 .iter()
1022 .flat_map(|s| s.content.chars())
1023 .collect::<String>();
1024 assert!(text.contains("Hello, World!"), "text: {}", text);
1025 }
1026
1027 #[test]
1028 fn test_get_multiline_from_string_and_cursor() {
1029 let color_settings = ColorSettings::get_default_dark_theme();
1030 let s = "Hello, World!\nThis is a test\n";
1031 let (lines, _) =
1032 App::get_multiline_from_string_and_cursor(&color_settings, s, 0, "Placeholder", 40);
1033 let text = lines
1034 .iter()
1035 .flat_map(|l| l.spans.iter().flat_map(|s| s.content.chars()))
1036 .collect::<String>();
1037 assert!(text.contains("Hello, World!"), "text: {}", text);
1038 assert!(text.contains("This is a test"), "text: {}", text);
1039 }
1040}