Skip to main content

datui_lib/
pivot_melt_keys.rs

1//! The Pivot & Melt builder's keys: the shared form keys (`crate::form`), then what
2//! each row does with them; and its live preview, rerun as the spec changes.
3
4use crate::form::{FormKey, PickerKey};
5use crate::jobs::{Answer, Job};
6use crate::pivot_melt_modal::{
7    PREVIEW_INPUT_ROWS, PivotMeltFocus, PivotMeltTab, PreviewFrame, PreviewInput,
8};
9use crate::{App, AppEvent, InputMode};
10use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
11
12impl App {
13    /// Open the builder over the view's columns, and preview what it stages.
14    pub(crate) fn open_pivot_builder(&mut self) {
15        let Some(state) = &self.data_table_state else {
16            return;
17        };
18        let modal = &mut self.pivot_melt_modal;
19        modal.available_columns = state.schema().iter_names().map(|s| s.to_string()).collect();
20        modal.column_dtypes = state
21            .schema()
22            .iter()
23            .map(|(n, d)| (n.to_string(), d.clone()))
24            .collect();
25        let view_rows = state.num_rows_if_valid();
26        let sorted = state.is_sorted();
27        modal.open(self.history_limit, &self.theme);
28        modal.preview.view_rows = view_rows;
29        modal.preview.sorted = sorted;
30        self.input_mode = InputMode::PivotMelt;
31        self.request_reshape_preview();
32    }
33
34    /// Keys in the Pivot & Melt builder. Whatever the key changed, the preview
35    /// follows the spec it leaves staged.
36    pub(crate) fn pivot_melt_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
37        let out = self.pivot_melt_form_key(event);
38        if self.pivot_melt_modal.active {
39            self.request_reshape_preview();
40        }
41        out
42    }
43
44    /// Ask for a preview of the staged spec, unless it is the one already asked for.
45    /// One worker runs at a time: an edit made while it runs is previewed when it
46    /// ends.
47    pub fn request_reshape_preview(&mut self) {
48        let modal = &mut self.pivot_melt_modal;
49        let wanted = modal.staged_spec();
50        let preview = &mut modal.preview;
51        if wanted == preview.wanted {
52            return;
53        }
54        preview.token = preview.token.wrapping_add(1);
55        preview.wanted = wanted;
56        if preview.running.is_none() {
57            self.spawn_reshape_preview();
58        }
59    }
60
61    /// Run the preview of the staged spec over the view's head, reading the head
62    /// first if this opening of the builder has not yet.
63    fn spawn_reshape_preview(&mut self) {
64        let preview = &self.pivot_melt_modal.preview;
65        let Some(spec) = preview.wanted.clone() else {
66            return;
67        };
68        let (epoch, token) = (preview.epoch, preview.token);
69        let input = preview.input.clone();
70        let read = match &input {
71            Some(_) => None,
72            None => {
73                let Some(state) = self.data_table_state.as_ref() else {
74                    // Nothing to read: say so rather than wait on an answer.
75                    self.pivot_melt_modal.preview.shown =
76                        Some((spec, Err("Nothing to preview".to_string())));
77                    return;
78                };
79                // A small view is read in its order: sorting it costs nothing, and
80                // the preview's rows are then the ones Enter makes.
81                let small = state
82                    .num_rows_if_valid()
83                    .is_some_and(|rows| rows <= PREVIEW_INPUT_ROWS);
84                let sorted = (state.is_sorted() && !small).then(|| state.visible_lf());
85                let lf = if small {
86                    state.visible_lf()
87                } else {
88                    state.preview_lf()
89                };
90                Some((lf, sorted, state.polars_streaming()))
91            }
92        };
93        self.pivot_melt_modal.preview.running = Some(token);
94        self.spawn_job(Job::ReshapePreview { epoch, token }, None, move |_| {
95            let (input, read) = match (input, read) {
96                (Some(input), _) => (input, None),
97                (None, Some((lf, sorted, streaming))) => {
98                    let message = |e: polars::prelude::PolarsError| {
99                        crate::error_display::user_message_from_polars(&e)
100                    };
101                    // One more than the preview takes says whether there is more.
102                    let head_of = |lf: polars::prelude::LazyFrame| {
103                        crate::statistics::collect_lazy(
104                            lf.slice(0, PREVIEW_INPUT_ROWS as u32 + 1),
105                            streaming,
106                        )
107                    };
108                    let mut head = head_of(lf).map_err(message)?;
109                    // The unsorted head turned out to be the whole view: small enough
110                    // to read again in the view's order.
111                    if head.height() <= PREVIEW_INPUT_ROWS
112                        && let Some(sorted) = sorted
113                    {
114                        head = head_of(sorted).map_err(message)?;
115                    }
116                    let input = PreviewInput {
117                        whole: head.height() <= PREVIEW_INPUT_ROWS,
118                        rows: std::sync::Arc::new(head.head(Some(PREVIEW_INPUT_ROWS))),
119                    };
120                    (input.clone(), Some(input))
121                }
122                (None, None) => return Err("Nothing to preview".to_string()),
123            };
124            let result = crate::pivot_melt_modal::run_preview(&input.rows, &spec);
125            Ok(Answer::ReshapePreviewed {
126                input: read,
127                result,
128            })
129        });
130    }
131
132    /// A preview's worker ended, with the head it read and its result, or `Err` with
133    /// why the head could not be read. An answer for an earlier opening is dropped.
134    pub(crate) fn reshape_preview_ended(
135        &mut self,
136        epoch: u64,
137        token: u64,
138        input: Option<PreviewInput>,
139        result: Result<PreviewFrame, String>,
140    ) {
141        let preview = &mut self.pivot_melt_modal.preview;
142        if !self.pivot_melt_modal.active || preview.epoch != epoch {
143            return;
144        }
145        if preview.running == Some(token) {
146            preview.running = None;
147        }
148        if preview.input.is_none() {
149            preview.input = input;
150        }
151        if token == preview.token {
152            if let Some(spec) = preview.wanted.clone() {
153                preview.shown = Some((spec, result));
154            }
155        } else if preview.running.is_none() && preview.stale() {
156            // The spec changed while this one ran: preview what is staged now.
157            self.spawn_reshape_preview();
158        }
159    }
160
161    /// Whether the builder's preview has an answer still to come.
162    pub fn reshape_preview_pending(&self) -> bool {
163        self.pivot_melt_modal.active && self.pivot_melt_modal.preview.running.is_some()
164    }
165
166    /// Keys in the builder's form.
167    fn pivot_melt_form_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
168        // Acts at once (see `hard_escape_while_busy`), ahead of the keys held
169        // behind the pivot; a second Esc closes the form.
170        if event.code == KeyCode::Esc && self.pivot_computing() {
171            self.cancel_pivot();
172            return None;
173        }
174        if event.code == KeyCode::Char('?') && event.modifiers.contains(KeyModifiers::CONTROL) {
175            self.open_help_overlay();
176            return None;
177        }
178
179        let multi = self
180            .pivot_melt_modal
181            .is_multi_row(self.pivot_melt_modal.focus);
182        if let Some(picker) = self.pivot_melt_modal.picker.as_mut() {
183            match crate::form::picker_key(picker, multi, event) {
184                PickerKey::Close => self.pivot_melt_modal.picker_cancel(),
185                PickerKey::Choose => self.pivot_melt_modal.picker_choose(),
186                PickerKey::Toggle => self.pivot_melt_modal.picker_toggle(),
187                PickerKey::ChooseAndMove(forward) => {
188                    self.pivot_melt_modal.picker_choose();
189                    crate::form::Form::move_focus(
190                        &mut self.pivot_melt_modal,
191                        if forward { 1 } else { -1 },
192                    );
193                }
194                PickerKey::Handled | PickerKey::Other => {}
195            }
196            return None;
197        }
198
199        // Whatever this key does, the form is being edited again: the
200        // re-accented gap line goes back to plain (Enter below re-arms it).
201        self.pivot_melt_modal.attention = false;
202
203        match crate::form::key(&mut self.pivot_melt_modal, event) {
204            FormKey::Cancel => {
205                self.pivot_melt_modal.close();
206                self.input_mode = InputMode::Normal;
207            }
208            // Enter applies from anywhere in the form; what it will do has
209            // been echoed on the spec line all along.
210            FormKey::Submit => return self.submit_pivot_melt(),
211            FormKey::Step(PivotMeltFocus::TabBar, _) => self.pivot_melt_modal.switch_tab(),
212            FormKey::Step(row, delta) => {
213                if self.pivot_melt_modal.is_choice_row(row) {
214                    self.pivot_melt_modal.step_choice(delta);
215                } else {
216                    self.pivot_melt_modal.step_picker_row(delta);
217                }
218            }
219            // A column row edits through the Picker scoped to that row alone.
220            FormKey::Act(_) => self.pivot_melt_modal.open_picker(),
221            // A text row is an ordinary text field, readline included.
222            FormKey::Text(_) => {
223                if let Some(input) = self.pivot_melt_modal.focused_text_input_mut() {
224                    let _ = input.handle_key(event, None);
225                }
226            }
227            FormKey::Other if event.code == KeyCode::Char('?') => self.open_help_overlay(),
228            FormKey::Moved | FormKey::Other => {}
229        }
230        None
231    }
232
233    /// Enter: run the staged pivot or melt, or re-accent the spec line that names
234    /// what is missing rather than a modal repeating it.
235    fn submit_pivot_melt(&mut self) -> Option<AppEvent> {
236        let modal = &mut self.pivot_melt_modal;
237        match modal.active_tab {
238            PivotMeltTab::Pivot => {
239                if modal.pivot_validation_error().is_some() {
240                    modal.attention = true;
241                    None
242                } else {
243                    modal.build_pivot_spec().map(AppEvent::Pivot)
244                }
245            }
246            PivotMeltTab::Melt => {
247                if modal.melt_validation_error().is_some() {
248                    modal.attention = true;
249                    None
250                } else {
251                    modal.build_melt_spec().map(AppEvent::Melt)
252                }
253            }
254        }
255    }
256}