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 data::Data,
17 files::filesystem::FileSystem,
18 frame_info::{FrameInfo, InfoViewFrameInfo},
19 help::HelpLine,
20 info_mode::InfoMode,
21 log::{logger::Logger, NotificationLevel},
22 pane::Pane,
23 plugins::plugin_manager::PluginManager,
24 popup::popup_state::PopupState,
25 settings::{color_settings::ColorSettings, Settings},
26 widgets::logo::Logo,
27};
28
29use crate::{args::Args, get_app_context, headers::Header};
30
31pub struct App {
32 pub(super) plugin_manager: PluginManager,
33 pub(super) filesystem: FileSystem,
34 pub(super) header: Header,
35 pub(super) logger: Logger,
36 pub(super) help_list: Vec<HelpLine>,
37 pub(super) data: Data,
38 pub(super) assembly_offsets: Vec<usize>,
39 pub(super) assembly_instructions: Vec<AssemblyLine>,
40 pub(super) text_last_searched_string: String,
41 pub(super) info_mode: InfoMode,
42 pub(super) scroll: usize,
43 pub(super) cursor: (u16, u16),
44 pub(super) selected_pane: Pane,
45 pub(super) fullscreen: bool,
46 pub(super) poll_time: Duration,
47 pub(super) needs_to_exit: bool,
48 pub(super) screen_size: (u16, u16),
49
50 pub(super) settings: Settings,
51
52 pub(super) popup: Option<PopupState>,
53
54 pub(super) vertical_margin: u16,
55 pub(super) block_size: usize,
56 pub(super) blocks_per_row: usize,
57
58 pub(super) last_frame_info: FrameInfo,
59}
60
61impl App {
62 pub(super) fn print_loading_status<B: Backend>(
63 color_settings: &ColorSettings,
64 status: &str,
65 terminal: &mut ratatui::Terminal<B>,
66 ) -> Result<(), String> {
67 terminal
68 .draw(|f| {
69 let area = f.area();
70 let mut text = Text::default();
71 for _ in 0..(area.height.saturating_sub(1)) {
72 text.lines.push(ratatui::text::Line::default());
73 }
74 text.lines
75 .push(Line::styled(status.to_string(), color_settings.menu_text));
76 let paragraph = ratatui::widgets::Paragraph::new(text)
77 .block(Block::default().borders(Borders::NONE));
78 let logo = Logo::default();
79 let logo_size = logo.get_size();
80 f.render_widget(paragraph, area);
81 if logo_size.0 < area.width && logo_size.1 < area.height {
82 f.render_widget(
83 logo,
84 Rect::new(
85 area.width / 2 - logo_size.0 / 2,
86 area.height / 2 - logo_size.1 / 2,
87 logo_size.0,
88 logo_size.1,
89 ),
90 );
91 }
92 })
93 .map_err(|e| e.to_string())?;
94 Ok(())
95 }
96
97 pub(super) fn get_size<B: Backend>(
98 terminal: &mut ratatui::Terminal<B>,
99 ) -> Result<(u16, u16), String> {
100 terminal
101 .size()
102 .map_err(|e| e.to_string())
103 .map(|s| (s.width, s.height))
104 }
105
106 pub(super) fn switch_fullscreen(&mut self) {
107 self.fullscreen = !self.fullscreen;
108 }
109
110 pub fn new<B: Backend>(
111 args: Args,
112 terminal: &mut ratatui::Terminal<B>,
113 terminal_theme: Result<Theme, termbg::Error>,
114 ) -> Result<Self, String> {
115 let mut logger = Logger::default();
116 let terminal_theme = match terminal_theme {
117 Ok(theme) => theme,
118 Err(e) => {
119 logger.log(
120 NotificationLevel::Debug,
121 &format!("Could not detect terminal theme: {e}"),
122 );
123 Theme::Dark
124 }
125 };
126 let settings = match Settings::load_or_create(args.config.as_deref(), terminal_theme) {
127 Ok(settings) => settings,
128 Err(e) => {
129 logger.log(
130 NotificationLevel::Error,
131 &format!("Error loading settings: {e}"),
132 );
133 Settings::default()
134 }
135 };
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 &format!("Opening \"{}\"...", 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| format!("Failed to connect to {}: {e}", ssh))?
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(
166 NotificationLevel::Error,
167 &format!("Error loading plugins: {e}"),
168 );
169 PluginManager::default()
170 }
171 };
172
173 if app.filesystem.is_file(app.filesystem.pwd()) {
174 let path = app.filesystem.pwd().to_string();
175 app.open_file(&path, terminal).map_err(|e| e.to_string())?;
176 } else {
177 let dir = app.filesystem.pwd().to_string();
178 Self::open_dir(&mut app.popup, &dir, &mut app.filesystem).map_err(|e| e.to_string())?;
179 }
180
181 Ok(app)
182 }
183
184 pub fn draw<B: Backend>(
185 &mut self,
186 terminal: &mut ratatui::Terminal<B>,
187 ) -> Result<(), Box<dyn std::error::Error>> {
188 terminal.draw(|f| {
189 let screen_size = (f.area().width, f.area().height);
190 self.resize_to_size(screen_size.0, screen_size.1);
191
192 let min_width = self.block_size as u16 * 3 + 17 + 3;
193 if f.area().width < min_width {
194 return;
195 }
196 let status_rect = Rect::new(0, f.area().height - 1, f.area().width, 1);
197 let address_rect = Rect::new(0, 0, 17, f.area().height - status_rect.height);
198 let hex_editor_rect: Rect;
199 let info_view_rect: Rect;
200 if self.fullscreen {
201 hex_editor_rect = Rect::new(
202 address_rect.width,
203 0,
204 f.area().width - address_rect.width - 2,
205 f.area().height - status_rect.height,
206 );
207 info_view_rect = Rect::new(
208 address_rect.width,
209 0,
210 f.area().width - address_rect.width - 2,
211 f.area().height - status_rect.height,
212 );
213 } else {
214 hex_editor_rect = Rect::new(
215 address_rect.width,
216 0,
217 (self.block_size * 3 * self.blocks_per_row + self.blocks_per_row) as u16,
218 f.area().height - status_rect.height,
219 );
220 info_view_rect = Rect::new(
221 address_rect.width + hex_editor_rect.width,
222 0,
223 f.area().width - hex_editor_rect.width - address_rect.width - 2,
224 f.area().height - status_rect.height,
225 );
226 }
227
228 let scrollbar_rect = Rect::new(f.area().width - 1, 0, 1, f.area().height);
229
230 let status_block = ratatui::widgets::Paragraph::new(self.build_status_bar())
231 .block(Block::default().borders(Borders::NONE));
232
233 let scrolled_amount = self.get_cursor_position().global_byte_index;
234 let total_amount = self.data.len();
235 let scrollbar = ratatui::widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight)
236 .style(self.settings.color.scrollbar)
237 .begin_symbol(None)
238 .end_symbol(None)
239 .track_symbol(None);
240 let mut scrollbar_state = ScrollbarState::new(total_amount).position(scrolled_amount);
241
242 let mut info_view_frame_info = InfoViewFrameInfo::TextView;
243
244 if !self.data.is_empty() {
245 let line_start_index = self.scroll;
246 let line_end_index = (self.scroll + f.area().height as usize).saturating_sub(2);
247
248 let address_view = self.get_address_view(line_start_index, line_end_index);
249 let hex_view = self.get_hex_view(line_start_index, line_end_index);
250
251 let address_block = ratatui::widgets::Paragraph::new(address_view).block(
252 Block::default()
253 .title("Address")
254 .borders(Borders::LEFT | Borders::TOP),
255 );
256
257 let editor_title =
258 format!("Hex Editor{}", if self.data.dirty() { " *" } else { "" });
259
260 let hex_border_style: Style;
261 let pretty_border_style: Style;
262 if self.selected_pane == Pane::Hex {
263 hex_border_style = self.settings.color.pane_selected;
264 pretty_border_style = self.settings.color.pane;
265 } else {
266 hex_border_style = self.settings.color.pane;
267 pretty_border_style = self.settings.color.pane_selected;
268 }
269
270 let hex_editor_block = ratatui::widgets::Paragraph::new(hex_view).block(
271 Block::default()
272 .title(editor_title)
273 .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
274 .border_style(hex_border_style),
275 );
276
277 let info_view_block_flags = if self.fullscreen {
278 Borders::TOP | Borders::RIGHT | Borders::LEFT
279 } else {
280 Borders::TOP | Borders::RIGHT
281 };
282
283 let info_view_block = match &self.info_mode {
284 InfoMode::Text => {
285 let text_subview_lines =
286 self.get_text_view(line_start_index, line_end_index);
287 info_view_frame_info = InfoViewFrameInfo::TextView;
288 let mut text_subview = Text::default();
289 text_subview
290 .lines
291 .extend(text_subview_lines.iter().cloned());
292 ratatui::widgets::Paragraph::new(text_subview).block(
293 Block::default()
294 .title("Text View")
295 .borders(info_view_block_flags)
296 .border_style(pretty_border_style),
297 )
298 }
299 InfoMode::Assembly => {
300 let assembly_start_index = self.get_assembly_view_scroll();
301 info_view_frame_info = InfoViewFrameInfo::AssemblyView {
302 scroll: assembly_start_index,
303 };
304 let assembly_end_index = (assembly_start_index + f.area().height as usize
305 - 2)
306 .min(self.assembly_instructions.len());
307 let assembly_subview_lines =
308 &self.assembly_instructions[assembly_start_index..assembly_end_index];
309 let mut assembly_subview = Text::default();
310 let address_min_width = self
311 .assembly_instructions
312 .last()
313 .map(|x| format!("{:X}", x.file_address()).len() + 1)
314 .unwrap_or(1);
315 assembly_subview
316 .lines
317 .extend(assembly_subview_lines.iter().map(|x| {
318 x.to_line(
319 &self.settings.color,
320 self.get_cursor_position().global_byte_index,
321 &self.header,
322 address_min_width,
323 )
324 }));
325 ratatui::widgets::Paragraph::new(assembly_subview).block(
326 Block::default()
327 .title("Assembly View")
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 = "Popup".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
405 .log(NotificationLevel::Error, &format!("Filling popup: {e}"));
406 }
407 }
408 this_frame_info.popup = Some(popup_rect)
409 }
410 self.last_frame_info = this_frame_info;
411 })?;
412
413 Ok(())
414 }
415
416 pub fn run<B: Backend>(
417 &mut self,
418 terminal: &mut ratatui::Terminal<B>,
419 ) -> Result<(), Box<dyn std::error::Error>> {
420 self.screen_size = (terminal.size()?.width, terminal.size()?.height);
421 self.resize_to_size(self.screen_size.0, self.screen_size.1);
422
423 while !self.needs_to_exit {
424 if event::poll(self.poll_time)? {
425 while event::poll(Duration::from_millis(0))? {
426 let event = event::read()?;
427 let event_result = self.handle_event(event, terminal);
428 if let Err(e) = event_result {
429 self.log(NotificationLevel::Error, &e.to_string());
430 }
431 }
432 }
433
434 self.draw(terminal)?;
435 }
436
437 Ok(())
438 }
439}
440
441impl Default for App {
442 fn default() -> Self {
443 App {
444 plugin_manager: PluginManager::default(),
445 filesystem: FileSystem::default(),
446 header: Header::None,
447 logger: Logger::default(),
448 help_list: Self::help_list(&Settings::default().key),
449 data: Data::default(),
450 assembly_offsets: Vec::new(),
451 assembly_instructions: Vec::new(),
452 text_last_searched_string: String::new(),
453 info_mode: InfoMode::Text,
454 scroll: 0,
455 cursor: (0, 0),
456 selected_pane: Pane::Hex,
457 fullscreen: false,
458 poll_time: Duration::from_millis(1000),
459 needs_to_exit: false,
460 screen_size: (0, 0),
461
462 settings: Settings::default(),
463
464 popup: None,
465
466 vertical_margin: 2,
467 block_size: 8,
468 blocks_per_row: 1,
469
470 last_frame_info: FrameInfo {
471 popup: None,
472 status_bar: Rect::default(),
473 scroll_bar: Rect::default(),
474 address_view: Rect::default(),
475 hex_view: Some(Rect::default()),
476 info_view: Some(Rect::default()),
477 info_view_frame_info: InfoViewFrameInfo::TextView,
478 blocks_per_row: 1,
479 scroll: 0,
480 file_size: 0,
481 },
482 }
483 }
484}