Skip to main content

datui_lib/inspector/
inspector_keys.rs

1//! The row inspector: opening and closing it, its keys, and drilling into nested
2//! values.
3
4use crate::app::form::ListMove;
5use crate::{
6    App, AppEvent, Overlay, app::modals::copy_modal, inspector::inspector_drill,
7    inspector::inspector_modal, inspector::inspector_reader,
8};
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11impl App {
12    /// Whether Enter at the table opens the inspector, as Space does: a table whose rows
13    /// do not drill into groups (a `by` view, a SQL GROUP BY), and not inside a
14    /// drill-down.
15    pub fn enter_inspects(&self) -> bool {
16        self.at_table()
17            && self
18                .data_table_state
19                .as_ref()
20                .is_some_and(|state| !state.can_drill_down())
21    }
22
23    /// Space at the table, and Enter where there is nothing to drill into: the
24    /// inspector over the selected row.
25    pub(crate) fn open_inspector(&mut self) {
26        let Some(state) = self.data_table_state.as_ref() else {
27            return;
28        };
29        if state.inspect_row().is_none() {
30            self.flash_note("No row to inspect".to_string());
31            return;
32        }
33        self.inspector_modal
34            .open(state.inspect_fields(), state.current_column());
35        self.open_overlay(Overlay::Inspect);
36    }
37
38    pub(super) fn close_inspector(&mut self) {
39        self.close_overlay();
40    }
41
42    /// Rebuild the inspector's list for the row shown: Filled, Compare and the find
43    /// depend on its values.
44    fn refresh_inspector_list(&mut self) {
45        if let Some(state) = self.data_table_state.as_ref() {
46            let row = state.inspect_row();
47            crate::widgets::inspector::refresh_list(&mut self.inspector_modal, state, row.as_ref());
48        }
49    }
50
51    /// The focused value's pane: as last drawn if still focused, else built for the key
52    /// (without the table's preview, which only a frame knows).
53    pub(super) fn inspector_pane(&self) -> Option<crate::widgets::inspector::Pane> {
54        let modal = &self.inspector_modal;
55        if modal.drill.is_some() {
56            return modal.pane.as_ref().map(|(_, pane)| pane.clone());
57        }
58        let state = self.data_table_state.as_ref()?;
59        let row = state.inspect_row()?;
60        let field = modal.focused()?;
61        if let Some(pane) = modal.pane_for(row.frame, row.row, &field.name) {
62            return Some(pane.clone());
63        }
64        let shown = crate::widgets::inspector::shown(field, &row, modal.read.as_ref(), state);
65        Some(crate::widgets::inspector::pane(
66            &field.dtype,
67            &shown,
68            &crate::widgets::inspector::PaneAsk {
69                choice: modal.view,
70                width: modal
71                    .pane
72                    .as_ref()
73                    .map_or(80, |(key, _)| key.width as usize),
74                table: None,
75                indented: crate::widgets::inspector::Indented::None,
76                not_json: modal.known_not_json(row.frame, row.row, &field.name),
77                unpacked: modal.unpacked(&(row.frame, row.row, field.name.clone())),
78                read_key: "Enter",
79            },
80        ))
81    }
82
83    /// The inspector's keys. Row moves move the table's cursor, so the table is where
84    /// the inspector left it.
85    pub(crate) fn inspector_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
86        if !event.is_press() {
87            return None;
88        }
89        let modal = &mut self.inspector_modal;
90        if modal.finding {
91            match event.code {
92                KeyCode::Esc => modal.clear_find(),
93                KeyCode::Enter | KeyCode::Tab | KeyCode::Down => modal.finding = false,
94                KeyCode::Up => {
95                    modal.finding = false;
96                    self.refresh_inspector_list();
97                    self.inspector_modal.move_field(ListMove::Up);
98                    return None;
99                }
100                KeyCode::Backspace => modal.find_backspace(),
101                KeyCode::Char(c) => modal.find_key(c, event.modifiers),
102                _ => {}
103            }
104            // Refocus now, not at the next frame: a replayed key must act on the field the find
105            // left focused.
106            self.refresh_inspector_list();
107            return None;
108        }
109        if modal.value_find.as_ref().is_some_and(|f| f.editing) {
110            self.value_find_key(event);
111            return None;
112        }
113        if event
114            .modifiers
115            .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
116        {
117            return None;
118        }
119        if modal.focus == inspector_modal::Focus::Value {
120            return self.inspector_value_key(event);
121        }
122        if modal.drill.is_some() {
123            return self.drill_key(event);
124        }
125        self.refresh_inspector_list();
126        let modal = &mut self.inspector_modal;
127        if let Some(step) = ListMove::from_key(event) {
128            modal.move_field(step);
129            return None;
130        }
131        match event.code {
132            // Esc backs out one level: the find, then Compare, then the inspector.
133            KeyCode::Esc if !modal.filter.is_empty() => modal.clear_find(),
134            KeyCode::Esc if modal.compare => {
135                modal.compare = false;
136                modal.filled_only = false;
137            }
138            KeyCode::Esc | KeyCode::Char(' ') => self.close_inspector(),
139            // Two panes: Tab and Shift+Tab both cross to the value.
140            KeyCode::Tab | KeyCode::BackTab => {
141                if modal.focused().is_some() {
142                    modal.focus = inspector_modal::Focus::Value;
143                }
144            }
145            KeyCode::Char('/') => modal.finding = true,
146            KeyCode::Char('e') => self.inspector_view(),
147            KeyCode::Char('w') => self.inspector_wrap(),
148            KeyCode::Char('f') => {
149                modal.filled_only = !modal.filled_only;
150                modal.list_offset = 0;
151            }
152            KeyCode::Char('s') => {
153                modal.order = modal.order.next();
154                modal.list_offset = 0;
155            }
156            KeyCode::Char('c') => {
157                modal.compare = !modal.compare;
158                if !modal.compare {
159                    modal.filled_only = false;
160                }
161            }
162            KeyCode::Char('m') => self.toggle_inspector_pin(),
163            KeyCode::Right | KeyCode::Char('l') => return self.scroll_key(crate::Scroll::Next),
164            KeyCode::Left | KeyCode::Char('h') => return self.scroll_key(crate::Scroll::Prev),
165            KeyCode::Char('y') => self.copy_inspected_field(),
166            KeyCode::Char('Y') => self.copy_inspected_row(),
167            KeyCode::Char('o') => self.open_inspected_value(),
168            KeyCode::Char('r') => self.read_focused_field(),
169            KeyCode::Enter => return self.inspector_enter(),
170            _ => {}
171        }
172        None
173    }
174
175    /// Keys inside a drilled level: the row's moves, but `→` and Enter open the focused
176    /// item and `←` and Esc step up.
177    fn drill_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
178        let modal = &mut self.inspector_modal;
179        if let Some(step) = ListMove::from_key(event) {
180            modal.move_field(step);
181            return None;
182        }
183        match event.code {
184            KeyCode::Esc | KeyCode::Left | KeyCode::Char('h') => {
185                modal.drill_out();
186            }
187            KeyCode::Char(' ') => self.close_inspector(),
188            // A level with nothing in it has no value to cross to.
189            KeyCode::Tab | KeyCode::BackTab
190                if modal
191                    .drill
192                    .as_ref()
193                    .is_some_and(|drill| drill.level().focused().is_some()) =>
194            {
195                modal.focus = inspector_modal::Focus::Value;
196            }
197            KeyCode::Char('e') => self.inspector_view(),
198            KeyCode::Char('w') => self.inspector_wrap(),
199            KeyCode::Char('y') => self.copy_drilled_item(),
200            KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => {
201                let drill = modal.drill.as_ref()?;
202                let (frame, row) = (drill.frame, drill.row);
203                let (label, node) = drill.level().focused()?;
204                let path = drill.item_key(&label);
205                if node.opens() && !modal.known_not_json(frame, row, &path) {
206                    self.inspector_open(frame, row, label, path, node);
207                }
208            }
209            _ => {}
210        }
211        None
212    }
213
214    /// `e` in the inspector: the focused value's next view, if it has several. A number
215    /// has one, so a text field's view is unchanged.
216    pub(super) fn inspector_view(&mut self) {
217        if let Some(view) = self.inspector_pane().and_then(|pane| pane.next_view()) {
218            self.inspector_modal.choose_view(view);
219        }
220    }
221
222    /// `w`: word wrap or hard wrap, for every value until it is pressed again.
223    pub(super) fn inspector_wrap(&mut self) {
224        let modal = &mut self.inspector_modal;
225        modal.wrap = match modal.wrap {
226            inspector_reader::Wrap::Word => inspector_reader::Wrap::Hard,
227            inspector_reader::Wrap::Hard => inspector_reader::Wrap::Word,
228        };
229    }
230
231    /// `m`: pin this row for Compare, or let the pin go when it is this row.
232    fn toggle_inspector_pin(&mut self) {
233        let Some(row) = self.data_table_state.as_ref().and_then(|s| s.inspect_row()) else {
234            return;
235        };
236        let modal = &mut self.inspector_modal;
237        let here = modal
238            .pinned
239            .as_ref()
240            .is_some_and(|p| (p.frame, p.row) == (row.frame, row.row));
241        if here {
242            modal.pinned = None;
243            self.flash_note("Unpinned".to_string());
244        } else {
245            let n = row.display_row;
246            modal.pinned = Some(row);
247            modal.compare = true;
248            self.flash_note(format!(
249                "Pinned row {}; Compare shows it",
250                copy_modal::thousands(n)
251            ));
252        }
253    }
254
255    /// Enter in the inspector: a group row's rows, as at the table; else open a nested
256    /// value, or read fields the buffer lacks.
257    fn inspector_enter(&mut self) -> Option<AppEvent> {
258        let state = self.data_table_state.as_ref()?;
259        if state.can_drill_down() {
260            self.close_inspector();
261            self.drill_selected_row();
262            return None;
263        }
264        let row = state.inspect_row()?;
265        let field = self.inspector_modal.focused().cloned()?;
266        let shown = crate::widgets::inspector::shown(
267            &field,
268            &row,
269            self.inspector_modal.read.as_ref(),
270            state,
271        );
272        use crate::widgets::inspector::Shown;
273        match shown {
274            // A failed read is asked again: the pane said why, and Enter is the retry.
275            Shown::Unread | Shown::Failed(_) => self.read_focused_field(),
276            Shown::Value(ref v)
277                if crate::widgets::inspector::value_opens(v)
278                    && !self
279                        .inspector_modal
280                        .known_not_json(row.frame, row.row, &field.name) =>
281            {
282                let column = if field.buffered() {
283                    row.values.column(&field.name).ok()
284                } else {
285                    self.inspector_modal
286                        .read_values(row.frame, row.row)
287                        .and_then(|values| values.column(&field.name).ok())
288                };
289                if let Some(column) = column {
290                    let node =
291                        inspector_drill::Node::Native(column.as_materialized_series().clone());
292                    let path = inspector_drill::path_key([field.name.as_str()]);
293                    self.inspector_open(row.frame, row.row, field.name.clone(), path, node);
294                }
295            }
296            _ => {}
297        }
298        None
299    }
300}