use crate::app::hex_view::{HexSource, HexView, MAX_RECORD_SIZE, Origin, PromptKind};
use crate::app::jobs::{Answer, Job};
use crate::{App, AppEvent, InputMode, Overlay};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::path::PathBuf;
#[derive(Default)]
pub struct HexState {
pub view: Option<crate::app::hex_view::HexView>,
pub(crate) serial: u64,
}
impl HexState {
pub(crate) fn close(&mut self) {
self.view = None;
}
}
impl App {
pub(crate) fn has_format_specs(&self) -> bool {
!self.formats.is_empty()
}
pub fn hex_view(&self) -> Option<&HexView> {
self.hex
.view
.as_ref()
.filter(|_| self.overlay == Overlay::Hex)
}
pub(crate) fn hex_target(&self) -> Option<PathBuf> {
let several = self
.source
.opened
.as_ref()
.is_some_and(|(paths, options)| paths.len() > 1 || options.hive);
self.path.clone().filter(|path| {
self.data_table_state.is_some()
&& !several
&& !self.reads_stdin()
&& !crate::cloud::source::is_remote_url(path)
&& !crate::cloud::source::is_prefix_or_glob(&path.to_string_lossy())
})
}
pub(crate) fn open_hex(
&mut self,
path: PathBuf,
origin: Origin,
fallback: bool,
record_size: Option<usize>,
) {
self.stop_hex_find();
let job = Job::HexOpen {
origin,
fallback,
record_size,
};
self.spawn_job(job, Some("Opening as hex..."), move |_| {
HexSource::open(path.clone())
.map(|source| Answer::HexOpened(Box::new(source)))
.map_err(|e| format!("{}: {e}", path.display()))
});
}
pub(crate) fn hex_opened(
&mut self,
origin: Origin,
fallback: bool,
record_size: Option<usize>,
source: HexSource,
) {
self.hex.serial += 1;
let mut view = HexView::new(source, origin, fallback, self.hex.serial);
view.record_size = record_size.map(|n| n.clamp(1, MAX_RECORD_SIZE));
view.input = crate::widgets::text_input::TextInput::new().with_theme(&self.theme);
self.hex.view = Some(view);
self.open_overlay(Overlay::Hex);
}
pub(crate) fn land_on_hex(&mut self, hex: crate::loading::Hex) {
let crate::loading::Hex {
file,
from_home,
asked,
record_size,
} = hex;
self.status_message = None;
self.busy = false;
let origin = if from_home {
Origin::Home
} else if self.data_table_state.is_some() {
Origin::Table
} else {
Origin::Launch
};
self.open_hex(file, origin, !asked, record_size);
}
fn leave_hex(&mut self, quit: bool) -> Option<AppEvent> {
let origin = self.hex.view.as_ref()?.origin;
match origin {
Origin::Home => {
self.enter_home();
None
}
Origin::Table | Origin::Info if quit && !self.source.opened_from_home => {
Some(AppEvent::Exit)
}
Origin::Table | Origin::Info if quit => {
self.enter_home();
None
}
Origin::Table | Origin::Info => {
self.close_overlay();
if self.data_table_state.is_none() {
self.input_mode = InputMode::Home;
}
if origin == Origin::Info && self.data_table_state.is_some() {
self.open_overlay(Overlay::Info);
}
None
}
Origin::Launch if quit => Some(AppEvent::Exit),
Origin::Launch => None,
}
}
pub(crate) fn hex_q_label(&self) -> &'static str {
match self.hex.view.as_ref().map(|v| v.origin) {
Some(Origin::Home) => "Home",
Some(Origin::Table | Origin::Info) if self.source.opened_from_home => "Home",
_ => "Quit",
}
}
pub(crate) fn hex_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
if !event.is_press() {
return None;
}
let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
let view = self.hex.view.as_mut()?;
if let Some(picker) = view.picker.as_mut() {
use crate::app::form::{PickerKey, picker_key};
match picker_key(picker, false, event) {
PickerKey::Close => view.picker = None,
PickerKey::Choose => {
let name = picker
.selected_original()
.map(|i| picker.items()[i].clone());
view.picker = None;
return name.and_then(|name| self.read_hex_with_spec(name));
}
PickerKey::ChooseAndMove(_)
| PickerKey::Toggle
| PickerKey::Handled
| PickerKey::Other => {}
}
return None;
}
if let Some(kind) = view.prompt {
match event.code {
KeyCode::Esc => {
view.prompt = None;
view.prompt_error = None;
view.input.set_focused(false);
}
KeyCode::Enter => return self.hex_prompt_submit(kind),
KeyCode::Char('u') if ctrl && kind == PromptKind::Find => {
view.utf16 = !view.utf16;
view.prompt_error = None;
}
_ => {
view.input.handle_key(event, None);
view.prompt_error = None;
}
}
return None;
}
let page = view.geometry.rows.max(1) as i64;
match event.code {
KeyCode::Esc => {
if view.inspector_open {
view.inspector_open = false;
} else if view.mark.is_some() {
view.mark = None;
} else {
return self.leave_hex(false);
}
}
KeyCode::Char('q') => return self.leave_hex(true),
KeyCode::Char('?') => self.open_help_overlay(),
KeyCode::Char('f') if ctrl => view.step_rows(page),
KeyCode::Char('b') if ctrl => view.step_rows(-page),
KeyCode::Char('d') if ctrl => view.step_rows((page / 2).max(1)),
KeyCode::Char('u') if ctrl => view.step_rows(-(page / 2).max(1)),
_ if ctrl => {}
KeyCode::Left | KeyCode::Char('h') => view.step(-1),
KeyCode::Right | KeyCode::Char('l') => view.step(1),
KeyCode::Up | KeyCode::Char('k') => view.step_rows(-1),
KeyCode::Down | KeyCode::Char('j') => view.step_rows(1),
KeyCode::Char('w') => view.next_group(),
KeyCode::Char('b') => view.previous_group(),
KeyCode::Char('0') => view.row_start(),
KeyCode::Char('$') => view.row_end(),
KeyCode::Home | KeyCode::Char('g') => view.go(0),
KeyCode::End | KeyCode::Char('G') => view.go(u64::MAX),
KeyCode::PageDown => view.step_rows(page),
KeyCode::PageUp => view.step_rows(-page),
KeyCode::Char('v') => {
view.mark = match view.mark {
Some(_) => None,
None if view.is_empty() => None,
None => Some(view.cursor),
};
}
KeyCode::Char('#') => view.decimal = !view.decimal,
KeyCode::Char('i') | KeyCode::Enter => {
if view.geometry.room_for_panel {
view.inspector = !view.inspector;
} else {
view.inspector_open = !view.inspector_open;
}
}
KeyCode::Char(':') => open_prompt(view, PromptKind::GoTo, String::new()),
KeyCode::Char('f') | KeyCode::Char('/') => {
let last = view
.found
.as_ref()
.map(|f| f.pattern.label.clone())
.unwrap_or_default();
open_prompt(view, PromptKind::Find, last);
}
KeyCode::Char('r') => {
let now = view.record_size.map(|n| n.to_string()).unwrap_or_default();
open_prompt(view, PromptKind::RecordSize, now);
}
KeyCode::Char('R') => {
if let Some(stride) = view.found.as_ref().and_then(|f| f.stride)
&& (1..=MAX_RECORD_SIZE as u64).contains(&stride)
{
view.record_size = Some(stride as usize);
}
}
KeyCode::Char('n') => return self.hex_find_again(true),
KeyCode::Char('N') => return self.hex_find_again(false),
KeyCode::Char('B') => self.open_hex_spec_picker(),
_ => {}
}
None
}
fn hex_prompt_submit(&mut self, kind: PromptKind) -> Option<AppEvent> {
let view = self.hex.view.as_mut()?;
let text = view.input.value().to_string();
match kind {
PromptKind::GoTo => {
match crate::app::hex_view::parse_offset(&text, view.cursor, view.len()) {
Ok(at) => {
view.go(at);
close_prompt(view);
}
Err(e) => view.prompt_error = Some(e),
}
}
PromptKind::RecordSize => {
let text = text.trim();
if text.is_empty() || text == "0" || text.eq_ignore_ascii_case("auto") {
view.record_size = None;
close_prompt(view);
} else {
match text.parse::<usize>() {
Ok(n) if (1..=MAX_RECORD_SIZE).contains(&n) => {
view.record_size = Some(n);
close_prompt(view);
}
_ => {
view.prompt_error = Some(format!(
"{text}: bytes per row are 1 to {MAX_RECORD_SIZE}, or empty for as many as fit"
));
}
}
}
}
PromptKind::Find => match crate::app::hex_view::parse_pattern(&text, view.utf16) {
Ok(pattern) => {
close_prompt(view);
let from = view.cursor;
self.start_hex_find(pattern, from, true);
}
Err(e) => view.prompt_error = Some(e),
},
}
None
}
fn open_hex_spec_picker(&mut self) {
if self.formats.is_empty() {
self.flash_note(
"No format specs on the search path; `datui formats` lists where it looks"
.to_string(),
);
return;
}
let names: Vec<String> = self
.formats
.specs
.iter()
.map(|found| found.spec.name.clone())
.collect();
if let Some(view) = self.hex.view.as_mut() {
view.picker = Some(crate::widgets::ui::PickerState::new(names));
}
}
fn read_hex_with_spec(&mut self, name: String) -> Option<AppEvent> {
let view = self.hex.view.as_ref()?;
let path = view.path.clone();
let from_home = view.origin == Origin::Home;
let options = crate::OpenOptions {
spec_name: Some(name),
..self
.source
.opened
.as_ref()
.filter(|(paths, _)| paths.first() == Some(&path))
.map(|(_, options)| options.clone())
.unwrap_or_default()
};
let options = crate::OpenOptions {
hex: false,
spec_file: None,
spec_fetched: None,
format_read: None,
format: None,
..options
};
self.source.opened_from_home = from_home || self.source.opened_from_home;
self.close_overlay();
self.set_loading_phase("Scanning input", 10);
self.name_what_is_loading(path.clone());
Some(AppEvent::Open(vec![path], options))
}
}
fn open_prompt(view: &mut HexView, kind: PromptKind, text: String) {
view.prompt = Some(kind);
view.prompt_error = None;
view.input.set_value(text);
view.input.select_all();
view.input.set_focused(true);
}
fn close_prompt(view: &mut HexView) {
view.prompt = None;
view.prompt_error = None;
view.input.set_focused(false);
}