1use color_eyre::Result;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use polars::datatypes::DataType;
4use polars::prelude::{DataFrame, LazyFrame, col};
5use std::collections::HashMap;
6
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, mpsc::Sender};
9use widgets::info::{FileFacts, InfoModal};
10
11use ratatui::style::Style;
12use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
13
14use ratatui::widgets::{Block, Clear};
15
16pub mod analysis;
17pub mod app;
18pub mod cache;
19pub mod canonical;
20pub mod chart;
21pub mod cli;
22pub mod clipboard;
23pub mod cloud;
24pub mod commands;
25pub mod config;
26pub mod error_display;
27pub mod exact;
28pub mod export;
29pub mod find;
30pub mod formats;
31pub mod glyphs;
32pub mod home;
33pub mod inspector;
34pub mod limits;
35pub mod loading;
36pub mod logging;
37pub mod notes;
38pub mod numfmt;
39pub use app::applied::Applied;
40pub use app::overlay::Overlay;
41pub mod past_calendar;
42pub use app::run::{ended_by_signal, run, run_captured};
43pub mod query;
45mod render;
46pub mod sanitize;
47pub mod table;
48pub mod typed_value;
49pub mod view;
50pub mod widgets;
51
52pub use cache::CacheManager;
53pub use cli::Args;
54pub use config::{
55 AppConfig, ColorParser, ConfigManager, QueryMode, Theme, ThemeMode, rgb_to_256_color,
56 rgb_to_basic_ansi,
57};
58
59use analysis::analysis_modal::{AnalysisModal, AnalysisProgress};
60use app::background::{CacheWrites, InflightCollect, LenCount, OwedCount};
61use chart::chart_export::ChartExportRequest;
62use chart::chart_export_modal::ChartExportModal;
63use chart::chart_jobs::ChartRequest;
64use chart::chart_modal::ChartColumns;
65
66pub use analysis::quality_memory::{KeptQualitySample, QUALITY_MEMORY_BUDGET, RetainedCopy};
67use app::feedback::Confirm;
68pub use app::feedback::{ConfirmationModal, ErrorModal, Flash};
69use app::jobs::{Answer, Job, Jobs, Outcome};
70pub use app::jobs::{JobKind, Progress, Ticket};
71use app::modals::filter_modal::{FilterOperator, FilterStatement, LogicalOperator};
72use app::modals::pivot_melt_modal::PivotMeltModal;
73use app::modals::sort_filter_modal::SortFilterModal;
74use app::modals::sort_modal::{SortColumn, order_with_hidden};
75pub use error_display::{ErrorKindForPython, error_for_python};
76use export::export_modal::{ExportFocus, ExportModal};
77use export::output_file::Overwrite;
78pub use export::{ExportOptions, ExportRequest};
79pub use loading::open_options::{
80 OpenOptions, ParseStringsTarget, ReadReport, SqliteOpen, TypedDialect, UnaskedDownload,
81};
82pub use loading::unfinished::ExitSweep;
83use numfmt::NumberFormatSettings;
84use table::{DataTableState, DrillRow};
85pub use view::{SavedView, ViewManager, Views};
86use widgets::column_widths::WidthChoice;
87use widgets::debug::DebugState;
88use widgets::text_input::TextInput;
89use widgets::view_modal::{FormFocus, ViewModal, ViewModalMode};
90
91pub const APP_NAME: &str = "datui";
93
94pub(crate) const UNSUPPORTED: &str =
96 "Unsupported file type. --format names the format to read it as.";
97
98pub use cli::{CompressionFormat, FileFormat, ReadMode, RemoteRead, Stored, Summary};
99
100#[cfg(test)]
101pub mod tests;
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Scroll {
106 Next,
107 Prev,
108 PageDown,
109 PageUp,
110 HalfDown,
111 HalfUp,
112 Start,
113 End,
114}
115
116impl Scroll {
117 fn delta(self, state: &DataTableState) -> i64 {
119 let page = state.visible_rows as i64;
120 let half = (state.visible_rows / 2).max(1) as i64;
121 match self {
122 Scroll::Next => 1,
123 Scroll::Prev => -1,
124 Scroll::PageDown => page,
125 Scroll::PageUp => -page,
126 Scroll::HalfDown => half,
127 Scroll::HalfUp => -half,
128 Scroll::Start | Scroll::End => 0,
129 }
130 }
131
132 fn run(self, state: &mut DataTableState) -> bool {
134 match self {
135 Scroll::Next => state.select_next(),
136 Scroll::Prev => state.select_previous(),
137 Scroll::PageDown => state.page_down(),
138 Scroll::PageUp => state.page_up(),
139 Scroll::HalfDown => state.half_page_down(),
140 Scroll::HalfUp => state.half_page_up(),
141 Scroll::Start => state.scroll_to_start(),
142 Scroll::End => state.scroll_to_end(),
143 }
144 }
145}
146
147pub enum AppEvent {
148 Key(KeyEvent),
149 Press(KeyEvent),
152 Paste(String),
155 Terminal(crossterm::event::Event),
158 Wake,
161 TerminalBackground(ThemeMode),
163 TerminalFocused,
165 SettingsRead(Box<Result<app::startup::Settings>>),
167 OpenNamed(Vec<PathBuf>, OpenOptions),
170 NamedPathMissing(PathBuf),
172 Open(Vec<PathBuf>, OpenOptions),
174 OpenLazyFrame(Box<LazyFrame>, OpenOptions),
176 HomeListingReady {
178 generation: u64,
179 listing: Box<crate::home::Listing>,
180 known: Option<crate::home::Known>,
183 learned: Vec<(PathBuf, crate::cache::DatasetFacts)>,
185 visits: std::collections::HashMap<PathBuf, crate::cache::Visits>,
187 newest: Option<PathBuf>,
189 folds: Option<std::collections::HashMap<String, bool>>,
191 },
192 HomeListingFailed,
194 HomePathListed {
196 listing: Box<crate::home::PathListing>,
197 },
198 HomePathCompleted {
200 generation: u64,
201 typed: String,
203 completed: String,
204 candidates: usize,
205 },
206 HomePreviewReady {
209 path: PathBuf,
210 stamp: crate::home::home_preview::Stamp,
212 read_at: Option<crate::home::home_preview::Stamp>,
214 rows: Option<Arc<crate::home::home_preview::PreviewRows>>,
215 prepared: crate::home::home_preview::Handoff,
216 },
217 HomeSchemaReady {
219 path: PathBuf,
220 preview: Option<crate::home::discover::SchemaPreview>,
221 },
222 HomeMeasured {
226 measured: Vec<(PathBuf, crate::home::Measured)>,
227 done: bool,
228 },
229 HomeSized {
231 path: PathBuf,
232 measured: crate::home::Measured,
233 },
234 HomeWebGone {
236 path: PathBuf,
237 gone: crate::error_display::HttpGone,
238 },
239 HomeClassified {
243 pass: PathBuf,
244 measured: Vec<(PathBuf, crate::home::Measured)>,
245 done: bool,
246 },
247 HomeSearchBatch {
249 generation: u64,
250 root: PathBuf,
251 found: Vec<crate::home::discover::Entry>,
252 scanned: usize,
253 },
254 HomeSearchScored {
256 epoch: u64,
257 matches: Option<Box<crate::home::search::Matches>>,
259 },
260 HomeSearchDone {
262 generation: u64,
263 root: PathBuf,
264 scanned: usize,
265 limited: Option<String>,
266 },
267 #[cfg(feature = "cloud")]
270 HomeCloudSources {
271 sources: Vec<crate::home::CloudSource>,
272 },
273 #[cfg(feature = "cloud")]
275 HomeCloudListed {
276 id: String,
277 buckets: Vec<PathBuf>,
278 details: Vec<(PathBuf, Vec<(String, String)>)>,
280 failure: Option<(String, String)>,
282 listed_at: std::time::SystemTime,
283 },
284 HomeProbeReady {
286 root: PathBuf,
287 rows: Option<Vec<crate::home::discover::Entry>>,
288 cut_short: bool,
290 },
291 HomeProbeProgress {
293 root: PathBuf,
294 rows: Vec<crate::home::discover::Entry>,
295 },
296 HomeCloudKinds {
298 kinds: Vec<(
299 PathBuf,
300 (
301 crate::home::discover::EntryKind,
302 crate::home::discover::Holds,
303 ),
304 )>,
305 failed: Vec<PathBuf>,
307 },
308 HomeProbeCancelled {
311 root: PathBuf,
312 },
313 HomeNarrowed {
316 dir: PathBuf,
317 prefix: String,
318 listed: Option<(Vec<crate::home::discover::Entry>, bool)>,
319 },
320 HomeProbeFailed {
322 root: PathBuf,
323 message: String,
324 },
325 Followed(crate::loading::follow::News),
327 Exit,
328 Crash(String),
329 Applied(Applied),
331 Collect,
332 Update,
333 Reset,
334 Resize(u16, u16), Scroll(Scroll),
337 AnalysisCompute(analysis::analysis_modal::AnalysisTool),
339 BackgroundQualitySampleKept {
341 kept: KeptQualitySample,
342 },
343 BackgroundQualityCopyKept {
346 dataset_generation: u64,
347 copy: Option<Arc<crate::cloud::local_copy::LocalCopy>>,
348 },
349 BackgroundLenReady {
353 len_generation: u64,
354 num_rows: usize,
355 file_row_groups: Option<Vec<Vec<usize>>>,
357 },
358 BackgroundLenFailed {
361 len_generation: u64,
362 },
363 FramePainted,
366 LookThenOpenDirectory(PathBuf, OpenOptions),
370 ClassifyThenOpen {
373 path: PathBuf,
374 typed: Option<String>,
377 },
378 JobEnded(Ticket),
380 JobProgress {
382 ticket: Ticket,
383 progress: Progress,
384 },
385}
386
387impl AppEvent {
388 pub fn is_progress(&self) -> bool {
392 matches!(
393 self,
394 AppEvent::HomeMeasured { .. }
395 | AppEvent::HomeClassified { .. }
396 | AppEvent::HomeSearchBatch { .. }
397 | AppEvent::HomeProbeProgress { .. }
398 | AppEvent::JobProgress {
399 progress: Progress::QualityPhase(_),
400 ..
401 }
402 )
403 }
404}
405
406#[cfg(test)]
409type HomeWorkerDies = Box<dyn FnMut(&AppEvent) -> bool + Send>;
410
411#[cfg(test)]
413type FileFactsReader = Arc<
414 dyn Fn(&Path, Option<crate::formats::readers::Facts>) -> std::result::Result<FileFacts, String>
415 + Send
416 + Sync,
417>;
418
419pub type EventOutcome = Result<Option<AppEvent>, KeyEvent>;
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum WhatEnter {
428 OpensFile,
430 OpensDirectory,
433 GoesInside,
435 LooksFirst,
437 FoldsSection,
439 ShowsMore,
441 GoesUp,
443 ShowsHidden,
445 Explains,
448 OpensHex,
450 Nothing,
452}
453
454impl App {
455 pub fn what_enter_does(&self) -> WhatEnter {
457 let entry = match self.home.cursor_row() {
459 Some(home::Row::Place { path, .. }) => {
461 return if home::place_is_browsable(&path) {
462 WhatEnter::GoesInside
463 } else {
464 WhatEnter::Explains
465 };
466 }
467 Some(home::Row::Header { .. }) => return WhatEnter::FoldsSection,
468 Some(home::Row::More { .. }) => return WhatEnter::ShowsMore,
469 Some(home::Row::Up { .. }) => return WhatEnter::GoesUp,
470 Some(home::Row::Hidden { .. }) => return WhatEnter::ShowsHidden,
471 None => return WhatEnter::Nothing,
472 Some(home::Row::Door { .. }) => return WhatEnter::OpensDirectory,
474 Some(home::Row::Entry { entry, .. }) => entry,
475 };
476 if entry.kind != home::discover::EntryKind::File
478 && self.home.bookmark(&entry.path).is_some()
479 {
480 return WhatEnter::OpensDirectory;
481 }
482 match entry.kind {
483 home::discover::EntryKind::Unknown => WhatEnter::LooksFirst,
484 home::discover::EntryKind::File if entry.enter_lists_tables() => WhatEnter::GoesInside,
486 home::discover::EntryKind::File => WhatEnter::OpensFile,
487 home::discover::EntryKind::Other
488 if matches!(
489 cloud::source::input_source(&entry.path),
490 cloud::source::InputSource::Local(_)
491 ) =>
492 {
493 WhatEnter::OpensHex
494 }
495 home::discover::EntryKind::Other => WhatEnter::Nothing,
496 home::discover::EntryKind::Hive | home::discover::EntryKind::MultiFile => {
497 WhatEnter::OpensDirectory
498 }
499 home::discover::EntryKind::Directory
501 | home::discover::EntryKind::Delta
502 | home::discover::EntryKind::Iceberg
503 | home::discover::EntryKind::Hudi => WhatEnter::GoesInside,
504 }
505 }
506}
507
508impl App {
509 fn was_fetched(
512 download: Option<&crate::cloud::download::TempDownload>,
513 path: Option<&Path>,
514 ) -> bool {
515 download.is_some() && path.is_some_and(cloud::source::is_remote_url)
516 }
517}
518
519#[derive(Clone)]
521pub enum RunInput {
522 Cli(Box<Args>),
525 Host(Box<Args>, Option<Box<LazyFrame>>),
528 Paths(Vec<PathBuf>, OpenOptions),
529 LazyFrame(Box<LazyFrame>, OpenOptions),
530}
531
532#[derive(Debug, Default, PartialEq, Eq)]
534pub enum InputMode {
535 #[default]
536 Normal,
537 Home,
539 Editing,
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544pub enum InputType {
545 Query,
547 Find,
548}
549
550struct QueryRun {
553 origin: RunOrigin,
554 frame: u64,
557 rollback: crate::table::ViewRollback,
558 counts: loading::counting::CountMarkers,
561 rows: Option<usize>,
563}
564
565enum RunOrigin {
567 Query(QueryMode),
569 View {
572 previous: Option<String>,
573 name: String,
574 applying: view::view_apply::Applying,
575 },
576}
577
578fn recording_label(spool: &crate::loading::follow::Spool) -> (String, bool) {
582 let dot = crate::glyphs::get().middot;
583 let size = crate::numfmt::bytes(spool.bytes());
584 match spool.ended() {
585 None => (
586 format!(
587 "rec {size} {dot} {}/s",
588 crate::numfmt::bytes(spool.rate() as u64)
589 ),
590 false,
591 ),
592 Some(None) => {
593 let secs = spool.duration().as_secs();
594 let length = if secs >= 3600 {
595 format!("{}:{:02}:{:02}", secs / 3600, secs / 60 % 60, secs % 60)
596 } else {
597 format!("{}:{:02}", secs / 60, secs % 60)
598 };
599 match spool.tee().filter(|tee| !tee.to_stdout()) {
600 Some(tee) => (
601 format!("saved {size} {dot} {length} {dot} {}", tee.name()),
602 false,
603 ),
604 None => (format!("sent {size} {dot} {length}"), false),
605 }
606 }
607 Some(Some(reason)) => (format!("stopped: {reason}"), true),
608 }
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub(crate) enum Leaving {
614 Quit,
615 Home,
616}
617
618#[derive(Clone, Debug)]
620pub struct ExportProgress {
621 pub file_path: PathBuf,
622 pub current_phase: String,
623 pub written: Option<u64>,
624}
625
626impl ExportProgress {
627 pub fn new(file_path: &Path, phase: &str) -> Self {
629 Self {
630 file_path: file_path.to_path_buf(),
631 current_phase: phase.to_string(),
632 written: None,
633 }
634 }
635}
636
637fn correlations_of_sample(
639 lf: &LazyFrame,
640 sample: &analysis::sampling::Sample,
641 known_total: Option<usize>,
642 streaming: bool,
643) -> Result<crate::analysis::statistics::AnalysisResults> {
644 let schema = lf.clone().collect_schema()?;
645 let numeric: Vec<polars::prelude::Expr> = schema
646 .iter()
647 .filter(|(_, dtype)| dtype.is_numeric())
648 .map(|(name, _)| col(name.clone()))
649 .collect();
650 let rows = crate::analysis::sampling::read(
651 &lf.clone().select(numeric),
652 sample,
653 known_total,
654 streaming,
655 )?;
656 Ok(crate::analysis::statistics::AnalysisResults {
657 column_statistics: vec![],
658 total_rows: rows.total_rows,
659 sample_size: rows.sample_size,
660 per_value: rows.per_value.map(|per_value| per_value.kept),
661 correlation_matrix: crate::analysis::statistics::compute_correlation_matrix(&rows.df).ok(),
662 distribution_analyses: vec![],
663 })
664}
665
666fn active_query_settings(
669 dsl_query: &str,
670 sql_query: &str,
671 fuzzy_query: &str,
672) -> (Option<String>, Option<String>, Option<String>) {
673 let sql_trimmed = sql_query.trim();
674 let fuzzy_trimmed = fuzzy_query.trim();
675 let dsl_trimmed = dsl_query.trim();
676 if !sql_trimmed.is_empty() {
677 (None, Some(sql_trimmed.to_string()), None)
678 } else if !fuzzy_trimmed.is_empty() {
679 (None, None, Some(fuzzy_trimmed.to_string()))
680 } else if !dsl_trimmed.is_empty() {
681 (Some(dsl_trimmed.to_string()), None, None)
682 } else {
683 (None, None, None)
684 }
685}
686
687pub(crate) fn view_settings_of(state: &DataTableState) -> view::ViewSettings {
689 let (query, sql_query, fuzzy_query) = active_query_settings(
690 state.get_active_query(),
691 state.get_active_sql_query(),
692 state.get_active_fuzzy_query(),
693 );
694 view::ViewSettings {
695 chart: None,
696 sample: saved_sample_of(state),
697 query,
698 sql_query,
699 fuzzy_query,
700 filters: state.get_filters().to_vec(),
701 sort_columns: state.get_sort_columns().to_vec(),
702 sort_descending: state.get_sort_descending().to_vec(),
703 sort_ascending: state.get_sort_ascending(),
704 column_order: state.get_column_order().to_vec(),
705 locked_columns_count: state.locked_columns_count(),
706 pivot: state.last_pivot_spec().cloned(),
707 melt: state.last_melt_spec().cloned(),
708 reshape_source: state.reshape_source().cloned(),
709 columns: state.column_changes().to_vec(),
710 }
711}
712
713fn saved_sample_of(state: &DataTableState) -> Option<view::SavedSample> {
716 let sampled = state.sampled()?;
717 let through = sampled.through().then(|| view::ViewSettings {
719 sample: None,
720 chart: None,
721 ..view_settings_of(sampled.source())
722 });
723 Some(view::SavedSample::of(
724 sampled.sample(),
725 sampled.path(),
726 through,
727 ))
728}
729
730pub(crate) enum Replayed {
732 Planned,
734 Pivot(Box<crate::table::PivotJob>),
736}
737
738const QUALITY_RUN_WAITS: &str = "Run waits: the cancelled run is still stopping";
740
741const ANALYSIS_READ_WAITS: &str = "A cancelled run is still finishing; try again shortly";
744
745const CANCEL_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
748
749pub struct App {
750 pub data_table_state: Option<DataTableState>,
751 counting: loading::counting::Counting,
753 pub home_app: home::home_app::HomeApp,
755 pub home: home::HomeState,
757 source: loading::open_scan::OpenedSource,
759 path: Option<PathBuf>,
760 pipes: app::run::Pipes,
762 events: Sender<AppEvent>,
763 debug: DebugState,
764 pub info_modal: InfoModal,
765 pub info: app::keys::info_keys::InfoState,
767 pub prompt: query::query_prompt::QueryPrompt,
769 pub input_mode: InputMode,
770 pub overlay: Overlay,
772 pub sort_filter_modal: SortFilterModal,
773 pub pivot_melt_modal: PivotMeltModal,
774 pub view_modal: ViewModal,
775 pub analysis_modal: AnalysisModal,
776 pub sample: analysis::sample_draw::SampleState,
778 quality: analysis::quality_runs::QualityRuns,
780 pub chart: chart::chart_jobs::Charts,
782 pub export_modal: ExportModal,
783 pub copy_modal: app::modals::copy_modal::CopyModal,
784 pub inspector_modal: inspector::inspector_modal::InspectorModal,
785 external: app::run::External,
787 pub pickers: app::keys::picker_keys::Pickers,
789 pub value_counts: analysis::value_counts_modal::ValueCountsModal,
791 pub column_forms: app::keys::retype_keys::ColumnForms,
793 pub hex: app::keys::hex_keys::HexState,
795 error_modal: ErrorModal,
796 flash: Option<Flash>,
797 pub confirmation_modal: ConfirmationModal,
798 help: app::help::Help,
800 pointer: app::pointer::Pointing,
802 context_menu: Option<app::context_menu::ContextMenu>,
804 cache: CacheManager,
805 cache_writes: CacheWrites,
807 views: view::view_apply::SavedViews,
809 export_progress: Option<ExportProgress>,
811 theme: Theme, display: render::context::DisplaySettings,
814 runtime: tokio::runtime::Handle, jobs: Jobs,
817 loading: loading::Loader,
819 dataset_generation: u64,
823 #[cfg(test)]
825 file_facts_reader: Option<FileFactsReader>,
826 busy: bool,
828 screen_generation: u64,
832 input_dropped: bool,
834 throbber_frame: u8,
836 status_message: Option<String>,
839 app_config: AppConfig,
840 formats: Arc<crate::formats::Registry>,
842}
843
844impl App {
845 pub fn is_busy(&self) -> bool {
848 self.busy || self.jobs.holds_keys() || self.loading.waits()
849 }
850
851 pub fn task_generation(&self) -> u64 {
853 self.jobs.generation()
854 }
855
856 pub fn job_is_current(&self, ticket: Ticket) -> bool {
858 self.jobs.is_current(ticket)
859 }
860
861 pub fn use_cache(&mut self, cache: CacheManager) {
864 self.cache = cache;
865 }
866
867 pub fn use_catalog_dir(&mut self, dir: &Path) -> Result<()> {
870 self.app_config.read_catalog_files(Some(dir))
871 }
872
873 pub fn open_path(&self) -> Option<&Path> {
875 self.path.as_deref()
876 }
877
878 fn reads_stdin(&self) -> bool {
880 self.source.opened.as_ref().is_some_and(
881 |(paths, _)| matches!(paths.as_slice(), [path] if loading::stdin::is_stdin(path)),
882 )
883 }
884
885 fn view_dataset(&self) -> Option<view::Dataset<'_>> {
888 self.data_table_state.as_ref()?;
889 let path = match self.path.as_deref() {
890 Some(path) if !self.reads_stdin() => path,
891 _ => Path::new(loading::stdin::PATH),
892 };
893 Some(view::Dataset {
894 path,
895 table: self.view_table(),
896 })
897 }
898
899 fn view_table(&self) -> Option<&str> {
902 let (_, options) = self.source.opened.as_ref()?;
903 options.table.as_deref()
904 }
905
906 #[doc(hidden)]
909 pub fn settle_cache_writes(&self) {
910 self.cache_writes.settle();
911 }
912
913 pub fn background_work_in_flight(&self) -> bool {
915 self.jobs.in_flight()
916 }
917
918 pub fn screen_generation(&self) -> u64 {
920 self.screen_generation
921 }
922
923 pub fn modal_showing(&self) -> bool {
925 self.error_modal.active || self.confirmation_modal.active
926 }
927
928 fn analysis_results_are_sampled(&self) -> bool {
930 self.analysis_modal.view == analysis::analysis_modal::AnalysisView::Main
931 && self.analysis_modal.computing.is_none()
932 && self
933 .analysis_modal
934 .current_results()
935 .is_some_and(|r| r.sample_size.is_some())
936 }
937
938 pub(crate) fn cancelled_analysis_running(&self) -> Option<std::time::Instant> {
941 self.cancelled_analysis().map(|(since, _)| since)
942 }
943
944 fn cancelled_analysis(&self) -> Option<(std::time::Instant, bool)> {
947 let (since, job) = self.jobs.cancelled_running(Self::is_analysis_read)?;
948 let runs_out = match job {
949 Job::Analysis(run) => run.runs_out,
950 _ => true,
951 };
952 Some((since, runs_out))
953 }
954
955 fn is_analysis_read(job: &Job) -> bool {
957 matches!(job, Job::Analysis(_) | Job::SampleRows)
958 }
959
960 pub(crate) fn cancelled_run_shown(&self) -> Option<crate::widgets::data_quality::Cancelling> {
963 let (since, read_runs_out) = self.cancelled_analysis()?;
964 (read_runs_out || since.elapsed() >= CANCEL_GRACE).then_some(
965 crate::widgets::data_quality::Cancelling {
966 since,
967 read_runs_out,
968 },
969 )
970 }
971
972 fn cancelled_work_running(&self) -> bool {
974 self.jobs.running_behind()
975 }
976
977 fn read_waits_for_cancelled(&mut self) -> bool {
979 if self.cancelled_analysis_running().is_none() {
980 return false;
981 }
982 self.flash_note(ANALYSIS_READ_WAITS.to_string());
983 true
984 }
985
986 fn spawn_analysis(&mut self, tool: analysis::analysis_modal::AnalysisTool) -> Option<AppEvent> {
989 use analysis::analysis_modal::AnalysisTool;
990 type Compute = fn(
991 &LazyFrame,
992 &analysis::sampling::Sample,
993 Option<usize>,
994 bool,
995 ) -> Result<crate::analysis::statistics::AnalysisResults>;
996 type Install = fn(
997 &mut analysis::analysis_modal::AnalysisModal,
998 crate::analysis::statistics::AnalysisResults,
999 );
1000 let (status, compute, install): (&str, Compute, Install) = match tool {
1001 AnalysisTool::DataQuality => return self.run_quality_compute(),
1002 AnalysisTool::Describe => (
1003 "Running analysis...",
1004 |lf, sample, known, streaming| {
1005 crate::analysis::statistics::compute_describe_from_lazy(
1006 lf, known, sample, streaming,
1007 )
1008 },
1009 |modal, results| modal.describe_results = Some(results),
1010 ),
1011 AnalysisTool::DistributionAnalysis => (
1012 "Analyzing distributions...",
1013 |lf, sample, known, streaming| {
1014 let options = crate::analysis::statistics::ComputeOptions {
1015 include_distribution_info: true,
1016 include_distribution_analyses: true,
1017 include_correlation_matrix: false,
1018 include_skewness_kurtosis_outliers: true,
1019 polars_streaming: streaming,
1020 };
1021 crate::analysis::statistics::compute_statistics_for_sample(
1022 lf, sample, known, options,
1023 )
1024 },
1025 |modal, results| modal.distribution_results = Some(results),
1026 ),
1027 AnalysisTool::CorrelationMatrix => (
1028 "Computing correlation matrix...",
1029 correlations_of_sample,
1030 analysis::analysis_modal::AnalysisModal::install_correlations,
1031 ),
1032 };
1033 let Some(state) = &self.data_table_state else {
1034 self.analysis_modal.computing = None;
1035 self.busy = false;
1036 return None;
1037 };
1038 let (source, known_total) = self.sample_source(state);
1040 let streaming = match tool {
1041 AnalysisTool::CorrelationMatrix => state.polars_streaming(),
1042 _ => self.app_config.performance.streaming,
1043 };
1044 let sample = self.analysis_modal.sample.clone();
1045 self.spawn_job(
1046 Job::Analysis(app::jobs::AnalysisRun::default()),
1047 Some(status),
1048 move |_| {
1049 let results = source
1050 .cut(&sample.scope)
1051 .and_then(|lf| compute(&lf, &sample, known_total, streaming))
1052 .map_err(|e| format!("{e}"))?;
1053 Ok(Answer::Analysis(install, results))
1054 },
1055 );
1056 None
1057 }
1058
1059 fn start_analysis_run(&mut self) -> Option<AppEvent> {
1061 let tool = self.analysis_modal.selected_tool?;
1062 if tool != analysis::analysis_modal::AnalysisTool::DataQuality
1063 && self.read_waits_for_cancelled()
1064 {
1065 return None;
1066 }
1067 let phase = match tool {
1068 analysis::analysis_modal::AnalysisTool::Describe => {
1069 self.analysis_modal.describe_results = None;
1070 "Describing data"
1071 }
1072 analysis::analysis_modal::AnalysisTool::DistributionAnalysis => {
1073 self.analysis_modal.distribution_results = None;
1074 "Analyzing distributions"
1075 }
1076 analysis::analysis_modal::AnalysisTool::CorrelationMatrix => {
1077 self.analysis_modal.correlation_results = None;
1078 "Computing correlations"
1079 }
1080 analysis::analysis_modal::AnalysisTool::DataQuality => return None,
1081 };
1082 self.analysis_modal.computing = Some(AnalysisProgress::new(phase));
1083 self.busy = true;
1084 Some(AppEvent::AnalysisCompute(tool))
1085 }
1086
1087 fn cancel_analysis(&mut self) {
1091 let reads_out = self
1094 .analysis_modal
1095 .computing
1096 .as_ref()
1097 .is_some_and(AnalysisProgress::read_runs_out);
1098 let mut read_runs_out = true;
1099 if let Some(Job::Analysis(run)) =
1100 self.jobs.current_mut(|job| matches!(job, Job::Analysis(_)))
1101 {
1102 read_runs_out = run.watch.is_none() || reads_out;
1103 run.runs_out = read_runs_out;
1104 if let Some(watch) = &run.watch {
1105 watch.cancel();
1106 }
1107 }
1108 let reading_sample = self
1109 .jobs
1110 .current(|job| matches!(job, Job::SampleRows))
1111 .is_some();
1112 self.jobs.cancel(Self::is_analysis_read);
1113 self.jobs.advance();
1114 self.screen_generation = self.screen_generation.wrapping_add(1);
1116 self.analysis_modal.computing = None;
1117 self.busy = false;
1118 self.status_message = None;
1119 if reading_sample {
1121 self.flash_note("Row view cancelled".to_string());
1122 return;
1123 }
1124 if self.analysis_modal.selected_tool
1128 == Some(analysis::analysis_modal::AnalysisTool::DataQuality)
1129 {
1130 self.open_quality_setup();
1131 if !read_runs_out {
1132 self.flash_note("Run cancelled".to_string());
1133 }
1134 return;
1135 }
1136 self.analysis_modal.selected_tool = None;
1137 self.analysis_modal.focus = analysis::analysis_modal::AnalysisFocus::Sidebar;
1138 self.flash_note("Analysis cancelled".to_string());
1139 }
1140
1141 pub(crate) fn pivot_computing(&self) -> bool {
1143 self.overlay == Overlay::PivotMelt
1144 && self.jobs.current(|job| matches!(job, Job::Pivot)).is_some()
1145 }
1146
1147 fn cancel_pivot(&mut self) {
1150 self.jobs.advance();
1151 self.screen_generation = self.screen_generation.wrapping_add(1);
1152 self.busy = false;
1153 self.status_message = None;
1154 self.flash_note("Pivot cancelled".to_string());
1155 }
1156
1157 fn drill_into(&mut self, group_index: usize, row: &DataFrame) {
1160 let Some(state) = self.data_table_state.as_mut() else {
1161 return;
1162 };
1163 let drilled = state.deferred(|s| s.drill_down_with_row(group_index, row));
1164 match drilled {
1165 Ok(()) => {
1166 self.sync_sort_filter_modal();
1167 self.spawn_async_collect(Self::LOADING_BUFFER);
1168 }
1169 Err(e) => self.flash_note(format!(
1170 "Could not drill in: {}",
1171 crate::error_display::user_message_from_report(&e, None)
1172 )),
1173 }
1174 }
1175
1176 #[doc(hidden)]
1178 pub fn read_stdin_from(&mut self, reader: impl std::io::Read + Send + 'static) {
1179 self.pipes.stdin_reader = Some(Box::new(reader));
1180 }
1181
1182 #[doc(hidden)]
1184 pub fn pass_stdout_to(&mut self, out: impl std::io::Write + Send + 'static) {
1185 self.pipes.stdout_pass = Some(Box::new(out));
1186 }
1187
1188 #[doc(hidden)]
1191 pub fn releases_settled(within: std::time::Duration) -> bool {
1192 crate::app::background::releases_settled(within)
1193 }
1194
1195 pub fn follow(&self) -> Option<&crate::loading::follow::Follow> {
1197 self.data_table_state.as_ref()?.follow()
1198 }
1199
1200 #[doc(hidden)]
1203 pub fn follow_settled(&self) -> bool {
1204 self.rows_in_flight().is_none() && !self.follow().is_some_and(|f| f.behind())
1205 }
1206
1207 #[doc(hidden)]
1209 pub fn check_follow_now(&self) {
1210 if let Some(follow) = self.follow() {
1211 follow.check_now();
1212 }
1213 }
1214
1215 fn followed(&mut self, news: &crate::loading::follow::News) {
1217 let Some(state) = self.data_table_state.as_mut() else {
1218 return;
1219 };
1220 let Some(follow) = state.follow_mut().filter(|f| f.id() == news.id) else {
1221 return;
1222 };
1223 let message = follow.take(&news.change);
1224 let fields = follow.take_new_fields();
1225 if let Some(handle) = follow.take_held() {
1226 state.read_followed_through(&handle);
1227 }
1228 if let Some(message) = message {
1229 self.flash_note(message);
1230 }
1231 if !fields.is_empty() {
1232 self.counting.followed_fields_held = Some((self.dataset_generation, fields));
1233 }
1234 self.catch_up_follow();
1235 self.join_followed_fields();
1236 self.describe_ended_journal();
1237 }
1238
1239 fn describe_ended_journal(&mut self) {
1242 let Some(lf) = self
1243 .data_table_state
1244 .as_mut()
1245 .and_then(|state| state.ended_journal_to_describe())
1246 else {
1247 return;
1248 };
1249 let dataset = self.dataset_generation;
1250 self.spawn_job(Job::JournalDetail { dataset }, None, move |_| {
1251 crate::formats::journal::summary(&lf)
1252 .map(|detail| Answer::JournalDescribed(Box::new(detail)))
1253 .map_err(|e| e.to_string())
1254 });
1255 }
1256
1257 fn join_followed_fields(&mut self) {
1260 let Some((generation, _)) = self.counting.followed_fields_held.as_ref() else {
1261 return;
1262 };
1263 if *generation != self.dataset_generation {
1264 self.counting.followed_fields_held = None;
1265 return;
1266 }
1267 if self.data_table_state.is_none()
1269 || self.work_the_join_would_cancel()
1270 || self.follow().is_some_and(|f| f.behind())
1271 {
1272 return;
1273 }
1274 let Some((generation, fields)) = self.counting.followed_fields_held.take() else {
1275 return;
1276 };
1277 let Some(state) = self.data_table_state.as_mut() else {
1278 return;
1279 };
1280 match state.join_followed_fields(&fields) {
1281 Ok(true) => {
1282 self.spawn_async_collect(Self::LOADING_BUFFER);
1283 }
1284 Ok(false) => {}
1285 Err(()) => self.counting.followed_fields_held = Some((generation, fields)),
1286 }
1287 }
1288
1289 fn catch_up_follow(&mut self) {
1293 if !self.in_normal_table_view()
1294 || self.is_busy()
1295 || self.loading.awaiting_dataset()
1296 || self.rows_in_flight().is_some()
1297 {
1298 return;
1299 }
1300 self.take_follow_rows(true);
1301 }
1302
1303 fn take_follow_rows(&mut self, read: bool) -> bool {
1306 let Some(state) = self.data_table_state.as_mut() else {
1307 return false;
1308 };
1309 let on_last_row = state.on_last_row();
1310 let counted = state.is_num_rows_valid();
1311 let drawn = state.visible_rows > 0 && counted;
1312 let Some(follow) = state.follow_mut() else {
1313 return false;
1314 };
1315 let settle = read && drawn && std::mem::take(&mut follow.settle_at_end);
1318 let to_end = settle || (read && counted && std::mem::take(&mut follow.end_pending));
1319 let stale = read && std::mem::take(&mut follow.stale_view);
1320 let at_bottom = to_end || on_last_row || follow.end_pending;
1321 if at_bottom {
1322 follow.new_below = 0;
1323 }
1324 if !follow.behind() {
1325 if (to_end && state.scroll_to_end()) || stale {
1326 self.spawn_collect(None);
1327 }
1328 return false;
1329 }
1330 let before = follow.shown();
1331 let (rows, restarted) = follow.catch_up();
1332 if at_bottom {
1333 follow.end_pending = true;
1334 } else if !restarted {
1335 follow.new_below += rows.saturating_sub(before);
1336 }
1337 follow.stale_view = !read;
1338 state.follow_to(rows, restarted);
1339 if at_bottom && read {
1340 if state.is_num_rows_valid() {
1341 state.aim_at_end();
1342 } else {
1343 self.counting.end_after_count = Some(state.len_generation());
1345 }
1346 }
1347 if read && !self.spawn_collect(None) {
1348 self.catch_up_follow();
1350 }
1351 true
1352 }
1353
1354 fn follow_rows_waiting(&self) -> bool {
1357 self.follow().is_some_and(|f| f.behind()) && !self.loading.awaiting_dataset()
1358 }
1359
1360 pub fn recording(&self) -> Option<&Arc<crate::loading::follow::Spool>> {
1362 self.data_table_state.as_ref()?;
1363 self.source
1364 .opened
1365 .as_ref()
1366 .and_then(|(_, options)| options.spool.as_ref())
1367 .map(|handle| handle.spool())
1368 .filter(|spool| spool.tee().is_some())
1369 }
1370
1371 fn leaving_by(&self, key: &KeyEvent) -> Option<Leaving> {
1373 if !key.is_press() {
1374 return None;
1375 }
1376 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1377 match key.code {
1378 KeyCode::Char('q') if ctrl => Some(Leaving::Quit),
1379 KeyCode::Char('c') if ctrl => Some(Leaving::Quit),
1380 KeyCode::Char('o') if ctrl => Some(Leaving::Home),
1381 KeyCode::Char('Q') if !ctrl && self.in_normal_table_view() => Some(Leaving::Quit),
1382 KeyCode::Char('q') if !ctrl && self.in_normal_table_view() => {
1383 Some(if self.source.opened_from_home {
1384 Leaving::Home
1385 } else {
1386 Leaving::Quit
1387 })
1388 }
1389 _ => None,
1390 }
1391 }
1392
1393 fn ask_about_recording(&mut self, leaving: Leaving) {
1395 let Some(tee) = self.recording().and_then(|spool| spool.tee()) else {
1396 return;
1397 };
1398 let doing = if tee.to_stdout() {
1399 "passed on"
1400 } else {
1401 "recorded"
1402 };
1403 let message = format!(
1404 "Standard input is still being {doing} to {}. Stop recording, or keep \
1405 recording until the stream ends?",
1406 tee.name()
1407 );
1408 self.confirmation_modal.show_choice(
1409 message,
1410 "Stop recording",
1411 "Keep recording",
1412 Confirm::Leave(leaving),
1413 );
1414 }
1415
1416 fn leave_recording(&mut self, leaving: Leaving, stop: bool) -> Option<AppEvent> {
1418 let handle = self
1419 .source
1420 .opened
1421 .as_ref()
1422 .and_then(|(_, options)| options.spool.clone());
1423 if stop {
1424 if let Some(handle) = &handle {
1425 handle.spool().stop();
1426 }
1427 } else {
1428 self.pipes.recording_on = handle;
1430 }
1431 match leaving {
1432 Leaving::Quit => Some(AppEvent::Exit),
1433 Leaving::Home => {
1434 self.enter_home();
1435 None
1436 }
1437 }
1438 }
1439
1440 pub fn recording_after_exit(
1442 &mut self,
1443 ) -> Option<(
1444 crate::loading::follow::Tee,
1445 Arc<crate::loading::follow::SpoolHandle>,
1446 )> {
1447 let handle = self.pipes.recording_on.take()?;
1448 let spool = handle.spool();
1449 let tee = spool.tee()?.clone();
1450 spool.live().then_some((tee, handle))
1451 }
1452
1453 fn notice_recording_end(&mut self) -> bool {
1456 let Some(spool) = self.recording().cloned() else {
1457 return false;
1458 };
1459 let Some(ended) = spool.ended() else {
1460 self.pipes.recording_end_said = false;
1461 return false;
1462 };
1463 if std::mem::replace(&mut self.pipes.recording_end_said, true) {
1464 return false;
1465 }
1466 let said = match spool.tee() {
1467 Some(tee) if tee.to_stdout() => "Standard input ended".to_string(),
1468 Some(tee) => format!("Saved {}", tee.path.display()),
1469 None => String::new(),
1470 };
1471 match ended {
1472 Some(reason) => self.error_modal.show(reason),
1473 None => self.flash_note(said),
1474 }
1475 true
1476 }
1477
1478 fn flash_note(&mut self, message: String) {
1480 self.flash = Some(Flash::new(message));
1481 }
1482
1483 fn flash_path(&mut self, prefix: &str, path: &std::path::Path) {
1485 self.flash = Some(Flash::path(prefix, path));
1486 }
1487
1488 pub fn flash_message(&self) -> Option<&str> {
1490 self.flash.as_ref().map(|f| f.message.as_str())
1491 }
1492
1493 pub fn error_message(&self) -> Option<&str> {
1495 self.error_modal
1496 .active
1497 .then_some(self.error_modal.message.as_str())
1498 }
1499
1500 pub fn next_deadline(&self) -> Option<std::time::Instant> {
1503 let flash = self.flash.as_ref().map(|f| f.expires);
1504 let clock = self
1505 .follow()
1506 .filter(|f| f.standing == crate::loading::follow::Standing::Following)
1507 .and_then(|f| f.last_append)
1508 .map(crate::loading::follow::next_tick);
1509 let recording = self
1511 .recording()
1512 .filter(|spool| spool.live())
1513 .map(|_| std::time::Instant::now() + std::time::Duration::from_secs(1));
1514 flash.into_iter().chain(clock).chain(recording).min()
1515 }
1516
1517 pub fn tick_follow_clock(&mut self) -> bool {
1519 let ended = self.notice_recording_end();
1520 let now = self.follow_mark();
1521 if now == self.pipes.follow_drawn {
1522 return ended;
1523 }
1524 self.pipes.follow_drawn = now;
1525 true
1526 }
1527
1528 fn follow_mark(&self) -> Option<crate::render::footer::FollowMark> {
1530 use crate::loading::follow::Standing;
1531 if self.overlay == Overlay::Hex {
1533 return None;
1534 }
1535 let state = self.data_table_state.as_ref()?;
1536 let rows = |n: usize, what: &str| format!("{} {what}", crate::numfmt::group_chrome(n));
1537 let (rec, rec_stopped) = match self.recording().map(|spool| recording_label(spool)) {
1538 Some((label, stopped)) => (Some(label), stopped),
1539 None => (None, false),
1540 };
1541 let Some(follow) = state.follow().filter(|f| f.standing != Standing::Ended) else {
1542 return rec.is_some().then(|| crate::render::footer::FollowMark {
1543 rec,
1544 rec_stopped,
1545 ..Default::default()
1546 });
1547 };
1548 let waiting = follow.waiting();
1549 let (chip, note) = match follow.standing {
1550 Standing::Following if follow.is_pipe() => {
1552 let read = follow.spool().map_or(0, |spool| spool.bytes());
1553 let chip = format!(
1554 "reading stdin {} {}",
1555 crate::glyphs::get().middot,
1556 crate::numfmt::bytes(read)
1557 );
1558 let note = (follow.new_below > 0 && !state.on_last_row())
1559 .then(|| rows(follow.new_below, "new below"));
1560 (chip, note)
1561 }
1562 Standing::Following => {
1563 let chip = match follow.last_append {
1564 Some(at) => format!(
1565 "following {} {}",
1566 crate::glyphs::get().middot,
1567 crate::loading::follow::age(at.elapsed())
1568 ),
1569 None => "following".to_string(),
1570 };
1571 let note = if waiting > 0 && !self.in_normal_table_view() {
1572 Some(rows(waiting, "new rows"))
1574 } else if follow.new_below > 0 && !state.on_last_row() {
1575 Some(rows(follow.new_below, "new below"))
1576 } else {
1577 None
1578 };
1579 (chip, note)
1580 }
1581 _ => (
1582 "paused".to_string(),
1583 (waiting > 0).then(|| rows(waiting, "new rows")),
1584 ),
1585 };
1586 let misfits = follow.misfits();
1587 let refreshes = self.overlay == Overlay::ValueCounts
1590 || (self.overlay == Overlay::Chart && self.chart.modal.picker.is_none())
1591 || (self.overlay == Overlay::Analysis
1592 && self.analysis_modal.current_results().is_some());
1593 let key = if self.in_normal_table_view() {
1594 Some(match follow.standing {
1595 Standing::Paused => "Resume",
1596 _ => "Pause",
1597 })
1598 } else if refreshes && follow.behind() {
1599 Some("Refresh")
1600 } else {
1601 None
1602 };
1603 Some(crate::render::footer::FollowMark {
1604 key,
1605 chip: Some(chip),
1606 note,
1607 warning: (misfits > 0).then(|| {
1608 if misfits == 1 {
1609 "1 row does not fit".to_string()
1610 } else {
1611 rows(misfits, "rows do not fit")
1612 }
1613 }),
1614 rec,
1615 rec_stopped,
1616 })
1617 }
1618
1619 fn toggle_follow(&mut self) -> Option<AppEvent> {
1622 use crate::loading::follow::Standing;
1623 if let Some(follow) = self.data_table_state.as_mut().and_then(|s| s.follow_mut()) {
1624 match follow.standing {
1625 Standing::Following => follow.pause(),
1626 Standing::Paused => {
1627 follow.resume();
1628 self.catch_up_follow();
1629 }
1630 Standing::Ended => {}
1631 }
1632 return None;
1633 }
1634 let (paths, options) = self.source.opened.clone()?;
1635 if paths
1636 .iter()
1637 .any(|path| crate::loading::stdin::is_stdin(path))
1638 {
1639 self.flash_note("Standard input is followed from the start: datui -f -".to_string());
1640 return None;
1641 }
1642 if let Some(refusal) = crate::loading::follow::refuse_paths(&paths, &options) {
1643 self.flash_note(refusal);
1644 return None;
1645 }
1646 let options = OpenOptions {
1647 follow: true,
1648 ..options
1649 };
1650 self.set_loading_phase("Scanning input", 10);
1651 self.name_what_is_loading(paths[0].clone());
1652 Some(AppEvent::Open(paths, options))
1653 }
1654
1655 pub fn tick_flash(&mut self) -> bool {
1657 if self.flash.as_ref().is_some_and(Flash::expired) {
1658 self.flash = None;
1659 return true;
1660 }
1661 false
1662 }
1663
1664 pub fn flash_polars_warning(&mut self) -> bool {
1667 if !self.bar_is_free() {
1668 return false;
1669 }
1670 match logging::next_polars_warning() {
1671 Some(warning) => {
1672 self.flash_note(format!("Polars: {warning}"));
1673 true
1674 }
1675 None => false,
1676 }
1677 }
1678
1679 pub fn flash_background_panic(&mut self) -> bool {
1682 if !self.bar_is_free() {
1683 return false;
1684 }
1685 match logging::take_unreported_panic() {
1686 Some(message) => {
1687 self.flash_note(message);
1688 true
1689 }
1690 None => false,
1691 }
1692 }
1693
1694 fn bar_is_free(&self) -> bool {
1696 !self.is_busy()
1697 && self.flash.is_none()
1698 && !self.error_modal.active
1699 && !self.confirmation_modal.active
1700 }
1701
1702 pub fn set_input_dropped(&mut self, dropped: bool) {
1704 self.input_dropped = dropped;
1705 }
1706
1707 pub fn hard_escape_while_busy(&self, key: &KeyEvent) -> bool {
1711 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1712 let quit = ctrl && matches!(key.code, KeyCode::Char('q' | 'c'));
1713 let home = ctrl && key.code == KeyCode::Char('o');
1714 let cancel_analysis = self.overlay == Overlay::Analysis
1715 && self.analysis_modal.computing.is_some()
1716 && key.code == KeyCode::Esc;
1717 let cancel_pivot = self.pivot_computing() && key.code == KeyCode::Esc;
1718 let leave_quality_evidence =
1719 self.quality.evidence_return.is_some() && self.at_table() && key.code == KeyCode::Esc;
1720 let cancel_view = key.code == KeyCode::Esc && self.view_applying();
1721 let cancel_find = key.code == KeyCode::Esc && self.finding();
1722 let stop_sample =
1723 key.code == KeyCode::Esc && self.sample_drawing() && self.in_normal_table_view();
1724 let close_help = self.help.is_open()
1726 && matches!(key.code, KeyCode::Esc | KeyCode::F(1) | KeyCode::Char('?'));
1727 quit || home
1728 || close_help
1729 || cancel_analysis
1730 || cancel_pivot
1731 || cancel_view
1732 || cancel_find
1733 || stop_sample
1734 || leave_quality_evidence
1735 || self.confirmation_modal.active
1736 || self.input_mode == InputMode::Home
1737 }
1738
1739 fn column_cursor_key(key: &KeyEvent) -> Option<crate::widgets::column_paging::CursorMove> {
1742 use crate::widgets::column_paging::CursorMove;
1743 if key
1744 .modifiers
1745 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
1746 {
1747 return None;
1748 }
1749 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1750 match key.code {
1751 KeyCode::Left if shift => Some(CursorMove::PageLeft),
1752 KeyCode::Right if shift => Some(CursorMove::PageRight),
1753 KeyCode::Left | KeyCode::Char('h') => Some(CursorMove::Left),
1754 KeyCode::Right | KeyCode::Char('l') => Some(CursorMove::Right),
1755 KeyCode::Char('{') => Some(CursorMove::First),
1756 KeyCode::Char('}') => Some(CursorMove::Last),
1757 _ => None,
1758 }
1759 }
1760
1761 pub fn key_acts_while_busy(&self, key: &KeyEvent) -> bool {
1768 if self.hard_escape_while_busy(key) || self.menu_takes(key) {
1769 return true;
1770 }
1771 if self.key_acts_while_sampling(key) {
1772 return true;
1773 }
1774 if !self.in_normal_table_view() {
1775 return false;
1776 }
1777 let step = match key.code {
1780 KeyCode::Down | KeyCode::Char('j') => Some(1),
1781 KeyCode::Up | KeyCode::Char('k') => Some(-1),
1782 _ => None,
1783 };
1784 if let Some(step) = step {
1785 return !self.busy
1786 && !self.loading.waits()
1787 && self
1788 .jobs
1789 .keys_held_only_by(|job| matches!(job, Job::Rows(_) | Job::OwedRows { .. }))
1790 && self
1791 .data_table_state
1792 .as_ref()
1793 .is_some_and(|s| !s.scroll_would_trigger_collect(step));
1794 }
1795 matches!(
1796 key.code,
1797 KeyCode::Char('q')
1798 | KeyCode::Char('Q')
1799 | KeyCode::Char('#')
1801 | KeyCode::Char('<')
1802 | KeyCode::Char('>')
1803 | KeyCode::Char('=')
1804 | KeyCode::Char('w')
1805 | KeyCode::Char(',')
1806 | KeyCode::Char('D')
1807 | KeyCode::Left
1808 | KeyCode::Right
1809 | KeyCode::Char('h')
1810 | KeyCode::Char('l')
1811 | KeyCode::Char('{')
1812 | KeyCode::Char('}')
1813 | KeyCode::F(1)
1814 | KeyCode::Char('?')
1815 )
1816 }
1817
1818 pub fn in_normal_table_view(&self) -> bool {
1820 self.at_table()
1821 && !self.help.is_open()
1822 && !self.error_modal.active
1823 && !self.confirmation_modal.active
1824 && self.context_menu.is_none()
1825 }
1826
1827 fn render_drop_mark(&self, buf: &mut Buffer, ctx: &crate::render::context::RenderContext) {
1830 let Some(app::pointer::Drag::Move { column, over }) = self.pointer.drag() else {
1831 return;
1832 };
1833 let Some(state) = self.data_table_state.as_ref() else {
1834 return;
1835 };
1836 let Some((header, columns)) = state.drawn_header() else {
1837 return;
1838 };
1839 let order = state.get_column_order();
1840 let (Some(from), Some(to)) = (
1841 order.iter().position(|c| c == column),
1842 order.iter().position(|c| c == over),
1843 ) else {
1844 return;
1845 };
1846 let locked = state.locked_columns_count().min(order.len());
1848 if from == to || (from < locked) != (to < locked) {
1849 return;
1850 }
1851 let Some((left, right, _)) = columns.iter().find(|(_, _, name)| name == over) else {
1852 return;
1853 };
1854 let x = if to > from {
1855 *right
1856 } else {
1857 left.saturating_sub(1)
1858 };
1859 if !(header.x..header.right()).contains(&x) {
1860 return;
1861 }
1862 let g = crate::glyphs::get();
1863 for y in header.y..header.bottom() {
1864 let cell = &mut buf[(x, y)];
1865 cell.set_symbol(g.rule);
1866 cell.set_style(Style::default().fg(ctx.accent));
1867 }
1868 }
1869
1870 pub(crate) fn menu_showing(&self) -> bool {
1872 self.context_menu.is_some()
1873 && self.at_table()
1874 && self.data_table_state.is_some()
1875 && !self.help.is_open()
1876 && !self.error_modal.active
1877 && !self.confirmation_modal.active
1878 }
1879
1880 pub(crate) fn menu_takes(&self, key: &KeyEvent) -> bool {
1883 self.menu_showing()
1884 && key.modifiers.is_empty()
1885 && matches!(
1886 key.code,
1887 KeyCode::Up
1888 | KeyCode::Down
1889 | KeyCode::Char('j')
1890 | KeyCode::Char('k')
1891 | KeyCode::Enter
1892 | KeyCode::Esc
1893 )
1894 }
1895
1896 pub fn open_context_menu(&mut self, at: ratatui::layout::Position) {
1898 let combine = self
1900 .data_table_state
1901 .as_ref()
1902 .and_then(|state| {
1903 let column = state.current_column()?;
1904 state.schema().get(column).cloned()
1905 })
1906 .is_some_and(|dtype| {
1907 matches!(dtype, DataType::String | DataType::Date | DataType::Time)
1908 });
1909 self.context_menu = Some(app::context_menu::ContextMenu::with(
1910 at,
1911 app::context_menu::column_items(combine),
1912 ));
1913 }
1914
1915 pub fn close_context_menu(&mut self) {
1917 self.context_menu = None;
1918 }
1919
1920 pub fn choose_from_menu(&mut self, i: usize) -> Option<AppEvent> {
1922 let menu = self.context_menu.take()?;
1923 match menu.chosen(i)? {
1924 app::context_menu::MenuKey::Run(key) => Some(AppEvent::Press(key)),
1925 app::context_menu::MenuKey::Do(action) => {
1926 self.menu_action(action);
1927 None
1928 }
1929 _ => None,
1930 }
1931 }
1932
1933 pub fn drop_column(&mut self, column: &str, onto: &str) -> Option<AppEvent> {
1936 let state = self.data_table_state.as_ref()?;
1937 let mut order = state.headers();
1938 let locked = state.locked_columns_count().min(order.len());
1939 let from = order.iter().position(|c| c == column)?;
1940 let to = order.iter().position(|c| c == onto)?;
1941 if from == to || (from < locked) != (to < locked) {
1942 return None;
1943 }
1944 self.flash = None;
1945 self.data_table_state.as_mut()?.set_current_column(column);
1946 let moving = order.remove(from);
1947 order.insert(to, moving);
1948 let applied = &mut self.sort_filter_modal.sort.applied_order;
1951 if let (Some(i), Some(j)) = (
1952 applied.iter().position(|c| c == column),
1953 applied.iter().position(|c| c == onto),
1954 ) {
1955 let moving = applied.remove(i);
1956 applied.insert(j, moving);
1957 }
1958 Some(AppEvent::Applied(Applied::ColumnOrder(order, locked)))
1959 }
1960
1961 pub fn text_field_focused(&self) -> bool {
1964 match self.overlay {
1965 Overlay::None => match self.input_mode {
1966 InputMode::Editing => true,
1967 InputMode::Home => false,
1968 InputMode::Normal => false,
1969 },
1970 Overlay::Analysis => {
1971 self.analysis_modal.sample_scope_typing()
1972 || self.analysis_modal.quality_expected_typing()
1973 || self.analysis_modal.intent_typing()
1974 || self.analysis_modal.export_typing()
1975 }
1976 Overlay::View => {
1977 self.view_modal.mode != ViewModalMode::List
1978 && matches!(
1979 self.view_modal.form_focus,
1980 FormFocus::Name
1981 | FormFocus::Description
1982 | FormFocus::ExactPath
1983 | FormFocus::RelativePath
1984 | FormFocus::PathPattern
1985 | FormFocus::FilenamePattern
1986 )
1987 }
1988 Overlay::Export { .. } => matches!(
1989 self.export_modal.focus,
1990 ExportFocus::PathInput | ExportFocus::CsvDelimiter
1991 ),
1992 Overlay::Copy => self.copy_modal.picker.is_some(),
1993 Overlay::Inspect => self.inspector_modal.finding,
1994 Overlay::GoToColumn => true,
1995 Overlay::PickFormat | Overlay::Retype { .. } => true,
1996 Overlay::Combine { .. } => self.column_forms.combine.as_ref().is_some_and(|c| {
1997 c.picker.is_some() || c.focus == app::modals::retype_modal::CombineField::Name
1998 }),
1999 Overlay::PickTable => true,
2000 Overlay::Sample => self
2001 .sample
2002 .form
2003 .as_ref()
2004 .is_some_and(|form| form.field.is_text()),
2005 Overlay::SortFilter => self.sort_filter_modal.typing(),
2007 Overlay::PivotMelt => {
2008 self.pivot_melt_modal.picker.is_some()
2009 || self
2010 .pivot_melt_modal
2011 .is_text_row(self.pivot_melt_modal.focus)
2012 }
2013 Overlay::ChartExport => self.chart.export_modal.focus.is_text(),
2014 Overlay::Chart => self.chart.modal.picker.is_some(),
2015 Overlay::Info | Overlay::ValueCounts => false,
2016 Overlay::Hex => self
2018 .hex
2019 .view
2020 .as_ref()
2021 .is_some_and(|view| view.prompt.is_some() || view.picker.is_some()),
2022 }
2023 }
2024
2025 pub fn something_is_spinning(&self) -> bool {
2027 self.is_busy()
2028 || (self.row_count_pending() && !self.awaiting_open_confirmation())
2029 || (self.overlay == Overlay::Analysis && self.cancelled_analysis_running().is_some())
2031 || self.chart_preparing()
2032 || self.value_counts_computing()
2033 || (self.input_mode == InputMode::Home
2034 && (self.home.awaiting_listing().is_some()
2035 || self.home.sections_waiting()
2036 || !self.home.peeking.is_empty()))
2037 }
2038
2039 fn begin_frame(&mut self) {
2042 self.take_in_what_arrived();
2043 self.counting.footers_this_frame = self.footer_progress().reading();
2044 self.counting.listed_this_frame = self.footer_progress().listed();
2045 self.pointer.forget_drawn();
2047 if let Some(state) = self.data_table_state.as_mut() {
2048 state.forget_drawn();
2049 }
2050 }
2051
2052 fn take_in_what_arrived(&mut self) {
2055 self.home.apply_new_measurements();
2058 if self.counting.indexing_paused && self.input_mode != InputMode::Home {
2060 self.index_lines();
2061 }
2062 }
2063
2064 pub fn frame_work(&mut self) {
2071 self.request_what_the_frame_needs();
2072 self.take_in_what_arrived();
2073 self.request_what_the_frame_needs();
2074 }
2075
2076 pub fn footer_progress(&self) -> &Arc<crate::formats::schema_union::FooterProgress> {
2079 self.loading
2080 .progress()
2081 .unwrap_or(&self.counting.footer_progress)
2082 }
2083
2084 pub fn exit_sweep(&self) -> ExitSweep {
2087 ExitSweep(self.loading.unfinished().clone())
2088 }
2089
2090 const INDEXING_FOR_ROW: &'static str = "Reading lines to find the row...";
2092
2093 const COUNTING_FOR_END: &'static str = "Counting rows to find the end...";
2096
2097 const LOOKING: &'static str = "Looking...";
2100
2101 pub const LOOKING_AT_A_DIRECTORY: &'static str = "Looking at the directory";
2104
2105 pub const LOADING_BUFFER: &'static str = "Loading buffer...";
2107
2108 const APPLYING_VIEW: &'static str = "Applying view...";
2110
2111 const READING_GROUP: &'static str = "Reading the group...";
2113
2114 const READING_FIELDS: &'static str = "Reading fields...";
2116
2117 const FIELD_COPY_INLINE_BYTES: usize = 1024 * 1024;
2119 const JSON_COPY_MAX_BYTES: usize = 64 * 1024 * 1024;
2121 const READING_JSON: &'static str = "Reading JSON...";
2123
2124 const COMPUTING_PIVOT: &'static str = "Computing pivot...";
2126
2127 const A_FETCH_WORTH_SAYING: std::time::Duration = std::time::Duration::from_millis(300);
2129
2130 fn load_ahead(&mut self) {
2134 if self.is_busy()
2137 || self.jobs.owed(Self::owed_rows).is_some()
2138 || self.rows_in_flight().is_some()
2139 || self.work_a_bump_would_strand()
2140 {
2141 return;
2142 }
2143 let Some(state) = self.data_table_state.as_ref() else {
2144 return;
2145 };
2146 let position = state.buffer_position();
2149 if !state.wants_to_load_ahead() || self.counting.loaded_ahead_from == Some(position) {
2150 return;
2151 }
2152 self.counting.loaded_ahead_from = Some(position);
2153 self.spawn_collect(None);
2154 }
2155
2156 fn flash_view_applied(&mut self, name: &str, why: view::MatchReason) {
2158 self.flash_note(format!("View \"{name}\" applied: {}", why.as_str()));
2159 }
2160
2161 pub fn spawn_async_collect(&mut self, status: &str) -> bool {
2166 self.spawn_collect(Some(status))
2167 }
2168
2169 fn spawn_collect(&mut self, status: Option<&str>) -> bool {
2172 let held = self
2173 .data_table_state
2174 .as_ref()
2175 .is_some_and(|state| self.count_held_at_estimate(state));
2176 let Some(state) = self.data_table_state.as_mut() else {
2177 return false;
2178 };
2179
2180 let mut count = None;
2186 let generation = state.len_generation();
2187 if !state.is_num_rows_valid()
2188 && self.counting.len_count_inflight != Some(generation)
2189 && !state.counts_itself_later()
2192 && self.counting.len_count_failed != Some(generation)
2194 && !held
2196 {
2197 self.counting.len_count_inflight = Some(generation);
2198 count = Some(LenCount::for_state(state));
2199 }
2200 let footers = count.take_if(|job| job.reads_footers());
2201 if count.take_if(|_| !state.is_remote_source()).is_some() {
2202 self.counting.count_after_paint = Some(generation);
2203 }
2204 if let Some(job) = footers {
2205 self.spawn_count(job);
2206 }
2207
2208 let a_bump_would_strand = self.work_a_bump_would_strand();
2210 let inflight = self.rows_in_flight();
2211
2212 let Some(state) = self.data_table_state.as_mut() else {
2215 return false;
2216 };
2217 let covered = inflight.is_some_and(|inflight| inflight.covers(state));
2218 if covered
2220 && let Some(status) = status
2221 && !self.jobs.waited_on(Self::reading_rows)
2222 {
2223 self.jobs.wait_on(Self::reading_rows, status);
2224 self.busy = false;
2225 self.status_message = Some(status.to_string());
2226 }
2227 let request = (!covered).then(|| state.prepare_async_collect(None));
2228 let Some(Some(request)) = request else {
2229 if let Some(job) = count {
2231 self.spawn_count(job);
2232 }
2233 return covered;
2234 };
2235 if a_bump_would_strand {
2240 if count.is_some() {
2243 self.counting.len_count_inflight = None;
2244 }
2245 let Some(status) = status else {
2247 return false;
2248 };
2249 self.jobs.take_owed(Self::owed_rows);
2251 let owed = Job::OwedRows {
2252 dataset: self.dataset_generation,
2253 status: status.to_string(),
2254 };
2255 self.jobs.owe(owed, Some(status));
2256 self.busy = false;
2257 self.status_message = Some(status.to_string());
2258 return true;
2259 }
2260 self.jobs.advance();
2261 self.home_app.reads.pages += 1;
2262 let inflight = InflightCollect {
2263 began: std::time::Instant::now(),
2264 files: state.files_a_page_reads(
2265 request.buffer_start,
2266 request.buffer_end.saturating_sub(request.buffer_start),
2267 ),
2268 dataset: state.len_generation(),
2269 columns: InflightCollect::columns_of(state),
2270 start: request.buffer_start,
2271 end: request.buffer_end,
2272 };
2273 let count = count.map(|job| OwedCount::new(job, self.events.clone()));
2275 self.spawn_job(Job::Rows(inflight), status, move |_| {
2276 let plan = request.plan;
2277 Ok(
2279 match crate::analysis::statistics::collect_lazy(
2280 request.lf,
2281 request.polars_streaming,
2282 ) {
2283 Ok(df) => {
2284 let returned = df.height();
2285 let requested = request.buffer_end - request.buffer_start;
2286 let start = request.buffer_start;
2287 Answer::Rows(plan.fit(df)).then(move || {
2289 if let Some(count) = count {
2290 count.answer(|job| job.after_collect(start, returned, requested));
2291 }
2292 })
2293 }
2294 Err(e) => Answer::RowsFailed {
2297 message: crate::error_display::user_message_from_polars(&e),
2298 conversion: crate::error_display::conversion_failure(&e).map(Box::new),
2299 }
2300 .then(move || drop(count)),
2301 },
2302 )
2303 });
2304 true
2305 }
2306
2307 fn spawn_job<F, R>(&mut self, job: Job, status: Option<&str>, work: F) -> Ticket
2312 where
2313 F: FnOnce(&app::jobs::Worker) -> std::result::Result<R, String> + Send + 'static,
2314 R: Into<app::jobs::Answered>,
2315 {
2316 let started = self.start_job(job, status);
2317 let ticket = started.ticket();
2318 started.run(&self.runtime, work);
2319 ticket
2320 }
2321
2322 fn start_job(&mut self, job: Job, status: Option<&str>) -> app::jobs::Started {
2325 if let Some(status) = status {
2326 self.busy = false;
2328 self.status_message = Some(status.to_string());
2329 }
2330 self.jobs.start(job, status)
2331 }
2332
2333 fn owed_rows(job: &Job) -> bool {
2334 matches!(job, Job::OwedRows { .. })
2335 }
2336
2337 fn reading_rows(job: &Job) -> bool {
2338 matches!(job, Job::Rows(_))
2339 }
2340
2341 fn rows_in_flight(&self) -> Option<InflightCollect> {
2343 match self.jobs.current(Self::reading_rows) {
2344 Some((_, Job::Rows(inflight))) => Some(*inflight),
2345 _ => None,
2346 }
2347 }
2348
2349 fn rows_waited_on(&self) -> bool {
2351 self.jobs.waited_on(Self::reading_rows)
2352 }
2353
2354 fn forget_the_rows_read(&mut self) {
2357 self.jobs
2358 .supersede(|job| Self::reading_rows(job) || Self::owed_rows(job));
2359 }
2360
2361 pub(crate) fn hold_the_generation(&self) -> app::jobs::Hold {
2364 self.jobs.hold()
2365 }
2366
2367 #[cfg(test)]
2369 pub(crate) fn job_for_tests(&mut self, job: Job, status: Option<&str>) -> app::jobs::Started {
2370 self.start_job(job, status)
2371 }
2372
2373 #[cfg(test)]
2375 pub(crate) fn nothing_loading(&self) -> bool {
2376 self.loading.current().is_none() && self.export_progress.is_none()
2377 }
2378
2379 #[cfg(test)]
2382 pub(crate) fn loading_for_tests(
2383 &mut self,
2384 path: Option<PathBuf>,
2385 size: u64,
2386 phase: &str,
2387 percent: u16,
2388 ) {
2389 self.announce_open(false, phase.to_string(), percent);
2390 if let Some(path) = path {
2391 self.loading.name(path);
2392 }
2393 self.loading.size_for_tests(size);
2394 }
2395
2396 #[cfg(test)]
2399 pub(crate) fn open_for_tests(&mut self, path: &str) -> loading::LoadId {
2400 self.put_down_load_in_flight();
2401 let _ = self.loading.open(loading::OpenRequest {
2402 paths: vec![PathBuf::from(path)],
2403 options: OpenOptions::default(),
2404 size: 0,
2405 recent: None,
2406 shown: None,
2407 warn_in_memory_above: None,
2408 });
2409 self.loading.id().expect("an open was begun")
2410 }
2411
2412 #[cfg(test)]
2415 pub(crate) fn install_for_tests(
2416 &mut self,
2417 state: DataTableState,
2418 path: Option<PathBuf>,
2419 options: &OpenOptions,
2420 debug_label: Option<String>,
2421 ) -> bool {
2422 self.put_down_load_in_flight();
2423 let _ = self
2424 .loading
2425 .open_frame(LazyFrame::default(), options.clone());
2426 let load = self.loading.id().expect("an open was begun");
2427 let loading::Step::Install(loaded) = self.loading.answered(
2428 load,
2429 loading::LoadAnswer::SchemaRead {
2430 state: Box::new(state),
2431 path,
2432 options: options.clone(),
2433 debug_label,
2434 },
2435 #[cfg(any(feature = "http", feature = "cloud"))]
2436 &self.jobs,
2437 ) else {
2438 unreachable!("a schema read installs");
2439 };
2440 let view = self.install_dataset(*loaded);
2441 self.first_rows_settled();
2442 view
2443 }
2444
2445 #[cfg(test)]
2447 pub(crate) fn owe_rows_for_tests(&mut self, status: &str) {
2448 let owed = Job::OwedRows {
2449 dataset: self.dataset_generation,
2450 status: status.to_string(),
2451 };
2452 self.jobs.owe(owed, Some(status));
2453 }
2454
2455 #[cfg(test)]
2457 pub(crate) fn rows_owed(&self) -> bool {
2458 self.jobs.owed(Self::owed_rows).is_some()
2459 }
2460
2461 #[cfg(test)]
2463 pub(crate) fn answer_for_tests(&mut self, job: Job, answer: Answer) -> Option<AppEvent> {
2464 let started = self.jobs.start(job, None);
2465 let ticket = started.ticket();
2466 started.end(Outcome::answered(answer));
2467 self.job_ended(ticket)
2468 }
2469
2470 fn work_a_bump_would_strand(&self) -> bool {
2473 self.jobs.would_strand()
2474 }
2475
2476 fn scroll_key(&mut self, scroll: Scroll) -> Option<AppEvent> {
2479 let state = self.data_table_state.as_mut()?;
2480 if state.scroll_would_trigger_collect(scroll.delta(state)) {
2481 self.busy = true;
2482 return Some(AppEvent::Scroll(scroll));
2483 }
2484 scroll.run(state);
2485 None
2486 }
2487
2488 fn jump_key(&mut self, jump: Scroll) -> Option<AppEvent> {
2491 if jump == Scroll::End
2497 && let Some(state) = self.data_table_state.as_ref()
2498 && state.indexing().is_some()
2499 {
2500 self.counting.end_when_indexed = Some(self.dataset_generation);
2501 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2502 return None;
2503 }
2504 if jump == Scroll::End
2505 && let Some(state) = self.data_table_state.as_ref()
2506 && state.footers_pending().is_some()
2507 && state.scan_is_the_root()
2508 && !state.is_num_rows_valid()
2509 {
2510 self.counting.end_when_the_footers_land = Some(self.dataset_generation);
2511 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2512 return None;
2513 }
2514 if jump == Scroll::End
2517 && let Some(state) = self.data_table_state.as_ref()
2518 && !state.is_num_rows_valid()
2519 {
2520 let generation = state.len_generation();
2521 self.counting.end_after_count = Some(generation);
2522 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2523 let held = self.counting.count_after_paint == Some(generation);
2524 if held {
2525 self.counting.count_after_paint = None;
2526 }
2527 if held || self.counting.len_count_inflight != Some(generation) {
2528 self.counting.len_count_inflight = Some(generation);
2529 self.spawn_count(LenCount::for_state(state));
2530 }
2531 return None;
2532 }
2533 let state = self.data_table_state.as_mut()?;
2534 let already_there = match jump {
2535 Scroll::Start => state.start_row() == 0,
2536 _ => state.at_end(),
2537 };
2538 if already_there {
2539 jump.run(state);
2540 return None;
2541 }
2542 self.busy = true;
2543 Some(AppEvent::Scroll(jump))
2544 }
2545
2546 fn handle_scroll<F>(&mut self, scroll: F) -> Option<AppEvent>
2549 where
2550 F: FnOnce(&mut crate::table::DataTableState) -> bool,
2551 {
2552 let needs = self.data_table_state.as_mut().is_some_and(scroll);
2553 if !needs || !self.spawn_async_collect(Self::LOADING_BUFFER) {
2554 self.busy = false;
2555 self.status_message = None;
2556 }
2557 None
2558 }
2559
2560 pub fn new(events: Sender<AppEvent>, runtime: tokio::runtime::Handle) -> App {
2561 let theme = Theme::from_config(&AppConfig::default().theme).unwrap_or_else(|_| Theme {
2562 colors: std::collections::HashMap::new(),
2563 });
2564
2565 Self::new_with_config(events, runtime, theme, AppConfig::default())
2566 }
2567
2568 pub fn new_with_theme(
2569 events: Sender<AppEvent>,
2570 runtime: tokio::runtime::Handle,
2571 theme: Theme,
2572 ) -> App {
2573 Self::new_with_config(events, runtime, theme, AppConfig::default())
2574 }
2575
2576 pub fn new_with_config(
2578 events: Sender<AppEvent>,
2579 runtime: tokio::runtime::Handle,
2580 theme: Theme,
2581 app_config: AppConfig,
2582 ) -> App {
2583 let views = ViewManager::load_or_empty().into();
2584 Self::new_with_views(events, runtime, theme, app_config, views)
2585 }
2586
2587 pub fn new_with_views(
2589 events: Sender<AppEvent>,
2590 runtime: tokio::runtime::Handle,
2591 theme: Theme,
2592 app_config: AppConfig,
2593 view_manager: Views,
2594 ) -> App {
2595 let cache = CacheManager::new(APP_NAME)
2596 .unwrap_or_else(|_| CacheManager::with_dir(std::env::temp_dir().join(APP_NAME)));
2597 let jobs = Jobs::new(events.clone());
2598 let formats = Arc::new(crate::formats::Registry::load(
2599 &crate::formats::search_path_for(&app_config),
2600 ));
2601 for error in &formats.errors {
2602 log::warn!("format spec skipped: {error}");
2603 }
2604
2605 let theme_problem = app_config.theme.fallbacks.first().cloned();
2606 let chart_export_modal = ChartExportModal {
2607 recipe: app_config.chart.export_recipe,
2608 ..ChartExportModal::new()
2609 };
2610 let mut app = App {
2611 path: None,
2612 data_table_state: None,
2613 counting: loading::counting::Counting::default(),
2614 home: home::HomeState {
2615 hide_unreadable: !app_config.home.show_unreadable,
2616 formats: formats.clone(),
2617 ..Default::default()
2618 },
2619 home_app: home::home_app::HomeApp {
2620 local_desktop: app::link_open::local_desktop(
2621 app::link_open::Platform::current(),
2622 |name| std::env::var(name).ok(),
2623 ),
2624 ..Default::default()
2625 },
2626 source: loading::open_scan::OpenedSource::default(),
2627 pipes: app::run::Pipes::default(),
2628 events,
2629 debug: DebugState::default(),
2630 info_modal: InfoModal::new(),
2631 info: app::keys::info_keys::InfoState {
2632 head_web_rows: !cache::running_as_a_cargo_test(),
2633 ..Default::default()
2634 },
2635 prompt: query::query_prompt::QueryPrompt {
2636 query_input: TextInput::new()
2637 .with_history_limit(app_config.query.history_limit)
2638 .with_theme(&theme)
2639 .with_history("query".to_string()),
2640 sql_input: TextInput::statement()
2641 .with_history_limit(app_config.query.history_limit)
2642 .with_theme(&theme)
2643 .with_history("sql".to_string()),
2644 find: find::Find::new(
2645 TextInput::new()
2646 .with_history_limit(app_config.query.history_limit)
2647 .with_theme(&theme)
2648 .with_history("find".to_string()),
2649 ),
2650 column_hints: false,
2651 input_type: None,
2652 query_mode: QueryMode::default().resolve(),
2653 query_mode_chosen: None,
2654 query_text_restored: false,
2655 sql_columns: Vec::new(),
2656 sql_completion: None,
2657 query_running: None,
2658 query_run_error: None,
2659 inline_failures: 0,
2660 },
2661 input_mode: InputMode::Normal,
2662 overlay: Overlay::None,
2663 sort_filter_modal: SortFilterModal::new(),
2664 pivot_melt_modal: PivotMeltModal::new(),
2665 view_modal: ViewModal::new(),
2666 analysis_modal: AnalysisModal::with_sample_rows(app_config.analysis.sample_rows),
2667 sample: analysis::sample_draw::SampleState {
2668 form: None,
2669 memory_probe: std::sync::Arc::new(analysis::table_sample::available_memory),
2670 paths: Vec::new(),
2671 },
2672 quality: analysis::quality_runs::QualityRuns {
2673 memory_budget: QUALITY_MEMORY_BUDGET,
2674 ..Default::default()
2675 },
2676 chart: chart::chart_jobs::Charts {
2677 export_modal: chart_export_modal,
2678 ..Default::default()
2679 },
2680 export_modal: ExportModal::new(),
2681 copy_modal: app::modals::copy_modal::CopyModal::new(),
2682 inspector_modal: inspector::inspector_modal::InspectorModal::new(),
2683 external: app::run::External::default(),
2684 pickers: app::keys::picker_keys::Pickers::default(),
2685 value_counts: analysis::value_counts_modal::ValueCountsModal::default(),
2686 hex: app::keys::hex_keys::HexState::default(),
2687 column_forms: app::keys::retype_keys::ColumnForms::default(),
2688 error_modal: ErrorModal::new(),
2689 flash: None,
2690 confirmation_modal: ConfirmationModal::new(),
2691 help: app::help::Help::default(),
2692 pointer: app::pointer::Pointing::default(),
2693 context_menu: None,
2694 cache,
2695 cache_writes: CacheWrites::default(),
2696 views: view::view_apply::SavedViews {
2697 manager: view_manager,
2698 active_id: None,
2699 },
2700 export_progress: None,
2701 theme,
2702 display: render::context::DisplaySettings {
2703 history_limit: app_config.query.history_limit,
2704 table_cell_padding: app_config.display.cell_padding.cells(),
2705 column_colors: app_config.display.column_colors,
2706 dtype_row: app_config.display.type_row,
2707 number_format: app_config
2708 .display
2709 .number_format
2710 .resolve(app_config.display.right_align_numbers)
2711 .unwrap_or_else(|_| NumberFormatSettings {
2714 align_numeric_right: app_config.display.right_align_numbers,
2715 ..Default::default()
2716 }),
2717 background_query: false,
2718 repaint: None,
2719 },
2720 jobs,
2721 runtime,
2722 loading: loading::Loader::default(),
2723 dataset_generation: 0,
2724 #[cfg(test)]
2725 file_facts_reader: None,
2726 busy: false,
2727 throbber_frame: 0,
2728 screen_generation: 0,
2729 input_dropped: false,
2730 status_message: None,
2731 app_config,
2732 formats,
2733 };
2734 if let Some(problem) = theme_problem {
2736 app.flash_note(problem);
2737 }
2738 app
2739 }
2740
2741 pub fn set_formats(&mut self, registry: crate::formats::Registry) {
2743 let registry = Arc::new(registry);
2744 self.home.formats = registry.clone();
2745 self.formats = registry;
2746 }
2747
2748 pub fn enable_debug(&mut self) {
2749 self.debug.enabled = true;
2750 }
2751
2752 pub fn request_what_the_frame_needs(&mut self) {
2757 if self.at_table() {
2758 self.load_ahead();
2759 self.catch_up_follow();
2760 }
2761 self.inspector_needs();
2762 if self.input_mode != InputMode::Home {
2763 return;
2764 }
2765 self.take_listing_news();
2766 #[cfg(feature = "http")]
2767 self.size_selected_web_file();
2768 if !self.home.path_input_active {
2771 self.request_selected_preview();
2772 self.request_home_schema();
2773 self.request_home_measurements();
2774 self.request_home_classifications();
2775 #[cfg(feature = "cloud")]
2776 self.peek_cloud_directories();
2777 }
2778 }
2779}
2780
2781impl App {
2782 pub fn help_visible(&self) -> bool {
2784 self.help.is_open()
2785 }
2786
2787 pub fn help_context(&self) -> Option<datui_cli::keys::Context> {
2789 self.help.context()
2790 }
2791
2792 pub(crate) fn open_help_overlay(&mut self) {
2794 if self.help.is_open() || self.confirmation_modal.active || self.error_modal.active {
2796 return;
2797 }
2798 let context = self.keys_context();
2799 let typing = self.text_field_focused()
2801 || (self.input_mode == InputMode::Home
2802 && !self.info.documentation.is_open()
2803 && (!self.home.filter.is_empty() || self.home.path_input_active));
2804 self.help.open(context, typing);
2805 }
2806
2807 fn close_help_left_behind(&mut self) {
2811 let left = self
2812 .help
2813 .context()
2814 .is_some_and(|shown| shown != self.keys_context());
2815 if left || self.confirmation_modal.active || self.error_modal.active {
2816 self.help.close();
2817 }
2818 }
2819
2820 pub fn keys_context(&self) -> datui_cli::keys::Context {
2822 use crate::analysis::analysis_modal::{AnalysisTool, AnalysisView};
2823 use datui_cli::keys::Context;
2824 match self.overlay {
2825 Overlay::Analysis => match self.analysis_modal.view {
2826 AnalysisView::DistributionDetail => Context::DistributionDetail,
2827 AnalysisView::CorrelationDetail => Context::CorrelationDetail,
2828 AnalysisView::Main => match self.analysis_modal.selected_tool {
2829 Some(AnalysisTool::DistributionAnalysis) => Context::Distribution,
2830 Some(AnalysisTool::CorrelationMatrix) => Context::Correlation,
2831 Some(AnalysisTool::DataQuality) => Context::DataQuality,
2832 Some(AnalysisTool::Describe) | None => Context::Describe,
2833 },
2834 },
2835 Overlay::View => Context::Views,
2836 Overlay::None => match self.input_mode {
2837 InputMode::Normal => Context::Table,
2838 InputMode::Editing => match self.prompt.input_type {
2839 Some(InputType::Find) => Context::Find,
2840 _ => Context::Query,
2841 },
2842 InputMode::Home if self.info.documentation.is_open() => Context::Documentation,
2843 InputMode::Home => Context::Home,
2844 },
2845 Overlay::SortFilter => Context::SortFilter,
2846 Overlay::PivotMelt => Context::PivotMelt,
2847 Overlay::Export { .. } => Context::Export,
2848 Overlay::Copy => Context::Copy,
2849 Overlay::Inspect => Context::Inspector,
2850 Overlay::GoToColumn => Context::GoToColumn,
2851 Overlay::PickFormat => Context::FormatPicker,
2852 Overlay::Retype { .. } => Context::Retype,
2853 Overlay::Combine { .. } => Context::Combine,
2854 Overlay::PickTable => Context::TablePicker,
2855 Overlay::Sample => Context::Sample,
2856 Overlay::Info => Context::Info,
2857 Overlay::Chart | Overlay::ChartExport => Context::Chart,
2858 Overlay::Hex => Context::Hex,
2859 Overlay::ValueCounts => Context::ValueCounts,
2860 }
2861 }
2862
2863 pub fn awaiting_open_confirmation(&self) -> bool {
2867 self.confirmation_modal.active
2868 && matches!(self.confirmation_modal.asking, Some(Confirm::Download))
2869 }
2870
2871 fn confirmed(&mut self) -> Option<AppEvent> {
2873 let stop = self.confirmation_modal.focus_yes;
2874 match self.confirmation_modal.take()? {
2875 Confirm::Leave(leaving) => self.leave_recording(leaving, stop),
2876 Confirm::ReadAll => {
2877 let sample = analysis::sampling::Sample {
2879 method: analysis::sampling::SampleMethod::EveryRow,
2880 ..self.analysis_modal.sample.clone()
2881 };
2882 self.apply_sample(sample)
2883 }
2884 Confirm::OpenLink(url) => Some(AppEvent::Applied(Applied::OpenLink(url))),
2885 Confirm::Reopen(asked) => Some(AppEvent::Applied(Applied::Reopen(asked))),
2886 Confirm::ClearRecents => {
2887 self.cache.clear_recents();
2888 self.home_refresh();
2889 self.home.status = None;
2890 None
2891 }
2892 Confirm::QualityFullScan => self.run_quality_setup(true),
2893 Confirm::HideExamples => {
2894 self.cache.hide_examples();
2895 self.home_refresh();
2896 self.home.select_first_entry();
2897 None
2898 }
2899 Confirm::DeleteView(id) => {
2900 if self.views.manager.delete_view(&id).is_ok() {
2901 self.refresh_view_list();
2902 }
2903 None
2904 }
2905 Confirm::ForgetPlace(place) => {
2906 let paths = self.home.recents_in(&place);
2907 self.cache.forget_recents(&paths);
2908 self.home_refresh();
2909 self.home.status = None;
2910 None
2911 }
2912 Confirm::QualityExport(path, format) => Some(AppEvent::Applied(
2914 Applied::QualityReportExport(path, format, Overwrite::Replace),
2915 )),
2916 Confirm::ChartExport(request) => Some(AppEvent::Applied(Applied::ChartExport(
2917 ChartExportRequest {
2918 overwrite: Overwrite::Replace,
2919 ..*request
2920 },
2921 ))),
2922 Confirm::Export(request) => Some(AppEvent::Applied(Applied::Export(ExportRequest {
2923 overwrite: Overwrite::Replace,
2924 ..*request
2925 }))),
2926 Confirm::Copy(format, header) => {
2927 Some(AppEvent::Applied(Applied::CopyTable { format, header }))
2928 }
2929 Confirm::Download => {
2930 let step = self.loading.confirmed();
2933 self.run_load_step(step)
2934 }
2935 }
2936 }
2937
2938 fn declined(&mut self) -> Option<AppEvent> {
2941 match self.confirmation_modal.take() {
2942 Some(Confirm::ChartExport(_)) => self.open_overlay(Overlay::ChartExport),
2943 Some(Confirm::Export(_)) => self.open_over(|returns_to| Overlay::Export { returns_to }),
2944 Some(Confirm::Download) => self.enter_home(),
2946 _ => {}
2947 }
2948 None
2949 }
2950
2951 fn key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
2952 self.debug.on_key(event);
2953
2954 self.flash = None;
2956 self.cancel_drag();
2958
2959 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
2960 if ctrl && event.code == KeyCode::Char('q') {
2963 return Some(AppEvent::Exit);
2964 }
2965 if ctrl && event.code == KeyCode::Char('c') {
2967 return Some(AppEvent::Exit);
2968 }
2969
2970 if !self.menu_showing() {
2974 self.context_menu = None;
2975 }
2976 if let Some(menu) = self.context_menu.as_mut() {
2977 match menu.key(event) {
2978 app::context_menu::MenuKey::Moved => return None,
2979 app::context_menu::MenuKey::Close => {
2980 self.context_menu = None;
2981 return None;
2982 }
2983 app::context_menu::MenuKey::Run(key) => {
2984 self.context_menu = None;
2985 return Some(AppEvent::Press(key));
2986 }
2987 app::context_menu::MenuKey::Do(action) => {
2988 self.context_menu = None;
2989 self.menu_action(action);
2990 return None;
2991 }
2992 app::context_menu::MenuKey::Other => self.context_menu = None,
2993 }
2994 }
2995
2996 if event.code == KeyCode::Esc && self.view_applying() {
2998 self.cancel_view();
2999 return None;
3000 }
3001 if event.code == KeyCode::Esc && self.finding() {
3003 self.cancel_find();
3004 return None;
3005 }
3006 if event.code == KeyCode::Esc
3008 && self.at_table()
3009 && self.in_normal_table_view()
3010 && self.footers_counted().is_some()
3011 {
3012 self.stop_count();
3013 return None;
3014 }
3015
3016 if event.code == KeyCode::Esc
3017 && self.at_table()
3018 && !self.error_modal.active
3019 && !self.confirmation_modal.active
3020 && self.return_from_quality_evidence(true)
3021 {
3022 return None;
3023 }
3024
3025 if event.code == KeyCode::F(1) {
3027 if self.help.is_open() {
3028 self.help.close();
3029 } else {
3030 self.open_help_overlay();
3031 }
3032 return None;
3033 }
3034
3035 if self.input_mode == InputMode::Home
3038 && !self.confirmation_modal.active
3039 && !self.error_modal.active
3040 && !self.help.is_open()
3041 {
3042 return self.home_key(event);
3043 }
3044
3045 if event.code == KeyCode::Char('o')
3047 && event.modifiers.contains(KeyModifiers::CONTROL)
3048 && (!self.confirmation_modal.active || self.awaiting_open_confirmation())
3049 {
3050 self.help.close();
3051 self.enter_home();
3052 return None;
3053 }
3054
3055 if self.confirmation_modal.active {
3056 match event.code {
3057 KeyCode::Left | KeyCode::Char('h') => {
3058 self.confirmation_modal.focus_yes = true;
3059 }
3060 KeyCode::Right | KeyCode::Char('l') => {
3061 self.confirmation_modal.focus_yes = false;
3062 }
3063 KeyCode::Tab => {
3064 self.confirmation_modal.focus_yes = !self.confirmation_modal.focus_yes;
3065 }
3066 KeyCode::Up | KeyCode::Char('k') => {
3068 self.confirmation_modal.scroll =
3069 self.confirmation_modal.scroll.saturating_sub(1);
3070 }
3071 KeyCode::Down | KeyCode::Char('j') => {
3072 self.confirmation_modal.scroll =
3073 self.confirmation_modal.scroll.saturating_add(1);
3074 }
3075 KeyCode::Enter if self.confirmation_modal.focus_yes => {
3076 return self.confirmed();
3077 }
3078 KeyCode::Enter => {
3079 if matches!(self.confirmation_modal.asking, Some(Confirm::Leave(_))) {
3081 return self.confirmed();
3082 }
3083 return self.declined();
3084 }
3085 KeyCode::Esc => {
3086 if matches!(self.confirmation_modal.asking, Some(Confirm::Leave(_))) {
3088 self.confirmation_modal.hide();
3089 return None;
3090 }
3091 return self.declined();
3092 }
3093 _ => {}
3094 }
3095 return None;
3096 }
3097 if self.error_modal.active {
3098 match event.code {
3099 KeyCode::Up | KeyCode::Char('k') => {
3101 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(1);
3102 return None;
3103 }
3104 KeyCode::Down | KeyCode::Char('j') => {
3105 self.error_modal.scroll = self.error_modal.scroll.saturating_add(1);
3106 return None;
3107 }
3108 KeyCode::PageUp => {
3109 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(8);
3110 return None;
3111 }
3112 KeyCode::PageDown => {
3113 self.error_modal.scroll = self.error_modal.scroll.saturating_add(8);
3114 return None;
3115 }
3116 KeyCode::Esc | KeyCode::Enter => {
3117 self.error_modal.hide();
3118 if self.data_table_state.is_none() {
3121 let reason = self.home_app.last_load_error.take();
3122 self.enter_home();
3123 self.home.status = reason;
3124 }
3125 }
3126 _ => {}
3127 }
3128 return None;
3129 }
3130
3131 let in_main_table = self.at_table() && !self.help.is_open();
3134 if in_main_table && event.is_press() {
3136 self.prompt.column_hints = Self::column_cursor_key(event).is_some()
3137 || (self.prompt.column_hints
3138 && matches!(
3139 event.code,
3140 KeyCode::Char(
3141 '+' | '-' | 'F' | '[' | ']' | 'H' | 'L' | '<' | '>' | '=' | 'w'
3142 )
3143 ));
3144 }
3145 if in_main_table
3146 && let Some(mv) = Self::column_cursor_key(event)
3147 && let Some(state) = self.data_table_state.as_mut()
3148 {
3149 state.move_cursor(mv);
3150 self.debug.action(|| format!("move_cursor({mv:?})"));
3151 return None;
3152 }
3153
3154 self.close_help_left_behind();
3157 if self.help.is_open() {
3158 return match self.help.key(event) {
3159 app::help::HelpKey::Press(key) => Some(AppEvent::Press(key)),
3160 app::help::HelpKey::Stay | app::help::HelpKey::Closed => None,
3161 };
3162 }
3163
3164 if event.code == KeyCode::Char('?') {
3165 let ctrl_help = event.modifiers.contains(KeyModifiers::CONTROL);
3166 let in_text_input = self.text_field_focused() || self.input_mode == InputMode::Home;
3168 if ctrl_help || !in_text_input {
3170 self.open_help_overlay();
3171 return None;
3172 }
3173 }
3174
3175 match self.overlay {
3176 Overlay::None => {}
3177 Overlay::SortFilter => return self.sort_filter_key(event),
3178 Overlay::Export { .. } => return self.export_key(event),
3179 Overlay::Sample => return self.table_sample_form_key(event),
3180 Overlay::Inspect => return self.inspector_key(event),
3181 Overlay::ValueCounts => return self.value_counts_key(event),
3182 Overlay::Hex => return self.hex_key(event),
3183 Overlay::GoToColumn => {
3184 self.go_to_column_key(event);
3185 return None;
3186 }
3187 Overlay::PickFormat => return self.format_picker_key(event),
3188 Overlay::Retype { .. } => return self.retype_key(event),
3189 Overlay::Combine { .. } => return self.combine_key(event),
3190 Overlay::PickTable => return self.table_picker_key(event),
3191 Overlay::Copy => return self.copy_key(event),
3192 Overlay::PivotMelt => return self.pivot_melt_key(event),
3193 Overlay::Info => return self.info_key(event),
3194 Overlay::Chart | Overlay::ChartExport => return self.chart_key(event),
3195 Overlay::Analysis => return self.analysis_key(event),
3196 Overlay::View => return self.view_key(event),
3197 }
3198
3199 if self.input_mode == InputMode::Editing {
3200 return self.editing_key(event);
3201 }
3202
3203 const RIGHT_KEYS: [KeyCode; 2] = [KeyCode::Right, KeyCode::Char('l')];
3204
3205 const LEFT_KEYS: [KeyCode; 2] = [KeyCode::Left, KeyCode::Char('h')];
3206
3207 const DOWN_KEYS: [KeyCode; 2] = [KeyCode::Down, KeyCode::Char('j')];
3208
3209 const UP_KEYS: [KeyCode; 2] = [KeyCode::Up, KeyCode::Char('k')];
3210
3211 if event
3214 .modifiers
3215 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
3216 && !matches!(event.code, KeyCode::Char('f' | 'b' | 'd' | 'u'))
3217 {
3218 return None;
3219 }
3220
3221 match event.code {
3222 KeyCode::Char('q') => {
3225 if self.source.opened_from_home {
3226 self.enter_home();
3227 None
3228 } else {
3229 Some(AppEvent::Exit)
3230 }
3231 }
3232 KeyCode::Char('Q') => Some(AppEvent::Exit),
3233 KeyCode::Char('R') => Some(AppEvent::Reset),
3234 KeyCode::Char('H' | 'L') if event.is_press() => {
3235 self.move_cursor_column(event.code == KeyCode::Char('L'))
3236 }
3237 KeyCode::Char('+' | '-') if event.is_press() => {
3238 self.quick_filter(event.code == KeyCode::Char('+'))
3239 }
3240 KeyCode::Char('#') => {
3241 let renumbered = self
3242 .data_table_state
3243 .as_mut()
3244 .is_some_and(|state| state.deferred(|s| s.toggle_row_numbers()));
3245 if renumbered {
3246 self.spawn_async_collect(Self::LOADING_BUFFER);
3247 }
3248 None
3249 }
3250 KeyCode::Char('<' | '>' | '=' | 'w')
3252 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
3253 {
3254 if let Some(state) = self.data_table_state.as_mut()
3255 && let Some(name) = state.current_column().map(str::to_string)
3256 {
3257 let (choice, shown) = (state.width_choice(&name), state.on_screen_width(&name));
3260 let width = match event.code {
3261 KeyCode::Char('<') => choice.narrower(shown),
3262 KeyCode::Char('>') => choice.wider(shown),
3263 KeyCode::Char('=') => WidthChoice::Fit,
3264 _ => WidthChoice::Auto,
3265 };
3266 state.set_width_choices([(name, width)]);
3267 }
3268 None
3269 }
3270 KeyCode::Char('/' | 'f')
3272 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
3273 {
3274 self.open_find();
3275 None
3276 }
3277 KeyCode::Char('[' | ']') if event.is_press() => {
3278 self.sort_by_cursor_column(event.code == KeyCode::Char(']'))
3279 }
3280 KeyCode::Char('n') if event.is_press() => {
3281 self.find_again(find::Direction::Next);
3282 None
3283 }
3284 KeyCode::Char('N') if event.is_press() => {
3285 self.find_again(find::Direction::Previous);
3286 None
3287 }
3288 KeyCode::Char('D') => {
3289 self.display.dtype_row = !self.display.dtype_row;
3291 let on = if self.display.dtype_row { "on" } else { "off" };
3292 self.debug.action(|| format!("toggle_dtype_row({on})"));
3293 None
3294 }
3295 KeyCode::Char('F') => {
3296 self.open_value_counts();
3297 None
3298 }
3299 KeyCode::Char(',') => {
3300 self.display.number_format.enabled = !self.display.number_format.enabled;
3302 let on = if self.display.number_format.enabled {
3303 "on"
3304 } else {
3305 "off"
3306 };
3307 self.debug.action(|| format!("toggle_number_format({on})"));
3308 None
3309 }
3310 KeyCode::Esc => {
3311 if self.find_shown() {
3313 self.prompt.find.active = None;
3314 return None;
3315 }
3316 if self.sample_drawing() {
3318 self.stop_sample_draw();
3319 return None;
3320 }
3321 let mut from_counts = false;
3322 let drilled_up = if let Some(ref mut state) = self.data_table_state {
3323 if state.is_drilled_down() {
3324 from_counts = state.drilled_into_value();
3325 let _ = state.deferred(|s| s.drill_up());
3326 true
3327 } else {
3328 false
3329 }
3330 } else {
3331 false
3332 };
3333 if drilled_up {
3334 self.sync_sort_filter_modal();
3335 }
3336 if from_counts
3338 && std::mem::take(&mut self.value_counts.drill_return)
3339 && let Some(state) = self.data_table_state.as_ref()
3340 {
3341 self.value_counts.rebase(state.len_generation());
3342 self.open_overlay(Overlay::ValueCounts);
3343 }
3344 if drilled_up {
3345 self.spawn_async_collect(Self::LOADING_BUFFER);
3346 return None;
3347 }
3348 if let Some(state) = self.data_table_state.as_mut()
3350 && state.follow().is_some()
3351 {
3352 state.stop_following();
3353 self.flash_note("Stopped following".to_string());
3354 }
3355 None
3357 }
3358 KeyCode::Char('t') if event.is_press() => self.toggle_follow(),
3359 code if RIGHT_KEYS.contains(&code) || LEFT_KEYS.contains(&code) => {
3360 if let Some(ref mut state) = self.data_table_state {
3361 state.move_cursor(if RIGHT_KEYS.contains(&code) {
3362 crate::widgets::column_paging::CursorMove::Right
3363 } else {
3364 crate::widgets::column_paging::CursorMove::Left
3365 });
3366 }
3367 None
3368 }
3369 code if event.is_press() && DOWN_KEYS.contains(&code) => self.scroll_key(Scroll::Next),
3370 code if event.is_press() && UP_KEYS.contains(&code) => self.scroll_key(Scroll::Prev),
3371 KeyCode::PageDown if event.is_press() => self.scroll_key(Scroll::PageDown),
3372 KeyCode::Home if event.is_press() => self.jump_key(Scroll::Start),
3373 KeyCode::End | KeyCode::Char('G') if event.is_press() => self.jump_key(Scroll::End),
3374 KeyCode::Char('f')
3375 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3376 {
3377 self.scroll_key(Scroll::PageDown)
3378 }
3379 KeyCode::Char('b')
3380 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3381 {
3382 self.scroll_key(Scroll::PageUp)
3383 }
3384 KeyCode::Char('d')
3385 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3386 {
3387 self.scroll_key(Scroll::HalfDown)
3388 }
3389 KeyCode::Char('u')
3390 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3391 {
3392 self.scroll_key(Scroll::HalfUp)
3393 }
3394 KeyCode::PageUp if event.is_press() => self.scroll_key(Scroll::PageUp),
3395 KeyCode::Enter if event.is_press() => {
3396 if !self.at_table() {
3397 return None;
3398 }
3399 if self.enter_inspects() {
3401 self.open_inspector();
3402 return None;
3403 }
3404 self.drill_selected_row();
3405 None
3406 }
3407 KeyCode::Char('i') if event.is_press() => {
3408 if let Some(state) = self.data_table_state.as_mut() {
3409 let unseen = state.notes_unseen();
3412 state.mark_notes_seen();
3413 if unseen {
3414 self.info_modal
3415 .open_on(crate::widgets::info::InfoTab::Notes);
3416 } else if state.format_detail().is_some_and(|d| d.first) {
3417 self.info_modal
3420 .open_on(crate::widgets::info::InfoTab::Format);
3421 } else {
3422 self.info_modal.open();
3423 }
3424 if let Some(detail) = state.format_detail()
3426 && let Some(at) = detail.list.iter().position(|(key, _)| {
3427 detail.table.as_ref() == Some(key) && detail.tables.contains(key)
3428 })
3429 {
3430 self.info_modal.detail_selected = at;
3431 }
3432 self.open_overlay(Overlay::Info);
3433 self.read_file_facts();
3434 self.count_unfit();
3435 }
3436 None
3437 }
3438 KeyCode::Char(':') if event.is_press() => {
3439 self.open_command_line();
3440 None
3441 }
3442 KeyCode::Char('V') => {
3443 if let Some(ref state) = self.data_table_state
3446 && let Some(dataset) = self.view_dataset()
3447 {
3448 match self
3449 .views
3450 .manager
3451 .get_most_relevant(dataset, state.source_schema())
3452 {
3453 Some((view, why)) => {
3454 if let Err(e) = self.apply_matched_view(&view, why) {
3455 self.error_modal.show(format!("Error applying view: {}", e));
3456 }
3457 }
3458 None => self.open_view_list(),
3459 }
3460 }
3461 None
3462 }
3463 KeyCode::Char('v') => {
3464 self.open_view_list();
3465 None
3466 }
3467 KeyCode::Char('S') => {
3468 if self.at_table() {
3469 self.open_table_sample_form();
3470 }
3471 None
3472 }
3473 KeyCode::Char('s') => {
3474 if self.data_table_state.is_some() {
3475 self.sync_sort_filter_modal();
3478 let current = self
3479 .data_table_state
3480 .as_ref()
3481 .and_then(|state| state.current_column())
3482 .map(str::to_string);
3483 self.sort_filter_modal.open(
3484 self.display.history_limit,
3485 &self.theme,
3486 current.as_deref(),
3487 );
3488 self.open_overlay(Overlay::SortFilter);
3489 }
3490 None
3491 }
3492 KeyCode::Char('r') => {
3493 if let Some(state) = &mut self.data_table_state {
3494 state.deferred(DataTableState::reverse);
3495 self.spawn_async_collect("Sorting...");
3496 }
3497 None
3498 }
3499 KeyCode::Char('a') => {
3500 if self.data_table_state.is_some()
3502 && self.at_table()
3503 && self.quality.evidence_return.is_none()
3504 {
3505 let view = self.data_table_state.as_ref().map(|s| s.len_generation());
3507 self.analysis_modal.open(view);
3508 self.open_overlay(Overlay::Analysis);
3509 if self.analysis_modal.sample_dataset != Some(self.dataset_generation) {
3512 self.analysis_modal.sample.scope =
3513 analysis::data_quality::QualityScope::CurrentView;
3514 self.analysis_modal.sample_dataset = Some(self.dataset_generation);
3515 }
3516 let sampled = self
3518 .data_table_state
3519 .as_ref()
3520 .is_some_and(|state| state.sampled().is_some());
3521 self.analysis_modal.follow_view_sample(sampled);
3522 self.sync_quality_plan();
3523 }
3524 None
3525 }
3526 KeyCode::Char('c') => {
3527 if let Some(state) = &self.data_table_state
3528 && self.at_table()
3529 {
3530 let numeric_columns: Vec<String> = state
3531 .schema()
3532 .iter()
3533 .filter(|(_, dtype)| dtype.is_numeric())
3534 .map(|(name, _)| name.to_string())
3535 .collect();
3536 let datetime_columns: Vec<String> = state
3537 .schema()
3538 .iter()
3539 .filter(|(_, dtype)| {
3540 matches!(
3541 dtype,
3542 DataType::Datetime(_, _) | DataType::Date | DataType::Time
3543 )
3544 })
3545 .map(|(name, _)| name.to_string())
3546 .collect();
3547 let category_columns: Vec<String> = state
3548 .schema()
3549 .iter()
3550 .filter(|(_, dtype)| chart::chart_data::is_category_dtype(dtype))
3551 .map(|(name, _)| name.to_string())
3552 .collect();
3553 let cursor = state.current_column().and_then(|name| {
3555 let dtype = state.schema().get(name)?.clone();
3556 Some((name.to_string(), dtype))
3557 });
3558 let bucketable_columns: Vec<String> = state
3560 .schema()
3561 .iter()
3562 .filter(|(_, dtype)| {
3563 matches!(dtype, DataType::Datetime(_, _) | DataType::Date)
3564 })
3565 .map(|(name, _)| name.to_string())
3566 .collect();
3567 self.chart.modal.series_cap = Some(self.theme.series_colors().len());
3568 self.chart.modal.row_order = self.view_state().sort;
3569 let sampled = state.sampled().is_some();
3570 self.chart.modal.open(
3571 ChartColumns {
3572 numeric: &numeric_columns,
3573 datetime: &datetime_columns,
3574 bucketable: &bucketable_columns,
3575 category: &category_columns,
3576 },
3577 cursor.as_ref().map(|(name, dtype)| (name.as_str(), dtype)),
3578 Some(self.app_config.analysis.chart_rows),
3579 self.app_config.analysis.chart_grid,
3580 self.dataset_generation,
3581 );
3582 if sampled {
3584 self.chart.modal.row_limit = None;
3585 } else if self.chart.modal.view_sampled {
3586 self.chart.modal.row_limit = Some(self.chart.modal.sample_rows);
3587 }
3588 self.chart.modal.view_sampled = sampled;
3589 self.chart.cache.clear();
3590 self.open_overlay(Overlay::Chart);
3591 }
3592 None
3593 }
3594 KeyCode::Char('p') => {
3595 if self.data_table_state.is_some() && self.at_table() {
3596 self.open_pivot_builder();
3597 }
3598 None
3599 }
3600 KeyCode::Char('e') => {
3601 if self.data_table_state.is_some() && self.at_table() {
3602 self.export_modal.open(
3603 self.source.original_file_format,
3604 self.display.history_limit,
3605 &self.theme,
3606 self.source.original_file_delimiter,
3607 );
3608 let stem = self.dataset_stem();
3610 self.export_modal.suggest_path(&format!("{stem}-export"));
3611 if let Some(state) = self.data_table_state.as_ref() {
3612 self.export_modal.offer_source_file = state.can_name_source_files();
3613 self.export_modal.nested_columns = state
3614 .get_column_order()
3615 .iter()
3616 .filter_map(|name| state.schema().get(name))
3617 .any(crate::export::nested_json::is_nested);
3618 self.export_modal.avro_renames =
3619 state.get_column_order().iter().any(|name| {
3620 state.schema().get(name).is_some_and(|dtype| {
3621 crate::export::avro_types::renames(name, dtype)
3622 })
3623 });
3624 }
3625 self.open_over(|returns_to| Overlay::Export { returns_to });
3626 }
3627 None
3628 }
3629 KeyCode::Char(' ') if event.is_press() => {
3630 if self.at_table() {
3631 self.open_inspector();
3632 }
3633 None
3634 }
3635 KeyCode::Char('g') if event.is_press() => {
3636 if self.at_table() {
3637 self.open_go_to_column();
3638 }
3639 None
3640 }
3641 KeyCode::Char('b') if event.is_press() => {
3642 if self.at_table() {
3643 self.open_format_picker();
3644 }
3645 None
3646 }
3647 KeyCode::Char('T') if event.is_press() => {
3648 if self.at_table() {
3649 self.open_table_picker();
3650 }
3651 None
3652 }
3653 KeyCode::Char('y') => {
3654 if self.at_table()
3655 && let Some(state) = self.data_table_state.as_ref()
3656 {
3657 let columns = state.get_column_order().to_vec();
3658 let context = app::modals::copy_modal::CopyContext {
3659 row_number: state.selected_display_row().unwrap_or(0),
3660 view_rows: state.copy_view_df().map(|d| d.height()).unwrap_or(0),
3661 view_cols: columns.len(),
3662 total_rows: state.num_rows_if_valid(),
3663 };
3664 let current = state.current_column().map(str::to_string);
3665 self.copy_modal.open(columns, current.as_deref(), context);
3666 self.open_overlay(Overlay::Copy);
3667 }
3668 None
3669 }
3670 _ => None,
3671 }
3672 }
3673
3674 pub fn handle(&mut self, event: AppEvent) -> EventOutcome {
3679 let started = std::time::Instant::now();
3680 let outcome = self.handle_event(event);
3681 self.debug.times.handler(started.elapsed());
3682 outcome
3683 }
3684
3685 fn handle_event(&mut self, event: AppEvent) -> EventOutcome {
3686 if let AppEvent::Press(key) = event {
3688 return self.handle_event(AppEvent::Key(key));
3689 }
3690 if let AppEvent::Key(key) = event
3691 && self.is_busy()
3692 && !self.key_acts_while_busy(&key)
3693 {
3694 return Err(key);
3695 }
3696 let out = self.dispatch_event(event);
3697 if out.is_none() {
3701 self.let_waiting_errands_in();
3702 }
3703 self.ensure_chart_data();
3704 self.home_score_search();
3705 self.refresh_stale_live_matches();
3707 Ok(out)
3708 }
3709
3710 pub(crate) fn let_waiting_errands_in(&mut self) {
3713 if self.join_held_footers() {
3716 self.reread_after_the_footers_joined();
3717 }
3718 self.join_followed_fields();
3719 self.describe_ended_journal();
3720 self.reread_when_the_work_allows();
3722 self.collect_when_the_work_allows();
3723 }
3724
3725 pub fn event(&mut self, event: AppEvent) -> Option<AppEvent> {
3726 self.handle(event).unwrap_or(None)
3727 }
3728
3729 fn dispatch_event(&mut self, event: AppEvent) -> Option<AppEvent> {
3730 self.debug.num_events += 1;
3731
3732 match event {
3733 AppEvent::Key(key) => {
3734 if let Some(leaving) = self.leaving_by(&key)
3736 && !self.confirmation_modal.active
3737 && self.recording().is_some_and(|spool| spool.live())
3738 {
3739 self.ask_about_recording(leaving);
3740 return None;
3741 }
3742 self.key(&key)
3743 }
3744 AppEvent::Open(paths, options) => {
3745 if paths.is_empty() {
3746 return Some(AppEvent::Crash("No paths provided".to_string()));
3747 }
3748 if self.input_mode == InputMode::Home {
3751 self.source.opened_from_home = true;
3752 }
3753 #[cfg(feature = "cloud")]
3755 let expanded = match paths
3756 .iter()
3757 .map(|p| {
3758 crate::cloud::cloud_sources::expand_azure_short_url(
3759 p,
3760 &self.app_config.cloud,
3761 self.home.browsing.as_deref(),
3762 )
3763 })
3764 .collect::<std::result::Result<Vec<_>, _>>()
3765 {
3766 Ok(expanded) => expanded,
3767 Err(message) => return Some(AppEvent::Crash(message)),
3768 };
3769 #[cfg(feature = "cloud")]
3770 if expanded != paths {
3771 return Some(AppEvent::Open(expanded, options));
3772 }
3773 let mut request = loading::OpenRequest::named(paths, options, &self.formats);
3776 request.warn_in_memory_above = self.app_config.read.memory_warning();
3777 self.begin_new_dataset();
3778 let step = self.loading.open(request);
3779 self.run_load_step(step)
3780 }
3781 AppEvent::OpenLazyFrame(lf, options) => {
3782 self.begin_new_dataset();
3783 let step = self.loading.open_frame(*lf, options);
3784 self.run_load_step(step)
3785 }
3786 AppEvent::HomeListingReady { .. }
3787 | AppEvent::HomeListingFailed
3788 | AppEvent::HomeMeasured { .. }
3789 | AppEvent::HomeSized { .. }
3790 | AppEvent::HomeWebGone { .. }
3791 | AppEvent::HomeClassified { .. }
3792 | AppEvent::HomePathListed { .. }
3793 | AppEvent::HomePathCompleted { .. }
3794 | AppEvent::HomePreviewReady { .. }
3795 | AppEvent::HomeSchemaReady { .. }
3796 | AppEvent::HomeSearchBatch { .. }
3797 | AppEvent::HomeSearchScored { .. }
3798 | AppEvent::HomeSearchDone { .. }
3799 | AppEvent::HomeNarrowed { .. }
3800 | AppEvent::HomeProbeCancelled { .. }
3801 | AppEvent::HomeProbeFailed { .. }
3802 | AppEvent::HomeProbeProgress { .. }
3803 | AppEvent::HomeProbeReady { .. }
3804 | AppEvent::HomeCloudKinds { .. } => self.home_event(event),
3805 #[cfg(feature = "cloud")]
3806 AppEvent::HomeCloudSources { .. } | AppEvent::HomeCloudListed { .. } => {
3807 self.home_event(event)
3808 }
3809 AppEvent::Resize(_cols, _rows) => {
3810 self.display.repaint = Some(render::context::Repaint::Whole);
3812 None
3813 }
3814 AppEvent::Collect => {
3815 self.spawn_async_collect(Self::LOADING_BUFFER);
3816 None
3817 }
3818 AppEvent::Scroll(scroll) => self.handle_scroll(|s| scroll.run(s)),
3819 AppEvent::AnalysisCompute(tool) => self.spawn_analysis(tool),
3820 AppEvent::BackgroundLenReady { .. }
3821 | AppEvent::FramePainted
3822 | AppEvent::BackgroundLenFailed { .. } => self.counting_event(event),
3823 AppEvent::BackgroundQualitySampleKept { kept } => {
3824 self.retain_quality_sample(&kept);
3825 None
3826 }
3827 AppEvent::BackgroundQualityCopyKept {
3828 dataset_generation,
3829 copy,
3830 } => {
3831 self.retain_quality_copy(dataset_generation, copy);
3832 None
3833 }
3834 AppEvent::OpenNamed(paths, options) => {
3835 if let Some(event) = Self::route_named_without_looking(&paths, &options) {
3836 return Some(event);
3837 }
3838 let formats = self.formats.clone();
3839 self.put_down_load_in_flight();
3842 let load = self.loading.look_at_paths();
3843 self.spawn_job(Job::OpenNamed(load), Some("Scanning input..."), move |_| {
3844 if let Some(missing) = Self::missing_named_path(&paths, &formats) {
3845 return Ok(Answer::NamedPathMissing(missing));
3846 }
3847 let (paths, options, directory) =
3848 match Self::route_named_paths_with(paths, options, &formats) {
3849 AppEvent::LookThenOpenDirectory(dir, options) => {
3850 (Vec::new(), options, Some(dir))
3851 }
3852 AppEvent::Open(paths, options) => (paths, options, None),
3853 _ => unreachable!("a named path is opened or looked at"),
3854 };
3855 Ok(Answer::NamedPaths {
3856 paths,
3857 options: Box::new(options),
3858 directory,
3859 })
3860 });
3861 None
3862 }
3863 AppEvent::LookThenOpenDirectory(looking, options) => {
3864 self.put_down_load_in_flight();
3867 let load = self.loading.look_at_directory(looking.clone());
3868 self.jobs
3870 .supersede(|job| matches!(job, Job::LookAtDirectory { .. }));
3871 #[cfg(feature = "cloud")]
3875 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
3876 let job = Job::LookAtDirectory {
3877 load,
3878 path: looking.clone(),
3879 };
3880 self.spawn_job(job, Some(Self::LOOKING_AT_A_DIRECTORY), move |_| {
3881 #[cfg(feature = "cloud")]
3882 if home::is_object_store_url(&looking) {
3883 let url = looking.to_string_lossy().into_owned();
3884 let peeked = wait_on_runtime(&runtime, async move {
3885 crate::cloud::cloud_browse::peek_kind(&url, &cloud).await
3886 })
3887 .and_then(Result::ok);
3888 let (kind, holds) = match peeked {
3889 Some((kind, holds)) => (kind, Some(Box::new(holds))),
3890 None => (home::discover::EntryKind::Unknown, None),
3891 };
3892 return Ok(Answer::LookedAt {
3893 kind,
3894 holds,
3895 options: Box::new(options),
3896 });
3897 }
3898 let as_read = Self::read_as(&options);
3902 let looked = logging::catch_panic(|| {
3903 let mut entry = home::discover::Entry::directory(&looking);
3904 entry.kind = home::discover::EntryKind::Unknown;
3905 home::look_into_as(&entry, &as_read)
3906 });
3907 let kind = match looked {
3908 Ok(entry) => entry.kind,
3909 Err(_) => home::discover::EntryKind::Directory,
3910 };
3911 Ok(Answer::LookedAt {
3912 kind,
3913 holds: None,
3914 options: Box::new(options),
3915 })
3916 });
3917 None
3918 }
3919 AppEvent::ClassifyThenOpen {
3920 path: looking,
3921 typed,
3922 } => {
3923 self.jobs.supersede(|job| matches!(job, Job::Classify(_)));
3926 let look = Job::Classify(app::jobs::Classify {
3927 path: looking.clone(),
3928 browsing: self.home.browsing.clone(),
3929 typed,
3930 });
3931 let name = looking
3932 .file_name()
3933 .map(|n| n.to_string_lossy().into_owned())
3934 .unwrap_or_else(|| looking.display().to_string());
3935 self.home.status = Some(format!("Looking at {name}..."));
3937 let formats = self.formats.clone();
3938 self.spawn_job(look, Some(Self::LOOKING), move |_| {
3939 let found = if looking.is_dir() {
3941 Some(crate::home::discover::classify_directory(&looking))
3942 } else if looking.exists()
3943 || crate::formats::members::split(&looking).is_some()
3945 || crate::formats::members::split_variant(&looking, &formats).is_some()
3946 {
3947 Some(crate::home::discover::EntryKind::File)
3948 } else {
3949 None
3950 };
3951 Ok(Answer::Kind(found))
3952 });
3953 None
3954 }
3955 AppEvent::JobEnded(ticket) => self.job_ended(ticket),
3956 AppEvent::Applied(applied) => self.apply(applied),
3957 AppEvent::JobProgress { ticket, progress } => {
3958 self.job_progress(ticket, &progress);
3959 None
3960 }
3961 AppEvent::Reset => {
3962 if self
3964 .data_table_state
3965 .as_ref()
3966 .is_some_and(|state| state.sampled().is_some())
3967 {
3968 self.put_down_sample_draw();
3969 if let Some(state) = self.data_table_state.take() {
3970 self.data_table_state = Some(state.into_unsampled());
3971 }
3972 self.sample_changed();
3973 }
3974 if let Some(state) = &mut self.data_table_state {
3975 state.deferred(|s| s.reset());
3976 }
3977 self.spawn_async_collect(Self::LOADING_BUFFER);
3978 self.views.active_id = None;
3979 None
3980 }
3981 AppEvent::Followed(news) => {
3982 self.followed(&news);
3983 None
3984 }
3985 AppEvent::Paste(text) => self.paste(&text),
3986 AppEvent::TerminalBackground(mode) => {
3987 self.terminal_answered(mode);
3988 None
3989 }
3990 AppEvent::TerminalFocused => {
3991 self.display.background_query |= self.app_config.theme.follow;
3992 self.display.repaint = self
3993 .display
3994 .repaint
3995 .max(Some(render::context::Repaint::BeforeMoving));
3996 None
3997 }
3998 AppEvent::Press(_)
4001 | AppEvent::Terminal(_)
4002 | AppEvent::Wake
4003 | AppEvent::SettingsRead(_)
4004 | AppEvent::NamedPathMissing(_)
4005 | AppEvent::Exit
4006 | AppEvent::Crash(_)
4007 | AppEvent::Update => None,
4008 }
4009 }
4010
4011 fn filter_problem(&self) -> Option<String> {
4014 let schema = self.data_table_state.as_ref()?.schema();
4015 self.sort_filter_modal
4016 .filter
4017 .statements
4018 .iter()
4019 .find_map(|f| {
4020 crate::export::python_script::SidebarFilter::problem(f, schema.get(&f.column))
4021 })
4022 }
4023
4024 pub fn header_toggle_offered(&self) -> bool {
4027 self.source
4028 .opened
4029 .as_ref()
4030 .and_then(|(_, options)| options.format)
4031 .and_then(FileFormat::separator)
4032 .is_some()
4033 }
4034
4035 pub(crate) fn toggle_header(&mut self) -> Option<AppEvent> {
4038 if !self.header_toggle_offered() {
4039 return None;
4040 }
4041 let (paths, options) = self.source.opened.clone()?;
4042 let options = OpenOptions {
4043 has_header: Some(!options.has_header.unwrap_or(true)),
4044 ..options
4045 };
4046 self.set_loading_phase("Scanning input", 10);
4047 self.name_what_is_loading(paths[0].clone());
4048 Some(AppEvent::Open(paths, options))
4049 }
4050
4051 fn move_cursor_column(&mut self, right: bool) -> Option<AppEvent> {
4055 let state = self.data_table_state.as_ref()?;
4056 let at = state.current_column_index()?;
4057 let mut order = state.headers();
4058 let locked = state.locked_columns_count().min(order.len());
4059 let to = if right { at + 1 } else { at.checked_sub(1)? };
4060 if to >= order.len() || (at < locked) != (to < locked) {
4061 return None;
4062 }
4063 let moving = order[at].clone();
4065 self.data_table_state.as_mut()?.set_current_column(&moving);
4066 order.swap(at, to);
4067 let applied = &mut self.sort_filter_modal.sort.applied_order;
4070 if let (Some(i), Some(j)) = (
4071 applied.iter().position(|c| *c == order[at]),
4072 applied.iter().position(|c| *c == order[to]),
4073 ) {
4074 applied.swap(i, j);
4075 }
4076 Some(AppEvent::Applied(Applied::ColumnOrder(order, locked)))
4077 }
4078
4079 fn sort_by_cursor_column(&mut self, descending: bool) -> Option<AppEvent> {
4082 let state = self.data_table_state.as_ref()?;
4083 let column = state.current_column()?.to_string();
4084 let already = state.view_sort_columns() == std::slice::from_ref(&column)
4085 && state.view_sort_descending() == [descending];
4086 if already {
4087 if let Some(state) = self.data_table_state.as_mut() {
4090 state.deferred(|s| s.sort(Vec::new(), true));
4091 }
4092 self.spawn_async_collect("Sorting...");
4093 return None;
4094 }
4095 Some(AppEvent::Applied(Applied::Sort(
4096 vec![column],
4097 vec![descending],
4098 )))
4099 }
4100}
4101
4102impl App {
4103 fn quick_filter(&mut self, keep: bool) -> Option<AppEvent> {
4107 let state = self.data_table_state.as_ref()?;
4108 let column = state.current_column()?.to_string();
4109 let row = state.copy_row_df()?;
4110 let series = row.column(&column).ok()?.as_materialized_series().clone();
4111 let value = series.get(0).ok()?;
4112 let dtype = state.schema().get(&column)?.clone();
4114 let (operator, text) = if value.is_null() {
4115 let operator = if keep {
4116 FilterOperator::IsNull
4117 } else {
4118 FilterOperator::IsNotNull
4119 };
4120 (operator, String::new())
4121 } else {
4122 let operator = if keep {
4123 FilterOperator::Eq
4124 } else {
4125 FilterOperator::NotEq
4126 };
4127 let text = crate::typed_value::text_of(&value, &dtype);
4130 let Some(text) = text else {
4131 let kind = match dtype {
4132 DataType::List(_) => "lists",
4133 DataType::Array(..) => "arrays",
4134 DataType::Struct(_) => "structs",
4135 DataType::Binary | DataType::BinaryOffset => "binary",
4136 _ => "this type",
4137 };
4138 self.flash_note(format!("+ and - filter on plain values, not {kind}"));
4139 return None;
4140 };
4141 (operator, text)
4142 };
4143 let statement = FilterStatement {
4144 columns: Vec::new(),
4145 column,
4146 operator,
4147 value: text,
4148 logical_op: LogicalOperator::And,
4149 };
4150 let mut statements = state.view_filters().to_vec();
4151 if statements.contains(&statement) {
4152 return None;
4153 }
4154 statements.push(statement);
4155 Some(AppEvent::Applied(Applied::Filter(statements)))
4156 }
4157
4158 fn sync_sort_filter_modal(&mut self) {
4163 let Some(state) = self.data_table_state.as_ref() else {
4164 return;
4165 };
4166 let filters = state.view_filters().to_vec();
4167 let sort_columns = state.view_sort_columns().to_vec();
4168 let sort_descending = state.view_sort_descending().to_vec();
4169 let headers: Vec<String> = state.schema().iter_names().map(|s| s.to_string()).collect();
4170 let schema = state.schema().clone();
4171 let order = state.headers();
4172 let locked = state.locked_columns_count();
4173
4174 let modal = &mut self.sort_filter_modal;
4175 modal.filter.applied = filters.clone();
4176 modal.filter.statements = filters;
4177 modal.filter.operands = order
4178 .iter()
4179 .map(|name| {
4180 schema
4181 .get(name)
4182 .map(crate::app::modals::filter_modal::Operand::of)
4183 .unwrap_or_default()
4184 })
4185 .collect();
4186 modal.filter.available_columns = order.clone();
4187 modal.filter.cursor = modal.filter.statements.len();
4189 modal.filter.editor = None;
4190 let shown: std::collections::HashSet<&str> = order.iter().map(String::as_str).collect();
4195 let applied = &modal.sort.applied_order;
4196 let current = applied
4197 .iter()
4198 .filter(|name| shown.contains(name.as_str()))
4199 .eq(order.iter());
4200 let reference: &[String] = if current { applied } else { &[] };
4201 let full = order_with_hidden(&order, &headers, reference);
4202 let places: HashMap<&str, usize> = full
4203 .iter()
4204 .enumerate()
4205 .map(|(i, name)| (name.as_str(), i))
4206 .collect();
4207 let place = |name: &String| places.get(name.as_str()).copied();
4208 let last_locked = applied
4211 .get(..modal.sort.applied_locked)
4212 .filter(|span| {
4213 current && span.iter().filter(|n| shown.contains(n.as_str())).count() == locked
4214 })
4215 .and_then(|span| span.iter().rev().find_map(place))
4216 .or_else(|| {
4217 locked
4218 .checked_sub(1)
4219 .and_then(|i| order.get(i))
4220 .and_then(place)
4221 });
4222 modal.sort.columns = headers
4223 .iter()
4224 .map(|name| {
4225 let display_order = places[name.as_str()];
4226 SortColumn {
4227 name: name.clone(),
4228 sort_order: sort_columns.iter().position(|c| c == name).map(|o| o + 1),
4230 sort_descending: sort_columns
4231 .iter()
4232 .position(|c| c == name)
4233 .and_then(|i| sort_descending.get(i).copied())
4234 .unwrap_or(false),
4235 display_order,
4236 is_locked: last_locked.is_some_and(|l| display_order <= l),
4237 is_to_be_locked: false,
4238 is_visible: shown.contains(name.as_str()),
4239 width: state.width_choice(name),
4240 shown_width: state.on_screen_width(name),
4241 }
4242 })
4243 .collect();
4244 modal.sort.has_unapplied_changes = false;
4245 }
4246
4247 fn apply_sort_filter(&mut self) -> Option<AppEvent> {
4249 if self.sort_filter_modal.filter.editor.is_some() {
4251 self.sort_filter_modal.filter.commit_editor();
4252 }
4253 if let Some(why) = self.filter_problem() {
4255 self.sort_filter_modal.sort.status = Some(why);
4256 return None;
4257 }
4258 let (columns, descending) = self.sort_filter_modal.sort.sorted_columns_and_directions();
4259 let column_order = self.sort_filter_modal.sort.get_column_order();
4260 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
4261 self.sort_filter_modal.sort.applied_order =
4262 self.sort_filter_modal.sort.get_full_column_order();
4263 self.sort_filter_modal.sort.applied_locked = self.sort_filter_modal.sort.get_locked_span();
4264 let statements = self.sort_filter_modal.filter.statements.clone();
4265 let view_unchanged = self.data_table_state.as_mut().is_some_and(|state| {
4268 state.set_width_choices(self.sort_filter_modal.sort.width_choices());
4269 state.headers() == column_order
4270 && state.locked_columns_count() == locked_count
4271 && state.view_filters() == statements.as_slice()
4272 && state.view_sort_columns() == columns.as_slice()
4273 && state.view_sort_descending() == descending.as_slice()
4274 });
4275 for col in &mut self.sort_filter_modal.sort.columns {
4276 col.is_to_be_locked = false;
4277 }
4278 self.sort_filter_modal.sort.has_unapplied_changes = false;
4279 self.close_overlay();
4280 if view_unchanged {
4281 return None;
4282 }
4283 Some(AppEvent::Applied(Applied::ApplyView(
4284 column_order,
4285 locked_count,
4286 statements,
4287 columns,
4288 descending,
4289 )))
4290 }
4291
4292 fn facts_of_open(&self) -> Option<(FileFormat, crate::formats::readers::Facts)> {
4295 let hive = self
4296 .source
4297 .opened
4298 .as_ref()
4299 .is_some_and(|(_, options)| options.hive);
4300 let format = self.opened_format()?;
4301 let facts = crate::formats::readers::of(format).facts?;
4302 let state = self.data_table_state.as_ref()?;
4303 let plain = state
4304 .read_mode()
4305 .is_none_or(|mode| Some(mode) == format.read_mode(crate::Stored::Plain));
4306 let one_file = state.dataset_schema().is_none();
4308 (!hive && plain && one_file).then_some((format, facts))
4309 }
4310
4311 pub(crate) fn query_reading(&self) -> Option<&str> {
4314 let run = self.prompt.query_running.as_ref()?;
4315 let frame = self.data_table_state.as_ref()?.len_generation();
4316 if !matches!(run.origin, RunOrigin::Query(_)) || run.frame != frame {
4317 return None;
4318 }
4319 self.jobs
4320 .waiting_status(|job| Self::reading_rows(job) || Self::owed_rows(job))
4321 }
4322
4323 fn job_ended(&mut self, ticket: Ticket) -> Option<AppEvent> {
4328 let app::jobs::Ended {
4329 job,
4330 current,
4331 keys,
4332 outcome,
4333 ..
4334 } = self.jobs.end(ticket)?;
4335 let cancelled_analysis = !current && Self::is_analysis_read(&job);
4336 let waited = keys.is_some();
4337 let out = match outcome {
4338 Outcome::Answered(answer) => self.answered(job, current, waited, *answer),
4339 Outcome::Failed { message, panicked } => {
4340 self.background_failed(&job, current, waited, &message, panicked);
4341 None
4342 }
4343 };
4344 if let Some(status) = keys {
4345 if out.is_some() {
4346 self.busy = true;
4347 } else {
4348 self.busy = false;
4349 if self.status_message.as_deref() == Some(status.as_str())
4352 && !self.jobs.shows(&status)
4353 {
4354 self.status_message = None;
4355 }
4356 }
4357 }
4358 if cancelled_analysis
4360 && self.cancelled_analysis().is_none()
4361 && self.analysis_modal.quality.setup_note.as_deref() == Some(QUALITY_RUN_WAITS)
4362 {
4363 self.analysis_modal.quality.setup_note = None;
4364 }
4365 out
4366 }
4367
4368 fn job_progress(&mut self, ticket: Ticket, progress: &Progress) {
4370 if !self.jobs.is_current(ticket) {
4371 return;
4372 }
4373 match progress {
4374 Progress::ExportWriting { phase, bytes } => {
4375 if let Some(export) = self.export_progress.as_mut() {
4376 export.current_phase = phase.to_string();
4377 export.written = Some(*bytes);
4378 }
4379 }
4380 Progress::QualityPhase(phase) => {
4381 if let Some(progress) = self.analysis_modal.computing.as_mut() {
4382 progress.phase = phase.stage.label().to_string();
4383 progress.reads_source = Some(phase.reads_source);
4384 progress.interruptible = Some(phase.interruptible);
4385 }
4386 }
4387 Progress::Finding { rows } => self.find_progress(*rows),
4388 Progress::HexFinding { read, total } => self.hex_find_progress(*read, *total),
4389 Progress::SampleBegun(schema) => self.sample_begun(schema),
4390 Progress::SampleGrew => self.sample_grew(),
4391 }
4392 }
4393
4394 fn answered(
4397 &mut self,
4398 job: Job,
4399 current: bool,
4400 waited: bool,
4401 answer: Answer,
4402 ) -> Option<AppEvent> {
4403 match (job, answer) {
4404 (Job::JournalDetail { dataset }, Answer::JournalDescribed(detail)) => {
4405 if dataset == self.dataset_generation
4406 && let Some(state) = self.data_table_state.as_mut()
4407 {
4408 state.set_format_detail(*detail);
4409 }
4410 None
4411 }
4412 (Job::IndexLines { dataset }, Answer::LinesIndexed(rows)) => {
4413 self.lines_indexed(dataset, rows);
4414 None
4415 }
4416 (Job::FootersJoin { dataset }, Answer::FootersJoined(found)) => {
4417 self.footers_joined(dataset, found.map(|found| *found))
4418 }
4419 (Job::Load(load), Answer::Load(answer)) => {
4420 let step = self.loading.answered(
4423 load,
4424 *answer,
4425 #[cfg(any(feature = "http", feature = "cloud"))]
4426 &self.jobs,
4427 );
4428 self.run_load_step(step)
4429 }
4430 (
4431 Job::OpenNamed(load),
4432 Answer::NamedPaths {
4433 paths,
4434 options,
4435 directory,
4436 },
4437 ) => {
4438 if !self.loading.looking_at_paths(load) {
4440 return None;
4441 }
4442 Some(match directory {
4444 Some(dir) => AppEvent::LookThenOpenDirectory(dir, *options),
4445 None => AppEvent::Open(paths, *options),
4446 })
4447 }
4448 (Job::OpenNamed(load), Answer::NamedPathMissing(path)) => {
4449 if !self.loading.looking_at_paths(load) {
4450 return None;
4451 }
4452 if let Some(retired) = self.loading.retire() {
4454 self.put_down_load(retired);
4455 }
4456 Some(AppEvent::NamedPathMissing(path))
4457 }
4458 (
4459 Job::LookAtDirectory { load, path },
4460 Answer::LookedAt {
4461 kind,
4462 holds,
4463 options,
4464 },
4465 ) => {
4466 if !self.loading.looking_at_directory(load) {
4468 return None;
4469 }
4470 self.open_the_directory_looked_at(path, kind, holds.as_deref(), *options)
4472 }
4473 (Job::Classify(asked), Answer::Kind(found)) => {
4474 if !current {
4476 return None;
4477 }
4478 self.home.status = None;
4479
4480 if self.input_mode != InputMode::Home || self.home.browsing != asked.browsing {
4483 return None;
4484 }
4485
4486 let path = asked.path;
4487 let Some(kind) = found else {
4488 self.home.status = Some(format!("No such path: {}", path.display()));
4489 if let Some(typed) = asked.typed {
4490 self.home.path_input = typed;
4493 self.home.path_input_active = true;
4494 self.list_the_typed_directory();
4495 }
4496 return None;
4497 };
4498 self.open_what_it_is(path, kind, asked.typed.is_some())
4499 }
4500 (Job::Rows(inflight), Answer::Rows(result)) => {
4501 if !current {
4503 return None;
4504 }
4505 if let Some(state) = self.data_table_state.as_ref() {
4508 let took = inflight.began.elapsed();
4509 log::debug!(
4510 target: "datui",
4511 "rows {}..{} of {}: read in {took:.1?}",
4512 inflight.start,
4513 inflight.end,
4514 inflight.dataset
4515 );
4516 state.measurements().read_page(took, inflight.files);
4517 }
4518 if let Some(state) = &mut self.data_table_state {
4519 state.apply_async_collect(result);
4520 }
4521 self.retire_a_count_the_rows_answered();
4522 self.remember_a_downloads_shape();
4523 self.catch_up_follow();
4525 let mut ran = self.take_query_run().map(|run| run.origin);
4527 if let Some(RunOrigin::View {
4529 applying: view::view_apply::Applying::Restored(drill),
4530 ..
4531 }) = ran.as_mut()
4532 && let Some(drill) = drill.take()
4533 {
4534 if waited {
4535 self.first_rows_settled();
4536 }
4537 self.drill_again(*drill);
4538 return None;
4539 }
4540 if waited {
4543 self.first_rows_settled();
4544 match ran {
4545 Some(RunOrigin::Query(mode)) if self.query_prompt_mode() == Some(mode) => {
4546 self.leave_query_prompt_after_run();
4547 }
4548 Some(RunOrigin::View {
4550 name,
4551 applying: view::view_apply::Applying::Matched(why),
4552 ..
4553 }) => self.flash_view_applied(&name, why),
4554 _ => {}
4555 }
4556 }
4557 None
4558 }
4559 (
4560 job @ Job::Rows(_),
4561 Answer::RowsFailed {
4562 message,
4563 conversion,
4564 },
4565 ) => {
4566 self.ask_reopen_if_gone(&job, current, waited, &message);
4568 self.rows_failed(current, waited, &message, conversion.as_deref());
4569 None
4570 }
4571 (Job::Analysis(_), Answer::Analysis(install, results)) => {
4572 if current {
4573 install(&mut self.analysis_modal, results);
4574 self.analysis_modal.computing = None;
4575 }
4576 None
4577 }
4578 (
4579 Job::Analysis(_),
4580 Answer::DataQuality {
4581 results,
4582 kept,
4583 plan,
4584 },
4585 ) => {
4586 if let Some(kept) = kept {
4588 self.retain_quality_sample(&kept);
4589 }
4590 if current
4591 && self.overlay == Overlay::Analysis
4592 && self.analysis_modal.selected_tool
4593 == Some(analysis::analysis_modal::AnalysisTool::DataQuality)
4594 {
4595 self.cache_quality_result(&results, (*plan).clone());
4597 self.analysis_modal.quality.last_plan = Some(*plan);
4598 self.analysis_modal.quality.results = Some(*results);
4599 self.analysis_modal.quality.from_cache = false;
4600 self.analysis_modal
4601 .set_quality_page(crate::analysis::data_quality::QualityPage::Overview);
4602 self.analysis_modal.computing = None;
4603 }
4604 None
4605 }
4606 (Job::SampleDraw(draw), Answer::SampleDrawn(drawn)) => {
4607 self.sample_drawn(*draw, current, drawn)
4608 }
4609 (Job::SampleRows, Answer::Sample { df, label }) => {
4610 if current {
4611 self.analysis_modal.computing = None;
4612 self.show_sample_view(df, label);
4613 }
4614 None
4615 }
4616 (job @ Job::Pivot, Answer::Pivoted { spec, pivoted }) => {
4617 if !current {
4619 return None;
4620 }
4621 let installed = self.data_table_state.as_mut().map(|state| {
4622 state
4623 .deferred(|s| s.install_pivot(&spec, pivoted))
4624 .map_err(|e| crate::error_display::user_message_from_report(&e, None))
4625 });
4626 match installed {
4627 Some(Ok(())) => {
4628 if self.overlay == Overlay::PivotMelt {
4630 self.close_overlay();
4631 }
4632 self.spawn_async_collect(Self::LOADING_BUFFER);
4634 }
4635 Some(Err(message)) => {
4637 self.background_failed(&job, current, waited, &message, false);
4638 }
4639 None => {}
4640 }
4641 None
4642 }
4643 (Job::ReshapePreview { epoch, token }, Answer::ReshapePreviewed { input, result }) => {
4644 self.reshape_preview_ended(epoch, token, input, result);
4645 None
4646 }
4647 (Job::ViewPivot(pivot), Answer::ViewPivoted(pivoted)) => {
4648 if !current {
4650 return None;
4651 }
4652 let settings = &pivot.0.settings;
4653 let planned = self.data_table_state.as_mut().map(|state| {
4654 state
4657 .try_transition(|s| Self::replay_view(s, settings, Some(pivoted)))
4658 .map(|(_, rollback)| rollback)
4659 .map_err(|e| e.to_string())
4660 });
4661 match planned {
4662 Some(Ok(rollback)) => {
4664 let (view, applying) = *pivot;
4665 self.view_planned(&view, rollback, applying);
4666 }
4667 Some(Err(message)) => {
4669 let job = Job::ViewPivot(pivot);
4670 self.background_failed(&job, current, waited, &message, false);
4671 }
4672 None => {}
4673 }
4674 None
4675 }
4676 (Job::DrillRow, Answer::DrillRow { group_index, row }) => {
4677 if current {
4679 self.drill_into(group_index, &row);
4680 }
4681 None
4682 }
4683 (Job::Regroup(place), Answer::Regrouped(found)) => {
4684 if current {
4685 self.drilled_again(&place, found);
4686 }
4687 None
4688 }
4689 (Job::InspectRow { frame, row }, Answer::FieldsRead(values)) => {
4690 if !current {
4692 return None;
4693 }
4694 let asked = self
4695 .inspector_modal
4696 .read
4697 .as_ref()
4698 .is_some_and(|read| read.key() == (frame, row));
4699 if self.overlay == Overlay::Inspect && asked {
4700 self.inspector_modal.read =
4701 Some(inspector::inspector_modal::FieldRead::Read { frame, row, values });
4702 }
4703 None
4704 }
4705 (Job::InspectJson { token }, Answer::JsonParsed(root)) => {
4706 if !current || self.overlay != Overlay::Inspect {
4708 return None;
4709 }
4710 let modal = &mut self.inspector_modal;
4711 if let Some(wait) = modal.json_wait.take_if(|w| w.token == token) {
4712 let node = inspector::inspector_drill::Node::Json {
4713 root,
4714 path: Vec::new(),
4715 };
4716 modal.drill_in(wait.frame, wait.row, wait.label, node);
4717 }
4718 None
4719 }
4720 (Job::InspectPretty { token }, Answer::Indented(text)) => {
4721 let modal = &mut self.inspector_modal;
4722 if current
4723 && let Some(inspector::inspector_modal::Pretty::Pending { token: t, place }) =
4724 modal.pretty.as_ref()
4725 && *t == token
4726 {
4727 modal.pretty = Some(inspector::inspector_modal::Pretty::Ready {
4728 place: place.clone(),
4729 text,
4730 });
4731 }
4732 None
4733 }
4734 (Job::InspectUnpack { token }, Answer::Unpacked(decoded)) => {
4735 let modal = &mut self.inspector_modal;
4736 if current
4737 && let Some(inspector::inspector_modal::Unpack::Pending { token: t, place }) =
4738 modal.unpack.as_ref()
4739 && *t == token
4740 {
4741 modal.unpack = Some(inspector::inspector_modal::Unpack::Ready {
4742 place: place.clone(),
4743 text: std::sync::Arc::new(decoded),
4744 });
4745 }
4746 None
4747 }
4748 (Job::OpenValue, Answer::ValueWritten(open)) => {
4749 if current && self.overlay == Overlay::Inspect {
4750 self.external.open = Some(open);
4751 }
4752 None
4753 }
4754 (Job::Export, Answer::Exported(path)) => {
4755 self.export_modal.close();
4757 if current {
4758 self.export_progress = None;
4759 self.flash_path("Exported to ", &path);
4760 }
4761 None
4762 }
4763 (Job::Copy, Answer::Copied { payload, message }) => {
4764 if current {
4765 self.export_progress = None;
4766 self.finish_copy(payload, message);
4767 }
4768 None
4769 }
4770 (Job::QualityReport, Answer::QualityReportWritten(path)) => {
4771 self.analysis_modal.quality.export = None;
4772 if current {
4773 self.flash_path("Report written to ", &path);
4774 }
4775 None
4776 }
4777 (Job::ChartPrepare(prep), Answer::ChartPrepared(prepared)) => {
4778 self.chart_prepared(*prep, current, Ok(*prepared));
4779 None
4780 }
4781 (Job::ChartExport { path, format }, Answer::ChartExported) => {
4782 if current {
4785 self.finish_chart_export(&path, format, Ok(()));
4786 }
4787 None
4788 }
4789 (Job::FileFacts { dataset }, Answer::FileFacts(facts)) => {
4790 self.file_facts_landed(dataset, facts);
4791 None
4792 }
4793 (Job::UnfitCount { dataset, version }, Answer::UnfitCounted(unfit)) => {
4794 let columns: Vec<&str> = unfit.iter().map(|u| u.column.as_str()).collect();
4796 let said = if columns.is_empty() {
4797 "none".to_string()
4798 } else {
4799 columns.join(", ")
4800 };
4801 log::debug!(target: "datui", "values column types made null, by column: {said}");
4802 if dataset == self.dataset_generation
4803 && let Some(state) = self.data_table_state.as_mut()
4804 {
4805 match version {
4806 None => state.unfit_counted(&unfit),
4807 Some(version) => state.changes_unfit_counted(version, &unfit),
4808 }
4809 }
4810 None
4811 }
4812 (Job::Find(run), Answer::Found(found)) => {
4813 self.find_answered(run, current, found);
4814 None
4815 }
4816 (
4817 Job::HexOpen {
4818 origin,
4819 fallback,
4820 record_size,
4821 },
4822 Answer::HexOpened(source),
4823 ) => {
4824 if current {
4826 self.hex_opened(origin, fallback, record_size, *source);
4827 }
4828 None
4829 }
4830 (Job::HexFind(run), Answer::HexFound(hit)) => {
4831 if current {
4832 self.hex_found(run, hit);
4833 }
4834 None
4835 }
4836 (Job::ValueCounts, Answer::ValueCounts(counts)) => {
4837 if current {
4839 self.value_counts.computing = None;
4840 self.value_counts.hold(*counts);
4841 }
4842 None
4843 }
4844 #[cfg(test)]
4846 (_, Answer::Probe(held)) => {
4847 drop(held);
4848 None
4849 }
4850 (
4853 job,
4854 Answer::Load(_)
4855 | Answer::NamedPaths { .. }
4856 | Answer::NamedPathMissing(_)
4857 | Answer::LookedAt { .. }
4858 | Answer::Kind(_)
4859 | Answer::Rows(_)
4860 | Answer::ReshapePreviewed { .. }
4861 | Answer::ViewPivoted(_)
4862 | Answer::FieldsRead(_)
4863 | Answer::JsonParsed(_)
4864 | Answer::Indented(_)
4865 | Answer::Unpacked(_)
4866 | Answer::ChartPrepared(_)
4867 | Answer::ChartExported
4868 | Answer::FileFacts(_)
4869 | Answer::UnfitCounted(_)
4870 | Answer::Found(_)
4871 | Answer::HexOpened(_)
4872 | Answer::HexFound(_)
4873 | Answer::FootersJoined(_)
4874 | Answer::JournalDescribed(_)
4875 | Answer::LinesIndexed(_)
4876 | Answer::RowsFailed { .. }
4877 | Answer::Analysis(..)
4878 | Answer::DataQuality { .. }
4879 | Answer::SampleDrawn(_)
4880 | Answer::Sample { .. }
4881 | Answer::Pivoted { .. }
4882 | Answer::DrillRow { .. }
4883 | Answer::Regrouped(_)
4884 | Answer::ValueWritten(_)
4885 | Answer::Exported(_)
4886 | Answer::Copied { .. }
4887 | Answer::QualityReportWritten(_)
4888 | Answer::ValueCounts(_),
4889 ) => {
4890 log::error!(target: "datui", "a {:?} job got another job's answer; dropped", job.kind());
4891 None
4892 }
4893 }
4894 }
4895
4896 fn background_failed(
4902 &mut self,
4903 job: &Job,
4904 current: bool,
4905 waited: bool,
4906 message: &str,
4907 panicked: bool,
4908 ) {
4909 let could_not = |what: &str| {
4911 if panicked {
4912 format!("Could not {what}; see the log")
4913 } else {
4914 format!("Could not {what}: {message}")
4915 }
4916 };
4917 self.ask_reopen_if_gone(job, current, waited, message);
4919 match job {
4920 Job::Load(load) | Job::OpenNamed(load) | Job::LookAtDirectory { load, .. } => {
4922 if let loading::Step::Failed(failed) = self.loading.failed(*load, message) {
4923 self.load_failed(failed);
4924 }
4925 }
4926 Job::FootersJoin { dataset } => {
4928 self.footers_joined(*dataset, None);
4929 }
4930 Job::ChartPrepare(prep) => {
4931 let message = if panicked {
4932 "Chart preparation panicked".to_string()
4933 } else {
4934 message.to_string()
4935 };
4936 self.chart_prepared(*prep.clone(), current, Err(message));
4937 }
4938 Job::Rows(_) | Job::OwedRows { .. } => {
4939 self.rows_failed(current, waited, message, None);
4940 }
4941 Job::SampleDraw(draw) => self.sample_draw_failed(draw, current, message),
4942 Job::ReshapePreview { epoch, token } => {
4944 let message = if panicked {
4945 "Could not preview; see the log".to_string()
4946 } else {
4947 message.to_string()
4948 };
4949 self.reshape_preview_ended(*epoch, *token, None, Err(message));
4950 }
4951 Job::InspectPretty { token } => {
4952 let modal = &mut self.inspector_modal;
4953 if let Some(inspector::inspector_modal::Pretty::Pending { token: t, place }) =
4954 modal.pretty.as_ref()
4955 && t == token
4956 {
4957 modal.pretty = Some(inspector::inspector_modal::Pretty::Failed {
4958 place: place.clone(),
4959 });
4960 }
4961 }
4962 Job::InspectUnpack { token } => {
4963 let modal = &mut self.inspector_modal;
4964 if let Some(inspector::inspector_modal::Unpack::Pending { token: t, place }) =
4965 modal.unpack.as_ref()
4966 && t == token
4967 {
4968 modal.unpack = Some(inspector::inspector_modal::Unpack::Failed {
4969 place: place.clone(),
4970 });
4971 }
4972 }
4973 Job::Find(_) => self.find_failed(current, message),
4974 Job::FileFacts { dataset } => {
4976 let why = if panicked {
4977 "could not read; see the log".to_string()
4978 } else {
4979 message.to_string()
4980 };
4981 self.file_facts_landed(*dataset, FileFacts::Failed(why));
4982 }
4983 Job::UnfitCount { .. } => {
4985 log::warn!(target: "datui", "counting values that did not fit their type failed: {message}");
4986 }
4987 Job::JournalDetail { .. } => {
4989 log::warn!(target: "datui", "reading the journal's detail failed: {message}");
4990 }
4991 Job::IndexLines { .. } => {}
4994 _ if !current => {
4996 if matches!(job, Job::Export) {
4997 self.export_modal.close();
4999 }
5000 }
5001 Job::Classify(_) => {
5002 self.home.status = None;
5003 self.error_modal.show(message.to_string());
5004 }
5005 Job::Analysis(_) | Job::SampleRows => {
5006 self.analysis_modal.computing = None;
5007 self.say_read_failed(message);
5008 }
5009 Job::Pivot | Job::Copy => self.say_read_failed(message),
5011 Job::HexOpen { .. } => {
5012 self.error_modal.show(message.to_string());
5013 }
5014 Job::QualityReport => match self.analysis_modal.quality.export.as_mut() {
5016 Some(form) => form.error = Some(message.to_string()),
5017 None => self.error_modal.show(message.to_string()),
5018 },
5019 Job::ViewPivot(pivot) => self.view_pivot_failed(&pivot.1, message),
5020 Job::DrillRow | Job::Regroup(_) => {
5022 if !self.asking_reopen(message) {
5023 self.flash_note(could_not("drill in"));
5024 }
5025 }
5026 Job::InspectJson { token } => {
5027 let modal = &mut self.inspector_modal;
5028 if let Some(wait) = modal.json_wait.take_if(|w| w.token == *token) {
5029 modal.not_json = Some((wait.frame, wait.row, wait.path));
5030 self.flash_note(if panicked {
5031 "Could not read the JSON; see the log".to_string()
5032 } else {
5033 sentence(message)
5034 });
5035 }
5036 }
5037 Job::OpenValue => self.flash_note(could_not("open the value")),
5038 Job::InspectRow { frame, row } => {
5039 let asked = self
5040 .inspector_modal
5041 .read
5042 .as_ref()
5043 .is_some_and(|read| read.key() == (*frame, *row));
5044 if asked {
5045 self.inspector_modal.read =
5046 Some(inspector::inspector_modal::FieldRead::Failed {
5047 frame: *frame,
5048 row: *row,
5049 message: could_not("read the field"),
5050 });
5051 }
5052 }
5053 Job::Export => {
5055 self.export_progress = None;
5056 self.export_modal.path_error = Some(message.to_string());
5057 self.open_over(|returns_to| Overlay::Export { returns_to });
5060 }
5061 Job::ChartExport { path, format } => {
5062 self.finish_chart_export(path, *format, Err(message.to_string()));
5063 }
5064 Job::HexFind(_) => {
5065 self.status_message = None;
5066 self.flash_note(message.to_string());
5067 }
5068 Job::ValueCounts => {
5070 if let Some(computing) = self.value_counts.computing.take() {
5071 let why = if panicked {
5072 "could not count; see the log".to_string()
5073 } else {
5074 message.to_string()
5075 };
5076 self.value_counts.failed = Some((computing.column, why));
5077 }
5078 }
5079 }
5080 }
5081
5082 fn read_file_facts(&mut self) {
5087 let dataset = self.dataset_generation;
5088 if self.data_table_state.is_none()
5089 || self
5090 .info
5091 .file_facts
5092 .as_ref()
5093 .is_some_and(|(read, _)| *read == dataset)
5094 || self.file_facts_reading()
5095 {
5096 return;
5097 }
5098 let several = self
5101 .source
5102 .opened
5103 .as_ref()
5104 .is_some_and(|(paths, _)| paths.len() > 1);
5105 let piped = self.reads_stdin();
5106 let Some(path) = self.path.clone().filter(|path| {
5107 !several
5108 && !piped
5109 && !cloud::source::is_remote_url(path)
5110 && !cloud::source::is_prefix_or_glob(&path.to_string_lossy())
5111 }) else {
5112 return;
5113 };
5114 let facts = self.facts_of_open().map(|(_, facts)| facts);
5115 #[cfg(test)]
5116 let read: FileFactsReader = self
5117 .file_facts_reader
5118 .clone()
5119 .unwrap_or_else(|| Arc::new(FileFacts::read));
5120 #[cfg(not(test))]
5121 let read = FileFacts::read;
5122 self.spawn_job(Job::FileFacts { dataset }, None, move |_| {
5123 Ok(Answer::FileFacts(read(&path, facts)?))
5124 });
5125 }
5126
5127 pub(crate) fn file_facts_reading(&self) -> bool {
5129 let dataset = self.dataset_generation;
5130 self.jobs
5131 .current(|job| matches!(job, Job::FileFacts { dataset: asked } if *asked == dataset))
5132 .is_some()
5133 }
5134
5135 fn file_facts_landed(&mut self, dataset: u64, facts: FileFacts) {
5137 if dataset == self.dataset_generation {
5138 self.info.file_facts = Some((dataset, facts));
5139 }
5140 }
5141
5142 pub fn file_facts(&self) -> Option<&FileFacts> {
5145 Self::facts_shown(
5146 &self.info.file_facts,
5147 self.dataset_generation,
5148 self.file_facts_reading(),
5149 )
5150 }
5151
5152 pub(crate) fn facts_shown(
5154 read: &Option<(u64, FileFacts)>,
5155 dataset: u64,
5156 reading: bool,
5157 ) -> Option<&FileFacts> {
5158 static READING: FileFacts = FileFacts::Reading;
5159 match read {
5160 Some((read_for, facts)) if *read_for == dataset => Some(facts),
5161 _ => reading.then_some(&READING),
5162 }
5163 }
5164
5165 fn land_on_tables(&mut self, tables: loading::Tables) {
5167 let loading::Tables {
5168 database,
5169 from_home,
5170 } = tables;
5171 self.status_message = None;
5172 self.busy = false;
5173 self.enter_home();
5174 if from_home {
5175 self.home_browse_into(database);
5176 } else {
5177 self.home_jump_into(database);
5178 }
5179 }
5180
5181 fn load_failed(&mut self, failed: loading::Failed) {
5184 let loading::Failed { message, from_home } = failed;
5185 self.status_message = None;
5186 self.busy = false;
5187 if from_home {
5190 self.home_app.last_load_error = None;
5191 self.enter_home();
5192 } else {
5193 self.home_app.last_load_error = Some(message.clone());
5194 }
5195 self.error_modal.show(message);
5196 }
5197
5198 fn rows_failed(
5202 &mut self,
5203 current: bool,
5204 waited: bool,
5205 message: &str,
5206 conversion: Option<&crate::error_display::ConversionFailure>,
5207 ) {
5208 if !current {
5209 return;
5210 }
5211 let message = &self.named_by_source(message);
5212 if let Some(run) = self.take_query_run() {
5213 self.fail_query_run(run, message, conversion);
5214 return;
5215 }
5216 if !waited {
5217 return;
5218 }
5219 if let Some(generation) = self.counting.count_after_paint.take() {
5222 if self.counting.len_count_inflight == Some(generation) {
5223 self.counting.len_count_inflight = None;
5224 }
5225 if self
5226 .data_table_state
5227 .as_ref()
5228 .is_some_and(|state| state.len_generation() == generation)
5229 {
5230 self.counting.len_count_failed = Some(generation);
5231 }
5232 }
5233 self.first_rows_settled();
5234 self.say_read_failed(message);
5235 }
5236
5237 fn ask_reopen_if_gone(&mut self, job: &Job, current: bool, waited: bool, message: &str) {
5243 if !current
5244 || !job.reads_dataset()
5245 || !crate::error_display::says_gone_since_opened(message)
5246 || self.source.opened.is_none()
5247 {
5248 return;
5249 }
5250 let Some((said, asked)) = self.read_that_failed(job, waited, message) else {
5251 return;
5252 };
5253 self.confirmation_modal.show_choice(
5254 said,
5255 "Reopen",
5256 "Close",
5257 Confirm::Reopen(asked.map(Box::new)),
5258 );
5259 }
5260
5261 fn read_that_failed(
5266 &self,
5267 job: &Job,
5268 waited: bool,
5269 message: &str,
5270 ) -> Option<(String, Option<loading::open_options::KeptPlace>)> {
5271 match job {
5272 Job::Rows(_) | Job::OwedRows { .. } => {
5273 let message = self.named_by_source(message);
5274 let Some(run) = self.query_run() else {
5275 return waited.then_some((message, None));
5276 };
5277 let mut asked = self.place_on_screen();
5280 match &run.origin {
5281 RunOrigin::Query(_) => Some((message, asked)),
5282 RunOrigin::View { applying, .. } => {
5283 if let (Some(asked), view::view_apply::Applying::Restored(Some(drill))) =
5286 (asked.as_mut(), applying)
5287 {
5288 asked.drill = Some(drill.clone());
5289 }
5290 Some((view::view_apply::view_failure(applying, &message), asked))
5291 }
5292 }
5293 }
5294 Job::ViewPivot(pivot) => {
5295 let (view, applying) = &**pivot;
5296 let asked = loading::open_options::KeptPlace {
5297 settings: view.settings.clone(),
5298 active: (!view.id.is_empty()).then(|| view.id.clone()),
5299 drill: match applying {
5300 view::view_apply::Applying::Restored(drill) => drill.clone(),
5301 _ => None,
5302 },
5303 };
5304 Some((
5305 view::view_apply::view_failure(applying, message),
5306 Some(asked),
5307 ))
5308 }
5309 Job::InspectRow { frame, row } => self
5310 .inspector_modal
5311 .read
5312 .as_ref()
5313 .is_some_and(|read| read.key() == (*frame, *row))
5314 .then(|| (message.to_string(), None)),
5315 Job::ValueCounts => self
5316 .value_counts
5317 .computing
5318 .is_some()
5319 .then(|| (message.to_string(), None)),
5320 Job::ChartPrepare(prep) => {
5321 let dataset = self.data_table_state.as_ref().map(|s| s.len_generation());
5322 let stopped = prep.cancel.load(std::sync::atomic::Ordering::Relaxed);
5323 (!stopped && dataset == prep.dataset).then(|| (message.to_string(), None))
5324 }
5325 _ => Some((message.to_string(), None)),
5326 }
5327 }
5328
5329 pub(crate) fn asking_reopen(&self, message: &str) -> bool {
5331 let modal = &self.confirmation_modal;
5332 modal.active && matches!(modal.asking, Some(Confirm::Reopen(_))) && modal.message == message
5333 }
5334
5335 pub(crate) fn say_read_failed(&mut self, message: &str) {
5338 if !self.asking_reopen(message) {
5339 self.error_modal.show(message.to_string());
5340 }
5341 }
5342
5343 fn drill_again(&mut self, place: crate::table::DrillPlace) {
5346 let Some(state) = self.data_table_state.as_ref() else {
5347 return;
5348 };
5349 match state.find_group(&place) {
5350 None => self.drilled_again(&place, None),
5351 Some(Err(e)) => self.drill_not_taken(&e),
5352 Some(Ok(lf)) => {
5353 let streaming = state.polars_streaming();
5354 let job = Job::Regroup(Box::new(place));
5355 self.spawn_job(job, Some(Self::READING_GROUP), move |_| {
5356 let read = crate::analysis::statistics::collect_lazy(lf, streaming)
5357 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
5358 let found = DataTableState::found_group(read)
5359 .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
5360 Ok(Answer::Regrouped(found))
5361 });
5362 }
5363 }
5364 }
5365
5366 fn drilled_again(
5369 &mut self,
5370 place: &crate::table::DrillPlace,
5371 found: Option<(usize, DataFrame)>,
5372 ) {
5373 if found.is_none() && place.by_group() {
5374 self.flash_note(crate::table::DrillGone(place.describe()).to_string());
5375 return;
5376 }
5377 let Some(state) = self.data_table_state.as_mut() else {
5378 return;
5379 };
5380 match state.try_transition(|s| s.redrill(place, found)) {
5382 Ok(_) => {
5383 self.sync_sort_filter_modal();
5384 self.spawn_async_collect(Self::LOADING_BUFFER);
5385 }
5386 Err(e) => {
5387 self.drill_not_taken(&e);
5388 self.spawn_async_collect(Self::LOADING_BUFFER);
5389 }
5390 }
5391 }
5392
5393 fn drill_not_taken(&mut self, e: &color_eyre::Report) {
5395 let said = match e.downcast_ref::<crate::table::DrillGone>() {
5396 Some(gone) => gone.to_string(),
5397 None => format!(
5398 "Could not drill in: {}",
5399 crate::error_display::user_message_from_report(e, None)
5400 ),
5401 };
5402 self.flash_note(said);
5403 }
5404
5405 fn named_by_source(&self, message: &str) -> String {
5408 let (Some(state), Some(source)) = (self.data_table_state.as_ref(), self.path.as_deref())
5409 else {
5410 return message.to_string();
5411 };
5412 state
5413 .temp_files()
5414 .into_iter()
5415 .fold(message.to_string(), |message, file| {
5416 crate::error_display::named_by_source(&message, file, source)
5417 })
5418 }
5419
5420 const COPY_CONFIRM_BYTES: usize = 10 * 1024 * 1024;
5423 const COPY_REFUSE_BYTES: usize = 200 * 1024 * 1024;
5426
5427 fn drill_selected_row(&mut self) {
5430 let Some(state) = self.data_table_state.as_ref() else {
5431 return;
5432 };
5433 let drill = state
5435 .table_state
5436 .selected()
5437 .map(|selected| state.start_row() + selected)
5438 .and_then(|index| Some((index, state.drill_row(index)?)));
5439 match drill {
5440 None => self.flash_note("No group to drill down into".to_string()),
5441 Some((group_index, DrillRow::Buffered(row))) => self.drill_into(group_index, &row),
5442 Some((group_index, DrillRow::Read(lf))) => {
5443 let streaming = state.polars_streaming();
5444 self.spawn_job(Job::DrillRow, Some(Self::READING_GROUP), move |_| {
5445 let row = crate::analysis::statistics::collect_lazy(*lf, streaming)
5446 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
5447 Ok(Answer::DrillRow { group_index, row })
5448 });
5449 }
5450 }
5451 }
5452
5453 pub fn follow_terminal_background(&mut self, mode: ThemeMode) {
5457 let theme = &self.app_config.theme;
5458 if !theme.follow || theme.mode == Some(mode) {
5459 return;
5460 }
5461 let mut next = theme.clone();
5462 let built = theme.palette_for(mode).and_then(|colors| {
5463 next.colors = colors;
5464 next.mode = Some(mode);
5465 Theme::from_config(&next)
5466 });
5467 match built {
5468 Ok(built) => {
5469 self.theme = built;
5470 self.chart.modal.series_cap = Some(self.theme.series_colors().len());
5471 self.app_config.theme = next;
5472 for input in [
5475 &mut self.prompt.query_input,
5476 &mut self.prompt.sql_input,
5477 &mut self.prompt.find.input,
5478 ] {
5479 *input = std::mem::take(input).with_theme(&self.theme);
5480 }
5481 }
5482 Err(e) => log::warn!("cannot switch to the {mode:?} palette: {e}"),
5484 }
5485 }
5486
5487 fn terminal_answered(&mut self, mode: ThemeMode) {
5490 if self.app_config.theme.follow {
5491 self.cache
5492 .remember_terminal_mode(&app::terminal_color::terminal_key(), mode);
5493 }
5494 self.follow_terminal_background(mode);
5495 }
5496
5497 pub fn settle_first_palette(&mut self, answered: Option<ThemeMode>) {
5500 if let Some(mode) = answered {
5501 self.terminal_answered(mode);
5502 } else if self.app_config.theme.follow
5503 && let Some(mode) = self
5504 .cache
5505 .terminal_mode(&app::terminal_color::terminal_key())
5506 {
5507 self.follow_terminal_background(mode);
5508 }
5509 }
5510
5511 pub fn take_background_query(&mut self) -> bool {
5514 std::mem::take(&mut self.display.background_query)
5515 }
5516
5517 pub fn take_repaint(&mut self) -> Option<render::context::Repaint> {
5520 std::mem::take(&mut self.display.repaint)
5521 }
5522
5523 pub fn theme(&self) -> &Theme {
5525 &self.theme
5526 }
5527
5528 pub fn follows_terminal(&self) -> bool {
5530 self.app_config.theme.follow
5531 }
5532
5533 pub fn take_external_open(&mut self) -> Option<inspector::external_open::ExternalOpen> {
5535 self.external.open.take()
5536 }
5537
5538 pub fn mouse_enabled(&self) -> bool {
5541 self.app_config.display.mouse
5542 }
5543
5544 pub fn external_opened(
5547 &mut self,
5548 open: &inspector::external_open::ExternalOpen,
5549 failed: Option<String>,
5550 ) {
5551 let program =
5552 inspector::external_open::program_for(open.document, |name| std::env::var(name).ok());
5553 if matches!(program, inspector::external_open::Program::Wait(_)) {
5554 let _ = std::fs::remove_file(&open.path);
5555 }
5556 match failed {
5557 Some(e) => self.flash_note(format!("Could not open the value: {e}")),
5558 None if matches!(program, inspector::external_open::Program::Opener(_)) => {
5559 self.flash_note("Opened in the system viewer".to_string())
5560 }
5561 None => {}
5562 }
5563 }
5564}
5565
5566impl Widget for &mut App {
5567 fn render(self, area: Rect, buf: &mut Buffer) {
5568 let started = std::time::Instant::now();
5569 self.draw_frame(area, buf);
5570 self.debug.times.frame(started.elapsed());
5571 }
5572}
5573
5574impl App {
5575 fn draw_frame(&mut self, area: Rect, buf: &mut Buffer) {
5576 self.begin_frame();
5577 self.debug.num_frames += 1;
5578 if self.debug.enabled {
5579 self.debug.show_help_at_render = self.help.is_open();
5580 }
5581
5582 use crate::render::context::RenderContext;
5583 use crate::render::layout::app_layout;
5584 use crate::render::main_view::MainViewContent;
5585
5586 let ctx = RenderContext::from_theme_and_config(
5587 &self.theme,
5588 self.display.table_cell_padding,
5589 self.display.column_colors,
5590 self.display.number_format.clone(),
5591 )
5592 .with_dtype_row(self.display.dtype_row)
5593 .with_stripes_follow_rows(self.app_config.display.scroll_region);
5594
5595 let main_view_content = MainViewContent::current(self);
5596
5597 Clear.render(area, buf);
5598 let background_color = self.theme.background();
5599 Block::default()
5600 .style(Style::default().bg(background_color))
5601 .render(area, buf);
5602
5603 let progress = self.footer_progress_line(main_view_content);
5606 let prompt_room = crate::render::footer::MAX_LINES - 1;
5607 let prompt_rows = if main_view_content == MainViewContent::Datatable {
5608 crate::render::input_strip::rows(self, area.width, prompt_room)
5609 } else {
5610 0
5611 };
5612 let progress_rows = u16::from(progress.is_some() && prompt_rows < prompt_room);
5613 let footer_lines = 1 + prompt_rows + progress_rows;
5614 let rule = self.overlay != Overlay::Inspect;
5616 let app_layout = app_layout(area, self.debug.enabled, footer_lines, rule);
5617 let room = app_layout.footer.height.saturating_sub(1);
5619 let prompt_rows = prompt_rows.min(room);
5620 let progress_rows = progress_rows.min(room - prompt_rows);
5621 let main_area = app_layout.main_view;
5622 Clear.render(main_area, buf);
5623
5624 crate::render::main_view_render::render_main_view(area, main_area, buf, self, &ctx);
5625 if self.in_normal_table_view() {
5626 self.render_drop_mark(buf, &ctx);
5627 }
5628 if self.menu_showing()
5629 && let Some(menu) = self.context_menu.clone()
5630 {
5631 menu.render(main_area, buf, &ctx);
5632 }
5633
5634 if self.confirmation_modal.active {
5635 crate::render::overlays::render_confirmation_modal(
5636 area,
5637 buf,
5638 &mut self.confirmation_modal,
5639 &ctx,
5640 );
5641 }
5642 if self.error_modal.active {
5643 crate::render::overlays::render_error_modal(area, buf, &mut self.error_modal, &ctx);
5644 }
5645 self.close_help_left_behind();
5646 if self.help.is_open() {
5647 crate::render::help::render_help(app_layout.main_view, buf, &mut self.help, &ctx);
5649 }
5650
5651 let footer = self.footer(main_view_content, progress_rows > 0);
5652 crate::render::footer::render_rule(app_layout.rule, buf, &ctx);
5653 let line = Rect {
5654 height: 1,
5655 ..app_layout.footer
5656 };
5657 let drawn = footer.render_line(line, buf, &ctx);
5658 if prompt_rows > 0 {
5659 crate::render::input_strip::render(
5660 Rect {
5661 y: line.y + 1,
5662 height: prompt_rows,
5663 ..line
5664 },
5665 buf,
5666 self,
5667 &ctx,
5668 );
5669 }
5670 if let Some(progress) = progress.filter(|_| progress_rows > 0) {
5671 crate::render::footer::render_progress(
5672 &progress,
5673 Rect {
5674 y: line.y + 1 + prompt_rows,
5675 height: 1,
5676 ..line
5677 },
5678 buf,
5679 &ctx,
5680 );
5681 }
5682 self.pointer.chips_drawn(
5683 drawn
5684 .iter()
5685 .map(|(rect, key)| (*rect, key.as_str()))
5686 .collect(),
5687 );
5688 if let Some(debug_area) = app_layout.debug {
5689 self.debug.render(debug_area, buf);
5690 }
5691 self.pointer.drawn();
5692
5693 crate::sanitize::sanitize_buffer(buf);
5699 }
5700}
5701
5702impl App {
5703 pub fn capture_view(&self) -> Result<Option<LazyFrame>> {
5708 let Some(state) = &self.data_table_state else {
5709 return Ok(None);
5710 };
5711 if state.scans_a_download() {
5712 return Err(color_eyre::eyre::eyre!(
5713 "cannot return this view: the data was downloaded to a temporary file \
5714 that is removed when datui exits. Export it from inside datui (press \
5715 e) instead."
5716 ));
5717 }
5718 if state.scans_a_temp_file() {
5719 return Err(color_eyre::eyre::eyre!(
5720 "cannot return this view: the data was decompressed or converted into a \
5721 temporary file that is removed when datui exits. Export it from \
5722 inside datui (press e) instead."
5723 ));
5724 }
5725 Ok(Some(state.visible_lf()))
5726 }
5727}
5728
5729impl Drop for App {
5730 fn drop(&mut self) {
5731 self.counting.stop_footer_pass();
5735 self.counting.stop_indexing();
5738 }
5739}
5740
5741#[cfg(feature = "cloud")]
5746pub(crate) fn wait_on_runtime<F>(runtime: &tokio::runtime::Handle, future: F) -> Option<F::Output>
5747where
5748 F: std::future::Future + Send + 'static,
5749 F::Output: Send + 'static,
5750{
5751 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5752 runtime.spawn(async move {
5753 let _ = tx.send(future.await);
5754 });
5755 rx.recv().ok()
5756}
5757
5758fn sentence(text: &str) -> String {
5760 let mut chars = text.chars();
5761 match chars.next() {
5762 Some(first) => first.to_uppercase().chain(chars).collect(),
5763 None => String::new(),
5764 }
5765}