use std::{error::Error, fs::File, process};
use arboard::Clipboard;
use ratatui::Frame;
use ratatui::crossterm::event::{self, Event, KeyEventKind};
use ratatui::layout::Rect;
use crate::buffer::AsyncBuffer;
use crate::decoder::Encoding;
use crate::windows::search::Search;
use crate::{
input,
label::Handler as LabelHandler,
screen::Handler as ScreenHandler,
windows::{
KeyHandler, Window, editor::Editor, jump_to_byte::JumpToByte,
unsaved_changes::UnsavedChanges,
},
};
#[derive(PartialEq, Copy, Clone, Debug)]
pub(crate) enum Nibble {
Beginning,
End,
}
impl Nibble {
pub(crate) fn toggle(&mut self) {
match self {
Self::Beginning => *self = Self::End,
Self::End => *self = Self::Beginning,
}
}
}
pub(crate) enum Action {
CharacterInput(usize, u8, Option<Nibble>),
Delete(usize, u8),
}
pub struct Data {
pub file: File,
pub(crate) contents: AsyncBuffer,
pub(crate) encoding: Encoding,
pub(crate) dirty: bool,
pub(crate) start_address: usize,
pub(crate) offset: usize,
pub(crate) nibble: Nibble,
pub(crate) last_click: Window,
pub(crate) drag_enabled: bool,
pub(crate) last_drag: Option<usize>,
pub(crate) drag_nibble: Option<Nibble>,
pub(crate) clipboard: Option<Clipboard>,
pub(crate) editor: Editor,
pub(crate) actions: Vec<Action>,
pub(crate) search_term: String,
pub(crate) search_offsets: Vec<usize>,
}
impl Data {
pub(crate) fn reindex_search(&mut self) {
self.search_offsets = self
.contents
.windows(self.search_term.len())
.enumerate()
.filter_map(|(idx, w)| (w == self.search_term.as_bytes()).then_some(idx))
.collect();
if let Ok(hex_search_term) = hex::decode(self.search_term.replace(' ', "")) {
self.search_offsets.extend(
self.contents
.windows(hex_search_term.len())
.enumerate()
.filter_map(|(idx, w)| (w == hex_search_term).then_some(idx))
.collect::<Vec<usize>>(),
);
}
}
}
pub struct Application {
pub data: Data,
pub(crate) display: ScreenHandler,
pub labels: LabelHandler,
pub key_handler: Box<dyn KeyHandler>,
}
impl Application {
pub fn new(file: File, encoding: Encoding, offset: usize) -> Result<Self, Box<dyn Error>> {
let contents = AsyncBuffer::new(&file)?;
if contents.is_empty() {
eprintln!("heh does not support editing empty files");
process::exit(1);
} else if offset >= contents.len() {
eprintln!(
"The specified offset ({offset}) is too large! (must be less than {})",
contents.len()
);
process::exit(1);
}
let mut labels = LabelHandler::new(&contents, offset);
let clipboard = Clipboard::new().ok();
if clipboard.is_none() {
labels.notification = String::from("Can't find clipboard!");
}
let display = ScreenHandler::new()?;
let app = Self {
data: Data {
file,
contents,
encoding,
dirty: false,
start_address: (offset / display.comp_layouts.bytes_per_line)
* display.comp_layouts.bytes_per_line,
offset,
nibble: Nibble::Beginning,
last_click: Window::Unhandled,
drag_enabled: false,
last_drag: None,
drag_nibble: None,
clipboard,
editor: Editor::Hex,
actions: vec![],
search_term: String::new(),
search_offsets: Vec::new(),
},
display,
labels,
key_handler: Box::from(Editor::Hex),
};
Ok(app)
}
pub fn run(&mut self) -> Result<(), Box<dyn Error>> {
ScreenHandler::setup()?;
loop {
self.render_display()?;
let event = event::read()?;
if !self.handle_input(&event)? {
break;
}
}
self.display.teardown()?;
Ok(())
}
fn render_display(&mut self) -> Result<(), Box<dyn Error>> {
self.display.render(&mut self.data, &self.labels, self.key_handler.as_ref())
}
pub fn render_frame(&mut self, frame: &mut Frame, area: Rect) {
self.data.contents.compute_new_window(self.data.offset);
if area != self.display.terminal_size {
self.display.terminal_size = area;
self.display.comp_layouts =
ScreenHandler::calculate_dimensions(area, self.key_handler.as_ref());
self.data.start_address = (self.data.start_address
+ (self.display.comp_layouts.bytes_per_line / 2))
/ self.display.comp_layouts.bytes_per_line
* self.display.comp_layouts.bytes_per_line;
}
ScreenHandler::render_frame(
frame,
self.display.terminal_size,
&mut self.data,
&self.labels,
self.key_handler.as_ref(),
&self.display.comp_layouts,
);
}
pub fn handle_input(&mut self, event: &Event) -> Result<bool, Box<dyn Error>> {
match event {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
self.labels.notification.clear();
return input::handle_key_input(self, *key);
}
}
Event::Mouse(mouse) => {
self.labels.notification.clear();
input::handle_mouse_input(self, *mouse);
}
Event::Resize(_, _) | Event::FocusGained | Event::FocusLost | Event::Paste(_) => {}
}
Ok(true)
}
pub(crate) fn set_focused_window(&mut self, window: Window) {
match window {
Window::Hex => {
self.key_handler = Box::from(Editor::Hex);
self.data.editor = Editor::Hex;
}
Window::Ascii => {
self.key_handler = Box::from(Editor::Ascii);
self.data.editor = Editor::Ascii;
}
Window::JumpToByte => {
self.key_handler = Box::from(JumpToByte::new());
self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions(
self.display.terminal_size,
self.key_handler.as_ref(),
);
}
Window::Search => {
self.key_handler = Box::from(Search::new());
self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions(
self.display.terminal_size,
self.key_handler.as_ref(),
);
}
Window::UnsavedChanges => {
self.key_handler = Box::from(UnsavedChanges::new());
self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions(
self.display.terminal_size,
self.key_handler.as_ref(),
);
}
Window::Unhandled | Window::Label(_) => {
panic!()
}
}
}
pub(crate) fn focus_editor(&mut self) {
self.key_handler = Box::from(self.data.editor);
}
}