hex_patch/app/popup/
popup_state.rs

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                    &current_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            &current_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                    // take the last 8 lines skipping "scroll" lines from the bottom
628                    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}