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 Terminal(crossterm::event::Event),
155 Wake,
158 TerminalBackground(ThemeMode),
160 TerminalFocused,
162 SettingsRead(Box<Result<app::startup::Settings>>),
164 OpenNamed(Vec<PathBuf>, OpenOptions),
167 NamedPathMissing(PathBuf),
169 Open(Vec<PathBuf>, OpenOptions),
171 OpenLazyFrame(Box<LazyFrame>, OpenOptions),
173 HomeListingReady {
175 generation: u64,
176 listing: Box<crate::home::Listing>,
177 known: std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
179 visits: std::collections::HashMap<PathBuf, crate::cache::Visits>,
181 newest: Option<PathBuf>,
183 folds: Option<std::collections::HashMap<String, bool>>,
185 },
186 HomeListingFailed,
188 HomePathListed {
190 listing: Box<crate::home::PathListing>,
191 },
192 HomePathCompleted {
194 generation: u64,
195 typed: String,
197 completed: String,
198 candidates: usize,
199 },
200 HomePreviewReady {
203 path: PathBuf,
204 stamp: crate::home::home_preview::Stamp,
206 read_at: Option<crate::home::home_preview::Stamp>,
208 rows: Option<Arc<crate::home::home_preview::PreviewRows>>,
209 prepared: crate::home::home_preview::Handoff,
210 },
211 HomeSchemaReady {
213 generation: u64,
214 path: PathBuf,
215 preview: Option<crate::home::discover::SchemaPreview>,
216 },
217 HomeMeasured {
221 measured: Vec<(PathBuf, crate::home::Measured)>,
222 done: bool,
223 },
224 HomeSized {
226 path: PathBuf,
227 measured: crate::home::Measured,
228 },
229 HomeWebGone {
231 path: PathBuf,
232 gone: crate::error_display::HttpGone,
233 },
234 HomeClassified {
237 measured: Vec<(PathBuf, crate::home::Measured)>,
238 done: bool,
239 },
240 HomeSearchBatch {
242 generation: u64,
243 root: PathBuf,
244 found: Vec<crate::home::discover::Entry>,
245 scanned: usize,
246 },
247 HomeSearchScored {
249 epoch: u64,
250 matches: Option<Box<crate::home::search::Matches>>,
252 },
253 HomeSearchDone {
255 generation: u64,
256 root: PathBuf,
257 scanned: usize,
258 limited: Option<String>,
259 },
260 #[cfg(feature = "cloud")]
263 HomeCloudSources {
264 sources: Vec<crate::home::CloudSource>,
265 },
266 #[cfg(feature = "cloud")]
268 HomeCloudListed {
269 id: String,
270 buckets: Vec<PathBuf>,
271 details: Vec<(PathBuf, Vec<(String, String)>)>,
273 failure: Option<(String, String)>,
275 listed_at: std::time::SystemTime,
276 },
277 HomeProbeReady {
279 root: PathBuf,
280 rows: Option<Vec<crate::home::discover::Entry>>,
281 cut_short: bool,
283 },
284 HomeProbeProgress {
286 root: PathBuf,
287 rows: Vec<crate::home::discover::Entry>,
288 },
289 HomeCloudKinds {
291 kinds: Vec<(
292 PathBuf,
293 (
294 crate::home::discover::EntryKind,
295 crate::home::discover::Holds,
296 ),
297 )>,
298 failed: Vec<PathBuf>,
300 },
301 HomeProbeCancelled {
304 root: PathBuf,
305 },
306 HomeNarrowed {
309 dir: PathBuf,
310 prefix: String,
311 listed: Option<(Vec<crate::home::discover::Entry>, bool)>,
312 },
313 HomeProbeFailed {
315 root: PathBuf,
316 message: String,
317 },
318 Followed(crate::loading::follow::News),
320 Exit,
321 Crash(String),
322 Applied(Applied),
324 Collect,
325 Update,
326 Reset,
327 Resize(u16, u16), Scroll(Scroll),
330 AnalysisCompute(analysis::analysis_modal::AnalysisTool),
332 BackgroundQualitySampleKept {
334 kept: KeptQualitySample,
335 },
336 BackgroundQualityCopyKept {
339 dataset_generation: u64,
340 copy: Option<Arc<crate::cloud::local_copy::LocalCopy>>,
341 },
342 BackgroundLenReady {
346 len_generation: u64,
347 num_rows: usize,
348 file_row_groups: Option<Vec<Vec<usize>>>,
350 },
351 BackgroundLenFailed {
354 len_generation: u64,
355 },
356 FramePainted,
359 LookThenOpenDirectory(PathBuf, OpenOptions),
363 ClassifyThenOpen {
366 path: PathBuf,
367 jump: bool,
369 },
370 JobEnded(Ticket),
372 JobProgress {
374 ticket: Ticket,
375 progress: Progress,
376 },
377}
378
379impl AppEvent {
380 pub fn is_progress(&self) -> bool {
383 matches!(
384 self,
385 AppEvent::HomeMeasured { done: false, .. }
386 | AppEvent::HomeClassified { done: false, .. }
387 | AppEvent::HomeSearchBatch { .. }
388 | AppEvent::HomeProbeProgress { .. }
389 | AppEvent::JobProgress {
390 progress: Progress::QualityPhase(_),
391 ..
392 }
393 )
394 }
395}
396
397#[cfg(test)]
400type HomeWorkerDies = Box<dyn FnMut(&AppEvent) -> bool + Send>;
401
402#[cfg(test)]
404type FileFactsReader = Arc<
405 dyn Fn(&Path, Option<crate::formats::readers::Facts>) -> std::result::Result<FileFacts, String>
406 + Send
407 + Sync,
408>;
409
410pub type EventOutcome = Result<Option<AppEvent>, KeyEvent>;
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub enum WhatEnter {
419 OpensFile,
421 OpensDirectory,
424 GoesInside,
426 LooksFirst,
428 FoldsSection,
430 ShowsMore,
432 GoesUp,
434 ShowsHidden,
436 Explains,
439 OpensHex,
441 Nothing,
443}
444
445impl App {
446 pub fn what_enter_does(&self) -> WhatEnter {
448 let entry = match self.home.selected_row() {
449 Some(home::Row::Place { path, .. }) => {
451 return if home::place_is_browsable(&path) {
452 WhatEnter::GoesInside
453 } else {
454 WhatEnter::Explains
455 };
456 }
457 Some(home::Row::Header { .. }) => return WhatEnter::FoldsSection,
458 Some(home::Row::More { .. }) => return WhatEnter::ShowsMore,
459 Some(home::Row::Up { .. }) => return WhatEnter::GoesUp,
460 Some(home::Row::Hidden { .. }) => return WhatEnter::ShowsHidden,
461 None => return WhatEnter::Nothing,
462 Some(home::Row::Door { .. }) => return WhatEnter::OpensDirectory,
464 Some(home::Row::Entry { entry, .. }) => entry,
465 };
466 if entry.kind != home::discover::EntryKind::File
468 && self.home.bookmark(&entry.path).is_some()
469 {
470 return WhatEnter::OpensDirectory;
471 }
472 match entry.kind {
473 home::discover::EntryKind::Unknown => WhatEnter::LooksFirst,
474 home::discover::EntryKind::File if entry.enter_lists_tables() => WhatEnter::GoesInside,
476 home::discover::EntryKind::File => WhatEnter::OpensFile,
477 home::discover::EntryKind::Other
478 if matches!(
479 cloud::source::input_source(&entry.path),
480 cloud::source::InputSource::Local(_)
481 ) =>
482 {
483 WhatEnter::OpensHex
484 }
485 home::discover::EntryKind::Other => WhatEnter::Nothing,
486 home::discover::EntryKind::Hive | home::discover::EntryKind::MultiFile => {
487 WhatEnter::OpensDirectory
488 }
489 home::discover::EntryKind::Directory
491 | home::discover::EntryKind::Delta
492 | home::discover::EntryKind::Iceberg
493 | home::discover::EntryKind::Hudi => WhatEnter::GoesInside,
494 }
495 }
496}
497
498impl App {
499 fn was_fetched(
502 download: Option<&crate::cloud::download::TempDownload>,
503 path: Option<&Path>,
504 ) -> bool {
505 download.is_some() && path.is_some_and(cloud::source::is_remote_url)
506 }
507}
508
509#[derive(Clone)]
511pub enum RunInput {
512 Cli(Box<Args>),
515 Host(Box<Args>, Option<Box<LazyFrame>>),
518 Paths(Vec<PathBuf>, OpenOptions),
519 LazyFrame(Box<LazyFrame>, OpenOptions),
520}
521
522#[derive(Debug, Default, PartialEq, Eq)]
524pub enum InputMode {
525 #[default]
526 Normal,
527 Home,
529 Editing,
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum InputType {
535 Query,
537 Find,
538}
539
540struct QueryRun {
543 origin: RunOrigin,
544 frame: u64,
547 rollback: crate::table::ViewRollback,
548 counts: loading::counting::CountMarkers,
551 rows: Option<usize>,
553}
554
555enum RunOrigin {
557 Query(QueryMode),
559 View {
562 previous: Option<String>,
563 matched: Option<(String, view::MatchReason)>,
564 },
565}
566
567fn recording_label(spool: &crate::loading::follow::Spool) -> (String, bool) {
571 let dot = crate::glyphs::get().middot;
572 let size = crate::numfmt::bytes(spool.bytes());
573 match spool.ended() {
574 None => (
575 format!(
576 "rec {size} {dot} {}/s",
577 crate::numfmt::bytes(spool.rate() as u64)
578 ),
579 false,
580 ),
581 Some(None) => {
582 let secs = spool.duration().as_secs();
583 let length = if secs >= 3600 {
584 format!("{}:{:02}:{:02}", secs / 3600, secs / 60 % 60, secs % 60)
585 } else {
586 format!("{}:{:02}", secs / 60, secs % 60)
587 };
588 match spool.tee().filter(|tee| !tee.to_stdout()) {
589 Some(tee) => (
590 format!("saved {size} {dot} {length} {dot} {}", tee.name()),
591 false,
592 ),
593 None => (format!("sent {size} {dot} {length}"), false),
594 }
595 }
596 Some(Some(reason)) => (format!("stopped: {reason}"), true),
597 }
598}
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
602pub(crate) enum Leaving {
603 Quit,
604 Home,
605}
606
607#[derive(Clone, Debug)]
609pub struct ExportProgress {
610 pub file_path: PathBuf,
611 pub current_phase: String,
612 pub written: Option<u64>,
613}
614
615impl ExportProgress {
616 pub fn new(file_path: &Path, phase: &str) -> Self {
618 Self {
619 file_path: file_path.to_path_buf(),
620 current_phase: phase.to_string(),
621 written: None,
622 }
623 }
624}
625
626fn correlations_of_sample(
628 lf: &LazyFrame,
629 sample: &analysis::sampling::Sample,
630 known_total: Option<usize>,
631 streaming: bool,
632) -> Result<crate::analysis::statistics::AnalysisResults> {
633 let schema = lf.clone().collect_schema()?;
634 let numeric: Vec<polars::prelude::Expr> = schema
635 .iter()
636 .filter(|(_, dtype)| dtype.is_numeric())
637 .map(|(name, _)| col(name.clone()))
638 .collect();
639 let rows = crate::analysis::sampling::read(
640 &lf.clone().select(numeric),
641 sample,
642 known_total,
643 streaming,
644 )?;
645 Ok(crate::analysis::statistics::AnalysisResults {
646 column_statistics: vec![],
647 total_rows: rows.total_rows,
648 sample_size: rows.sample_size,
649 per_value: rows.per_value.map(|per_value| per_value.kept),
650 correlation_matrix: crate::analysis::statistics::compute_correlation_matrix(&rows.df).ok(),
651 distribution_analyses: vec![],
652 })
653}
654
655fn active_query_settings(
658 dsl_query: &str,
659 sql_query: &str,
660 fuzzy_query: &str,
661) -> (Option<String>, Option<String>, Option<String>) {
662 let sql_trimmed = sql_query.trim();
663 let fuzzy_trimmed = fuzzy_query.trim();
664 let dsl_trimmed = dsl_query.trim();
665 if !sql_trimmed.is_empty() {
666 (None, Some(sql_trimmed.to_string()), None)
667 } else if !fuzzy_trimmed.is_empty() {
668 (None, None, Some(fuzzy_trimmed.to_string()))
669 } else if !dsl_trimmed.is_empty() {
670 (Some(dsl_trimmed.to_string()), None, None)
671 } else {
672 (None, None, None)
673 }
674}
675
676pub(crate) fn view_settings_of(state: &DataTableState) -> view::ViewSettings {
678 let (query, sql_query, fuzzy_query) = active_query_settings(
679 state.get_active_query(),
680 state.get_active_sql_query(),
681 state.get_active_fuzzy_query(),
682 );
683 view::ViewSettings {
684 chart: None,
685 sample: saved_sample_of(state),
686 query,
687 sql_query,
688 fuzzy_query,
689 filters: state.get_filters().to_vec(),
690 sort_columns: state.get_sort_columns().to_vec(),
691 sort_descending: state.get_sort_descending().to_vec(),
692 sort_ascending: state.get_sort_ascending(),
693 column_order: state.get_column_order().to_vec(),
694 locked_columns_count: state.locked_columns_count(),
695 pivot: state.last_pivot_spec().cloned(),
696 melt: state.last_melt_spec().cloned(),
697 reshape_source: state.reshape_source().cloned(),
698 columns: state.column_changes().to_vec(),
699 }
700}
701
702fn saved_sample_of(state: &DataTableState) -> Option<view::SavedSample> {
705 let sampled = state.sampled()?;
706 let through = sampled.through().then(|| view::ViewSettings {
708 sample: None,
709 chart: None,
710 ..view_settings_of(sampled.source())
711 });
712 Some(view::SavedSample::of(
713 sampled.sample(),
714 sampled.path(),
715 through,
716 ))
717}
718
719pub(crate) enum Replayed {
721 Planned,
723 Pivot(Box<crate::table::PivotJob>),
725}
726
727const QUALITY_RUN_WAITS: &str = "Run waits: the cancelled run is still stopping";
729
730const ANALYSIS_READ_WAITS: &str = "A cancelled run is still finishing; try again shortly";
733
734const CANCEL_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
737
738pub struct App {
739 pub data_table_state: Option<DataTableState>,
740 counting: loading::counting::Counting,
742 pub home_app: home::home_app::HomeApp,
744 pub home: home::HomeState,
746 source: loading::open_scan::OpenedSource,
748 path: Option<PathBuf>,
749 pipes: app::run::Pipes,
751 events: Sender<AppEvent>,
752 debug: DebugState,
753 pub info_modal: InfoModal,
754 pub info: app::keys::info_keys::InfoState,
756 pub prompt: query::query_prompt::QueryPrompt,
758 pub input_mode: InputMode,
759 pub overlay: Overlay,
761 pub sort_filter_modal: SortFilterModal,
762 pub pivot_melt_modal: PivotMeltModal,
763 pub view_modal: ViewModal,
764 pub analysis_modal: AnalysisModal,
765 pub sample: analysis::sample_draw::SampleState,
767 quality: analysis::quality_runs::QualityRuns,
769 pub chart: chart::chart_jobs::Charts,
771 pub export_modal: ExportModal,
772 pub copy_modal: app::modals::copy_modal::CopyModal,
773 pub inspector_modal: inspector::inspector_modal::InspectorModal,
774 external: app::run::External,
776 pub pickers: app::keys::picker_keys::Pickers,
778 pub value_counts: analysis::value_counts_modal::ValueCountsModal,
780 pub column_forms: app::keys::retype_keys::ColumnForms,
782 pub hex: app::keys::hex_keys::HexState,
784 error_modal: ErrorModal,
785 flash: Option<Flash>,
786 pub confirmation_modal: ConfirmationModal,
787 help: app::help::Help,
789 pointer: app::pointer::Pointing,
791 context_menu: Option<app::context_menu::ContextMenu>,
793 cache: CacheManager,
794 cache_writes: CacheWrites,
796 views: view::view_apply::SavedViews,
798 export_progress: Option<ExportProgress>,
800 theme: Theme, display: render::context::DisplaySettings,
803 runtime: tokio::runtime::Handle, jobs: Jobs,
806 loading: loading::Loader,
808 dataset_generation: u64,
812 #[cfg(test)]
814 file_facts_reader: Option<FileFactsReader>,
815 busy: bool,
817 screen_generation: u64,
821 input_dropped: bool,
823 throbber_frame: u8,
825 status_message: Option<String>,
828 app_config: AppConfig,
829 formats: Arc<crate::formats::Registry>,
831}
832
833impl App {
834 pub fn is_busy(&self) -> bool {
837 self.busy || self.jobs.holds_keys() || self.loading.waits()
838 }
839
840 pub fn task_generation(&self) -> u64 {
842 self.jobs.generation()
843 }
844
845 pub fn job_is_current(&self, ticket: Ticket) -> bool {
847 self.jobs.is_current(ticket)
848 }
849
850 pub fn use_cache(&mut self, cache: CacheManager) {
853 self.cache = cache;
854 }
855
856 pub fn use_catalog_dir(&mut self, dir: &Path) -> Result<()> {
859 self.app_config.read_catalog_files(Some(dir))
860 }
861
862 pub fn open_path(&self) -> Option<&Path> {
864 self.path.as_deref()
865 }
866
867 fn reads_stdin(&self) -> bool {
869 self.source.opened.as_ref().is_some_and(
870 |(paths, _)| matches!(paths.as_slice(), [path] if loading::stdin::is_stdin(path)),
871 )
872 }
873
874 fn view_dataset(&self) -> Option<view::Dataset<'_>> {
877 self.data_table_state.as_ref()?;
878 let path = match self.path.as_deref() {
879 Some(path) if !self.reads_stdin() => path,
880 _ => Path::new(loading::stdin::PATH),
881 };
882 Some(view::Dataset {
883 path,
884 table: self.view_table(),
885 })
886 }
887
888 fn view_table(&self) -> Option<&str> {
891 let (_, options) = self.source.opened.as_ref()?;
892 options.table.as_deref()
893 }
894
895 #[doc(hidden)]
898 pub fn settle_cache_writes(&self) {
899 self.cache_writes.settle();
900 }
901
902 pub fn background_work_in_flight(&self) -> bool {
904 self.jobs.in_flight()
905 }
906
907 pub fn screen_generation(&self) -> u64 {
909 self.screen_generation
910 }
911
912 pub fn modal_showing(&self) -> bool {
914 self.error_modal.active || self.confirmation_modal.active
915 }
916
917 fn analysis_results_are_sampled(&self) -> bool {
919 self.analysis_modal.view == analysis::analysis_modal::AnalysisView::Main
920 && self.analysis_modal.computing.is_none()
921 && self
922 .analysis_modal
923 .current_results()
924 .is_some_and(|r| r.sample_size.is_some())
925 }
926
927 pub(crate) fn cancelled_analysis_running(&self) -> Option<std::time::Instant> {
930 self.cancelled_analysis().map(|(since, _)| since)
931 }
932
933 fn cancelled_analysis(&self) -> Option<(std::time::Instant, bool)> {
936 let (since, job) = self.jobs.cancelled_running(Self::is_analysis_read)?;
937 let runs_out = match job {
938 Job::Analysis(run) => run.runs_out,
939 _ => true,
940 };
941 Some((since, runs_out))
942 }
943
944 fn is_analysis_read(job: &Job) -> bool {
946 matches!(job, Job::Analysis(_) | Job::SampleRows)
947 }
948
949 pub(crate) fn cancelled_run_shown(&self) -> Option<crate::widgets::data_quality::Cancelling> {
952 let (since, read_runs_out) = self.cancelled_analysis()?;
953 (read_runs_out || since.elapsed() >= CANCEL_GRACE).then_some(
954 crate::widgets::data_quality::Cancelling {
955 since,
956 read_runs_out,
957 },
958 )
959 }
960
961 fn cancelled_work_running(&self) -> bool {
963 self.jobs.running_behind()
964 }
965
966 fn read_waits_for_cancelled(&mut self) -> bool {
968 if self.cancelled_analysis_running().is_none() {
969 return false;
970 }
971 self.flash_note(ANALYSIS_READ_WAITS.to_string());
972 true
973 }
974
975 fn spawn_analysis(&mut self, tool: analysis::analysis_modal::AnalysisTool) -> Option<AppEvent> {
978 use analysis::analysis_modal::AnalysisTool;
979 type Compute = fn(
980 &LazyFrame,
981 &analysis::sampling::Sample,
982 Option<usize>,
983 bool,
984 ) -> Result<crate::analysis::statistics::AnalysisResults>;
985 type Install = fn(
986 &mut analysis::analysis_modal::AnalysisModal,
987 crate::analysis::statistics::AnalysisResults,
988 );
989 let (status, compute, install): (&str, Compute, Install) = match tool {
990 AnalysisTool::DataQuality => return self.run_quality_compute(),
991 AnalysisTool::Describe => (
992 "Running analysis...",
993 |lf, sample, known, streaming| {
994 crate::analysis::statistics::compute_describe_from_lazy(
995 lf, known, sample, streaming,
996 )
997 },
998 |modal, results| modal.describe_results = Some(results),
999 ),
1000 AnalysisTool::DistributionAnalysis => (
1001 "Analyzing distributions...",
1002 |lf, sample, known, streaming| {
1003 let options = crate::analysis::statistics::ComputeOptions {
1004 include_distribution_info: true,
1005 include_distribution_analyses: true,
1006 include_correlation_matrix: false,
1007 include_skewness_kurtosis_outliers: true,
1008 polars_streaming: streaming,
1009 };
1010 crate::analysis::statistics::compute_statistics_for_sample(
1011 lf, sample, known, options,
1012 )
1013 },
1014 |modal, results| modal.distribution_results = Some(results),
1015 ),
1016 AnalysisTool::CorrelationMatrix => (
1017 "Computing correlation matrix...",
1018 correlations_of_sample,
1019 analysis::analysis_modal::AnalysisModal::install_correlations,
1020 ),
1021 };
1022 let Some(state) = &self.data_table_state else {
1023 self.analysis_modal.computing = None;
1024 self.busy = false;
1025 return None;
1026 };
1027 let (source, known_total) = self.sample_source(state);
1029 let streaming = match tool {
1030 AnalysisTool::CorrelationMatrix => state.polars_streaming(),
1031 _ => self.app_config.performance.streaming,
1032 };
1033 let sample = self.analysis_modal.sample.clone();
1034 self.spawn_job(
1035 Job::Analysis(app::jobs::AnalysisRun::default()),
1036 Some(status),
1037 move |_| {
1038 let results = source
1039 .cut(&sample.scope)
1040 .and_then(|lf| compute(&lf, &sample, known_total, streaming))
1041 .map_err(|e| format!("{e}"))?;
1042 Ok(Answer::Analysis(install, results))
1043 },
1044 );
1045 None
1046 }
1047
1048 fn start_analysis_run(&mut self) -> Option<AppEvent> {
1050 let tool = self.analysis_modal.selected_tool?;
1051 if tool != analysis::analysis_modal::AnalysisTool::DataQuality
1052 && self.read_waits_for_cancelled()
1053 {
1054 return None;
1055 }
1056 let phase = match tool {
1057 analysis::analysis_modal::AnalysisTool::Describe => {
1058 self.analysis_modal.describe_results = None;
1059 "Describing data"
1060 }
1061 analysis::analysis_modal::AnalysisTool::DistributionAnalysis => {
1062 self.analysis_modal.distribution_results = None;
1063 "Analyzing distributions"
1064 }
1065 analysis::analysis_modal::AnalysisTool::CorrelationMatrix => {
1066 self.analysis_modal.correlation_results = None;
1067 "Computing correlations"
1068 }
1069 analysis::analysis_modal::AnalysisTool::DataQuality => return None,
1070 };
1071 self.analysis_modal.computing = Some(AnalysisProgress::new(phase));
1072 self.busy = true;
1073 Some(AppEvent::AnalysisCompute(tool))
1074 }
1075
1076 fn cancel_analysis(&mut self) {
1080 let reads_out = self
1083 .analysis_modal
1084 .computing
1085 .as_ref()
1086 .is_some_and(AnalysisProgress::read_runs_out);
1087 let mut read_runs_out = true;
1088 if let Some(Job::Analysis(run)) =
1089 self.jobs.current_mut(|job| matches!(job, Job::Analysis(_)))
1090 {
1091 read_runs_out = run.watch.is_none() || reads_out;
1092 run.runs_out = read_runs_out;
1093 if let Some(watch) = &run.watch {
1094 watch.cancel();
1095 }
1096 }
1097 let reading_sample = self
1098 .jobs
1099 .current(|job| matches!(job, Job::SampleRows))
1100 .is_some();
1101 self.jobs.cancel(Self::is_analysis_read);
1102 self.jobs.advance();
1103 self.screen_generation = self.screen_generation.wrapping_add(1);
1105 self.analysis_modal.computing = None;
1106 self.busy = false;
1107 self.status_message = None;
1108 if reading_sample {
1110 self.flash_note("Row view cancelled".to_string());
1111 return;
1112 }
1113 if self.analysis_modal.selected_tool
1117 == Some(analysis::analysis_modal::AnalysisTool::DataQuality)
1118 {
1119 self.open_quality_setup();
1120 if !read_runs_out {
1121 self.flash_note("Run cancelled".to_string());
1122 }
1123 return;
1124 }
1125 self.analysis_modal.selected_tool = None;
1126 self.analysis_modal.focus = analysis::analysis_modal::AnalysisFocus::Sidebar;
1127 self.flash_note("Analysis cancelled".to_string());
1128 }
1129
1130 pub(crate) fn pivot_computing(&self) -> bool {
1132 self.overlay == Overlay::PivotMelt
1133 && self.jobs.current(|job| matches!(job, Job::Pivot)).is_some()
1134 }
1135
1136 fn cancel_pivot(&mut self) {
1139 self.jobs.advance();
1140 self.screen_generation = self.screen_generation.wrapping_add(1);
1141 self.busy = false;
1142 self.status_message = None;
1143 self.flash_note("Pivot cancelled".to_string());
1144 }
1145
1146 fn drill_into(&mut self, group_index: usize, row: &DataFrame) {
1149 let Some(state) = self.data_table_state.as_mut() else {
1150 return;
1151 };
1152 let drilled = state.deferred(|s| s.drill_down_with_row(group_index, row));
1153 match drilled {
1154 Ok(()) => {
1155 self.sync_sort_filter_modal();
1156 self.spawn_async_collect(Self::LOADING_BUFFER);
1157 }
1158 Err(e) => self.flash_note(format!(
1159 "Could not drill in: {}",
1160 crate::error_display::user_message_from_report(&e, None)
1161 )),
1162 }
1163 }
1164
1165 #[doc(hidden)]
1167 pub fn read_stdin_from(&mut self, reader: impl std::io::Read + Send + 'static) {
1168 self.pipes.stdin_reader = Some(Box::new(reader));
1169 }
1170
1171 #[doc(hidden)]
1173 pub fn pass_stdout_to(&mut self, out: impl std::io::Write + Send + 'static) {
1174 self.pipes.stdout_pass = Some(Box::new(out));
1175 }
1176
1177 pub fn follow(&self) -> Option<&crate::loading::follow::Follow> {
1179 self.data_table_state.as_ref()?.follow()
1180 }
1181
1182 #[doc(hidden)]
1185 pub fn follow_settled(&self) -> bool {
1186 self.rows_in_flight().is_none() && !self.follow().is_some_and(|f| f.behind())
1187 }
1188
1189 #[doc(hidden)]
1191 pub fn check_follow_now(&self) {
1192 if let Some(follow) = self.follow() {
1193 follow.check_now();
1194 }
1195 }
1196
1197 fn followed(&mut self, news: &crate::loading::follow::News) {
1199 let Some(state) = self.data_table_state.as_mut() else {
1200 return;
1201 };
1202 let Some(follow) = state.follow_mut().filter(|f| f.id() == news.id) else {
1203 return;
1204 };
1205 let message = follow.take(&news.change);
1206 let fields = follow.take_new_fields();
1207 if let Some(handle) = follow.take_held() {
1208 state.read_followed_through(&handle);
1209 }
1210 if let Some(message) = message {
1211 self.flash_note(message);
1212 }
1213 if !fields.is_empty() {
1214 self.counting.followed_fields_held = Some((self.dataset_generation, fields));
1215 }
1216 self.catch_up_follow();
1217 self.join_followed_fields();
1218 self.describe_ended_journal();
1219 }
1220
1221 fn describe_ended_journal(&mut self) {
1224 let Some(lf) = self
1225 .data_table_state
1226 .as_mut()
1227 .and_then(|state| state.ended_journal_to_describe())
1228 else {
1229 return;
1230 };
1231 let dataset = self.dataset_generation;
1232 self.spawn_job(Job::JournalDetail { dataset }, None, move |_| {
1233 crate::formats::journal::summary(&lf)
1234 .map(|detail| Answer::JournalDescribed(Box::new(detail)))
1235 .map_err(|e| e.to_string())
1236 });
1237 }
1238
1239 fn join_followed_fields(&mut self) {
1242 let Some((generation, _)) = self.counting.followed_fields_held.as_ref() else {
1243 return;
1244 };
1245 if *generation != self.dataset_generation {
1246 self.counting.followed_fields_held = None;
1247 return;
1248 }
1249 if self.data_table_state.is_none()
1251 || self.work_the_join_would_cancel()
1252 || self.follow().is_some_and(|f| f.behind())
1253 {
1254 return;
1255 }
1256 let Some((generation, fields)) = self.counting.followed_fields_held.take() else {
1257 return;
1258 };
1259 let Some(state) = self.data_table_state.as_mut() else {
1260 return;
1261 };
1262 match state.join_followed_fields(&fields) {
1263 Ok(true) => {
1264 self.spawn_async_collect(Self::LOADING_BUFFER);
1265 }
1266 Ok(false) => {}
1267 Err(()) => self.counting.followed_fields_held = Some((generation, fields)),
1268 }
1269 }
1270
1271 fn catch_up_follow(&mut self) {
1275 if !self.in_normal_table_view()
1276 || self.is_busy()
1277 || self.loading.awaiting_dataset()
1278 || self.rows_in_flight().is_some()
1279 {
1280 return;
1281 }
1282 self.take_follow_rows(true);
1283 }
1284
1285 fn take_follow_rows(&mut self, read: bool) -> bool {
1288 let Some(state) = self.data_table_state.as_mut() else {
1289 return false;
1290 };
1291 let on_last_row = state.on_last_row();
1292 let counted = state.is_num_rows_valid();
1293 let drawn = state.visible_rows > 0 && counted;
1294 let Some(follow) = state.follow_mut() else {
1295 return false;
1296 };
1297 let settle = read && drawn && std::mem::take(&mut follow.settle_at_end);
1300 let to_end = settle || (read && counted && std::mem::take(&mut follow.end_pending));
1301 let stale = read && std::mem::take(&mut follow.stale_view);
1302 let at_bottom = to_end || on_last_row || follow.end_pending;
1303 if at_bottom {
1304 follow.new_below = 0;
1305 }
1306 if !follow.behind() {
1307 if (to_end && state.scroll_to_end()) || stale {
1308 self.spawn_collect(None);
1309 }
1310 return false;
1311 }
1312 let before = follow.shown();
1313 let (rows, restarted) = follow.catch_up();
1314 if at_bottom {
1315 follow.end_pending = true;
1316 } else if !restarted {
1317 follow.new_below += rows.saturating_sub(before);
1318 }
1319 follow.stale_view = !read;
1320 state.follow_to(rows, restarted);
1321 if at_bottom && read {
1322 if state.is_num_rows_valid() {
1323 state.aim_at_end();
1324 } else {
1325 self.counting.end_after_count = Some(state.len_generation());
1327 }
1328 }
1329 if read && !self.spawn_collect(None) {
1330 self.catch_up_follow();
1332 }
1333 true
1334 }
1335
1336 fn follow_rows_waiting(&self) -> bool {
1339 self.follow().is_some_and(|f| f.behind()) && !self.loading.awaiting_dataset()
1340 }
1341
1342 pub fn recording(&self) -> Option<&Arc<crate::loading::follow::Spool>> {
1344 self.data_table_state.as_ref()?;
1345 self.source
1346 .opened
1347 .as_ref()
1348 .and_then(|(_, options)| options.spool.as_ref())
1349 .map(|handle| handle.spool())
1350 .filter(|spool| spool.tee().is_some())
1351 }
1352
1353 fn leaving_by(&self, key: &KeyEvent) -> Option<Leaving> {
1355 if !key.is_press() {
1356 return None;
1357 }
1358 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1359 match key.code {
1360 KeyCode::Char('q') if ctrl => Some(Leaving::Quit),
1361 KeyCode::Char('c') if ctrl => Some(Leaving::Quit),
1362 KeyCode::Char('o') if ctrl => Some(Leaving::Home),
1363 KeyCode::Char('Q') if !ctrl && self.in_normal_table_view() => Some(Leaving::Quit),
1364 KeyCode::Char('q') if !ctrl && self.in_normal_table_view() => {
1365 Some(if self.source.opened_from_home {
1366 Leaving::Home
1367 } else {
1368 Leaving::Quit
1369 })
1370 }
1371 _ => None,
1372 }
1373 }
1374
1375 fn ask_about_recording(&mut self, leaving: Leaving) {
1377 let Some(tee) = self.recording().and_then(|spool| spool.tee()) else {
1378 return;
1379 };
1380 let doing = if tee.to_stdout() {
1381 "passed on"
1382 } else {
1383 "recorded"
1384 };
1385 let message = format!(
1386 "Standard input is still being {doing} to {}. Stop recording, or keep \
1387 recording until the stream ends?",
1388 tee.name()
1389 );
1390 self.confirmation_modal.show_choice(
1391 message,
1392 "Stop recording",
1393 "Keep recording",
1394 Confirm::Leave(leaving),
1395 );
1396 }
1397
1398 fn leave_recording(&mut self, leaving: Leaving, stop: bool) -> Option<AppEvent> {
1400 let handle = self
1401 .source
1402 .opened
1403 .as_ref()
1404 .and_then(|(_, options)| options.spool.clone());
1405 if stop {
1406 if let Some(handle) = &handle {
1407 handle.spool().stop();
1408 }
1409 } else {
1410 self.pipes.recording_on = handle;
1412 }
1413 match leaving {
1414 Leaving::Quit => Some(AppEvent::Exit),
1415 Leaving::Home => {
1416 self.enter_home();
1417 None
1418 }
1419 }
1420 }
1421
1422 pub fn recording_after_exit(
1424 &mut self,
1425 ) -> Option<(
1426 crate::loading::follow::Tee,
1427 Arc<crate::loading::follow::SpoolHandle>,
1428 )> {
1429 let handle = self.pipes.recording_on.take()?;
1430 let spool = handle.spool();
1431 let tee = spool.tee()?.clone();
1432 spool.live().then_some((tee, handle))
1433 }
1434
1435 fn notice_recording_end(&mut self) -> bool {
1438 let Some(spool) = self.recording().cloned() else {
1439 return false;
1440 };
1441 let Some(ended) = spool.ended() else {
1442 self.pipes.recording_end_said = false;
1443 return false;
1444 };
1445 if std::mem::replace(&mut self.pipes.recording_end_said, true) {
1446 return false;
1447 }
1448 let said = match spool.tee() {
1449 Some(tee) if tee.to_stdout() => "Standard input ended".to_string(),
1450 Some(tee) => format!("Saved {}", tee.path.display()),
1451 None => String::new(),
1452 };
1453 match ended {
1454 Some(reason) => self.error_modal.show(reason),
1455 None => self.flash_note(said),
1456 }
1457 true
1458 }
1459
1460 fn flash_note(&mut self, message: String) {
1462 self.flash = Some(Flash::new(message));
1463 }
1464
1465 fn flash_path(&mut self, prefix: &str, path: &std::path::Path) {
1467 self.flash = Some(Flash::path(prefix, path));
1468 }
1469
1470 pub fn flash_message(&self) -> Option<&str> {
1472 self.flash.as_ref().map(|f| f.message.as_str())
1473 }
1474
1475 pub fn error_message(&self) -> Option<&str> {
1477 self.error_modal
1478 .active
1479 .then_some(self.error_modal.message.as_str())
1480 }
1481
1482 pub fn next_deadline(&self) -> Option<std::time::Instant> {
1485 let flash = self.flash.as_ref().map(|f| f.expires);
1486 let clock = self
1487 .follow()
1488 .filter(|f| f.standing == crate::loading::follow::Standing::Following)
1489 .and_then(|f| f.last_append)
1490 .map(crate::loading::follow::next_tick);
1491 let recording = self
1493 .recording()
1494 .filter(|spool| spool.live())
1495 .map(|_| std::time::Instant::now() + std::time::Duration::from_secs(1));
1496 flash.into_iter().chain(clock).chain(recording).min()
1497 }
1498
1499 pub fn tick_follow_clock(&mut self) -> bool {
1501 let ended = self.notice_recording_end();
1502 let now = self.follow_mark();
1503 if now == self.pipes.follow_drawn {
1504 return ended;
1505 }
1506 self.pipes.follow_drawn = now;
1507 true
1508 }
1509
1510 fn follow_mark(&self) -> Option<crate::render::footer::FollowMark> {
1512 use crate::loading::follow::Standing;
1513 if self.overlay == Overlay::Hex {
1515 return None;
1516 }
1517 let state = self.data_table_state.as_ref()?;
1518 let rows = |n: usize, what: &str| format!("{} {what}", crate::numfmt::group_chrome(n));
1519 let (rec, rec_stopped) = match self.recording().map(|spool| recording_label(spool)) {
1520 Some((label, stopped)) => (Some(label), stopped),
1521 None => (None, false),
1522 };
1523 let Some(follow) = state.follow().filter(|f| f.standing != Standing::Ended) else {
1524 return rec.is_some().then(|| crate::render::footer::FollowMark {
1525 rec,
1526 rec_stopped,
1527 ..Default::default()
1528 });
1529 };
1530 let waiting = follow.waiting();
1531 let (chip, note) = match follow.standing {
1532 Standing::Following if follow.is_pipe() => {
1534 let read = follow.spool().map_or(0, |spool| spool.bytes());
1535 let chip = format!(
1536 "reading stdin {} {}",
1537 crate::glyphs::get().middot,
1538 crate::numfmt::bytes(read)
1539 );
1540 let note = (follow.new_below > 0 && !state.on_last_row())
1541 .then(|| rows(follow.new_below, "new below"));
1542 (chip, note)
1543 }
1544 Standing::Following => {
1545 let chip = match follow.last_append {
1546 Some(at) => format!(
1547 "following {} {}",
1548 crate::glyphs::get().middot,
1549 crate::loading::follow::age(at.elapsed())
1550 ),
1551 None => "following".to_string(),
1552 };
1553 let note = if waiting > 0 && !self.in_normal_table_view() {
1554 Some(rows(waiting, "new rows"))
1556 } else if follow.new_below > 0 && !state.on_last_row() {
1557 Some(rows(follow.new_below, "new below"))
1558 } else {
1559 None
1560 };
1561 (chip, note)
1562 }
1563 _ => (
1564 "paused".to_string(),
1565 (waiting > 0).then(|| rows(waiting, "new rows")),
1566 ),
1567 };
1568 let misfits = follow.misfits();
1569 let refreshes = self.overlay == Overlay::ValueCounts
1572 || (self.overlay == Overlay::Chart && self.chart.modal.picker.is_none())
1573 || (self.overlay == Overlay::Analysis
1574 && self.analysis_modal.current_results().is_some());
1575 let key = if self.in_normal_table_view() {
1576 Some(match follow.standing {
1577 Standing::Paused => "Resume",
1578 _ => "Pause",
1579 })
1580 } else if refreshes && follow.behind() {
1581 Some("Refresh")
1582 } else {
1583 None
1584 };
1585 Some(crate::render::footer::FollowMark {
1586 key,
1587 chip: Some(chip),
1588 note,
1589 warning: (misfits > 0).then(|| {
1590 if misfits == 1 {
1591 "1 row does not fit".to_string()
1592 } else {
1593 rows(misfits, "rows do not fit")
1594 }
1595 }),
1596 rec,
1597 rec_stopped,
1598 })
1599 }
1600
1601 fn toggle_follow(&mut self) -> Option<AppEvent> {
1604 use crate::loading::follow::Standing;
1605 if let Some(follow) = self.data_table_state.as_mut().and_then(|s| s.follow_mut()) {
1606 match follow.standing {
1607 Standing::Following => follow.pause(),
1608 Standing::Paused => {
1609 follow.resume();
1610 self.catch_up_follow();
1611 }
1612 Standing::Ended => {}
1613 }
1614 return None;
1615 }
1616 let (paths, options) = self.source.opened.clone()?;
1617 if paths
1618 .iter()
1619 .any(|path| crate::loading::stdin::is_stdin(path))
1620 {
1621 self.flash_note("Standard input is followed from the start: datui -f -".to_string());
1622 return None;
1623 }
1624 if let Some(refusal) = crate::loading::follow::refuse_paths(&paths, &options) {
1625 self.flash_note(refusal);
1626 return None;
1627 }
1628 let options = OpenOptions {
1629 follow: true,
1630 ..options
1631 };
1632 self.set_loading_phase("Scanning input", 10);
1633 self.name_what_is_loading(paths[0].clone());
1634 Some(AppEvent::Open(paths, options))
1635 }
1636
1637 pub fn tick_flash(&mut self) -> bool {
1639 if self.flash.as_ref().is_some_and(Flash::expired) {
1640 self.flash = None;
1641 return true;
1642 }
1643 false
1644 }
1645
1646 pub fn flash_polars_warning(&mut self) -> bool {
1649 if !self.bar_is_free() {
1650 return false;
1651 }
1652 match logging::next_polars_warning() {
1653 Some(warning) => {
1654 self.flash_note(format!("Polars: {warning}"));
1655 true
1656 }
1657 None => false,
1658 }
1659 }
1660
1661 pub fn flash_background_panic(&mut self) -> bool {
1664 if !self.bar_is_free() {
1665 return false;
1666 }
1667 match logging::take_unreported_panic() {
1668 Some(message) => {
1669 self.flash_note(message);
1670 true
1671 }
1672 None => false,
1673 }
1674 }
1675
1676 fn bar_is_free(&self) -> bool {
1678 !self.is_busy()
1679 && self.flash.is_none()
1680 && !self.error_modal.active
1681 && !self.confirmation_modal.active
1682 }
1683
1684 pub fn set_input_dropped(&mut self, dropped: bool) {
1686 self.input_dropped = dropped;
1687 }
1688
1689 pub fn hard_escape_while_busy(&self, key: &KeyEvent) -> bool {
1693 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1694 let quit = ctrl && matches!(key.code, KeyCode::Char('q' | 'c'));
1695 let home = ctrl && key.code == KeyCode::Char('o');
1696 let cancel_analysis = self.overlay == Overlay::Analysis
1697 && self.analysis_modal.computing.is_some()
1698 && key.code == KeyCode::Esc;
1699 let cancel_pivot = self.pivot_computing() && key.code == KeyCode::Esc;
1700 let leave_quality_evidence =
1701 self.quality.evidence_return.is_some() && self.at_table() && key.code == KeyCode::Esc;
1702 let cancel_view = key.code == KeyCode::Esc && self.view_applying();
1703 let cancel_find = key.code == KeyCode::Esc && self.finding();
1704 let stop_sample =
1705 key.code == KeyCode::Esc && self.sample_drawing() && self.in_normal_table_view();
1706 let close_help = self.help.is_open()
1708 && matches!(key.code, KeyCode::Esc | KeyCode::F(1) | KeyCode::Char('?'));
1709 quit || home
1710 || close_help
1711 || cancel_analysis
1712 || cancel_pivot
1713 || cancel_view
1714 || cancel_find
1715 || stop_sample
1716 || leave_quality_evidence
1717 || self.confirmation_modal.active
1718 || self.input_mode == InputMode::Home
1719 }
1720
1721 fn column_cursor_key(key: &KeyEvent) -> Option<crate::widgets::column_paging::CursorMove> {
1724 use crate::widgets::column_paging::CursorMove;
1725 if key
1726 .modifiers
1727 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
1728 {
1729 return None;
1730 }
1731 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1732 match key.code {
1733 KeyCode::Left if shift => Some(CursorMove::PageLeft),
1734 KeyCode::Right if shift => Some(CursorMove::PageRight),
1735 KeyCode::Left | KeyCode::Char('h') => Some(CursorMove::Left),
1736 KeyCode::Right | KeyCode::Char('l') => Some(CursorMove::Right),
1737 KeyCode::Char('{') => Some(CursorMove::First),
1738 KeyCode::Char('}') => Some(CursorMove::Last),
1739 _ => None,
1740 }
1741 }
1742
1743 pub fn key_acts_while_busy(&self, key: &KeyEvent) -> bool {
1750 if self.hard_escape_while_busy(key) || self.menu_takes(key) {
1751 return true;
1752 }
1753 if self.key_acts_while_sampling(key) {
1754 return true;
1755 }
1756 if !self.in_normal_table_view() {
1757 return false;
1758 }
1759 let step = match key.code {
1762 KeyCode::Down | KeyCode::Char('j') => Some(1),
1763 KeyCode::Up | KeyCode::Char('k') => Some(-1),
1764 _ => None,
1765 };
1766 if let Some(step) = step {
1767 return !self.busy
1768 && !self.loading.waits()
1769 && self
1770 .jobs
1771 .keys_held_only_by(|job| matches!(job, Job::Rows(_) | Job::OwedRows { .. }))
1772 && self
1773 .data_table_state
1774 .as_ref()
1775 .is_some_and(|s| !s.scroll_would_trigger_collect(step));
1776 }
1777 matches!(
1778 key.code,
1779 KeyCode::Char('q')
1780 | KeyCode::Char('Q')
1781 | KeyCode::Char('#')
1783 | KeyCode::Char('<')
1784 | KeyCode::Char('>')
1785 | KeyCode::Char('=')
1786 | KeyCode::Char('w')
1787 | KeyCode::Char(',')
1788 | KeyCode::Char('D')
1789 | KeyCode::Left
1790 | KeyCode::Right
1791 | KeyCode::Char('h')
1792 | KeyCode::Char('l')
1793 | KeyCode::Char('{')
1794 | KeyCode::Char('}')
1795 | KeyCode::F(1)
1796 | KeyCode::Char('?')
1797 )
1798 }
1799
1800 pub fn in_normal_table_view(&self) -> bool {
1802 self.at_table()
1803 && !self.help.is_open()
1804 && !self.error_modal.active
1805 && !self.confirmation_modal.active
1806 && self.context_menu.is_none()
1807 }
1808
1809 fn render_drop_mark(&self, buf: &mut Buffer, ctx: &crate::render::context::RenderContext) {
1812 let Some(app::pointer::Drag::Move { column, over }) = self.pointer.drag() else {
1813 return;
1814 };
1815 let Some(state) = self.data_table_state.as_ref() else {
1816 return;
1817 };
1818 let Some((header, columns)) = state.drawn_header() else {
1819 return;
1820 };
1821 let order = state.get_column_order();
1822 let (Some(from), Some(to)) = (
1823 order.iter().position(|c| c == column),
1824 order.iter().position(|c| c == over),
1825 ) else {
1826 return;
1827 };
1828 let locked = state.locked_columns_count().min(order.len());
1830 if from == to || (from < locked) != (to < locked) {
1831 return;
1832 }
1833 let Some((left, right, _)) = columns.iter().find(|(_, _, name)| name == over) else {
1834 return;
1835 };
1836 let x = if to > from {
1837 *right
1838 } else {
1839 left.saturating_sub(1)
1840 };
1841 if !(header.x..header.right()).contains(&x) {
1842 return;
1843 }
1844 let g = crate::glyphs::get();
1845 for y in header.y..header.bottom() {
1846 let cell = &mut buf[(x, y)];
1847 cell.set_symbol(g.rule);
1848 cell.set_style(Style::default().fg(ctx.accent));
1849 }
1850 }
1851
1852 pub(crate) fn menu_showing(&self) -> bool {
1854 self.context_menu.is_some()
1855 && self.at_table()
1856 && self.data_table_state.is_some()
1857 && !self.help.is_open()
1858 && !self.error_modal.active
1859 && !self.confirmation_modal.active
1860 }
1861
1862 pub(crate) fn menu_takes(&self, key: &KeyEvent) -> bool {
1865 self.menu_showing()
1866 && key.modifiers.is_empty()
1867 && matches!(
1868 key.code,
1869 KeyCode::Up
1870 | KeyCode::Down
1871 | KeyCode::Char('j')
1872 | KeyCode::Char('k')
1873 | KeyCode::Enter
1874 | KeyCode::Esc
1875 )
1876 }
1877
1878 pub fn open_context_menu(&mut self, at: ratatui::layout::Position) {
1880 let combine = self
1882 .data_table_state
1883 .as_ref()
1884 .and_then(|state| {
1885 let column = state.current_column()?;
1886 state.schema().get(column).cloned()
1887 })
1888 .is_some_and(|dtype| {
1889 matches!(dtype, DataType::String | DataType::Date | DataType::Time)
1890 });
1891 self.context_menu = Some(app::context_menu::ContextMenu::with(
1892 at,
1893 app::context_menu::column_items(combine),
1894 ));
1895 }
1896
1897 pub fn close_context_menu(&mut self) {
1899 self.context_menu = None;
1900 }
1901
1902 pub fn choose_from_menu(&mut self, i: usize) -> Option<AppEvent> {
1904 let menu = self.context_menu.take()?;
1905 match menu.chosen(i)? {
1906 app::context_menu::MenuKey::Run(key) => Some(AppEvent::Press(key)),
1907 app::context_menu::MenuKey::Do(action) => {
1908 self.menu_action(action);
1909 None
1910 }
1911 _ => None,
1912 }
1913 }
1914
1915 pub fn drop_column(&mut self, column: &str, onto: &str) -> Option<AppEvent> {
1918 let state = self.data_table_state.as_ref()?;
1919 let mut order = state.headers();
1920 let locked = state.locked_columns_count().min(order.len());
1921 let from = order.iter().position(|c| c == column)?;
1922 let to = order.iter().position(|c| c == onto)?;
1923 if from == to || (from < locked) != (to < locked) {
1924 return None;
1925 }
1926 self.flash = None;
1927 self.data_table_state.as_mut()?.set_current_column(column);
1928 let moving = order.remove(from);
1929 order.insert(to, moving);
1930 let applied = &mut self.sort_filter_modal.sort.applied_order;
1933 if let (Some(i), Some(j)) = (
1934 applied.iter().position(|c| c == column),
1935 applied.iter().position(|c| c == onto),
1936 ) {
1937 let moving = applied.remove(i);
1938 applied.insert(j, moving);
1939 }
1940 Some(AppEvent::Applied(Applied::ColumnOrder(order, locked)))
1941 }
1942
1943 pub fn text_field_focused(&self) -> bool {
1946 match self.overlay {
1947 Overlay::None => match self.input_mode {
1948 InputMode::Editing => true,
1949 InputMode::Home => false,
1950 InputMode::Normal => false,
1951 },
1952 Overlay::Analysis => {
1953 self.analysis_modal.sample_scope_typing()
1954 || self.analysis_modal.quality_expected_typing()
1955 || self.analysis_modal.intent_typing()
1956 || self.analysis_modal.export_typing()
1957 }
1958 Overlay::View => {
1959 self.view_modal.mode != ViewModalMode::List
1960 && matches!(
1961 self.view_modal.form_focus,
1962 FormFocus::Name
1963 | FormFocus::Description
1964 | FormFocus::ExactPath
1965 | FormFocus::RelativePath
1966 | FormFocus::PathPattern
1967 | FormFocus::FilenamePattern
1968 )
1969 }
1970 Overlay::Export { .. } => matches!(
1971 self.export_modal.focus,
1972 ExportFocus::PathInput | ExportFocus::CsvDelimiter
1973 ),
1974 Overlay::Copy => self.copy_modal.picker.is_some(),
1975 Overlay::Inspect => self.inspector_modal.finding,
1976 Overlay::GoToColumn => true,
1977 Overlay::PickFormat | Overlay::Retype { .. } => true,
1978 Overlay::Combine { .. } => self.column_forms.combine.as_ref().is_some_and(|c| {
1979 c.picker.is_some() || c.focus == app::modals::retype_modal::CombineField::Name
1980 }),
1981 Overlay::PickTable => true,
1982 Overlay::Sample => self
1983 .sample
1984 .form
1985 .as_ref()
1986 .is_some_and(|form| form.field.is_text()),
1987 Overlay::SortFilter => self.sort_filter_modal.typing(),
1989 Overlay::PivotMelt => {
1990 self.pivot_melt_modal.picker.is_some()
1991 || self
1992 .pivot_melt_modal
1993 .is_text_row(self.pivot_melt_modal.focus)
1994 }
1995 Overlay::ChartExport => self.chart.export_modal.focus.is_text(),
1996 Overlay::Chart => self.chart.modal.picker.is_some(),
1997 Overlay::Info | Overlay::ValueCounts => false,
1998 Overlay::Hex => self
2000 .hex
2001 .view
2002 .as_ref()
2003 .is_some_and(|view| view.prompt.is_some() || view.picker.is_some()),
2004 }
2005 }
2006
2007 pub fn something_is_spinning(&self) -> bool {
2009 self.is_busy()
2010 || (self.row_count_pending() && !self.awaiting_open_confirmation())
2011 || (self.overlay == Overlay::Analysis && self.cancelled_analysis_running().is_some())
2013 || self.chart_preparing()
2014 || self.value_counts_computing()
2015 || (self.input_mode == InputMode::Home
2016 && (self.home.awaiting_listing().is_some()
2017 || self.home.sections_waiting()
2018 || !self.home.peeking.is_empty()))
2019 }
2020
2021 fn begin_frame(&mut self) {
2024 if self.counting.indexing_paused && self.input_mode != InputMode::Home {
2026 self.index_lines();
2027 }
2028 self.counting.footers_this_frame = self.footer_progress().reading();
2029 self.counting.listed_this_frame = self.footer_progress().listed();
2030 self.pointer.forget_drawn();
2032 if let Some(state) = self.data_table_state.as_mut() {
2033 state.forget_drawn();
2034 }
2035 }
2036
2037 pub fn footer_progress(&self) -> &Arc<crate::formats::schema_union::FooterProgress> {
2040 self.loading
2041 .progress()
2042 .unwrap_or(&self.counting.footer_progress)
2043 }
2044
2045 pub fn exit_sweep(&self) -> ExitSweep {
2048 ExitSweep(self.loading.unfinished().clone())
2049 }
2050
2051 const INDEXING_FOR_ROW: &'static str = "Reading lines to find the row...";
2053
2054 const COUNTING_FOR_END: &'static str = "Counting rows to find the end...";
2057
2058 const LOOKING: &'static str = "Looking...";
2061
2062 pub const LOOKING_AT_A_DIRECTORY: &'static str = "Looking at the directory";
2065
2066 pub const LOADING_BUFFER: &'static str = "Loading buffer...";
2068
2069 const APPLYING_VIEW: &'static str = "Applying view...";
2071
2072 const READING_GROUP: &'static str = "Reading the group...";
2074
2075 const READING_FIELDS: &'static str = "Reading fields...";
2077
2078 const FIELD_COPY_INLINE_BYTES: usize = 1024 * 1024;
2080 const JSON_COPY_MAX_BYTES: usize = 64 * 1024 * 1024;
2082 const READING_JSON: &'static str = "Reading JSON...";
2084
2085 const COMPUTING_PIVOT: &'static str = "Computing pivot...";
2087
2088 const A_FETCH_WORTH_SAYING: std::time::Duration = std::time::Duration::from_millis(300);
2090
2091 fn load_ahead(&mut self) {
2095 if self.is_busy()
2098 || self.jobs.owed(Self::owed_rows).is_some()
2099 || self.rows_in_flight().is_some()
2100 || self.work_a_bump_would_strand()
2101 {
2102 return;
2103 }
2104 let Some(state) = self.data_table_state.as_ref() else {
2105 return;
2106 };
2107 let position = state.buffer_position();
2110 if !state.wants_to_load_ahead() || self.counting.loaded_ahead_from == Some(position) {
2111 return;
2112 }
2113 self.counting.loaded_ahead_from = Some(position);
2114 self.spawn_collect(None);
2115 }
2116
2117 fn flash_view_applied(&mut self, name: &str, why: view::MatchReason) {
2119 self.flash_note(format!("View \"{name}\" applied: {}", why.as_str()));
2120 }
2121
2122 pub fn spawn_async_collect(&mut self, status: &str) -> bool {
2127 self.spawn_collect(Some(status))
2128 }
2129
2130 fn spawn_collect(&mut self, status: Option<&str>) -> bool {
2133 let held = self
2134 .data_table_state
2135 .as_ref()
2136 .is_some_and(|state| self.count_held_at_estimate(state));
2137 let Some(state) = self.data_table_state.as_mut() else {
2138 return false;
2139 };
2140
2141 let mut count = None;
2147 let generation = state.len_generation();
2148 if !state.is_num_rows_valid()
2149 && self.counting.len_count_inflight != Some(generation)
2150 && !state.counts_itself_later()
2153 && self.counting.len_count_failed != Some(generation)
2155 && !held
2157 {
2158 self.counting.len_count_inflight = Some(generation);
2159 count = Some(LenCount::for_state(state));
2160 }
2161 let footers = count.take_if(|job| job.reads_footers());
2162 if count.take_if(|_| !state.is_remote_source()).is_some() {
2163 self.counting.count_after_paint = Some(generation);
2164 }
2165 if let Some(job) = footers {
2166 self.spawn_count(job);
2167 }
2168
2169 let a_bump_would_strand = self.work_a_bump_would_strand();
2171 let inflight = self.rows_in_flight();
2172
2173 let Some(state) = self.data_table_state.as_mut() else {
2176 return false;
2177 };
2178 let covered = inflight.is_some_and(|inflight| inflight.covers(state));
2179 if covered
2181 && let Some(status) = status
2182 && !self.jobs.waited_on(Self::reading_rows)
2183 {
2184 self.jobs.wait_on(Self::reading_rows, status);
2185 self.busy = false;
2186 self.status_message = Some(status.to_string());
2187 }
2188 let request = (!covered).then(|| state.prepare_async_collect(None));
2189 let Some(Some(request)) = request else {
2190 if let Some(job) = count {
2192 self.spawn_count(job);
2193 }
2194 return covered;
2195 };
2196 if a_bump_would_strand {
2201 if count.is_some() {
2204 self.counting.len_count_inflight = None;
2205 }
2206 let Some(status) = status else {
2208 return false;
2209 };
2210 self.jobs.take_owed(Self::owed_rows);
2212 let owed = Job::OwedRows {
2213 dataset: self.dataset_generation,
2214 status: status.to_string(),
2215 };
2216 self.jobs.owe(owed, Some(status));
2217 self.busy = false;
2218 self.status_message = Some(status.to_string());
2219 return true;
2220 }
2221 self.jobs.advance();
2222 self.home_app.reads.pages += 1;
2223 let inflight = InflightCollect {
2224 began: std::time::Instant::now(),
2225 files: state.files_a_page_reads(
2226 request.buffer_start,
2227 request.buffer_end.saturating_sub(request.buffer_start),
2228 ),
2229 dataset: state.len_generation(),
2230 columns: InflightCollect::columns_of(state),
2231 start: request.buffer_start,
2232 end: request.buffer_end,
2233 };
2234 let count = count.map(|job| OwedCount::new(job, self.events.clone()));
2236 self.spawn_job(Job::Rows(inflight), status, move |_| {
2237 let plan = request.plan;
2238 Ok(
2240 match crate::analysis::statistics::collect_lazy(
2241 request.lf,
2242 request.polars_streaming,
2243 ) {
2244 Ok(df) => {
2245 let returned = df.height();
2246 let requested = request.buffer_end - request.buffer_start;
2247 let start = request.buffer_start;
2248 Answer::Rows(plan.fit(df)).then(move || {
2250 if let Some(count) = count {
2251 count.answer(|job| job.after_collect(start, returned, requested));
2252 }
2253 })
2254 }
2255 Err(e) => Answer::RowsFailed {
2258 message: crate::error_display::user_message_from_polars(&e),
2259 conversion: crate::error_display::conversion_failure(&e).map(Box::new),
2260 }
2261 .then(move || drop(count)),
2262 },
2263 )
2264 });
2265 true
2266 }
2267
2268 fn spawn_job<F, R>(&mut self, job: Job, status: Option<&str>, work: F) -> Ticket
2273 where
2274 F: FnOnce(&app::jobs::Worker) -> std::result::Result<R, String> + Send + 'static,
2275 R: Into<app::jobs::Answered>,
2276 {
2277 let started = self.start_job(job, status);
2278 let ticket = started.ticket();
2279 started.run(&self.runtime, work);
2280 ticket
2281 }
2282
2283 fn start_job(&mut self, job: Job, status: Option<&str>) -> app::jobs::Started {
2286 if let Some(status) = status {
2287 self.busy = false;
2289 self.status_message = Some(status.to_string());
2290 }
2291 self.jobs.start(job, status)
2292 }
2293
2294 fn owed_rows(job: &Job) -> bool {
2295 matches!(job, Job::OwedRows { .. })
2296 }
2297
2298 fn reading_rows(job: &Job) -> bool {
2299 matches!(job, Job::Rows(_))
2300 }
2301
2302 fn rows_in_flight(&self) -> Option<InflightCollect> {
2304 match self.jobs.current(Self::reading_rows) {
2305 Some((_, Job::Rows(inflight))) => Some(*inflight),
2306 _ => None,
2307 }
2308 }
2309
2310 fn rows_waited_on(&self) -> bool {
2312 self.jobs.waited_on(Self::reading_rows)
2313 }
2314
2315 fn forget_the_rows_read(&mut self) {
2318 self.jobs
2319 .supersede(|job| Self::reading_rows(job) || Self::owed_rows(job));
2320 }
2321
2322 pub(crate) fn hold_the_generation(&self) -> app::jobs::Hold {
2325 self.jobs.hold()
2326 }
2327
2328 #[cfg(test)]
2330 pub(crate) fn job_for_tests(&mut self, job: Job, status: Option<&str>) -> app::jobs::Started {
2331 self.start_job(job, status)
2332 }
2333
2334 #[cfg(test)]
2336 pub(crate) fn nothing_loading(&self) -> bool {
2337 self.loading.current().is_none() && self.export_progress.is_none()
2338 }
2339
2340 #[cfg(test)]
2343 pub(crate) fn loading_for_tests(
2344 &mut self,
2345 path: Option<PathBuf>,
2346 size: u64,
2347 phase: &str,
2348 percent: u16,
2349 ) {
2350 self.announce_open(false, phase.to_string(), percent);
2351 if let Some(path) = path {
2352 self.loading.name(path);
2353 }
2354 self.loading.size_for_tests(size);
2355 }
2356
2357 #[cfg(test)]
2360 pub(crate) fn open_for_tests(&mut self, path: &str) -> loading::LoadId {
2361 self.put_down_load_in_flight();
2362 let _ = self.loading.open(loading::OpenRequest {
2363 paths: vec![PathBuf::from(path)],
2364 options: OpenOptions::default(),
2365 size: 0,
2366 recent: None,
2367 shown: None,
2368 warn_in_memory_above: None,
2369 });
2370 self.loading.id().expect("an open was begun")
2371 }
2372
2373 #[cfg(test)]
2376 pub(crate) fn install_for_tests(
2377 &mut self,
2378 state: DataTableState,
2379 path: Option<PathBuf>,
2380 options: &OpenOptions,
2381 debug_label: Option<String>,
2382 ) -> bool {
2383 self.put_down_load_in_flight();
2384 let _ = self
2385 .loading
2386 .open_frame(LazyFrame::default(), options.clone());
2387 let load = self.loading.id().expect("an open was begun");
2388 let loading::Step::Install(loaded) = self.loading.answered(
2389 load,
2390 loading::LoadAnswer::SchemaRead {
2391 state: Box::new(state),
2392 path,
2393 options: options.clone(),
2394 debug_label,
2395 },
2396 #[cfg(any(feature = "http", feature = "cloud"))]
2397 &self.jobs,
2398 ) else {
2399 unreachable!("a schema read installs");
2400 };
2401 let view = self.install_dataset(*loaded);
2402 self.first_rows_settled();
2403 view
2404 }
2405
2406 #[cfg(test)]
2408 pub(crate) fn owe_rows_for_tests(&mut self, status: &str) {
2409 let owed = Job::OwedRows {
2410 dataset: self.dataset_generation,
2411 status: status.to_string(),
2412 };
2413 self.jobs.owe(owed, Some(status));
2414 }
2415
2416 #[cfg(test)]
2418 pub(crate) fn rows_owed(&self) -> bool {
2419 self.jobs.owed(Self::owed_rows).is_some()
2420 }
2421
2422 #[cfg(test)]
2424 pub(crate) fn answer_for_tests(&mut self, job: Job, answer: Answer) -> Option<AppEvent> {
2425 let started = self.jobs.start(job, None);
2426 let ticket = started.ticket();
2427 started.end(Outcome::answered(answer));
2428 self.job_ended(ticket)
2429 }
2430
2431 fn work_a_bump_would_strand(&self) -> bool {
2434 self.jobs.would_strand()
2435 }
2436
2437 fn scroll_key(&mut self, scroll: Scroll) -> Option<AppEvent> {
2440 let state = self.data_table_state.as_mut()?;
2441 if state.scroll_would_trigger_collect(scroll.delta(state)) {
2442 self.busy = true;
2443 return Some(AppEvent::Scroll(scroll));
2444 }
2445 scroll.run(state);
2446 None
2447 }
2448
2449 fn jump_key(&mut self, jump: Scroll) -> Option<AppEvent> {
2452 if jump == Scroll::End
2458 && let Some(state) = self.data_table_state.as_ref()
2459 && state.indexing().is_some()
2460 {
2461 self.counting.end_when_indexed = Some(self.dataset_generation);
2462 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2463 return None;
2464 }
2465 if jump == Scroll::End
2466 && let Some(state) = self.data_table_state.as_ref()
2467 && state.footers_pending().is_some()
2468 && state.scan_is_the_root()
2469 && !state.is_num_rows_valid()
2470 {
2471 self.counting.end_when_the_footers_land = Some(self.dataset_generation);
2472 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2473 return None;
2474 }
2475 if jump == Scroll::End
2478 && let Some(state) = self.data_table_state.as_ref()
2479 && !state.is_num_rows_valid()
2480 {
2481 let generation = state.len_generation();
2482 self.counting.end_after_count = Some(generation);
2483 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
2484 let held = self.counting.count_after_paint == Some(generation);
2485 if held {
2486 self.counting.count_after_paint = None;
2487 }
2488 if held || self.counting.len_count_inflight != Some(generation) {
2489 self.counting.len_count_inflight = Some(generation);
2490 self.spawn_count(LenCount::for_state(state));
2491 }
2492 return None;
2493 }
2494 let state = self.data_table_state.as_mut()?;
2495 let already_there = match jump {
2496 Scroll::Start => state.start_row() == 0,
2497 _ => state.at_end(),
2498 };
2499 if already_there {
2500 jump.run(state);
2501 return None;
2502 }
2503 self.busy = true;
2504 Some(AppEvent::Scroll(jump))
2505 }
2506
2507 fn handle_scroll<F>(&mut self, scroll: F) -> Option<AppEvent>
2510 where
2511 F: FnOnce(&mut crate::table::DataTableState) -> bool,
2512 {
2513 let needs = self.data_table_state.as_mut().is_some_and(scroll);
2514 if !needs || !self.spawn_async_collect(Self::LOADING_BUFFER) {
2515 self.busy = false;
2516 self.status_message = None;
2517 }
2518 None
2519 }
2520
2521 pub fn new(events: Sender<AppEvent>, runtime: tokio::runtime::Handle) -> App {
2522 let theme = Theme::from_config(&AppConfig::default().theme).unwrap_or_else(|_| Theme {
2523 colors: std::collections::HashMap::new(),
2524 });
2525
2526 Self::new_with_config(events, runtime, theme, AppConfig::default())
2527 }
2528
2529 pub fn new_with_theme(
2530 events: Sender<AppEvent>,
2531 runtime: tokio::runtime::Handle,
2532 theme: Theme,
2533 ) -> App {
2534 Self::new_with_config(events, runtime, theme, AppConfig::default())
2535 }
2536
2537 pub fn new_with_config(
2539 events: Sender<AppEvent>,
2540 runtime: tokio::runtime::Handle,
2541 theme: Theme,
2542 app_config: AppConfig,
2543 ) -> App {
2544 let views = ViewManager::load_or_empty().into();
2545 Self::new_with_views(events, runtime, theme, app_config, views)
2546 }
2547
2548 pub fn new_with_views(
2550 events: Sender<AppEvent>,
2551 runtime: tokio::runtime::Handle,
2552 theme: Theme,
2553 app_config: AppConfig,
2554 view_manager: Views,
2555 ) -> App {
2556 let cache = CacheManager::new(APP_NAME).unwrap_or_else(|_| CacheManager {
2557 cache_dir: std::env::temp_dir().join(APP_NAME),
2558 });
2559 let jobs = Jobs::new(events.clone());
2560 let formats = Arc::new(crate::formats::Registry::load(
2561 &crate::formats::search_path_for(&app_config),
2562 ));
2563 for error in &formats.errors {
2564 log::warn!("format spec skipped: {error}");
2565 }
2566
2567 let theme_problem = app_config.theme.fallbacks.first().cloned();
2568 let chart_export_modal = ChartExportModal {
2569 recipe: app_config.chart.export_recipe,
2570 ..ChartExportModal::new()
2571 };
2572 let mut app = App {
2573 path: None,
2574 data_table_state: None,
2575 counting: loading::counting::Counting::default(),
2576 home: home::HomeState {
2577 hide_unreadable: !app_config.home.show_unreadable,
2578 formats: formats.clone(),
2579 ..Default::default()
2580 },
2581 home_app: home::home_app::HomeApp {
2582 local_desktop: app::link_open::local_desktop(
2583 app::link_open::Platform::current(),
2584 |name| std::env::var(name).ok(),
2585 ),
2586 ..Default::default()
2587 },
2588 source: loading::open_scan::OpenedSource::default(),
2589 pipes: app::run::Pipes::default(),
2590 events,
2591 debug: DebugState::default(),
2592 info_modal: InfoModal::new(),
2593 info: app::keys::info_keys::InfoState {
2594 head_web_rows: !cache::running_as_a_cargo_test(),
2595 ..Default::default()
2596 },
2597 prompt: query::query_prompt::QueryPrompt {
2598 query_input: TextInput::new()
2599 .with_history_limit(app_config.query.history_limit)
2600 .with_theme(&theme)
2601 .with_history("query".to_string()),
2602 sql_input: TextInput::statement()
2603 .with_history_limit(app_config.query.history_limit)
2604 .with_theme(&theme)
2605 .with_history("sql".to_string()),
2606 find: find::Find::new(
2607 TextInput::new()
2608 .with_history_limit(app_config.query.history_limit)
2609 .with_theme(&theme)
2610 .with_history("find".to_string()),
2611 ),
2612 column_hints: false,
2613 input_type: None,
2614 query_mode: QueryMode::default().resolve(),
2615 query_mode_chosen: None,
2616 query_text_restored: false,
2617 sql_columns: Vec::new(),
2618 sql_completion: None,
2619 query_running: None,
2620 query_run_error: None,
2621 inline_failures: 0,
2622 },
2623 input_mode: InputMode::Normal,
2624 overlay: Overlay::None,
2625 sort_filter_modal: SortFilterModal::new(),
2626 pivot_melt_modal: PivotMeltModal::new(),
2627 view_modal: ViewModal::new(),
2628 analysis_modal: AnalysisModal::with_sample_rows(app_config.analysis.sample_rows),
2629 sample: analysis::sample_draw::SampleState {
2630 form: None,
2631 memory_probe: std::sync::Arc::new(analysis::table_sample::available_memory),
2632 paths: Vec::new(),
2633 },
2634 quality: analysis::quality_runs::QualityRuns {
2635 memory_budget: QUALITY_MEMORY_BUDGET,
2636 ..Default::default()
2637 },
2638 chart: chart::chart_jobs::Charts {
2639 export_modal: chart_export_modal,
2640 ..Default::default()
2641 },
2642 export_modal: ExportModal::new(),
2643 copy_modal: app::modals::copy_modal::CopyModal::new(),
2644 inspector_modal: inspector::inspector_modal::InspectorModal::new(),
2645 external: app::run::External::default(),
2646 pickers: app::keys::picker_keys::Pickers::default(),
2647 value_counts: analysis::value_counts_modal::ValueCountsModal::default(),
2648 hex: app::keys::hex_keys::HexState::default(),
2649 column_forms: app::keys::retype_keys::ColumnForms::default(),
2650 error_modal: ErrorModal::new(),
2651 flash: None,
2652 confirmation_modal: ConfirmationModal::new(),
2653 help: app::help::Help::default(),
2654 pointer: app::pointer::Pointing::default(),
2655 context_menu: None,
2656 cache,
2657 cache_writes: CacheWrites::default(),
2658 views: view::view_apply::SavedViews {
2659 manager: view_manager,
2660 active_id: None,
2661 },
2662 export_progress: None,
2663 theme,
2664 display: render::context::DisplaySettings {
2665 history_limit: app_config.query.history_limit,
2666 table_cell_padding: app_config.display.cell_padding.cells(),
2667 column_colors: app_config.display.column_colors,
2668 dtype_row: app_config.display.type_row,
2669 number_format: app_config
2670 .display
2671 .number_format
2672 .resolve(app_config.display.right_align_numbers)
2673 .unwrap_or_else(|_| NumberFormatSettings {
2676 align_numeric_right: app_config.display.right_align_numbers,
2677 ..Default::default()
2678 }),
2679 background_query: false,
2680 },
2681 jobs,
2682 runtime,
2683 loading: loading::Loader::default(),
2684 dataset_generation: 0,
2685 #[cfg(test)]
2686 file_facts_reader: None,
2687 busy: false,
2688 throbber_frame: 0,
2689 screen_generation: 0,
2690 input_dropped: false,
2691 status_message: None,
2692 app_config,
2693 formats,
2694 };
2695 if let Some(problem) = theme_problem {
2697 app.flash_note(problem);
2698 }
2699 app
2700 }
2701
2702 pub fn set_formats(&mut self, registry: crate::formats::Registry) {
2704 let registry = Arc::new(registry);
2705 self.home.formats = registry.clone();
2706 self.formats = registry;
2707 }
2708
2709 pub fn enable_debug(&mut self) {
2710 self.debug.enabled = true;
2711 }
2712
2713 pub(crate) fn apply_owed_measurements(&mut self) {
2717 if std::mem::take(&mut self.home_app.measured_owed) {
2718 self.home.apply_measurements();
2719 }
2720 }
2721
2722 pub fn request_what_the_frame_needs(&mut self) {
2725 if self.at_table() {
2726 self.load_ahead();
2727 self.catch_up_follow();
2728 }
2729 self.inspector_needs();
2730 if self.input_mode != InputMode::Home {
2731 return;
2732 }
2733 self.apply_owed_measurements();
2734 if self.home_app.refresh_owed {
2735 self.home_refresh();
2736 }
2737 #[cfg(feature = "http")]
2738 self.size_selected_web_file();
2739 if !self.home.path_input_active {
2742 self.request_home_measurements();
2743 self.request_home_classifications();
2744 #[cfg(feature = "cloud")]
2745 self.peek_cloud_directories();
2746 }
2747 }
2748}
2749
2750impl App {
2751 pub fn help_visible(&self) -> bool {
2753 self.help.is_open()
2754 }
2755
2756 pub fn help_context(&self) -> Option<datui_cli::keys::Context> {
2758 self.help.context()
2759 }
2760
2761 pub(crate) fn open_help_overlay(&mut self) {
2763 if self.help.is_open() || self.confirmation_modal.active || self.error_modal.active {
2765 return;
2766 }
2767 let context = self.keys_context();
2768 let typing = self.text_field_focused()
2770 || (self.input_mode == InputMode::Home
2771 && !self.info.documentation.is_open()
2772 && (!self.home.filter.is_empty() || self.home.path_input_active));
2773 self.help.open(context, typing);
2774 }
2775
2776 fn close_help_left_behind(&mut self) {
2780 let left = self
2781 .help
2782 .context()
2783 .is_some_and(|shown| shown != self.keys_context());
2784 if left || self.confirmation_modal.active || self.error_modal.active {
2785 self.help.close();
2786 }
2787 }
2788
2789 pub fn keys_context(&self) -> datui_cli::keys::Context {
2791 use crate::analysis::analysis_modal::{AnalysisTool, AnalysisView};
2792 use datui_cli::keys::Context;
2793 match self.overlay {
2794 Overlay::Analysis => match self.analysis_modal.view {
2795 AnalysisView::DistributionDetail => Context::DistributionDetail,
2796 AnalysisView::CorrelationDetail => Context::CorrelationDetail,
2797 AnalysisView::Main => match self.analysis_modal.selected_tool {
2798 Some(AnalysisTool::DistributionAnalysis) => Context::Distribution,
2799 Some(AnalysisTool::CorrelationMatrix) => Context::Correlation,
2800 Some(AnalysisTool::DataQuality) => Context::DataQuality,
2801 Some(AnalysisTool::Describe) | None => Context::Describe,
2802 },
2803 },
2804 Overlay::View => Context::Views,
2805 Overlay::None => match self.input_mode {
2806 InputMode::Normal => Context::Table,
2807 InputMode::Editing => match self.prompt.input_type {
2808 Some(InputType::Find) => Context::Find,
2809 _ => Context::Query,
2810 },
2811 InputMode::Home if self.info.documentation.is_open() => Context::Documentation,
2812 InputMode::Home => Context::Home,
2813 },
2814 Overlay::SortFilter => Context::SortFilter,
2815 Overlay::PivotMelt => Context::PivotMelt,
2816 Overlay::Export { .. } => Context::Export,
2817 Overlay::Copy => Context::Copy,
2818 Overlay::Inspect => Context::Inspector,
2819 Overlay::GoToColumn => Context::GoToColumn,
2820 Overlay::PickFormat => Context::FormatPicker,
2821 Overlay::Retype { .. } => Context::Retype,
2822 Overlay::Combine { .. } => Context::Combine,
2823 Overlay::PickTable => Context::TablePicker,
2824 Overlay::Sample => Context::Sample,
2825 Overlay::Info => Context::Info,
2826 Overlay::Chart | Overlay::ChartExport => Context::Chart,
2827 Overlay::Hex => Context::Hex,
2828 Overlay::ValueCounts => Context::ValueCounts,
2829 }
2830 }
2831
2832 pub fn awaiting_open_confirmation(&self) -> bool {
2836 self.confirmation_modal.active
2837 && matches!(self.confirmation_modal.asking, Some(Confirm::Download))
2838 }
2839
2840 fn confirmed(&mut self) -> Option<AppEvent> {
2842 let stop = self.confirmation_modal.focus_yes;
2843 match self.confirmation_modal.take()? {
2844 Confirm::Leave(leaving) => self.leave_recording(leaving, stop),
2845 Confirm::ReadAll => {
2846 let sample = analysis::sampling::Sample {
2848 method: analysis::sampling::SampleMethod::EveryRow,
2849 ..self.analysis_modal.sample.clone()
2850 };
2851 self.apply_sample(sample)
2852 }
2853 Confirm::OpenLink(url) => Some(AppEvent::Applied(Applied::OpenLink(url))),
2854 Confirm::ClearRecents => {
2855 self.cache.clear_recents();
2856 self.home_refresh();
2857 self.home.status = None;
2858 None
2859 }
2860 Confirm::QualityFullScan => self.run_quality_setup(true),
2861 Confirm::HideExamples => {
2862 self.cache.hide_examples();
2863 self.home_refresh();
2864 self.home.select_first_entry();
2865 None
2866 }
2867 Confirm::DeleteView(id) => {
2868 if self.views.manager.delete_view(&id).is_ok() {
2869 self.refresh_view_list();
2870 }
2871 None
2872 }
2873 Confirm::ForgetPlace(place) => {
2874 let paths = self.home.recents_in(&place);
2875 self.cache.forget_recents(&paths);
2876 self.home_refresh();
2877 self.home.status = None;
2878 None
2879 }
2880 Confirm::QualityExport(path, format) => Some(AppEvent::Applied(
2882 Applied::QualityReportExport(path, format, Overwrite::Replace),
2883 )),
2884 Confirm::ChartExport(request) => Some(AppEvent::Applied(Applied::ChartExport(
2885 ChartExportRequest {
2886 overwrite: Overwrite::Replace,
2887 ..*request
2888 },
2889 ))),
2890 Confirm::Export(request) => Some(AppEvent::Applied(Applied::Export(ExportRequest {
2891 overwrite: Overwrite::Replace,
2892 ..*request
2893 }))),
2894 Confirm::Copy(format, header) => {
2895 Some(AppEvent::Applied(Applied::CopyTable { format, header }))
2896 }
2897 Confirm::Download => {
2898 let step = self.loading.confirmed();
2901 self.run_load_step(step)
2902 }
2903 }
2904 }
2905
2906 fn declined(&mut self) -> Option<AppEvent> {
2909 match self.confirmation_modal.take() {
2910 Some(Confirm::ChartExport(_)) => self.open_overlay(Overlay::ChartExport),
2911 Some(Confirm::Export(_)) => self.open_over(|returns_to| Overlay::Export { returns_to }),
2912 Some(Confirm::Download) => self.enter_home(),
2914 _ => {}
2915 }
2916 None
2917 }
2918
2919 fn key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
2920 self.debug.on_key(event);
2921
2922 self.flash = None;
2924 self.cancel_drag();
2926
2927 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
2928 if ctrl && event.code == KeyCode::Char('q') {
2931 return Some(AppEvent::Exit);
2932 }
2933 if ctrl && event.code == KeyCode::Char('c') {
2935 return Some(AppEvent::Exit);
2936 }
2937
2938 if !self.menu_showing() {
2942 self.context_menu = None;
2943 }
2944 if let Some(menu) = self.context_menu.as_mut() {
2945 match menu.key(event) {
2946 app::context_menu::MenuKey::Moved => return None,
2947 app::context_menu::MenuKey::Close => {
2948 self.context_menu = None;
2949 return None;
2950 }
2951 app::context_menu::MenuKey::Run(key) => {
2952 self.context_menu = None;
2953 return Some(AppEvent::Press(key));
2954 }
2955 app::context_menu::MenuKey::Do(action) => {
2956 self.context_menu = None;
2957 self.menu_action(action);
2958 return None;
2959 }
2960 app::context_menu::MenuKey::Other => self.context_menu = None,
2961 }
2962 }
2963
2964 if event.code == KeyCode::Esc && self.view_applying() {
2966 self.cancel_view();
2967 return None;
2968 }
2969 if event.code == KeyCode::Esc && self.finding() {
2971 self.cancel_find();
2972 return None;
2973 }
2974 if event.code == KeyCode::Esc
2976 && self.at_table()
2977 && self.in_normal_table_view()
2978 && self.footers_counted().is_some()
2979 {
2980 self.stop_count();
2981 return None;
2982 }
2983
2984 if event.code == KeyCode::Esc
2985 && self.at_table()
2986 && !self.error_modal.active
2987 && !self.confirmation_modal.active
2988 && self.return_from_quality_evidence(true)
2989 {
2990 return None;
2991 }
2992
2993 if event.code == KeyCode::F(1) {
2995 if self.help.is_open() {
2996 self.help.close();
2997 } else {
2998 self.open_help_overlay();
2999 }
3000 return None;
3001 }
3002
3003 if self.input_mode == InputMode::Home
3006 && !self.confirmation_modal.active
3007 && !self.error_modal.active
3008 && !self.help.is_open()
3009 {
3010 return self.home_key(event);
3011 }
3012
3013 if event.code == KeyCode::Char('o')
3015 && event.modifiers.contains(KeyModifiers::CONTROL)
3016 && (!self.confirmation_modal.active || self.awaiting_open_confirmation())
3017 {
3018 self.help.close();
3019 self.enter_home();
3020 return None;
3021 }
3022
3023 if self.confirmation_modal.active {
3024 match event.code {
3025 KeyCode::Left | KeyCode::Char('h') => {
3026 self.confirmation_modal.focus_yes = true;
3027 }
3028 KeyCode::Right | KeyCode::Char('l') => {
3029 self.confirmation_modal.focus_yes = false;
3030 }
3031 KeyCode::Tab => {
3032 self.confirmation_modal.focus_yes = !self.confirmation_modal.focus_yes;
3033 }
3034 KeyCode::Up | KeyCode::Char('k') => {
3036 self.confirmation_modal.scroll =
3037 self.confirmation_modal.scroll.saturating_sub(1);
3038 }
3039 KeyCode::Down | KeyCode::Char('j') => {
3040 self.confirmation_modal.scroll =
3041 self.confirmation_modal.scroll.saturating_add(1);
3042 }
3043 KeyCode::Enter if self.confirmation_modal.focus_yes => {
3044 return self.confirmed();
3045 }
3046 KeyCode::Enter => {
3047 if matches!(self.confirmation_modal.asking, Some(Confirm::Leave(_))) {
3049 return self.confirmed();
3050 }
3051 return self.declined();
3052 }
3053 KeyCode::Esc => {
3054 if matches!(self.confirmation_modal.asking, Some(Confirm::Leave(_))) {
3056 self.confirmation_modal.hide();
3057 return None;
3058 }
3059 return self.declined();
3060 }
3061 _ => {}
3062 }
3063 return None;
3064 }
3065 if self.error_modal.active {
3066 match event.code {
3067 KeyCode::Up | KeyCode::Char('k') => {
3069 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(1);
3070 return None;
3071 }
3072 KeyCode::Down | KeyCode::Char('j') => {
3073 self.error_modal.scroll = self.error_modal.scroll.saturating_add(1);
3074 return None;
3075 }
3076 KeyCode::PageUp => {
3077 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(8);
3078 return None;
3079 }
3080 KeyCode::PageDown => {
3081 self.error_modal.scroll = self.error_modal.scroll.saturating_add(8);
3082 return None;
3083 }
3084 KeyCode::Esc | KeyCode::Enter => {
3085 self.error_modal.hide();
3086 if self.data_table_state.is_none() {
3089 let reason = self.home_app.last_load_error.take();
3090 self.enter_home();
3091 self.home.status = reason;
3092 }
3093 }
3094 _ => {}
3095 }
3096 return None;
3097 }
3098
3099 let in_main_table = self.at_table() && !self.help.is_open();
3102 if in_main_table && event.is_press() {
3104 self.prompt.column_hints = Self::column_cursor_key(event).is_some()
3105 || (self.prompt.column_hints
3106 && matches!(
3107 event.code,
3108 KeyCode::Char(
3109 '+' | '-' | 'F' | '[' | ']' | 'H' | 'L' | '<' | '>' | '=' | 'w'
3110 )
3111 ));
3112 }
3113 if in_main_table
3114 && let Some(mv) = Self::column_cursor_key(event)
3115 && let Some(state) = self.data_table_state.as_mut()
3116 {
3117 state.move_cursor(mv);
3118 self.debug.action(|| format!("move_cursor({mv:?})"));
3119 return None;
3120 }
3121
3122 self.close_help_left_behind();
3125 if self.help.is_open() {
3126 return match self.help.key(event) {
3127 app::help::HelpKey::Press(key) => Some(AppEvent::Press(key)),
3128 app::help::HelpKey::Stay | app::help::HelpKey::Closed => None,
3129 };
3130 }
3131
3132 if event.code == KeyCode::Char('?') {
3133 let ctrl_help = event.modifiers.contains(KeyModifiers::CONTROL);
3134 let in_text_input = self.text_field_focused() || self.input_mode == InputMode::Home;
3136 if ctrl_help || !in_text_input {
3138 self.open_help_overlay();
3139 return None;
3140 }
3141 }
3142
3143 match self.overlay {
3144 Overlay::None => {}
3145 Overlay::SortFilter => return self.sort_filter_key(event),
3146 Overlay::Export { .. } => return self.export_key(event),
3147 Overlay::Sample => return self.table_sample_form_key(event),
3148 Overlay::Inspect => return self.inspector_key(event),
3149 Overlay::ValueCounts => return self.value_counts_key(event),
3150 Overlay::Hex => return self.hex_key(event),
3151 Overlay::GoToColumn => {
3152 self.go_to_column_key(event);
3153 return None;
3154 }
3155 Overlay::PickFormat => return self.format_picker_key(event),
3156 Overlay::Retype { .. } => return self.retype_key(event),
3157 Overlay::Combine { .. } => return self.combine_key(event),
3158 Overlay::PickTable => return self.table_picker_key(event),
3159 Overlay::Copy => return self.copy_key(event),
3160 Overlay::PivotMelt => return self.pivot_melt_key(event),
3161 Overlay::Info => return self.info_key(event),
3162 Overlay::Chart | Overlay::ChartExport => return self.chart_key(event),
3163 Overlay::Analysis => return self.analysis_key(event),
3164 Overlay::View => return self.view_key(event),
3165 }
3166
3167 if self.input_mode == InputMode::Editing {
3168 return self.editing_key(event);
3169 }
3170
3171 const RIGHT_KEYS: [KeyCode; 2] = [KeyCode::Right, KeyCode::Char('l')];
3172
3173 const LEFT_KEYS: [KeyCode; 2] = [KeyCode::Left, KeyCode::Char('h')];
3174
3175 const DOWN_KEYS: [KeyCode; 2] = [KeyCode::Down, KeyCode::Char('j')];
3176
3177 const UP_KEYS: [KeyCode; 2] = [KeyCode::Up, KeyCode::Char('k')];
3178
3179 if event
3182 .modifiers
3183 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
3184 && !matches!(event.code, KeyCode::Char('f' | 'b' | 'd' | 'u'))
3185 {
3186 return None;
3187 }
3188
3189 match event.code {
3190 KeyCode::Char('q') => {
3193 if self.source.opened_from_home {
3194 self.enter_home();
3195 None
3196 } else {
3197 Some(AppEvent::Exit)
3198 }
3199 }
3200 KeyCode::Char('Q') => Some(AppEvent::Exit),
3201 KeyCode::Char('R') => Some(AppEvent::Reset),
3202 KeyCode::Char('H' | 'L') if event.is_press() => {
3203 self.move_cursor_column(event.code == KeyCode::Char('L'))
3204 }
3205 KeyCode::Char('+' | '-') if event.is_press() => {
3206 self.quick_filter(event.code == KeyCode::Char('+'))
3207 }
3208 KeyCode::Char('#') => {
3209 let renumbered = self
3210 .data_table_state
3211 .as_mut()
3212 .is_some_and(|state| state.deferred(|s| s.toggle_row_numbers()));
3213 if renumbered {
3214 self.spawn_async_collect(Self::LOADING_BUFFER);
3215 }
3216 None
3217 }
3218 KeyCode::Char('<' | '>' | '=' | 'w')
3220 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
3221 {
3222 if let Some(state) = self.data_table_state.as_mut()
3223 && let Some(name) = state.current_column().map(str::to_string)
3224 {
3225 let (choice, shown) = (state.width_choice(&name), state.on_screen_width(&name));
3228 let width = match event.code {
3229 KeyCode::Char('<') => choice.narrower(shown),
3230 KeyCode::Char('>') => choice.wider(shown),
3231 KeyCode::Char('=') => WidthChoice::Fit,
3232 _ => WidthChoice::Auto,
3233 };
3234 state.set_width_choices([(name, width)]);
3235 }
3236 None
3237 }
3238 KeyCode::Char('/' | 'f')
3240 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
3241 {
3242 self.open_find();
3243 None
3244 }
3245 KeyCode::Char('[' | ']') if event.is_press() => {
3246 self.sort_by_cursor_column(event.code == KeyCode::Char(']'))
3247 }
3248 KeyCode::Char('n') if event.is_press() => {
3249 self.find_again(find::Direction::Next);
3250 None
3251 }
3252 KeyCode::Char('N') if event.is_press() => {
3253 self.find_again(find::Direction::Previous);
3254 None
3255 }
3256 KeyCode::Char('D') => {
3257 self.display.dtype_row = !self.display.dtype_row;
3259 let on = if self.display.dtype_row { "on" } else { "off" };
3260 self.debug.action(|| format!("toggle_dtype_row({on})"));
3261 None
3262 }
3263 KeyCode::Char('F') => {
3264 self.open_value_counts();
3265 None
3266 }
3267 KeyCode::Char(',') => {
3268 self.display.number_format.enabled = !self.display.number_format.enabled;
3270 let on = if self.display.number_format.enabled {
3271 "on"
3272 } else {
3273 "off"
3274 };
3275 self.debug.action(|| format!("toggle_number_format({on})"));
3276 None
3277 }
3278 KeyCode::Esc => {
3279 if self.find_shown() {
3281 self.prompt.find.active = None;
3282 return None;
3283 }
3284 if self.sample_drawing() {
3286 self.stop_sample_draw();
3287 return None;
3288 }
3289 let mut from_counts = false;
3290 let drilled_up = if let Some(ref mut state) = self.data_table_state {
3291 if state.is_drilled_down() {
3292 from_counts = state.drilled_into_value();
3293 let _ = state.deferred(|s| s.drill_up());
3294 true
3295 } else {
3296 false
3297 }
3298 } else {
3299 false
3300 };
3301 if drilled_up {
3302 self.sync_sort_filter_modal();
3303 }
3304 if from_counts
3306 && std::mem::take(&mut self.value_counts.drill_return)
3307 && let Some(state) = self.data_table_state.as_ref()
3308 {
3309 self.value_counts.rebase(state.len_generation());
3310 self.open_overlay(Overlay::ValueCounts);
3311 }
3312 if drilled_up {
3313 self.spawn_async_collect(Self::LOADING_BUFFER);
3314 return None;
3315 }
3316 if let Some(state) = self.data_table_state.as_mut()
3318 && state.follow().is_some()
3319 {
3320 state.stop_following();
3321 self.flash_note("Stopped following".to_string());
3322 }
3323 None
3325 }
3326 KeyCode::Char('t') if event.is_press() => self.toggle_follow(),
3327 code if RIGHT_KEYS.contains(&code) || LEFT_KEYS.contains(&code) => {
3328 if let Some(ref mut state) = self.data_table_state {
3329 state.move_cursor(if RIGHT_KEYS.contains(&code) {
3330 crate::widgets::column_paging::CursorMove::Right
3331 } else {
3332 crate::widgets::column_paging::CursorMove::Left
3333 });
3334 }
3335 None
3336 }
3337 code if event.is_press() && DOWN_KEYS.contains(&code) => self.scroll_key(Scroll::Next),
3338 code if event.is_press() && UP_KEYS.contains(&code) => self.scroll_key(Scroll::Prev),
3339 KeyCode::PageDown if event.is_press() => self.scroll_key(Scroll::PageDown),
3340 KeyCode::Home if event.is_press() => self.jump_key(Scroll::Start),
3341 KeyCode::End | KeyCode::Char('G') if event.is_press() => self.jump_key(Scroll::End),
3342 KeyCode::Char('f')
3343 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3344 {
3345 self.scroll_key(Scroll::PageDown)
3346 }
3347 KeyCode::Char('b')
3348 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3349 {
3350 self.scroll_key(Scroll::PageUp)
3351 }
3352 KeyCode::Char('d')
3353 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3354 {
3355 self.scroll_key(Scroll::HalfDown)
3356 }
3357 KeyCode::Char('u')
3358 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
3359 {
3360 self.scroll_key(Scroll::HalfUp)
3361 }
3362 KeyCode::PageUp if event.is_press() => self.scroll_key(Scroll::PageUp),
3363 KeyCode::Enter if event.is_press() => {
3364 if !self.at_table() {
3365 return None;
3366 }
3367 if self.enter_inspects() {
3369 self.open_inspector();
3370 return None;
3371 }
3372 self.drill_selected_row();
3373 None
3374 }
3375 KeyCode::Char('i') if event.is_press() => {
3376 if let Some(state) = self.data_table_state.as_mut() {
3377 let unseen = state.notes_unseen();
3380 state.mark_notes_seen();
3381 if unseen {
3382 self.info_modal
3383 .open_on(crate::widgets::info::InfoTab::Notes);
3384 } else if state.format_detail().is_some_and(|d| d.first) {
3385 self.info_modal
3388 .open_on(crate::widgets::info::InfoTab::Format);
3389 } else {
3390 self.info_modal.open();
3391 }
3392 if let Some(detail) = state.format_detail()
3394 && let Some(at) = detail.list.iter().position(|(key, _)| {
3395 detail.table.as_ref() == Some(key) && detail.tables.contains(key)
3396 })
3397 {
3398 self.info_modal.detail_selected = at;
3399 }
3400 self.open_overlay(Overlay::Info);
3401 self.read_file_facts();
3402 self.count_unfit();
3403 }
3404 None
3405 }
3406 KeyCode::Char(':') if event.is_press() => {
3407 self.open_command_line();
3408 None
3409 }
3410 KeyCode::Char('V') => {
3411 if let Some(ref state) = self.data_table_state
3414 && let Some(dataset) = self.view_dataset()
3415 {
3416 match self
3417 .views
3418 .manager
3419 .get_most_relevant(dataset, state.source_schema())
3420 {
3421 Some((view, why)) => {
3422 if let Err(e) = self.apply_matched_view(&view, why) {
3423 self.error_modal.show(format!("Error applying view: {}", e));
3424 }
3425 }
3426 None => self.open_view_list(),
3427 }
3428 }
3429 None
3430 }
3431 KeyCode::Char('v') => {
3432 self.open_view_list();
3433 None
3434 }
3435 KeyCode::Char('S') => {
3436 if self.at_table() {
3437 self.open_table_sample_form();
3438 }
3439 None
3440 }
3441 KeyCode::Char('s') => {
3442 if self.data_table_state.is_some() {
3443 self.sync_sort_filter_modal();
3446 let current = self
3447 .data_table_state
3448 .as_ref()
3449 .and_then(|state| state.current_column())
3450 .map(str::to_string);
3451 self.sort_filter_modal.open(
3452 self.display.history_limit,
3453 &self.theme,
3454 current.as_deref(),
3455 );
3456 self.open_overlay(Overlay::SortFilter);
3457 }
3458 None
3459 }
3460 KeyCode::Char('r') => {
3461 if let Some(state) = &mut self.data_table_state {
3462 state.deferred(DataTableState::reverse);
3463 self.spawn_async_collect("Sorting...");
3464 }
3465 None
3466 }
3467 KeyCode::Char('a') => {
3468 if self.data_table_state.is_some()
3470 && self.at_table()
3471 && self.quality.evidence_return.is_none()
3472 {
3473 let view = self.data_table_state.as_ref().map(|s| s.len_generation());
3475 self.analysis_modal.open(view);
3476 self.open_overlay(Overlay::Analysis);
3477 if self.analysis_modal.sample_dataset != Some(self.dataset_generation) {
3480 self.analysis_modal.sample.scope =
3481 analysis::data_quality::QualityScope::CurrentView;
3482 self.analysis_modal.sample_dataset = Some(self.dataset_generation);
3483 }
3484 let sampled = self
3486 .data_table_state
3487 .as_ref()
3488 .is_some_and(|state| state.sampled().is_some());
3489 self.analysis_modal.follow_view_sample(sampled);
3490 self.sync_quality_plan();
3491 }
3492 None
3493 }
3494 KeyCode::Char('c') => {
3495 if let Some(state) = &self.data_table_state
3496 && self.at_table()
3497 {
3498 let numeric_columns: Vec<String> = state
3499 .schema()
3500 .iter()
3501 .filter(|(_, dtype)| dtype.is_numeric())
3502 .map(|(name, _)| name.to_string())
3503 .collect();
3504 let datetime_columns: Vec<String> = state
3505 .schema()
3506 .iter()
3507 .filter(|(_, dtype)| {
3508 matches!(
3509 dtype,
3510 DataType::Datetime(_, _) | DataType::Date | DataType::Time
3511 )
3512 })
3513 .map(|(name, _)| name.to_string())
3514 .collect();
3515 let category_columns: Vec<String> = state
3516 .schema()
3517 .iter()
3518 .filter(|(_, dtype)| chart::chart_data::is_category_dtype(dtype))
3519 .map(|(name, _)| name.to_string())
3520 .collect();
3521 let cursor = state.current_column().and_then(|name| {
3523 let dtype = state.schema().get(name)?.clone();
3524 Some((name.to_string(), dtype))
3525 });
3526 let bucketable_columns: Vec<String> = state
3528 .schema()
3529 .iter()
3530 .filter(|(_, dtype)| {
3531 matches!(dtype, DataType::Datetime(_, _) | DataType::Date)
3532 })
3533 .map(|(name, _)| name.to_string())
3534 .collect();
3535 self.chart.modal.series_cap = Some(self.theme.series_colors().len());
3536 self.chart.modal.row_order = self.view_state().sort;
3537 let sampled = state.sampled().is_some();
3538 self.chart.modal.open(
3539 ChartColumns {
3540 numeric: &numeric_columns,
3541 datetime: &datetime_columns,
3542 bucketable: &bucketable_columns,
3543 category: &category_columns,
3544 },
3545 cursor.as_ref().map(|(name, dtype)| (name.as_str(), dtype)),
3546 Some(self.app_config.analysis.chart_rows),
3547 self.app_config.analysis.chart_grid,
3548 self.dataset_generation,
3549 );
3550 if sampled {
3552 self.chart.modal.row_limit = None;
3553 } else if self.chart.modal.view_sampled {
3554 self.chart.modal.row_limit = Some(self.chart.modal.sample_rows);
3555 }
3556 self.chart.modal.view_sampled = sampled;
3557 self.chart.cache.clear();
3558 self.open_overlay(Overlay::Chart);
3559 }
3560 None
3561 }
3562 KeyCode::Char('p') => {
3563 if self.data_table_state.is_some() && self.at_table() {
3564 self.open_pivot_builder();
3565 }
3566 None
3567 }
3568 KeyCode::Char('e') => {
3569 if self.data_table_state.is_some() && self.at_table() {
3570 self.export_modal.open(
3571 self.source.original_file_format,
3572 self.display.history_limit,
3573 &self.theme,
3574 self.source.original_file_delimiter,
3575 );
3576 let stem = self.dataset_stem();
3578 self.export_modal.suggest_path(&format!("{stem}-export"));
3579 if let Some(state) = self.data_table_state.as_ref() {
3580 self.export_modal.offer_source_file = state.can_name_source_files();
3581 self.export_modal.nested_columns = state
3582 .get_column_order()
3583 .iter()
3584 .filter_map(|name| state.schema().get(name))
3585 .any(crate::export::nested_json::is_nested);
3586 self.export_modal.avro_renames =
3587 state.get_column_order().iter().any(|name| {
3588 state.schema().get(name).is_some_and(|dtype| {
3589 crate::export::avro_types::renames(name, dtype)
3590 })
3591 });
3592 }
3593 self.open_over(|returns_to| Overlay::Export { returns_to });
3594 }
3595 None
3596 }
3597 KeyCode::Char(' ') if event.is_press() => {
3598 if self.at_table() {
3599 self.open_inspector();
3600 }
3601 None
3602 }
3603 KeyCode::Char('g') if event.is_press() => {
3604 if self.at_table() {
3605 self.open_go_to_column();
3606 }
3607 None
3608 }
3609 KeyCode::Char('b') if event.is_press() => {
3610 if self.at_table() {
3611 self.open_format_picker();
3612 }
3613 None
3614 }
3615 KeyCode::Char('T') if event.is_press() => {
3616 if self.at_table() {
3617 self.open_table_picker();
3618 }
3619 None
3620 }
3621 KeyCode::Char('y') => {
3622 if self.at_table()
3623 && let Some(state) = self.data_table_state.as_ref()
3624 {
3625 let columns = state.get_column_order().to_vec();
3626 let context = app::modals::copy_modal::CopyContext {
3627 row_number: state.selected_display_row().unwrap_or(0),
3628 view_rows: state.copy_view_df().map(|d| d.height()).unwrap_or(0),
3629 view_cols: columns.len(),
3630 total_rows: state.num_rows_if_valid(),
3631 };
3632 let current = state.current_column().map(str::to_string);
3633 self.copy_modal.open(columns, current.as_deref(), context);
3634 self.open_overlay(Overlay::Copy);
3635 }
3636 None
3637 }
3638 _ => None,
3639 }
3640 }
3641
3642 pub fn handle(&mut self, event: AppEvent) -> EventOutcome {
3647 let started = std::time::Instant::now();
3648 let outcome = self.handle_event(event);
3649 self.debug.times.handler(started.elapsed());
3650 outcome
3651 }
3652
3653 fn handle_event(&mut self, event: AppEvent) -> EventOutcome {
3654 if let AppEvent::Press(key) = event {
3656 return self.handle_event(AppEvent::Key(key));
3657 }
3658 if let AppEvent::Key(key) = event
3659 && self.is_busy()
3660 && !self.key_acts_while_busy(&key)
3661 {
3662 return Err(key);
3663 }
3664 let out = self.dispatch_event(event);
3665 if out.is_none() {
3669 self.let_waiting_errands_in();
3670 }
3671 self.ensure_chart_data();
3672 self.home_score_search();
3673 self.refresh_stale_live_matches();
3675 Ok(out)
3676 }
3677
3678 pub(crate) fn let_waiting_errands_in(&mut self) {
3681 if self.join_held_footers() {
3684 self.reread_after_the_footers_joined();
3685 }
3686 self.join_followed_fields();
3687 self.describe_ended_journal();
3688 self.reread_when_the_work_allows();
3690 self.collect_when_the_work_allows();
3691 }
3692
3693 pub fn event(&mut self, event: AppEvent) -> Option<AppEvent> {
3694 self.handle(event).unwrap_or(None)
3695 }
3696
3697 fn dispatch_event(&mut self, event: AppEvent) -> Option<AppEvent> {
3698 self.debug.num_events += 1;
3699
3700 match event {
3701 AppEvent::Key(key) => {
3702 if let Some(leaving) = self.leaving_by(&key)
3704 && !self.confirmation_modal.active
3705 && self.recording().is_some_and(|spool| spool.live())
3706 {
3707 self.ask_about_recording(leaving);
3708 return None;
3709 }
3710 self.key(&key)
3711 }
3712 AppEvent::Open(paths, options) => {
3713 if paths.is_empty() {
3714 return Some(AppEvent::Crash("No paths provided".to_string()));
3715 }
3716 if self.input_mode == InputMode::Home {
3719 self.source.opened_from_home = true;
3720 }
3721 #[cfg(feature = "cloud")]
3723 let expanded = match paths
3724 .iter()
3725 .map(|p| {
3726 crate::cloud::cloud_sources::expand_azure_short_url(
3727 p,
3728 &self.app_config.cloud,
3729 self.home.browsing.as_deref(),
3730 )
3731 })
3732 .collect::<std::result::Result<Vec<_>, _>>()
3733 {
3734 Ok(expanded) => expanded,
3735 Err(message) => return Some(AppEvent::Crash(message)),
3736 };
3737 #[cfg(feature = "cloud")]
3738 if expanded != paths {
3739 return Some(AppEvent::Open(expanded, options));
3740 }
3741 let mut request = loading::OpenRequest::named(paths, options, &self.formats);
3744 request.warn_in_memory_above = self.app_config.read.memory_warning();
3745 self.begin_new_dataset();
3746 let step = self.loading.open(request);
3747 self.run_load_step(step)
3748 }
3749 AppEvent::OpenLazyFrame(lf, options) => {
3750 self.begin_new_dataset();
3751 let step = self.loading.open_frame(*lf, options);
3752 self.run_load_step(step)
3753 }
3754 AppEvent::HomeListingReady { .. }
3755 | AppEvent::HomeListingFailed
3756 | AppEvent::HomeMeasured { .. }
3757 | AppEvent::HomeSized { .. }
3758 | AppEvent::HomeWebGone { .. }
3759 | AppEvent::HomeClassified { .. }
3760 | AppEvent::HomePathListed { .. }
3761 | AppEvent::HomePathCompleted { .. }
3762 | AppEvent::HomePreviewReady { .. }
3763 | AppEvent::HomeSchemaReady { .. }
3764 | AppEvent::HomeSearchBatch { .. }
3765 | AppEvent::HomeSearchScored { .. }
3766 | AppEvent::HomeSearchDone { .. }
3767 | AppEvent::HomeNarrowed { .. }
3768 | AppEvent::HomeProbeCancelled { .. }
3769 | AppEvent::HomeProbeFailed { .. }
3770 | AppEvent::HomeProbeProgress { .. }
3771 | AppEvent::HomeProbeReady { .. }
3772 | AppEvent::HomeCloudKinds { .. } => self.home_event(event),
3773 #[cfg(feature = "cloud")]
3774 AppEvent::HomeCloudSources { .. } | AppEvent::HomeCloudListed { .. } => {
3775 self.home_event(event)
3776 }
3777 AppEvent::Resize(_cols, _rows) => {
3778 None
3780 }
3781 AppEvent::Collect => {
3782 self.spawn_async_collect(Self::LOADING_BUFFER);
3783 None
3784 }
3785 AppEvent::Scroll(scroll) => self.handle_scroll(|s| scroll.run(s)),
3786 AppEvent::AnalysisCompute(tool) => self.spawn_analysis(tool),
3787 AppEvent::BackgroundLenReady { .. }
3788 | AppEvent::FramePainted
3789 | AppEvent::BackgroundLenFailed { .. } => self.counting_event(event),
3790 AppEvent::BackgroundQualitySampleKept { kept } => {
3791 self.retain_quality_sample(&kept);
3792 None
3793 }
3794 AppEvent::BackgroundQualityCopyKept {
3795 dataset_generation,
3796 copy,
3797 } => {
3798 self.retain_quality_copy(dataset_generation, copy);
3799 None
3800 }
3801 AppEvent::OpenNamed(paths, options) => {
3802 if let Some(event) = Self::route_named_without_looking(&paths, &options) {
3803 return Some(event);
3804 }
3805 let formats = self.formats.clone();
3806 self.put_down_load_in_flight();
3809 let load = self.loading.look_at_paths();
3810 self.spawn_job(Job::OpenNamed(load), Some("Scanning input..."), move |_| {
3811 if let Some(missing) = Self::missing_named_path(&paths, &formats) {
3812 return Ok(Answer::NamedPathMissing(missing));
3813 }
3814 let (paths, options, directory) =
3815 match Self::route_named_paths_with(paths, options, &formats) {
3816 AppEvent::LookThenOpenDirectory(dir, options) => {
3817 (Vec::new(), options, Some(dir))
3818 }
3819 AppEvent::Open(paths, options) => (paths, options, None),
3820 _ => unreachable!("a named path is opened or looked at"),
3821 };
3822 Ok(Answer::NamedPaths {
3823 paths,
3824 options: Box::new(options),
3825 directory,
3826 })
3827 });
3828 None
3829 }
3830 AppEvent::LookThenOpenDirectory(looking, options) => {
3831 self.put_down_load_in_flight();
3834 let load = self.loading.look_at_directory(looking.clone());
3835 self.jobs
3837 .supersede(|job| matches!(job, Job::LookAtDirectory { .. }));
3838 #[cfg(feature = "cloud")]
3842 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
3843 let job = Job::LookAtDirectory {
3844 load,
3845 path: looking.clone(),
3846 };
3847 self.spawn_job(job, Some(Self::LOOKING_AT_A_DIRECTORY), move |_| {
3848 #[cfg(feature = "cloud")]
3849 if home::is_object_store_url(&looking) {
3850 let url = looking.to_string_lossy().into_owned();
3851 let peeked = wait_on_runtime(&runtime, async move {
3852 crate::cloud::cloud_browse::peek_kind(&url, &cloud).await
3853 })
3854 .and_then(Result::ok);
3855 let (kind, holds) = match peeked {
3856 Some((kind, holds)) => (kind, Some(Box::new(holds))),
3857 None => (home::discover::EntryKind::Unknown, None),
3858 };
3859 return Ok(Answer::LookedAt {
3860 kind,
3861 holds,
3862 options: Box::new(options),
3863 });
3864 }
3865 let as_read = Self::read_as(&options);
3869 let looked = logging::catch_panic(|| {
3870 let mut entry = home::discover::Entry::directory(&looking);
3871 entry.kind = home::discover::EntryKind::Unknown;
3872 home::look_into_as(&entry, &as_read)
3873 });
3874 let kind = match looked {
3875 Ok(entry) => entry.kind,
3876 Err(_) => home::discover::EntryKind::Directory,
3877 };
3878 Ok(Answer::LookedAt {
3879 kind,
3880 holds: None,
3881 options: Box::new(options),
3882 })
3883 });
3884 None
3885 }
3886 AppEvent::ClassifyThenOpen {
3887 path: looking,
3888 jump,
3889 } => {
3890 self.jobs.supersede(|job| matches!(job, Job::Classify(_)));
3893 let look = Job::Classify(app::jobs::Classify {
3894 path: looking.clone(),
3895 browsing: self.home.browsing.clone(),
3896 jump,
3897 });
3898 let name = looking
3899 .file_name()
3900 .map(|n| n.to_string_lossy().into_owned())
3901 .unwrap_or_else(|| looking.display().to_string());
3902 self.home.status = Some(format!("Looking at {name}..."));
3904 self.spawn_job(look, Some(Self::LOOKING), move |_| {
3905 let found = if !looking.exists() {
3907 None
3908 } else if looking.is_dir() {
3909 Some(crate::home::discover::classify_directory(&looking))
3910 } else {
3911 Some(crate::home::discover::EntryKind::File)
3912 };
3913 Ok(Answer::Kind(found))
3914 });
3915 None
3916 }
3917 AppEvent::JobEnded(ticket) => self.job_ended(ticket),
3918 AppEvent::Applied(applied) => self.apply(applied),
3919 AppEvent::JobProgress { ticket, progress } => {
3920 self.job_progress(ticket, &progress);
3921 None
3922 }
3923 AppEvent::Reset => {
3924 if self
3926 .data_table_state
3927 .as_ref()
3928 .is_some_and(|state| state.sampled().is_some())
3929 {
3930 self.put_down_sample_draw();
3931 if let Some(state) = self.data_table_state.take() {
3932 self.data_table_state = Some(state.into_unsampled());
3933 }
3934 self.sample_changed();
3935 }
3936 if let Some(state) = &mut self.data_table_state {
3937 state.deferred(|s| s.reset());
3938 }
3939 self.spawn_async_collect(Self::LOADING_BUFFER);
3940 self.views.active_id = None;
3941 None
3942 }
3943 AppEvent::Followed(news) => {
3944 self.followed(&news);
3945 None
3946 }
3947 AppEvent::TerminalBackground(mode) => {
3948 self.terminal_answered(mode);
3949 None
3950 }
3951 AppEvent::TerminalFocused => {
3952 self.display.background_query |= self.app_config.theme.follow;
3953 None
3954 }
3955 AppEvent::Press(_)
3958 | AppEvent::Terminal(_)
3959 | AppEvent::Wake
3960 | AppEvent::SettingsRead(_)
3961 | AppEvent::NamedPathMissing(_)
3962 | AppEvent::Exit
3963 | AppEvent::Crash(_)
3964 | AppEvent::Update => None,
3965 }
3966 }
3967
3968 fn filter_problem(&self) -> Option<String> {
3971 let schema = self.data_table_state.as_ref()?.schema();
3972 self.sort_filter_modal
3973 .filter
3974 .statements
3975 .iter()
3976 .find_map(|f| {
3977 crate::export::python_script::SidebarFilter::problem(f, schema.get(&f.column))
3978 })
3979 }
3980
3981 pub fn header_toggle_offered(&self) -> bool {
3984 self.source
3985 .opened
3986 .as_ref()
3987 .and_then(|(_, options)| options.format)
3988 .and_then(FileFormat::separator)
3989 .is_some()
3990 }
3991
3992 pub(crate) fn toggle_header(&mut self) -> Option<AppEvent> {
3995 if !self.header_toggle_offered() {
3996 return None;
3997 }
3998 let (paths, options) = self.source.opened.clone()?;
3999 let options = OpenOptions {
4000 has_header: Some(!options.has_header.unwrap_or(true)),
4001 ..options
4002 };
4003 self.set_loading_phase("Scanning input", 10);
4004 self.name_what_is_loading(paths[0].clone());
4005 Some(AppEvent::Open(paths, options))
4006 }
4007
4008 fn move_cursor_column(&mut self, right: bool) -> Option<AppEvent> {
4012 let state = self.data_table_state.as_ref()?;
4013 let at = state.current_column_index()?;
4014 let mut order = state.headers();
4015 let locked = state.locked_columns_count().min(order.len());
4016 let to = if right { at + 1 } else { at.checked_sub(1)? };
4017 if to >= order.len() || (at < locked) != (to < locked) {
4018 return None;
4019 }
4020 let moving = order[at].clone();
4022 self.data_table_state.as_mut()?.set_current_column(&moving);
4023 order.swap(at, to);
4024 let applied = &mut self.sort_filter_modal.sort.applied_order;
4027 if let (Some(i), Some(j)) = (
4028 applied.iter().position(|c| *c == order[at]),
4029 applied.iter().position(|c| *c == order[to]),
4030 ) {
4031 applied.swap(i, j);
4032 }
4033 Some(AppEvent::Applied(Applied::ColumnOrder(order, locked)))
4034 }
4035
4036 fn sort_by_cursor_column(&mut self, descending: bool) -> Option<AppEvent> {
4039 let state = self.data_table_state.as_ref()?;
4040 let column = state.current_column()?.to_string();
4041 let already = state.view_sort_columns() == std::slice::from_ref(&column)
4042 && state.view_sort_descending() == [descending];
4043 if already {
4044 if let Some(state) = self.data_table_state.as_mut() {
4047 state.deferred(|s| s.sort(Vec::new(), true));
4048 }
4049 self.spawn_async_collect("Sorting...");
4050 return None;
4051 }
4052 Some(AppEvent::Applied(Applied::Sort(
4053 vec![column],
4054 vec![descending],
4055 )))
4056 }
4057}
4058
4059impl App {
4060 fn quick_filter(&mut self, keep: bool) -> Option<AppEvent> {
4064 let state = self.data_table_state.as_ref()?;
4065 let column = state.current_column()?.to_string();
4066 let row = state.copy_row_df()?;
4067 let series = row.column(&column).ok()?.as_materialized_series().clone();
4068 let value = series.get(0).ok()?;
4069 let dtype = state.schema().get(&column)?.clone();
4071 let (operator, text) = if value.is_null() {
4072 let operator = if keep {
4073 FilterOperator::IsNull
4074 } else {
4075 FilterOperator::IsNotNull
4076 };
4077 (operator, String::new())
4078 } else {
4079 let operator = if keep {
4080 FilterOperator::Eq
4081 } else {
4082 FilterOperator::NotEq
4083 };
4084 let text = crate::typed_value::text_of(&value, &dtype);
4087 let Some(text) = text else {
4088 let kind = match dtype {
4089 DataType::List(_) => "lists",
4090 DataType::Array(..) => "arrays",
4091 DataType::Struct(_) => "structs",
4092 DataType::Binary | DataType::BinaryOffset => "binary",
4093 _ => "this type",
4094 };
4095 self.flash_note(format!("+ and - filter on plain values, not {kind}"));
4096 return None;
4097 };
4098 (operator, text)
4099 };
4100 let statement = FilterStatement {
4101 columns: Vec::new(),
4102 column,
4103 operator,
4104 value: text,
4105 logical_op: LogicalOperator::And,
4106 };
4107 let mut statements = state.view_filters().to_vec();
4108 if statements.contains(&statement) {
4109 return None;
4110 }
4111 statements.push(statement);
4112 Some(AppEvent::Applied(Applied::Filter(statements)))
4113 }
4114
4115 fn sync_sort_filter_modal(&mut self) {
4120 let Some(state) = self.data_table_state.as_ref() else {
4121 return;
4122 };
4123 let filters = state.view_filters().to_vec();
4124 let sort_columns = state.view_sort_columns().to_vec();
4125 let sort_descending = state.view_sort_descending().to_vec();
4126 let headers: Vec<String> = state.schema().iter_names().map(|s| s.to_string()).collect();
4127 let schema = state.schema().clone();
4128 let order = state.headers();
4129 let locked = state.locked_columns_count();
4130
4131 let modal = &mut self.sort_filter_modal;
4132 modal.filter.applied = filters.clone();
4133 modal.filter.statements = filters;
4134 modal.filter.operands = order
4135 .iter()
4136 .map(|name| {
4137 schema
4138 .get(name)
4139 .map(crate::app::modals::filter_modal::Operand::of)
4140 .unwrap_or_default()
4141 })
4142 .collect();
4143 modal.filter.available_columns = order.clone();
4144 modal.filter.cursor = modal.filter.statements.len();
4146 modal.filter.editor = None;
4147 let shown: std::collections::HashSet<&str> = order.iter().map(String::as_str).collect();
4152 let applied = &modal.sort.applied_order;
4153 let current = applied
4154 .iter()
4155 .filter(|name| shown.contains(name.as_str()))
4156 .eq(order.iter());
4157 let reference: &[String] = if current { applied } else { &[] };
4158 let full = order_with_hidden(&order, &headers, reference);
4159 let places: HashMap<&str, usize> = full
4160 .iter()
4161 .enumerate()
4162 .map(|(i, name)| (name.as_str(), i))
4163 .collect();
4164 let place = |name: &String| places.get(name.as_str()).copied();
4165 let last_locked = applied
4168 .get(..modal.sort.applied_locked)
4169 .filter(|span| {
4170 current && span.iter().filter(|n| shown.contains(n.as_str())).count() == locked
4171 })
4172 .and_then(|span| span.iter().rev().find_map(place))
4173 .or_else(|| {
4174 locked
4175 .checked_sub(1)
4176 .and_then(|i| order.get(i))
4177 .and_then(place)
4178 });
4179 modal.sort.columns = headers
4180 .iter()
4181 .map(|name| {
4182 let display_order = places[name.as_str()];
4183 SortColumn {
4184 name: name.clone(),
4185 sort_order: sort_columns.iter().position(|c| c == name).map(|o| o + 1),
4187 sort_descending: sort_columns
4188 .iter()
4189 .position(|c| c == name)
4190 .and_then(|i| sort_descending.get(i).copied())
4191 .unwrap_or(false),
4192 display_order,
4193 is_locked: last_locked.is_some_and(|l| display_order <= l),
4194 is_to_be_locked: false,
4195 is_visible: shown.contains(name.as_str()),
4196 width: state.width_choice(name),
4197 shown_width: state.on_screen_width(name),
4198 }
4199 })
4200 .collect();
4201 modal.sort.has_unapplied_changes = false;
4202 }
4203
4204 fn apply_sort_filter(&mut self) -> Option<AppEvent> {
4206 if self.sort_filter_modal.filter.editor.is_some() {
4208 self.sort_filter_modal.filter.commit_editor();
4209 }
4210 if let Some(why) = self.filter_problem() {
4212 self.sort_filter_modal.sort.status = Some(why);
4213 return None;
4214 }
4215 let (columns, descending) = self.sort_filter_modal.sort.sorted_columns_and_directions();
4216 let column_order = self.sort_filter_modal.sort.get_column_order();
4217 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
4218 self.sort_filter_modal.sort.applied_order =
4219 self.sort_filter_modal.sort.get_full_column_order();
4220 self.sort_filter_modal.sort.applied_locked = self.sort_filter_modal.sort.get_locked_span();
4221 let statements = self.sort_filter_modal.filter.statements.clone();
4222 let view_unchanged = self.data_table_state.as_mut().is_some_and(|state| {
4225 state.set_width_choices(self.sort_filter_modal.sort.width_choices());
4226 state.headers() == column_order
4227 && state.locked_columns_count() == locked_count
4228 && state.view_filters() == statements.as_slice()
4229 && state.view_sort_columns() == columns.as_slice()
4230 && state.view_sort_descending() == descending.as_slice()
4231 });
4232 for col in &mut self.sort_filter_modal.sort.columns {
4233 col.is_to_be_locked = false;
4234 }
4235 self.sort_filter_modal.sort.has_unapplied_changes = false;
4236 self.close_overlay();
4237 if view_unchanged {
4238 return None;
4239 }
4240 Some(AppEvent::Applied(Applied::ApplyView(
4241 column_order,
4242 locked_count,
4243 statements,
4244 columns,
4245 descending,
4246 )))
4247 }
4248
4249 fn facts_of_open(&self) -> Option<(FileFormat, crate::formats::readers::Facts)> {
4252 let hive = self
4253 .source
4254 .opened
4255 .as_ref()
4256 .is_some_and(|(_, options)| options.hive);
4257 let format = self.opened_format()?;
4258 let facts = crate::formats::readers::of(format).facts?;
4259 let state = self.data_table_state.as_ref()?;
4260 let plain = state
4261 .read_mode()
4262 .is_none_or(|mode| Some(mode) == format.read_mode(crate::Stored::Plain));
4263 let one_file = state.dataset_schema().is_none();
4265 (!hive && plain && one_file).then_some((format, facts))
4266 }
4267
4268 pub(crate) fn query_reading(&self) -> Option<&str> {
4271 let run = self.prompt.query_running.as_ref()?;
4272 let frame = self.data_table_state.as_ref()?.len_generation();
4273 if !matches!(run.origin, RunOrigin::Query(_)) || run.frame != frame {
4274 return None;
4275 }
4276 self.jobs
4277 .waiting_status(|job| Self::reading_rows(job) || Self::owed_rows(job))
4278 }
4279
4280 fn job_ended(&mut self, ticket: Ticket) -> Option<AppEvent> {
4285 let app::jobs::Ended {
4286 job,
4287 current,
4288 keys,
4289 outcome,
4290 ..
4291 } = self.jobs.end(ticket)?;
4292 let cancelled_analysis = !current && Self::is_analysis_read(&job);
4293 let waited = keys.is_some();
4294 let out = match outcome {
4295 Outcome::Answered(answer) => self.answered(job, current, waited, *answer),
4296 Outcome::Failed { message, panicked } => {
4297 self.background_failed(&job, current, waited, &message, panicked);
4298 None
4299 }
4300 };
4301 if let Some(status) = keys {
4302 if out.is_some() {
4303 self.busy = true;
4304 } else {
4305 self.busy = false;
4306 if self.status_message.as_deref() == Some(status.as_str())
4309 && !self.jobs.shows(&status)
4310 {
4311 self.status_message = None;
4312 }
4313 }
4314 }
4315 if cancelled_analysis
4317 && self.cancelled_analysis().is_none()
4318 && self.analysis_modal.quality.setup_note.as_deref() == Some(QUALITY_RUN_WAITS)
4319 {
4320 self.analysis_modal.quality.setup_note = None;
4321 }
4322 out
4323 }
4324
4325 fn job_progress(&mut self, ticket: Ticket, progress: &Progress) {
4327 if !self.jobs.is_current(ticket) {
4328 return;
4329 }
4330 match progress {
4331 Progress::ExportWriting { phase, bytes } => {
4332 if let Some(export) = self.export_progress.as_mut() {
4333 export.current_phase = phase.to_string();
4334 export.written = Some(*bytes);
4335 }
4336 }
4337 Progress::QualityPhase(phase) => {
4338 if let Some(progress) = self.analysis_modal.computing.as_mut() {
4339 progress.phase = phase.stage.label().to_string();
4340 progress.reads_source = Some(phase.reads_source);
4341 progress.interruptible = Some(phase.interruptible);
4342 }
4343 }
4344 Progress::Finding { rows } => self.find_progress(*rows),
4345 Progress::HexFinding { read, total } => self.hex_find_progress(*read, *total),
4346 Progress::SampleBegun(schema) => self.sample_begun(schema),
4347 Progress::SampleGrew => self.sample_grew(),
4348 }
4349 }
4350
4351 fn answered(
4354 &mut self,
4355 job: Job,
4356 current: bool,
4357 waited: bool,
4358 answer: Answer,
4359 ) -> Option<AppEvent> {
4360 match (job, answer) {
4361 (Job::JournalDetail { dataset }, Answer::JournalDescribed(detail)) => {
4362 if dataset == self.dataset_generation
4363 && let Some(state) = self.data_table_state.as_mut()
4364 {
4365 state.set_format_detail(*detail);
4366 }
4367 None
4368 }
4369 (Job::IndexLines { dataset }, Answer::LinesIndexed(rows)) => {
4370 self.lines_indexed(dataset, rows);
4371 None
4372 }
4373 (Job::FootersJoin { dataset }, Answer::FootersJoined(found)) => {
4374 self.footers_joined(dataset, found.map(|found| *found))
4375 }
4376 (Job::Load(load), Answer::Load(answer)) => {
4377 let step = self.loading.answered(
4380 load,
4381 *answer,
4382 #[cfg(any(feature = "http", feature = "cloud"))]
4383 &self.jobs,
4384 );
4385 self.run_load_step(step)
4386 }
4387 (
4388 Job::OpenNamed(load),
4389 Answer::NamedPaths {
4390 paths,
4391 options,
4392 directory,
4393 },
4394 ) => {
4395 if !self.loading.looking_at_paths(load) {
4397 return None;
4398 }
4399 Some(match directory {
4401 Some(dir) => AppEvent::LookThenOpenDirectory(dir, *options),
4402 None => AppEvent::Open(paths, *options),
4403 })
4404 }
4405 (Job::OpenNamed(load), Answer::NamedPathMissing(path)) => {
4406 if !self.loading.looking_at_paths(load) {
4407 return None;
4408 }
4409 if let Some(retired) = self.loading.retire() {
4411 self.put_down_load(retired);
4412 }
4413 Some(AppEvent::NamedPathMissing(path))
4414 }
4415 (
4416 Job::LookAtDirectory { load, path },
4417 Answer::LookedAt {
4418 kind,
4419 holds,
4420 options,
4421 },
4422 ) => {
4423 if !self.loading.looking_at_directory(load) {
4425 return None;
4426 }
4427 self.open_the_directory_looked_at(path, kind, holds.as_deref(), *options)
4429 }
4430 (Job::Classify(asked), Answer::Kind(found)) => {
4431 if !current {
4433 return None;
4434 }
4435 self.home.status = None;
4436
4437 if self.input_mode != InputMode::Home || self.home.browsing != asked.browsing {
4440 return None;
4441 }
4442
4443 let path = asked.path;
4444 let Some(kind) = found else {
4445 self.home.status = Some(format!("No such path: {}", path.display()));
4446 if asked.jump {
4447 self.home.path_input = path.display().to_string();
4449 self.home.path_input_active = true;
4450 self.list_the_typed_directory();
4451 }
4452 return None;
4453 };
4454 self.open_what_it_is(path, kind, asked.jump)
4455 }
4456 (Job::Rows(inflight), Answer::Rows(result)) => {
4457 if !current {
4459 return None;
4460 }
4461 if let Some(state) = self.data_table_state.as_ref() {
4464 let took = inflight.began.elapsed();
4465 log::debug!(
4466 target: "datui",
4467 "rows {}..{} of {}: read in {took:.1?}",
4468 inflight.start,
4469 inflight.end,
4470 inflight.dataset
4471 );
4472 state.measurements().read_page(took, inflight.files);
4473 }
4474 if let Some(state) = &mut self.data_table_state {
4475 state.apply_async_collect(result);
4476 }
4477 self.retire_a_count_the_rows_answered();
4478 self.remember_a_downloads_shape();
4479 self.catch_up_follow();
4481 let ran = self.take_query_run();
4483 if waited {
4486 self.first_rows_settled();
4487 match ran.map(|run| run.origin) {
4488 Some(RunOrigin::Query(mode)) if self.query_prompt_mode() == Some(mode) => {
4489 self.leave_query_prompt_after_run();
4490 }
4491 Some(RunOrigin::View {
4493 matched: Some((name, why)),
4494 ..
4495 }) => self.flash_view_applied(&name, why),
4496 _ => {}
4497 }
4498 }
4499 None
4500 }
4501 (
4502 Job::Rows(_),
4503 Answer::RowsFailed {
4504 message,
4505 conversion,
4506 },
4507 ) => {
4508 self.rows_failed(current, waited, &message, conversion.as_deref());
4509 None
4510 }
4511 (Job::Analysis(_), Answer::Analysis(install, results)) => {
4512 if current {
4513 install(&mut self.analysis_modal, results);
4514 self.analysis_modal.computing = None;
4515 }
4516 None
4517 }
4518 (
4519 Job::Analysis(_),
4520 Answer::DataQuality {
4521 results,
4522 kept,
4523 plan,
4524 },
4525 ) => {
4526 if let Some(kept) = kept {
4528 self.retain_quality_sample(&kept);
4529 }
4530 if current
4531 && self.overlay == Overlay::Analysis
4532 && self.analysis_modal.selected_tool
4533 == Some(analysis::analysis_modal::AnalysisTool::DataQuality)
4534 {
4535 self.cache_quality_result(&results, (*plan).clone());
4537 self.analysis_modal.quality.last_plan = Some(*plan);
4538 self.analysis_modal.quality.results = Some(*results);
4539 self.analysis_modal.quality.from_cache = false;
4540 self.analysis_modal
4541 .set_quality_page(crate::analysis::data_quality::QualityPage::Overview);
4542 self.analysis_modal.computing = None;
4543 }
4544 None
4545 }
4546 (Job::SampleDraw(draw), Answer::SampleDrawn(drawn)) => {
4547 self.sample_drawn(*draw, current, drawn)
4548 }
4549 (Job::SampleRows, Answer::Sample { df, label }) => {
4550 if current {
4551 self.analysis_modal.computing = None;
4552 self.show_sample_view(df, label);
4553 }
4554 None
4555 }
4556 (Job::Pivot, Answer::Pivoted { spec, pivoted }) => {
4557 if !current {
4559 return None;
4560 }
4561 let installed = self.data_table_state.as_mut().map(|state| {
4562 state
4563 .deferred(|s| s.install_pivot(&spec, pivoted))
4564 .map_err(|e| crate::error_display::user_message_from_report(&e, None))
4565 });
4566 match installed {
4567 Some(Ok(())) => {
4568 if self.overlay == Overlay::PivotMelt {
4570 self.close_overlay();
4571 }
4572 self.spawn_async_collect(Self::LOADING_BUFFER);
4574 }
4575 Some(Err(message)) => self.error_modal.show(message),
4576 None => {}
4577 }
4578 None
4579 }
4580 (Job::ReshapePreview { epoch, token }, Answer::ReshapePreviewed { input, result }) => {
4581 self.reshape_preview_ended(epoch, token, input, result);
4582 None
4583 }
4584 (Job::ViewPivot(pivot), Answer::ViewPivoted(pivoted)) => {
4585 let (view, why) = *pivot;
4587 if !current {
4588 return None;
4589 }
4590 let planned = self.data_table_state.as_mut().map(|state| {
4591 state
4594 .try_transition(|s| Self::replay_view(s, &view.settings, Some(pivoted)))
4595 .map(|(_, rollback)| rollback)
4596 .map_err(|e| e.to_string())
4597 });
4598 match planned {
4599 Some(Ok(rollback)) => self.view_planned(&view, rollback, why),
4601 Some(Err(message)) => self.view_pivot_failed(&message),
4602 None => {}
4603 }
4604 None
4605 }
4606 (Job::DrillRow, Answer::DrillRow { group_index, row }) => {
4607 if current {
4609 self.drill_into(group_index, &row);
4610 }
4611 None
4612 }
4613 (Job::InspectRow { frame, row }, Answer::FieldsRead(values)) => {
4614 if !current {
4616 return None;
4617 }
4618 let asked = self
4619 .inspector_modal
4620 .read
4621 .as_ref()
4622 .is_some_and(|read| read.key() == (frame, row));
4623 if self.overlay == Overlay::Inspect && asked {
4624 self.inspector_modal.read =
4625 Some(inspector::inspector_modal::FieldRead::Read { frame, row, values });
4626 }
4627 None
4628 }
4629 (Job::InspectJson { token }, Answer::JsonParsed(root)) => {
4630 if !current || self.overlay != Overlay::Inspect {
4632 return None;
4633 }
4634 let modal = &mut self.inspector_modal;
4635 if let Some(wait) = modal.json_wait.take_if(|w| w.token == token) {
4636 let node = inspector::inspector_drill::Node::Json {
4637 root,
4638 path: Vec::new(),
4639 };
4640 modal.drill_in(wait.frame, wait.row, wait.label, node);
4641 }
4642 None
4643 }
4644 (Job::InspectPretty { token }, Answer::Indented(text)) => {
4645 let modal = &mut self.inspector_modal;
4646 if current
4647 && let Some(inspector::inspector_modal::Pretty::Pending { token: t, place }) =
4648 modal.pretty.as_ref()
4649 && *t == token
4650 {
4651 modal.pretty = Some(inspector::inspector_modal::Pretty::Ready {
4652 place: place.clone(),
4653 text,
4654 });
4655 }
4656 None
4657 }
4658 (Job::InspectUnpack { token }, Answer::Unpacked(decoded)) => {
4659 let modal = &mut self.inspector_modal;
4660 if current
4661 && let Some(inspector::inspector_modal::Unpack::Pending { token: t, place }) =
4662 modal.unpack.as_ref()
4663 && *t == token
4664 {
4665 modal.unpack = Some(inspector::inspector_modal::Unpack::Ready {
4666 place: place.clone(),
4667 text: std::sync::Arc::new(decoded),
4668 });
4669 }
4670 None
4671 }
4672 (Job::OpenValue, Answer::ValueWritten(open)) => {
4673 if current && self.overlay == Overlay::Inspect {
4674 self.external.open = Some(open);
4675 }
4676 None
4677 }
4678 (Job::Export, Answer::Exported(path)) => {
4679 self.export_modal.close();
4681 if current {
4682 self.export_progress = None;
4683 self.flash_path("Exported to ", &path);
4684 }
4685 None
4686 }
4687 (Job::Copy, Answer::Copied { payload, message }) => {
4688 if current {
4689 self.export_progress = None;
4690 self.finish_copy(payload, message);
4691 }
4692 None
4693 }
4694 (Job::QualityReport, Answer::QualityReportWritten(path)) => {
4695 self.analysis_modal.quality.export = None;
4696 if current {
4697 self.flash_path("Report written to ", &path);
4698 }
4699 None
4700 }
4701 (Job::ChartPrepare(prep), Answer::ChartPrepared(prepared)) => {
4702 self.chart_prepared(*prep, current, Ok(*prepared));
4703 None
4704 }
4705 (Job::ChartExport { path, format }, Answer::ChartExported) => {
4706 if current {
4709 self.finish_chart_export(&path, format, Ok(()));
4710 }
4711 None
4712 }
4713 (Job::FileFacts { dataset }, Answer::FileFacts(facts)) => {
4714 self.file_facts_landed(dataset, facts);
4715 None
4716 }
4717 (Job::UnfitCount { dataset, version }, Answer::UnfitCounted(unfit)) => {
4718 let columns: Vec<&str> = unfit.iter().map(|u| u.column.as_str()).collect();
4720 let said = if columns.is_empty() {
4721 "none".to_string()
4722 } else {
4723 columns.join(", ")
4724 };
4725 log::debug!(target: "datui", "values column types made null, by column: {said}");
4726 if dataset == self.dataset_generation
4727 && let Some(state) = self.data_table_state.as_mut()
4728 {
4729 match version {
4730 None => state.unfit_counted(&unfit),
4731 Some(version) => state.changes_unfit_counted(version, &unfit),
4732 }
4733 }
4734 None
4735 }
4736 (Job::Find(run), Answer::Found(found)) => {
4737 self.find_answered(run, current, found);
4738 None
4739 }
4740 (
4741 Job::HexOpen {
4742 origin,
4743 fallback,
4744 record_size,
4745 },
4746 Answer::HexOpened(source),
4747 ) => {
4748 if current {
4750 self.hex_opened(origin, fallback, record_size, *source);
4751 }
4752 None
4753 }
4754 (Job::HexFind(run), Answer::HexFound(hit)) => {
4755 if current {
4756 self.hex_found(run, hit);
4757 }
4758 None
4759 }
4760 (Job::ValueCounts, Answer::ValueCounts(counts)) => {
4761 if current {
4763 self.value_counts.computing = None;
4764 self.value_counts.hold(*counts);
4765 }
4766 None
4767 }
4768 #[cfg(test)]
4770 (_, Answer::Probe(held)) => {
4771 drop(held);
4772 None
4773 }
4774 (
4777 job,
4778 Answer::Load(_)
4779 | Answer::NamedPaths { .. }
4780 | Answer::NamedPathMissing(_)
4781 | Answer::LookedAt { .. }
4782 | Answer::Kind(_)
4783 | Answer::Rows(_)
4784 | Answer::ReshapePreviewed { .. }
4785 | Answer::ViewPivoted(_)
4786 | Answer::FieldsRead(_)
4787 | Answer::JsonParsed(_)
4788 | Answer::Indented(_)
4789 | Answer::Unpacked(_)
4790 | Answer::ChartPrepared(_)
4791 | Answer::ChartExported
4792 | Answer::FileFacts(_)
4793 | Answer::UnfitCounted(_)
4794 | Answer::Found(_)
4795 | Answer::HexOpened(_)
4796 | Answer::HexFound(_)
4797 | Answer::FootersJoined(_)
4798 | Answer::JournalDescribed(_)
4799 | Answer::LinesIndexed(_)
4800 | Answer::RowsFailed { .. }
4801 | Answer::Analysis(..)
4802 | Answer::DataQuality { .. }
4803 | Answer::SampleDrawn(_)
4804 | Answer::Sample { .. }
4805 | Answer::Pivoted { .. }
4806 | Answer::DrillRow { .. }
4807 | Answer::ValueWritten(_)
4808 | Answer::Exported(_)
4809 | Answer::Copied { .. }
4810 | Answer::QualityReportWritten(_)
4811 | Answer::ValueCounts(_),
4812 ) => {
4813 log::error!(target: "datui", "a {:?} job got another job's answer; dropped", job.kind());
4814 None
4815 }
4816 }
4817 }
4818
4819 fn background_failed(
4823 &mut self,
4824 job: &Job,
4825 current: bool,
4826 waited: bool,
4827 message: &str,
4828 panicked: bool,
4829 ) {
4830 let could_not = |what: &str| {
4832 if panicked {
4833 format!("Could not {what}; see the log")
4834 } else {
4835 format!("Could not {what}: {message}")
4836 }
4837 };
4838 match job {
4839 Job::Load(load) | Job::OpenNamed(load) | Job::LookAtDirectory { load, .. } => {
4841 if let loading::Step::Failed(failed) = self.loading.failed(*load, message) {
4842 self.load_failed(failed);
4843 }
4844 }
4845 Job::FootersJoin { dataset } => {
4847 self.footers_joined(*dataset, None);
4848 }
4849 Job::ChartPrepare(prep) => {
4850 let message = if panicked {
4851 "Chart preparation panicked".to_string()
4852 } else {
4853 message.to_string()
4854 };
4855 self.chart_prepared(*prep.clone(), current, Err(message));
4856 }
4857 Job::Rows(_) | Job::OwedRows { .. } => {
4858 self.rows_failed(current, waited, message, None);
4859 }
4860 Job::SampleDraw(draw) => self.sample_draw_failed(draw, current, message),
4861 Job::ReshapePreview { epoch, token } => {
4863 let message = if panicked {
4864 "Could not preview; see the log".to_string()
4865 } else {
4866 message.to_string()
4867 };
4868 self.reshape_preview_ended(*epoch, *token, None, Err(message));
4869 }
4870 Job::InspectPretty { token } => {
4871 let modal = &mut self.inspector_modal;
4872 if let Some(inspector::inspector_modal::Pretty::Pending { token: t, place }) =
4873 modal.pretty.as_ref()
4874 && t == token
4875 {
4876 modal.pretty = Some(inspector::inspector_modal::Pretty::Failed {
4877 place: place.clone(),
4878 });
4879 }
4880 }
4881 Job::InspectUnpack { token } => {
4882 let modal = &mut self.inspector_modal;
4883 if let Some(inspector::inspector_modal::Unpack::Pending { token: t, place }) =
4884 modal.unpack.as_ref()
4885 && t == token
4886 {
4887 modal.unpack = Some(inspector::inspector_modal::Unpack::Failed {
4888 place: place.clone(),
4889 });
4890 }
4891 }
4892 Job::Find(_) => self.find_failed(current, message),
4893 Job::FileFacts { dataset } => {
4895 let why = if panicked {
4896 "could not read; see the log".to_string()
4897 } else {
4898 message.to_string()
4899 };
4900 self.file_facts_landed(*dataset, FileFacts::Failed(why));
4901 }
4902 Job::UnfitCount { .. } => {
4904 log::warn!(target: "datui", "counting values that did not fit their type failed: {message}");
4905 }
4906 Job::JournalDetail { .. } => {
4908 log::warn!(target: "datui", "reading the journal's detail failed: {message}");
4909 }
4910 Job::IndexLines { .. } => {}
4913 _ if !current => {
4915 if matches!(job, Job::Export) {
4916 self.export_modal.close();
4918 }
4919 }
4920 Job::Classify(_) => {
4921 self.home.status = None;
4922 self.error_modal.show(message.to_string());
4923 }
4924 Job::Analysis(_) | Job::SampleRows => {
4925 self.analysis_modal.computing = None;
4926 self.error_modal.show(message.to_string());
4927 }
4928 Job::Pivot | Job::Copy | Job::HexOpen { .. } => {
4930 self.error_modal.show(message.to_string());
4931 }
4932 Job::QualityReport => match self.analysis_modal.quality.export.as_mut() {
4934 Some(form) => form.error = Some(message.to_string()),
4935 None => self.error_modal.show(message.to_string()),
4936 },
4937 Job::ViewPivot(_) => self.view_pivot_failed(message),
4938 Job::DrillRow => self.flash_note(could_not("drill in")),
4940 Job::InspectJson { token } => {
4941 let modal = &mut self.inspector_modal;
4942 if let Some(wait) = modal.json_wait.take_if(|w| w.token == *token) {
4943 modal.not_json = Some((wait.frame, wait.row, wait.path));
4944 self.flash_note(if panicked {
4945 "Could not read the JSON; see the log".to_string()
4946 } else {
4947 sentence(message)
4948 });
4949 }
4950 }
4951 Job::OpenValue => self.flash_note(could_not("open the value")),
4952 Job::InspectRow { frame, row } => {
4953 let asked = self
4954 .inspector_modal
4955 .read
4956 .as_ref()
4957 .is_some_and(|read| read.key() == (*frame, *row));
4958 if asked {
4959 self.inspector_modal.read =
4960 Some(inspector::inspector_modal::FieldRead::Failed {
4961 frame: *frame,
4962 row: *row,
4963 message: could_not("read the field"),
4964 });
4965 }
4966 }
4967 Job::Export => {
4969 self.export_progress = None;
4970 self.export_modal.path_error = Some(message.to_string());
4971 self.open_over(|returns_to| Overlay::Export { returns_to });
4972 }
4973 Job::ChartExport { path, format } => {
4974 self.finish_chart_export(path, *format, Err(message.to_string()));
4975 }
4976 Job::HexFind(_) => {
4977 self.status_message = None;
4978 self.flash_note(message.to_string());
4979 }
4980 Job::ValueCounts => {
4982 if let Some(computing) = self.value_counts.computing.take() {
4983 let why = if panicked {
4984 "could not count; see the log".to_string()
4985 } else {
4986 message.to_string()
4987 };
4988 self.value_counts.failed = Some((computing.column, why));
4989 }
4990 }
4991 }
4992 }
4993
4994 fn read_file_facts(&mut self) {
4999 let dataset = self.dataset_generation;
5000 if self.data_table_state.is_none()
5001 || self
5002 .info
5003 .file_facts
5004 .as_ref()
5005 .is_some_and(|(read, _)| *read == dataset)
5006 || self.file_facts_reading()
5007 {
5008 return;
5009 }
5010 let several = self
5013 .source
5014 .opened
5015 .as_ref()
5016 .is_some_and(|(paths, _)| paths.len() > 1);
5017 let piped = self.reads_stdin();
5018 let Some(path) = self.path.clone().filter(|path| {
5019 !several
5020 && !piped
5021 && !cloud::source::is_remote_url(path)
5022 && !cloud::source::is_prefix_or_glob(&path.to_string_lossy())
5023 }) else {
5024 return;
5025 };
5026 let facts = self.facts_of_open().map(|(_, facts)| facts);
5027 #[cfg(test)]
5028 let read: FileFactsReader = self
5029 .file_facts_reader
5030 .clone()
5031 .unwrap_or_else(|| Arc::new(FileFacts::read));
5032 #[cfg(not(test))]
5033 let read = FileFacts::read;
5034 self.spawn_job(Job::FileFacts { dataset }, None, move |_| {
5035 Ok(Answer::FileFacts(read(&path, facts)?))
5036 });
5037 }
5038
5039 pub(crate) fn file_facts_reading(&self) -> bool {
5041 let dataset = self.dataset_generation;
5042 self.jobs
5043 .current(|job| matches!(job, Job::FileFacts { dataset: asked } if *asked == dataset))
5044 .is_some()
5045 }
5046
5047 fn file_facts_landed(&mut self, dataset: u64, facts: FileFacts) {
5049 if dataset == self.dataset_generation {
5050 self.info.file_facts = Some((dataset, facts));
5051 }
5052 }
5053
5054 pub fn file_facts(&self) -> Option<&FileFacts> {
5057 Self::facts_shown(
5058 &self.info.file_facts,
5059 self.dataset_generation,
5060 self.file_facts_reading(),
5061 )
5062 }
5063
5064 pub(crate) fn facts_shown(
5066 read: &Option<(u64, FileFacts)>,
5067 dataset: u64,
5068 reading: bool,
5069 ) -> Option<&FileFacts> {
5070 static READING: FileFacts = FileFacts::Reading;
5071 match read {
5072 Some((read_for, facts)) if *read_for == dataset => Some(facts),
5073 _ => reading.then_some(&READING),
5074 }
5075 }
5076
5077 fn land_on_tables(&mut self, tables: loading::Tables) {
5079 let loading::Tables {
5080 database,
5081 from_home,
5082 } = tables;
5083 self.status_message = None;
5084 self.busy = false;
5085 self.enter_home();
5086 if from_home {
5087 self.home_browse_into(database);
5088 } else {
5089 self.home_jump_into(database);
5090 }
5091 }
5092
5093 fn load_failed(&mut self, failed: loading::Failed) {
5096 let loading::Failed { message, from_home } = failed;
5097 self.status_message = None;
5098 self.busy = false;
5099 if from_home {
5102 self.home_app.last_load_error = None;
5103 self.enter_home();
5104 } else {
5105 self.home_app.last_load_error = Some(message.clone());
5106 }
5107 self.error_modal.show(message);
5108 }
5109
5110 fn rows_failed(
5114 &mut self,
5115 current: bool,
5116 waited: bool,
5117 message: &str,
5118 conversion: Option<&crate::error_display::ConversionFailure>,
5119 ) {
5120 if !current {
5121 return;
5122 }
5123 let message = &self.named_by_source(message);
5124 if let Some(run) = self.take_query_run() {
5125 self.fail_query_run(run, message, conversion);
5126 return;
5127 }
5128 if !waited {
5129 return;
5130 }
5131 if let Some(generation) = self.counting.count_after_paint.take() {
5134 if self.counting.len_count_inflight == Some(generation) {
5135 self.counting.len_count_inflight = None;
5136 }
5137 if self
5138 .data_table_state
5139 .as_ref()
5140 .is_some_and(|state| state.len_generation() == generation)
5141 {
5142 self.counting.len_count_failed = Some(generation);
5143 }
5144 }
5145 self.first_rows_settled();
5146 self.error_modal.show(message.to_string());
5147 }
5148
5149 fn named_by_source(&self, message: &str) -> String {
5152 let (Some(state), Some(source)) = (self.data_table_state.as_ref(), self.path.as_deref())
5153 else {
5154 return message.to_string();
5155 };
5156 state
5157 .temp_files()
5158 .into_iter()
5159 .fold(message.to_string(), |message, file| {
5160 crate::error_display::named_by_source(&message, file, source)
5161 })
5162 }
5163
5164 const COPY_CONFIRM_BYTES: usize = 10 * 1024 * 1024;
5167 const COPY_REFUSE_BYTES: usize = 200 * 1024 * 1024;
5170
5171 fn drill_selected_row(&mut self) {
5174 let Some(state) = self.data_table_state.as_ref() else {
5175 return;
5176 };
5177 let drill = state
5179 .table_state
5180 .selected()
5181 .map(|selected| state.start_row() + selected)
5182 .and_then(|index| Some((index, state.drill_row(index)?)));
5183 match drill {
5184 None => self.flash_note("No group to drill down into".to_string()),
5185 Some((group_index, DrillRow::Buffered(row))) => self.drill_into(group_index, &row),
5186 Some((group_index, DrillRow::Read(lf))) => {
5187 let streaming = state.polars_streaming();
5188 self.spawn_job(Job::DrillRow, Some(Self::READING_GROUP), move |_| {
5189 let row = crate::analysis::statistics::collect_lazy(*lf, streaming)
5190 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
5191 Ok(Answer::DrillRow { group_index, row })
5192 });
5193 }
5194 }
5195 }
5196
5197 pub fn follow_terminal_background(&mut self, mode: ThemeMode) {
5201 let theme = &self.app_config.theme;
5202 if !theme.follow || theme.mode == Some(mode) {
5203 return;
5204 }
5205 let mut next = theme.clone();
5206 let built = theme.palette_for(mode).and_then(|colors| {
5207 next.colors = colors;
5208 next.mode = Some(mode);
5209 Theme::from_config(&next)
5210 });
5211 match built {
5212 Ok(built) => {
5213 self.theme = built;
5214 self.chart.modal.series_cap = Some(self.theme.series_colors().len());
5215 self.app_config.theme = next;
5216 for input in [
5219 &mut self.prompt.query_input,
5220 &mut self.prompt.sql_input,
5221 &mut self.prompt.find.input,
5222 ] {
5223 *input = std::mem::take(input).with_theme(&self.theme);
5224 }
5225 }
5226 Err(e) => log::warn!("cannot switch to the {mode:?} palette: {e}"),
5228 }
5229 }
5230
5231 fn terminal_answered(&mut self, mode: ThemeMode) {
5234 if self.app_config.theme.follow {
5235 self.cache
5236 .remember_terminal_mode(&app::terminal_color::terminal_key(), mode);
5237 }
5238 self.follow_terminal_background(mode);
5239 }
5240
5241 pub fn settle_first_palette(&mut self, answered: Option<ThemeMode>) {
5244 if let Some(mode) = answered {
5245 self.terminal_answered(mode);
5246 } else if self.app_config.theme.follow
5247 && let Some(mode) = self
5248 .cache
5249 .terminal_mode(&app::terminal_color::terminal_key())
5250 {
5251 self.follow_terminal_background(mode);
5252 }
5253 }
5254
5255 pub fn take_background_query(&mut self) -> bool {
5258 std::mem::take(&mut self.display.background_query)
5259 }
5260
5261 pub fn theme(&self) -> &Theme {
5263 &self.theme
5264 }
5265
5266 pub fn follows_terminal(&self) -> bool {
5268 self.app_config.theme.follow
5269 }
5270
5271 pub fn take_external_open(&mut self) -> Option<inspector::external_open::ExternalOpen> {
5273 self.external.open.take()
5274 }
5275
5276 pub fn mouse_enabled(&self) -> bool {
5279 self.app_config.display.mouse
5280 }
5281
5282 pub fn external_opened(
5285 &mut self,
5286 open: &inspector::external_open::ExternalOpen,
5287 failed: Option<String>,
5288 ) {
5289 let program =
5290 inspector::external_open::program_for(open.document, |name| std::env::var(name).ok());
5291 if matches!(program, inspector::external_open::Program::Wait(_)) {
5292 let _ = std::fs::remove_file(&open.path);
5293 }
5294 match failed {
5295 Some(e) => self.flash_note(format!("Could not open the value: {e}")),
5296 None if matches!(program, inspector::external_open::Program::Opener(_)) => {
5297 self.flash_note("Opened in the system viewer".to_string())
5298 }
5299 None => {}
5300 }
5301 }
5302}
5303
5304impl Widget for &mut App {
5305 fn render(self, area: Rect, buf: &mut Buffer) {
5306 let started = std::time::Instant::now();
5307 self.draw_frame(area, buf);
5308 self.debug.times.frame(started.elapsed());
5309 }
5310}
5311
5312impl App {
5313 fn draw_frame(&mut self, area: Rect, buf: &mut Buffer) {
5314 self.begin_frame();
5315 self.debug.num_frames += 1;
5316 if self.debug.enabled {
5317 self.debug.show_help_at_render = self.help.is_open();
5318 }
5319
5320 use crate::render::context::RenderContext;
5321 use crate::render::layout::app_layout;
5322 use crate::render::main_view::MainViewContent;
5323
5324 let ctx = RenderContext::from_theme_and_config(
5325 &self.theme,
5326 self.display.table_cell_padding,
5327 self.display.column_colors,
5328 self.display.number_format.clone(),
5329 )
5330 .with_dtype_row(self.display.dtype_row);
5331
5332 let main_view_content = MainViewContent::current(self);
5333
5334 Clear.render(area, buf);
5335 let background_color = self.theme.background();
5336 Block::default()
5337 .style(Style::default().bg(background_color))
5338 .render(area, buf);
5339
5340 let progress = self.footer_progress_line(main_view_content);
5343 let prompt_room = crate::render::footer::MAX_LINES - 1;
5344 let prompt_rows = if main_view_content == MainViewContent::Datatable {
5345 crate::render::input_strip::rows(self, area.width, prompt_room)
5346 } else {
5347 0
5348 };
5349 let progress_rows = u16::from(progress.is_some() && prompt_rows < prompt_room);
5350 let footer_lines = 1 + prompt_rows + progress_rows;
5351 let rule = self.overlay != Overlay::Inspect;
5353 let app_layout = app_layout(area, self.debug.enabled, footer_lines, rule);
5354 let room = app_layout.footer.height.saturating_sub(1);
5356 let prompt_rows = prompt_rows.min(room);
5357 let progress_rows = progress_rows.min(room - prompt_rows);
5358 let main_area = app_layout.main_view;
5359 Clear.render(main_area, buf);
5360
5361 crate::render::main_view_render::render_main_view(area, main_area, buf, self, &ctx);
5362 if self.in_normal_table_view() {
5363 self.render_drop_mark(buf, &ctx);
5364 }
5365 if self.menu_showing()
5366 && let Some(menu) = self.context_menu.clone()
5367 {
5368 menu.render(main_area, buf, &ctx);
5369 }
5370
5371 if self.confirmation_modal.active {
5372 crate::render::overlays::render_confirmation_modal(
5373 area,
5374 buf,
5375 &mut self.confirmation_modal,
5376 &ctx,
5377 );
5378 }
5379 if self.error_modal.active {
5380 crate::render::overlays::render_error_modal(area, buf, &mut self.error_modal, &ctx);
5381 }
5382 self.close_help_left_behind();
5383 if self.help.is_open() {
5384 crate::render::help::render_help(app_layout.main_view, buf, &mut self.help, &ctx);
5386 }
5387
5388 let footer = self.footer(main_view_content, progress_rows > 0);
5389 crate::render::footer::render_rule(app_layout.rule, buf, &ctx);
5390 let line = Rect {
5391 height: 1,
5392 ..app_layout.footer
5393 };
5394 let drawn = footer.render_line(line, buf, &ctx);
5395 if prompt_rows > 0 {
5396 crate::render::input_strip::render(
5397 Rect {
5398 y: line.y + 1,
5399 height: prompt_rows,
5400 ..line
5401 },
5402 buf,
5403 self,
5404 &ctx,
5405 );
5406 }
5407 if let Some(progress) = progress.filter(|_| progress_rows > 0) {
5408 crate::render::footer::render_progress(
5409 &progress,
5410 Rect {
5411 y: line.y + 1 + prompt_rows,
5412 height: 1,
5413 ..line
5414 },
5415 buf,
5416 &ctx,
5417 );
5418 }
5419 self.pointer.chips_drawn(
5420 drawn
5421 .iter()
5422 .map(|(rect, key)| (*rect, key.as_str()))
5423 .collect(),
5424 );
5425 if let Some(debug_area) = app_layout.debug {
5426 self.debug.render(debug_area, buf);
5427 }
5428 self.pointer.drawn();
5429
5430 crate::sanitize::sanitize_buffer(buf);
5436 }
5437}
5438
5439impl App {
5440 pub fn capture_view(&self) -> Result<Option<LazyFrame>> {
5445 let Some(state) = &self.data_table_state else {
5446 return Ok(None);
5447 };
5448 if state.scans_a_download() {
5449 return Err(color_eyre::eyre::eyre!(
5450 "cannot return this view: the data was downloaded to a temporary file \
5451 that is removed when datui exits. Export it from inside datui (press \
5452 e) instead."
5453 ));
5454 }
5455 if state.scans_a_temp_file() {
5456 return Err(color_eyre::eyre::eyre!(
5457 "cannot return this view: the data was decompressed or converted into a \
5458 temporary file that is removed when datui exits. Export it from \
5459 inside datui (press e) instead."
5460 ));
5461 }
5462 Ok(Some(state.visible_lf()))
5463 }
5464}
5465
5466impl Drop for App {
5467 fn drop(&mut self) {
5468 self.counting.stop_footer_pass();
5472 self.counting.stop_indexing();
5475 }
5476}
5477
5478#[cfg(feature = "cloud")]
5483pub(crate) fn wait_on_runtime<F>(runtime: &tokio::runtime::Handle, future: F) -> Option<F::Output>
5484where
5485 F: std::future::Future + Send + 'static,
5486 F::Output: Send + 'static,
5487{
5488 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5489 runtime.spawn(async move {
5490 let _ = tx.send(future.await);
5491 });
5492 rx.recv().ok()
5493}
5494
5495fn sentence(text: &str) -> String {
5497 let mut chars = text.chars();
5498 match chars.next() {
5499 Some(first) => first.to_uppercase().chain(chars).collect(),
5500 None => String::new(),
5501 }
5502}