1use crate::hex_view::{
5 Found, HexFindRun, HexHit, HexSource, HexView, MAX_RECORD_SIZE, Origin, PromptKind,
6};
7use crate::jobs::{Answer, Job, Progress};
8use crate::{App, AppEvent, InputMode};
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13
14impl App {
15 pub(crate) fn has_format_specs(&self) -> bool {
17 !self.formats.is_empty()
18 }
19
20 pub fn hex_view(&self) -> Option<&HexView> {
22 self.hex
23 .as_ref()
24 .filter(|_| self.input_mode == InputMode::Hex)
25 }
26
27 pub(crate) fn hex_target(&self) -> Option<PathBuf> {
30 let several = self
31 .opened
32 .as_ref()
33 .is_some_and(|(paths, options)| paths.len() > 1 || options.hive);
34 self.path.clone().filter(|path| {
35 self.data_table_state.is_some()
36 && !several
37 && !self.reads_stdin()
38 && !crate::source::is_remote_url(path)
39 && !crate::source::is_prefix_or_glob(&path.to_string_lossy())
40 })
41 }
42
43 pub(crate) fn open_hex(
46 &mut self,
47 path: PathBuf,
48 origin: Origin,
49 fallback: bool,
50 record_size: Option<usize>,
51 ) {
52 self.stop_hex_find();
53 let job = Job::HexOpen {
54 origin,
55 fallback,
56 record_size,
57 };
58 self.spawn_job(job, Some("Opening as hex..."), move |_| {
59 HexSource::open(path.clone())
60 .map(|source| Answer::HexOpened(Box::new(source)))
61 .map_err(|e| format!("{}: {e}", path.display()))
62 });
63 }
64
65 pub(crate) fn hex_opened(&mut self, job: Job, current: bool, source: HexSource) {
67 let Job::HexOpen {
68 origin,
69 fallback,
70 record_size,
71 } = job
72 else {
73 return;
74 };
75 if !current {
76 return;
77 }
78 self.hex_serial += 1;
79 let mut view = HexView::new(source, origin, fallback, self.hex_serial);
80 view.record_size = record_size.map(|n| n.clamp(1, MAX_RECORD_SIZE));
81 view.input = crate::widgets::text_input::TextInput::new().with_theme(&self.theme);
82 self.hex = Some(view);
83 self.info_modal.close();
84 self.input_mode = InputMode::Hex;
85 }
86
87 pub(crate) fn land_on_hex(&mut self, hex: crate::loading::Hex) {
89 let crate::loading::Hex {
90 file,
91 from_home,
92 asked,
93 record_size,
94 } = hex;
95 self.status_message = None;
96 self.busy = false;
97 let origin = if from_home {
98 Origin::Home
99 } else if self.data_table_state.is_some() {
100 Origin::Table
101 } else {
102 Origin::Launch
103 };
104 self.open_hex(file, origin, !asked, record_size);
105 }
106
107 fn leave_hex(&mut self, quit: bool) -> Option<AppEvent> {
110 let origin = self.hex.as_ref()?.origin;
111 match origin {
112 Origin::Home => {
113 self.enter_home();
114 None
115 }
116 Origin::Table | Origin::Info if quit && !self.opened_from_home => Some(AppEvent::Exit),
117 Origin::Table | Origin::Info if quit => {
118 self.enter_home();
119 None
120 }
121 Origin::Table | Origin::Info => {
122 self.stop_hex_find();
123 self.hex = None;
124 self.input_mode = if self.data_table_state.is_some() {
125 InputMode::Normal
126 } else {
127 InputMode::Home
128 };
129 if origin == Origin::Info && self.data_table_state.is_some() {
131 self.info_modal.active = true;
132 self.input_mode = InputMode::Info;
133 }
134 None
135 }
136 Origin::Launch if quit => Some(AppEvent::Exit),
137 Origin::Launch => None,
138 }
139 }
140
141 pub(crate) fn hex_q_label(&self) -> &'static str {
143 match self.hex.as_ref().map(|v| v.origin) {
144 Some(Origin::Home) => "Home",
145 Some(Origin::Table | Origin::Info) if self.opened_from_home => "Home",
146 _ => "Quit",
147 }
148 }
149
150 pub(crate) fn hex_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
152 if !event.is_press() {
153 return None;
154 }
155 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
156 let view = self.hex.as_mut()?;
157
158 if let Some(picker) = view.picker.as_mut() {
160 match event.code {
161 KeyCode::Esc => view.picker = None,
162 KeyCode::Enter => {
163 let name = picker
164 .selected_original()
165 .map(|i| picker.items()[i].clone());
166 view.picker = None;
167 return name.and_then(|name| self.read_hex_with_spec(name));
168 }
169 KeyCode::Up => picker.move_up(),
170 KeyCode::Down => picker.move_down(),
171 KeyCode::Backspace => picker.backspace(),
172 KeyCode::Char(c) => picker.filter_key(c, event.modifiers),
173 _ => {}
174 }
175 return None;
176 }
177
178 if let Some(kind) = view.prompt {
180 match event.code {
181 KeyCode::Esc => {
182 view.prompt = None;
183 view.prompt_error = None;
184 view.input.set_focused(false);
185 }
186 KeyCode::Enter => return self.hex_prompt_submit(kind),
187 KeyCode::Char('u') if ctrl && kind == PromptKind::Find => {
188 view.utf16 = !view.utf16;
189 view.prompt_error = None;
190 }
191 _ => {
192 view.input.handle_key(event, None);
193 view.prompt_error = None;
194 }
195 }
196 return None;
197 }
198
199 let page = view.geometry.rows.max(1) as i64;
200 match event.code {
201 KeyCode::Esc => {
202 if view.inspector_open {
203 view.inspector_open = false;
204 } else if view.mark.is_some() {
205 view.mark = None;
206 } else {
207 return self.leave_hex(false);
208 }
209 }
210 KeyCode::Char('q') => return self.leave_hex(true),
211 KeyCode::Char('?') => self.open_help_overlay(),
212 KeyCode::Char('f') if ctrl => view.step_rows(page),
213 KeyCode::Char('b') if ctrl => view.step_rows(-page),
214 KeyCode::Char('d') if ctrl => view.step_rows((page / 2).max(1)),
215 KeyCode::Char('u') if ctrl => view.step_rows(-(page / 2).max(1)),
216 _ if ctrl => {}
217 KeyCode::Left | KeyCode::Char('h') => view.step(-1),
218 KeyCode::Right | KeyCode::Char('l') => view.step(1),
219 KeyCode::Up | KeyCode::Char('k') => view.step_rows(-1),
220 KeyCode::Down | KeyCode::Char('j') => view.step_rows(1),
221 KeyCode::Char('w') => view.next_group(),
222 KeyCode::Char('b') => view.previous_group(),
223 KeyCode::Char('0') => view.row_start(),
224 KeyCode::Char('$') => view.row_end(),
225 KeyCode::Home | KeyCode::Char('g') => view.go(0),
226 KeyCode::End | KeyCode::Char('G') => view.go(u64::MAX),
227 KeyCode::PageDown => view.step_rows(page),
228 KeyCode::PageUp => view.step_rows(-page),
229 KeyCode::Char('v') => {
230 view.mark = match view.mark {
231 Some(_) => None,
232 None if view.is_empty() => None,
233 None => Some(view.cursor),
234 };
235 }
236 KeyCode::Char('#') => view.decimal = !view.decimal,
237 KeyCode::Char('i') | KeyCode::Enter => {
238 if view.geometry.room_for_panel {
239 view.inspector = !view.inspector;
240 } else {
241 view.inspector_open = !view.inspector_open;
242 }
243 }
244 KeyCode::Char(':') => open_prompt(view, PromptKind::GoTo, String::new()),
245 KeyCode::Char('f') | KeyCode::Char('/') => {
246 let last = view
247 .found
248 .as_ref()
249 .map(|f| f.pattern.label.clone())
250 .unwrap_or_default();
251 open_prompt(view, PromptKind::Find, last);
252 }
253 KeyCode::Char('r') => {
254 let now = view.record_size.map(|n| n.to_string()).unwrap_or_default();
255 open_prompt(view, PromptKind::RecordSize, now);
256 }
257 KeyCode::Char('R') => {
258 if let Some(stride) = view.found.as_ref().and_then(|f| f.stride)
259 && (1..=MAX_RECORD_SIZE as u64).contains(&stride)
260 {
261 view.record_size = Some(stride as usize);
262 }
263 }
264 KeyCode::Char('n') => return self.hex_find_again(true),
265 KeyCode::Char('N') => return self.hex_find_again(false),
266 KeyCode::Char('B') => self.open_hex_spec_picker(),
267 _ => {}
268 }
269 None
270 }
271
272 fn hex_prompt_submit(&mut self, kind: PromptKind) -> Option<AppEvent> {
274 let view = self.hex.as_mut()?;
275 let text = view.input.value().to_string();
276 match kind {
277 PromptKind::GoTo => {
278 match crate::hex_view::parse_offset(&text, view.cursor, view.len()) {
279 Ok(at) => {
280 view.go(at);
281 close_prompt(view);
282 }
283 Err(e) => view.prompt_error = Some(e),
284 }
285 }
286 PromptKind::RecordSize => {
287 let text = text.trim();
288 if text.is_empty() || text == "0" || text.eq_ignore_ascii_case("auto") {
289 view.record_size = None;
290 close_prompt(view);
291 } else {
292 match text.parse::<usize>() {
293 Ok(n) if (1..=MAX_RECORD_SIZE).contains(&n) => {
294 view.record_size = Some(n);
295 close_prompt(view);
296 }
297 _ => {
298 view.prompt_error = Some(format!(
299 "{text}: bytes per row are 1 to {MAX_RECORD_SIZE}, or empty for as many as fit"
300 ));
301 }
302 }
303 }
304 }
305 PromptKind::Find => match crate::hex_view::parse_pattern(&text, view.utf16) {
306 Ok(pattern) => {
307 close_prompt(view);
308 let from = view.cursor;
309 self.start_hex_find(pattern, from, true);
310 }
311 Err(e) => view.prompt_error = Some(e),
312 },
313 }
314 None
315 }
316
317 fn hex_find_again(&mut self, forward: bool) -> Option<AppEvent> {
319 let view = self.hex.as_ref()?;
320 let pattern = view.found.as_ref()?.pattern.clone();
321 let len = view.len();
322 if len == 0 {
323 return None;
324 }
325 let from = if forward {
326 if view.cursor + 1 >= len {
327 0
328 } else {
329 view.cursor + 1
330 }
331 } else if view.cursor == 0 {
332 len - 1
333 } else {
334 view.cursor - 1
335 };
336 self.start_hex_find(pattern, from, forward);
337 None
338 }
339
340 fn start_hex_find(&mut self, pattern: crate::hex_view::Pattern, from: u64, forward: bool) {
342 self.stop_hex_find();
343 let Some(view) = self.hex.as_mut() else {
344 return;
345 };
346 let stop = Arc::new(AtomicBool::new(false));
347 let run = HexFindRun {
348 stop: stop.clone(),
349 view: view.serial,
350 pattern: pattern.clone(),
351 };
352 let bytes = view.bytes.clone();
353 let total = view.len();
354 let status = format!("Finding {}...", pattern.label);
355 self.spawn_job(Job::HexFind(run), Some(&status), move |worker| {
356 let report = worker.reporter();
357 let hay = bytes.as_slice();
358 let mut hit = crate::hex_view::find(hay, &pattern, from, forward, &stop, |read| {
359 report(Progress::HexFinding { read, total })
360 })
361 .map_err(|_| crate::find::CANCELLED.to_string())?;
362 hit.stride = hit
363 .at
364 .and_then(|at| crate::hex_view::stride(hay, &pattern, at, &stop));
365 Ok(Answer::HexFound(hit))
366 });
367 }
368
369 pub(crate) fn hex_find_progress(&mut self, read: u64, total: u64) {
371 let Some(label) = self
372 .jobs
373 .current(|job| matches!(job, Job::HexFind(_)))
374 .and_then(|(_, job)| match job {
375 Job::HexFind(run) => Some(run.pattern.label.clone()),
376 _ => None,
377 })
378 else {
379 return;
380 };
381 let percent = (read.min(total) * 100).checked_div(total).unwrap_or(100);
382 self.status_message = Some(format!("Finding {label}... {percent}%"));
383 }
384
385 pub(crate) fn hex_found(&mut self, job: Job, current: bool, hit: HexHit) {
387 let Job::HexFind(run) = job else {
388 return;
389 };
390 if !current {
391 return;
392 }
393 self.status_message = None;
394 let Some(view) = self.hex.as_mut().filter(|v| v.serial == run.view) else {
395 return;
396 };
397 view.found = Some(Found {
398 pattern: run.pattern.clone(),
399 hit: hit.at,
400 stride: hit.stride,
401 });
402 match hit.at {
403 Some(at) => {
404 view.go(at);
405 if hit.wrapped {
406 self.flash_note(format!("Found {}, round past the end", run.pattern.label));
407 }
408 }
409 None => self.flash_note(format!("No match for {}", run.pattern.label)),
410 }
411 }
412
413 pub(crate) fn stop_hex_find(&mut self) -> bool {
415 let Some((_, Job::HexFind(run))) = self.jobs.current(|job| matches!(job, Job::HexFind(_)))
416 else {
417 return false;
418 };
419 run.stop.store(true, Ordering::Relaxed);
420 self.jobs.cancel(|job| matches!(job, Job::HexFind(_)));
421 self.status_message = None;
422 true
423 }
424
425 fn open_hex_spec_picker(&mut self) {
427 if self.formats.is_empty() {
428 self.flash_note(
429 "No format specs on the search path; `datui formats` lists where it looks"
430 .to_string(),
431 );
432 return;
433 }
434 let names: Vec<String> = self
435 .formats
436 .specs
437 .iter()
438 .map(|found| found.spec.name.clone())
439 .collect();
440 if let Some(view) = self.hex.as_mut() {
441 view.picker = Some(crate::widgets::ui::PickerState::new(names));
442 }
443 }
444
445 fn read_hex_with_spec(&mut self, name: String) -> Option<AppEvent> {
447 let view = self.hex.as_ref()?;
448 let path = view.path.clone();
449 let from_home = view.origin == Origin::Home;
450 let options = crate::OpenOptions {
451 spec_name: Some(name),
452 ..self
453 .opened
454 .as_ref()
455 .filter(|(paths, _)| paths.first() == Some(&path))
456 .map(|(_, options)| options.clone())
457 .unwrap_or_default()
458 };
459 let options = crate::OpenOptions {
460 hex: false,
461 spec_file: None,
462 spec_fetched: None,
463 format_read: None,
464 format: None,
465 ..options
466 };
467 self.opened_from_home = from_home || self.opened_from_home;
468 self.stop_hex_find();
470 self.hex = None;
471 self.input_mode = InputMode::Normal;
472 self.set_loading_phase("Scanning input", 10);
473 self.name_what_is_loading(path.clone());
474 Some(AppEvent::Open(vec![path], options))
475 }
476}
477
478fn open_prompt(view: &mut HexView, kind: PromptKind, text: String) {
479 view.prompt = Some(kind);
480 view.prompt_error = None;
481 view.input.set_value(text);
482 view.input.select_all();
483 view.input.set_focused(true);
484}
485
486fn close_prompt(view: &mut HexView) {
487 view.prompt = None;
488 view.prompt_error = None;
489 view.input.set_focused(false);
490}