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 }, info_view: if !self.fullscreen || self.selected_pane == Pane::View {
360 Some(info_view_rect)
361 } else {
362 None
363 }, 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 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}