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