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