Skip to main content

datui_lib/app/keys/
hex_keys.rs

1//! The hex view's App side: opening a file in it and its keys; its find is
2//! [`crate::app::keys::hex_find`] and the view itself [`crate::app::hex_view`].
3
4use 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/// The hex view, and the number its next read is tagged with.
11#[derive(Default)]
12pub struct HexState {
13    /// The hex view (`InputMode::Hex`), kept while it is up.
14    pub view: Option<crate::app::hex_view::HexView>,
15    /// Bumped per hex view opened, so a find's answer for another is dropped.
16    pub(crate) serial: u64,
17}
18
19impl HexState {
20    /// The hex view is closed; the serial goes on counting.
21    pub(crate) fn close(&mut self) {
22        self.view = None;
23    }
24}
25
26impl App {
27    /// Whether any format spec is on the search path: `B` has something to offer.
28    pub(crate) fn has_format_specs(&self) -> bool {
29        !self.formats.is_empty()
30    }
31
32    /// The hex view on screen, if it is.
33    pub fn hex_view(&self) -> Option<&HexView> {
34        self.hex
35            .view
36            .as_ref()
37            .filter(|_| self.overlay == Overlay::Hex)
38    }
39
40    /// The one local file the dataset was read from, which Info's `x` shows as hex: not
41    /// a glob, remote source, stdin, or several paths.
42    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    /// Show `path` in the hex view once a worker maps it. `fallback`: no reader or spec
58    /// took it.
59    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    /// The file is mapped: the view opens on it.
80    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    /// An open found a local file nothing reads, or was asked for its bytes.
96    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    /// Leave the hex view the way it was entered: home, the table, or (from the command
116    /// line) out of datui.
117    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                // Back to the panel it was opened from, on the tab it was on.
137                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    /// What `q` says it does in the hex view.
148    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    /// Keys in the hex view.
157    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        // The spec picker owns the keys while it is open.
165        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                // No field to move on to.
177                PickerKey::ChooseAndMove(_)
178                | PickerKey::Toggle
179                | PickerKey::Handled
180                | PickerKey::Other => {}
181            }
182            return None;
183        }
184
185        // So does the prompt.
186        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    /// Enter in a prompt: go to the offset, set the bytes per row, or find.
280    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    /// `B`: every spec on the search path, to read the file with.
325    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    /// Read the hex view's file with the spec `name`.
345    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        // The table takes the screen; a read that fails says why over it.
369        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}