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