1use crate::app::hex_view::{HexSource, HexView, MAX_RECORD_SIZE, Origin, PromptKind};
5use crate::app::jobs::{Answer, Job};
6use crate::{App, AppEvent, InputMode, Overlay};
7use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8use std::path::PathBuf;
9
10#[derive(Default)]
12pub struct HexState {
13 pub view: Option<crate::app::hex_view::HexView>,
15 pub(crate) serial: u64,
17}
18
19impl HexState {
20 pub(crate) fn close(&mut self) {
22 self.view = None;
23 }
24}
25
26impl App {
27 pub(crate) fn has_format_specs(&self) -> bool {
29 !self.formats.is_empty()
30 }
31
32 pub fn hex_view(&self) -> Option<&HexView> {
34 self.hex
35 .view
36 .as_ref()
37 .filter(|_| self.overlay == Overlay::Hex)
38 }
39
40 pub(crate) fn hex_target(&self) -> Option<PathBuf> {
43 let several = self
44 .source
45 .opened
46 .as_ref()
47 .is_some_and(|(paths, options)| paths.len() > 1 || options.hive);
48 self.path.clone().filter(|path| {
49 self.data_table_state.is_some()
50 && !several
51 && !self.reads_stdin()
52 && !crate::cloud::source::is_remote_url(path)
53 && !crate::cloud::source::is_prefix_or_glob(&path.to_string_lossy())
54 })
55 }
56
57 pub(crate) fn open_hex(
60 &mut self,
61 path: PathBuf,
62 origin: Origin,
63 fallback: bool,
64 record_size: Option<usize>,
65 ) {
66 self.open_hex_fetched(path, None, origin, fallback, record_size);
67 }
68
69 fn open_hex_fetched(
72 &mut self,
73 path: PathBuf,
74 fetched: Option<(PathBuf, crate::cloud::download::TempDownload)>,
75 origin: Origin,
76 fallback: bool,
77 record_size: Option<usize>,
78 ) {
79 self.stop_hex_find();
80 let job = Job::HexOpen {
81 origin,
82 fallback,
83 record_size,
84 };
85 self.spawn_job(job, Some("Opening as hex..."), move |_| {
86 HexSource::open(path.clone())
87 .map(|source| Answer::HexOpened(Box::new(HexSource { fetched, ..source })))
88 .map_err(|e| format!("{}: {e}", path.display()))
89 });
90 }
91
92 pub(crate) fn hex_opened(
94 &mut self,
95 origin: Origin,
96 fallback: bool,
97 record_size: Option<usize>,
98 source: HexSource,
99 ) {
100 self.hex.serial += 1;
101 let mut view = HexView::new(source, origin, fallback, self.hex.serial);
102 view.record_size = record_size.map(|n| n.clamp(1, MAX_RECORD_SIZE));
103 view.input = crate::widgets::text_input::TextInput::new().with_theme(&self.theme);
104 self.hex.view = Some(view);
105 self.open_overlay(Overlay::Hex);
106 }
107
108 pub(crate) fn land_on_hex(&mut self, hex: crate::loading::Hex) {
110 let crate::loading::Hex {
111 file,
112 fetched,
113 from_home,
114 asked,
115 record_size,
116 } = hex;
117 self.status_message = None;
118 self.busy = false;
119 let origin = if from_home {
120 Origin::Home
121 } else if self.data_table_state.is_some() {
122 Origin::Table
123 } else {
124 Origin::Launch
125 };
126 self.open_hex_fetched(file, fetched, origin, !asked, record_size);
127 }
128
129 fn leave_hex(&mut self, quit: bool) -> Option<AppEvent> {
132 let origin = self.hex.view.as_ref()?.origin;
133 match origin {
134 Origin::Home => {
135 self.enter_home();
136 None
137 }
138 Origin::Table | Origin::Info if quit && !self.source.opened_from_home => {
139 Some(AppEvent::Exit)
140 }
141 Origin::Table | Origin::Info if quit => {
142 self.enter_home();
143 None
144 }
145 Origin::Table | Origin::Info => {
146 self.close_overlay();
147 if self.data_table_state.is_none() {
148 self.input_mode = InputMode::Home;
149 }
150 if origin == Origin::Info && self.data_table_state.is_some() {
152 self.open_overlay(Overlay::Info);
153 }
154 None
155 }
156 Origin::Launch if quit => Some(AppEvent::Exit),
157 Origin::Launch => None,
158 }
159 }
160
161 pub(crate) fn hex_q_label(&self) -> &'static str {
163 match self.hex.view.as_ref().map(|v| v.origin) {
164 Some(Origin::Home) => "Home",
165 Some(Origin::Table | Origin::Info) if self.source.opened_from_home => "Home",
166 _ => "Quit",
167 }
168 }
169
170 pub(crate) fn hex_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
172 if !event.is_press() {
173 return None;
174 }
175 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
176 let view = self.hex.view.as_mut()?;
177
178 if let Some(picker) = view.picker.as_mut() {
180 use crate::app::form::{PickerKey, picker_key};
181 match picker_key(picker, false, event) {
182 PickerKey::Close => view.picker = None,
183 PickerKey::Choose => {
184 let name = picker
185 .selected_original()
186 .map(|i| picker.items()[i].clone());
187 view.picker = None;
188 return name.and_then(|name| self.read_hex_with_spec(name));
189 }
190 PickerKey::ChooseAndMove(_)
192 | PickerKey::Toggle
193 | PickerKey::Handled
194 | PickerKey::Other => {}
195 }
196 return None;
197 }
198
199 if let Some(kind) = view.prompt {
201 match event.code {
202 KeyCode::Esc => {
203 view.prompt = None;
204 view.prompt_error = None;
205 view.input.set_focused(false);
206 }
207 KeyCode::Enter => return self.hex_prompt_submit(kind),
208 KeyCode::Char('u') if ctrl && kind == PromptKind::Find => {
209 view.utf16 = !view.utf16;
210 view.prompt_error = None;
211 }
212 _ => {
213 view.input.handle_key(event, None);
214 view.prompt_error = None;
215 }
216 }
217 return None;
218 }
219
220 let page = view.geometry.rows.max(1) as i64;
221 match event.code {
222 KeyCode::Esc => {
223 if view.inspector_open {
224 view.inspector_open = false;
225 } else if view.mark.is_some() {
226 view.mark = None;
227 } else {
228 return self.leave_hex(false);
229 }
230 }
231 KeyCode::Char('q') => return self.leave_hex(true),
232 KeyCode::Char('?') => self.open_help_overlay(),
233 KeyCode::Char('f') if ctrl => view.step_rows(page),
234 KeyCode::Char('b') if ctrl => view.step_rows(-page),
235 KeyCode::Char('d') if ctrl => view.step_rows((page / 2).max(1)),
236 KeyCode::Char('u') if ctrl => view.step_rows(-(page / 2).max(1)),
237 _ if ctrl => {}
238 KeyCode::Left | KeyCode::Char('h') => view.step(-1),
239 KeyCode::Right | KeyCode::Char('l') => view.step(1),
240 KeyCode::Up | KeyCode::Char('k') => view.step_rows(-1),
241 KeyCode::Down | KeyCode::Char('j') => view.step_rows(1),
242 KeyCode::Char('w') => view.next_group(),
243 KeyCode::Char('b') => view.previous_group(),
244 KeyCode::Char('0') => view.row_start(),
245 KeyCode::Char('$') => view.row_end(),
246 KeyCode::Home | KeyCode::Char('g') => view.go(0),
247 KeyCode::End | KeyCode::Char('G') => view.go(u64::MAX),
248 KeyCode::PageDown => view.step_rows(page),
249 KeyCode::PageUp => view.step_rows(-page),
250 KeyCode::Char('v') => {
251 view.mark = match view.mark {
252 Some(_) => None,
253 None if view.is_empty() => None,
254 None => Some(view.cursor),
255 };
256 }
257 KeyCode::Char('#') => view.decimal = !view.decimal,
258 KeyCode::Char('i') | KeyCode::Enter => {
259 if view.geometry.room_for_panel {
260 view.inspector = !view.inspector;
261 } else {
262 view.inspector_open = !view.inspector_open;
263 }
264 }
265 KeyCode::Char(':') => open_prompt(view, PromptKind::GoTo, String::new()),
266 KeyCode::Char('f') | KeyCode::Char('/') => {
267 let last = view
268 .found
269 .as_ref()
270 .map(|f| f.pattern.label.clone())
271 .unwrap_or_default();
272 open_prompt(view, PromptKind::Find, last);
273 }
274 KeyCode::Char('r') => {
275 let now = view.record_size.map(|n| n.to_string()).unwrap_or_default();
276 open_prompt(view, PromptKind::RecordSize, now);
277 }
278 KeyCode::Char('R') => {
279 if let Some(stride) = view.found.as_ref().and_then(|f| f.stride)
280 && (1..=MAX_RECORD_SIZE as u64).contains(&stride)
281 {
282 view.record_size = Some(stride as usize);
283 }
284 }
285 KeyCode::Char('n') => return self.hex_find_again(true),
286 KeyCode::Char('N') => return self.hex_find_again(false),
287 KeyCode::Char('B') => self.open_hex_spec_picker(),
288 _ => {}
289 }
290 None
291 }
292
293 fn hex_prompt_submit(&mut self, kind: PromptKind) -> Option<AppEvent> {
295 let view = self.hex.view.as_mut()?;
296 let text = view.input.value().to_string();
297 match kind {
298 PromptKind::GoTo => {
299 match crate::app::hex_view::parse_offset(&text, view.cursor, view.len()) {
300 Ok(at) => {
301 view.go(at);
302 close_prompt(view);
303 }
304 Err(e) => view.prompt_error = Some(e),
305 }
306 }
307 PromptKind::RecordSize => {
308 let text = text.trim();
309 if text.is_empty() || text == "0" || text.eq_ignore_ascii_case("auto") {
310 view.record_size = None;
311 close_prompt(view);
312 } else {
313 match text.parse::<usize>() {
314 Ok(n) if (1..=MAX_RECORD_SIZE).contains(&n) => {
315 view.record_size = Some(n);
316 close_prompt(view);
317 }
318 _ => {
319 view.prompt_error = Some(format!(
320 "{text}: bytes per row are 1 to {MAX_RECORD_SIZE}, or empty for as many as fit"
321 ));
322 }
323 }
324 }
325 }
326 PromptKind::Find => match crate::app::hex_view::parse_pattern(&text, view.utf16) {
327 Ok(pattern) => {
328 close_prompt(view);
329 let from = view.cursor;
330 self.start_hex_find(pattern, from, true);
331 }
332 Err(e) => view.prompt_error = Some(e),
333 },
334 }
335 None
336 }
337
338 fn open_hex_spec_picker(&mut self) {
340 if self.formats.is_empty() {
341 self.flash_note(
342 "No format specs on the search path; `datui formats` lists where it looks"
343 .to_string(),
344 );
345 return;
346 }
347 let names: Vec<String> = self
348 .formats
349 .specs
350 .iter()
351 .map(|found| found.spec.name.clone())
352 .collect();
353 if let Some(view) = self.hex.view.as_mut() {
354 view.picker = Some(crate::widgets::ui::PickerState::new(names));
355 }
356 }
357
358 fn read_hex_with_spec(&mut self, name: String) -> Option<AppEvent> {
360 let view = self.hex.view.as_ref()?;
361 let path = view.path.clone();
362 let from_home = view.origin == Origin::Home;
363 let options = crate::OpenOptions {
364 spec_name: Some(name),
365 ..self
366 .source
367 .opened
368 .as_ref()
369 .filter(|(paths, _)| paths.first() == Some(&path))
370 .map(|(_, options)| options.clone())
371 .unwrap_or_default()
372 };
373 let options = crate::OpenOptions {
374 hex: false,
375 spec_file: None,
376 spec_fetched: None,
377 format_read: None,
378 format: None,
379 ..options
380 };
381 self.source.opened_from_home = from_home || self.source.opened_from_home;
382 self.close_overlay();
384 self.set_loading_phase("Scanning input", 10);
385 self.name_what_is_loading(path.clone());
386 Some(AppEvent::Open(vec![path], options))
387 }
388}
389
390fn open_prompt(view: &mut HexView, kind: PromptKind, text: String) {
391 view.prompt = Some(kind);
392 view.prompt_error = None;
393 view.input.set_value(text);
394 view.input.select_all();
395 view.input.set_focused(true);
396}
397
398fn close_prompt(view: &mut HexView) {
399 view.prompt = None;
400 view.prompt_error = None;
401 view.input.set_focused(false);
402}