hex_patch/app/
app.rs

1#![allow(clippy::module_inception)]
2use std::time::Duration;
3
4use crossterm::event;
5use ratatui::{
6    backend::Backend,
7    layout::Rect,
8    style::Style,
9    text::{Line, Text},
10    widgets::{Block, Borders, Clear, ScrollbarOrientation, ScrollbarState},
11};
12use termbg::Theme;
13
14use super::{
15    asm::assembly_line::AssemblyLine,
16    comments::Comments,
17    data::Data,
18    files::filesystem::FileSystem,
19    frame_info::{FrameInfo, InfoViewFrameInfo},
20    help::HelpLine,
21    info_mode::InfoMode,
22    log::{logger::Logger, NotificationLevel},
23    pane::Pane,
24    plugins::plugin_manager::PluginManager,
25    popup::popup_state::PopupState,
26    settings::{color_settings::ColorSettings, Settings},
27    widgets::logo::Logo,
28};
29
30use crate::{args::Args, get_app_context, headers::Header};
31
32pub struct App {
33    pub(super) plugin_manager: PluginManager,
34    pub(super) filesystem: FileSystem,
35    pub(super) header: Header,
36    pub(super) logger: Logger,
37    pub(super) help_list: Vec<HelpLine>,
38    pub(super) data: Data,
39    pub(super) comments: Comments,
40    pub(super) assembly_offsets: Vec<usize>,
41    pub(super) assembly_instructions: Vec<AssemblyLine>,
42    pub(super) text_last_searched_string: String,
43    pub(super) info_mode: InfoMode,
44    pub(super) scroll: usize,
45    pub(super) cursor: (u16, u16),
46    pub(super) selected_pane: Pane,
47    pub(super) fullscreen: bool,
48    pub(super) poll_time: Duration,
49    pub(super) needs_to_exit: bool,
50    pub(super) screen_size: (u16, u16),
51
52    pub(super) settings: Settings,
53
54    pub(super) popup: Option<PopupState>,
55
56    pub(super) vertical_margin: u16,
57    pub(super) block_size: usize,
58    pub(super) blocks_per_row: usize,
59
60    pub(super) last_frame_info: FrameInfo,
61}
62
63impl App {
64    pub(super) fn print_loading_status<B: Backend>(
65        color_settings: &ColorSettings,
66        status: &str,
67        terminal: &mut ratatui::Terminal<B>,
68    ) -> Result<(), String> {
69        terminal
70            .draw(|f| {
71                let area = f.area();
72                let mut text = Text::default();
73                for _ in 0..(area.height.saturating_sub(1)) {
74                    text.lines.push(ratatui::text::Line::default());
75                }
76                text.lines
77                    .push(Line::styled(status.to_string(), color_settings.menu_text));
78                let paragraph = ratatui::widgets::Paragraph::new(text)
79                    .block(Block::default().borders(Borders::NONE));
80                let logo = Logo::default();
81                let logo_size = logo.get_size();
82                f.render_widget(paragraph, area);
83                if logo_size.0 < area.width && logo_size.1 < area.height {
84                    f.render_widget(
85                        logo,
86                        Rect::new(
87                            area.width / 2 - logo_size.0 / 2,
88                            area.height / 2 - logo_size.1 / 2,
89                            logo_size.0,
90                            logo_size.1,
91                        ),
92                    );
93                }
94            })
95            .map_err(|e| e.to_string())?;
96        Ok(())
97    }
98
99    pub(super) fn get_size<B: Backend>(
100        terminal: &mut ratatui::Terminal<B>,
101    ) -> Result<(u16, u16), String> {
102        terminal
103            .size()
104            .map_err(|e| e.to_string())
105            .map(|s| (s.width, s.height))
106    }
107
108    pub(super) fn switch_fullscreen(&mut self) {
109        self.fullscreen = !self.fullscreen;
110    }
111
112    pub fn new<B: Backend>(
113        args: Args,
114        terminal: &mut ratatui::Terminal<B>,
115        terminal_theme: Result<Theme, termbg::Error>,
116    ) -> Result<Self, String> {
117        let mut logger = Logger::default();
118        let terminal_theme = match terminal_theme {
119            Ok(theme) => theme,
120            Err(e) => {
121                logger.log(
122                    NotificationLevel::Debug,
123                    t!("errors.detect_terminal_theme", e = e),
124                );
125                Theme::Dark
126            }
127        };
128        let settings = match Settings::load_or_create(args.config.as_deref(), terminal_theme) {
129            Ok(settings) => settings,
130            Err(e) => {
131                logger.log(NotificationLevel::Error, t!("errors.load_settings", e = e));
132                Settings::default()
133            }
134        };
135        settings.app.locale.apply();
136        logger.change_limit(settings.app.log_limit);
137        logger.change_verbosity(settings.app.log_level);
138        Self::print_loading_status(
139            &settings.color,
140            &t!("app.messages.opening_path", path = &args.path),
141            terminal,
142        )?;
143
144        let filesystem = if let Some(ssh) = &args.ssh {
145            FileSystem::new_remote(&args.path, ssh, args.password.as_deref())
146                .map_err(|e| t!("errors.connect_ssh", ssh = ssh, e = e))?
147        } else {
148            FileSystem::new_local(&args.path).map_err(|e| e.to_string())?
149        };
150        let screen_size = Self::get_size(terminal)?;
151
152        let mut app = App {
153            filesystem,
154            screen_size,
155            help_list: Self::help_list(&settings.key),
156            settings,
157            logger,
158            ..Default::default()
159        };
160
161        let mut app_context = get_app_context!(app);
162        app.plugin_manager = match PluginManager::load(args.plugins.as_deref(), &mut app_context) {
163            Ok(plugins) => plugins,
164            Err(e) => {
165                app.log(NotificationLevel::Error, t!("errors.load_plugins", e = e));
166                PluginManager::default()
167            }
168        };
169
170        if app.filesystem.is_file(app.filesystem.pwd()) {
171            let path = app.filesystem.pwd().to_string();
172            app.open_file(&path, terminal).map_err(|e| e.to_string())?;
173        } else {
174            let dir = app.filesystem.pwd().to_string();
175            Self::open_dir(&mut app.popup, &dir, &mut app.filesystem).map_err(|e| e.to_string())?;
176        }
177
178        Ok(app)
179    }
180
181    pub fn draw<B: Backend>(
182        &mut self,
183        terminal: &mut ratatui::Terminal<B>,
184    ) -> Result<(), Box<dyn std::error::Error>> {
185        terminal.draw(|f| {
186            let screen_size = (f.area().width, f.area().height);
187            self.resize_to_size(screen_size.0, screen_size.1);
188
189            let min_width = self.block_size as u16 * 3 + 17 + 3;
190            if f.area().width < min_width {
191                return;
192            }
193            let status_rect = Rect::new(0, f.area().height - 1, f.area().width, 1);
194            let address_rect = Rect::new(0, 0, 17, f.area().height - status_rect.height);
195            let hex_editor_rect: Rect;
196            let info_view_rect: Rect;
197            if self.fullscreen {
198                hex_editor_rect = Rect::new(
199                    address_rect.width,
200                    0,
201                    f.area().width - address_rect.width - 2,
202                    f.area().height - status_rect.height,
203                );
204                info_view_rect = Rect::new(
205                    address_rect.width,
206                    0,
207                    f.area().width - address_rect.width - 2,
208                    f.area().height - status_rect.height,
209                );
210            } else {
211                hex_editor_rect = Rect::new(
212                    address_rect.width,
213                    0,
214                    (self.block_size * 3 * self.blocks_per_row + self.blocks_per_row) as u16,
215                    f.area().height - status_rect.height,
216                );
217                info_view_rect = Rect::new(
218                    address_rect.width + hex_editor_rect.width,
219                    0,
220                    f.area().width - hex_editor_rect.width - address_rect.width - 2,
221                    f.area().height - status_rect.height,
222                );
223            }
224
225            let scrollbar_rect = Rect::new(f.area().width - 1, 0, 1, f.area().height);
226
227            let status_block = ratatui::widgets::Paragraph::new(self.build_status_bar())
228                .block(Block::default().borders(Borders::NONE));
229
230            let scrolled_amount = self.get_cursor_position().global_byte_index;
231            let total_amount = self.data.len();
232            let scrollbar = ratatui::widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight)
233                .style(self.settings.color.scrollbar)
234                .begin_symbol(None)
235                .end_symbol(None)
236                .track_symbol(None);
237            let mut scrollbar_state = ScrollbarState::new(total_amount).position(scrolled_amount);
238
239            let mut info_view_frame_info = InfoViewFrameInfo::TextView;
240
241            if !self.data.is_empty() {
242                let line_start_index = self.scroll;
243                let line_end_index = (self.scroll + f.area().height as usize).saturating_sub(2);
244
245                let address_view = self.get_address_view(line_start_index, line_end_index);
246                let hex_view = self.get_hex_view(line_start_index, line_end_index);
247
248                let address_block = ratatui::widgets::Paragraph::new(address_view).block(
249                    Block::default()
250                        .title(t!("app.address_view_title"))
251                        .borders(Borders::LEFT | Borders::TOP),
252                );
253
254                let editor_title = t!(
255                    "app.hex_view_title",
256                    dirty = if self.data.dirty() { " *" } else { "" }
257                );
258
259                let hex_border_style: Style;
260                let pretty_border_style: Style;
261                if self.selected_pane == Pane::Hex {
262                    hex_border_style = self.settings.color.pane_selected;
263                    pretty_border_style = self.settings.color.pane;
264                } else {
265                    hex_border_style = self.settings.color.pane;
266                    pretty_border_style = self.settings.color.pane_selected;
267                }
268
269                let hex_editor_block = ratatui::widgets::Paragraph::new(hex_view).block(
270                    Block::default()
271                        .title(editor_title)
272                        .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
273                        .border_style(hex_border_style),
274                );
275
276                let info_view_block_flags = if self.fullscreen {
277                    Borders::TOP | Borders::RIGHT | Borders::LEFT
278                } else {
279                    Borders::TOP | Borders::RIGHT
280                };
281
282                let info_view_block = match &self.info_mode {
283                    InfoMode::Text => {
284                        let text_subview_lines =
285                            self.get_text_view(line_start_index, line_end_index);
286                        info_view_frame_info = InfoViewFrameInfo::TextView;
287                        let mut text_subview = Text::default();
288                        text_subview
289                            .lines
290                            .extend(text_subview_lines.iter().cloned());
291                        ratatui::widgets::Paragraph::new(text_subview).block(
292                            Block::default()
293                                .title(t!("app.text_view_title"))
294                                .borders(info_view_block_flags)
295                                .border_style(pretty_border_style),
296                        )
297                    }
298                    InfoMode::Assembly => {
299                        let assembly_start_index = self.get_assembly_view_scroll();
300                        info_view_frame_info = InfoViewFrameInfo::AssemblyView {
301                            scroll: assembly_start_index,
302                        };
303                        let assembly_end_index = (assembly_start_index + f.area().height as usize
304                            - 2)
305                        .min(self.assembly_instructions.len());
306                        let assembly_subview_lines =
307                            &self.assembly_instructions[assembly_start_index..assembly_end_index];
308                        let mut assembly_subview = Text::default();
309                        let address_min_width = self
310                            .assembly_instructions
311                            .last()
312                            .map(|x| format!("{:X}", x.file_address()).len() + 1)
313                            .unwrap_or(1);
314                        assembly_subview
315                            .lines
316                            .extend(assembly_subview_lines.iter().map(|x| {
317                                x.to_line(
318                                    &self.settings.color,
319                                    self.get_cursor_position().global_byte_index,
320                                    &self.header,
321                                    address_min_width,
322                                    &self.comments,
323                                )
324                            }));
325                        ratatui::widgets::Paragraph::new(assembly_subview).block(
326                            Block::default()
327                                .title(t!("app.assembly_view_title"))
328                                .borders(info_view_block_flags)
329                                .border_style(pretty_border_style),
330                        )
331                    }
332                };
333
334                f.render_widget(address_block, address_rect);
335                if self.fullscreen {
336                    match self.selected_pane {
337                        Pane::Hex => f.render_widget(hex_editor_block, hex_editor_rect),
338                        Pane::View => f.render_widget(info_view_block, info_view_rect),
339                    }
340                } else {
341                    f.render_widget(hex_editor_block, hex_editor_rect);
342                    f.render_widget(info_view_block, info_view_rect);
343                }
344            }
345            f.render_widget(status_block, status_rect);
346            f.render_stateful_widget(scrollbar, scrollbar_rect, &mut scrollbar_state);
347
348            let mut this_frame_info = FrameInfo {
349                popup: None,
350                status_bar: status_rect,
351                scroll_bar: scrollbar_rect,
352                address_view: address_rect,
353                hex_view: if !self.fullscreen || self.selected_pane == Pane::Hex {
354                    Some(hex_editor_rect)
355                } else {
356                    None
357                }, // only save the hex view rect if it's visible, we need to know if it's visible to
358                // determine the cursor position
359                info_view: if !self.fullscreen || self.selected_pane == Pane::View {
360                    Some(info_view_rect)
361                } else {
362                    None
363                }, // only save the info view rect if it's visible, we need to know if it's visible to
364                // determine the cursor position
365                info_view_frame_info,
366                blocks_per_row: self.blocks_per_row,
367                scroll: self.scroll,
368                file_size: self.data.len(),
369            };
370
371            // Draw popup
372            if self.popup.is_some() {
373                let mut popup_text = Text::default();
374                let mut popup_title = t!("app.default_popup_title").into();
375
376                let mut popup_width = 60;
377                let mut popup_height = 5;
378
379                let popup_result = self.fill_popup(
380                    &mut popup_title,
381                    &mut popup_text,
382                    &mut popup_height,
383                    &mut popup_width,
384                );
385
386                popup_height = popup_height.min(f.area().height.saturating_sub(2) as usize);
387                popup_width = popup_width.min(f.area().width.saturating_sub(1) as usize);
388                let popup_rect = Rect::new(
389                    (f.area().width / 2).saturating_sub((popup_width / 2 + 1) as u16),
390                    (f.area().height / 2).saturating_sub((popup_height / 2) as u16),
391                    popup_width as u16,
392                    popup_height as u16,
393                );
394
395                match popup_result {
396                    Ok(()) => {
397                        let popup = ratatui::widgets::Paragraph::new(popup_text)
398                            .block(Block::default().title(popup_title).borders(Borders::ALL))
399                            .alignment(ratatui::layout::Alignment::Center);
400                        f.render_widget(Clear, popup_rect);
401                        f.render_widget(popup, popup_rect);
402                    }
403                    Err(e) => {
404                        self.logger.log(
405                            NotificationLevel::Error,
406                            t!("app.messages.popup_error", e = e),
407                        );
408                    }
409                }
410                this_frame_info.popup = Some(popup_rect)
411            }
412            self.last_frame_info = this_frame_info;
413        })?;
414
415        Ok(())
416    }
417
418    pub fn run<B: Backend>(
419        &mut self,
420        terminal: &mut ratatui::Terminal<B>,
421    ) -> Result<(), Box<dyn std::error::Error>> {
422        self.screen_size = (terminal.size()?.width, terminal.size()?.height);
423        self.resize_to_size(self.screen_size.0, self.screen_size.1);
424
425        while !self.needs_to_exit {
426            if event::poll(self.poll_time)? {
427                while event::poll(Duration::from_millis(0))? {
428                    let event = event::read()?;
429                    let event_result = self.handle_event(event, terminal);
430                    if let Err(e) = event_result {
431                        self.log(NotificationLevel::Error, e.to_string());
432                    }
433                }
434            }
435
436            self.draw(terminal)?;
437        }
438
439        Ok(())
440    }
441}
442
443impl Default for App {
444    fn default() -> Self {
445        App {
446            plugin_manager: PluginManager::default(),
447            filesystem: FileSystem::default(),
448            header: Header::None,
449            logger: Logger::default(),
450            help_list: Self::help_list(&Settings::default().key),
451            data: Data::default(),
452            comments: Comments::default(),
453            assembly_offsets: Vec::new(),
454            assembly_instructions: Vec::new(),
455            text_last_searched_string: String::new(),
456            info_mode: InfoMode::Text,
457            scroll: 0,
458            cursor: (0, 0),
459            selected_pane: Pane::Hex,
460            fullscreen: false,
461            poll_time: Duration::from_millis(1000),
462            needs_to_exit: false,
463            screen_size: (0, 0),
464
465            settings: Settings::default(),
466
467            popup: None,
468
469            vertical_margin: 2,
470            block_size: 8,
471            blocks_per_row: 1,
472
473            last_frame_info: FrameInfo {
474                popup: None,
475                status_bar: Rect::default(),
476                scroll_bar: Rect::default(),
477                address_view: Rect::default(),
478                hex_view: Some(Rect::default()),
479                info_view: Some(Rect::default()),
480                info_view_frame_info: InfoViewFrameInfo::TextView,
481                blocks_per_row: 1,
482                scroll: 0,
483                file_size: 0,
484            },
485        }
486    }
487}