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}
58
59impl From<Applied> for AppEvent {
60    fn from(applied: Applied) -> Self {
61        AppEvent::Applied(applied)
62    }
63}
64
65impl App {
66    /// Carry out what a dialog or prompt asked for.
67    pub(crate) fn apply(&mut self, applied: Applied) -> Option<AppEvent> {
68        match applied {
69            Applied::GoToLine(n) => {
70                // Past the lines indexed so far: gone to once they all are.
71                if let Some(state) = self.data_table_state.as_ref()
72                    && state.indexing().is_some()
73                    && (n >= state.num_rows()
74                        || state.changes_rows()
75                        || !state.view_sort_columns().is_empty()
76                        || !state.view_sort_ascending())
77                {
78                    self.counting.goto_when_indexed = Some((self.dataset_generation, n));
79                    self.status_message = Some(Self::INDEXING_FOR_ROW.to_string());
80                    self.busy = false;
81                    return None;
82                }
83                self.handle_scroll(|s| s.scroll_to_row_centered(n))
84            }
85            Applied::QQuery(query) => {
86                self.run_query(QueryMode::Q, &query, "Applying query...");
87                None
88            }
89            Applied::SqlQuery(sql) => {
90                self.run_query(QueryMode::Sql, &sql, "Applying SQL query...");
91                None
92            }
93            Applied::Filter(statements) => {
94                if let Some(state) = &mut self.data_table_state {
95                    state.deferred(|s| s.filter(statements.clone()));
96                }
97                self.spawn_async_collect("Filtering...");
98                None
99            }
100            Applied::Sort(columns, descending) => {
101                if let Some(state) = &mut self.data_table_state {
102                    state.deferred(|s| s.sort_by(columns.clone(), descending.clone()));
103                }
104                self.spawn_async_collect("Sorting...");
105                None
106            }
107            Applied::ApplyView(order, locked, filters, columns, descending) => {
108                if let Some(state) = &mut self.data_table_state {
109                    let change = state
110                        .deferred(|s| s.apply_view(order, locked, filters, columns, descending));
111                    self.spawn_async_collect(match change {
112                        crate::table::ViewChange { sort: true, .. } => "Sorting...",
113                        crate::table::ViewChange { filters: true, .. } => "Filtering...",
114                        _ => Self::LOADING_BUFFER,
115                    });
116                }
117                None
118            }
119            Applied::ColumnOrder(order, locked_count) => {
120                if let Some(state) = &mut self.data_table_state {
121                    state.deferred(|s| {
122                        s.set_column_order(order);
123                        s.set_locked_columns(locked_count);
124                    });
125                    self.spawn_async_collect(Self::LOADING_BUFFER);
126                }
127                None
128            }
129            Applied::Pivot(spec) => {
130                // The modal stays up until the result is in, so a failed pivot leaves the spec to
131                // fix.
132                let job = self.data_table_state.as_ref()?.plan_pivot(&spec);
133                self.spawn_job(Job::Pivot, Some(Self::COMPUTING_PIVOT), move |_| {
134                    let pivoted = job
135                        .run()
136                        .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
137                    Ok(Answer::Pivoted { spec, pivoted })
138                });
139                None
140            }
141            Applied::Melt(spec) => {
142                self.busy = true;
143                if let Some(state) = &mut self.data_table_state {
144                    let result = state.deferred(|s| s.melt(&spec));
145                    match result {
146                        Ok(()) => {
147                            self.close_overlay();
148                            self.spawn_async_collect("Computing melt...");
149                            None
150                        }
151                        Err(e) => {
152                            self.busy = false;
153                            self.error_modal
154                                .show(crate::error_display::user_message_from_report(&e, None));
155                            None
156                        }
157                    }
158                } else {
159                    self.busy = false;
160                    None
161                }
162            }
163            Applied::QualityReportExport(path, format, overwrite) => {
164                // The report and the plan it was measured with, cloned to the writer: built from
165                // memory, nothing read.
166                let results = self.analysis_modal.quality.results.clone()?;
167                let plan = self.analysis_modal.quality_result_plan().clone();
168                self.spawn_job(
169                    Job::QualityReport,
170                    Some("Writing the report..."),
171                    move |_| {
172                        crate::analysis::quality_export::write(
173                            &path, &results, &plan, format, overwrite,
174                        )
175                        .map_err(|error| Self::format_export_error(&error))?;
176                        Ok(Answer::QualityReportWritten(path))
177                    },
178                );
179                None
180            }
181            Applied::Export(request) => {
182                if self.data_table_state.is_some() {
183                    self.busy = true;
184                    self.export_progress =
185                        Some(ExportProgress::new(&request.path, "Preparing export"));
186                    // Drawn before the export starts.
187                    Some(Applied::DoExport(request).into())
188                } else {
189                    None
190                }
191            }
192            Applied::DoExport(request) => {
193                let Some(state) = &self.data_table_state else {
194                    self.export_progress = None;
195                    self.busy = false;
196                    return None;
197                };
198                // Cloned, not taken: a failed write reopens the dialog on the same counts.
199                let frame = match self.export_modal.counts.clone() {
200                    Some(counts) => {
201                        crate::table::ExportFrame::of(polars::prelude::IntoLazy::lazy(counts))
202                    }
203                    None => state.export_frame(request.options.source_file),
204                };
205                let streaming = state.polars_streaming();
206                // One job from plan to commit: it holds the generation throughout, and any rows
207                // it collects die with it.
208                let phase = match request.route(streaming) {
209                    crate::export::Route::Streamed => Self::export_write_phase(&request),
210                    crate::export::Route::Collected => "Collecting data",
211                };
212                self.export_progress = Some(ExportProgress::new(&request.path, phase));
213                let writing = Self::export_write_phase(&request);
214                self.spawn_job(Job::Export, Some("Exporting..."), move |worker| {
215                    let report = worker.reporter();
216                    let written = move |bytes| {
217                        report(Progress::ExportWriting {
218                            phase: writing,
219                            bytes,
220                        })
221                    };
222                    frame
223                        .into_lazy()
224                        .map_err(color_eyre::eyre::Report::from)
225                        .and_then(|lf| crate::export::run(lf, &request, streaming, written))
226                        .map_err(|e| Self::format_export_error(&e))?;
227                    // Success is reported only once the file is committed.
228                    Ok(Answer::Exported(request.path))
229                });
230                None
231            }
232            Applied::OpenLink(url) => {
233                // Not waited on; a browser that will not start gets a flash, not an error.
234                if app::link_open::open(&url).is_err() {
235                    self.flash_note("Couldn't open the link; y copies it".to_string());
236                }
237                None
238            }
239            Applied::CopyTable { format, header } => {
240                let accepts = match self.copy_destination() {
241                    Ok(destination) => destination.accepts(),
242                    Err(e) => {
243                        self.busy = false;
244                        self.error_modal.show(e);
245                        return None;
246                    }
247                };
248                if let Some(state) = &self.data_table_state {
249                    let lf = state.visible_lf();
250                    let streaming = state.polars_streaming();
251                    self.spawn_job(Job::Copy, Some("Collecting data for copy..."), move |_| {
252                        // A capped destination's copy is read in batches and abandoned at the cap; others
253                        // are collected whole.
254                        let (payload, rows) = match accepts.base64_limit {
255                            Some(limit) => crate::clipboard::bounded_table_text(
256                                lf, format, header, limit,
257                            )
258                            .map(|(text, rows)| (crate::clipboard::Payload::text(text), rows)),
259                            None => crate::analysis::statistics::collect_lazy(lf, streaming)
260                                .map_err(|e| crate::error_display::user_message_from_polars(&e))
261                                .and_then(|df| {
262                                    crate::clipboard::tabular_payload(
263                                        &df,
264                                        format,
265                                        header,
266                                        accepts.html,
267                                    )
268                                    .map(|payload| (payload, df.height()))
269                                }),
270                        }
271                        .map_err(|message| format!("Copy failed: {message}"))?;
272                        // Handed on whole, never copied.
273                        Ok(Answer::Copied {
274                            payload,
275                            message: format!(
276                                "Copied {} rows as {}",
277                                app::modals::copy_modal::thousands(rows),
278                                format.as_str()
279                            ),
280                        })
281                    });
282                } else {
283                    self.busy = false;
284                }
285                None
286            }
287            Applied::ChartExport(request) => {
288                self.busy = true;
289                self.export_progress = Some(ExportProgress::new(&request.path, "Exporting chart"));
290                Some(Applied::DoChartExport(request).into())
291            }
292            Applied::DoChartExport(request) => {
293                // `ChartExport` arms `busy` and defers here to draw its phase; a Ctrl-O meanwhile left
294                // the chart, so release rather than park an export nothing will prepare.
295                if !self.overlay.shows(&Overlay::Chart) {
296                    self.export_progress = None;
297                    self.status_message = None;
298                    self.busy = false;
299                    return None;
300                }
301                self.start_chart_export(request);
302                None
303            }
304        }
305    }
306}