Skip to main content

datui_lib/app/
applied.rs

1//! What a dialog or prompt asks of the app once it is answered: one event,
2//! [`AppEvent::Applied`], carries every such result back into the loop, so the frame
3//! that closes the dialog is drawn before the work starts.
4
5use crate::app::jobs::{Answer, Job, Progress};
6use crate::app::modals::filter_modal::FilterStatement;
7use crate::app::modals::pivot_melt_modal::{MeltSpec, PivotSpec};
8use crate::chart::chart_export::ChartExportRequest;
9use crate::config::QueryMode;
10use crate::export::ExportRequest;
11use crate::{App, AppEvent, ExportProgress, Overlay, app};
12use std::path::PathBuf;
13
14/// A dialog's or prompt's answer, applied by [`App::apply`].
15pub enum Applied {
16    /// Jump to row `n`, waiting for line indexing past what is indexed so far.
17    GoToLine(usize),
18    QQuery(String),
19    SqlQuery(String),
20    Filter(Vec<FilterStatement>),
21    /// Columns, and per column whether it runs descending.
22    Sort(Vec<String>, Vec<bool>),
23    /// Column order, and how many columns are locked.
24    ColumnOrder(Vec<String>, usize),
25    /// The sidebar's Apply as one change: column order, locked count, filters, and the
26    /// sort's columns with whether each runs descending.
27    ApplyView(
28        Vec<String>,
29        usize,
30        Vec<FilterStatement>,
31        Vec<String>,
32        Vec<bool>,
33    ),
34    Pivot(PivotSpec),
35    Melt(MeltSpec),
36    /// Write the on-screen Data Quality report from memory; nothing is read.
37    QualityReportExport(
38        PathBuf,
39        crate::analysis::quality_export::ReportFormat,
40        crate::Overwrite,
41    ),
42    /// Show the export's progress, then run it as [`Applied::DoExport`].
43    Export(ExportRequest),
44    /// Run the export once the UI has drawn its progress.
45    DoExport(ExportRequest),
46    /// Collect and format the whole view off-thread for a table-scope copy.
47    CopyTable {
48        format: crate::clipboard::CopyFormat,
49        header: bool,
50    },
51    /// Show the chart export's phase, then run it as [`Applied::DoChartExport`].
52    ChartExport(ChartExportRequest),
53    /// Run the chart export once its phase is drawn.
54    DoChartExport(ChartExportRequest),
55    /// A confirmed documentation link, checked by `app::link_open::checked_url`.
56    OpenLink(String),
57    /// Read the dataset on screen again, keeping its place. See
58    /// [`App::reopen_in_place`].
59    Reopen(Option<Box<crate::loading::open_options::KeptPlace>>),
60}
61
62impl From<Applied> for AppEvent {
63    fn from(applied: Applied) -> Self {
64        AppEvent::Applied(applied)
65    }
66}
67
68impl App {
69    /// Carry out what a dialog or prompt asked for.
70    pub(crate) fn apply(&mut self, applied: Applied) -> Option<AppEvent> {
71        match applied {
72            Applied::GoToLine(n) => {
73                // Past the lines indexed so far: gone to once they all are.
74                if let Some(state) = self.data_table_state.as_ref()
75                    && state.indexing().is_some()
76                    && (n >= state.num_rows()
77                        || state.changes_rows()
78                        || !state.view_sort_columns().is_empty()
79                        || !state.view_sort_ascending())
80                {
81                    self.counting.goto_when_indexed = Some((self.dataset_generation, n));
82                    self.status_message = Some(Self::INDEXING_FOR_ROW.to_string());
83                    self.busy = false;
84                    return None;
85                }
86                self.handle_scroll(|s| s.scroll_to_row_centered(n))
87            }
88            Applied::QQuery(query) => {
89                self.run_query(QueryMode::Q, &query, "Applying query...");
90                None
91            }
92            Applied::SqlQuery(sql) => {
93                self.run_query(QueryMode::Sql, &sql, "Applying SQL query...");
94                None
95            }
96            Applied::Filter(statements) => {
97                if let Some(state) = &mut self.data_table_state {
98                    state.deferred(|s| s.filter(statements.clone()));
99                }
100                self.spawn_async_collect("Filtering...");
101                None
102            }
103            Applied::Sort(columns, descending) => {
104                if let Some(state) = &mut self.data_table_state {
105                    state.deferred(|s| s.sort_by(columns.clone(), descending.clone()));
106                }
107                self.spawn_async_collect("Sorting...");
108                None
109            }
110            Applied::ApplyView(order, locked, filters, columns, descending) => {
111                if let Some(state) = &mut self.data_table_state {
112                    let change = state
113                        .deferred(|s| s.apply_view(order, locked, filters, columns, descending));
114                    self.spawn_async_collect(match change {
115                        crate::table::ViewChange { sort: true, .. } => "Sorting...",
116                        crate::table::ViewChange { filters: true, .. } => "Filtering...",
117                        _ => Self::LOADING_BUFFER,
118                    });
119                }
120                None
121            }
122            Applied::ColumnOrder(order, locked_count) => {
123                if let Some(state) = &mut self.data_table_state {
124                    state.deferred(|s| {
125                        s.set_column_order(order);
126                        s.set_locked_columns(locked_count);
127                    });
128                    self.spawn_async_collect(Self::LOADING_BUFFER);
129                }
130                None
131            }
132            Applied::Pivot(spec) => {
133                // The modal stays up until the result is in, so a failed pivot leaves the spec to
134                // fix.
135                let job = self.data_table_state.as_ref()?.plan_pivot(&spec);
136                self.spawn_job(Job::Pivot, Some(Self::COMPUTING_PIVOT), move |_| {
137                    let pivoted = job
138                        .run()
139                        .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
140                    Ok(Answer::Pivoted { spec, pivoted })
141                });
142                None
143            }
144            Applied::Melt(spec) => {
145                self.busy = true;
146                if let Some(state) = &mut self.data_table_state {
147                    let result = state.deferred(|s| s.melt(&spec));
148                    match result {
149                        Ok(()) => {
150                            self.close_overlay();
151                            self.spawn_async_collect("Computing melt...");
152                            None
153                        }
154                        Err(e) => {
155                            self.busy = false;
156                            self.error_modal
157                                .show(crate::error_display::user_message_from_report(&e, None));
158                            None
159                        }
160                    }
161                } else {
162                    self.busy = false;
163                    None
164                }
165            }
166            Applied::QualityReportExport(path, format, overwrite) => {
167                // The report and the plan it was measured with, cloned to the writer: built from
168                // memory, nothing read.
169                let results = self.analysis_modal.quality.results.clone()?;
170                let plan = self.analysis_modal.quality_result_plan().clone();
171                self.spawn_job(
172                    Job::QualityReport,
173                    Some("Writing the report..."),
174                    move |_| {
175                        crate::analysis::quality_export::write(
176                            &path, &results, &plan, format, overwrite,
177                        )
178                        .map_err(|error| Self::format_export_error(&error))?;
179                        Ok(Answer::QualityReportWritten(path))
180                    },
181                );
182                None
183            }
184            Applied::Export(request) => {
185                if self.data_table_state.is_some() {
186                    self.busy = true;
187                    self.export_progress =
188                        Some(ExportProgress::new(&request.path, "Preparing export"));
189                    // Drawn before the export starts.
190                    Some(Applied::DoExport(request).into())
191                } else {
192                    None
193                }
194            }
195            Applied::DoExport(request) => {
196                let Some(state) = &self.data_table_state else {
197                    self.export_progress = None;
198                    self.busy = false;
199                    return None;
200                };
201                // Cloned, not taken: a failed write reopens the dialog on the same counts.
202                let frame = match self.export_modal.counts.clone() {
203                    Some(counts) => {
204                        crate::table::ExportFrame::of(polars::prelude::IntoLazy::lazy(counts))
205                    }
206                    None => state.export_frame(request.options.source_file),
207                };
208                let streaming = state.polars_streaming();
209                // One job from plan to commit: it holds the generation throughout, and any rows
210                // it collects die with it.
211                let phase = match request.route(streaming) {
212                    crate::export::Route::Streamed => Self::export_write_phase(&request),
213                    crate::export::Route::Collected => "Collecting data",
214                };
215                self.export_progress = Some(ExportProgress::new(&request.path, phase));
216                let writing = Self::export_write_phase(&request);
217                self.spawn_job(Job::Export, Some("Exporting..."), move |worker| {
218                    let report = worker.reporter();
219                    let written = move |bytes| {
220                        report(Progress::ExportWriting {
221                            phase: writing,
222                            bytes,
223                        })
224                    };
225                    frame
226                        .into_lazy()
227                        .map_err(color_eyre::eyre::Report::from)
228                        .and_then(|lf| crate::export::run(lf, &request, streaming, written))
229                        .map_err(|e| Self::format_export_error(&e))?;
230                    // Success is reported only once the file is committed.
231                    Ok(Answer::Exported(request.path))
232                });
233                None
234            }
235            Applied::Reopen(asked) => self.reopen_in_place(asked),
236            Applied::OpenLink(url) => {
237                // Not waited on; a browser that will not start gets a flash, not an error.
238                if app::link_open::open(&url).is_err() {
239                    self.flash_note("Couldn't open the link; y copies it".to_string());
240                }
241                None
242            }
243            Applied::CopyTable { format, header } => {
244                let accepts = match self.copy_destination() {
245                    Ok(destination) => destination.accepts(),
246                    Err(e) => {
247                        self.busy = false;
248                        self.error_modal.show(e);
249                        return None;
250                    }
251                };
252                if let Some(state) = &self.data_table_state {
253                    let lf = state.visible_lf();
254                    let streaming = state.polars_streaming();
255                    self.spawn_job(Job::Copy, Some("Collecting data for copy..."), move |_| {
256                        // A capped destination's copy is read in batches and abandoned at the cap; others
257                        // are collected whole.
258                        let (payload, rows) = match accepts.base64_limit {
259                            Some(limit) => crate::clipboard::bounded_table_text(
260                                lf, format, header, limit,
261                            )
262                            .map(|(text, rows)| (crate::clipboard::Payload::text(text), rows)),
263                            None => crate::analysis::statistics::collect_lazy(lf, streaming)
264                                .map_err(|e| crate::error_display::user_message_from_polars(&e))
265                                .and_then(|df| {
266                                    crate::clipboard::tabular_payload(
267                                        &df,
268                                        format,
269                                        header,
270                                        accepts.html,
271                                    )
272                                    .map(|payload| (payload, df.height()))
273                                }),
274                        }
275                        .map_err(|message| format!("Copy failed: {message}"))?;
276                        // Handed on whole, never copied.
277                        Ok(Answer::Copied {
278                            payload,
279                            message: format!(
280                                "Copied {} rows as {}",
281                                app::modals::copy_modal::thousands(rows),
282                                format.as_str()
283                            ),
284                        })
285                    });
286                } else {
287                    self.busy = false;
288                }
289                None
290            }
291            Applied::ChartExport(request) => {
292                self.busy = true;
293                self.export_progress = Some(ExportProgress::new(&request.path, "Exporting chart"));
294                Some(Applied::DoChartExport(request).into())
295            }
296            Applied::DoChartExport(request) => {
297                // `ChartExport` arms `busy` and defers here to draw its phase; a Ctrl-O meanwhile left
298                // the chart, so release rather than park an export nothing will prepare.
299                if !self.overlay.shows(&Overlay::Chart) {
300                    self.export_progress = None;
301                    self.status_message = None;
302                    self.busy = false;
303                    return None;
304                }
305                self.start_chart_export(request);
306                None
307            }
308        }
309    }
310}