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