Skip to main content

datui_lib/app/keys/
pivot_melt_keys.rs

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