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 &format!("Could not detect terminal theme: {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(
132 NotificationLevel::Error,
133 &format!("Error loading settings: {e}"),
134 );
135 Settings::default()
136 }
137 };
138 logger.change_limit(settings.app.log_limit);
139 logger.change_verbosity(settings.app.log_level);
140 Self::print_loading_status(
141 &settings.color,
142 &format!("Opening \"{}\"...", args.path),
143 terminal,
144 )?;
145
146 let filesystem = if let Some(ssh) = &args.ssh {
147 FileSystem::new_remote(&args.path, ssh, args.password.as_deref())
148 .map_err(|e| format!("Failed to connect to {}: {e}", ssh))?
149 } else {
150 FileSystem::new_local(&args.path).map_err(|e| e.to_string())?
151 };
152 let screen_size = Self::get_size(terminal)?;
153
154 let mut app = App {
155 filesystem,
156 screen_size,
157 help_list: Self::help_list(&settings.key),
158 settings,
159 logger,
160 ..Default::default()
161 };
162
163 let mut app_context = get_app_context!(app);
164 app.plugin_manager = match PluginManager::load(args.plugins.as_deref(), &mut app_context) {
165 Ok(plugins) => plugins,
166 Err(e) => {
167 app.log(
168 NotificationLevel::Error,
169 &format!("Error loading plugins: {e}"),
170 );
171 PluginManager::default()
172 }
173 };
174
175 if app.filesystem.is_file(app.filesystem.pwd()) {
176 let path = app.filesystem.pwd().to_string();
177 app.open_file(&path, terminal).map_err(|e| e.to_string())?;
178 } else {
179 let dir = app.filesystem.pwd().to_string();
180 Self::open_dir(&mut app.popup, &dir, &mut app.filesystem).map_err(|e| e.to_string())?;
181 }
182
183 Ok(app)
184 }
185
186 pub fn draw<B: Backend>(
187 &mut self,
188 terminal: &mut ratatui::Terminal<B>,
189 ) -> Result<(), Box<dyn std::error::Error>> {
190 terminal.draw(|f| {
191 let screen_size = (f.area().width, f.area().height);
192 self.resize_to_size(screen_size.0, screen_size.1);
193
194 let min_width = self.block_size as u16 * 3 + 17 + 3;
195 if f.area().width < min_width {
196 return;
197 }
198 let status_rect = Rect::new(0, f.area().height - 1, f.area().width, 1);
199 let address_rect = Rect::new(0, 0, 17, f.area().height - status_rect.height);
200 let hex_editor_rect: Rect;
201 let info_view_rect: Rect;
202 if self.fullscreen {
203 hex_editor_rect = Rect::new(
204 address_rect.width,
205 0,
206 f.area().width - address_rect.width - 2,
207 f.area().height - status_rect.height,
208 );
209 info_view_rect = Rect::new(
210 address_rect.width,
211 0,
212 f.area().width - address_rect.width - 2,
213 f.area().height - status_rect.height,
214 );
215 } else {
216 hex_editor_rect = Rect::new(
217 address_rect.width,
218 0,
219 (self.block_size * 3 * self.blocks_per_row + self.blocks_per_row) as u16,
220 f.area().height - status_rect.height,
221 );
222 info_view_rect = Rect::new(
223 address_rect.width + hex_editor_rect.width,
224 0,
225 f.area().width - hex_editor_rect.width - address_rect.width - 2,
226 f.area().height - status_rect.height,
227 );
228 }
229
230 let scrollbar_rect = Rect::new(f.area().width - 1, 0, 1, f.area().height);
231
232 let status_block = ratatui::widgets::Paragraph::new(self.build_status_bar())
233 .block(Block::default().borders(Borders::NONE));
234
235 let scrolled_amount = self.get_cursor_position().global_byte_index;
236 let total_amount = self.data.len();
237 let scrollbar = ratatui::widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight)
238 .style(self.settings.color.scrollbar)
239 .begin_symbol(None)
240 .end_symbol(None)
241 .track_symbol(None);
242 let mut scrollbar_state = ScrollbarState::new(total_amount).position(scrolled_amount);
243
244 let mut info_view_frame_info = InfoViewFrameInfo::TextView;
245
246 if !self.data.is_empty() {
247 let line_start_index = self.scroll;
248 let line_end_index = (self.scroll + f.area().height as usize).saturating_sub(2);
249
250 let address_view = self.get_address_view(line_start_index, line_end_index);
251 let hex_view = self.get_hex_view(line_start_index, line_end_index);
252
253 let address_block = ratatui::widgets::Paragraph::new(address_view).block(
254 Block::default()
255 .title("Address")
256 .borders(Borders::LEFT | Borders::TOP),
257 );
258
259 let editor_title =
260 format!("Hex Editor{}", if self.data.dirty() { " *" } else { "" });
261
262 let hex_border_style: Style;
263 let pretty_border_style: Style;
264 if self.selected_pane == Pane::Hex {
265 hex_border_style = self.settings.color.pane_selected;
266 pretty_border_style = self.settings.color.pane;
267 } else {
268 hex_border_style = self.settings.color.pane;
269 pretty_border_style = self.settings.color.pane_selected;
270 }
271
272 let hex_editor_block = ratatui::widgets::Paragraph::new(hex_view).block(
273 Block::default()
274 .title(editor_title)
275 .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
276 .border_style(hex_border_style),
277 );
278
279 let info_view_block_flags = if self.fullscreen {
280 Borders::TOP | Borders::RIGHT | Borders::LEFT
281 } else {
282 Borders::TOP | Borders::RIGHT
283 };
284
285 let info_view_block = match &self.info_mode {
286 InfoMode::Text => {
287 let text_subview_lines =
288 self.get_text_view(line_start_index, line_end_index);
289 info_view_frame_info = InfoViewFrameInfo::TextView;
290 let mut text_subview = Text::default();
291 text_subview
292 .lines
293 .extend(text_subview_lines.iter().cloned());
294 ratatui::widgets::Paragraph::new(text_subview).block(
295 Block::default()
296 .title("Text View")
297 .borders(info_view_block_flags)
298 .border_style(pretty_border_style),
299 )
300 }
301 InfoMode::Assembly => {
302 let assembly_start_index = self.get_assembly_view_scroll();
303 info_view_frame_info = InfoViewFrameInfo::AssemblyView {
304 scroll: assembly_start_index,
305 };
306 let assembly_end_index = (assembly_start_index + f.area().height as usize
307 - 2)
308 .min(self.assembly_instructions.len());
309 let assembly_subview_lines =
310 &self.assembly_instructions[assembly_start_index..assembly_end_index];
311 let mut assembly_subview = Text::default();
312 let address_min_width = self
313 .assembly_instructions
314 .last()
315 .map(|x| format!("{:X}", x.file_address()).len() + 1)
316 .unwrap_or(1);
317 assembly_subview
318 .lines
319 .extend(assembly_subview_lines.iter().map(|x| {
320 x.to_line(
321 &self.settings.color,
322 self.get_cursor_position().global_byte_index,
323 &self.header,
324 address_min_width,
325 &self.comments,
326 )
327 }));
328 ratatui::widgets::Paragraph::new(assembly_subview).block(
329 Block::default()
330 .title("Assembly View")
331 .borders(info_view_block_flags)
332 .border_style(pretty_border_style),
333 )
334 }
335 };
336
337 f.render_widget(address_block, address_rect);
338 if self.fullscreen {
339 match self.selected_pane {
340 Pane::Hex => f.render_widget(hex_editor_block, hex_editor_rect),
341 Pane::View => f.render_widget(info_view_block, info_view_rect),
342 }
343 } else {
344 f.render_widget(hex_editor_block, hex_editor_rect);
345 f.render_widget(info_view_block, info_view_rect);
346 }
347 }
348 f.render_widget(status_block, status_rect);
349 f.render_stateful_widget(scrollbar, scrollbar_rect, &mut scrollbar_state);
350
351 let mut this_frame_info = FrameInfo {
352 popup: None,
353 status_bar: status_rect,
354 scroll_bar: scrollbar_rect,
355 address_view: address_rect,
356 hex_view: if !self.fullscreen || self.selected_pane == Pane::Hex {
357 Some(hex_editor_rect)
358 } else {
359 None
360 }, info_view: if !self.fullscreen || self.selected_pane == Pane::View {
363 Some(info_view_rect)
364 } else {
365 None
366 }, info_view_frame_info,
369 blocks_per_row: self.blocks_per_row,
370 scroll: self.scroll,
371 file_size: self.data.len(),
372 };
373
374 if self.popup.is_some() {
376 let mut popup_text = Text::default();
377 let mut popup_title = "Popup".into();
378
379 let mut popup_width = 60;
380 let mut popup_height = 5;
381
382 let popup_result = self.fill_popup(
383 &mut popup_title,
384 &mut popup_text,
385 &mut popup_height,
386 &mut popup_width,
387 );
388
389 popup_height = popup_height.min(f.area().height.saturating_sub(2) as usize);
390 popup_width = popup_width.min(f.area().width.saturating_sub(1) as usize);
391 let popup_rect = Rect::new(
392 (f.area().width / 2).saturating_sub((popup_width / 2 + 1) as u16),
393 (f.area().height / 2).saturating_sub((popup_height / 2) as u16),
394 popup_width as u16,
395 popup_height as u16,
396 );
397
398 match popup_result {
399 Ok(()) => {
400 let popup = ratatui::widgets::Paragraph::new(popup_text)
401 .block(Block::default().title(popup_title).borders(Borders::ALL))
402 .alignment(ratatui::layout::Alignment::Center);
403 f.render_widget(Clear, popup_rect);
404 f.render_widget(popup, popup_rect);
405 }
406 Err(e) => {
407 self.logger
408 .log(NotificationLevel::Error, &format!("Filling popup: {e}"));
409 }
410 }
411 this_frame_info.popup = Some(popup_rect)
412 }
413 self.last_frame_info = this_frame_info;
414 })?;
415
416 Ok(())
417 }
418
419 pub fn run<B: Backend>(
420 &mut self,
421 terminal: &mut ratatui::Terminal<B>,
422 ) -> Result<(), Box<dyn std::error::Error>> {
423 self.screen_size = (terminal.size()?.width, terminal.size()?.height);
424 self.resize_to_size(self.screen_size.0, self.screen_size.1);
425
426 while !self.needs_to_exit {
427 if event::poll(self.poll_time)? {
428 while event::poll(Duration::from_millis(0))? {
429 let event = event::read()?;
430 let event_result = self.handle_event(event, terminal);
431 if let Err(e) = event_result {
432 self.log(NotificationLevel::Error, &e.to_string());
433 }
434 }
435 }
436
437 self.draw(terminal)?;
438 }
439
440 Ok(())
441 }
442}
443
444impl Default for App {
445 fn default() -> Self {
446 App {
447 plugin_manager: PluginManager::default(),
448 filesystem: FileSystem::default(),
449 header: Header::None,
450 logger: Logger::default(),
451 help_list: Self::help_list(&Settings::default().key),
452 data: Data::default(),
453 comments: Comments::default(),
454 assembly_offsets: Vec::new(),
455 assembly_instructions: Vec::new(),
456 text_last_searched_string: String::new(),
457 info_mode: InfoMode::Text,
458 scroll: 0,
459 cursor: (0, 0),
460 selected_pane: Pane::Hex,
461 fullscreen: false,
462 poll_time: Duration::from_millis(1000),
463 needs_to_exit: false,
464 screen_size: (0, 0),
465
466 settings: Settings::default(),
467
468 popup: None,
469
470 vertical_margin: 2,
471 block_size: 8,
472 blocks_per_row: 1,
473
474 last_frame_info: FrameInfo {
475 popup: None,
476 status_bar: Rect::default(),
477 scroll_bar: Rect::default(),
478 address_view: Rect::default(),
479 hex_view: Some(Rect::default()),
480 info_view: Some(Rect::default()),
481 info_view_frame_info: InfoViewFrameInfo::TextView,
482 blocks_per_row: 1,
483 scroll: 0,
484 file_size: 0,
485 },
486 }
487 }
488}