Skip to main content

datui_lib/app/keys/
picker_keys.rs

1//! The three pickers: go to a column, read as a format, and switch to another table
2//! of the same source.
3
4use crate::loading::open_options::OpenOptions;
5use crate::{App, AppEvent, Overlay, app::table_switch};
6use crossterm::event::{KeyCode, KeyEvent};
7
8/// The go-to-column, format and table pickers.
9#[derive(Default)]
10pub struct Pickers {
11    /// The shown columns, narrowed by what is typed, while `g` is choosing one.
12    pub go_to_column: crate::widgets::ui::PickerState,
13    /// The specs `b` offers for the dataset on screen.
14    pub format_picker: crate::widgets::ui::PickerState,
15    /// The tables `T` offers: the picker's lines, and what each opens.
16    pub table_picker: crate::widgets::ui::PickerState,
17    pub table_choices: Option<crate::app::table_switch::Tables>,
18}
19
20impl App {
21    /// `g` at the table: pick a shown column by name, bring it on screen and put the
22    /// cursor on it, starting from the cursor's column.
23    pub(crate) fn open_go_to_column(&mut self) {
24        let Some(state) = self.data_table_state.as_ref() else {
25            return;
26        };
27        let names = state.get_column_order().to_vec();
28        if names.is_empty() {
29            return;
30        }
31        let at = state
32            .current_column()
33            .and_then(|current| names.iter().position(|n| n == current))
34            .unwrap_or(0);
35        self.pickers.go_to_column = crate::widgets::ui::PickerState::new(names);
36        self.pickers.go_to_column.select_original(at);
37        self.open_overlay(Overlay::GoToColumn);
38    }
39
40    /// The column picker owns the keys: type to narrow, ↑↓ move, Enter goes, Esc
41    /// closes without moving.
42    pub(crate) fn go_to_column_key(&mut self, event: &KeyEvent) {
43        match event.code {
44            KeyCode::Esc => self.close_overlay(),
45            KeyCode::Enter => {
46                let Some(index) = self.pickers.go_to_column.selected_original() else {
47                    // Nothing matches; the picker says so and stays.
48                    return;
49                };
50                // By name: the order may have changed under the picker since it opened.
51                let name = self.pickers.go_to_column.items()[index].clone();
52                if let Some(state) = self.data_table_state.as_mut() {
53                    state.go_to_column(&name);
54                }
55                self.close_overlay();
56            }
57            KeyCode::Up => self.pickers.go_to_column.move_up(),
58            KeyCode::Down => self.pickers.go_to_column.move_down(),
59            KeyCode::Backspace => self.pickers.go_to_column.backspace(),
60            KeyCode::Char(c) => self.pickers.go_to_column.filter_key(c, event.modifiers),
61            _ => {}
62        }
63    }
64
65    /// `b` at a table read through a format spec: the specs that could read it, to
66    /// reread with another.
67    pub(crate) fn open_format_picker(&mut self) {
68        let Some(read) = self
69            .data_table_state
70            .as_ref()
71            .and_then(|s| s.format_read())
72            .cloned()
73        else {
74            // Only a file read through a spec has a format to pick.
75            return;
76        };
77        let mut names = vec![read.spec.name.clone()];
78        names.extend(read.also.iter().cloned());
79        for found in &self.formats.specs {
80            if found.spec.layout == read.spec.layout
81                && !found.spec.is_delimited()
82                && !names.contains(&found.spec.name)
83            {
84                names.push(found.spec.name.clone());
85            }
86        }
87        self.pickers.format_picker = crate::widgets::ui::PickerState::new(names);
88        self.open_overlay(Overlay::PickFormat);
89    }
90
91    /// The format picker's keys: type to narrow, ↑↓ move, Enter rereads with the chosen
92    /// spec, Esc closes.
93    pub(crate) fn format_picker_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
94        match event.code {
95            KeyCode::Esc => self.close_overlay(),
96            KeyCode::Enter => {
97                let index = self.pickers.format_picker.selected_original()?;
98                let name = self.pickers.format_picker.items()[index].clone();
99                self.close_overlay();
100                let current = self
101                    .data_table_state
102                    .as_ref()
103                    .and_then(|s| s.format_read())
104                    .map(|read| read.spec.name.clone());
105                if current.as_deref() == Some(name.as_str()) {
106                    return None;
107                }
108                let (paths, options) = self.source.opened.clone()?;
109                let options = OpenOptions {
110                    spec_name: Some(name),
111                    spec_file: None,
112                    spec_fetched: None,
113                    table: None,
114                    format_read: None,
115                    sqlite: None,
116                    format: None,
117                    ..options
118                };
119                self.set_loading_phase("Scanning input", 10);
120                self.name_what_is_loading(paths[0].clone());
121                return Some(AppEvent::Open(paths, options));
122            }
123            KeyCode::Up => self.pickers.format_picker.move_up(),
124            KeyCode::Down => self.pickers.format_picker.move_down(),
125            KeyCode::Backspace => self.pickers.format_picker.backspace(),
126            KeyCode::Char(c) => self.pickers.format_picker.filter_key(c, event.modifiers),
127            _ => {}
128        }
129        None
130    }
131
132    /// The tables of the source on screen, from what its open holds; `None` for a
133    /// source of one.
134    pub fn sibling_tables(&self) -> Option<table_switch::Tables> {
135        let state = self.data_table_state.as_ref()?;
136        let (paths, options) = self.source.opened.as_ref()?;
137        table_switch::of(state, paths, options)
138    }
139
140    /// Whether the source on screen has another table for `T` to open.
141    pub fn offers_other_tables(&self) -> bool {
142        match (self.data_table_state.as_ref(), self.source.opened.as_ref()) {
143            (Some(state), Some((paths, options))) => table_switch::several(state, paths, options),
144            _ => false,
145        }
146    }
147
148    /// `T` at the table: the source's tables, the current one marked. A source of one
149    /// says so.
150    pub(crate) fn open_table_picker(&mut self) {
151        let Some(tables) = self.sibling_tables().filter(table_switch::Tables::several) else {
152            self.flash_note("Only one table here".to_string());
153            return;
154        };
155        let labels = tables.tables.iter().map(|t| t.label.clone()).collect();
156        self.pickers.table_picker = crate::widgets::ui::PickerState::new(labels);
157        if let Some(at) = tables.current {
158            self.pickers.table_picker.select_original(at);
159        }
160        self.pickers.table_choices = Some(tables);
161        self.open_overlay(Overlay::PickTable);
162    }
163
164    /// The table picker owns the keys: type to narrow, ↑↓ move, Enter opens the table
165    /// chosen, Esc closes.
166    pub(crate) fn table_picker_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
167        match event.code {
168            KeyCode::Esc => self.close_overlay(),
169            KeyCode::Enter => {
170                let index = self.pickers.table_picker.selected_original()?;
171                let tables = self.pickers.table_choices.take()?;
172                self.close_overlay();
173                if tables.current == Some(index) {
174                    return None;
175                }
176                let table = tables.tables.get(index)?.table.clone();
177                return self.switch_table(table);
178            }
179            KeyCode::Up => self.pickers.table_picker.move_up(),
180            KeyCode::Down => self.pickers.table_picker.move_down(),
181            KeyCode::Backspace => self.pickers.table_picker.backspace(),
182            KeyCode::Char(c) => self.pickers.table_picker.filter_key(c, event.modifiers),
183            _ => {}
184        }
185        None
186    }
187
188    /// Open `table` of the file on screen in its place (`None`: the whole file), as
189    /// `--table` would: query, filters and sort stay with their table, recents record
190    /// it, and a matching view applies.
191    pub(crate) fn switch_table(&mut self, table: Option<String>) -> Option<AppEvent> {
192        let (paths, options) = self.source.opened.clone()?;
193        let shown = match &table {
194            Some(name) => crate::formats::members::place(&paths[0], name),
195            None => paths[0].clone(),
196        };
197        let options = OpenOptions {
198            table,
199            // `--view` was for the first open; a matching view applies as on any open.
200            view: None,
201            prepared: None,
202            ..options
203        };
204        self.set_loading_phase("Scanning input", 10);
205        self.name_what_is_loading(shown);
206        Some(AppEvent::Open(paths, options))
207    }
208}