1use color_eyre::Result;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use polars::datatypes::DataType;
4#[cfg(feature = "cloud")]
5use polars::io::cloud::{AmazonS3ConfigKey, CloudOptions};
6use polars::prelude::{DataFrame, LazyFrame, Schema, col};
7#[cfg(feature = "cloud")]
8use polars::prelude::{PlRefPath, ScanArgsParquet};
9use std::collections::HashMap;
10
11const MEASURE_BATCH: usize = 12;
14
15const PROBE_MEASURE_LIMIT: usize = 24;
17
18const CLASSIFY_BATCH: usize = 16;
26
27const MAX_CONCURRENT_PROBES: usize = 4;
30use std::path::{Path, PathBuf};
31use std::sync::{Arc, Mutex, mpsc::Sender};
32use widgets::info::{FileFacts, InfoModal};
33
34use ratatui::style::{Color, Style};
35use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
36
37use ratatui::widgets::{Block, Clear};
38
39mod analysis_keys;
40pub mod analysis_modal;
41pub mod audio;
42pub mod avro_types;
43#[cfg(feature = "cloud")]
44pub mod aws_profiles;
45#[cfg(feature = "cloud")]
46pub mod azure;
47mod background;
48pub mod cache;
49pub mod candump;
50pub mod canonical;
51pub mod catalog;
52pub mod chart_data;
53pub mod chart_export;
54pub mod chart_export_modal;
55mod chart_jobs;
56mod chart_keys;
57pub mod chart_modal;
58mod chart_pdf;
59mod chart_recipe;
60pub mod cli;
61pub mod clipboard;
62#[cfg(feature = "cloud")]
63mod cloud_arrow;
64#[cfg(feature = "cloud")]
65pub mod cloud_browse;
66#[cfg(feature = "cloud")]
67pub mod cloud_command;
68pub mod cloud_env;
69#[cfg(feature = "cloud")]
70mod cloud_hive;
71#[cfg(feature = "cloud")]
72pub mod cloud_sources;
73pub mod codebook;
74pub mod column_types;
75pub mod commands;
76pub mod config;
77pub mod config_command;
78pub mod context_menu;
79mod copy_keys;
80pub mod copy_modal;
81pub mod csv_dialect;
82pub mod data_quality;
83pub mod dataflash;
84mod dataset_files;
85pub mod dbc;
86pub mod delimited_spec;
87pub mod discover;
88pub mod distribution_fit;
89pub mod download;
90mod editing_keys;
91pub mod elf;
92pub mod error_display;
93pub mod event_pump;
94pub mod exact;
95pub mod excel;
96pub mod export;
97mod export_keys;
98pub mod export_modal;
99pub mod external_open;
100mod feedback;
101pub mod filter_modal;
102pub mod find;
103mod first_rows_trace;
104pub mod fix;
105pub mod fixed_records;
106pub mod follow;
107mod footer_state;
108pub mod form;
109pub mod formats;
110pub mod framed_records;
111pub mod fuzzy;
112#[cfg(feature = "cloud")]
113pub mod gcloud;
114pub mod glyphs;
115pub mod gps;
116pub mod help;
117mod hex_keys;
118pub mod hex_view;
119pub mod hf_splits;
120pub mod home;
121pub mod home_preview;
122pub mod indexed;
123mod info_keys;
124pub mod inspector_bytes;
125pub mod inspector_drill;
126pub mod inspector_modal;
127pub mod inspector_reader;
128pub mod intent_modal;
129pub mod ipc_stream;
130mod jobs;
131pub mod journal;
132pub mod lines;
133pub mod link_open;
134mod loading;
135pub mod local_copy;
136pub(crate) mod local_glob;
137pub mod locality;
138pub mod logging;
139pub mod measurements;
140pub mod members;
141pub mod midi;
142pub mod model_files;
143pub mod nested_json;
144pub mod notes;
145pub mod nul_tail;
146pub mod numfmt;
147pub mod numpy;
148mod open_options;
149pub mod output_file;
150pub mod parquet_footer;
151pub mod past_calendar;
152mod pivot_melt_keys;
153pub mod pivot_melt_modal;
154pub mod pointer;
155pub mod pushdown;
156pub mod python_script;
157pub mod quality_export;
158pub mod quality_intent;
159mod quality_memory;
160pub mod quality_report;
161pub mod quality_trends;
162#[cfg(any(feature = "http", feature = "cloud"))]
163mod remote_model;
164mod retype_keys;
165pub mod retype_modal;
166pub mod row_index;
167#[cfg(feature = "cloud")]
168pub mod s3_tools;
169mod sample_keys;
170pub mod sample_modal;
171pub mod sampling;
172pub mod table_sample;
173pub mod query;
176mod readers;
177mod render;
178pub mod sanitize;
179mod scan;
180pub mod schema_union;
181pub mod sdf;
182pub mod search;
183pub(crate) mod segments;
184mod sort_filter_keys;
185pub mod sort_filter_modal;
186pub mod sort_modal;
187pub mod source;
188pub(crate) mod spec_union;
189mod sql_assist;
190pub mod sqlite;
191#[cfg(feature = "sql")]
194pub mod sql_group;
195pub mod startup;
196pub mod statistics;
197pub mod stdin;
198pub mod table_switch;
199pub mod tee;
200mod terminal;
201mod terminal_color;
202pub mod terminal_input;
203pub mod text_formats;
204pub mod themes;
205pub mod typed_value;
206pub mod ulog;
207mod unfinished;
208pub mod user_agent;
209pub mod value_counts;
210pub mod value_counts_modal;
211pub mod vcd;
212pub mod view;
213mod view_keys;
214pub mod widgets;
215
216pub use cache::CacheManager;
217pub use cli::Args;
218pub use config::{
219 AppConfig, ColorParser, ConfigManager, QueryMode, Theme, ThemeMode, rgb_to_256_color,
220 rgb_to_basic_ansi,
221};
222
223use analysis_modal::{AnalysisModal, AnalysisProgress};
224use background::{CacheWrites, InflightCollect, LenCount, OwedAnswer, OwedCount};
225use chart_export::{ChartExportFormat, ChartExportRequest};
226use chart_export_modal::ChartExportModal;
227use chart_jobs::{
228 ChartCache, ChartExportJob, ChartInflight, ChartPrepared, ChartRequest, ChartResultSlot,
229 log_series,
230};
231use chart_modal::{ChartColumns, ChartModal};
232pub use error_display::{ErrorKindForPython, error_for_python};
233pub use export::{ExportOptions, ExportRequest};
234use export_modal::{ExportFocus, ExportFormat, ExportModal};
235pub use feedback::{ConfirmationModal, ErrorModal, Flash};
236use filter_modal::{FilterOperator, FilterStatement, LogicalOperator};
237use form::FormKey;
238use jobs::{Answer, Job, Jobs, Outcome};
239pub use jobs::{JobKind, Progress, Ticket};
240use numfmt::NumberFormatSettings;
241pub use open_options::{
242 OpenOptions, ParseStringsTarget, ReadReport, SqliteOpen, TypedDialect, UnaskedDownload,
243};
244use output_file::Overwrite;
245use pivot_melt_modal::{MeltSpec, PivotMeltModal, PivotSpec};
246pub use quality_memory::{KeptQualitySample, QUALITY_MEMORY_BUDGET, RetainedCopy};
247use quality_memory::{QUALITY_RELEASED_REMEMBERED, QualityCacheEntry, QualityCopyJob};
248use scan::Scan;
249use sort_filter_modal::SortFilterModal;
250use sort_modal::{SortColumn, order_with_hidden};
251use terminal::{QuietTerminal, TakenTerminal, follow_focus, push_keyboard_flags, restore_terminal};
252pub use unfinished::ExitSweep;
253pub use view::{SavedView, ViewManager, Views};
254use widgets::column_widths::WidthChoice;
255use widgets::datatable::{DataTableState, DrillRow, OpenFacts};
256use widgets::debug::DebugState;
257use widgets::text_input::TextInput;
258use widgets::view_modal::{FormFocus, ViewModal, ViewModalMode, ViewRow};
259
260pub const APP_NAME: &str = "datui";
262
263pub(crate) const UNSUPPORTED: &str =
265 "Unsupported file type. --format names the format to read it as.";
266
267pub use cli::{CompressionFormat, FileFormat, ReadMode, RemoteRead, Stored, Summary};
269
270#[cfg(test)]
271mod text_input_flows;
272
273#[cfg(test)]
274pub mod tests;
275
276pub enum AppEvent {
277 Key(KeyEvent),
278 Press(KeyEvent),
282 Terminal(crossterm::event::Event),
286 Wake,
289 TerminalBackground(ThemeMode),
292 TerminalFocused,
295 SettingsRead(Box<Result<startup::Settings>>),
298 OpenNamed(Vec<PathBuf>, OpenOptions),
303 NamedPathMissing(PathBuf),
306 Open(Vec<PathBuf>, OpenOptions),
309 OpenLazyFrame(Box<LazyFrame>, OpenOptions),
311 HomeListingReady {
313 generation: u64,
314 listing: Box<crate::home::Listing>,
315 known: std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
317 visits: std::collections::HashMap<PathBuf, crate::cache::Visits>,
319 newest: Option<PathBuf>,
321 folds: Option<std::collections::HashMap<String, bool>>,
323 },
324 HomeListingFailed,
327 HomePathListed {
329 listing: Box<crate::home::PathListing>,
330 },
331 HomePathCompleted {
333 generation: u64,
334 typed: String,
337 completed: String,
338 candidates: usize,
339 },
340 HomePreviewReady {
343 path: PathBuf,
344 stamp: crate::home_preview::Stamp,
346 read_at: Option<crate::home_preview::Stamp>,
348 rows: Option<Arc<crate::home_preview::PreviewRows>>,
349 prepared: crate::home_preview::Handoff,
350 },
351 HomeSchemaReady {
353 generation: u64,
354 path: PathBuf,
355 preview: Option<crate::discover::SchemaPreview>,
356 },
357 HomeMeasured {
365 measured: Vec<(PathBuf, crate::home::Measured)>,
366 done: bool,
367 },
368 HomeSized {
370 path: PathBuf,
371 measured: crate::home::Measured,
372 },
373 HomeWebGone {
375 path: PathBuf,
376 gone: crate::error_display::HttpGone,
377 },
378 HomeClassified {
382 measured: Vec<(PathBuf, crate::home::Measured)>,
383 done: bool,
384 },
385 HomeSearchBatch {
389 generation: u64,
390 root: PathBuf,
391 found: Vec<crate::discover::Entry>,
392 scanned: usize,
393 },
394 HomeSearchScored {
396 epoch: u64,
397 matches: Option<Box<crate::search::Matches>>,
399 },
400 HomeSearchDone {
403 generation: u64,
404 root: PathBuf,
405 scanned: usize,
406 limited: Option<String>,
407 },
408 #[cfg(feature = "cloud")]
411 HomeCloudSources {
412 sources: Vec<crate::home::CloudSource>,
413 },
414 #[cfg(feature = "cloud")]
417 HomeCloudListed {
418 id: String,
419 buckets: Vec<PathBuf>,
420 details: Vec<(PathBuf, Vec<(String, String)>)>,
422 failure: Option<(String, String)>,
424 listed_at: std::time::SystemTime,
425 },
426 HomeProbeReady {
428 root: PathBuf,
429 rows: Option<Vec<crate::discover::Entry>>,
430 cut_short: bool,
432 },
433 HomeProbeProgress {
435 root: PathBuf,
436 rows: Vec<crate::discover::Entry>,
437 },
438 HomeCloudKinds {
441 kinds: Vec<(
442 PathBuf,
443 (crate::discover::EntryKind, crate::discover::Holds),
444 )>,
445 failed: Vec<PathBuf>,
448 },
449 HomeProbeCancelled {
452 root: PathBuf,
453 },
454 HomeNarrowed {
457 dir: PathBuf,
458 prefix: String,
459 listed: Option<(Vec<crate::discover::Entry>, bool)>,
460 },
461 HomeProbeFailed {
463 root: PathBuf,
464 message: String,
465 },
466 DoExport(ExportRequest),
469 Followed(crate::follow::News),
471 FollowedDetail {
474 dataset_generation: u64,
475 detail: Box<crate::text_formats::Detail>,
476 },
477 Exit,
478 Crash(String),
479 QQuery(String),
480 SqlQuery(String),
481 Filter(Vec<FilterStatement>),
482 Sort(Vec<String>, Vec<bool>), ColumnOrder(Vec<String>, usize), Pivot(PivotSpec),
485 Melt(MeltSpec),
486 Export(ExportRequest),
487 CopyTable {
489 format: crate::clipboard::CopyFormat,
490 header: bool,
491 },
492 ChartExport(ChartExportRequest),
493 OpenLink(String),
496 DoChartExport(ChartExportRequest),
498 Collect,
499 Update,
500 Reset,
501 Resize(u16, u16), DoScrollDown, DoScrollUp, DoScrollNext, DoScrollPrev, DoScrollEnd, DoScrollHome, DoScrollHalfDown, DoScrollHalfUp, GoToLine(usize), AnalysisChunk,
513 AnalysisDistributionCompute,
515 AnalysisCorrelationCompute,
517 AnalysisDataQualityCompute,
519 BackgroundQualitySampleKept {
522 kept: KeptQualitySample,
523 },
524 BackgroundQualityCopyKept {
528 dataset_generation: u64,
529 copy: Option<Arc<crate::local_copy::LocalCopy>>,
530 },
531 BackgroundLenReady {
536 len_generation: u64,
537 num_rows: usize,
538 file_row_groups: Option<Vec<Vec<usize>>>,
541 },
542 BackgroundLenFailed {
545 len_generation: u64,
546 },
547 FramePainted,
550 BackgroundFootersJoined {
554 generation: u64,
555 },
556 LinesIndexed {
559 generation: u64,
560 rows: usize,
561 },
562 BackgroundChartReady,
566 QualityReportExport(PathBuf, crate::quality_export::ReportFormat, Overwrite),
569 LookThenOpenDirectory(PathBuf, OpenOptions),
577 ClassifyThenOpen {
584 path: PathBuf,
585 jump: bool,
589 },
590 JobEnded(Ticket),
592 JobProgress {
594 ticket: Ticket,
595 progress: Progress,
596 },
597}
598
599impl AppEvent {
600 pub fn is_progress(&self) -> bool {
603 matches!(
604 self,
605 AppEvent::HomeMeasured { done: false, .. }
606 | AppEvent::HomeClassified { done: false, .. }
607 | AppEvent::HomeSearchBatch { .. }
608 | AppEvent::HomeProbeProgress { .. }
609 | AppEvent::JobProgress {
610 progress: Progress::QualityPhase(_),
611 ..
612 }
613 )
614 }
615}
616
617#[cfg(test)]
620type HomeWorkerDies = Box<dyn FnMut(&AppEvent) -> bool + Send>;
621
622#[cfg(test)]
624type FileFactsReader = Arc<
625 dyn Fn(&Path, Option<crate::readers::Facts>) -> std::result::Result<FileFacts, String>
626 + Send
627 + Sync,
628>;
629
630pub type EventOutcome = Result<Option<AppEvent>, KeyEvent>;
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub enum WhatEnter {
650 OpensFile,
652 OpensDirectory,
655 GoesInside,
657 LooksFirst,
659 FoldsSection,
661 ShowsMore,
663 ShowsHidden,
665 Explains,
668 OpensHex,
670 Nothing,
672}
673
674impl App {
675 pub fn what_enter_does(&self) -> WhatEnter {
677 let rows = self.home.visible();
681 let entry = match rows.get(self.home.selected) {
682 Some(home::Row::Place { path, .. }) => {
686 return if home::place_is_browsable(path) {
687 WhatEnter::GoesInside
688 } else {
689 WhatEnter::Explains
690 };
691 }
692 Some(home::Row::Header { .. }) => return WhatEnter::FoldsSection,
693 Some(home::Row::More { .. }) => return WhatEnter::ShowsMore,
694 Some(home::Row::Hidden { .. }) => return WhatEnter::ShowsHidden,
695 None => return WhatEnter::Nothing,
697 Some(home::Row::Door { .. }) => return WhatEnter::OpensDirectory,
700 Some(home::Row::Entry { entry, .. }) => *entry,
701 };
702 if entry.kind != discover::EntryKind::File && self.home.bookmark(&entry.path).is_some() {
704 return WhatEnter::OpensDirectory;
705 }
706 match entry.kind {
707 discover::EntryKind::Unknown => WhatEnter::LooksFirst,
708 discover::EntryKind::File if entry.enter_lists_tables() => WhatEnter::GoesInside,
710 discover::EntryKind::File => WhatEnter::OpensFile,
711 discover::EntryKind::Other
712 if matches!(
713 source::input_source(&entry.path),
714 source::InputSource::Local(_)
715 ) =>
716 {
717 WhatEnter::OpensHex
718 }
719 discover::EntryKind::Other => WhatEnter::Nothing,
720 discover::EntryKind::Hive | discover::EntryKind::MultiFile => WhatEnter::OpensDirectory,
721 discover::EntryKind::Directory
723 | discover::EntryKind::Delta
724 | discover::EntryKind::Iceberg
725 | discover::EntryKind::Hudi => WhatEnter::GoesInside,
726 }
727 }
728}
729
730impl App {
731 fn fetched(download: Option<&crate::download::TempDownload>, path: Option<&Path>) -> bool {
735 download.is_some() && path.is_some_and(source::is_remote_url)
736 }
737}
738
739#[derive(Clone)]
741pub enum RunInput {
742 Cli(Box<Args>),
745 Host(Box<Args>, Option<Box<LazyFrame>>),
749 Paths(Vec<PathBuf>, OpenOptions),
750 LazyFrame(Box<LazyFrame>, OpenOptions),
751}
752
753#[derive(Debug, Default, PartialEq, Eq)]
754pub enum InputMode {
755 #[default]
756 Normal,
757 Home,
761 SortFilter,
762 PivotMelt,
763 Editing,
764 Export,
765 Copy,
767 Inspect,
769 GoToColumn,
771 PickFormat,
773 Retype,
776 Combine,
778 PickTable,
780 Info,
781 Chart,
782 ValueCounts,
784 Hex,
786 Sample,
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub enum InputType {
792 Query,
794 Find,
795}
796
797struct QueryRun {
801 origin: RunOrigin,
802 frame: u64,
805 rollback: crate::widgets::datatable::ViewRollback,
806 len_count_inflight: Option<u64>,
810 count_after_paint: Option<u64>,
811 len_count_failed: Option<u64>,
812 rows: Option<usize>,
814}
815
816enum RunOrigin {
818 Query(QueryMode),
821 View {
825 previous: Option<String>,
826 matched: Option<(String, view::MatchReason)>,
827 },
828}
829
830fn recording_label(spool: &crate::follow::Spool) -> (String, bool) {
835 let dot = crate::glyphs::get().middot;
836 let size = crate::discover::format_size(spool.bytes());
837 match spool.ended() {
838 None => (
839 format!(
840 "rec {size} {dot} {}/s",
841 crate::discover::format_size(spool.rate() as u64)
842 ),
843 false,
844 ),
845 Some(None) => {
846 let secs = spool.duration().as_secs();
847 let length = if secs >= 3600 {
848 format!("{}:{:02}:{:02}", secs / 3600, secs / 60 % 60, secs % 60)
849 } else {
850 format!("{}:{:02}", secs / 60, secs % 60)
851 };
852 match spool.tee().filter(|tee| !tee.to_stdout()) {
853 Some(tee) => (
854 format!("saved {size} {dot} {length} {dot} {}", tee.name()),
855 false,
856 ),
857 None => (format!("sent {size} {dot} {length}"), false),
858 }
859 }
860 Some(Some(reason)) => (format!("stopped: {reason}"), true),
861 }
862}
863
864#[derive(Debug, Clone, Copy, PartialEq, Eq)]
866enum Leaving {
867 Quit,
868 Home,
869}
870
871#[derive(Clone, Debug)]
874pub struct ExportProgress {
875 pub file_path: PathBuf,
876 pub current_phase: String,
877 pub written: Option<u64>,
878}
879
880#[allow(dead_code)]
882struct AnalysisComputationState {
883 df: Option<DataFrame>,
884 schema: Option<Arc<Schema>>,
885 partial_stats: Vec<crate::statistics::ColumnStatistics>,
886 current: usize,
887 total: usize,
888 total_rows: usize,
889 sample_seed: u64,
890 sample_size: Option<usize>,
891}
892
893fn active_query_settings(
896 dsl_query: &str,
897 sql_query: &str,
898 fuzzy_query: &str,
899) -> (Option<String>, Option<String>, Option<String>) {
900 let sql_trimmed = sql_query.trim();
901 let fuzzy_trimmed = fuzzy_query.trim();
902 let dsl_trimmed = dsl_query.trim();
903 if !sql_trimmed.is_empty() {
904 (None, Some(sql_trimmed.to_string()), None)
905 } else if !fuzzy_trimmed.is_empty() {
906 (None, None, Some(fuzzy_trimmed.to_string()))
907 } else if !dsl_trimmed.is_empty() {
908 (Some(dsl_trimmed.to_string()), None, None)
909 } else {
910 (None, None, None)
911 }
912}
913
914pub(crate) fn view_settings_of(state: &DataTableState) -> view::ViewSettings {
917 let (query, sql_query, fuzzy_query) = active_query_settings(
918 state.get_active_query(),
919 state.get_active_sql_query(),
920 state.get_active_fuzzy_query(),
921 );
922 view::ViewSettings {
923 chart: None,
924 sample: saved_sample_of(state),
925 query,
926 sql_query,
927 fuzzy_query,
928 filters: state.get_filters().to_vec(),
929 sort_columns: state.get_sort_columns().to_vec(),
930 sort_descending: state.get_sort_descending().to_vec(),
931 sort_ascending: state.get_sort_ascending(),
932 column_order: state.get_column_order().to_vec(),
933 locked_columns_count: state.locked_columns_count(),
934 pivot: state.last_pivot_spec().cloned(),
935 melt: state.last_melt_spec().cloned(),
936 reshape_source: state.reshape_source().cloned(),
937 columns: state.column_changes().to_vec(),
938 }
939}
940
941fn saved_sample_of(state: &DataTableState) -> Option<view::SavedSample> {
944 let sampled = state.sampled()?;
945 let through = sampled.through().then(|| view::ViewSettings {
948 sample: None,
949 chart: None,
950 ..view_settings_of(sampled.source())
951 });
952 Some(view::SavedSample::of(
953 sampled.sample(),
954 sampled.path(),
955 through,
956 ))
957}
958
959pub(crate) enum Replayed {
961 Planned,
963 Pivot(Box<crate::widgets::datatable::PivotJob>),
966}
967
968#[cfg(feature = "cloud")]
974struct CloudTarget<'a> {
975 full: &'a str,
977 key: String,
979 pattern: Option<&'a globset::GlobMatcher>,
982}
983
984const QUALITY_RUN_WAITS: &str = "Run waits: the cancelled run is still stopping";
987
988const ANALYSIS_READ_WAITS: &str = "A cancelled run is still finishing; try again shortly";
991
992const CANCEL_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
995
996pub struct App {
997 pub data_table_state: Option<DataTableState>,
998 footer_progress: Arc<crate::schema_union::FooterProgress>,
1002 footers_this_frame: Option<(usize, usize)>,
1010 listed_this_frame: Option<usize>,
1012 home_probes_inflight: Vec<PathBuf>,
1016 home_listing_cancels: HashMap<PathBuf, Arc<std::sync::atomic::AtomicBool>>,
1019 home_narrowing: Option<(PathBuf, String, Arc<std::sync::atomic::AtomicBool>)>,
1022 #[cfg(feature = "cloud")]
1025 cloud_discovery_started: bool,
1026 home_search_inflight: bool,
1029 home_search_generation: u64,
1033 pending_clear_recents: bool,
1035 pending_link: Option<String>,
1037 pub local_desktop: bool,
1040 pending_forget_place: Option<PathBuf>,
1042 last_load_error: Option<String>,
1045 home_schema_inflight: Vec<PathBuf>,
1047 home_generation: u64,
1049 pub home: home::HomeState,
1052 home_schema_cache: HashMap<PathBuf, Option<discover::SchemaPreview>>,
1055 pub home_previews: crate::home_preview::Previews,
1057 pub reads: crate::home_preview::ReadCounts,
1059 shape_remembered: Option<u64>,
1062 path: Option<PathBuf>,
1063 original_file_format: Option<ExportFormat>, original_file_delimiter: Option<u8>, stdin_reader: Option<Box<dyn std::io::Read + Send>>,
1067 stdout_pass: Option<Box<dyn std::io::Write + Send>>,
1069 follow_drawn: Option<crate::render::footer::FollowMark>,
1071 pending_leave: Option<Leaving>,
1073 recording_on: Option<Arc<crate::follow::SpoolHandle>>,
1075 recording_end_said: bool,
1077 events: Sender<AppEvent>,
1078 debug: DebugState,
1079 pub info_modal: InfoModal,
1080 file_facts: Option<(u64, FileFacts)>,
1085 pub codebook: Option<std::sync::Arc<codebook::Codebook>>,
1087 pub catalog_entry: Option<(String, std::sync::Arc<catalog::Dataset>)>,
1090 pub documentation: widgets::documentation::DocState,
1092 pub info_documentation: widgets::documentation::DocState,
1094 remembered_moved: bool,
1097 pub head_web_rows: bool,
1100 query_input: TextInput, sql_input: TextInput, pub find: find::Find,
1107 column_hints: bool,
1109 pub input_mode: InputMode,
1110 input_type: Option<InputType>,
1111 query_mode: QueryMode,
1112 query_mode_chosen: Option<QueryMode>,
1115 query_text_restored: bool,
1118 sql_columns: Vec<(String, DataType)>,
1121 sql_completion: Option<sql_assist::Cycle>,
1123 query_running: Option<QueryRun>,
1126 query_run_error: Option<String>,
1128 inline_failures: u64,
1131 pub sort_filter_modal: SortFilterModal,
1132 pub pivot_melt_modal: PivotMeltModal,
1133 pub view_modal: ViewModal,
1134 opened_from_home: bool,
1138 startup_view: Option<String>,
1141 pub analysis_modal: AnalysisModal,
1142 pub sample_form: Option<sample_modal::SampleForm>,
1145 memory_probe: table_sample::MemoryProbe,
1148 sample_paths: Vec<(String, table_sample::DrawPath)>,
1152 quality_cache: Vec<QualityCacheEntry>,
1154 quality_samples: Vec<KeptQualitySample>,
1156 quality_released: Vec<(u64, u64, sampling::Sample)>,
1158 quality_memory_budget: usize,
1160 quality_copies: Vec<RetainedCopy>,
1164 quality_copy_released: Option<u64>,
1166 quality_copy_unusable: Option<u64>,
1169 quality_copy_free: std::sync::Mutex<Option<(std::time::Instant, Option<u64>)>>,
1172 quality_evidence_return: Option<Box<DataTableState>>,
1175 pub(crate) quality_evidence_label: Option<String>,
1176 pub chart_modal: ChartModal,
1177 pub chart_export_modal: ChartExportModal,
1178 pub export_modal: ExportModal,
1179 pub copy_modal: copy_modal::CopyModal,
1180 pub inspector_modal: inspector_modal::InspectorModal,
1181 external_open: Option<external_open::ExternalOpen>,
1184 open_dir: Option<tempfile::TempDir>,
1186 pub go_to_column: crate::widgets::ui::PickerState,
1188 pub value_counts: value_counts_modal::ValueCountsModal,
1190 export_counts: Option<polars::prelude::DataFrame>,
1192 pub format_picker: crate::widgets::ui::PickerState,
1194 pub retype: Option<retype_modal::RetypeModal>,
1196 pub combine: Option<retype_modal::CombineModal>,
1198 pub(crate) retype_from_info: bool,
1200 pub table_picker: crate::widgets::ui::PickerState,
1202 pub table_choices: Option<table_switch::Tables>,
1203 pub hex: Option<hex_view::HexView>,
1205 hex_serial: u64,
1207 clipboard: Option<Box<dyn clipboard::Destination>>,
1211 pending_copy: Option<(clipboard::CopyFormat, bool)>,
1213 pub(crate) chart_cache: ChartCache,
1214 chart_inflight: Option<ChartInflight>,
1221 chart_asked: Option<(ChartRequest, Option<std::time::Instant>)>,
1224 pending_chart_result: ChartResultSlot,
1227 chart_export_waiting: Option<ChartExportRequest>,
1230 error_modal: ErrorModal,
1231 flash: Option<Flash>,
1232 pub confirmation_modal: ConfirmationModal,
1233 pending_export: Option<ExportRequest>,
1235 pending_delete_view: Option<String>,
1237 pending_hide_examples: bool,
1239 pending_chart_export: Option<ChartExportRequest>,
1240 pending_quality_export: Option<(PathBuf, crate::quality_export::ReportFormat)>,
1242 help: help::Help,
1244 pointer: pointer::Pointing,
1246 context_menu: Option<context_menu::ContextMenu>,
1248 cache: CacheManager,
1249 cache_writes: CacheWrites,
1251 view_manager: Views,
1252 active_view_id: Option<String>, export_progress: Option<ExportProgress>,
1255 theme: Theme, pending_read_all: bool,
1258 history_limit: usize, table_cell_padding: u16, column_colors: bool, dtype_row: bool,
1263 number_format: NumberFormatSettings,
1265 runtime: tokio::runtime::Handle, jobs: Jobs,
1269 loading: loading::Loader,
1273 opened: Option<(Vec<PathBuf>, OpenOptions)>,
1276 loaded_ahead_from: Option<(u64, usize, usize, usize)>,
1278 len_count_inflight: Option<u64>,
1281 count_after_paint: Option<u64>,
1286 #[cfg(test)]
1288 counts_spawned: std::cell::Cell<usize>,
1289 #[cfg(test)]
1292 first_rows_asked: usize,
1293 len_count_failed: Option<u64>,
1297 end_after_count: Option<u64>,
1300 pending_footers_result: std::sync::Arc<std::sync::Mutex<FootersReported>>,
1304 dataset_generation: u64,
1309 end_when_the_footers_land: Option<u64>,
1315 end_when_indexed: Option<u64>,
1318 indexing_stop: Arc<std::sync::atomic::AtomicBool>,
1320 indexing_lines: Option<Arc<crate::lines::Lines>>,
1322 indexing_paused: bool,
1324 goto_when_indexed: Option<(u64, usize)>,
1326 count_progress: Arc<crate::schema_union::FooterProgress>,
1328 exact_count_asked: Option<u64>,
1331 count_after_stop: Option<u64>,
1334 footers_held: Option<(u64, crate::widgets::datatable::FootersFound)>,
1339 followed_fields_held: Option<(u64, Vec<polars::prelude::Field>)>,
1342 reread_owed: Option<u64>,
1349 #[cfg(test)]
1352 home_worker_dies: Option<HomeWorkerDies>,
1353 #[cfg(test)]
1356 file_facts_reader: Option<FileFactsReader>,
1357 busy: bool,
1360 screen_generation: u64,
1364 input_dropped: bool,
1368 throbber_frame: u8, status_message: Option<String>,
1372 analysis_computation: Option<AnalysisComputationState>,
1373 app_config: AppConfig,
1374 background_query: bool,
1376 formats: Arc<crate::formats::Registry>,
1378}
1379
1380impl App {
1381 fn open_quality_evidence(&mut self) -> Option<AppEvent> {
1385 let (_, finding) = self.analysis_modal.selected_finding()?;
1386 let results = self.analysis_modal.data_quality_results.as_ref()?;
1387 let rows = finding.evidence(results).ok()?;
1388 let sampled = results.precision == data_quality::QualityPrecision::Sampled;
1389 let count = finding.evidence_count(results);
1390 let label = format!(
1391 "Data Quality / {} / {}",
1392 finding.title,
1393 quality_report::columns_label(&finding.columns, 40)
1394 );
1395 let what = format!(
1396 "{} {} {}",
1397 finding.title,
1398 glyphs::get().middot,
1399 quality_report::columns_label(&finding.columns, 40)
1400 );
1401 self.show_quality_rows(rows, label, sampled, what, count)
1402 }
1403
1404 fn open_interval_evidence(&mut self) -> Option<AppEvent> {
1407 let schema = self.data_table_state.as_ref().map(|state| state.schema());
1408 let (predicate, label, count) = self
1409 .analysis_modal
1410 .interval_evidence(schema.map(|schema| schema.as_ref()))?;
1411 let sampled = self
1412 .analysis_modal
1413 .data_quality_results
1414 .as_ref()
1415 .is_some_and(|results| results.precision == data_quality::QualityPrecision::Sampled);
1416 let what = label
1417 .trim_start_matches("Data Quality / ")
1418 .replace(" / ", &format!(" {} ", glyphs::get().middot));
1419 self.show_quality_rows(
1420 quality_report::EvidenceRows::Matching(predicate),
1421 label,
1422 sampled,
1423 what,
1424 Some(count),
1425 )
1426 }
1427
1428 pub(crate) fn quality_rows_kept(&self) -> Option<std::sync::Arc<data_quality::QualitySample>> {
1432 self.analysis_modal.data_quality_results.as_ref()?;
1433 let plan = self.analysis_modal.quality_result_plan();
1434 if plan.compute != data_quality::QualityCompute::Sample {
1435 return None;
1436 }
1437 self.kept_quality_sample(&plan.sample())
1438 }
1439
1440 fn show_quality_rows(
1444 &mut self,
1445 rows: quality_report::EvidenceRows,
1446 label: String,
1447 sampled: bool,
1448 what: String,
1449 count: Option<usize>,
1450 ) -> Option<AppEvent> {
1451 let plan = self.analysis_modal.quality_result_plan().clone();
1452 let by_files = matches!(rows, quality_report::EvidenceRows::Files(_));
1453 let label = if sampled {
1454 format!("{label} / sampled")
1455 } else {
1456 label
1457 };
1458 if !by_files && self.quality_rows_kept().is_some() {
1459 return self.read_sample_rows(plan.sample(), Some((rows, label)));
1460 }
1461 let state = self.data_table_state.as_ref()?;
1462 let g = glyphs::get();
1463 let rows_label = |rows: usize| {
1464 format!(
1465 "{} {}",
1466 numfmt::group_chrome(rows),
1467 if rows == 1 { "row" } else { "rows" }
1468 )
1469 };
1470 let scope = match &rows {
1471 quality_report::EvidenceRows::Files(files) => files.clone(),
1472 _ => plan.scope.clone(),
1473 };
1474 let not_kept = if plan.compute == data_quality::QualityCompute::Full {
1477 "a full scan keeps no rows"
1478 } else {
1479 "the rows read are no longer kept"
1480 };
1481 let (why, reads) = match &rows {
1482 quality_report::EvidenceRows::Files(data_quality::QualityScope::SourceFiles(files)) => {
1483 (
1484 "their rows are in the files, not the report".to_string(),
1485 format!(
1486 "the {} named {}",
1487 files.len(),
1488 if files.len() == 1 { "file" } else { "files" }
1489 ),
1490 )
1491 }
1492 _ if sampled => (
1493 "the sampled rows are no longer kept".to_string(),
1494 format!(
1495 "the sample again: {} {} {}",
1496 widgets::data_quality::compute_label(&plan),
1497 g.middot,
1498 widgets::data_quality::planned_read_label(state, &plan)
1499 ),
1500 ),
1501 quality_report::EvidenceRows::Duplicates => (
1502 not_kept.to_string(),
1503 format!(
1504 "every row of {}, once {} {}",
1505 plan.scope.label(),
1506 g.middot,
1507 widgets::data_quality::scope_read_label(state, &plan)
1508 ),
1509 ),
1510 _ => (
1513 not_kept.to_string(),
1514 format!(
1515 "every row of {} to count them, then the rows on screen {} {}",
1516 plan.scope.label(),
1517 g.middot,
1518 widgets::data_quality::scope_read_label(state, &plan)
1519 ),
1520 ),
1521 };
1522 let shows = match (&rows, count) {
1523 (quality_report::EvidenceRows::Duplicates, Some(count)) => {
1524 format!("{}, copies together", rows_label(count))
1525 }
1526 (_, Some(count)) => rows_label(count),
1527 (_, None) => "the rows that match".to_string(),
1528 };
1529 let source = if state.is_remote_source() {
1530 "remote, read only"
1531 } else {
1532 "local, read only"
1533 };
1534 self.analysis_modal.data_quality_evidence_read = Some(analysis_modal::EvidenceRead {
1535 summary: vec![
1536 ("Rows", what),
1537 ("Why", why),
1538 ("Reads", reads),
1539 ("Shows", shows),
1540 ("Source", source.to_string()),
1541 ],
1542 sample: (sampled && !by_files).then(|| plan.sample()),
1543 scope,
1544 rows,
1545 label,
1546 });
1547 None
1548 }
1549
1550 fn confirm_evidence_read(&mut self) -> Option<AppEvent> {
1552 let staged = self.analysis_modal.data_quality_evidence_read.as_ref()?;
1554 let kept = staged
1555 .sample
1556 .as_ref()
1557 .is_some_and(|sample| self.kept_quality_sample(sample).is_some());
1558 if !kept && self.read_waits_for_cancelled() {
1559 return None;
1560 }
1561 let read = self.analysis_modal.data_quality_evidence_read.take()?;
1562 if let Some(sample) = read.sample {
1563 return self.read_sample_rows(sample, Some((read.rows, read.label)));
1564 }
1565 let predicate = match read.rows {
1566 quality_report::EvidenceRows::Matching(predicate) => predicate,
1567 quality_report::EvidenceRows::Files(_) => polars::prelude::lit(true),
1570 quality_report::EvidenceRows::Duplicates => {
1571 return self.read_duplicate_rows(read.scope, read.label);
1572 }
1573 };
1574 self.open_quality_scope_rows(&read.scope, predicate, read.label)
1575 }
1576
1577 fn read_duplicate_rows(
1580 &mut self,
1581 scope: data_quality::QualityScope,
1582 label: String,
1583 ) -> Option<AppEvent> {
1584 let state = self.data_table_state.as_ref()?;
1585 let (lf, schema) = match state.quality_scope_frame(&scope) {
1586 Ok(frame) => frame,
1587 Err(error) => {
1588 self.error_modal
1589 .show(format!("Cannot open matching rows: {error}"));
1590 return None;
1591 }
1592 };
1593 let keys = schema.iter_names().cloned().collect::<Vec<_>>();
1594 let columns = schema
1597 .iter()
1598 .map(|(name, dtype)| {
1599 if matches!(dtype, polars::prelude::DataType::Binary) {
1600 polars::prelude::lit(widgets::datatable::binary_stub()).alias(name.clone())
1601 } else {
1602 polars::prelude::col(name.clone())
1603 }
1604 })
1605 .collect::<Vec<_>>();
1606 let streaming = self.app_config.performance.streaming;
1607 self.analysis_modal.computing = Some(AnalysisProgress::new("Reading the rows that repeat"));
1608 self.spawn_job(
1609 Job::SampleRows,
1610 Some("Reading the rows that repeat..."),
1611 move |_| {
1612 let df = data_quality::duplicate_rows(lf.select(columns), &keys, streaming)
1613 .map_err(|error| format!("{error}"))?;
1614 Ok(Answer::Sample { df, label })
1615 },
1616 );
1617 None
1618 }
1619
1620 fn open_quality_scope_rows(
1623 &mut self,
1624 scope: &data_quality::QualityScope,
1625 predicate: polars::prelude::Expr,
1626 label: String,
1627 ) -> Option<AppEvent> {
1628 let state = self.data_table_state.as_ref()?;
1629 let view = match state.quality_evidence_view(scope, predicate) {
1630 Ok(view) => view,
1631 Err(error) => {
1632 self.error_modal
1633 .show(format!("Cannot open matching rows: {error}"));
1634 return None;
1635 }
1636 };
1637 if let Some(original) = self.data_table_state.replace(view) {
1638 self.quality_evidence_return = Some(Box::new(original));
1639 self.quality_evidence_label = Some(label);
1640 self.analysis_modal.active = false;
1641 self.forget_the_rows_read();
1642 self.spawn_async_collect("Loading matching rows...");
1643 }
1644 None
1645 }
1646
1647 fn return_from_quality_evidence(&mut self, reopen_analysis: bool) -> bool {
1648 let Some(original) = self.quality_evidence_return.take() else {
1649 return false;
1650 };
1651 self.jobs.advance();
1652 self.len_count_inflight = None;
1653 self.data_table_state = Some(*original);
1654 self.quality_evidence_label = None;
1655 self.analysis_modal.active = reopen_analysis;
1656 self.busy = false;
1657 self.status_message = None;
1658 true
1659 }
1660
1661 fn restore_recent_quality_plan(&mut self) {
1662 let Some(view_generation) = self
1663 .data_table_state
1664 .as_ref()
1665 .map(DataTableState::len_generation)
1666 else {
1667 return;
1668 };
1669 if self.analysis_modal.data_quality_plan != data_quality::DataQualityPlan::default() {
1670 return;
1671 }
1672 if let Some(cached) = self.quality_cache.iter().find(|entry| {
1673 entry.dataset_generation == self.dataset_generation
1674 && entry.view_generation == view_generation
1675 }) {
1676 self.analysis_modal.data_quality_plan = cached.plan.clone();
1677 }
1678 }
1679
1680 fn quality_plan_context(&self) -> analysis_modal::PlanContext {
1683 let Some(state) = self.data_table_state.as_ref() else {
1684 return analysis_modal::PlanContext::default();
1685 };
1686 let plan = &self.analysis_modal.data_quality_plan;
1687 let scope = &plan.scope;
1688 let schema = state.schema();
1689 let mut partitions = state.partition_columns().unwrap_or_default().to_vec();
1690 if partitions.is_empty()
1693 && let Some(dir) = self.path.as_ref().filter(|path| path.is_dir())
1694 {
1695 partitions = DataTableState::discover_hive_partition_columns(dir)
1696 .into_iter()
1697 .filter(|column| schema.get(column).is_some())
1698 .collect();
1699 }
1700 let mut time_columns: Vec<(String, bool)> = state
1702 .quality_temporal_columns(scope)
1703 .into_iter()
1704 .map(|column| {
1705 let has_time =
1706 !matches!(schema.get(&column), Some(polars::prelude::DataType::Date));
1707 (column, has_time)
1708 })
1709 .collect();
1710 for format in &plan.time_formats {
1711 if !time_columns
1712 .iter()
1713 .any(|(column, _)| *column == format.column)
1714 {
1715 time_columns.push((
1716 format.column.clone(),
1717 format.kind == data_quality::TimeKind::Datetime,
1718 ));
1719 }
1720 }
1721 analysis_modal::PlanContext {
1722 partitions,
1723 time_columns,
1724 files: state.quality_source_file_count() > 1,
1725 text_columns: state
1726 .quality_text_columns(scope)
1727 .into_iter()
1728 .map(|column| {
1729 let examples = state.buffered_values(&column, 3);
1730 (column, examples)
1731 })
1732 .collect(),
1733 }
1734 }
1735
1736 pub(crate) fn quality_time_candidates(&self) -> Vec<String> {
1739 let Some(state) = self.data_table_state.as_ref() else {
1740 return Vec::new();
1741 };
1742 let scope = &self.analysis_modal.data_quality_plan.scope;
1743 let mut columns = state.quality_temporal_columns(scope);
1744 columns.extend(state.quality_text_columns(scope));
1745 columns
1746 }
1747
1748 pub(crate) fn quality_intent_columns(&self) -> Vec<(String, polars::prelude::DataType)> {
1750 self.data_table_state
1751 .as_ref()
1752 .map(|state| {
1753 crate::widgets::quality_intent::intent_columns(
1754 state.quality_schema(&self.analysis_modal.data_quality_plan.scope),
1755 )
1756 })
1757 .unwrap_or_default()
1758 }
1759
1760 fn open_intent_form(&mut self) {
1762 let columns = self.quality_intent_columns();
1763 let modal = &mut self.analysis_modal;
1764 let Some((column, dtype)) = columns.get(modal.data_quality_plan_field) else {
1765 return;
1766 };
1767 let plan = &modal.data_quality_plan;
1768 modal.data_quality_intent_form = Some(intent_modal::IntentForm::new(
1769 column,
1770 dtype.clone(),
1771 plan.time_format(column).cloned(),
1772 &plan.intent,
1773 &self.theme,
1774 ));
1775 }
1776
1777 fn intent_form_key(&mut self, event: &KeyEvent) {
1781 let modal = &mut self.analysis_modal;
1782 let Some(form) = modal.data_quality_intent_form.as_mut() else {
1783 return;
1784 };
1785 match form::key(form, event) {
1786 FormKey::Cancel => modal.data_quality_intent_form = None,
1787 FormKey::Submit => match form.apply(&mut modal.data_quality_plan.intent) {
1788 Ok(()) => modal.data_quality_intent_form = None,
1789 Err(error) => form.error = Some(error),
1790 },
1791 FormKey::Act(_) => form.adjust(true),
1792 FormKey::Step(_, delta) => form.adjust(delta > 0),
1793 FormKey::Text(_) => {
1794 if let Some(input) = form.input_mut() {
1795 let _ = input.handle_key(event, None);
1796 }
1797 form.error = None;
1798 }
1799 FormKey::Moved | FormKey::Other => {}
1800 }
1801 }
1802
1803 fn quality_source_identity(
1807 &self,
1808 state: &DataTableState,
1809 scope: &data_quality::QualityScope,
1810 ) -> crate::quality_export::SourceIdentity {
1811 let format = self
1812 .original_file_format
1813 .map(|format| format.as_str().to_string())
1814 .or_else(|| {
1815 self.path
1816 .as_ref()
1817 .and_then(|path| path.extension())
1818 .and_then(|extension| extension.to_str())
1819 .map(str::to_string)
1820 });
1821 let mut view = Vec::new();
1822 if !scope.uses_source() {
1823 if !state.get_active_query().is_empty() {
1824 view.push(format!("query: {}", state.get_active_query()));
1825 }
1826 if !state.get_active_sql_query().is_empty() {
1827 view.push(format!("SQL: {}", state.get_active_sql_query()));
1828 }
1829 if !state.get_active_fuzzy_query().is_empty() {
1830 view.push(format!("text: {}", state.get_active_fuzzy_query()));
1831 }
1832 for (index, filter) in state.view_filters().iter().enumerate() {
1833 let join = if index == 0 {
1834 String::new()
1835 } else {
1836 format!("{} ", filter.logical_op.as_str())
1837 };
1838 view.push(format!(
1839 "filter: {join}{} {} {}",
1840 filter.column,
1841 filter.operator.as_str(),
1842 filter.value
1843 ));
1844 }
1845 if state.reshape_source().is_some() {
1846 view.push("reshaped: pivot or melt".to_string());
1847 }
1848 }
1849 let remote = state.is_remote_source();
1850 let piped = self.reads_stdin();
1853 let location = self.path.as_ref().map(|path| {
1854 match std::path::absolute(path).ok().filter(|_| !remote && !piped) {
1855 Some(path) => path.display().to_string(),
1856 None => path.display().to_string(),
1857 }
1858 });
1859 crate::quality_export::SourceIdentity {
1860 location,
1861 remote,
1862 format,
1863 view,
1864 ..crate::quality_export::SourceIdentity::default()
1865 }
1866 .with_files(state.quality_source_file_names())
1867 }
1868
1869 fn open_quality_export(&mut self) {
1871 let stem = self
1872 .path
1873 .as_ref()
1874 .and_then(|path| path.file_stem())
1875 .and_then(|stem| stem.to_str())
1876 .filter(|stem| !stem.is_empty())
1877 .unwrap_or("data")
1878 .to_string();
1879 self.analysis_modal.data_quality_export =
1880 Some(crate::quality_export::ExportForm::new(&stem, &self.theme));
1881 }
1882
1883 fn quality_export_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
1887 let form = self.analysis_modal.data_quality_export.as_mut()?;
1888 match event.code {
1889 KeyCode::Esc => self.analysis_modal.data_quality_export = None,
1890 KeyCode::Tab | KeyCode::BackTab | KeyCode::Up | KeyCode::Down => form.toggle_focus(),
1891 KeyCode::Left
1892 | KeyCode::Right
1893 | KeyCode::Char(' ')
1894 | KeyCode::Char('h')
1895 | KeyCode::Char('l')
1896 if form.on_format =>
1897 {
1898 form.cycle_format();
1899 }
1900 KeyCode::Enter => match form.target() {
1901 Err(error) => form.error = Some(error),
1902 Ok((path, format)) => {
1903 if path.exists() {
1904 let shown = path.display().to_string();
1905 self.pending_quality_export = Some((path, format));
1906 self.confirmation_modal.show_destructive(
1907 format!("File already exists:\n{shown}\n\nOverwrite it?"),
1908 "Overwrite",
1909 );
1910 } else {
1911 return Some(AppEvent::QualityReportExport(
1914 path,
1915 format,
1916 Overwrite::Forbid,
1917 ));
1918 }
1919 }
1920 },
1921 _ if !form.on_format => {
1922 let _ = form.path.handle_key(event, None);
1923 form.error = None;
1924 }
1925 _ => {}
1926 }
1927 None
1928 }
1929
1930 fn open_setup_row(&mut self) -> Option<AppEvent> {
1932 use analysis_modal::SetupRow;
1933 self.analysis_modal.data_quality_setup_note = None;
1934 match self.analysis_modal.setup_row() {
1935 SetupRow::Sample => self.open_quality_sample_form(),
1936 SetupRow::TimeRoles => {
1937 if !self.quality_time_candidates().is_empty() {
1939 self.analysis_modal.data_quality_plan_before_edit =
1940 Some(self.analysis_modal.data_quality_plan.clone());
1941 self.analysis_modal
1942 .set_quality_page(data_quality::QualityPage::TimeRoles);
1943 self.analysis_modal.data_quality_plan_field = 0;
1944 }
1945 }
1946 SetupRow::Intent => {
1947 if !self.quality_intent_columns().is_empty() {
1948 self.analysis_modal.data_quality_plan_before_edit =
1949 Some(self.analysis_modal.data_quality_plan.clone());
1950 self.analysis_modal
1951 .set_quality_page(data_quality::QualityPage::Intent);
1952 self.analysis_modal.data_quality_plan_field = 0;
1953 }
1954 }
1955 SetupRow::Intervals => {
1956 if !self
1958 .analysis_modal
1959 .data_quality_plan
1960 .candidate_pairs()
1961 .is_empty()
1962 {
1963 self.analysis_modal.data_quality_plan_before_edit =
1964 Some(self.analysis_modal.data_quality_plan.clone());
1965 self.analysis_modal
1966 .set_quality_page(data_quality::QualityPage::IntervalPairs);
1967 self.analysis_modal.data_quality_plan_field = 0;
1968 }
1969 }
1970 SetupRow::Expected => {
1971 if matches!(
1974 self.analysis_modal.data_quality_plan.grain,
1975 data_quality::QualityGrain::TimeWindows { .. }
1976 ) {
1977 self.analysis_modal.data_quality_expected_form =
1978 Some(analysis_modal::ExpectedForm::new(
1979 &self.analysis_modal.data_quality_plan,
1980 &self.theme,
1981 ));
1982 self.analysis_modal
1983 .set_quality_page(data_quality::QualityPage::ExpectedWindows);
1984 }
1985 }
1986 row => {
1987 let context = self.quality_plan_context();
1988 self.analysis_modal.open_plan_picker(row, &context);
1989 }
1990 }
1991 None
1992 }
1993
1994 fn expected_form_key(&mut self, event: &KeyEvent) {
1998 let every = match &self.analysis_modal.data_quality_plan.grain {
1999 data_quality::QualityGrain::TimeWindows { every, .. } => every.clone(),
2000 _ => String::new(),
2001 };
2002 let Some(form) = self.analysis_modal.data_quality_expected_form.as_mut() else {
2003 return;
2004 };
2005 match form::key(form, event) {
2006 FormKey::Cancel => {}
2007 FormKey::Submit => match form.expected() {
2008 Ok(expected) => self.analysis_modal.data_quality_plan.expected = expected,
2009 Err(problem) => {
2010 form.error = Some(problem);
2011 return;
2012 }
2013 },
2014 FormKey::Step(_, delta) => {
2015 form.cycle(&every, delta > 0);
2016 return;
2017 }
2018 FormKey::Text(_) => {
2019 if let Some(input) = form.input_mut() {
2020 let _ = input.handle_key(event, None);
2021 form.error = None;
2022 }
2023 return;
2024 }
2025 FormKey::Act(_) | FormKey::Moved | FormKey::Other => return,
2026 }
2027 self.analysis_modal.data_quality_expected_form = None;
2028 self.analysis_modal.data_quality_setup_note = None;
2029 self.analysis_modal
2030 .set_quality_page(data_quality::QualityPage::Setup);
2031 self.analysis_modal.data_quality_plan_field = analysis_modal::SetupRow::Expected.index();
2032 }
2033
2034 fn stage_coarser_grain(&mut self) {
2038 let Some(coarser) = self.analysis_modal.quality_result_plan().coarser_grain() else {
2039 return;
2040 };
2041 self.open_quality_setup();
2042 let plan = &mut self.analysis_modal.data_quality_plan;
2043 plan.grain = coarser;
2044 plan.baseline_segment = None;
2045 self.analysis_modal.data_quality_plan_field = analysis_modal::SetupRow::Grain.index();
2046 }
2047
2048 fn choose_setup_picker(&mut self) {
2051 self.analysis_modal.data_quality_setup_note = None;
2052 if let Some(column) = self.analysis_modal.choose_plan_picker() {
2053 let examples = self
2054 .data_table_state
2055 .as_ref()
2056 .map(|state| state.buffered_values(&column, 3))
2057 .unwrap_or_default();
2058 self.analysis_modal.open_format_picker(&column, &examples);
2059 }
2060 }
2061
2062 pub(crate) fn quality_page_setup(&self) -> Option<data_quality::QualitySetup> {
2065 let modal = &self.analysis_modal;
2066 data_quality::page_setup(
2067 modal.data_quality_page,
2068 modal.quality_result_plan(),
2069 modal.data_quality_results.as_ref(),
2070 self.has_quality_time_columns(),
2071 )
2072 }
2073
2074 pub(crate) fn has_quality_time_columns(&self) -> bool {
2077 !self
2078 .analysis_modal
2079 .data_quality_plan
2080 .time_formats
2081 .is_empty()
2082 || self.data_table_state.as_ref().is_some_and(|state| {
2083 !state
2084 .quality_temporal_columns(&self.analysis_modal.data_quality_plan.scope)
2085 .is_empty()
2086 })
2087 }
2088
2089 pub(crate) fn quality_kept_serves(&self, plan: &data_quality::DataQualityPlan) -> bool {
2093 plan.compute == data_quality::QualityCompute::Sample
2094 && self.kept_quality_sample(&plan.sample()).is_some()
2095 }
2096
2097 pub(crate) fn quality_segment_count(
2100 &self,
2101 plan: &data_quality::DataQualityPlan,
2102 ) -> data_quality::SegmentCount {
2103 if plan.compute != data_quality::QualityCompute::Sample {
2104 return data_quality::SegmentCount::NotNeeded;
2105 }
2106 match self.kept_quality_sample(&plan.sample()) {
2107 Some(kept) => kept.segment_count(plan),
2108 None => data_quality::fresh_segment_count(plan, self.quality_may_read_blocks(plan)),
2109 }
2110 }
2111
2112 pub(crate) fn quality_released(&self, plan: &data_quality::DataQualityPlan) -> bool {
2115 let Some(view_generation) = self
2116 .data_table_state
2117 .as_ref()
2118 .map(DataTableState::len_generation)
2119 else {
2120 return false;
2121 };
2122 let sample = plan.sample();
2123 plan.compute == data_quality::QualityCompute::Sample
2124 && self
2125 .quality_released
2126 .iter()
2127 .any(|(dataset, view, released)| {
2128 *dataset == self.dataset_generation
2129 && *view == view_generation
2130 && *released == sample
2131 })
2132 }
2133
2134 fn quality_one_columnar_file(&self) -> bool {
2137 let Some(state) = self.data_table_state.as_ref() else {
2138 return false;
2139 };
2140 let columnar = matches!(
2141 self.original_file_format,
2142 Some(ExportFormat::Parquet | ExportFormat::Ipc)
2143 ) || self.path.as_ref().is_some_and(|path| {
2144 path.extension()
2145 .and_then(|extension| extension.to_str())
2146 .is_some_and(|extension| {
2147 matches!(
2148 extension.to_ascii_lowercase().as_str(),
2149 "parquet" | "pq" | "arrow" | "arrows" | "ipc" | "feather"
2150 )
2151 })
2152 });
2153 columnar && state.loaded_file_count() == 1
2154 }
2155
2156 pub(crate) fn quality_reads_blocks(&self, plan: &data_quality::DataQualityPlan) -> bool {
2163 let Some(state) = self.data_table_state.as_ref() else {
2164 return false;
2165 };
2166 self.quality_one_columnar_file()
2167 && match plan.scope {
2168 data_quality::QualityScope::WholeSource => true,
2169 data_quality::QualityScope::CurrentView => !state.changes_rows(),
2170 data_quality::QualityScope::FirstRows(_)
2172 | data_quality::QualityScope::ViewRows { .. } => {
2173 state.source_file_count() == Some(1)
2174 }
2175 _ => false,
2176 }
2177 }
2178
2179 pub(crate) fn quality_may_read_blocks(&self, plan: &data_quality::DataQualityPlan) -> bool {
2187 let Some(state) = self.data_table_state.as_ref() else {
2188 return false;
2189 };
2190 self.quality_reads_blocks(plan)
2191 || (self.quality_one_columnar_file()
2192 && state.may_keep_scan_rows()
2193 && matches!(
2194 plan.scope,
2195 data_quality::QualityScope::CurrentView
2196 | data_quality::QualityScope::FirstRows(_)
2197 | data_quality::QualityScope::ViewRows { .. }
2198 ))
2199 }
2200
2201 pub(crate) fn quality_cached(&self, plan: &data_quality::DataQualityPlan) -> bool {
2204 let Some(view_generation) = self
2205 .data_table_state
2206 .as_ref()
2207 .map(DataTableState::len_generation)
2208 else {
2209 return false;
2210 };
2211 self.quality_cache.iter().any(|entry| {
2212 entry.dataset_generation == self.dataset_generation
2213 && entry.view_generation == view_generation
2214 && entry.plan.same_measurement(plan)
2215 })
2216 }
2217
2218 pub(crate) fn quality_kept_rows(&self) -> Option<widgets::data_quality::KeptRows> {
2222 let kept = self
2223 .quality_samples
2224 .iter()
2225 .filter(|kept| kept.dataset_generation == self.dataset_generation)
2226 .collect::<Vec<_>>();
2227 let copy_bytes = self
2228 .quality_copies
2229 .iter()
2230 .filter(|kept| kept.dataset_generation == self.dataset_generation)
2231 .map(|kept| kept.copy.bytes())
2232 .sum::<u64>();
2233 (!kept.is_empty() || copy_bytes > 0).then(|| widgets::data_quality::KeptRows {
2234 samples: kept.len(),
2235 rows: kept.iter().map(|kept| kept.rows.df().height()).sum(),
2236 bytes: kept.iter().map(|kept| kept.rows.estimated_bytes()).sum(),
2237 copy_bytes,
2238 })
2239 }
2240
2241 fn release_quality_rows(&mut self) {
2246 let Some(kept) = self.quality_kept_rows() else {
2247 self.flash_note("Nothing kept to release".to_string());
2248 return;
2249 };
2250 for released in std::mem::take(&mut self.quality_samples) {
2251 self.quality_released.retain(|(dataset, view, sample)| {
2252 !(*dataset == released.dataset_generation
2253 && *view == released.view_generation
2254 && *sample == released.sample)
2255 });
2256 self.quality_released.insert(
2257 0,
2258 (
2259 released.dataset_generation,
2260 released.view_generation,
2261 released.sample,
2262 ),
2263 );
2264 }
2265 self.quality_released.truncate(QUALITY_RELEASED_REMEMBERED);
2266 let generation = self.dataset_generation;
2268 self.quality_copies
2269 .retain(|kept| kept.dataset_generation != generation);
2270 if kept.copy_bytes > 0 {
2271 self.quality_copy_released = Some(generation);
2272 }
2273 let rows = format!(
2274 "{} kept {} ({})",
2275 numfmt::group_chrome(kept.rows),
2276 if kept.rows == 1 { "row" } else { "rows" },
2277 widgets::info::format_bytes(kept.bytes as u64)
2278 );
2279 let copy = format!(
2280 "the local copy ({})",
2281 widgets::info::format_bytes(kept.copy_bytes)
2282 );
2283 self.flash_note(match (kept.samples > 0, kept.copy_bytes > 0) {
2284 (true, true) => format!("Released {rows} and {copy}; the next run reads again"),
2285 (false, true) => format!("Released {copy}; the next full scan fetches again"),
2286 _ => format!("Released {rows}; the next run reads again"),
2287 });
2288 }
2289
2290 fn kept_quality_sample(
2293 &self,
2294 sample: &sampling::Sample,
2295 ) -> Option<std::sync::Arc<data_quality::QualitySample>> {
2296 self.kept_quality_entry(sample)
2297 .map(|kept| kept.rows.clone())
2298 }
2299
2300 fn kept_quality_entry(&self, sample: &sampling::Sample) -> Option<&KeptQualitySample> {
2301 let view_generation = self.data_table_state.as_ref()?.len_generation();
2302 self.quality_samples.iter().find(|kept| {
2303 kept.dataset_generation == self.dataset_generation
2304 && kept.view_generation == view_generation
2305 && &kept.sample == sample
2306 })
2307 }
2308
2309 fn retain_quality_sample(&mut self, kept: &KeptQualitySample) {
2312 if kept.dataset_generation != self.dataset_generation {
2313 return;
2314 }
2315 self.quality_samples.retain(|entry| !entry.same_rows(kept));
2316 self.quality_released.retain(|(dataset, view, sample)| {
2317 !(*dataset == kept.dataset_generation
2318 && *view == kept.view_generation
2319 && *sample == kept.sample)
2320 });
2321 self.quality_samples.insert(0, kept.clone());
2322 self.trim_quality_memory();
2323 }
2324
2325 fn trim_quality_memory(&mut self) {
2333 loop {
2334 let used = self
2335 .quality_cache
2336 .iter()
2337 .map(|entry| entry.bytes)
2338 .sum::<usize>()
2339 + self
2340 .quality_samples
2341 .iter()
2342 .map(|kept| kept.rows.estimated_bytes())
2343 .sum::<usize>();
2344 if used <= self.quality_memory_budget {
2345 return;
2346 }
2347 let remakeable = self
2348 .quality_cache
2349 .iter()
2350 .enumerate()
2351 .skip(1)
2352 .rev()
2353 .find(|(_, entry)| {
2354 entry.plan.compute == data_quality::QualityCompute::Sample
2355 && self.quality_samples.iter().any(|kept| {
2356 kept.dataset_generation == entry.dataset_generation
2357 && kept.view_generation == entry.view_generation
2358 && kept.sample == entry.plan.sample()
2359 })
2360 })
2361 .map(|(index, _)| index);
2362 if let Some(index) = remakeable {
2363 self.quality_cache.remove(index);
2364 } else if self.quality_samples.len() > 1 {
2365 if let Some(released) = self.quality_samples.pop() {
2366 self.quality_released.insert(
2367 0,
2368 (
2369 released.dataset_generation,
2370 released.view_generation,
2371 released.sample,
2372 ),
2373 );
2374 self.quality_released.truncate(QUALITY_RELEASED_REMEMBERED);
2375 }
2376 } else if self.quality_cache.len() > 1 {
2377 self.quality_cache.pop();
2378 } else {
2379 return;
2380 }
2381 }
2382 }
2383
2384 fn restore_cached_quality(&mut self) -> bool {
2385 let Some(view_generation) = self
2386 .data_table_state
2387 .as_ref()
2388 .map(DataTableState::len_generation)
2389 else {
2390 return false;
2391 };
2392 let plan = self.analysis_modal.data_quality_plan.clone();
2393 let Some(cached) = self.quality_cache.iter().find(|entry| {
2394 entry.dataset_generation == self.dataset_generation
2395 && entry.view_generation == view_generation
2396 && entry.plan.same_measurement(&plan)
2397 }) else {
2398 return false;
2399 };
2400 let mut results = cached.results.clone();
2401 if cached.plan != plan {
2402 if cached.plan.compares_differently(&plan) {
2404 results.compare_segments(&plan);
2405 }
2406 self.cache_quality_result(&results, plan.clone());
2407 }
2408 self.analysis_modal.data_quality_results = Some(results);
2409 self.analysis_modal.data_quality_last_plan = Some(plan);
2410 self.analysis_modal.data_quality_from_cache = true;
2411 self.analysis_modal
2412 .set_quality_page(data_quality::QualityPage::Overview);
2413 true
2414 }
2415
2416 fn cache_quality_result(
2417 &mut self,
2418 results: &data_quality::DataQualityResults,
2419 plan: data_quality::DataQualityPlan,
2420 ) {
2421 let Some(view_generation) = self
2422 .data_table_state
2423 .as_ref()
2424 .map(DataTableState::len_generation)
2425 else {
2426 return;
2427 };
2428 self.quality_cache.retain(|entry| {
2431 !(entry.dataset_generation == self.dataset_generation
2432 && entry.view_generation == view_generation
2433 && entry.plan.same_measurement(&plan))
2434 });
2435 self.quality_cache.insert(
2436 0,
2437 QualityCacheEntry {
2438 dataset_generation: self.dataset_generation,
2439 view_generation,
2440 plan,
2441 bytes: results.estimated_bytes(),
2442 results: results.clone(),
2443 },
2444 );
2445 self.trim_quality_memory();
2446 }
2447
2448 fn quality_scope_on_copy(
2454 lf: LazyFrame,
2455 job: QualityCopyJob,
2456 watch: &data_quality::QualityWatch,
2457 fetch: impl FnOnce(
2458 &[crate::local_copy::RemoteObject],
2459 &Path,
2460 ) -> Result<crate::local_copy::LocalCopy>,
2461 kept: impl FnOnce(Option<Arc<crate::local_copy::LocalCopy>>),
2462 ) -> Result<(LazyFrame, Option<Arc<crate::local_copy::LocalCopy>>)> {
2463 let (copy, fetched) = match job {
2464 QualityCopyJob::Source => return Ok((lf, None)),
2465 QualityCopyJob::Kept(copy) => (copy, false),
2466 QualityCopyJob::Fetch { objects, root } => {
2467 watch.stage(data_quality::QualityStage::CopyingSource, true, true)?;
2468 let copy = fetch(&objects, &root).map_err(|error| {
2469 if watch.cancelled() {
2470 color_eyre::eyre::eyre!(crate::sampling::CANCELLED)
2471 } else {
2472 error
2473 }
2474 })?;
2475 (Arc::new(copy), true)
2476 }
2477 };
2478 let local = copy.redirect(&lf).filter(|local| {
2480 let schemas = (local.clone().collect_schema(), lf.clone().collect_schema());
2481 matches!(schemas, (Ok(local), Ok(source)) if local == source)
2482 });
2483 let Some(local) = local else {
2484 log::warn!(target: "datui", "local copy does not read as the source; reading the source");
2485 kept(None);
2486 return Ok((lf, None));
2487 };
2488 if fetched {
2489 kept(Some(copy.clone()));
2490 }
2491 watch.use_copy(data_quality::CopyRead {
2492 bytes: copy.bytes(),
2493 objects: copy.objects(),
2494 fetched,
2495 });
2496 Ok((local, Some(copy)))
2497 }
2498
2499 #[cfg(feature = "cloud")]
2502 fn fetch_quality_copy(
2503 objects: &[crate::local_copy::RemoteObject],
2504 root: &Path,
2505 cloud: &crate::config::CloudConfig,
2506 runtime: &tokio::runtime::Handle,
2507 stop: &crate::sampling::ReadWatch,
2508 ) -> Result<crate::local_copy::LocalCopy> {
2509 use crate::download::StreamError;
2510 use object_store::ObjectStoreExt;
2511
2512 crate::local_copy::LocalCopy::fetch(root, objects, stop, |object, write| {
2513 let url = object.url.as_str();
2514 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
2515 let (_, key) = Self::cloud_bucket_and_key(url)?;
2516 let path = crate::cloud_browse::object_path(&key);
2517 let listed = object.etag.clone();
2518 let open = async move {
2519 let got = store.get(&path).await.map_err(|e| e.to_string())?;
2520 if let (Some(listed), Some(fetched)) = (&listed, &got.meta.e_tag)
2523 && !crate::local_copy::same_etag(listed, fetched)
2524 {
2525 return Err("it changed since it opened. Open the dataset again".to_string());
2526 }
2527 Ok((got.into_stream(), None))
2528 };
2529 let watch = stop.clone();
2530 crate::download::stream_into(runtime, open, move || watch.stopped(), write)
2531 .map(drop)
2532 .map_err(|error| match error {
2533 StreamError::Write(report) => report,
2534 StreamError::Open(e) | StreamError::Read(e) => {
2535 color_eyre::eyre::eyre!("Could not copy {url}: {e}")
2536 }
2537 StreamError::Short { expected, got } => color_eyre::eyre::eyre!(
2538 "Could not copy {url}: it ended after {got} of {expected} bytes"
2539 ),
2540 StreamError::Cut => color_eyre::eyre::eyre!(crate::sampling::CANCELLED),
2541 })
2542 })
2543 }
2544
2545 fn quality_copies_root(&self) -> PathBuf {
2547 self.cache.cache_dir().join(crate::local_copy::COPIES_DIR)
2548 }
2549
2550 fn quality_copy_limit(&self) -> u64 {
2552 self.app_config.analysis.quality_local_copy.bytes()
2553 }
2554
2555 pub fn quality_copy_bytes(&self) -> u64 {
2557 self.quality_copies
2558 .iter()
2559 .map(|kept| kept.copy.bytes())
2560 .sum()
2561 }
2562
2563 fn quality_copy_kept(&self) -> Option<&Arc<crate::local_copy::LocalCopy>> {
2565 let state = self.data_table_state.as_ref()?;
2566 self.quality_copies
2567 .iter()
2568 .find(|kept| {
2569 kept.dataset_generation == self.dataset_generation
2570 && state.each_remote_object().is_some_and(|mut objects| {
2571 objects.all(|object| {
2572 object.is_some_and(|object| kept.copy.covers(&object.url))
2573 })
2574 })
2575 })
2576 .map(|kept| &kept.copy)
2577 }
2578
2579 fn quality_copy_free_space(&self) -> Option<u64> {
2581 let root = self.quality_copies_root();
2582 let Ok(mut cached) = self.quality_copy_free.lock() else {
2583 return crate::local_copy::free_space(&root);
2584 };
2585 match *cached {
2586 Some((asked, free)) if asked.elapsed() < std::time::Duration::from_secs(5) => free,
2587 _ => {
2588 let free = crate::local_copy::free_space(&root);
2589 *cached = Some((std::time::Instant::now(), free));
2590 free
2591 }
2592 }
2593 }
2594
2595 pub(crate) fn quality_copy_plan(
2598 &self,
2599 plan: &data_quality::DataQualityPlan,
2600 ) -> data_quality::CopyPlan {
2601 use data_quality::{CopyPlan, NoCopy};
2602 let Some(state) = self.data_table_state.as_ref() else {
2603 return CopyPlan::NotApplicable;
2604 };
2605 if plan.compute != data_quality::QualityCompute::Full || !state.is_remote_source() {
2606 return CopyPlan::NotApplicable;
2607 }
2608 if !state.quality_reads_whole_source(&plan.scope) {
2609 return CopyPlan::Passes(NoCopy::PartOfTheSource);
2610 }
2611 if let Some(copy) = self.quality_copy_kept() {
2612 return CopyPlan::Kept {
2613 bytes: copy.bytes(),
2614 objects: copy.objects(),
2615 };
2616 }
2617 let limit = self.quality_copy_limit();
2618 if limit == 0 {
2619 return CopyPlan::Passes(NoCopy::Off);
2620 }
2621 if self.quality_copy_unusable == Some(self.dataset_generation) {
2622 return CopyPlan::Passes(NoCopy::Unusable);
2623 }
2624 let Some((bytes, objects)) = state.remote_objects_size() else {
2625 return CopyPlan::Passes(NoCopy::SizeUnknown);
2626 };
2627 if bytes > limit {
2628 return CopyPlan::Passes(NoCopy::TooLarge { bytes, limit });
2629 }
2630 let free = self.quality_copy_free_space();
2631 if free.is_none_or(|free| bytes > free) {
2632 return CopyPlan::Passes(NoCopy::NoRoom { bytes, free });
2633 }
2634 CopyPlan::Fetch { bytes, objects }
2635 }
2636
2637 pub(crate) fn quality_copy_released(&self) -> bool {
2639 self.quality_copy_released == Some(self.dataset_generation)
2640 }
2641
2642 fn retain_quality_copy(
2646 &mut self,
2647 dataset_generation: u64,
2648 copy: Option<Arc<crate::local_copy::LocalCopy>>,
2649 ) {
2650 if dataset_generation != self.dataset_generation {
2651 return;
2652 }
2653 let Some(copy) = copy else {
2654 self.quality_copies
2655 .retain(|kept| kept.dataset_generation != dataset_generation);
2656 self.quality_copy_unusable = Some(dataset_generation);
2657 return;
2658 };
2659 self.quality_copies.insert(
2660 0,
2661 RetainedCopy {
2662 dataset_generation,
2663 copy,
2664 },
2665 );
2666 self.quality_copy_released = None;
2667 let limit = self.quality_copy_limit();
2668 while self.quality_copies.len() > 1 && self.quality_copy_bytes() > limit {
2669 self.quality_copies.pop();
2670 }
2671 }
2672
2673 pub fn is_busy(&self) -> bool {
2676 self.busy || self.jobs.holds_keys() || self.loading.waits()
2677 }
2678
2679 pub fn task_generation(&self) -> u64 {
2682 self.jobs.generation()
2683 }
2684
2685 pub fn job_is_current(&self, ticket: Ticket) -> bool {
2687 self.jobs.is_current(ticket)
2688 }
2689
2690 pub fn use_cache(&mut self, cache: CacheManager) {
2694 self.cache = cache;
2695 }
2696
2697 pub fn use_catalog_dir(&mut self, dir: &Path) -> Result<()> {
2700 self.app_config.read_catalog_files(Some(dir))
2701 }
2702
2703 pub fn open_path(&self) -> Option<&Path> {
2706 self.path.as_deref()
2707 }
2708
2709 fn reads_stdin(&self) -> bool {
2712 self.opened
2713 .as_ref()
2714 .is_some_and(|(paths, _)| matches!(paths.as_slice(), [path] if stdin::is_stdin(path)))
2715 }
2716
2717 fn view_dataset(&self) -> Option<view::Dataset<'_>> {
2721 self.data_table_state.as_ref()?;
2722 let path = match self.path.as_deref() {
2723 Some(path) if !self.reads_stdin() => path,
2724 _ => Path::new(stdin::PATH),
2725 };
2726 Some(view::Dataset {
2727 path,
2728 table: self.view_table(),
2729 })
2730 }
2731
2732 fn view_table(&self) -> Option<&str> {
2736 let (_, options) = self.opened.as_ref()?;
2737 options.table.as_deref()
2738 }
2739
2740 pub fn background_work_in_flight(&self) -> bool {
2744 self.jobs.in_flight()
2745 }
2746
2747 pub fn screen_generation(&self) -> u64 {
2749 self.screen_generation
2750 }
2751
2752 pub fn modal_showing(&self) -> bool {
2754 self.error_modal.active || self.confirmation_modal.active
2755 }
2756
2757 fn analysis_results_are_sampled(&self) -> bool {
2759 self.analysis_modal.view == analysis_modal::AnalysisView::Main
2760 && self.analysis_modal.computing.is_none()
2761 && self
2762 .analysis_modal
2763 .current_results()
2764 .is_some_and(|r| r.sample_size.is_some())
2765 }
2766
2767 pub(crate) fn cancelled_analysis_running(&self) -> Option<std::time::Instant> {
2770 self.cancelled_analysis().map(|(since, _)| since)
2771 }
2772
2773 fn cancelled_analysis(&self) -> Option<(std::time::Instant, bool)> {
2776 let (since, job) = self.jobs.cancelled_running(Self::reads_for_analysis)?;
2777 let runs_out = match job {
2778 Job::Analysis(run) => run.runs_out,
2779 _ => true,
2780 };
2781 Some((since, runs_out))
2782 }
2783
2784 fn reads_for_analysis(job: &Job) -> bool {
2786 matches!(job, Job::Analysis(_) | Job::SampleRows)
2787 }
2788
2789 pub(crate) fn cancelled_run_shown(&self) -> Option<crate::widgets::data_quality::Cancelling> {
2793 let (since, read_runs_out) = self.cancelled_analysis()?;
2794 (read_runs_out || since.elapsed() >= CANCEL_GRACE).then_some(
2795 crate::widgets::data_quality::Cancelling {
2796 since,
2797 read_runs_out,
2798 },
2799 )
2800 }
2801
2802 fn cancelled_work_running(&self) -> bool {
2804 self.jobs.running_behind()
2805 }
2806
2807 fn sample_source(&self, state: &DataTableState) -> (sampling::SampleSource, Option<usize>) {
2812 Self::sample_source_for(state, &self.analysis_modal.sample.scope)
2813 }
2814
2815 fn sample_source_for(
2816 state: &DataTableState,
2817 scope: &data_quality::QualityScope,
2818 ) -> (sampling::SampleSource, Option<usize>) {
2819 if scope.uses_source() {
2820 let (lf, source) = state.data_quality_source_scan();
2821 return (sampling::SampleSource::loaded(lf, source), None);
2822 }
2823 let lf = match scope {
2824 data_quality::QualityScope::FirstRows(_)
2825 | data_quality::QualityScope::ViewRows { .. } => state.lf().clone(),
2826 _ => state.analysis_lf(),
2827 };
2828 (
2829 sampling::SampleSource::view(lf.select(state.binary_stub_exprs())),
2830 sampling::view_scope_rows(state.num_rows_if_valid(), scope),
2831 )
2832 }
2833
2834 fn open_sample_form(&mut self) {
2838 self.open_sample_form_as(false);
2839 }
2840
2841 fn open_first_run_form(&mut self) {
2845 self.open_sample_form_as(true);
2846 self.sync_sample_form_focus();
2847 }
2848
2849 fn sync_sample_form_focus(&mut self) {
2851 let focused = self.analysis_modal.focus == analysis_modal::AnalysisFocus::Main;
2852 if let Some(form) = self.analysis_modal.sample_form.as_mut() {
2853 let has_cursor = focused || !form.inline;
2854 form.sync_focus(has_cursor);
2855 }
2856 }
2857
2858 fn run_sample_form(&mut self) -> Option<AppEvent> {
2862 let quality =
2863 self.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality);
2864 if self.analysis_modal.sample_form.as_ref()?.view {
2866 let memory = self.memory_check();
2867 let form = self.analysis_modal.sample_form.as_mut()?;
2868 return match Self::submit_view_sample(form, memory) {
2869 sample_keys::Submitted::Stays => None,
2870 sample_keys::Submitted::Clear => {
2871 self.analysis_modal.sample_form = None;
2872 self.clear_table_sample();
2873 if quality {
2874 None
2875 } else {
2876 self.start_analysis_run()
2877 }
2878 }
2879 sample_keys::Submitted::Draw { sample, anyway } => {
2880 self.analysis_modal.sample_form = None;
2881 self.apply_table_sample(sample, None, anyway, !quality);
2882 if !quality {
2883 self.analysis_modal.computing =
2884 Some(AnalysisProgress::new("Drawing the sample"));
2885 }
2886 None
2887 }
2888 };
2889 }
2890 let finished = self.analysis_modal.sample_form.as_mut()?.finish();
2891 match finished {
2892 Ok(sample) if quality => {
2893 self.analysis_modal.sample_form = None;
2894 self.analysis_modal.data_quality_plan.adopt_sample(&sample);
2895 self.analysis_modal.data_quality_setup_note = None;
2896 None
2897 }
2898 Ok(_) if self.read_waits_for_cancelled() => None,
2900 Ok(sample) => {
2901 self.analysis_modal.sample_form = None;
2902 self.apply_sample(sample)
2903 }
2904 Err(error) => {
2905 if let Some(form) = self.analysis_modal.sample_form.as_mut() {
2906 form.error = Some(error);
2907 }
2908 None
2909 }
2910 }
2911 }
2912
2913 fn open_sample_form_as(&mut self, inline: bool) {
2914 let sample = self.analysis_modal.sample.clone();
2915 self.open_sample_form_on(&sample, inline);
2916 }
2917
2918 fn open_quality_sample_form(&mut self) {
2921 self.open_quality_setup();
2922 let sample = self.analysis_modal.data_quality_plan.sample();
2923 self.open_sample_form_on(&sample, false);
2924 }
2925
2926 fn open_sample_form_on(&mut self, sample: &sampling::Sample, inline: bool) {
2927 let Some(state) = self.data_table_state.as_ref() else {
2928 return;
2929 };
2930 let (sample, view) = match state.sampled() {
2933 Some(sampled) => (sampled.sample().clone(), true),
2934 None => (sample.clone(), false),
2935 };
2936 let context = self.sample_context(state.unsampled());
2937 let mut form = sample_modal::SampleForm::new(&sample, context, &self.theme);
2938 form.inline = inline;
2939 form.view = view;
2940 form.bytes_per_row = Some(state.unsampled().sample_row_bytes(false));
2941 form.source_bytes_per_row = Some(state.unsampled().sample_row_bytes(true));
2942 self.analysis_modal.sample_form = Some(form);
2943 self.sync_sample_form_focus();
2944 }
2945
2946 pub(crate) fn sample_context(&self, state: &DataTableState) -> sample_modal::SampleContext {
2949 let mut partition_columns = state.partition_columns().unwrap_or_default().to_vec();
2950 let mut partition_values = Vec::new();
2951 if let Some(dir) = self.path.as_ref().filter(|path| path.is_dir()) {
2956 if partition_columns.is_empty() {
2957 partition_columns = DataTableState::discover_hive_partition_columns(dir)
2958 .into_iter()
2959 .filter(|column| state.schema().get(column).is_some())
2960 .collect();
2961 }
2962 if let Some(first) = partition_columns.first() {
2963 let prefix = format!("{first}=");
2964 let mut values: Vec<String> = std::fs::read_dir(dir)
2965 .into_iter()
2966 .flatten()
2967 .flatten()
2968 .filter_map(|entry| {
2969 let name = entry.file_name().to_string_lossy().to_string();
2970 name.strip_prefix(&prefix).map(str::to_string)
2971 })
2972 .collect();
2973 values.sort();
2974 if !values.is_empty() {
2975 partition_values.push((first.clone(), values));
2976 }
2977 }
2978 }
2979 let mut value_columns = partition_columns.clone();
2983 for kind in 0..3 {
2984 for (name, dtype) in state.schema().iter() {
2985 let rank = match dtype {
2986 DataType::String | DataType::Categorical(..) | DataType::Boolean => 0,
2987 DataType::Date => 1,
2988 dtype if dtype.is_integer() => 2,
2989 _ => continue,
2990 };
2991 if rank == kind && !value_columns.iter().any(|column| column == name.as_str()) {
2992 value_columns.push(name.to_string());
2993 }
2994 }
2995 }
2996 sample_modal::SampleContext {
2997 view_rows: state.num_rows_if_valid(),
2998 filtered: state.changes_rows(),
2999 files: state.quality_source_file_names().to_vec(),
3000 partition_columns,
3001 partition_values,
3002 time_columns: state.quality_temporal_columns(&data_quality::QualityScope::WholeSource),
3003 value_columns,
3004 }
3005 }
3006
3007 fn sample_form_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
3008 let form = self.analysis_modal.sample_form.as_mut()?;
3009 let file_count = form.context.files.len();
3010 let key = form::key(form, event);
3011 match key {
3012 FormKey::Cancel if form.inline => {
3015 self.analysis_modal.focus = analysis_modal::AnalysisFocus::Sidebar;
3016 self.open_first_run_form();
3017 }
3018 FormKey::Cancel => self.analysis_modal.sample_form = None,
3019 FormKey::Submit => return self.run_sample_form(),
3020 FormKey::Step(_, delta) => {
3021 form.adjust(delta > 0);
3022 form.edited();
3023 }
3024 FormKey::Text(sample_modal::SampleField::Files)
3025 if matches!(event.code, KeyCode::PageDown | KeyCode::PageUp) =>
3026 {
3027 form.file_offset = if event.code == KeyCode::PageDown {
3028 (form.file_offset + crate::widgets::sample_form::FILES_SHOWN)
3029 .min(file_count.saturating_sub(1))
3030 } else {
3031 form.file_offset
3032 .saturating_sub(crate::widgets::sample_form::FILES_SHOWN)
3033 };
3034 }
3035 FormKey::Text(_) => {
3036 if let Some(input) = form.input_mut(form.field) {
3037 let _ = input.handle_key(event, None);
3038 }
3039 form.edited();
3040 }
3041 FormKey::Act(_) | FormKey::Moved | FormKey::Other => {}
3042 }
3043 None
3044 }
3045
3046 fn read_sample_view(&mut self) -> Option<AppEvent> {
3051 let sample = self.analysis_modal.sample.clone();
3052 self.read_sample_rows(sample, None)
3053 }
3054
3055 fn read_sample_rows(
3059 &mut self,
3060 sample: sampling::Sample,
3061 evidence: Option<(quality_report::EvidenceRows, String)>,
3062 ) -> Option<AppEvent> {
3063 let state = self.data_table_state.as_ref()?;
3064 let (source, known_total) = Self::sample_source_for(state, &sample.scope);
3065 let streaming = self.app_config.performance.streaming;
3066 let kept = self.kept_quality_sample(&sample).map(|kept| {
3069 let columns: Vec<_> = state
3070 .schema()
3071 .iter_names()
3072 .filter(|name| kept.df().column(name.as_str()).is_ok())
3073 .map(|name| polars::prelude::col(name.clone()))
3074 .collect();
3075 (kept, columns)
3076 });
3077 if kept.is_none() && self.read_waits_for_cancelled() {
3078 return None;
3079 }
3080 self.analysis_modal.computing = Some(AnalysisProgress::new(if evidence.is_some() {
3081 "Reading the matching sampled rows"
3082 } else {
3083 "Reading the sample"
3084 }));
3085 self.spawn_job(Job::SampleRows, Some("Reading the sample..."), move |_| {
3086 let (rows, columns) = match kept {
3089 Some((kept, columns)) => (Ok(kept.analysis_rows(kept.df().clone())), Some(columns)),
3090 None => (
3091 source
3092 .cut(&sample.scope)
3093 .and_then(|lf| sampling::read(&lf, &sample, known_total, streaming)),
3094 None,
3095 ),
3096 };
3097 let shown = |df: polars::prelude::DataFrame| match &columns {
3098 Some(columns) => polars::prelude::IntoLazy::lazy(df)
3099 .select(columns.clone())
3100 .collect()
3101 .map_err(color_eyre::eyre::Report::from),
3102 None => Ok(df),
3103 };
3104 let read = rows.and_then(|rows| {
3105 let label = format!(
3106 "Sample {} {}",
3107 crate::glyphs::get().middot,
3108 sample.outcome(
3109 rows.total_rows,
3110 rows.sample_size,
3111 rows.per_value.as_ref().map(|per_value| per_value.kept),
3112 )
3113 );
3114 match evidence {
3115 Some((quality_report::EvidenceRows::Duplicates, label)) => {
3116 let keys = rows
3119 .df
3120 .get_column_names()
3121 .into_iter()
3122 .filter(|name| !name.starts_with("__datui"))
3123 .cloned()
3124 .collect::<Vec<_>>();
3125 let df = data_quality::duplicate_rows(
3126 polars::prelude::IntoLazy::lazy(rows.df),
3127 &keys,
3128 streaming,
3129 )?;
3130 Ok((shown(df)?, label))
3131 }
3132 Some((quality_report::EvidenceRows::Matching(predicate), label)) => {
3133 let df = polars::prelude::IntoLazy::lazy(rows.df)
3134 .filter(predicate)
3135 .collect()?;
3136 Ok((shown(df)?, label))
3137 }
3138 Some((quality_report::EvidenceRows::Files(_), label)) => {
3140 Ok((shown(rows.df)?, label))
3141 }
3142 None => Ok((shown(rows.df)?, label)),
3143 }
3144 });
3145 let (df, label) = read.map_err(|error| format!("{error}"))?;
3146 Ok(Answer::Sample { df, label })
3147 });
3148 None
3149 }
3150
3151 fn show_sample_view(&mut self, df: polars::prelude::DataFrame, label: String) {
3154 let Some(state) = self.data_table_state.as_ref() else {
3155 return;
3156 };
3157 let view = match state.sample_view(df) {
3158 Ok(view) => view,
3159 Err(error) => {
3160 self.error_modal
3161 .show(format!("Cannot show the sample: {error}"));
3162 return;
3163 }
3164 };
3165 if let Some(original) = self.data_table_state.replace(view) {
3166 self.quality_evidence_return = Some(Box::new(original));
3167 self.quality_evidence_label = Some(label);
3168 self.analysis_modal.active = false;
3169 self.forget_the_rows_read();
3170 self.spawn_async_collect("Loading the sample...");
3171 }
3172 }
3173
3174 fn sync_quality_plan(&mut self) {
3177 let sample = self.analysis_modal.sample.clone();
3178 self.analysis_modal.data_quality_plan.adopt_sample(&sample);
3179 }
3180
3181 fn open_quality_setup(&mut self) {
3184 use data_quality::QualityPage;
3185 let modal = &mut self.analysis_modal;
3186 if !modal.data_quality_page.is_setup() {
3187 modal.data_quality_setup_return = modal.data_quality_page.tab();
3188 }
3189 if modal.data_quality_setup_before.is_none() {
3190 modal.data_quality_setup_before = Some(modal.data_quality_plan.clone());
3191 }
3192 if modal.data_quality_page != QualityPage::Setup {
3193 modal.set_quality_page(QualityPage::Setup);
3194 modal.data_quality_plan_field = 0;
3195 }
3196 modal.focus = analysis_modal::AnalysisFocus::Main;
3197 }
3198
3199 fn leave_quality_setup(&mut self) {
3202 use data_quality::QualityPage;
3203 let modal = &mut self.analysis_modal;
3204 if let Some(before) = modal.data_quality_setup_before.take() {
3205 modal.data_quality_plan = before;
3206 }
3207 modal.data_quality_setup_note = None;
3208 modal.data_quality_confirm_run = false;
3209 modal.data_quality_picker = None;
3210 if modal.data_quality_results.is_some() {
3211 let back = match modal.data_quality_setup_return {
3212 page if page.is_setup() => QualityPage::Overview,
3213 page => page,
3214 };
3215 modal.set_quality_page(back);
3216 } else {
3217 modal.set_quality_page(QualityPage::Setup);
3218 modal.focus = analysis_modal::AnalysisFocus::Sidebar;
3219 }
3220 }
3221
3222 fn quality_full_scan_question(&self, plan: &data_quality::DataQualityPlan) -> String {
3225 let mut lines = vec![
3226 "Run a full scan?".to_string(),
3227 String::new(),
3228 "Reads: every eligible row, up to the whole source".to_string(),
3229 ];
3230 if let data_quality::CopyPlan::Fetch { bytes, .. } = self.quality_copy_plan(plan) {
3231 lines.push(format!(
3232 "Fetch: {} once, to a local copy",
3233 crate::widgets::info::format_bytes(bytes)
3234 ));
3235 }
3236 lines.push("Source writes: none".to_string());
3237 lines.join("\n")
3238 }
3239
3240 fn quality_setup_problem(&self) -> Option<String> {
3243 let plan = &self.analysis_modal.data_quality_plan;
3244 let schema = self.data_table_state.as_ref()?.quality_schema(&plan.scope);
3245 match &plan.grain {
3246 data_quality::QualityGrain::TimeWindows { column, .. }
3247 if plan.compute != data_quality::QualityCompute::Metadata
3248 && !plan.reads_as_time(column, schema) =>
3249 {
3250 Some(format!(
3251 "{column}: text, no format {} set Text as time",
3252 crate::glyphs::get().middot
3253 ))
3254 }
3255 _ => None,
3256 }
3257 }
3258
3259 fn run_quality_setup(&mut self) -> Option<AppEvent> {
3267 use data_quality::QualityPage;
3268 if self.cancelled_analysis_running().is_some() {
3269 self.analysis_modal.data_quality_confirm_run = false;
3270 self.analysis_modal.data_quality_setup_note = Some(QUALITY_RUN_WAITS.to_string());
3271 return None;
3272 }
3273 if let Some(problem) = self.quality_setup_problem() {
3274 self.analysis_modal.data_quality_setup_note = Some(problem);
3275 return None;
3276 }
3277 let plan = &self.analysis_modal.data_quality_plan;
3279 let here = (self.analysis_modal.data_quality_results.is_some()
3280 && self
3281 .analysis_modal
3282 .data_quality_last_plan
3283 .as_ref()
3284 .is_some_and(|last| last.same_measurement(plan)))
3285 || self.quality_cached(plan);
3286 if plan.requires_confirmation() && !here && !self.analysis_modal.data_quality_confirm_run {
3287 let message = self.quality_full_scan_question(plan);
3289 self.analysis_modal.data_quality_confirm_run = true;
3290 self.confirmation_modal.show(message);
3291 self.confirmation_modal.yes_label = "Run";
3292 return None;
3293 }
3294 self.analysis_modal.data_quality_confirm_run = false;
3295 self.commit_quality_plan();
3296 let modal = &mut self.analysis_modal;
3297 if modal.data_quality_results.is_some()
3298 && modal.data_quality_last_plan.as_ref() == Some(&modal.data_quality_plan)
3299 {
3300 let back = match modal.data_quality_setup_return {
3301 page if page.is_setup() => QualityPage::Overview,
3302 page => page,
3303 };
3304 modal.set_quality_page(back);
3305 return None;
3306 }
3307 if let (Some(results), Some(last)) = (
3311 modal.data_quality_results.as_ref(),
3312 modal.data_quality_last_plan.as_ref(),
3313 ) && last.same_measurement(&modal.data_quality_plan)
3314 {
3315 let mut results = results.clone();
3316 let plan = modal.data_quality_plan.clone();
3317 let page = if last.compares_differently(&plan) {
3318 results.compare_segments(&plan);
3319 QualityPage::Segments
3320 } else {
3321 QualityPage::Trends
3322 };
3323 modal.data_quality_results = Some(results.clone());
3324 modal.data_quality_last_plan = Some(plan.clone());
3325 modal.set_quality_page(page);
3326 self.cache_quality_result(&results, plan);
3327 return None;
3328 }
3329 if self.restore_cached_quality() {
3330 return None;
3331 }
3332 self.analysis_modal.data_quality_from_cache = false;
3333 let mut progress = AnalysisProgress::new("Preparing the plan");
3334 if self.quality_kept_serves(&self.analysis_modal.data_quality_plan) {
3335 progress.reuse = Some("Starts from rows a run already read".to_string());
3336 }
3337 self.analysis_modal.computing = Some(progress);
3338 self.busy = true;
3339 Some(AppEvent::AnalysisDataQualityCompute)
3340 }
3341
3342 fn commit_quality_plan(&mut self) {
3347 let modal = &mut self.analysis_modal;
3348 let sample = modal.data_quality_plan.sample();
3349 if sample != modal.sample {
3350 modal.describe_results = None;
3351 modal.distribution_results = None;
3352 modal.correlation_results = None;
3353 }
3354 modal.sample = sample;
3355 modal.sample_dataset = Some(self.dataset_generation);
3356 modal.sample_run_for = Some(self.dataset_generation);
3357 modal.data_quality_setup_before = None;
3358 modal.data_quality_setup_note = None;
3359 modal.data_quality_picker = None;
3360 }
3361
3362 fn apply_sample(&mut self, sample: sampling::Sample) -> Option<AppEvent> {
3366 if self.analysis_modal.selected_tool != Some(analysis_modal::AnalysisTool::DataQuality)
3368 && self.read_waits_for_cancelled()
3369 {
3370 return None;
3371 }
3372 if sample != self.analysis_modal.sample {
3374 self.analysis_modal.describe_results = None;
3375 self.analysis_modal.distribution_results = None;
3376 self.analysis_modal.correlation_results = None;
3377 self.analysis_modal.data_quality_results = None;
3378 self.analysis_modal.data_quality_last_plan = None;
3379 self.analysis_modal.data_quality_from_cache = false;
3380 }
3381 self.analysis_modal.sample = sample;
3382 self.analysis_modal.sample_dataset = Some(self.dataset_generation);
3383 self.analysis_modal.sample_run_for = Some(self.dataset_generation);
3384 self.sync_quality_plan();
3385 if self.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality) {
3386 return None;
3387 }
3388 self.start_analysis_run()
3389 }
3390
3391 fn read_waits_for_cancelled(&mut self) -> bool {
3393 if self.cancelled_analysis_running().is_none() {
3394 return false;
3395 }
3396 self.flash_note(ANALYSIS_READ_WAITS.to_string());
3397 true
3398 }
3399
3400 fn start_analysis_run(&mut self) -> Option<AppEvent> {
3402 let tool = self.analysis_modal.selected_tool?;
3403 if tool != analysis_modal::AnalysisTool::DataQuality && self.read_waits_for_cancelled() {
3404 return None;
3405 }
3406 let (phase, event) = match tool {
3407 analysis_modal::AnalysisTool::Describe => {
3408 self.analysis_modal.describe_results = None;
3409 self.analysis_computation = Some(AnalysisComputationState {
3410 df: None,
3411 schema: None,
3412 partial_stats: Vec::new(),
3413 current: 0,
3414 total: 0,
3415 total_rows: 0,
3416 sample_seed: self.analysis_modal.sample.seed,
3417 sample_size: None,
3418 });
3419 ("Describing data", AppEvent::AnalysisChunk)
3420 }
3421 analysis_modal::AnalysisTool::DistributionAnalysis => {
3422 self.analysis_modal.distribution_results = None;
3423 (
3424 "Analyzing distributions",
3425 AppEvent::AnalysisDistributionCompute,
3426 )
3427 }
3428 analysis_modal::AnalysisTool::CorrelationMatrix => {
3429 self.analysis_modal.correlation_results = None;
3430 (
3431 "Computing correlations",
3432 AppEvent::AnalysisCorrelationCompute,
3433 )
3434 }
3435 analysis_modal::AnalysisTool::DataQuality => return None,
3436 };
3437 self.analysis_modal.computing = Some(AnalysisProgress::new(phase));
3438 self.busy = true;
3439 Some(event)
3440 }
3441
3442 fn cancel_analysis(&mut self) {
3450 let reads_out = self
3454 .analysis_modal
3455 .computing
3456 .as_ref()
3457 .is_some_and(AnalysisProgress::read_runs_out);
3458 let mut read_runs_out = true;
3459 if let Some(Job::Analysis(run)) =
3460 self.jobs.current_mut(|job| matches!(job, Job::Analysis(_)))
3461 {
3462 read_runs_out = run.watch.is_none() || reads_out;
3463 run.runs_out = read_runs_out;
3464 if let Some(watch) = &run.watch {
3465 watch.cancel();
3466 }
3467 }
3468 let reading_sample = self
3469 .jobs
3470 .current(|job| matches!(job, Job::SampleRows))
3471 .is_some();
3472 self.jobs.cancel(Self::reads_for_analysis);
3473 self.jobs.advance();
3474 self.screen_generation = self.screen_generation.wrapping_add(1);
3477 self.analysis_modal.computing = None;
3478 self.analysis_computation = None;
3479 self.busy = false;
3480 self.status_message = None;
3481 if reading_sample {
3483 self.flash_note("Row view cancelled".to_string());
3484 return;
3485 }
3486 if self.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality) {
3491 self.open_quality_setup();
3492 if !read_runs_out {
3493 self.flash_note("Run cancelled".to_string());
3494 }
3495 return;
3496 }
3497 self.analysis_modal.selected_tool = None;
3498 self.analysis_modal.focus = analysis_modal::AnalysisFocus::Sidebar;
3499 self.flash_note("Analysis cancelled".to_string());
3500 }
3501
3502 pub(crate) fn pivot_computing(&self) -> bool {
3504 self.input_mode == InputMode::PivotMelt
3505 && self.jobs.current(|job| matches!(job, Job::Pivot)).is_some()
3506 }
3507
3508 fn cancel_pivot(&mut self) {
3511 self.jobs.advance();
3512 self.screen_generation = self.screen_generation.wrapping_add(1);
3513 self.busy = false;
3514 self.status_message = None;
3515 self.flash_note("Pivot cancelled".to_string());
3516 }
3517
3518 fn drill_into(&mut self, group_index: usize, row: &DataFrame) {
3522 let Some(state) = self.data_table_state.as_mut() else {
3523 return;
3524 };
3525 let drilled = state.deferred(|s| s.drill_down_with_row(group_index, row));
3526 match drilled {
3527 Ok(()) => {
3528 self.sync_sort_filter_modal();
3529 self.spawn_async_collect(Self::LOADING_BUFFER);
3530 }
3531 Err(e) => self.flash_note(format!(
3532 "Could not drill in: {}",
3533 crate::error_display::user_message_from_report(&e, None)
3534 )),
3535 }
3536 }
3537
3538 #[doc(hidden)]
3540 pub fn read_stdin_from(&mut self, reader: impl std::io::Read + Send + 'static) {
3541 self.stdin_reader = Some(Box::new(reader));
3542 }
3543
3544 #[doc(hidden)]
3547 pub fn pass_stdout_to(&mut self, out: impl std::io::Write + Send + 'static) {
3548 self.stdout_pass = Some(Box::new(out));
3549 }
3550
3551 pub fn follow(&self) -> Option<&crate::follow::Follow> {
3553 self.data_table_state.as_ref()?.follow()
3554 }
3555
3556 #[doc(hidden)]
3560 pub fn follow_settled(&self) -> bool {
3561 self.rows_in_flight().is_none() && !self.follow().is_some_and(|f| f.behind())
3562 }
3563
3564 #[doc(hidden)]
3566 pub fn check_follow_now(&self) {
3567 if let Some(follow) = self.follow() {
3568 follow.check_now();
3569 }
3570 }
3571
3572 fn followed(&mut self, news: &crate::follow::News) {
3575 let Some(state) = self.data_table_state.as_mut() else {
3576 return;
3577 };
3578 let Some(follow) = state.follow_mut().filter(|f| f.id() == news.id) else {
3579 return;
3580 };
3581 let message = follow.take(&news.change);
3582 let fields = follow.take_new_fields();
3583 if let Some(handle) = follow.take_held() {
3584 state.read_followed_through(&handle);
3585 }
3586 if let Some(message) = message {
3587 self.flash_note(message);
3588 }
3589 if !fields.is_empty() {
3590 self.followed_fields_held = Some((self.dataset_generation, fields));
3591 }
3592 self.catch_up_follow();
3593 self.join_followed_fields();
3594 self.describe_ended_journal();
3595 }
3596
3597 fn describe_ended_journal(&mut self) {
3601 let Some(lf) = self
3602 .data_table_state
3603 .as_mut()
3604 .and_then(|state| state.ended_journal_to_describe())
3605 else {
3606 return;
3607 };
3608 let generation = self.dataset_generation;
3609 let tx = self.events.clone();
3610 self.runtime.spawn_blocking(move || {
3611 let detail = logging::catch_panic(|| crate::journal::summary(&lf).ok())
3612 .ok()
3613 .flatten();
3614 if let Some(detail) = detail {
3615 let _ = tx.send(AppEvent::FollowedDetail {
3616 dataset_generation: generation,
3617 detail: Box::new(detail),
3618 });
3619 }
3620 });
3621 }
3622
3623 fn join_followed_fields(&mut self) {
3627 let Some((generation, _)) = self.followed_fields_held.as_ref() else {
3628 return;
3629 };
3630 if *generation != self.dataset_generation {
3631 self.followed_fields_held = None;
3632 return;
3633 }
3634 if self.data_table_state.is_none()
3636 || self.work_the_join_would_cancel()
3637 || self.follow().is_some_and(|f| f.behind())
3638 {
3639 return;
3640 }
3641 let Some((generation, fields)) = self.followed_fields_held.take() else {
3642 return;
3643 };
3644 let Some(state) = self.data_table_state.as_mut() else {
3645 return;
3646 };
3647 match state.join_followed_fields(&fields) {
3648 Ok(true) => {
3649 self.spawn_async_collect(Self::LOADING_BUFFER);
3650 }
3651 Ok(false) => {}
3652 Err(()) => self.followed_fields_held = Some((generation, fields)),
3653 }
3654 }
3655
3656 fn catch_up_follow(&mut self) {
3661 if !self.in_normal_table_view()
3662 || self.is_busy()
3663 || self.loading.awaiting_dataset()
3664 || self.rows_in_flight().is_some()
3665 {
3666 return;
3667 }
3668 self.take_follow_rows(true);
3669 }
3670
3671 fn take_follow_rows(&mut self, read: bool) -> bool {
3675 let Some(state) = self.data_table_state.as_mut() else {
3676 return false;
3677 };
3678 let on_last_row = state.on_last_row();
3679 let counted = state.is_num_rows_valid();
3680 let drawn = state.visible_rows > 0 && counted;
3681 let Some(follow) = state.follow_mut() else {
3682 return false;
3683 };
3684 let settle = read && drawn && std::mem::take(&mut follow.settle_at_end);
3688 let to_end = settle || (read && counted && std::mem::take(&mut follow.end_pending));
3689 let stale = read && std::mem::take(&mut follow.stale_view);
3690 let at_bottom = to_end || on_last_row || follow.end_pending;
3691 if at_bottom {
3692 follow.new_below = 0;
3693 }
3694 if !follow.behind() {
3695 if (to_end && state.scroll_to_end()) || stale {
3696 self.spawn_collect(None);
3697 }
3698 return false;
3699 }
3700 let before = follow.shown();
3701 let (rows, restarted) = follow.catch_up();
3702 if at_bottom {
3703 follow.end_pending = true;
3704 } else if !restarted {
3705 follow.new_below += rows.saturating_sub(before);
3706 }
3707 follow.stale_view = !read;
3708 state.follow_to(rows, restarted);
3709 if at_bottom && read {
3710 if state.is_num_rows_valid() {
3711 state.aim_at_end();
3712 } else {
3713 self.end_after_count = Some(state.len_generation());
3715 }
3716 }
3717 if read && !self.spawn_collect(None) {
3718 self.catch_up_follow();
3720 }
3721 true
3722 }
3723
3724 fn follow_rows_waiting(&self) -> bool {
3727 self.follow().is_some_and(|f| f.behind()) && !self.loading.awaiting_dataset()
3728 }
3729
3730 pub fn recording(&self) -> Option<&Arc<crate::follow::Spool>> {
3733 self.data_table_state.as_ref()?;
3734 self.opened
3735 .as_ref()
3736 .and_then(|(_, options)| options.spool.as_ref())
3737 .map(|handle| handle.spool())
3738 .filter(|spool| spool.tee().is_some())
3739 }
3740
3741 fn leaves(&self, key: &KeyEvent) -> Option<Leaving> {
3743 if !key.is_press() {
3744 return None;
3745 }
3746 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
3747 match key.code {
3748 KeyCode::Char('q') if ctrl => Some(Leaving::Quit),
3749 KeyCode::Char('c') if ctrl => Some(Leaving::Quit),
3750 KeyCode::Char('o') if ctrl => Some(Leaving::Home),
3751 KeyCode::Char('Q') if !ctrl && self.in_normal_table_view() => Some(Leaving::Quit),
3752 KeyCode::Char('q') if !ctrl && self.in_normal_table_view() => {
3753 Some(if self.opened_from_home {
3754 Leaving::Home
3755 } else {
3756 Leaving::Quit
3757 })
3758 }
3759 _ => None,
3760 }
3761 }
3762
3763 fn ask_about_recording(&mut self, leaving: Leaving) {
3765 let Some(tee) = self.recording().and_then(|spool| spool.tee()) else {
3766 return;
3767 };
3768 let doing = if tee.to_stdout() {
3769 "passed on"
3770 } else {
3771 "recorded"
3772 };
3773 let message = format!(
3774 "Standard input is still being {doing} to {}. Stop recording, or keep \
3775 recording until the stream ends?",
3776 tee.name()
3777 );
3778 self.pending_leave = Some(leaving);
3779 self.confirmation_modal
3780 .show_choice(message, "Stop recording", "Keep recording");
3781 }
3782
3783 fn leave_recording(&mut self, stop: bool) -> Option<AppEvent> {
3786 let leaving = self.pending_leave.take()?;
3787 let handle = self
3788 .opened
3789 .as_ref()
3790 .and_then(|(_, options)| options.spool.clone());
3791 if stop {
3792 if let Some(handle) = &handle {
3793 handle.spool().stop();
3794 }
3795 } else {
3796 self.recording_on = handle;
3798 }
3799 match leaving {
3800 Leaving::Quit => Some(AppEvent::Exit),
3801 Leaving::Home => {
3802 self.enter_home();
3803 None
3804 }
3805 }
3806 }
3807
3808 pub fn recording_after_exit(
3811 &mut self,
3812 ) -> Option<(crate::follow::Tee, Arc<crate::follow::SpoolHandle>)> {
3813 let handle = self.recording_on.take()?;
3814 let spool = handle.spool();
3815 let tee = spool.tee()?.clone();
3816 spool.live().then_some((tee, handle))
3817 }
3818
3819 fn notice_recording_end(&mut self) -> bool {
3822 let Some(spool) = self.recording().cloned() else {
3823 return false;
3824 };
3825 let Some(ended) = spool.ended() else {
3826 self.recording_end_said = false;
3827 return false;
3828 };
3829 if std::mem::replace(&mut self.recording_end_said, true) {
3830 return false;
3831 }
3832 let said = match spool.tee() {
3833 Some(tee) if tee.to_stdout() => "Standard input ended".to_string(),
3834 Some(tee) => format!("Saved {}", tee.path.display()),
3835 None => String::new(),
3836 };
3837 match ended {
3838 Some(reason) => self.error_modal.show(reason),
3839 None => self.flash_note(said),
3840 }
3841 true
3842 }
3843
3844 fn flash_note(&mut self, message: String) {
3846 self.flash = Some(Flash::new(message));
3847 }
3848
3849 fn flash_path(&mut self, prefix: &str, path: &std::path::Path) {
3851 self.flash = Some(Flash::path(prefix, path));
3852 }
3853
3854 pub fn flash_message(&self) -> Option<&str> {
3856 self.flash.as_ref().map(|f| f.message.as_str())
3857 }
3858
3859 pub fn error_message(&self) -> Option<&str> {
3861 self.error_modal
3862 .active
3863 .then_some(self.error_modal.message.as_str())
3864 }
3865
3866 pub fn next_deadline(&self) -> Option<std::time::Instant> {
3869 let flash = self.flash.as_ref().map(|f| f.expires);
3870 let clock = self
3871 .follow()
3872 .filter(|f| f.standing == crate::follow::Standing::Following)
3873 .and_then(|f| f.last_append)
3874 .map(crate::follow::next_tick);
3875 let recording = self
3878 .recording()
3879 .filter(|spool| spool.live())
3880 .map(|_| std::time::Instant::now() + std::time::Duration::from_secs(1));
3881 flash.into_iter().chain(clock).chain(recording).min()
3882 }
3883
3884 pub fn tick_follow_clock(&mut self) -> bool {
3886 let ended = self.notice_recording_end();
3887 let now = self.follow_mark();
3888 if now == self.follow_drawn {
3889 return ended;
3890 }
3891 self.follow_drawn = now;
3892 true
3893 }
3894
3895 fn follow_mark(&self) -> Option<crate::render::footer::FollowMark> {
3897 use crate::follow::Standing;
3898 if self.input_mode == InputMode::Hex {
3900 return None;
3901 }
3902 let state = self.data_table_state.as_ref()?;
3903 let rows = |n: usize, what: &str| format!("{} {what}", crate::numfmt::group_chrome(n));
3904 let (rec, rec_stopped) = match self.recording().map(|spool| recording_label(spool)) {
3905 Some((label, stopped)) => (Some(label), stopped),
3906 None => (None, false),
3907 };
3908 let Some(follow) = state.follow().filter(|f| f.standing != Standing::Ended) else {
3909 return rec.is_some().then(|| crate::render::footer::FollowMark {
3910 rec,
3911 rec_stopped,
3912 ..Default::default()
3913 });
3914 };
3915 let waiting = follow.waiting();
3916 let (chip, note) = match follow.standing {
3917 Standing::Following if follow.is_pipe() => {
3919 let read = follow.spool().map_or(0, |spool| spool.bytes());
3920 let chip = format!(
3921 "reading stdin {} {}",
3922 crate::glyphs::get().middot,
3923 crate::discover::format_size(read)
3924 );
3925 let note = (follow.new_below > 0 && !state.on_last_row())
3926 .then(|| rows(follow.new_below, "new below"));
3927 (chip, note)
3928 }
3929 Standing::Following => {
3930 let chip = match follow.last_append {
3931 Some(at) => format!(
3932 "following {} {}",
3933 crate::glyphs::get().middot,
3934 crate::follow::age(at.elapsed())
3935 ),
3936 None => "following".to_string(),
3937 };
3938 let note = if waiting > 0 && !self.in_normal_table_view() {
3939 Some(rows(waiting, "new rows"))
3941 } else if follow.new_below > 0 && !state.on_last_row() {
3942 Some(rows(follow.new_below, "new below"))
3943 } else {
3944 None
3945 };
3946 (chip, note)
3947 }
3948 _ => (
3949 "paused".to_string(),
3950 (waiting > 0).then(|| rows(waiting, "new rows")),
3951 ),
3952 };
3953 let misfits = follow.misfits();
3954 let refreshes = self.input_mode == InputMode::ValueCounts
3957 || (self.input_mode == InputMode::Chart
3958 && self.chart_modal.picker.is_none()
3959 && !self.chart_export_modal.active)
3960 || (self.analysis_modal.active && self.analysis_modal.current_results().is_some());
3961 let key = if self.in_normal_table_view() {
3962 Some(match follow.standing {
3963 Standing::Paused => "Resume",
3964 _ => "Pause",
3965 })
3966 } else if refreshes && follow.behind() {
3967 Some("Refresh")
3968 } else {
3969 None
3970 };
3971 Some(crate::render::footer::FollowMark {
3972 key,
3973 chip: Some(chip),
3974 note,
3975 warning: (misfits > 0).then(|| {
3976 if misfits == 1 {
3977 "1 row does not fit".to_string()
3978 } else {
3979 rows(misfits, "rows do not fit")
3980 }
3981 }),
3982 rec,
3983 rec_stopped,
3984 })
3985 }
3986
3987 fn toggle_follow(&mut self) -> Option<AppEvent> {
3990 use crate::follow::Standing;
3991 if let Some(follow) = self.data_table_state.as_mut().and_then(|s| s.follow_mut()) {
3992 match follow.standing {
3993 Standing::Following => follow.pause(),
3994 Standing::Paused => {
3995 follow.resume();
3996 self.catch_up_follow();
3997 }
3998 Standing::Ended => {}
3999 }
4000 return None;
4001 }
4002 let (paths, options) = self.opened.clone()?;
4003 if paths.iter().any(|path| crate::stdin::is_stdin(path)) {
4004 self.flash_note("Standard input is followed from the start: datui -f -".to_string());
4005 return None;
4006 }
4007 if let Some(refusal) = crate::follow::refuse_paths(&paths, &options) {
4008 self.flash_note(refusal);
4009 return None;
4010 }
4011 let options = OpenOptions {
4012 follow: true,
4013 ..options
4014 };
4015 self.set_loading_phase("Scanning input", 10);
4016 self.name_what_is_loading(paths[0].clone());
4017 Some(AppEvent::Open(paths, options))
4018 }
4019
4020 pub fn tick_flash(&mut self) -> bool {
4022 if self.flash.as_ref().is_some_and(Flash::expired) {
4023 self.flash = None;
4024 return true;
4025 }
4026 false
4027 }
4028
4029 pub fn flash_polars_warning(&mut self) -> bool {
4032 if !self.bar_is_free() {
4033 return false;
4034 }
4035 match logging::next_polars_warning() {
4036 Some(warning) => {
4037 self.flash_note(format!("Polars: {warning}"));
4038 true
4039 }
4040 None => false,
4041 }
4042 }
4043
4044 pub fn flash_background_panic(&mut self) -> bool {
4048 if !self.bar_is_free() {
4049 return false;
4050 }
4051 match logging::take_unreported_panic() {
4052 Some(message) => {
4053 self.flash_note(message);
4054 true
4055 }
4056 None => false,
4057 }
4058 }
4059
4060 fn bar_is_free(&self) -> bool {
4063 !self.is_busy()
4064 && self.flash.is_none()
4065 && !self.error_modal.active
4066 && !self.confirmation_modal.active
4067 }
4068
4069 pub fn set_input_dropped(&mut self, dropped: bool) {
4071 self.input_dropped = dropped;
4072 }
4073
4074 pub fn hard_escape_while_busy(&self, key: &KeyEvent) -> bool {
4080 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
4081 let quit = ctrl && matches!(key.code, KeyCode::Char('q' | 'c'));
4082 let home = ctrl && key.code == KeyCode::Char('o');
4083 let cancel_analysis = self.analysis_modal.active
4084 && self.analysis_modal.computing.is_some()
4085 && key.code == KeyCode::Esc;
4086 let cancel_pivot = self.pivot_computing() && key.code == KeyCode::Esc;
4087 let leave_quality_evidence = self.quality_evidence_return.is_some()
4088 && self.input_mode == InputMode::Normal
4089 && key.code == KeyCode::Esc;
4090 let cancel_view = key.code == KeyCode::Esc && self.view_applying();
4091 let cancel_find = key.code == KeyCode::Esc && self.finding();
4092 let stop_sample =
4093 key.code == KeyCode::Esc && self.sample_drawing() && self.in_normal_table_view();
4094 let close_help = self.help.is_open()
4096 && matches!(key.code, KeyCode::Esc | KeyCode::F(1) | KeyCode::Char('?'));
4097 quit || home
4098 || close_help
4099 || cancel_analysis
4100 || cancel_pivot
4101 || cancel_view
4102 || cancel_find
4103 || stop_sample
4104 || leave_quality_evidence
4105 || self.confirmation_modal.active
4106 || self.input_mode == InputMode::Home
4107 }
4108
4109 fn column_cursor_key(key: &KeyEvent) -> Option<crate::widgets::column_paging::CursorMove> {
4113 use crate::widgets::column_paging::CursorMove;
4114 if key
4115 .modifiers
4116 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
4117 {
4118 return None;
4119 }
4120 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
4121 match key.code {
4122 KeyCode::Left if shift => Some(CursorMove::PageLeft),
4123 KeyCode::Right if shift => Some(CursorMove::PageRight),
4124 KeyCode::Left | KeyCode::Char('h') => Some(CursorMove::Left),
4125 KeyCode::Right | KeyCode::Char('l') => Some(CursorMove::Right),
4126 KeyCode::Char('{') => Some(CursorMove::First),
4127 KeyCode::Char('}') => Some(CursorMove::Last),
4128 _ => None,
4129 }
4130 }
4131
4132 pub fn key_acts_while_busy(&self, key: &KeyEvent) -> bool {
4147 if self.hard_escape_while_busy(key) || self.menu_takes(key) {
4148 return true;
4149 }
4150 if self.key_acts_while_sampling(key) {
4151 return true;
4152 }
4153 if !self.in_normal_table_view() {
4154 return false;
4155 }
4156 let step = match key.code {
4159 KeyCode::Down | KeyCode::Char('j') => Some(1),
4160 KeyCode::Up | KeyCode::Char('k') => Some(-1),
4161 _ => None,
4162 };
4163 if let Some(step) = step {
4164 return !self.busy
4165 && !self.loading.waits()
4166 && self
4167 .jobs
4168 .keys_held_only_by(|job| matches!(job, Job::Rows(_) | Job::OwedRows { .. }))
4169 && self
4170 .data_table_state
4171 .as_ref()
4172 .is_some_and(|s| !s.scroll_would_trigger_collect(step));
4173 }
4174 matches!(
4175 key.code,
4176 KeyCode::Char('q')
4177 | KeyCode::Char('Q')
4178 | KeyCode::Char('#')
4181 | KeyCode::Char('<')
4182 | KeyCode::Char('>')
4183 | KeyCode::Char('=')
4184 | KeyCode::Char('w')
4185 | KeyCode::Char(',')
4186 | KeyCode::Char('D')
4187 | KeyCode::Left
4188 | KeyCode::Right
4189 | KeyCode::Char('h')
4190 | KeyCode::Char('l')
4191 | KeyCode::Char('{')
4192 | KeyCode::Char('}')
4193 | KeyCode::F(1)
4194 | KeyCode::Char('?')
4195 )
4196 }
4197
4198 pub fn in_normal_table_view(&self) -> bool {
4201 self.input_mode == InputMode::Normal
4202 && !self.help.is_open()
4203 && !self.view_modal.active
4204 && !self.analysis_modal.active
4205 && !self.error_modal.active
4206 && !self.confirmation_modal.active
4207 && self.context_menu.is_none()
4208 }
4209
4210 fn render_drop_mark(&self, buf: &mut Buffer, ctx: &crate::render::context::RenderContext) {
4213 let Some(pointer::Drag::Move { column, over }) = self.pointer.drag() else {
4214 return;
4215 };
4216 let Some(state) = self.data_table_state.as_ref() else {
4217 return;
4218 };
4219 let Some((header, columns)) = state.drawn_header() else {
4220 return;
4221 };
4222 let order = state.get_column_order();
4223 let (Some(from), Some(to)) = (
4224 order.iter().position(|c| c == column),
4225 order.iter().position(|c| c == over),
4226 ) else {
4227 return;
4228 };
4229 let locked = state.locked_columns_count().min(order.len());
4231 if from == to || (from < locked) != (to < locked) {
4232 return;
4233 }
4234 let Some((left, right, _)) = columns.iter().find(|(_, _, name)| name == over) else {
4235 return;
4236 };
4237 let x = if to > from {
4238 *right
4239 } else {
4240 left.saturating_sub(1)
4241 };
4242 if !(header.x..header.right()).contains(&x) {
4243 return;
4244 }
4245 let g = crate::glyphs::get();
4246 for y in header.y..header.bottom() {
4247 let cell = &mut buf[(x, y)];
4248 cell.set_symbol(g.rule);
4249 cell.set_style(Style::default().fg(ctx.accent));
4250 }
4251 }
4252
4253 pub(crate) fn menu_showing(&self) -> bool {
4255 self.context_menu.is_some()
4256 && self.input_mode == InputMode::Normal
4257 && self.data_table_state.is_some()
4258 && !self.help.is_open()
4259 && !self.view_modal.active
4260 && !self.analysis_modal.active
4261 && !self.error_modal.active
4262 && !self.confirmation_modal.active
4263 }
4264
4265 pub(crate) fn menu_takes(&self, key: &KeyEvent) -> bool {
4268 self.menu_showing()
4269 && key.modifiers.is_empty()
4270 && matches!(
4271 key.code,
4272 KeyCode::Up
4273 | KeyCode::Down
4274 | KeyCode::Char('j')
4275 | KeyCode::Char('k')
4276 | KeyCode::Enter
4277 | KeyCode::Esc
4278 )
4279 }
4280
4281 pub fn open_context_menu(&mut self, at: ratatui::layout::Position) {
4283 let combine = self
4285 .data_table_state
4286 .as_ref()
4287 .and_then(|state| {
4288 let column = state.current_column()?;
4289 state.schema().get(column).cloned()
4290 })
4291 .is_some_and(|dtype| {
4292 matches!(dtype, DataType::String | DataType::Date | DataType::Time)
4293 });
4294 self.context_menu = Some(context_menu::ContextMenu::with(
4295 at,
4296 context_menu::column_items(combine),
4297 ));
4298 }
4299
4300 pub fn close_context_menu(&mut self) {
4302 self.context_menu = None;
4303 }
4304
4305 pub fn choose_from_menu(&mut self, i: usize) -> Option<AppEvent> {
4308 let menu = self.context_menu.take()?;
4309 match menu.chosen(i)? {
4310 context_menu::MenuKey::Run(key) => Some(AppEvent::Press(key)),
4311 context_menu::MenuKey::Do(action) => {
4312 self.menu_action(action);
4313 None
4314 }
4315 _ => None,
4316 }
4317 }
4318
4319 pub fn drop_column(&mut self, column: &str, onto: &str) -> Option<AppEvent> {
4323 let state = self.data_table_state.as_ref()?;
4324 let mut order = state.headers();
4325 let locked = state.locked_columns_count().min(order.len());
4326 let from = order.iter().position(|c| c == column)?;
4327 let to = order.iter().position(|c| c == onto)?;
4328 if from == to || (from < locked) != (to < locked) {
4329 return None;
4330 }
4331 self.flash = None;
4332 self.data_table_state.as_mut()?.set_current_column(column);
4333 let moving = order.remove(from);
4334 order.insert(to, moving);
4335 let applied = &mut self.sort_filter_modal.sort.applied_order;
4339 if let (Some(i), Some(j)) = (
4340 applied.iter().position(|c| c == column),
4341 applied.iter().position(|c| c == onto),
4342 ) {
4343 let moving = applied.remove(i);
4344 applied.insert(j, moving);
4345 }
4346 Some(AppEvent::ColumnOrder(order, locked))
4347 }
4348
4349 pub fn text_field_focused(&self) -> bool {
4352 match self.input_mode {
4353 InputMode::Editing => true,
4354 InputMode::Export => matches!(
4355 self.export_modal.focus,
4356 ExportFocus::PathInput | ExportFocus::CsvDelimiter
4357 ),
4358 InputMode::Copy => self.copy_modal.picker.is_some(),
4360 InputMode::Inspect => self.inspector_modal.finding,
4362 InputMode::GoToColumn => true,
4364 InputMode::PickFormat | InputMode::Retype => true,
4365 InputMode::Combine => self
4366 .combine
4367 .as_ref()
4368 .is_some_and(|c| c.picker.is_some() || c.focus == retype_modal::CombineField::Name),
4369 InputMode::PickTable => true,
4370 InputMode::Sample => self
4371 .sample_form
4372 .as_ref()
4373 .is_some_and(|form| form.field.is_text()),
4374 InputMode::SortFilter => self.sort_filter_modal.typing(),
4377 InputMode::PivotMelt => {
4378 self.pivot_melt_modal.picker.is_some()
4380 || self
4381 .pivot_melt_modal
4382 .is_text_row(self.pivot_melt_modal.focus)
4383 }
4384 InputMode::Chart => {
4385 if self.chart_export_modal.active {
4386 self.chart_export_modal
4387 .input(self.chart_export_modal.focus)
4388 .is_some()
4389 } else {
4390 self.chart_modal.picker.is_some()
4392 }
4393 }
4394 InputMode::Normal => {
4395 self.analysis_modal.sample_scope_typing()
4396 || self.analysis_modal.quality_expected_typing()
4397 || self.analysis_modal.intent_typing()
4398 || self.analysis_modal.export_typing()
4399 || (self.view_modal.active
4400 && self.view_modal.mode != ViewModalMode::List
4401 && matches!(
4402 self.view_modal.form_focus,
4403 FormFocus::Name
4404 | FormFocus::Description
4405 | FormFocus::ExactPath
4406 | FormFocus::RelativePath
4407 | FormFocus::PathPattern
4408 | FormFocus::FilenamePattern
4409 ))
4410 }
4411 InputMode::Home | InputMode::Info | InputMode::ValueCounts => false,
4412 InputMode::Hex => self
4414 .hex
4415 .as_ref()
4416 .is_some_and(|view| view.prompt.is_some() || view.picker.is_some()),
4417 }
4418 }
4419
4420 pub fn send_event(&mut self, event: AppEvent) -> Result<()> {
4421 self.events.send(event)?;
4422 Ok(())
4423 }
4424
4425 pub fn row_count_pending(&self) -> bool {
4436 self.len_count_inflight.is_some()
4444 || self.loading.awaiting_dataset()
4445 || self.reread_owed.is_some()
4453 || self
4454 .data_table_state
4455 .as_ref()
4456 .is_some_and(|state| state.counts_itself_later())
4457 }
4458
4459 pub fn something_is_spinning(&self) -> bool {
4461 self.is_busy()
4462 || (self.row_count_pending() && !self.awaiting_open_confirmation())
4463 || (self.analysis_modal.active && self.cancelled_analysis_running().is_some())
4465 || self.chart_preparing()
4466 || self.value_counts_computing()
4467 || (self.input_mode == InputMode::Home
4468 && (self.home.awaiting_listing().is_some()
4469 || self.home.sections_waiting()
4470 || !self.home.peeking.is_empty()))
4471 }
4472
4473 fn dataset_is_still_reading_its_footers(&self) -> bool {
4474 self.data_table_state
4475 .as_ref()
4476 .is_some_and(|state| state.footers_pending().is_some())
4477 }
4478
4479 fn begin_frame(&mut self) {
4485 if self.indexing_paused && self.input_mode != InputMode::Home {
4487 self.index_lines();
4488 }
4489 self.footers_this_frame = self.footer_progress().reading();
4490 self.listed_this_frame = self.footer_progress().listed();
4491 self.pointer.forget_drawn();
4493 if let Some(state) = self.data_table_state.as_mut() {
4494 state.forget_drawn();
4495 }
4496 }
4497
4498 pub fn footer_progress(&self) -> &Arc<crate::schema_union::FooterProgress> {
4503 self.loading.progress().unwrap_or(&self.footer_progress)
4504 }
4505
4506 pub fn exit_sweep(&self) -> ExitSweep {
4511 ExitSweep(self.loading.unfinished().clone())
4512 }
4513
4514 pub(crate) fn awaiting_dataset(&self) -> bool {
4518 self.loading.awaiting_dataset()
4519 }
4520
4521 pub(crate) fn load_shown(&self) -> Option<(&str, u16, Option<&Path>, u64)> {
4524 self.loading.current().map(|load| {
4525 let (phase, percent) = load.phase().label();
4526 (phase, percent, load.path(), load.size())
4527 })
4528 }
4529
4530 pub(crate) fn loading_phase<'a>(&self, phase: &'a str) -> std::borrow::Cow<'a, str> {
4537 match self.footers_this_frame {
4538 Some((read, total)) => std::borrow::Cow::Owned(format!(
4539 "Reading footers: {} of {}",
4540 crate::numfmt::group_chrome(read),
4541 crate::numfmt::group_chrome(total)
4542 )),
4543 None => match self.listed_this_frame {
4545 Some(listed) => std::borrow::Cow::Owned(format!(
4546 "Listing files: {}",
4547 crate::numfmt::group_chrome(listed)
4548 )),
4549 None => std::borrow::Cow::Borrowed(phase),
4550 },
4551 }
4552 }
4553
4554 pub fn set_loading_phase(&mut self, phase: impl Into<String>, progress_percent: u16) {
4559 self.announce_open(false, phase.into(), progress_percent);
4560 }
4561
4562 fn announce_open(&mut self, from_home: bool, phase: String, percent: u16) {
4565 self.make_way_for_an_open();
4566 self.loading.announce(from_home, phase, percent);
4567 }
4568
4569 pub(crate) fn name_what_is_loading(&mut self, path: PathBuf) {
4571 self.loading.name(path);
4572 }
4573
4574 fn make_way_for_an_open(&mut self) {
4577 if let Some(retired) = self.loading.make_way() {
4578 self.put_down_load(retired);
4579 }
4580 }
4581
4582 fn begin_new_dataset(&mut self) {
4585 self.make_way_for_an_open();
4586 self.home_previews.drop_prepared();
4588 self.reset_chart_state();
4589 self.jobs.advance();
4590 self.footer_progress.cancel();
4598 *self
4599 .pending_footers_result
4600 .lock()
4601 .unwrap_or_else(|e| e.into_inner()) = None;
4602 }
4603
4604 fn put_down_load(&mut self, retired: loading::Retired) {
4608 let id = retired.id;
4609 let lines = self.jobs.quiet(|job| job.load() == Some(id));
4610 self.jobs.supersede(|job| job.load() == Some(id));
4611 if self
4612 .status_message
4613 .as_ref()
4614 .is_some_and(|status| lines.contains(status))
4615 {
4616 self.status_message = None;
4617 }
4618 if retired.asking {
4619 self.confirmation_modal.hide();
4620 }
4621 }
4622
4623 fn run_load_step(&mut self, step: loading::Step) -> Option<AppEvent> {
4625 use loading::Step;
4626 let load = self.loading.id();
4627 match step {
4628 Step::Nothing => None,
4629 Step::Crash(message) => Some(AppEvent::Crash(message)),
4630 Step::Failed(failed) => {
4631 self.load_failed(failed);
4632 None
4633 }
4634 Step::Tables(tables) => {
4635 self.land_on_tables(tables);
4636 None
4637 }
4638 Step::Hex(hex) => {
4639 self.land_on_hex(hex);
4640 None
4641 }
4642 Step::Install(loaded) => {
4643 if self.install_dataset(*loaded) {
4646 return None;
4647 }
4648 #[cfg(test)]
4649 {
4650 self.first_rows_asked += 1;
4651 }
4652 if !self.spawn_async_collect(Self::LOADING_BUFFER) {
4653 if self.status_message.as_deref() == Some(Self::LOADING_BUFFER) {
4655 self.status_message = None;
4656 }
4657 self.first_rows_settled();
4658 }
4659 None
4660 }
4661 #[cfg(any(feature = "http", feature = "cloud"))]
4662 Step::Ask(pending) => {
4663 self.confirmation_modal
4667 .show(Self::download_confirmation_message(
4668 &pending,
4669 self.loading.download_note(),
4670 ));
4671 None
4672 }
4673 Step::AskRead(read) => {
4674 self.loading.hold_while_asking(self.jobs.hold());
4676 self.confirmation_modal
4677 .show(Self::in_memory_confirmation_message(&read));
4678 None
4679 }
4680 step => {
4681 let load = load.expect("a step that runs work belongs to the open in flight");
4682 self.spawn_load_phase(load, step);
4683 None
4684 }
4685 }
4686 }
4687
4688 fn remember_a_downloads_shape(&mut self) {
4692 if self.shape_remembered == Some(self.dataset_generation) {
4693 return;
4694 }
4695 let Some(url) = self.path.clone().filter(|p| source::is_remote_url(p)) else {
4696 return;
4697 };
4698 let Some(state) = self.data_table_state.as_ref().filter(|s| s.fetched()) else {
4699 return;
4700 };
4701 let Some(rows) = state.num_rows_if_valid().filter(|_| !state.changes_rows()) else {
4702 return;
4703 };
4704 self.shape_remembered = Some(self.dataset_generation);
4705 let columns: Vec<String> = state
4706 .source_schema()
4707 .iter_names()
4708 .map(|name| name.to_string())
4709 .collect();
4710 let facts = crate::cache::DatasetFacts {
4711 mtime: std::time::SystemTime::now()
4712 .duration_since(std::time::UNIX_EPOCH)
4713 .map(|d| d.as_secs())
4714 .unwrap_or_default(),
4715 size: 0,
4716 rows: Some(rows),
4717 cols: Some(columns.len()),
4718 cols_sampled: false,
4719 columns,
4720 kind: Some(discover::EntryKind::File),
4721 classified_by: discover::CLASSIFIER_VERSION,
4722 cost: Default::default(),
4723 holds: Default::default(),
4724 };
4725 let cache = self.cache.clone();
4727 self.cache_writes
4728 .spawn(move || cache.record_dataset_facts(&[(url, facts)]));
4729 }
4730
4731 fn first_rows_settled(&mut self) {
4733 self.loading.first_rows_settled();
4734 }
4735
4736 fn reread_after_the_footers_joined(&mut self) {
4744 self.reread_owed = None;
4747 if self.end_when_the_footers_land.take() == Some(self.dataset_generation) {
4752 self.status_message = None;
4753 if let Some(next) = self.jump_key(AppEvent::DoScrollEnd) {
4754 let _ = self.events.send(next);
4757 return;
4758 }
4759 }
4764 self.spawn_async_collect(Self::LOADING_BUFFER);
4765 }
4766
4767 fn collect_when_the_work_allows(&mut self) {
4774 let Some(&Job::OwedRows { dataset, .. }) = self.jobs.owed(Self::owed_rows) else {
4775 return;
4776 };
4777 if dataset != self.dataset_generation {
4778 self.jobs.take_owed(Self::owed_rows);
4782 return;
4783 }
4784 if self.work_a_bump_would_strand() {
4785 return;
4786 }
4787 let Some(Job::OwedRows { status, .. }) = self.jobs.take_owed(Self::owed_rows) else {
4788 return;
4789 };
4790 if !self.spawn_async_collect(&status) {
4791 self.busy = false;
4792 self.status_message = None;
4793 self.first_rows_settled();
4797 }
4798 }
4799
4800 fn reread_when_the_work_allows(&mut self) {
4809 let Some(generation) = self.reread_owed else {
4810 return;
4811 };
4812 if generation != self.dataset_generation {
4813 self.reread_owed = None;
4815 return;
4816 }
4817 if self.work_the_join_would_cancel() {
4818 return;
4819 }
4820 self.reread_after_the_footers_joined();
4821 }
4822
4823 fn retire_the_end_that_was_waiting(&mut self) {
4828 self.end_after_count = None;
4829 self.take_down_the_counting_status();
4830 }
4831
4832 fn take_down_the_counting_status(&mut self) {
4837 if matches!(
4838 self.status_message.as_deref(),
4839 Some(Self::COUNTING_FOR_END | Self::INDEXING_FOR_ROW)
4840 ) {
4841 self.status_message = None;
4842 }
4843 }
4844
4845 const INDEXING_FOR_ROW: &'static str = "Reading lines to find the row...";
4847
4848 const COUNTING_FOR_END: &'static str = "Counting rows to find the end...";
4852
4853 const LOOKING: &'static str = "Looking...";
4856
4857 pub const LOOKING_AT_A_DIRECTORY: &'static str = "Looking at the directory";
4860
4861 pub const LOADING_BUFFER: &'static str = "Loading buffer...";
4863
4864 const APPLYING_VIEW: &'static str = "Applying view...";
4866
4867 const READING_GROUP: &'static str = "Reading the group...";
4869
4870 const READING_FIELDS: &'static str = "Reading fields...";
4872
4873 const FIELD_COPY_INLINE_BYTES: usize = 1024 * 1024;
4876 const JSON_COPY_MAX_BYTES: usize = 64 * 1024 * 1024;
4878 const READING_JSON: &'static str = "Reading JSON...";
4880
4881 const COMPUTING_PIVOT: &'static str = "Computing pivot...";
4883
4884 const A_FETCH_WORTH_SAYING: std::time::Duration = std::time::Duration::from_millis(300);
4888
4889 fn load_ahead(&mut self) {
4896 if self.is_busy()
4900 || self.jobs.owed(Self::owed_rows).is_some()
4901 || self.rows_in_flight().is_some()
4902 || self.work_a_bump_would_strand()
4903 {
4904 return;
4905 }
4906 let Some(state) = self.data_table_state.as_ref() else {
4907 return;
4908 };
4909 let position = state.buffer_position();
4913 if !state.wants_to_load_ahead() || self.loaded_ahead_from == Some(position) {
4914 return;
4915 }
4916 self.loaded_ahead_from = Some(position);
4917 self.spawn_collect(None);
4918 }
4919
4920 fn fetch_too_young_to_mention(&self) -> bool {
4922 self.status_message.as_deref() == Some(Self::LOADING_BUFFER)
4923 && self
4924 .rows_in_flight()
4925 .is_some_and(|inflight| inflight.began.elapsed() < Self::A_FETCH_WORTH_SAYING)
4926 }
4927
4928 fn work_the_join_would_cancel(&self) -> bool {
4940 self.work_a_bump_would_strand() || self.chart_preparing()
4941 }
4942
4943 fn join_held_footers(&mut self) -> bool {
4954 let Some((generation, _)) = self.footers_held.as_ref() else {
4955 return false;
4956 };
4957 if *generation != self.dataset_generation {
4958 self.footers_held = None;
4960 return false;
4961 }
4962 if self.data_table_state.is_none() || self.work_the_join_would_cancel() {
4963 return false;
4964 }
4965 let Some((generation, found)) = self.footers_held.take() else {
4966 return false;
4967 };
4968 let state = self
4969 .data_table_state
4970 .as_mut()
4971 .expect("checked just above, and nothing since takes it");
4972 match state.join_dataset_schema(found) {
4975 Ok(()) => true,
4976 Err(found) => {
4977 self.footers_held = Some((generation, *found));
4978 false
4979 }
4980 }
4981 }
4982
4983 fn start_pending_footers(&mut self) {
4990 let Some(join) = self
4991 .data_table_state
4992 .as_ref()
4993 .and_then(|state| state.footers_pending())
4994 else {
4995 return;
4996 };
4997 let generation = self.dataset_generation;
4998 let slot = self.pending_footers_result.clone();
4999 let tx = self.events.clone();
5000 let progress = self.footer_progress.clone();
5001 self.runtime.spawn_blocking(move || {
5002 let found = logging::catch_panic(|| join(&progress)).unwrap_or(None);
5007 if !Self::record_footers(&slot, generation, found) {
5008 return;
5009 }
5010 let _ = tx.send(AppEvent::BackgroundFootersJoined { generation });
5011 });
5012 }
5013
5014 fn start_indexing(&mut self) {
5019 self.end_when_indexed = None;
5020 self.goto_when_indexed = None;
5021 self.index_lines();
5022 }
5023
5024 fn index_lines(&mut self) {
5028 use std::sync::atomic::Ordering;
5029 self.indexing_stop.store(true, Ordering::Relaxed);
5030 self.indexing_paused = false;
5031 let lines = self
5032 .data_table_state
5033 .as_ref()
5034 .and_then(|state| state.lines_to_index().cloned());
5035 if let Some(old) = self.indexing_lines.take()
5036 && lines.as_ref().is_none_or(|lines| !Arc::ptr_eq(lines, &old))
5037 {
5038 old.stop_indexing();
5039 }
5040 let Some(lines) = lines.filter(|lines| lines.resume_indexing()) else {
5041 return;
5042 };
5043 self.indexing_lines = Some(lines.clone());
5044 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
5045 self.indexing_stop = stop.clone();
5046 let generation = self.dataset_generation;
5047 let tx = self.events.clone();
5048 let waiting = lines.clone();
5049 let spawned = std::thread::Builder::new()
5050 .name("datui-index".to_string())
5051 .spawn(move || {
5052 loop {
5053 if stop.load(Ordering::Relaxed) {
5056 return;
5057 }
5058 let done =
5061 logging::catch_panic(|| lines.index_more(INDEX_STEP)).unwrap_or(true);
5062 if done {
5063 lines.stop_indexing();
5064 let rows = lines.rows();
5065 let _ = tx.send(AppEvent::LinesIndexed { generation, rows });
5066 return;
5067 }
5068 }
5069 });
5070 if spawned.is_err() {
5073 waiting.stop_indexing();
5074 self.indexing_lines = None;
5075 if let Some(state) = self.data_table_state.as_mut() {
5076 state.lines_indexed(waiting.rows());
5077 }
5078 }
5079 }
5080
5081 fn pause_indexing(&mut self) {
5084 if self.indexing_lines.is_some() {
5085 self.indexing_stop
5086 .store(true, std::sync::atomic::Ordering::Relaxed);
5087 self.indexing_paused = true;
5088 }
5089 }
5090
5091 fn lines_indexed(&mut self, generation: u64, rows: usize) {
5094 if generation != self.dataset_generation {
5095 return;
5096 }
5097 let Some(state) = self.data_table_state.as_mut() else {
5098 return;
5099 };
5100 self.indexing_lines = None;
5101 if !state.lines_indexed(rows) {
5102 if let Some(held) = self.quality_evidence_return.as_mut() {
5105 held.lines_indexed(rows);
5106 }
5107 return;
5108 }
5109 if let Some((goto, row)) = self.goto_when_indexed.take()
5110 && goto == generation
5111 {
5112 self.take_down_the_counting_status();
5113 let _ = self.events.send(AppEvent::GoToLine(row));
5114 }
5115 if self.end_when_indexed.take() == Some(generation) {
5116 self.take_down_the_counting_status();
5117 if let Some(next) = self.jump_key(AppEvent::DoScrollEnd) {
5118 let _ = self.events.send(next);
5119 return;
5120 }
5121 }
5122 if self.in_normal_table_view() && !self.loading.awaiting_dataset() {
5125 self.spawn_collect(None);
5126 }
5127 }
5128
5129 fn count_held_at_estimate(&self, state: &DataTableState) -> bool {
5132 let limit = self.app_config.read.exact_count_files;
5133 limit > 0
5134 && state.files_to_count().is_some_and(|files| files > limit)
5135 && self.exact_count_asked != Some(self.dataset_generation)
5136 && state.row_estimate(None).is_some()
5137 }
5138
5139 pub(crate) fn row_estimate(&self) -> Option<crate::schema_union::RowEstimate> {
5142 self.data_table_state
5143 .as_ref()?
5144 .row_estimate(self.footer_progress.estimate())
5145 }
5146
5147 pub(crate) fn footers_counted(&self) -> Option<(usize, usize)> {
5150 self.len_count_inflight?;
5151 self.count_progress
5152 .reading()
5153 .filter(|_| !self.count_progress.is_cancelled())
5154 }
5155
5156 pub(crate) fn count_exactly(&mut self) {
5159 let Some(state) = self.data_table_state.as_ref() else {
5160 return;
5161 };
5162 if state.is_num_rows_valid() {
5163 return;
5164 }
5165 let generation = state.len_generation();
5166 self.exact_count_asked = Some(self.dataset_generation);
5167 if state.counts_itself_later() {
5170 return;
5171 }
5172 if self.len_count_failed == Some(generation) {
5174 self.len_count_failed = None;
5175 }
5176 if self.len_count_inflight == Some(generation) && self.count_progress.is_cancelled() {
5178 self.count_after_stop = Some(generation);
5179 return;
5180 }
5181 if self.len_count_inflight != Some(generation) {
5182 self.len_count_inflight = Some(generation);
5183 let job = LenCount::for_state(state);
5184 self.spawn_count(job);
5185 }
5186 }
5187
5188 fn stop_count(&mut self) {
5190 self.count_progress.cancel();
5191 }
5192
5193 fn record_footers(
5203 slot: &std::sync::Mutex<FootersReported>,
5204 generation: u64,
5205 found: Option<crate::widgets::datatable::FootersFound>,
5206 ) -> bool {
5207 let mut slot = slot.lock().unwrap_or_else(|e| e.into_inner());
5208 if slot.as_ref().is_some_and(|(held, _)| *held > generation) {
5209 return false;
5210 }
5211 *slot = Some((generation, found));
5212 true
5213 }
5214
5215 fn install_dataset(&mut self, loaded: loading::Loaded) -> bool {
5222 let loading::Loaded {
5223 state,
5224 path,
5225 options,
5226 debug_label,
5227 paths,
5228 recent,
5229 from_home,
5230 footers,
5231 } = loaded;
5232 let options = &options;
5233 self.end_when_the_footers_land = None;
5235 self.end_after_count = None;
5240 self.dataset_generation = self.dataset_generation.wrapping_add(1);
5244 self.quality_cache.clear();
5245 self.quality_samples.clear();
5246 self.quality_released.clear();
5247 self.quality_copies.clear();
5250 self.quality_copy_released = None;
5251 self.quality_copy_unusable = None;
5252 self.quality_evidence_return = None;
5253 self.quality_evidence_label = None;
5254 self.analysis_modal.data_quality_findings = quality_report::FindingsView::default();
5256 self.analysis_modal.data_quality_evidence_read = None;
5257 self.query_running = None;
5260 self.jobs.supersede(|job| matches!(job, Job::ViewPivot(_)));
5261 self.put_down_sample_draw();
5263 self.sample_paths.clear();
5264 self.reset_chart_state();
5266 self.debug.schema_load = debug_label;
5267 if from_home {
5270 self.opened_from_home = true;
5271 }
5272 self.opened = paths.map(|paths| {
5276 let options = OpenOptions {
5277 format_read: None,
5278 sqlite: None,
5279 tail: None,
5281 prepared: None,
5282 ..options.clone()
5283 };
5284 (paths, options)
5285 });
5286 if let Some(path) = recent {
5289 let cache = self.cache.clone();
5293 self.cache_writes.spawn(move || {
5294 cache.push_recent(&path);
5295 });
5296 }
5297 self.forget_the_rows_read();
5298 self.file_facts = None;
5299 let shown = home::catalogs(&self.app_config);
5300 self.codebook = path.as_deref().and_then(|p| home::codebook_for(&shown, p));
5301 self.catalog_entry = path
5302 .as_deref()
5303 .and_then(|p| home::catalog_entry_for(&shown, p));
5304 self.footer_progress.cancel();
5307 self.footer_progress = footers;
5308 self.data_table_state = Some(state);
5309 if options.follow
5311 && let Some(state) = self.data_table_state.as_mut()
5312 {
5313 match options.tail.as_deref() {
5314 Some(tail) => {
5315 let follow = crate::follow::Follow::start(
5316 tail.clone(),
5317 self.app_config.read.follow_interval.duration(),
5318 self.events.clone(),
5319 options.spool.clone(),
5320 );
5321 state.start_following(if options.pipe {
5322 follow.as_pipe()
5323 } else {
5324 follow
5325 });
5326 state.follow_to(tail.rows(), false);
5328 }
5329 None => self.flash_note(
5331 "Only text and Arrow streams are followed: this shows what had arrived, and recording goes on"
5332 .to_string(),
5333 ),
5334 }
5335 }
5336 self.retire_a_count_the_rows_answered();
5338 self.path = path.clone();
5339 self.open_info_documentation();
5341 if let Some(ref p) = path {
5342 let read_as = self
5343 .data_table_state
5344 .as_ref()
5345 .and_then(DataTableState::read_as);
5346 self.original_file_format = Self::export_format_for(p, read_as.or(options.format));
5347 self.original_file_delimiter = Some(options.separator_or(b','));
5351 } else {
5352 self.original_file_format = None;
5353 self.original_file_delimiter = None;
5354 }
5355 if self.info_modal.active {
5357 self.read_file_facts();
5358 self.count_unfit();
5359 }
5360 self.start_pending_footers();
5363 self.start_indexing();
5364 if options.row_numbers_auto
5366 && let Some(state) = self.data_table_state.as_mut()
5367 && state.numbered_by_default()
5368 {
5369 state.set_row_numbers(true);
5370 }
5371 self.sort_filter_modal = SortFilterModal::new();
5372 self.pivot_melt_modal = PivotMeltModal::new();
5373 self.status_message = Some(Self::LOADING_BUFFER.to_string());
5374
5375 self.active_view_id = None;
5381 let (view, reason) = match self.startup_view.take() {
5382 Some(name) => match self.view_manager.get_view_by_name(&name).cloned() {
5383 Some(view) => (Some(view), None),
5384 None => {
5385 self.error_modal.show(format!("No view named \"{name}\""));
5386 (None, None)
5387 }
5388 },
5389 None if self.app_config.views.auto_apply => self
5390 .view_dataset()
5391 .zip(self.data_table_state.as_ref())
5392 .and_then(|(dataset, state)| {
5393 self.view_manager
5394 .get_most_relevant(dataset, state.source_schema())
5395 })
5396 .map_or((None, None), |(view, reason)| (Some(view), Some(reason))),
5397 None => (None, None),
5398 };
5399 let Some(view) = view else {
5400 return false;
5401 };
5402 let applied = match reason {
5403 Some(why) => self.apply_matched_view(&view, why),
5405 None => self.apply_view(&view),
5406 };
5407 match applied {
5408 Ok(()) => true,
5410 Err(e) => {
5411 self.error_modal
5412 .show(format!("Error applying view \"{}\": {e}", view.name));
5413 false
5414 }
5415 }
5416 }
5417
5418 fn flash_view_applied(&mut self, name: &str, why: view::MatchReason) {
5420 self.flash_note(format!("View \"{name}\" applied: {}", why.as_str()));
5421 }
5422
5423 pub fn spawn_async_collect(&mut self, status: &str) -> bool {
5436 self.spawn_collect(Some(status))
5437 }
5438
5439 fn spawn_collect(&mut self, status: Option<&str>) -> bool {
5442 let held = self
5443 .data_table_state
5444 .as_ref()
5445 .is_some_and(|state| self.count_held_at_estimate(state));
5446 let Some(state) = self.data_table_state.as_mut() else {
5447 return false;
5448 };
5449
5450 let mut count = None;
5459 let generation = state.len_generation();
5460 if !state.is_num_rows_valid()
5461 && self.len_count_inflight != Some(generation)
5462 && !state.counts_itself_later()
5469 && self.len_count_failed != Some(generation)
5471 && !held
5473 {
5474 self.len_count_inflight = Some(generation);
5475 count = Some(LenCount::for_state(state));
5476 }
5477 let footers = count.take_if(|job| job.reads_footers());
5478 if count.take_if(|_| !state.is_remote_source()).is_some() {
5479 self.count_after_paint = Some(generation);
5480 }
5481 if let Some(job) = footers {
5482 self.spawn_count(job);
5483 }
5484
5485 let a_bump_would_strand = self.work_a_bump_would_strand();
5487 let inflight = self.rows_in_flight();
5488
5489 let Some(state) = self.data_table_state.as_mut() else {
5492 return false;
5493 };
5494 let covered = inflight.is_some_and(|inflight| inflight.covers(state));
5495 if covered
5498 && let Some(status) = status
5499 && !self.jobs.waited_on(Self::reading_rows)
5500 {
5501 self.jobs.wait_on(Self::reading_rows, status);
5502 self.busy = false;
5503 self.status_message = Some(status.to_string());
5504 }
5505 let request = (!covered).then(|| state.prepare_async_collect(None));
5506 let Some(Some(request)) = request else {
5507 if let Some(job) = count {
5509 self.spawn_count(job);
5510 }
5511 return covered;
5512 };
5513 if a_bump_would_strand {
5525 if count.is_some() {
5531 self.len_count_inflight = None;
5532 }
5533 let Some(status) = status else {
5535 return false;
5536 };
5537 self.jobs.take_owed(Self::owed_rows);
5539 let owed = Job::OwedRows {
5540 dataset: self.dataset_generation,
5541 status: status.to_string(),
5542 };
5543 self.jobs.owe(owed, Some(status));
5544 self.busy = false;
5545 self.status_message = Some(status.to_string());
5546 return true;
5547 }
5548 self.jobs.advance();
5549 self.reads.pages += 1;
5550 let inflight = InflightCollect {
5551 began: std::time::Instant::now(),
5552 files: state.files_a_page_reads(
5553 request.buffer_start,
5554 request.buffer_end.saturating_sub(request.buffer_start),
5555 ),
5556 dataset: state.len_generation(),
5557 columns: InflightCollect::columns_of(state),
5558 start: request.buffer_start,
5559 end: request.buffer_end,
5560 };
5561 let count = count.map(|job| OwedCount::new(job, self.events.clone()));
5564 self.spawn_job(Job::Rows(inflight), status, move |_| {
5565 let plan = request.plan;
5566 Ok(
5569 match crate::statistics::collect_lazy(request.lf, request.polars_streaming) {
5570 Ok(df) => {
5571 let returned = df.height();
5572 let requested = request.buffer_end - request.buffer_start;
5573 let start = request.buffer_start;
5574 Answer::Rows(plan.fit(df)).then(move || {
5578 if let Some(count) = count {
5579 count.answer(|job| job.after_collect(start, returned, requested));
5580 }
5581 })
5582 }
5583 Err(e) => Answer::RowsFailed {
5587 message: crate::error_display::user_message_from_polars(&e),
5588 conversion: crate::error_display::conversion_failure(&e).map(Box::new),
5589 }
5590 .then(move || drop(count)),
5591 },
5592 )
5593 });
5594 true
5595 }
5596
5597 fn spawn_count(&mut self, job: LenCount) {
5600 #[cfg(test)]
5601 self.counts_spawned.set(self.counts_spawned.get() + 1);
5602 self.count_progress = job.progress.clone();
5603 let count = OwedCount::new(job, self.events.clone());
5604 self.runtime
5605 .spawn_blocking(move || count.answer(LenCount::run));
5606 }
5607
5608 fn waited_on_rows_pending(&self, generation: u64) -> bool {
5612 self.loading.awaiting_dataset()
5613 || self.jobs.owed(Self::owed_rows).is_some()
5614 || (self.rows_waited_on()
5615 && self
5616 .rows_in_flight()
5617 .is_some_and(|inflight| inflight.dataset == generation))
5618 }
5619
5620 pub fn count_waits_for_a_frame(&self) -> bool {
5624 self.count_after_paint
5625 .is_some_and(|generation| !self.waited_on_rows_pending(generation))
5626 }
5627
5628 pub fn frame_painted(&mut self) {
5632 self.pointer.painted();
5633 let Some(generation) = self.count_after_paint else {
5634 return;
5635 };
5636 if self.waited_on_rows_pending(generation) {
5637 return;
5638 }
5639 self.count_after_paint = None;
5640 let wanted = self
5641 .data_table_state
5642 .as_ref()
5643 .filter(|state| state.len_generation() == generation && !state.is_num_rows_valid());
5644 match wanted {
5645 Some(state) => {
5646 self.len_count_inflight = Some(generation);
5647 self.spawn_count(LenCount::for_state(state));
5648 }
5649 None => {
5650 if self.len_count_inflight == Some(generation) {
5651 self.len_count_inflight = None;
5652 }
5653 }
5654 }
5655 }
5656
5657 fn retire_a_count_the_rows_answered(&mut self) {
5661 let Some(generation) = self.count_after_paint else {
5662 return;
5663 };
5664 let answered = self
5665 .data_table_state
5666 .as_ref()
5667 .is_none_or(|state| state.len_generation() != generation || state.is_num_rows_valid());
5668 if answered {
5669 self.count_after_paint = None;
5670 if self.len_count_inflight == Some(generation) {
5671 self.len_count_inflight = None;
5672 }
5673 }
5674 }
5675
5676 fn spawn_job<F, R>(&mut self, job: Job, status: Option<&str>, work: F) -> Ticket
5684 where
5685 F: FnOnce(&jobs::Worker) -> std::result::Result<R, String> + Send + 'static,
5686 R: Into<jobs::Answered>,
5687 {
5688 let started = self.start_job(job, status);
5689 let ticket = started.ticket();
5690 started.run(&self.runtime, work);
5691 ticket
5692 }
5693
5694 fn start_job(&mut self, job: Job, status: Option<&str>) -> jobs::Started {
5697 if let Some(status) = status {
5698 self.busy = false;
5701 self.status_message = Some(status.to_string());
5702 }
5703 self.jobs.start(job, status)
5704 }
5705
5706 fn owed_rows(job: &Job) -> bool {
5707 matches!(job, Job::OwedRows { .. })
5708 }
5709
5710 fn reading_rows(job: &Job) -> bool {
5711 matches!(job, Job::Rows(_))
5712 }
5713
5714 fn rows_in_flight(&self) -> Option<InflightCollect> {
5717 match self.jobs.current(Self::reading_rows) {
5718 Some((_, Job::Rows(inflight))) => Some(*inflight),
5719 _ => None,
5720 }
5721 }
5722
5723 fn rows_waited_on(&self) -> bool {
5725 self.jobs.waited_on(Self::reading_rows)
5726 }
5727
5728 fn forget_the_rows_read(&mut self) {
5731 self.jobs
5732 .supersede(|job| Self::reading_rows(job) || Self::owed_rows(job));
5733 }
5734
5735 pub(crate) fn hold_the_generation(&self) -> jobs::Hold {
5738 self.jobs.hold()
5739 }
5740
5741 #[cfg(test)]
5744 pub(crate) fn job_for_tests(&mut self, job: Job, status: Option<&str>) -> jobs::Started {
5745 self.start_job(job, status)
5746 }
5747
5748 #[cfg(test)]
5750 pub(crate) fn nothing_loading(&self) -> bool {
5751 self.loading.current().is_none() && self.export_progress.is_none()
5752 }
5753
5754 #[cfg(test)]
5757 pub(crate) fn loading_for_tests(
5758 &mut self,
5759 path: Option<PathBuf>,
5760 size: u64,
5761 phase: &str,
5762 percent: u16,
5763 ) {
5764 self.announce_open(false, phase.to_string(), percent);
5765 if let Some(path) = path {
5766 self.loading.name(path);
5767 }
5768 self.loading.size_for_tests(size);
5769 }
5770
5771 #[cfg(test)]
5774 pub(crate) fn open_for_tests(&mut self, path: &str) -> loading::LoadId {
5775 self.make_way_for_an_open();
5776 let _ = self.loading.open(loading::OpenRequest {
5777 paths: vec![PathBuf::from(path)],
5778 options: OpenOptions::default(),
5779 size: 0,
5780 recent: None,
5781 shown: None,
5782 warn_in_memory_above: None,
5783 });
5784 self.loading.id().expect("an open was begun")
5785 }
5786
5787 #[cfg(test)]
5791 pub(crate) fn install_for_tests(
5792 &mut self,
5793 state: DataTableState,
5794 path: Option<PathBuf>,
5795 options: &OpenOptions,
5796 debug_label: Option<String>,
5797 ) -> bool {
5798 self.make_way_for_an_open();
5799 let _ = self
5800 .loading
5801 .open_frame(LazyFrame::default(), options.clone());
5802 let load = self.loading.id().expect("an open was begun");
5803 let loading::Step::Install(loaded) = self.loading.answered(
5804 load,
5805 loading::LoadAnswer::SchemaRead {
5806 state: Box::new(state),
5807 path,
5808 options: options.clone(),
5809 debug_label,
5810 },
5811 #[cfg(any(feature = "http", feature = "cloud"))]
5812 &self.jobs,
5813 ) else {
5814 unreachable!("a schema read installs");
5815 };
5816 let view = self.install_dataset(*loaded);
5817 self.first_rows_settled();
5818 view
5819 }
5820
5821 #[cfg(test)]
5824 pub(crate) fn owe_rows_for_tests(&mut self, status: &str) {
5825 let owed = Job::OwedRows {
5826 dataset: self.dataset_generation,
5827 status: status.to_string(),
5828 };
5829 self.jobs.owe(owed, Some(status));
5830 }
5831
5832 #[cfg(test)]
5834 pub(crate) fn rows_owed(&self) -> bool {
5835 self.jobs.owed(Self::owed_rows).is_some()
5836 }
5837
5838 #[cfg(test)]
5841 pub(crate) fn answer_for_tests(&mut self, job: Job, answer: Answer) -> Option<AppEvent> {
5842 let started = self.jobs.start(job, None);
5843 let ticket = started.ticket();
5844 started.end(Outcome::answered(answer));
5845 self.job_ended(ticket)
5846 }
5847
5848 fn work_a_bump_would_strand(&self) -> bool {
5852 self.jobs.would_strand()
5853 }
5854
5855 fn jump_key(&mut self, jump: AppEvent) -> Option<AppEvent> {
5864 if matches!(jump, AppEvent::DoScrollEnd)
5879 && let Some(state) = self.data_table_state.as_ref()
5880 && state.indexing().is_some()
5881 {
5882 self.end_when_indexed = Some(self.dataset_generation);
5883 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
5884 return None;
5885 }
5886 if matches!(jump, AppEvent::DoScrollEnd)
5887 && let Some(state) = self.data_table_state.as_ref()
5888 && state.footers_pending().is_some()
5889 && state.scan_is_the_root()
5890 && !state.is_num_rows_valid()
5891 {
5892 self.end_when_the_footers_land = Some(self.dataset_generation);
5893 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
5894 return None;
5895 }
5896 if matches!(jump, AppEvent::DoScrollEnd)
5900 && let Some(state) = self.data_table_state.as_ref()
5901 && !state.is_num_rows_valid()
5902 {
5903 let generation = state.len_generation();
5904 self.end_after_count = Some(generation);
5905 self.status_message = Some(Self::COUNTING_FOR_END.to_string());
5906 let held = self.count_after_paint == Some(generation);
5907 if held {
5908 self.count_after_paint = None;
5909 }
5910 if held || self.len_count_inflight != Some(generation) {
5911 self.len_count_inflight = Some(generation);
5912 self.spawn_count(LenCount::for_state(state));
5913 }
5914 return None;
5915 }
5916 let state = self.data_table_state.as_mut()?;
5917 let (already_there, settle): (bool, fn(&mut DataTableState) -> bool) = match jump {
5918 AppEvent::DoScrollHome => (state.start_row() == 0, DataTableState::scroll_to_start),
5919 _ => (state.at_end(), DataTableState::scroll_to_end),
5920 };
5921 if already_there {
5922 settle(state);
5923 return None;
5924 }
5925 self.busy = true;
5926 Some(jump)
5927 }
5928
5929 fn handle_scroll<F>(&mut self, scroll: F) -> Option<AppEvent>
5930 where
5931 F: FnOnce(&mut crate::widgets::datatable::DataTableState) -> bool,
5932 {
5933 let needs = self.data_table_state.as_mut().is_some_and(scroll);
5934 if !needs || !self.spawn_async_collect(Self::LOADING_BUFFER) {
5935 self.busy = false;
5936 self.status_message = None;
5937 }
5938 None
5939 }
5940
5941 fn export_path_key(&mut self, event: &KeyEvent) {
5946 let before = self.export_modal.path_input.value().to_string();
5947 self.export_modal
5948 .path_input
5949 .handle_key(event, Some(&self.cache));
5950 if self.export_modal.path_input.value() != before {
5951 self.export_modal.sync_format_to_path();
5952 self.export_modal.path_error = None;
5954 }
5955 }
5956
5957 fn ensure_file_extension(
5958 path: &Path,
5959 format: ExportFormat,
5960 compression: Option<CompressionFormat>,
5961 ) -> PathBuf {
5962 let current_ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
5963 let mut new_path = path.to_path_buf();
5964
5965 if current_ext.is_empty() {
5966 let desired_ext = if let Some(comp) = compression {
5968 format!("{}.{}", format.extension(), comp.extension())
5969 } else {
5970 format.extension().to_string()
5971 };
5972 new_path.set_extension(&desired_ext);
5973 } else {
5974 let is_compression_only = matches!(
5976 current_ext.to_lowercase().as_str(),
5977 "gz" | "zst" | "bz2" | "xz"
5978 ) && ExportFormat::from_extension(current_ext).is_none();
5979
5980 if is_compression_only {
5981 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
5983 let stem_has_format = stem
5984 .split('.')
5985 .next_back()
5986 .and_then(ExportFormat::from_extension)
5987 .is_some();
5988 if stem_has_format {
5989 if let Some(comp) = compression
5990 && let Some(format_ext) = stem
5991 .split('.')
5992 .next_back()
5993 .and_then(ExportFormat::from_extension)
5994 .map(|f| f.extension())
5995 {
5996 new_path =
5997 PathBuf::from(stem.rsplit_once('.').map(|x| x.0).unwrap_or(stem));
5998 new_path.set_extension(format!("{}.{}", format_ext, comp.extension()));
5999 }
6000 } else if let Some(comp) = compression {
6001 new_path.set_extension(format!("{}.{}", format.extension(), comp.extension()));
6002 } else {
6003 new_path.set_extension(format.extension());
6004 }
6005 } else if let Some(comp) = compression
6006 && format.supports_compression()
6007 {
6008 new_path.set_extension(format!("{}.{}", current_ext, comp.extension()));
6009 }
6010 }
6013
6014 new_path
6015 }
6016
6017 pub fn new(events: Sender<AppEvent>, runtime: tokio::runtime::Handle) -> App {
6018 let theme = Theme::from_config(&AppConfig::default().theme).unwrap_or_else(|_| {
6020 Theme {
6022 colors: std::collections::HashMap::new(),
6023 }
6024 });
6025
6026 Self::new_with_config(events, runtime, theme, AppConfig::default())
6027 }
6028
6029 pub fn new_with_theme(
6030 events: Sender<AppEvent>,
6031 runtime: tokio::runtime::Handle,
6032 theme: Theme,
6033 ) -> App {
6034 Self::new_with_config(events, runtime, theme, AppConfig::default())
6035 }
6036
6037 pub fn new_with_config(
6039 events: Sender<AppEvent>,
6040 runtime: tokio::runtime::Handle,
6041 theme: Theme,
6042 app_config: AppConfig,
6043 ) -> App {
6044 let views = ViewManager::load_or_empty().into();
6045 Self::new_with_views(events, runtime, theme, app_config, views)
6046 }
6047
6048 pub fn new_with_views(
6050 events: Sender<AppEvent>,
6051 runtime: tokio::runtime::Handle,
6052 theme: Theme,
6053 app_config: AppConfig,
6054 view_manager: Views,
6055 ) -> App {
6056 let cache = CacheManager::new(APP_NAME).unwrap_or_else(|_| CacheManager {
6057 cache_dir: std::env::temp_dir().join(APP_NAME),
6058 });
6059 let jobs = Jobs::new(events.clone());
6060 let formats = Arc::new(crate::formats::Registry::load(
6061 &crate::formats::search_path_for(&app_config),
6062 ));
6063 for error in &formats.errors {
6064 log::warn!("format spec skipped: {error}");
6065 }
6066
6067 let theme_problem = app_config.theme.fallbacks.first().cloned();
6068 let chart_export_modal = ChartExportModal {
6069 recipe: app_config.chart.export_recipe,
6070 ..ChartExportModal::new()
6071 };
6072 let mut app = App {
6073 path: None,
6074 data_table_state: None,
6075 footer_progress: Arc::new(crate::schema_union::FooterProgress::default()),
6076 footers_this_frame: None,
6077 listed_this_frame: None,
6078 home: home::HomeState {
6079 hide_unreadable: !app_config.home.show_unreadable,
6080 formats: formats.clone(),
6081 ..Default::default()
6082 },
6083 home_probes_inflight: Vec::new(),
6084 home_listing_cancels: HashMap::new(),
6085 home_narrowing: None,
6086 #[cfg(feature = "cloud")]
6087 cloud_discovery_started: false,
6088 home_search_inflight: false,
6089 home_search_generation: 0,
6090 home_generation: 0,
6091 home_schema_inflight: Vec::new(),
6092 last_load_error: None,
6093 pending_clear_recents: false,
6094 pending_link: None,
6095 local_desktop: link_open::local_desktop(link_open::Platform::current(), |name| {
6096 std::env::var(name).ok()
6097 }),
6098 pending_forget_place: None,
6099 home_schema_cache: HashMap::new(),
6100 home_previews: crate::home_preview::Previews::default(),
6101 reads: crate::home_preview::ReadCounts::default(),
6102 shape_remembered: None,
6103 original_file_format: None,
6104 original_file_delimiter: None,
6105 stdin_reader: None,
6106 stdout_pass: None,
6107 follow_drawn: None,
6108 pending_leave: None,
6109 recording_on: None,
6110 recording_end_said: false,
6111 events,
6112 debug: DebugState::default(),
6113 info_modal: InfoModal::new(),
6114 file_facts: None,
6115 codebook: None,
6116 catalog_entry: None,
6117 documentation: Default::default(),
6118 info_documentation: Default::default(),
6119 remembered_moved: false,
6120 head_web_rows: !cache::running_as_a_cargo_test(),
6121 query_input: TextInput::new()
6122 .with_history_limit(app_config.query.history_limit)
6123 .with_theme(&theme)
6124 .with_history("query".to_string()),
6125 sql_input: TextInput::statement()
6126 .with_history_limit(app_config.query.history_limit)
6127 .with_theme(&theme)
6128 .with_history("sql".to_string()),
6129 find: find::Find::new(
6130 TextInput::new()
6131 .with_history_limit(app_config.query.history_limit)
6132 .with_theme(&theme)
6133 .with_history("find".to_string()),
6134 ),
6135 column_hints: false,
6136 input_mode: InputMode::Normal,
6137 input_type: None,
6138 query_mode: QueryMode::default().resolve(),
6139 query_mode_chosen: None,
6140 query_text_restored: false,
6141 sql_columns: Vec::new(),
6142 sql_completion: None,
6143 query_running: None,
6144 query_run_error: None,
6145 inline_failures: 0,
6146 sort_filter_modal: SortFilterModal::new(),
6147 pivot_melt_modal: PivotMeltModal::new(),
6148 view_modal: ViewModal::new(),
6149 opened_from_home: false,
6150 startup_view: None,
6151 analysis_modal: AnalysisModal::with_sample_rows(app_config.analysis.sample_rows),
6152 sample_form: None,
6153 memory_probe: std::sync::Arc::new(table_sample::available_memory),
6154 sample_paths: Vec::new(),
6155 quality_cache: Vec::new(),
6156 quality_samples: Vec::new(),
6157 quality_released: Vec::new(),
6158 quality_memory_budget: QUALITY_MEMORY_BUDGET,
6159 quality_copies: Vec::new(),
6160 quality_copy_released: None,
6161 quality_copy_unusable: None,
6162 quality_copy_free: std::sync::Mutex::new(None),
6163 quality_evidence_return: None,
6164 quality_evidence_label: None,
6165 chart_modal: ChartModal::new(),
6166 chart_export_modal,
6167 export_modal: ExportModal::new(),
6168 copy_modal: copy_modal::CopyModal::new(),
6169 inspector_modal: inspector_modal::InspectorModal::new(),
6170 external_open: None,
6171 open_dir: None,
6172 go_to_column: crate::widgets::ui::PickerState::default(),
6173 value_counts: value_counts_modal::ValueCountsModal::default(),
6174 hex: None,
6175 hex_serial: 0,
6176 export_counts: None,
6177 format_picker: crate::widgets::ui::PickerState::default(),
6178 retype: None,
6179 combine: None,
6180 retype_from_info: false,
6181 table_picker: crate::widgets::ui::PickerState::default(),
6182 table_choices: None,
6183 clipboard: None,
6184 pending_copy: None,
6185 chart_cache: ChartCache::default(),
6186 chart_inflight: None,
6187 chart_asked: None,
6188 pending_chart_result: Arc::new(Mutex::new(None)),
6189 chart_export_waiting: None,
6190 error_modal: ErrorModal::new(),
6191 flash: None,
6192 confirmation_modal: ConfirmationModal::new(),
6193 pending_export: None,
6194 pending_delete_view: None,
6195 pending_hide_examples: false,
6196 pending_chart_export: None,
6197 pending_quality_export: None,
6198 help: help::Help::default(),
6199 pointer: pointer::Pointing::default(),
6200 context_menu: None,
6201 cache,
6202 cache_writes: CacheWrites::default(),
6203 view_manager,
6204 active_view_id: None,
6205 export_progress: None,
6206 theme,
6207 pending_read_all: false,
6208 history_limit: app_config.query.history_limit,
6209 table_cell_padding: app_config.display.cell_padding.cells(),
6210 column_colors: app_config.display.column_colors,
6211 dtype_row: app_config.display.type_row,
6212 number_format: app_config
6213 .display
6214 .number_format
6215 .resolve(app_config.display.right_align_numbers)
6216 .unwrap_or_else(|_| NumberFormatSettings {
6220 align_numeric_right: app_config.display.right_align_numbers,
6221 ..Default::default()
6222 }),
6223 jobs,
6224 runtime,
6225 loading: loading::Loader::default(),
6226 opened: None,
6227 loaded_ahead_from: None,
6228 pending_footers_result: std::sync::Arc::new(std::sync::Mutex::new(None)),
6229 dataset_generation: 0,
6230 footers_held: None,
6231 followed_fields_held: None,
6232 reread_owed: None,
6233 #[cfg(test)]
6234 home_worker_dies: None,
6235 #[cfg(test)]
6236 file_facts_reader: None,
6237 end_when_the_footers_land: None,
6238 end_when_indexed: None,
6239 indexing_stop: Arc::default(),
6240 indexing_lines: None,
6241 indexing_paused: false,
6242 goto_when_indexed: None,
6243 count_progress: Arc::default(),
6244 exact_count_asked: None,
6245 count_after_stop: None,
6246 len_count_inflight: None,
6247 count_after_paint: None,
6248 #[cfg(test)]
6249 counts_spawned: std::cell::Cell::new(0),
6250 #[cfg(test)]
6251 first_rows_asked: 0,
6252 len_count_failed: None,
6253 end_after_count: None,
6254 busy: false,
6255 throbber_frame: 0,
6256 screen_generation: 0,
6257 input_dropped: false,
6258 status_message: None,
6259 analysis_computation: None,
6260 app_config,
6261 background_query: false,
6262 formats,
6263 };
6264 if let Some(problem) = theme_problem {
6266 app.flash_note(problem);
6267 }
6268 app
6269 }
6270
6271 pub fn set_formats(&mut self, registry: crate::formats::Registry) {
6274 let registry = Arc::new(registry);
6275 self.home.formats = registry.clone();
6276 self.formats = registry;
6277 }
6278
6279 pub fn enable_debug(&mut self) {
6280 self.debug.enabled = true;
6281 }
6282
6283 pub fn home_schema(&mut self, entry: &discover::Entry) -> Option<discover::SchemaPreview> {
6289 if home::is_cloud_place(&entry.path) || home::is_object_store_url(&entry.path) {
6292 return None;
6293 }
6294 if let Some(cached) = self.home_schema_cache.get(&entry.path) {
6295 return cached.clone();
6296 }
6297 self.request_home_schema(entry.clone());
6300 None
6301 }
6302
6303 fn owed_answer(&mut self, instead: AppEvent) -> OwedAnswer {
6305 OwedAnswer {
6306 tx: self.events.clone(),
6307 #[cfg(test)]
6308 dies: self
6309 .home_worker_dies
6310 .as_mut()
6311 .is_some_and(|dies| dies(&instead)),
6312 instead: Some(instead),
6313 }
6314 }
6315
6316 fn list_the_typed_directory(&mut self) {
6320 if !self.home.path_input_active {
6321 return;
6322 }
6323 let dir = home::typed_dir(&self.home.path_input).to_string();
6324 if self
6325 .home
6326 .path_listing
6327 .as_ref()
6328 .is_some_and(|l| l.dir == dir)
6329 {
6330 return;
6331 }
6332 if home::typed_dir_is_url(&dir) {
6333 self.home.path_listing = Some(home::names_under(&dir, self.home.known_urls()));
6334 return;
6335 }
6336 let tx = self.events.clone();
6338 let owed = self.owed_answer(AppEvent::HomePathListed {
6339 listing: Box::new(home::PathListing {
6340 dir: dir.clone(),
6341 names: Vec::new(),
6342 failed: true,
6343 }),
6344 });
6345 std::thread::spawn(move || {
6346 owed.run(|| {
6347 let listing = home::list_typed_dir(&dir);
6348 let _ = tx.send(AppEvent::HomePathListed {
6349 listing: Box::new(listing),
6350 });
6351 })
6352 });
6353 }
6354
6355 fn request_path_completion(&mut self) {
6357 let typed = self.home.path_input.clone();
6358 if typed.is_empty() {
6359 return;
6360 }
6361 let generation = self.home_generation;
6362 let tx = self.events.clone();
6363 std::thread::spawn(move || {
6364 let (completed, candidates) = home::complete_path(&typed);
6365 let _ = tx.send(AppEvent::HomePathCompleted {
6366 generation,
6367 typed,
6368 completed,
6369 candidates,
6370 });
6371 });
6372 }
6373
6374 pub fn home_preview_rows(
6378 &mut self,
6379 entry: &discover::Entry,
6380 screen_height: u16,
6381 ) -> Option<Arc<crate::home_preview::PreviewRows>> {
6382 let max = self.app_config.home.preview_max.bytes();
6383 if !crate::home_preview::previewable(entry, max) {
6384 return None;
6385 }
6386 let stamp = crate::home_preview::Stamp::of_entry(entry);
6387 if let Some(known) = self.home_previews.rows(&entry.path, stamp) {
6388 return known;
6389 }
6390 if self.home_previews.inflight.is_none() {
6391 self.request_home_preview(entry.path.clone(), stamp, screen_height);
6392 }
6393 None
6394 }
6395
6396 pub fn home_preview_pending(&self, path: &Path) -> bool {
6398 self.home_previews.reading(path)
6399 }
6400
6401 fn request_home_preview(
6404 &mut self,
6405 path: PathBuf,
6406 stamp: crate::home_preview::Stamp,
6407 screen_height: u16,
6408 ) {
6409 self.home_previews.inflight = Some(path.clone());
6410 self.reads.previews += 1;
6411 let tx = self.events.clone();
6412 let cloud = self.app_config.cloud.clone();
6413 let formats = self.formats.clone();
6414 let runtime = self.runtime.clone();
6415 let cache = self.cache.clone();
6416 let writes = self.cache_writes.clone();
6417 let visible = (screen_height as usize).saturating_sub(3).max(1);
6419 let owed = self.owed_answer(AppEvent::HomePreviewReady {
6420 path: path.clone(),
6421 stamp,
6422 read_at: None,
6423 rows: None,
6424 prepared: crate::home_preview::Handoff::default(),
6425 });
6426 self.runtime.spawn_blocking(move || {
6427 owed.run(|| {
6428 let began = std::time::Instant::now();
6429 let read_at = crate::home_preview::Stamp::of_file(&path);
6430 let read = Self::read_home_preview(
6431 &path, &cloud, &formats, &runtime, cache, writes, visible,
6432 );
6433 log::debug!(
6434 target: "datui",
6435 "home preview of {}: {:.1?}",
6436 path.display(),
6437 began.elapsed()
6438 );
6439 let (rows, prepared) = match read {
6440 Some((rows, prepared)) => (Some(Arc::new(rows)), Some(Box::new(prepared))),
6441 None => (None, None),
6442 };
6443 let _ = tx.send(AppEvent::HomePreviewReady {
6444 path,
6445 stamp,
6446 read_at,
6447 rows,
6448 prepared: Arc::new(Mutex::new(prepared)),
6449 });
6450 })
6451 });
6452 }
6453
6454 fn read_home_preview(
6459 path: &Path,
6460 cloud: &crate::config::CloudConfig,
6461 formats: &crate::formats::Registry,
6462 runtime: &tokio::runtime::Handle,
6463 cache: CacheManager,
6464 writes: CacheWrites,
6465 visible: usize,
6466 ) -> Option<(
6467 crate::home_preview::PreviewRows,
6468 crate::home_preview::Prepared,
6469 )> {
6470 let paths = [path.to_path_buf()];
6471 let scanned = Self::scan_for_open(
6472 cloud,
6473 formats,
6474 &paths,
6475 OpenOptions::default(),
6476 Some(path.to_path_buf()),
6477 )
6478 .ok()?;
6479 let loading::LoadAnswer::Scanned { lf, path, options } = scanned else {
6480 return None;
6481 };
6482 let progress = Arc::<crate::schema_union::FooterProgress>::default();
6483 let report = crate::measurements::OpenReport {
6484 progress: progress.clone(),
6485 meter: Arc::new(crate::measurements::Meter::default()),
6486 remembered: Some(cache),
6487 writes,
6488 };
6489 let read = Self::read_schema_for_open(
6490 *lf,
6491 path,
6492 options,
6493 cloud,
6494 runtime,
6495 &report,
6496 loading::Made::default(),
6497 )
6498 .ok()?;
6499 let loading::LoadAnswer::SchemaRead {
6500 mut state,
6501 options,
6502 debug_label,
6503 ..
6504 } = read
6505 else {
6506 return None;
6507 };
6508 state.visible_rows = visible;
6510 let began = std::time::Instant::now();
6511 let request = state.prepare_async_collect(None)?;
6512 let df = crate::statistics::collect_lazy(request.lf, request.polars_streaming).ok()?;
6513 let result = request.plan.fit(df);
6514 let rows = crate::home_preview::PreviewRows::from_frame(result.rows());
6515 state.measurements().read_page(began.elapsed(), Some(1));
6516 state.apply_async_collect(result);
6517 Some((
6518 rows,
6519 crate::home_preview::Prepared {
6520 state,
6521 options,
6522 debug_label,
6523 progress,
6524 },
6525 ))
6526 }
6527
6528 pub fn home_schema_pending(&self, path: &Path) -> bool {
6530 self.home_schema_inflight.iter().any(|p| p == path)
6531 }
6532
6533 fn request_home_schema(&mut self, entry: discover::Entry) {
6535 if self.home_schema_inflight.contains(&entry.path) {
6536 return;
6537 }
6538 self.home_schema_inflight.push(entry.path.clone());
6539
6540 let generation = self.home_generation;
6541 let tx = self.events.clone();
6542 let owed = self.owed_answer(AppEvent::HomeSchemaReady {
6544 generation,
6545 path: entry.path.clone(),
6546 preview: None,
6547 });
6548 self.runtime.spawn_blocking(move || {
6549 owed.run(|| {
6550 let preview = discover::schema_preview(&entry);
6551 let _ = tx.send(AppEvent::HomeSchemaReady {
6552 generation,
6553 path: entry.path,
6554 preview,
6555 });
6556 })
6557 });
6558 }
6559
6560 fn spawn_home_probes(&mut self) {
6567 self.stop_listings_left_behind();
6568 for root in self.home.pending_probes() {
6569 if self.home_probes_inflight.contains(&root) {
6570 if let Some(cancelled) = self.home_listing_cancels.get(&root) {
6572 cancelled.store(false, std::sync::atomic::Ordering::Relaxed);
6573 }
6574 continue;
6575 }
6576 let browsed = self.home.browsing.as_ref() == Some(&root);
6586 if !browsed && self.home_probes_inflight.len() >= MAX_CONCURRENT_PROBES {
6587 continue;
6588 }
6589 self.home_probes_inflight.push(root.clone());
6590 let tx = self.events.clone();
6591 let cache = self.cache.clone();
6592 let owed = self.owed_answer(AppEvent::HomeProbeFailed {
6593 root: root.clone(),
6594 message: "Could not read it; see the log".to_string(),
6595 });
6596 #[cfg(feature = "cloud")]
6597 let cloud = self.app_config.cloud.clone();
6598 #[cfg(feature = "cloud")]
6599 let runtime = self.runtime.clone();
6600 #[cfg(feature = "cloud")]
6601 let cancelled = {
6602 let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
6603 self.home_listing_cancels.insert(root.clone(), flag.clone());
6604 flag
6605 };
6606 std::thread::spawn(move || {
6611 owed.run(|| {
6612 #[cfg(feature = "cloud")]
6624 if let Some((id, account)) = home::cloud_account(&root) {
6625 let listed = wait_on_runtime(&runtime, async move {
6626 crate::cloud_browse::list_account(&id, &account, &cloud).await
6627 });
6628 match listed {
6629 Some(Ok(rows)) => {
6630 let _ = tx.send(AppEvent::HomeProbeReady {
6631 root,
6632 rows: Some(rows),
6633 cut_short: false,
6634 });
6635 }
6636 Some(Err(message)) => {
6637 log::warn!(
6638 target: "datui::cloud",
6639 "listing {} failed: {message}",
6640 root.display()
6641 );
6642 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
6643 }
6644 None => {
6645 let _ = tx.send(AppEvent::HomeProbeReady {
6646 root,
6647 rows: None,
6648 cut_short: false,
6649 });
6650 }
6651 }
6652 return;
6653 }
6654 #[cfg(feature = "cloud")]
6655 if crate::cloud_browse::split_bucket_url(&root.to_string_lossy()).is_some()
6656 || source::azure_parts(&root.to_string_lossy()).is_some()
6657 {
6658 let url = root.to_string_lossy().into_owned();
6659 let watch = crate::cloud_browse::Watch {
6662 progress: Some(std::sync::Arc::new({
6663 let (tx, root) = (tx.clone(), root.clone());
6664 move |so_far: &[crate::discover::Entry]| {
6665 let _ = tx.send(AppEvent::HomeProbeProgress {
6666 root: root.clone(),
6667 rows: so_far.to_vec(),
6668 });
6669 }
6670 })),
6671 cancelled,
6672 names_from: None,
6673 };
6674 let listed = wait_on_runtime(&runtime, async move {
6675 crate::cloud_browse::list_objects_watched(&url, &cloud, &watch).await
6676 });
6677 match listed {
6680 Some(Err(message)) => {
6681 log::warn!(
6682 target: "datui::cloud",
6683 "listing {} failed: {message}",
6684 root.display()
6685 );
6686 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
6687 }
6688 Some(Ok(level)) if level.cancelled => {
6689 let _ = tx.send(AppEvent::HomeProbeCancelled { root });
6690 }
6691 other => {
6692 let (rows, cut_short) = match other {
6693 Some(Ok(level)) => (Some(level.rows), level.truncated),
6694 _ => (None, false),
6695 };
6696 let _ = tx.send(AppEvent::HomeProbeReady {
6697 root,
6698 rows,
6699 cut_short,
6700 });
6701 }
6702 }
6703 return;
6704 }
6705 #[cfg(not(feature = "cloud"))]
6708 if source::is_remote_url(&root) {
6709 let message = "cloud support not in this build".to_string();
6710 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
6711 return;
6712 }
6713 let mut cut_short = false;
6714 let rows = if std::fs::read_dir(&root).is_ok() {
6715 let scan = crate::discover::scan_dir_progressive(&root, |so_far| {
6718 let _ = tx.send(AppEvent::HomeProbeProgress {
6719 root: root.clone(),
6720 rows: so_far.to_vec(),
6721 });
6722 });
6723 cut_short = scan.truncated;
6724 let mut rows = scan.entries;
6725 for row in rows.iter_mut().take(PROBE_MEASURE_LIMIT) {
6728 crate::discover::enrich(row);
6729 }
6730 let mounts = crate::locality::Mounts::current();
6735 for row in rows.iter_mut() {
6736 row.cost.source = Some(mounts.describe(&row.path).fstype);
6737 }
6738 let facts: Vec<_> = rows.iter().filter_map(home::facts_for).collect();
6739 cache.record_dataset_facts(&facts);
6740 Some(rows)
6741 } else {
6742 None
6743 };
6744 let _ = tx.send(AppEvent::HomeProbeReady {
6745 root,
6746 rows,
6747 cut_short,
6748 });
6749 })
6750 });
6751 }
6752 }
6753
6754 fn stop_listings_left_behind(&mut self) {
6757 let home = &self.home;
6758 for (root, cancelled) in &self.home_listing_cancels {
6759 let wanted = match &home.browsing {
6760 Some(dir) => dir == root,
6761 None => home
6762 .sections
6763 .iter()
6764 .any(|s| s.remote_root.as_ref() == Some(root)),
6765 };
6766 if !wanted {
6767 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
6768 }
6769 }
6770 if let Some((dir, _, cancelled)) = &self.home_narrowing
6771 && home.browsing.as_ref() != Some(dir)
6772 {
6773 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
6774 self.home_narrowing = None;
6775 }
6776 if self
6777 .home
6778 .narrowed
6779 .as_ref()
6780 .is_some_and(|n| self.home.browsing.as_ref() != Some(&n.dir))
6781 {
6782 self.home.narrowed = None;
6783 }
6784 }
6785
6786 #[cfg(feature = "cloud")]
6790 fn narrow_cloud_listing(&mut self) {
6791 let dir = self.home.browsing.clone();
6792 let prefix = dir.as_ref().and_then(|dir| {
6793 if !self.home.cut_short.contains(dir) {
6794 return None;
6795 }
6796 let rows = self.home.probed.get(dir)?;
6797 let names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
6798 crate::cloud_browse::narrowing_prefix(&self.home.filter, &names)
6799 });
6800 let (Some(dir), Some(prefix)) = (dir, prefix) else {
6801 if let Some((_, _, cancelled)) = self.home_narrowing.take() {
6802 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
6803 }
6804 if self.home.narrowed.take().is_some() {
6805 self.home_refresh();
6806 }
6807 return;
6808 };
6809 if self.home.narrowed.as_ref().is_some_and(|n| {
6811 n.dir == dir && (n.prefix == prefix || (!n.truncated && prefix.starts_with(&n.prefix)))
6812 }) {
6813 return;
6814 }
6815 if let Some((d, p, _)) = &self.home_narrowing
6816 && *d == dir
6817 && *p == prefix
6818 {
6819 return;
6820 }
6821 if let Some((_, _, cancelled)) = self.home_narrowing.take() {
6822 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
6823 }
6824 let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
6825 self.home_narrowing = Some((dir.clone(), prefix.clone(), cancelled.clone()));
6826 let tx = self.events.clone();
6827 let owed = self.owed_answer(AppEvent::HomeNarrowed {
6828 dir: dir.clone(),
6829 prefix: prefix.clone(),
6830 listed: None,
6831 });
6832 let cloud = self.app_config.cloud.clone();
6833 let runtime = self.runtime.clone();
6834 std::thread::spawn(move || {
6835 owed.run(|| {
6836 let url = dir.to_string_lossy().into_owned();
6837 let watch = crate::cloud_browse::Watch {
6838 progress: None,
6839 cancelled,
6840 names_from: Some(prefix.clone()),
6841 };
6842 let listed = wait_on_runtime(&runtime, async move {
6843 crate::cloud_browse::list_objects_watched(&url, &cloud, &watch).await
6844 });
6845 let listed = match listed {
6846 Some(Ok(level)) if !level.cancelled => Some((level.rows, level.truncated)),
6847 _ => None,
6848 };
6849 let _ = tx.send(AppEvent::HomeNarrowed {
6850 dir,
6851 prefix,
6852 listed,
6853 });
6854 })
6855 });
6856 }
6857
6858 #[cfg(feature = "cloud")]
6861 fn spawn_cloud_discovery(&mut self) {
6862 if self.cloud_discovery_started {
6863 return;
6864 }
6865 self.cloud_discovery_started = true;
6866 let list = self.app_config.cloud.list_on_start;
6867 self.list_cloud_sources(None, list);
6868 }
6869
6870 #[cfg(feature = "cloud")]
6873 fn list_browsed_cloud_source(&mut self) {
6874 let Some(id) = self
6875 .home
6876 .browsing
6877 .as_deref()
6878 .and_then(home::cloud_source_id)
6879 else {
6880 return;
6881 };
6882 let Some(source) = self.home.cloud.iter_mut().find(|s| s.id == id) else {
6883 return;
6884 };
6885 if source.asked {
6886 return;
6887 }
6888 source.begin_listing();
6889 self.list_cloud_sources(Some(id), true);
6890 }
6891
6892 #[cfg(feature = "cloud")]
6904 fn list_cloud_sources(&mut self, only: Option<String>, list: bool) {
6905 let tx = self.events.clone();
6906 let cloud = self.app_config.cloud.clone();
6907 let cache = self.cache.clone();
6908 self.runtime.spawn(async move {
6909 let mut hidden = cache.load_hidden_cloud_sources();
6910 hidden.extend(cloud.hide.iter().cloned());
6911 let cached_for = |source: &crate::cloud_sources::Source| {
6912 cache.cloud_listing(&source.id, &source.fingerprint())
6913 };
6914 let found = {
6915 let env = crate::cloud_browse::Environment::current();
6916 crate::cloud_sources::discover(&cloud, &env)
6917 };
6918 if only.is_none() {
6923 for source in found.iter().filter(|s| !hidden.contains(&s.id)) {
6924 if let Some(cached) = cached_for(source) {
6925 crate::cloud_sources::remember_listed(source, &cached.buckets);
6926 }
6927 }
6928 }
6929 let sources: Vec<crate::cloud_sources::Source> =
6930 crate::cloud_sources::on_home(found, &cloud)
6931 .into_iter()
6932 .filter(|s| !hidden.contains(&s.id))
6933 .collect();
6934
6935 match &only {
6936 None => {
6937 let rows = sources
6938 .iter()
6939 .map(|source| home_cloud_source(source, cached_for(source).as_ref(), list))
6940 .collect();
6941 let _ = tx.send(AppEvent::HomeCloudSources { sources: rows });
6942 }
6943 Some(id) if !sources.iter().any(|s| &s.id == id) => {
6946 let _ = tx.send(AppEvent::HomeCloudListed {
6947 id: id.clone(),
6948 buckets: Vec::new(),
6949 details: Vec::new(),
6950 failure: Some((
6951 "not found".to_string(),
6952 format!("{id} is gone or hidden. Ctrl+R at the top looks again."),
6953 )),
6954 listed_at: std::time::SystemTime::now(),
6955 });
6956 return;
6957 }
6958 Some(_) => {}
6959 }
6960 if !list {
6961 return;
6962 }
6963
6964 const LISTING_AT_ONCE: usize = 4;
6967 let permits = std::sync::Arc::new(tokio::sync::Semaphore::new(LISTING_AT_ONCE));
6968 let mut listings = tokio::task::JoinSet::new();
6969 for source in sources
6970 .into_iter()
6971 .filter(|s| only.as_ref().is_none_or(|id| &s.id == id))
6972 {
6973 let permits = permits.clone();
6974 listings.spawn(async move {
6975 let _permit = permits.acquire_owned().await;
6976 let result = crate::cloud_browse::list_first_level(&source).await;
6977 (source, result)
6978 });
6979 }
6980 while let Some(joined) = listings.join_next().await {
6981 let Ok((source, result)) = joined else {
6982 continue;
6983 };
6984 let listed_at = std::time::SystemTime::now();
6985 let mut names = source.buckets.clone();
6988 let mut details = Vec::new();
6989 let failure = match result {
6990 Ok(listed) => {
6991 for item in listed {
6992 crate::cloud_sources::remember_bucket(&source, &item.name);
6993 if !item.details.is_empty() {
6994 details.push((item.place.clone(), item.details));
6995 }
6996 let name = item.name;
6997 if !names.contains(&name) {
6998 names.push(name);
6999 }
7000 }
7001 cache.save_cloud_listing(
7002 &source.id,
7003 crate::cache::CloudListing {
7004 fingerprint: source.fingerprint(),
7005 buckets: names.clone(),
7006 listed_at: listed_at
7007 .duration_since(std::time::UNIX_EPOCH)
7008 .map(|d| d.as_secs())
7009 .unwrap_or(0),
7010 },
7011 );
7012 None
7013 }
7014 Err(e) => {
7015 log::warn!(target: "datui::cloud", "listing {} failed: {e}", source.id);
7016 Some(summarize_cloud_failure(&e))
7017 }
7018 };
7019 let _ = tx.send(AppEvent::HomeCloudListed {
7020 id: source.id.clone(),
7021 buckets: names
7022 .iter()
7023 .map(|b| PathBuf::from(source.bucket_url(b)))
7024 .collect(),
7025 details,
7026 failure,
7027 listed_at,
7028 });
7029 }
7030 });
7031 }
7032
7033 fn home_reload(&mut self) {
7037 #[cfg(feature = "cloud")]
7038 {
7039 let browsing = self.home.browsing.clone();
7040 match browsing.as_deref().and_then(home::cloud_source_id) {
7041 Some(id) => {
7042 if let Some(source) = self.home.cloud.iter_mut().find(|s| s.id == id) {
7043 source.begin_listing();
7044 }
7045 self.list_cloud_sources(Some(id), true);
7046 }
7047 None if browsing.is_none() && !self.home.cloud.is_empty() => {
7048 for source in &mut self.home.cloud {
7049 source.begin_listing();
7050 }
7051 self.list_cloud_sources(None, true);
7052 }
7053 None => {}
7054 }
7055 }
7056 if let Some(dir) = self.home.browsing.clone() {
7057 self.home.probed.remove(&dir);
7058 self.home.unreachable.remove(&dir);
7059 self.home.cut_short.remove(&dir);
7060 }
7061 self.home.peek_failed.clear();
7064 for path in std::mem::take(&mut self.home.web_gone).into_keys() {
7065 self.home.sized.remove(&path);
7066 }
7067 self.home.status = None;
7068 self.home_refresh();
7069 }
7070
7071 fn spawn_home_search(&mut self) {
7078 if self.home_search_inflight || self.home.search.done {
7079 return;
7080 }
7081 let config = self.app_config.home.search.clone();
7082 if !config.enabled {
7083 return;
7084 }
7085 let Some(root) =
7086 crate::search::search_root(self.home.browsing.as_ref(), self.home.network_check)
7087 else {
7088 return;
7089 };
7090
7091 self.home.search.reset();
7092 self.home.search.root = Some(root.clone());
7093 self.home.search.running = true;
7094 self.home.search.epoch = next_search_epoch();
7095 self.home.search_limit = config.max_results;
7096 self.home_search_inflight = true;
7097
7098 let generation = self.home_generation;
7099 self.home_search_generation = generation;
7100 let tx = self.events.clone();
7101 let formats = self.formats.clone();
7102 let owed = self.owed_answer(AppEvent::HomeSearchDone {
7104 generation,
7105 root: root.clone(),
7106 scanned: 0,
7107 limited: Some("partial · failed".to_string()),
7108 });
7109 std::thread::spawn(move || {
7113 owed.run(|| {
7114 let walk_root = root.clone();
7115 let batch_tx = tx.clone();
7116 let batch_gen = generation;
7117 let batch_root = root.clone();
7118 let outcome = crate::search::walk_with_specs(
7119 &walk_root,
7120 &config,
7121 &formats,
7122 move |found, outcome| {
7123 batch_tx
7126 .send(AppEvent::HomeSearchBatch {
7127 generation: batch_gen,
7128 root: batch_root.clone(),
7129 found,
7130 scanned: outcome.scanned,
7131 })
7132 .is_ok()
7134 },
7135 );
7136 let _ = tx.send(AppEvent::HomeSearchDone {
7137 generation,
7138 root,
7139 scanned: outcome.scanned,
7140 limited: outcome.note().map(str::to_string),
7141 });
7142 })
7143 });
7144 }
7145
7146 fn home_score_search(&mut self) {
7152 if self.input_mode != InputMode::Home {
7153 return;
7154 }
7155 let Some(job) = self.home.score_job() else {
7156 return;
7157 };
7158 let epoch = job.epoch;
7159 let tx = self.events.clone();
7160 let owed = self.owed_answer(AppEvent::HomeSearchScored {
7162 epoch,
7163 matches: None,
7164 });
7165 self.runtime.spawn_blocking(move || {
7166 owed.run(move || {
7167 let matches =
7168 crate::search::score(&job.results, &job.query, job.base.as_ref(), job.limit);
7169 let _ = tx.send(AppEvent::HomeSearchScored {
7170 epoch,
7171 matches: Some(Box::new(matches)),
7172 });
7173 })
7174 });
7175 }
7176
7177 fn home_refresh(&mut self) {
7179 #[cfg(feature = "cloud")]
7182 self.list_browsed_cloud_source();
7183 self.stop_listings_left_behind();
7185 self.home_generation = self.home_generation.wrapping_add(1);
7186 let generation = self.home_generation;
7187
7188 self.move_remembered_places();
7189 self.home.catalogs = home::catalogs(&self.app_config);
7190 if self.cache.examples_hidden() {
7193 self.home
7194 .catalogs
7195 .retain(|c| c.origin != crate::catalog::Origin::Bundled);
7196 }
7197 let mut request = home::ListingRequest {
7198 recents: Vec::new(),
7201 desktop_dirs: Vec::new(),
7202 browsing: self.home.browsing.clone(),
7203 probed: self.home.probed.clone(),
7204 unreachable: self.home.unreachable.clone(),
7205 listing_so_far: self.home.listing_so_far.clone(),
7206 cut_short: self.home.cut_short.clone(),
7207 narrowed: self.home.narrowed.clone(),
7208 probe_errors: self.home.probe_errors.clone(),
7209 network_check: self.home.network_check,
7210 cloud: self.home.cloud.clone(),
7211 catalogs: self.home.catalogs.clone(),
7212 known: Default::default(),
7213 formats: self.formats.clone(),
7214 };
7215 let read_folds = std::mem::take(&mut self.home.folds_owed);
7216 let desktop = self.app_config.home.desktop_recents;
7217 let cache = self.cache.clone();
7218 let writes = self.cache_writes.clone();
7219
7220 self.home.listing_in_flight = true;
7221 let tx = self.events.clone();
7222 let owed = self.owed_answer(AppEvent::HomeListingFailed);
7223 self.runtime.spawn_blocking(move || {
7224 owed.run(move || {
7225 writes.settle();
7227 let (recents, visits) = cache.load_recents_with_visits();
7230 let newest = recents.first().cloned();
7231 request.recents = crate::cache::by_frecency(recents, &visits);
7232 request.known = cache.load_dataset_facts();
7233 if desktop {
7234 request.desktop_dirs = home::desktop_recent_dirs();
7235 }
7236 let listing = home::build_listing(&request);
7237 let mut visits = visits;
7238 listing.alias_visits(&mut visits);
7239 let shown: Vec<PathBuf> = listing
7241 .sections
7242 .iter()
7243 .flat_map(|s| s.rows.iter().chain(&s.door))
7244 .flat_map(|row| [row.path.clone(), home::index_key(&row.path)])
7245 .filter(|key| request.known.contains_key(key))
7246 .collect();
7247 cache.touch_dataset_facts(shown.iter().map(PathBuf::as_path));
7248 let _ = tx.send(AppEvent::HomeListingReady {
7249 generation,
7250 listing: Box::new(listing),
7251 known: request.known,
7252 visits,
7253 newest,
7254 folds: read_folds.then(|| cache.load_folds()),
7255 });
7256 })
7257 });
7258 }
7259
7260 fn request_home_measurements(&mut self) {
7266 if self.home.measure_in_flight {
7267 return;
7268 }
7269 let wanted = self.home.unmeasured_visible(MEASURE_BATCH);
7270 if wanted.is_empty() {
7271 return;
7272 }
7273
7274 self.home.measure_in_flight = true;
7275 let tx = self.events.clone();
7276 let cache = self.cache.clone();
7277 self.runtime.spawn_blocking(move || {
7278 home::look_into_batch(wanted, &cache, |path, m| {
7279 let _ = tx.send(AppEvent::HomeMeasured {
7280 measured: vec![(path, m)],
7281 done: false,
7282 });
7283 });
7284 let _ = tx.send(AppEvent::HomeMeasured {
7285 measured: Vec::new(),
7286 done: true,
7287 });
7288 });
7289 }
7290
7291 pub fn request_what_the_frame_needs(&mut self) {
7298 if self.input_mode == InputMode::Normal {
7299 self.load_ahead();
7300 self.catch_up_follow();
7301 }
7302 self.inspector_needs();
7303 if self.input_mode != InputMode::Home {
7304 return;
7305 }
7306 if std::mem::take(&mut self.home.pending_enrich) {
7307 self.request_home_measurements();
7308 }
7309 #[cfg(feature = "http")]
7310 self.size_selected_web_file();
7311 if std::mem::take(&mut self.home.pending_classify) {
7312 self.request_home_classifications();
7313 }
7314 #[cfg(feature = "cloud")]
7315 if std::mem::take(&mut self.home.pending_peek) {
7316 self.peek_cloud_directories();
7317 }
7318 }
7319
7320 #[cfg(feature = "http")]
7324 fn size_selected_web_file(&mut self) {
7325 if !self.head_web_rows {
7326 return;
7327 }
7328 let Some(entry) = self.home.selected_entry() else {
7329 return;
7330 };
7331 if entry.size.is_some()
7332 || !matches!(
7333 source::input_source(&entry.path),
7334 source::InputSource::Http(_)
7335 )
7336 || !self.home.sized.insert(entry.path.clone())
7337 {
7338 return;
7339 }
7340 let tx = self.events.clone();
7341 let cache = self.cache.clone();
7342 self.runtime.spawn_blocking(move || {
7343 let size = match Self::fetch_remote_size_http(&entry.path.to_string_lossy()) {
7344 Ok(Some(size)) => size,
7345 Ok(None) => return,
7346 Err(gone) => {
7347 let _ = tx.send(AppEvent::HomeWebGone {
7348 path: entry.path,
7349 gone,
7350 });
7351 return;
7352 }
7353 };
7354 let key = home::index_key(&entry.path);
7355 let mut facts = cache.dataset_facts(&key).unwrap_or_default();
7356 facts.size = size;
7357 cache.record_dataset_facts(&[(key, facts)]);
7358 let measured = home::Measured {
7359 rows: entry.rows,
7360 cols: entry.cols,
7361 cols_sampled: entry.cols_sampled,
7362 size: Some(size),
7363 columns: entry.columns.clone(),
7364 cost: entry.cost.clone(),
7365 kind: None,
7366 holds: entry.holds.clone(),
7367 };
7368 let _ = tx.send(AppEvent::HomeSized {
7369 path: entry.path,
7370 measured,
7371 });
7372 });
7373 }
7374
7375 fn request_home_classifications(&mut self) {
7395 if self.home.classify_in_flight {
7396 return;
7397 }
7398 let wanted = self.home.unclassified_visible(CLASSIFY_BATCH);
7399 if wanted.is_empty() {
7400 return;
7401 }
7402
7403 self.home.classify_in_flight = true;
7404 let tx = self.events.clone();
7405 let cache = self.cache.clone();
7406 std::thread::spawn(move || {
7407 home::look_into_batch(wanted, &cache, |path, m| {
7408 let _ = tx.send(AppEvent::HomeClassified {
7409 measured: vec![(path, m)],
7410 done: false,
7411 });
7412 });
7413 let _ = tx.send(AppEvent::HomeClassified {
7414 measured: Vec::new(),
7415 done: true,
7416 });
7417 });
7418 }
7419
7420 pub fn abandon_load(&mut self) {
7433 let retired = self.loading.retire();
7434 if let Some(retired) = retired {
7435 self.put_down_load(retired);
7436 }
7437 self.reset_chart_state();
7441 if self.jobs.supersede(|job| matches!(job, Job::Classify(_))) {
7445 self.home.status = None;
7446 }
7447 self.jobs.take_owed(Self::owed_rows);
7451 if retired.is_some() {
7455 self.busy = false;
7456 let quieted = self.jobs.quiet(Self::reading_rows);
7457 if self
7458 .status_message
7459 .as_ref()
7460 .is_some_and(|status| quieted.contains(status))
7461 {
7462 self.status_message = None;
7463 }
7464 }
7465 self.screen_generation = self.screen_generation.wrapping_add(1);
7468 }
7469
7470 pub fn enter_home(&mut self) {
7471 self.pause_indexing();
7472 if self.return_from_quality_evidence(false) {
7473 self.analysis_modal.close();
7474 }
7475 self.view_modal.close();
7479 self.inspector_modal.close();
7480 self.stop_find();
7481 self.hex = None;
7482 self.stop_value_count();
7484 self.export_counts = None;
7485 self.abandon_load();
7486 if let Some(state) = self.data_table_state.as_mut() {
7488 state.stop_following();
7489 }
7490 self.home.status = None;
7491 self.home.filter_selected = !self.home.filter.is_empty();
7494 self.home.folds_owed = true;
7495 self.home_refresh();
7496 if let Some(open_path) = self.path.clone() {
7497 let target =
7498 crate::canonical::canonicalize(&open_path).unwrap_or_else(|_| open_path.clone());
7499 if let Some(idx) = self.home.visible().iter().position(|row| match row {
7500 home::Row::Entry { entry, .. } => {
7501 crate::canonical::canonicalize(&entry.path)
7502 .unwrap_or_else(|_| entry.path.clone())
7503 == target
7504 }
7505 home::Row::Header { .. }
7509 | home::Row::Place { .. }
7510 | home::Row::More { .. }
7511 | home::Row::Hidden { .. }
7512 | home::Row::Door { .. } => false,
7513 }) {
7514 self.home.selected = idx;
7515 }
7516 }
7517 self.input_mode = InputMode::Home;
7518 }
7519
7520 fn home_escape(&mut self) -> Option<AppEvent> {
7525 if !self.home.filter.is_empty() {
7526 self.home.filter.clear();
7527 self.home.sync_search_section();
7528 self.home.select_first_entry();
7530 return None;
7531 }
7532 if self.home.browsing.is_some() {
7533 if self.home.below_browse_start() {
7534 self.home_ascend();
7535 } else {
7536 self.home_leave_browsing(None);
7539 }
7540 return None;
7541 }
7542 if self.data_table_state.is_some() {
7543 self.input_mode = InputMode::Normal;
7544 let name = self
7547 .path
7548 .as_deref()
7549 .and_then(|p| p.file_name())
7550 .map(|n| n.to_string_lossy().into_owned());
7551 if let Some(name) = name {
7552 self.flash_note(format!("Back to {name}"));
7553 }
7554 }
7555 None
7556 }
7557
7558 fn home_forget_selected(&mut self) {
7564 if let Some(catalog) = self.home.selected_catalog() {
7567 if catalog.origin == crate::catalog::Origin::Bundled {
7568 let message = format!(
7569 "Hide {}? It comes back after datui cache clear.",
7570 catalog.label
7571 );
7572 self.pending_hide_examples = true;
7573 self.confirmation_modal.show_destructive(message, "Hide");
7574 } else {
7575 self.home.status = Some(format!(
7576 "[home] hide = [\"{}\"] in config.toml hides it",
7577 catalog.id
7578 ));
7579 }
7580 return;
7581 }
7582 if let Some(home::Row::Place { path, held, .. }) = self.home.selected_row() {
7585 self.pending_forget_place = Some(path.clone());
7586 self.confirmation_modal.show(format!(
7587 "Forget {held} recently opened {} under {}?",
7588 if held == 1 { "dataset" } else { "datasets" },
7589 home::display_path(&path)
7590 ));
7591 return;
7592 }
7593 let in_mine = self
7596 .home
7597 .selected_section()
7598 .and_then(|i| self.home.sections.get(i))
7599 .is_some_and(|s| s.origin == Some("catalog.toml"));
7600 if in_mine
7601 && let Some((location, _)) = self.home_row_for_catalog()
7602 && let Some((id, name)) = self.mine_entry_at(&location)
7603 {
7604 self.home_forget_from_catalog(&id, &name);
7605 return;
7606 }
7607 let section_title = self
7608 .home
7609 .selected_section()
7610 .and_then(|i| self.home.sections.get(i))
7611 .map(|s| s.title.clone())
7612 .unwrap_or_default();
7613 if self.home.browsing.is_none()
7614 && section_title == home::HomeState::CLOUD_SECTION
7615 && let Some(id) = self
7616 .home
7617 .selected_entry()
7618 .and_then(|e| home::cloud_source_id(&e.path))
7619 {
7620 self.cache.hide_cloud_source(&id);
7621 self.home.cloud.retain(|s| s.id != id);
7622 self.home_refresh();
7623 return;
7624 }
7625 let in_recents = section_title == "Recent";
7626 if !in_recents {
7627 self.home.status = Some("Only recents and catalog.toml rows can be forgotten".into());
7628 return;
7629 }
7630 let Some(entry) = self.home.selected_entry() else {
7631 return;
7632 };
7633 self.cache.forget_recent(&entry.path);
7634 self.home.status = None;
7636 self.home_refresh();
7637 }
7638
7639 fn home_row_for_catalog(&self) -> Option<(PathBuf, String)> {
7644 match self.home.selected_row()? {
7645 home::Row::Place { path, .. } => {
7646 let name = home::display_path(&path);
7647 Some((path, name))
7648 }
7649 home::Row::Door { entry, .. } => {
7650 let path: PathBuf = if matches!(
7653 source::input_source(&entry.path),
7654 source::InputSource::Local(_)
7655 ) {
7656 entry.path.components().collect()
7657 } else {
7658 entry.path.clone()
7659 };
7660 Some((path.clone(), home::display_path(&path)))
7661 }
7662 home::Row::Entry { entry, .. } => (entry.table.is_none()
7663 && !home::is_cloud_place(&entry.path)
7664 && crate::members::split(&entry.path).is_none())
7665 .then(|| (entry.path.clone(), entry.name.clone())),
7666 home::Row::Header { section, .. } => {
7667 let root = self.home.sections.get(section)?.root.clone()?;
7668 let name = home::display_path(&root);
7669 Some((root, name))
7670 }
7671 home::Row::More { .. } | home::Row::Hidden { .. } => None,
7672 }
7673 }
7674
7675 fn mine_catalog_file(&self) -> Option<PathBuf> {
7677 let dir = match &self.app_config.catalog_dir {
7678 Some(dir) => dir.clone(),
7679 None => config::ConfigManager::new(APP_NAME)
7680 .ok()?
7681 .config_dir()
7682 .to_path_buf(),
7683 };
7684 Some(dir.join(catalog::MINE_FILE))
7685 }
7686
7687 fn mine_entry_at(&self, location: &Path) -> Option<(String, String)> {
7689 self.app_config
7690 .read_catalogs
7691 .iter()
7692 .find(|c| c.origin == catalog::Origin::Mine)?
7693 .dataset_at(location)
7694 .map(|d| (d.id.clone(), d.name.clone()))
7695 }
7696
7697 fn reload_catalogs(&mut self) -> Result<(), String> {
7699 let dir = self
7700 .mine_catalog_file()
7701 .and_then(|f| f.parent().map(Path::to_path_buf));
7702 self.app_config
7703 .read_catalog_files(dir.as_deref())
7704 .map_err(|e| e.to_string())?;
7705 let shown = home::catalogs(&self.app_config);
7707 let path = self.path.clone();
7708 self.codebook = path.as_deref().and_then(|p| home::codebook_for(&shown, p));
7709 self.catalog_entry = path
7710 .as_deref()
7711 .and_then(|p| home::catalog_entry_for(&shown, p));
7712 self.open_info_documentation();
7713 self.home_refresh();
7714 Ok(())
7715 }
7716
7717 fn new_catalog_dataset(&self, location: &Path, name: &str) -> catalog::NewDataset {
7720 if let Some((_, shown)) = self.home.catalog_dataset(location) {
7721 let entry = &shown.entry;
7722 return catalog::NewDataset {
7723 name: entry.name.clone(),
7724 path: entry.local_path().map(|p| home::display_path(&p)),
7725 url: entry.url.clone(),
7726 auth: entry.auth.clone(),
7727 connection: entry.connection.clone(),
7728 description: entry.description.clone(),
7729 size: entry.size,
7730 };
7731 }
7732 let mut new = catalog::NewDataset {
7733 name: name.to_string(),
7734 ..Default::default()
7735 };
7736 if matches!(
7737 source::input_source(location),
7738 source::InputSource::Local(_)
7739 ) {
7740 let absolute = if location.is_relative() {
7741 std::env::current_dir()
7742 .map(|cwd| cwd.join(location))
7743 .unwrap_or_else(|_| location.to_path_buf())
7744 } else {
7745 location.to_path_buf()
7746 };
7747 new.path = Some(home::display_path(&absolute));
7748 return new;
7749 }
7750 let text = location.to_string_lossy();
7753 let (id, plain) = source::split_source_id(&text);
7754 new.url = Some(plain.into_owned());
7755 if let Some(id) = id {
7756 new.connection = Some(id.to_string());
7757 }
7758 new
7759 }
7760
7761 fn home_toggle_catalog(&mut self) {
7763 let Some((location, name)) = self.home_row_for_catalog() else {
7764 self.home.status = Some("Move to a dataset or directory to add it".into());
7765 return;
7766 };
7767 if let Some((id, name)) = self.mine_entry_at(&location) {
7768 self.home_forget_from_catalog(&id, &name);
7769 return;
7770 }
7771 let Some(file) = self.mine_catalog_file() else {
7772 self.home.status = Some("No config directory to keep catalog.toml in".into());
7773 return;
7774 };
7775 let new = self.new_catalog_dataset(&location, &name);
7776 if let Some(connection) = &new.connection
7779 && !self
7780 .app_config
7781 .cloud
7782 .connections
7783 .iter()
7784 .any(|c| c.name == *connection)
7785 {
7786 self.home.status = Some(format!(
7787 "Not added: {connection} is not a [[cloud.connections]] entry in the config"
7788 ));
7789 return;
7790 }
7791 if let Err(why) = new.check() {
7792 self.home.status = Some(format!("Not added: {why}"));
7793 return;
7794 }
7795 let label = self
7796 .app_config
7797 .read_catalogs
7798 .iter()
7799 .find(|c| c.origin == catalog::Origin::Mine)
7800 .map(|c| c.label.clone())
7801 .unwrap_or_else(|| catalog::MINE_LABEL.to_string());
7802 match catalog::add(&file, &new) {
7803 Ok(_) => match self.reload_catalogs() {
7804 Ok(()) => self.flash_note(format!("Added {} to {label}", new.name)),
7805 Err(e) => self.error_modal.show(e),
7806 },
7807 Err(e) => self.error_modal.show(e.to_string()),
7808 }
7809 }
7810
7811 fn home_forget_from_catalog(&mut self, id: &str, name: &str) {
7813 let Some(file) = self.mine_catalog_file() else {
7814 return;
7815 };
7816 match catalog::forget(&file, id) {
7817 Ok(()) => match self.reload_catalogs() {
7818 Ok(()) => self.flash_note(format!("Forgot {name}")),
7819 Err(e) => self.error_modal.show(e),
7820 },
7821 Err(e) => self.error_modal.show(e.to_string()),
7822 }
7823 }
7824
7825 fn move_remembered_places(&mut self) {
7828 if std::mem::replace(&mut self.remembered_moved, true) {
7829 return;
7830 }
7831 let places = self.cache.load_remembered_places();
7832 if places.is_empty() {
7833 return;
7834 }
7835 let Some(file) = self.mine_catalog_file() else {
7836 return;
7837 };
7838 match catalog::move_places(&file, &places) {
7841 Ok(_) => self.cache.clear_remembered_places(),
7842 Err(e) => {
7843 log::warn!(target: "datui", "moving remembered places into catalog.toml: {e:#}")
7844 }
7845 }
7846 let dir = file.parent().map(Path::to_path_buf);
7847 if let Err(e) = self.app_config.read_catalog_files(dir.as_deref()) {
7848 log::warn!(target: "datui", "reading catalog.toml: {e:#}");
7849 }
7850 }
7851
7852 fn open_info_documentation(&mut self) {
7855 self.info_documentation.close();
7856 let spec = self.data_table_state.as_ref().and_then(|state| {
7857 state
7858 .format_read()
7859 .map(|read| read.spec.clone())
7860 .or_else(|| state.delimited_read().map(|read| read.spec.clone()))
7861 });
7862 let name = self
7863 .path
7864 .as_deref()
7865 .and_then(Path::file_name)
7866 .map(|n| n.to_string_lossy().into_owned())
7867 .unwrap_or_default();
7868 if let Some(doc) = widgets::documentation::Documented::new(
7869 self.catalog_entry.clone(),
7870 spec.and_then(|s| s.docs()).map(std::sync::Arc::new),
7871 name,
7872 ) {
7873 self.info_documentation.open(doc, None);
7874 self.info_documentation.links_open = self.local_desktop;
7875 }
7876 }
7877
7878 fn home_open_documentation(&mut self) {
7881 let Some((path, doc)) = self.home_documented_row() else {
7882 self.home.status =
7883 Some("Ctrl+E shows what a catalog or a format spec says of a row".into());
7884 return;
7885 };
7886 let measured = self
7887 .home
7888 .selected_entry()
7889 .filter(|e| {
7890 e.path == path
7891 && doc
7892 .catalog
7893 .as_ref()
7894 .is_some_and(|(_, entry)| entry.location() == path)
7895 })
7896 .and_then(|e| e.size);
7897 self.documentation.open(doc, measured);
7898 self.documentation.links_open = self.local_desktop;
7899 }
7900
7901 pub(crate) fn home_documented_row(
7905 &self,
7906 ) -> Option<(PathBuf, widgets::documentation::Documented)> {
7907 let row = self.home.selected_row();
7908 let (path, file) = match &row {
7909 Some(home::Row::Entry { entry, .. }) | Some(home::Row::Door { entry, .. }) => {
7910 (Some(entry.path.clone()), Some(*entry))
7911 }
7912 Some(home::Row::Place { path, .. }) => (Some(path.clone()), None),
7913 Some(home::Row::Header { section, .. }) => (
7914 self.home
7915 .sections
7916 .get(*section)
7917 .and_then(|s| s.root.clone()),
7918 None,
7919 ),
7920 _ => (None, None),
7921 };
7922 let path = path?;
7923 let catalog = home::catalog_entry_for(&self.home.catalogs, &path);
7924 let spec = file
7925 .filter(|e| e.kind == discover::EntryKind::File)
7926 .and_then(|e| e.format_spec.as_deref())
7927 .and_then(|name| self.home.formats.get(name))
7928 .and_then(|spec| spec.docs())
7929 .map(std::sync::Arc::new);
7930 let name = file
7932 .map(|e| {
7933 let text = e.path.to_string_lossy();
7934 text.strip_suffix(e.name.as_str())
7935 .filter(|_| e.table.is_some())
7936 .map(|file| file.trim_end_matches(std::path::is_separator))
7937 .and_then(|file| Path::new(file).file_name())
7938 .map_or_else(|| e.name.clone(), |n| n.to_string_lossy().into_owned())
7939 })
7940 .unwrap_or_default();
7941 let doc = widgets::documentation::Documented::new(catalog, spec, name)?;
7942 Some((path, doc))
7943 }
7944
7945 pub(crate) fn home_hides_catalog(&self) -> bool {
7950 self.home
7951 .selected_catalog()
7952 .is_some_and(|c| c.origin == crate::catalog::Origin::Bundled)
7953 }
7954
7955 pub(crate) fn home_catalog_action(&self) -> Option<&'static str> {
7956 let (location, _) = self.home_row_for_catalog()?;
7957 Some(if self.mine_entry_at(&location).is_some() {
7958 "Forget"
7959 } else {
7960 "Add"
7961 })
7962 }
7963
7964 fn documentation_key(&mut self, event: &KeyEvent) {
7966 let page = self.documentation.view_height.max(1) as isize;
7967 match event.code {
7968 KeyCode::Esc | KeyCode::Char('q') | KeyCode::Left => self.documentation.close(),
7969 KeyCode::Up | KeyCode::Char('k') => self.documentation.move_cursor(-1),
7970 KeyCode::Down | KeyCode::Char('j') => self.documentation.move_cursor(1),
7971 KeyCode::PageUp => self.documentation.move_cursor(-page),
7972 KeyCode::PageDown => self.documentation.move_cursor(page),
7973 KeyCode::Home | KeyCode::Char('g') => self.documentation.move_cursor(isize::MIN / 2),
7974 KeyCode::End | KeyCode::Char('G') => self.documentation.move_cursor(isize::MAX / 2),
7975 KeyCode::Enter | KeyCode::Char(' ') | KeyCode::Right => {
7976 self.documentation.toggle_legend();
7977 }
7978 KeyCode::Char('y') => self.copy_documentation_line(),
7979 KeyCode::Char('o') => {
7980 let link = self.documentation.link();
7981 self.ask_to_open_link(link);
7982 }
7983 KeyCode::Char('?') => self.open_help_overlay(),
7985 _ => {}
7986 }
7987 }
7988
7989 fn copy_documentation_line(&mut self) {
7991 match self.documentation.copy_text() {
7992 Some(text) => self.copy_documentation_text(text),
7993 None => self.flash_note("Nothing to copy on this line".to_string()),
7994 }
7995 }
7996
7997 pub(crate) fn ask_to_open_link(&mut self, link: Option<String>) {
8001 let Some(link) = link else {
8002 return;
8003 };
8004 if !self.local_desktop {
8005 self.flash_note("o opens links on a local desktop; y copies it".to_string());
8006 return;
8007 }
8008 match link_open::checked_url(&link) {
8009 Ok(url) => {
8010 self.confirmation_modal
8011 .show_choice(format!("Open {url}?"), "Open", "Cancel");
8012 self.pending_link = Some(url);
8013 }
8014 Err(why) => self.flash_note(format!("Not opened: {why}; y copies it")),
8015 }
8016 }
8017
8018 pub(crate) fn copy_documentation_text(&mut self, text: String) {
8020 let shown: String = text.chars().take(60).collect();
8021 let said = if shown.len() < text.len() {
8022 format!("Copied {shown}{}", glyphs::get().ellipsis)
8023 } else {
8024 format!("Copied {shown}")
8025 };
8026 self.finish_copy(clipboard::Payload::text(text), said);
8027 }
8028
8029 fn home_toggle_fold(&mut self) {
8035 if let Some(section) = self.home.selected_section() {
8036 self.home.toggle_collapsed(section);
8037 self.home.clamp_selection();
8038 self.cache.save_folds(&self.home.folds);
8039 }
8040 }
8041
8042 fn home_collapse(&mut self, collapse: bool) {
8043 if self.home.browsing.is_some() {
8046 return;
8047 }
8048 let Some(section) = self.home.selected_section() else {
8049 return;
8050 };
8051 if collapse && !self.home.is_collapsed(section) {
8052 self.home.set_collapsed(section, true);
8053 if let Some(idx) = self
8054 .home
8055 .visible()
8056 .iter()
8057 .position(|row| row.section() == section)
8058 {
8059 self.home.selected = idx;
8060 }
8061 } else if !collapse {
8062 self.home.set_collapsed(section, false);
8063 }
8064 self.home.clamp_selection();
8065 self.cache.save_folds(&self.home.folds);
8066 }
8067
8068 fn home_ascend(&mut self) {
8070 let Some(current) = self.home.browsing.clone() else {
8071 return;
8072 };
8073 let parent = self.home.parent_of(¤t);
8074 self.home_leave_browsing(parent);
8075 }
8076
8077 fn home_leave_browsing(&mut self, to: Option<PathBuf>) {
8079 self.home.status = None;
8082 let from = std::mem::replace(&mut self.home.browsing, to);
8083 if !self.home.below_browse_start() {
8086 self.home.browse_start = self.home.browsing.clone();
8087 }
8088 self.home.come_back(from);
8092 self.home.sync_search_section();
8093 self.home.selected = 0;
8094 self.home_refresh();
8095 if !self.home.filter.is_empty() {
8096 self.spawn_home_search();
8097 }
8098 }
8099
8100 #[cfg(feature = "cloud")]
8126 fn peek_tells_a_row_something(answer: &(discover::EntryKind, discover::Holds)) -> bool {
8127 answer.0 != discover::EntryKind::Directory || !answer.1.is_empty()
8128 }
8129
8130 #[cfg(feature = "cloud")]
8145 fn peek_cloud_directories(&mut self) {
8146 const PEEKS_AT_ONCE: usize = 4;
8147 let directories = self.home.cloud_directories_to_peek(PEEKS_AT_ONCE);
8148 if directories.is_empty() {
8149 return;
8150 }
8151 for directory in &directories {
8155 self.home.peeking.insert(directory.clone());
8156 }
8157 let tx = self.events.clone();
8158 let cloud = self.app_config.cloud.clone();
8159 self.runtime.spawn(async move {
8160 let permits = Arc::new(tokio::sync::Semaphore::new(PEEKS_AT_ONCE));
8161 let mut peeks = tokio::task::JoinSet::new();
8162 let mut asked = std::collections::HashMap::new();
8165 for directory in directories {
8166 let (permits, cloud) = (permits.clone(), cloud.clone());
8167 let task_directory = directory.clone();
8168 let task = peeks.spawn(async move {
8169 let directory = task_directory;
8170 let _permit = permits.acquire_owned().await;
8171 let kind =
8172 crate::cloud_browse::peek_kind(&directory.to_string_lossy(), &cloud).await;
8173 (directory, kind)
8174 });
8175 asked.insert(task.id(), directory);
8176 }
8177 let mut found = Vec::new();
8187 let mut failed = Vec::new();
8188 while let Some(joined) = peeks.join_next_with_id().await {
8189 match joined {
8190 Ok((_, (directory, Ok(answer)))) => {
8191 let answer = Some(answer)
8192 .filter(Self::peek_tells_a_row_something)
8193 .unwrap_or((discover::EntryKind::Directory, Default::default()));
8194 found.push((directory, answer));
8195 }
8196 Ok((_, (directory, Err(_)))) => failed.push(directory),
8197 Err(error) => failed.extend(asked.remove(&error.id())),
8198 }
8199 if found.len() + failed.len() >= PEEKS_AT_ONCE {
8200 let _ = tx.send(AppEvent::HomeCloudKinds {
8201 kinds: std::mem::take(&mut found),
8202 failed: std::mem::take(&mut failed),
8203 });
8204 }
8205 }
8206 if !found.is_empty() || !failed.is_empty() {
8207 let _ = tx.send(AppEvent::HomeCloudKinds {
8208 kinds: found,
8209 failed,
8210 });
8211 }
8212 });
8213 }
8214
8215 fn home_browse_into(&mut self, path: PathBuf) {
8217 self.home.leave_mark();
8218 if self.home.browsing.is_none() {
8219 self.home.browse_start = Some(path.clone());
8220 } else if self.home.browse_start.is_none() {
8221 self.home.browse_start = self.home.browsing.clone();
8222 }
8223 self.home.browsing = Some(path);
8224 self.home.status = None;
8230 self.home.search.reset();
8231 self.home.filter.clear();
8232 self.home.sync_search_section();
8233 self.home.selected = 0;
8234 self.home_refresh();
8235 }
8236
8237 fn selection_opens_the_whole_directory(&self) -> bool {
8245 self.home.selection_is_the_door()
8246 }
8247
8248 fn selected_directory_to_enter(&self) -> Option<PathBuf> {
8261 if let Some(home::Row::Place { path, .. }) = self.home.selected_row() {
8264 return home::place_is_browsable(&path).then_some(path);
8265 }
8266 let entry = self.home.selected_entry()?;
8267 if self.selection_opens_the_whole_directory() || self.home.missing.contains(&entry.path) {
8268 return None;
8269 }
8270 if entry.cost.tables.is_some() {
8272 return Some(entry.path);
8273 }
8274 (!matches!(
8275 entry.kind,
8276 discover::EntryKind::File | discover::EntryKind::Other
8277 ))
8278 .then_some(entry.path)
8279 }
8280
8281 #[cfg(feature = "cloud")]
8295 pub(crate) fn why_a_door_reads_nothing(entry: &discover::Entry) -> Option<String> {
8296 if !home::is_object_store_url(&entry.path)
8297 || matches!(
8298 entry.kind,
8299 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8300 )
8301 || Self::cloud_prefix_format(&entry.holds).is_some()
8302 {
8303 return None;
8304 }
8305 Self::why_a_cloud_prefix_cannot_be_read(&entry.holds)
8306 }
8307
8308 #[cfg(feature = "cloud")]
8309 fn why_a_cloud_prefix_cannot_be_read(holds: &discover::Holds) -> Option<String> {
8310 let reads_parquet =
8311 |name: &str| crate::FileFormat::from_name(name) == Some(crate::FileFormat::Parquet);
8312 if holds.formats.iter().any(|(name, _)| reads_parquet(name)) {
8313 return None;
8314 }
8315 match holds.formats.as_slice() {
8316 [] => {
8320 (holds.not_read > 0 && holds.directories == 0).then(|| {
8324 "this prefix holds nothing datui can read — datui reads a directory in \
8325 an object store as Parquet only."
8326 .to_string()
8327 })
8328 }
8329 formats => {
8330 let held = formats
8331 .iter()
8332 .map(|(name, count)| format!("{count} {name}"))
8333 .collect::<Vec<_>>()
8334 .join(", ");
8335 Some(format!(
8336 "this prefix holds {held} — datui reads a directory in an object store \
8337 as Parquet only. Open one of the files below instead."
8338 ))
8339 }
8340 }
8341 }
8342
8343 #[cfg(feature = "cloud")]
8354 fn cloud_prefix_format(
8355 holds: &discover::Holds,
8356 ) -> Option<(FileFormat, Vec<(FileFormat, usize)>)> {
8357 if holds.dataset_dict {
8359 return Some((FileFormat::Arrow, Vec::new()));
8360 }
8361 if let Some((name, _)) = holds.model_weights() {
8364 return FileFormat::from_name(name).map(|format| (format, Vec::new()));
8365 }
8366 let (name, _) = holds.formats.first()?;
8367 let format = FileFormat::from_name(name)
8370 .filter(|f| f.reads_many_files() && !f.reads_into() && !f.is_lines())?;
8371 let left_out = holds
8375 .formats
8376 .iter()
8377 .skip(1)
8378 .filter_map(|(name, n)| FileFormat::from_name(name).map(|f| (f, *n)))
8379 .collect();
8380 Some((format, left_out))
8381 }
8382
8383 fn home_open_selected(&mut self) -> Option<AppEvent> {
8386 match self.home.selected_row() {
8387 Some(home::Row::Place { path, .. }) => {
8390 if home::place_is_browsable(&path) {
8391 self.home_browse_into(path);
8392 } else {
8393 self.home.status = Some(
8394 "An HTTP server has no listing to browse. Open a file under it".into(),
8395 );
8396 }
8397 return None;
8398 }
8399 Some(home::Row::More { .. }) => {
8401 self.home.recent_expanded = true;
8402 return None;
8403 }
8404 Some(home::Row::Hidden { .. }) => {
8407 self.home.hide_unreadable = false;
8408 if let Some(idx) = self.home.visible().iter().position(|row| {
8409 matches!(row, home::Row::Entry { entry, .. }
8410 if entry.hidden_by_default())
8411 }) {
8412 self.home.selected = idx;
8413 }
8414 return None;
8415 }
8416 _ => {}
8417 }
8418 if self.home.selection_is_header() {
8419 self.home_toggle_fold();
8420 return None;
8421 }
8422 let entry = self.home.selected_entry()?;
8423 if self.home.missing.contains(&entry.path) {
8425 self.home.status = Some(format!(
8426 "{} does not exist",
8427 home::display_path(&entry.path)
8428 ));
8429 return None;
8430 }
8431 if entry.kind != discover::EntryKind::File && self.home.bookmark(&entry.path).is_some() {
8434 #[cfg(feature = "cloud")]
8435 let reader = if home::is_object_store_url(&entry.path)
8436 && !matches!(
8437 entry.kind,
8438 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8439 ) {
8440 Self::cloud_prefix_format(&entry.holds)
8441 } else {
8442 None
8443 };
8444 #[cfg(not(feature = "cloud"))]
8445 let reader = None;
8446 let directory = home::directory_dataset_url(&entry.path);
8447 return Some(self.home_open_directory_as(directory, true, None, reader));
8448 }
8449 if self.selection_opens_the_whole_directory() {
8455 let lake = entry.kind.lake_name();
8466 #[cfg(feature = "cloud")]
8476 let reader = if home::is_object_store_url(&entry.path)
8477 && !matches!(
8478 entry.kind,
8479 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8480 ) {
8481 let reader = Self::cloud_prefix_format(&entry.holds);
8482 if reader.is_none()
8485 && let Some(what) = Self::why_a_cloud_prefix_cannot_be_read(&entry.holds)
8486 {
8487 self.home.status = Some(what);
8488 return None;
8489 }
8490 reader
8491 } else {
8492 None
8493 };
8494 #[cfg(not(feature = "cloud"))]
8495 let reader = None;
8496 let directory = home::directory_dataset_url(&entry.path);
8501 return Some(self.home_open_directory_as(directory, true, lake, reader));
8502 }
8503 let mut entry = entry;
8508 if entry.kind == discover::EntryKind::Unknown {
8509 if self.looking_could_block(&entry.path) {
8510 return Some(AppEvent::ClassifyThenOpen {
8511 path: entry.path,
8512 jump: false,
8513 });
8514 }
8515 if entry.path.is_dir() {
8516 entry.kind = discover::classify_directory(&entry.path);
8517 }
8518 }
8519 if entry.enter_lists_tables() {
8522 self.home_browse_into(entry.path);
8523 return None;
8524 }
8525 self.open_what_it_is(entry.path, entry.kind, false)
8526 }
8527
8528 fn looking_could_block(&self, path: &Path) -> bool {
8539 !home::is_cloud_place(path)
8543 && matches!(source::input_source(path), source::InputSource::Local(_))
8544 && (self.home.network_check)(path)
8545 }
8546
8547 fn open_what_it_is(
8553 &mut self,
8554 path: PathBuf,
8555 kind: discover::EntryKind,
8556 jump: bool,
8557 ) -> Option<AppEvent> {
8558 let go_inside = |app: &mut Self, path: PathBuf| {
8559 if jump {
8560 app.home_jump_into(path);
8561 } else {
8562 app.home_browse_into(path);
8563 }
8564 };
8565 if kind == discover::EntryKind::Directory {
8566 go_inside(self, path);
8567 return None;
8568 }
8569 if kind == discover::EntryKind::Other {
8572 if matches!(source::input_source(&path), source::InputSource::Local(_)) {
8573 self.open_hex(path, crate::hex_view::Origin::Home, true, None);
8574 }
8575 return None;
8576 }
8577 if let Some(format) = kind.lake_name() {
8582 self.home.lake_here = Some((path.clone(), format));
8583 go_inside(self, path);
8584 return None;
8585 }
8586 let directory = matches!(
8587 kind,
8588 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8589 );
8590 if directory && jump {
8593 go_inside(self, path);
8594 return None;
8595 }
8596 if directory && home::is_object_store_url(&path) {
8599 return Some(self.home_open_path(home::directory_dataset_url(&path), false));
8601 }
8602 let a_spec_may_read = !self.formats.by_glob(&path, false).is_empty()
8607 || self.formats.specs.iter().any(|f| !f.spec.magic.is_empty());
8608 if kind == discover::EntryKind::File
8612 && discover::unreadable_by_name(&path)
8613 && !a_spec_may_read
8614 && crate::members::split(&path).is_none()
8615 && crate::members::holder(&path).is_none()
8616 && crate::members::split_variant(&path, &self.formats).is_none()
8617 && crate::hf_splits::split_place(&path).is_none()
8618 {
8619 self.home.status = Some(discover::NO_READER.to_string());
8620 return None;
8621 }
8622 let prepared = (!directory)
8625 .then(|| self.home_previews.take_prepared(&path))
8626 .flatten();
8627 let unasked = self
8630 .home
8631 .catalogs
8632 .iter()
8633 .filter(|c| c.origin == catalog::Origin::Bundled)
8634 .flat_map(|c| c.datasets.iter())
8635 .find(|d| d.location == path)
8636 .filter(|_| {
8637 !jump && matches!(source::input_source(&path), source::InputSource::Http(_))
8638 })
8639 .map(|dataset| UnaskedDownload {
8640 limit: UnaskedDownload::LIMIT,
8641 listed: dataset.size,
8642 });
8643 match self.home_open_path(path, directory) {
8644 AppEvent::Open(paths, mut options) => {
8645 options.prepared = prepared.map(|p| Arc::new(Mutex::new(Some(p))));
8646 options.download_unasked = unasked;
8647 Some(AppEvent::Open(paths, options))
8648 }
8649 event => Some(event),
8650 }
8651 }
8652
8653 pub fn route_named_paths(paths: Vec<PathBuf>, options: OpenOptions) -> AppEvent {
8675 Self::route_named_paths_with(paths, options, &crate::formats::Registry::default())
8676 }
8677
8678 pub fn route_named_paths_with(
8681 paths: Vec<PathBuf>,
8682 options: OpenOptions,
8683 formats: &crate::formats::Registry,
8684 ) -> AppEvent {
8685 if let Some(event) = Self::route_named_without_looking(&paths, &options) {
8686 return event;
8687 }
8688 let single = (paths.len() == 1 && !options.hive).then(|| paths[0].clone());
8691 let Some(dir) = single.filter(|p| p.is_dir()) else {
8692 return AppEvent::Open(paths, options);
8693 };
8694 if options.spec_file.is_some()
8696 || options.spec_name.is_some()
8697 || !formats.by_glob(&dir, true).is_empty()
8698 {
8699 return AppEvent::Open(paths, options);
8700 }
8701 AppEvent::LookThenOpenDirectory(dir, options)
8706 }
8707
8708 fn route_named_without_looking(paths: &[PathBuf], options: &OpenOptions) -> Option<AppEvent> {
8714 #[cfg(feature = "cloud")]
8715 if let [dir] = paths
8716 && !options.hive
8717 && home::is_object_store_url(dir)
8718 && options.format.is_none()
8719 && !dir.to_string_lossy().contains('*')
8720 && !home::names_a_file(dir)
8721 {
8722 return Some(AppEvent::LookThenOpenDirectory(
8723 dir.clone(),
8724 options.clone(),
8725 ));
8726 }
8727 let _ = (paths, options);
8728 None
8729 }
8730
8731 pub fn missing_named_path(
8734 paths: &[PathBuf],
8735 formats: &crate::formats::Registry,
8736 ) -> Option<PathBuf> {
8737 paths
8738 .iter()
8739 .find(|path| {
8740 !source::is_remote_url(path)
8741 && !crate::stdin::is_stdin(path)
8742 && !source::expands_as_glob(path)
8743 && !path.exists()
8744 && crate::members::split(path).is_none()
8745 && crate::members::split_variant(path, formats).is_none()
8746 })
8747 .cloned()
8748 }
8749
8750 fn open_the_directory_looked_at(
8755 &mut self,
8756 dir: PathBuf,
8757 kind: discover::EntryKind,
8758 holds: Option<&discover::Holds>,
8759 mut options: OpenOptions,
8760 ) -> Option<AppEvent> {
8761 #[cfg(feature = "cloud")]
8762 if home::is_object_store_url(&dir) {
8763 return self.open_the_cloud_directory_looked_at(dir, kind, holds, options);
8764 }
8765 let _ = holds;
8766 if let Some(format) = kind.lake_name() {
8776 self.enter_home();
8777 self.home.lake_here = Some((dir.clone(), format));
8778 self.home_jump_into(dir);
8779 return None;
8780 }
8781 if matches!(
8784 kind,
8785 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8786 ) {
8787 options.hive = true;
8788 self.set_loading_phase("Scanning input", 10);
8789 self.name_what_is_loading(dir.clone());
8790 return Some(AppEvent::Open(vec![dir], options));
8791 }
8792 self.enter_home();
8796 self.home_jump_into(dir);
8797 None
8798 }
8799
8800 #[cfg(feature = "cloud")]
8804 fn open_the_cloud_directory_looked_at(
8805 &mut self,
8806 dir: PathBuf,
8807 kind: discover::EntryKind,
8808 holds: Option<&discover::Holds>,
8809 options: OpenOptions,
8810 ) -> Option<AppEvent> {
8811 let open = |app: &mut Self, path: PathBuf, options: OpenOptions| {
8812 app.set_loading_phase("Scanning input", 10);
8813 app.name_what_is_loading(path.clone());
8814 Some(AppEvent::Open(vec![path], options))
8815 };
8816 let Some(holds) = holds else {
8819 return open(self, dir, options);
8820 };
8821 if let Some(format) = kind.lake_name() {
8822 self.enter_home();
8823 self.home.lake_here = Some((dir.clone(), format));
8824 self.home_jump_into(dir);
8825 return None;
8826 }
8827 let directory = home::directory_dataset_url(&dir);
8828 if matches!(
8829 kind,
8830 discover::EntryKind::Hive | discover::EntryKind::MultiFile
8831 ) {
8832 let options = OpenOptions {
8833 hive: true,
8834 ..options
8835 };
8836 return open(self, directory, options);
8837 }
8838 if let Some((format, left_out)) = Self::cloud_prefix_format(holds) {
8839 let options = OpenOptions {
8840 hive: true,
8841 format: Some(format),
8842 left_out,
8843 ..options
8844 };
8845 return open(self, directory, options);
8846 }
8847 self.enter_home();
8850 self.home_jump_into(dir);
8851 self.home.status = Self::why_a_cloud_prefix_cannot_be_read(holds);
8852 None
8853 }
8854
8855 fn home_jump_into(&mut self, path: PathBuf) {
8860 self.home.trail.retain(|mark| mark.place.is_none());
8863 if self.home.browsing.is_none() {
8864 self.home.leave_mark();
8865 }
8866 self.home.browse_start = Some(path.clone());
8867 self.home.browsing = Some(path);
8868 self.home.status = None;
8869 self.home.search.reset();
8870 self.home.filter.clear();
8871 self.home.sync_search_section();
8872 self.home.selected = 0;
8873 self.home_refresh();
8874 }
8875
8876 fn open_defaults(&self) -> OpenOptions {
8879 match crate::cli::parse_args(["datui"]) {
8880 Ok(args) => OpenOptions::from_args_and_config(&args, &self.app_config),
8881 Err(_) => OpenOptions::default(),
8882 }
8883 }
8884
8885 fn home_open_path(&mut self, path: PathBuf, hive: bool) -> AppEvent {
8890 self.home_open_directory(path, hive, None)
8891 }
8892
8893 fn home_open_directory(
8896 &mut self,
8897 path: PathBuf,
8898 hive: bool,
8899 lake: Option<&'static str>,
8900 ) -> AppEvent {
8901 self.home_open_directory_as(path, hive, lake, None)
8902 }
8903
8904 fn home_open_directory_as(
8911 &mut self,
8912 path: PathBuf,
8913 hive: bool,
8914 lake: Option<&'static str>,
8915 reader: Option<(FileFormat, Vec<(FileFormat, usize)>)>,
8916 ) -> AppEvent {
8917 let (format, left_out) = match reader {
8918 Some((format, left_out)) => (Some(format), left_out),
8919 None => (None, Vec::new()),
8920 };
8921 let options = OpenOptions {
8926 hive,
8927 read_as_plain_files_of: lake,
8928 format,
8929 left_out,
8930 ..self.open_defaults()
8931 };
8932 self.input_mode = InputMode::Normal;
8933 self.announce_open(true, "Scanning input".to_string(), 10);
8935 self.name_what_is_loading(path.clone());
8940 AppEvent::Open(vec![path], options)
8941 }
8942
8943 fn home_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
8945 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
8946 self.home.status = None;
8950
8951 if self.documentation.is_open() {
8952 self.documentation_key(event);
8953 return None;
8954 }
8955
8956 if self.home.path_input_active {
8961 match event.code {
8962 KeyCode::Esc => {
8963 self.home.path_input_active = false;
8964 self.home.path_input.clear();
8965 self.home.path_listing = None;
8966 self.home.path_pick = None;
8967 self.home.status = None;
8968 }
8969 KeyCode::Up | KeyCode::Down => {
8972 let n = self.home.path_candidates().len();
8973 self.home.path_pick = match (event.code, self.home.path_pick) {
8974 _ if n == 0 => None,
8975 (KeyCode::Down, None) => Some(0),
8976 (KeyCode::Down, Some(i)) => Some((i + 1).min(n - 1)),
8977 (KeyCode::Up, Some(0)) | (KeyCode::Up, None) => None,
8978 (KeyCode::Up, Some(i)) => Some(i - 1),
8979 (_, pick) => pick,
8980 };
8981 }
8982 KeyCode::Enter => {
8983 if let Some(picked) = self.home.picked_path() {
8984 self.home.path_input = picked;
8985 self.home.path_pick = None;
8986 }
8987 let raw = self.home.path_input.trim().to_string();
8988 if raw.is_empty() {
8989 self.home.path_input_active = false;
8990 return None;
8991 }
8992 let path = home::expand_user_path(&raw);
8993 if home::is_object_store_url(&path) || home::is_cloud_place(&path) {
8998 self.home.path_input.clear();
8999 self.home.path_input_active = false;
9000 let kind = if home::names_a_file(&path) {
9001 discover::EntryKind::File
9002 } else {
9003 discover::EntryKind::Directory
9004 };
9005 return self.open_what_it_is(path, kind, true);
9006 }
9007 if !matches!(source::input_source(&path), source::InputSource::Local(_)) {
9009 self.home.path_input.clear();
9010 self.home.path_input_active = false;
9011 return self.open_what_it_is(path, discover::EntryKind::File, true);
9012 }
9013 if self.looking_could_block(&path) {
9018 self.home.path_input.clear();
9019 self.home.path_input_active = false;
9020 return Some(AppEvent::ClassifyThenOpen { path, jump: true });
9021 }
9022 if !path.exists()
9025 && crate::members::split(&path).is_none()
9026 && crate::members::split_variant(&path, &self.formats).is_none()
9027 {
9028 self.home.status = Some(format!("No such path: {}", path.display()));
9029 return None;
9030 }
9031 self.home.path_input.clear();
9032 self.home.path_input_active = false;
9033 let kind = if path.is_dir() {
9034 discover::classify_directory(&path)
9035 } else {
9036 discover::EntryKind::File
9037 };
9038 return self.open_what_it_is(path, kind, true);
9039 }
9040 KeyCode::Backspace => {
9041 self.home.path_input.pop();
9042 self.home.status = None;
9043 }
9044 KeyCode::Char('u') if ctrl => self.home.path_input.clear(),
9045 KeyCode::Tab => {
9049 let completed = match self.home.path_pick {
9050 Some(i) if i > 0 => self.home.picked_path(),
9051 _ => self.home.path_completion(),
9052 };
9053 let listed = self
9054 .home
9055 .path_listing
9056 .as_ref()
9057 .is_some_and(|l| l.dir == home::typed_dir(&self.home.path_input));
9058 match completed {
9059 Some(completed) => self.home.path_input = completed,
9060 None if !listed => self.request_path_completion(),
9061 None => {}
9062 }
9063 }
9064 KeyCode::Char(c) if !ctrl => {
9065 self.home.path_input.push(c);
9066 self.home.status = None;
9067 }
9068 _ => {}
9069 }
9070 self.list_the_typed_directory();
9073 if !matches!(event.code, KeyCode::Up | KeyCode::Down) {
9074 self.home.pick_first_path();
9075 }
9076 return None;
9077 }
9078
9079 if std::mem::take(&mut self.home.filter_selected) {
9082 let replaces = match event.code {
9083 KeyCode::Char(_) => !ctrl,
9084 KeyCode::Backspace => true,
9085 _ => false,
9086 };
9087 if replaces {
9088 self.home.filter.clear();
9089 self.home.sync_search_section();
9090 self.home.select_first_entry();
9091 if event.code == KeyCode::Backspace {
9092 return None;
9093 }
9094 }
9095 }
9096
9097 match event.code {
9101 KeyCode::Esc => return self.home_escape(),
9102 KeyCode::Enter => return self.home_open_selected(),
9103 KeyCode::Down if ctrl => self.home.jump_section(1),
9105 KeyCode::Up if ctrl => self.home.jump_section(-1),
9106 KeyCode::Up => self.home.move_selection(-1),
9107 KeyCode::Down => self.home.move_selection(1),
9108 KeyCode::Char('n') if ctrl => self.home.move_selection(1),
9109 KeyCode::Char('p') if ctrl => self.home.move_selection(-1),
9110 KeyCode::Tab => {
9115 self.home.sort = self.home.sort.next();
9116 self.home.select_first_entry();
9117 }
9118 KeyCode::Left => self.home_collapse(true),
9119 KeyCode::Right => match self.selected_directory_to_enter() {
9120 Some(directory) => {
9124 if let Some(format) = self
9128 .home
9129 .selected_entry()
9130 .and_then(|entry| entry.kind.lake_name())
9131 {
9132 self.home.lake_here = Some((directory.clone(), format));
9133 }
9134 self.home_browse_into(directory);
9135 }
9136 None => self.home_collapse(false),
9137 },
9138 KeyCode::PageUp => {
9140 let page = self.home.view_height.max(1) as isize;
9141 self.home.page_selection(-page);
9142 }
9143 KeyCode::PageDown => {
9144 let page = self.home.view_height.max(1) as isize;
9145 self.home.page_selection(page);
9146 }
9147 KeyCode::Home => self.home.page_selection(isize::MIN),
9148 KeyCode::End => self.home.page_selection(isize::MAX),
9149 KeyCode::Char('u') if ctrl => {
9150 self.home.filter.clear();
9151 self.home.sync_search_section();
9152 self.home.select_first_entry();
9153 #[cfg(feature = "cloud")]
9154 self.narrow_cloud_listing();
9155 }
9156 KeyCode::Char('r') if ctrl => self.home_reload(),
9157 KeyCode::Char('d') if ctrl => self.home_toggle_catalog(),
9159 KeyCode::Char('e') if ctrl => self.home_open_documentation(),
9160 KeyCode::Char('x') if ctrl => {
9162 let local = |path: &Path| {
9163 matches!(source::input_source(path), source::InputSource::Local(_))
9164 };
9165 match self.home.selected_entry() {
9166 Some(entry)
9167 if !self.home.selection_is_the_door()
9168 && entry.table.is_none()
9169 && matches!(
9170 entry.kind,
9171 discover::EntryKind::File
9172 | discover::EntryKind::Other
9173 | discover::EntryKind::Unknown
9174 )
9175 && local(&entry.path) =>
9176 {
9177 self.open_hex(entry.path, crate::hex_view::Origin::Home, false, None);
9178 }
9179 _ => self.flash_note("Ctrl+X shows a local file's bytes".to_string()),
9180 }
9181 }
9182 KeyCode::Char('a') if ctrl => {
9183 let on = self.home.selected_key();
9184 self.home.hide_unreadable = !self.home.hide_unreadable;
9185 let tables = self
9187 .home
9188 .sections
9189 .iter()
9190 .any(|section| section.rows.iter().any(|row| row.table.is_some()));
9191 self.flash_note(
9192 match (self.home.hide_unreadable, tables) {
9193 (true, true) => "Hiding internal tables",
9194 (false, true) => "Showing internal tables",
9195 (true, false) => "Hiding files with no reader",
9196 (false, false) => "Showing files with no reader",
9197 }
9198 .to_string(),
9199 );
9200 self.home.reselect(on);
9202 }
9203 KeyCode::Backspace => {
9204 if self.home.filter.is_empty() {
9205 self.home_ascend();
9206 } else {
9207 self.home.filter.pop();
9208 self.home.sync_search_section();
9209 self.home.select_first_entry();
9210 #[cfg(feature = "cloud")]
9211 self.narrow_cloud_listing();
9212 }
9213 }
9214 KeyCode::Delete if event.modifiers.contains(KeyModifiers::SHIFT) => {
9220 let count = self.cache.load_recents().len();
9221 if count == 0 {
9222 self.home.status = Some("Nothing to forget".into());
9223 } else {
9224 self.pending_clear_recents = true;
9225 self.confirmation_modal
9226 .show(format!("Forget all {count} recently opened datasets?"));
9227 }
9228 }
9229 KeyCode::Delete => self.home_forget_selected(),
9230 KeyCode::Char('~') if self.home.filter.is_empty() => {
9231 self.home.path_input_active = true;
9232 self.home.status = None;
9233 self.home.path_listing = None;
9234 self.home.path_pick = None;
9235 self.list_the_typed_directory();
9236 self.home.pick_first_path();
9237 }
9238 KeyCode::Char('?') if self.home.filter.is_empty() && !ctrl => {
9242 self.open_help_overlay();
9243 }
9244 KeyCode::Char(' ') if self.home.filter.is_empty() && !ctrl => {
9248 if self.home.selection_is_header() {
9249 self.home_toggle_fold();
9250 }
9251 }
9252 KeyCode::Char(c) if !ctrl => {
9253 self.home.filter.push(c);
9254 self.spawn_home_search();
9258 self.home.sync_search_section();
9259 self.home.select_first_entry();
9260 #[cfg(feature = "cloud")]
9261 self.narrow_cloud_listing();
9262 }
9263 _ => {}
9264 }
9265 None
9266 }
9267
9268 fn color(&self, name: &str) -> Color {
9270 self.theme.get(name)
9271 }
9272
9273 fn export_format_for(path: &Path, format: Option<FileFormat>) -> Option<ExportFormat> {
9280 format
9281 .or_else(|| FileFormat::from_path(path))
9282 .or_else(|| {
9283 CompressionFormat::from_extension(path)
9284 .and(path.file_stem())
9285 .and_then(|stem| FileFormat::from_path(Path::new(stem)))
9286 })
9287 .and_then(crate::readers::export_default)
9288 }
9289
9290 fn with_delimited_spec(
9294 file: &Path,
9295 mut options: OpenOptions,
9296 formats: &crate::formats::Registry,
9297 ) -> Result<OpenOptions> {
9298 if options.delimited.is_some() {
9299 return Ok(options);
9300 }
9301 let asked = crate::formats::Asked {
9302 spec_file: options.spec_file.clone(),
9303 spec: options.spec_fetched.clone(),
9304 spec_name: options.spec_name.clone(),
9305 compression: options.compression,
9306 ..Default::default()
9307 };
9308 let crate::formats::Route::Delimited(choice) =
9309 crate::formats::route(file, &asked, formats).map_err(|e| color_eyre::eyre::eyre!(e))?
9310 else {
9311 return Ok(options);
9312 };
9313 let Some(delimited) = choice.spec.delimited.clone() else {
9314 return Ok(options);
9315 };
9316 delimited.apply(&mut options);
9317 let chosen =
9318 crate::delimited_spec::DelimitedRead::chosen(choice.spec, choice.by, choice.also);
9319 let read = crate::delimited_spec::read_facts(&chosen, &[file.to_path_buf()], &options)?;
9320 options.delimited = Some(Arc::new(read));
9321 Ok(options)
9322 }
9323
9324 fn decompressed_delimited_state(
9331 path: &Path,
9332 options: &OpenOptions,
9333 writer: &crate::unfinished::Writer,
9334 ) -> Result<DataTableState> {
9335 let separator = options
9336 .format
9337 .and_then(FileFormat::separator)
9338 .unwrap_or(b',');
9339 DataTableState::from_delimited_for_open(path, separator, options, writer)
9340 }
9341
9342 #[cfg(feature = "cloud")]
9344 fn build_s3_cloud_options(settings: &crate::cloud_sources::S3Settings) -> CloudOptions {
9345 let settings = settings.clone();
9346 let virtual_hosted = (settings.endpoint.is_some() || settings.virtual_hosted.is_some())
9347 .then(|| settings.virtual_hosted_style().to_string());
9348 let configs: Vec<(AmazonS3ConfigKey, String)> = [
9349 (AmazonS3ConfigKey::Endpoint, settings.endpoint),
9350 (AmazonS3ConfigKey::AccessKeyId, settings.access_key_id),
9351 (
9352 AmazonS3ConfigKey::SecretAccessKey,
9353 settings.secret_access_key,
9354 ),
9355 (AmazonS3ConfigKey::Token, settings.session_token),
9356 (AmazonS3ConfigKey::Region, settings.region),
9357 (AmazonS3ConfigKey::VirtualHostedStyleRequest, virtual_hosted),
9358 (
9359 AmazonS3ConfigKey::SkipSignature,
9360 settings.skip_signature.then(|| "true".to_string()),
9361 ),
9362 ]
9363 .into_iter()
9364 .filter_map(|(key, value)| value.map(|v| (key, v)))
9365 .chain([(
9366 AmazonS3ConfigKey::Client(crate::user_agent::CLIENT_KEY),
9367 crate::user_agent::get(),
9368 )])
9369 .collect();
9370 CloudOptions::default().with_aws(configs)
9371 }
9372
9373 #[cfg(feature = "cloud")]
9376 fn cloud_bucket_and_key(url: &str) -> Result<(String, String)> {
9377 if let Some((_, container, key)) = source::azure_parts(url) {
9378 return Ok((container, key.trim_matches('/').to_string()));
9379 }
9380 crate::cloud_browse::split_bucket_url(url)
9381 .map(|(_, bucket, key)| (bucket, key))
9382 .ok_or_else(|| {
9383 color_eyre::eyre::eyre!("URL must be s3://bucket/key or gs://bucket/key")
9384 })
9385 }
9386
9387 #[cfg(feature = "cloud")]
9392 fn polars_object_store(
9393 url: &str,
9394 options: &CloudOptions,
9395 runtime: &tokio::runtime::Handle,
9396 ) -> Result<Arc<dyn object_store::ObjectStore>> {
9397 let url = url.to_string();
9398 let options = options.clone();
9399 wait_on_runtime(runtime, async move {
9400 let (_, store) = polars::io::cloud::build_object_store(
9401 PlRefPath::new(url.as_str()),
9402 Some(&options),
9403 false,
9404 )
9405 .await?;
9406 polars::prelude::PolarsResult::Ok(store.to_dyn_object_store().await.into_owned())
9407 })
9408 .ok_or_else(|| color_eyre::eyre::eyre!("cancelled"))?
9409 .map_err(|e| color_eyre::eyre::eyre!("Object store config failed: {}", e))
9410 }
9411
9412 fn format_bytes(n: u64) -> String {
9414 const KB: u64 = 1024;
9415 const MB: u64 = KB * 1024;
9416 const GB: u64 = MB * 1024;
9417 const TB: u64 = GB * 1024;
9418 if n >= TB {
9419 format!("{:.2} TB", n as f64 / TB as f64)
9420 } else if n >= GB {
9421 format!("{:.2} GB", n as f64 / GB as f64)
9422 } else if n >= MB {
9423 format!("{:.2} MB", n as f64 / MB as f64)
9424 } else if n >= KB {
9425 format!("{:.2} KB", n as f64 / KB as f64)
9426 } else {
9427 format!("{} bytes", n)
9428 }
9429 }
9430
9431 #[cfg(feature = "http")]
9444 fn http_agent(total: std::time::Duration) -> ureq::Agent {
9445 crate::user_agent::ureq_config()
9446 .timeout_global(Some(total))
9447 .build()
9448 .into()
9449 }
9450
9451 #[cfg(feature = "http")]
9455 fn fetch_remote_size_http(
9456 url: &str,
9457 ) -> std::result::Result<Option<u64>, crate::error_display::HttpGone> {
9458 let agent = Self::http_agent(std::time::Duration::from_secs(15));
9459 match agent.head(url).header("Accept-Encoding", "identity").call() {
9463 Ok(r) => Ok(r
9464 .headers()
9465 .get("Content-Length")
9466 .and_then(|v| v.to_str().ok())
9467 .and_then(|s| s.parse::<u64>().ok())),
9468 Err(e) => crate::error_display::http_gone(url, &e).map_or(Ok(None), Err),
9469 }
9470 }
9471
9472 #[cfg(feature = "cloud")]
9474 fn fetch_remote_size_cloud(
9475 url: &str,
9476 cloud: &crate::config::CloudConfig,
9477 runtime: &tokio::runtime::Handle,
9478 ) -> Result<Option<u64>> {
9479 use object_store::ObjectStoreExt;
9480
9481 let (_bucket, key) = Self::cloud_bucket_and_key(url)?;
9482 if key.is_empty() {
9483 return Ok(None);
9484 }
9485 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
9486 let path = crate::cloud_browse::object_path(&key);
9487 let head = wait_on_runtime(runtime, async move { store.head(&path).await });
9488 Ok(head.and_then(|r| r.ok()).map(|meta| meta.size))
9489 }
9490
9491 #[cfg(feature = "http")]
9497 fn download_http_to_temp(
9498 url: &str,
9499 temp_dir: Option<&Path>,
9500 extension: Option<&str>,
9501 limit: Option<u64>,
9502 writer: &crate::unfinished::Writer,
9503 ) -> Result<crate::download::TempDownload> {
9504 use crate::download::StreamError;
9505
9506 let url = url.to_string();
9507 let open = move || {
9508 let agent = Self::http_agent(std::time::Duration::from_secs(300));
9509 let response = agent
9511 .get(&url)
9512 .call()
9513 .map_err(|e| crate::error_display::http_message(&url, &e))?;
9514 Ok((response.into_body().into_reader(), None))
9517 };
9518 crate::download::read_to_temp(temp_dir, extension, open, writer, limit).map_err(|error| {
9519 match error {
9520 StreamError::Open(message) => color_eyre::eyre::eyre!(message),
9521 StreamError::Read(e) => {
9522 color_eyre::eyre::eyre!("Download failed partway. Check your connection: {e}")
9523 }
9524 StreamError::Short { expected, got } => color_eyre::eyre::eyre!(
9525 "Download failed partway: it ended after {got} of {expected} bytes."
9526 ),
9527 StreamError::Write(report) => report,
9528 StreamError::Cut => color_eyre::eyre::eyre!("Download was cancelled."),
9529 }
9530 })
9531 }
9532
9533 #[cfg(feature = "cloud")]
9538 fn download_cloud_to_temp(
9539 url: &str,
9540 cloud: &crate::config::CloudConfig,
9541 options: &OpenOptions,
9542 runtime: &tokio::runtime::Handle,
9543 writer: &crate::unfinished::Writer,
9544 ) -> Result<crate::download::TempDownload> {
9545 use crate::download::StreamError;
9546 use object_store::ObjectStoreExt;
9547
9548 let (label, example) = match source::input_source(Path::new(url)) {
9549 source::InputSource::Gcs(_) => ("GCS", "gs://bucket/path/file.csv"),
9550 source::InputSource::Azure(_) => (
9551 "Azure",
9552 "abfss://container@account.dfs.core.windows.net/path/file.csv",
9553 ),
9554 _ => ("S3", "s3://bucket/path/file.csv"),
9555 };
9556 let ext = source::download_suffix(url);
9557 let (_bucket, key) = Self::cloud_bucket_and_key(url)?;
9558 if key.is_empty() {
9559 return Err(crate::error_display::FileError::new(
9560 Path::new(url),
9561 format!("a {label} URL names an object here, such as {example}"),
9562 )
9563 .into());
9564 }
9565 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
9566
9567 let path = crate::cloud_browse::object_path(&key);
9568 let open = async move {
9569 let got = store
9570 .get(&path)
9571 .await
9572 .map_err(|e| crate::error_display::store_message(&e))?;
9573 let len = got.range.end - got.range.start;
9574 Ok((got.into_stream(), Some(len)))
9575 };
9576 let failed = |what: String| -> color_eyre::Report {
9577 crate::error_display::FileError::new(Path::new(url), what).into()
9578 };
9579 crate::download::stream_to_temp(
9580 runtime,
9581 options.temp_dir.as_deref(),
9582 ext.as_deref(),
9583 open,
9584 writer,
9585 )
9586 .map_err(|error| match error {
9587 StreamError::Open(e) => failed(e),
9588 StreamError::Read(e) => failed(format!("the download stopped: {e}")),
9589 StreamError::Short { expected, got } => {
9590 failed(format!("it ended after {got} of {expected} bytes"))
9591 }
9592 StreamError::Write(report) => report,
9593 StreamError::Cut => failed("the download was cancelled".to_string()),
9594 })
9595 }
9596
9597 pub(crate) fn scan_for_open(
9608 cloud: &crate::config::CloudConfig,
9609 formats: &crate::formats::Registry,
9610 paths: &[PathBuf],
9611 options: OpenOptions,
9612 path: Option<PathBuf>,
9613 ) -> std::result::Result<loading::LoadAnswer, String> {
9614 use loading::LoadAnswer;
9615 let bytes_of = |files: &[PathBuf]| -> u64 {
9616 files
9617 .iter()
9618 .filter_map(|f| std::fs::metadata(f).ok())
9619 .map(|m| m.len())
9620 .sum()
9621 };
9622 let mut report = ReadReport {
9630 left_out: options.left_out.clone(),
9631 files_disagree: options.files_disagree,
9632 format: None,
9633 format_read: None,
9634 read_python: Vec::new(),
9635 sqlite: None,
9636 opened: None,
9637 splits: options.splits.clone(),
9638 delimited: None,
9639 table: None,
9640 guessed: false,
9641 read_notes: Vec::new(),
9642 typing: Default::default(),
9643 };
9644 let options = OpenOptions {
9647 ignore_errors: options.ignore_errors || options.follow,
9648 ..options
9649 };
9650 let named = |e: color_eyre::Report| {
9651 crate::error_display::user_message_from_report(&e, path.as_deref())
9652 };
9653 let followed_stream = options.follow
9656 && crate::follow::followed_stream(
9657 &paths[0],
9658 Some(crate::follow::format_of(&paths[0], options.format)),
9659 &options,
9660 );
9661 let scan = if followed_stream {
9662 crate::follow::stream::scan(&paths[0])
9663 .map(Scan::from)
9664 .map_err(|e| color_eyre::eyre::eyre!(e))
9665 } else {
9666 Self::build_lazyframe_from_paths_with(cloud, paths, &options, &mut report, formats)
9667 }
9668 .map_err(named)?;
9670 let format = scan.format(report.format.or(options.format));
9671 let recording = options
9674 .spool
9675 .as_ref()
9676 .is_some_and(|handle| handle.spool().tee().is_some());
9677 let (scan, tail) = match scan {
9678 Scan::Frame(lf) if options.follow => {
9679 let format = crate::follow::format_of(&paths[0], format);
9680 let refused = (!followed_stream)
9681 .then(|| crate::follow::refusal(Some(format), &options))
9682 .flatten();
9683 match refused {
9684 Some(_) if recording => (Scan::Frame(lf), None),
9685 Some(refusal) => return Err(refusal),
9686 None => {
9687 let (lf, tail) =
9688 crate::follow::bound_to_complete(*lf, &paths[0], format, &options)
9689 .map_err(named)?;
9690 (Scan::Frame(Box::new(lf)), Some(Arc::new(tail)))
9691 }
9692 }
9693 }
9694 _ if options.follow && !recording => {
9695 return Err(crate::follow::refusal(format, &options)
9696 .unwrap_or_else(|| "This file cannot be followed as it grows.".to_string()));
9697 }
9698 scan => (scan, None),
9699 };
9700 let read_mode = scan.read_mode(format, report.format_read.is_some(), &options);
9701 let mut options = OpenOptions {
9702 left_out: report.left_out,
9703 files_disagree: report.files_disagree,
9704 format,
9705 format_read: report.format_read,
9706 sqlite: report.sqlite,
9707 opened: report.opened,
9708 splits: report.splits,
9709 read_python: report.read_python,
9710 read_mode,
9711 tail,
9712 table: report.table.or_else(|| options.table.clone()),
9713 format_guessed: options.format_guessed || report.guessed,
9714 read_notes: report.read_notes,
9715 typing: report.typing,
9716 ..options
9717 };
9718 if let Some(read) = report.delimited {
9721 read.delimited().apply(&mut options);
9722 options.delimited = Some(read);
9723 }
9724 Ok(match scan {
9725 Scan::Frame(lf) => LoadAnswer::Scanned { lf, path, options },
9726 Scan::Decompress { file, .. } => LoadAnswer::Compressed {
9727 file,
9728 path,
9729 options,
9730 },
9731 Scan::Streams(files) => LoadAnswer::Convert {
9732 what: loading::Conversion::Streams,
9733 bytes: bytes_of(&files),
9734 files,
9735 path,
9736 options,
9737 },
9738 Scan::DecompressSpec { file, choice } => LoadAnswer::CompressedRecords {
9739 file,
9740 path,
9741 choice,
9742 options,
9743 },
9744 Scan::ReadInto { files, format } => LoadAnswer::Convert {
9745 what: loading::Conversion::Text(format),
9746 bytes: bytes_of(&files),
9747 files,
9748 path,
9749 options,
9750 },
9751 Scan::Tables { file, tables, .. } => LoadAnswer::Tables { file, tables, path },
9752 Scan::Unpack {
9753 file,
9754 member,
9755 format,
9756 } => LoadAnswer::Convert {
9757 what: loading::Conversion::Text(format),
9758 bytes: bytes_of(std::slice::from_ref(&file)),
9759 files: vec![file],
9760 path,
9761 options: OpenOptions {
9762 table: Some(member),
9763 ..options
9764 },
9765 },
9766 Scan::Hex { file, asked } => LoadAnswer::Hex {
9767 file,
9768 asked,
9769 record_size: options.record_size,
9770 },
9771 })
9772 }
9773
9774 pub(crate) fn read_schema_for_open(
9778 lf: LazyFrame,
9779 path: Option<PathBuf>,
9780 options: OpenOptions,
9781 cloud: &crate::config::CloudConfig,
9782 runtime: &tokio::runtime::Handle,
9783 report: &crate::measurements::OpenReport,
9784 made: loading::Made,
9785 ) -> std::result::Result<loading::LoadAnswer, String> {
9786 use loading::LoadAnswer;
9787 let (state, facts, debug_label) =
9788 Self::build_schema_state(lf, path.as_deref(), &options, cloud, runtime, report)
9789 .map_err(|e| crate::error_display::user_message_from_report(&e, path.as_deref()))?;
9790 let loading::Made {
9792 download,
9793 converted,
9794 notes,
9795 other_tables,
9796 detail,
9797 } = made;
9798 let mut open_notes = facts.open_notes;
9799 open_notes.extend(notes);
9800 let mut other_tables_found = facts.other_tables;
9801 other_tables_found.extend(other_tables);
9802 let state = state.with_open(OpenFacts {
9803 fetched: Self::fetched(download.as_ref(), path.as_deref()),
9804 download,
9805 converted,
9806 other_tables: other_tables_found,
9807 open_notes,
9808 detail: detail.or(facts.detail),
9809 ..facts
9810 });
9811 Ok(LoadAnswer::SchemaRead {
9812 state: Box::new(state),
9813 path,
9814 options,
9815 debug_label: Some(debug_label),
9816 })
9817 }
9818
9819 fn spawn_load_phase(&mut self, load: loading::LoadId, step: loading::Step) {
9820 use loading::{LoadAnswer, Step};
9821 let job = Job::Load(load);
9822 match step {
9823 #[cfg(any(feature = "http", feature = "cloud"))]
9824 Step::ReadHeaders {
9825 url,
9826 format,
9827 options,
9828 writer,
9829 } => {
9830 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
9831 self.spawn_job(job, Some("Reading headers..."), move |_| {
9832 let read = crate::remote_model::read(&url, format, &cloud, &runtime, &|| {
9833 writer.stopped()
9834 });
9835 let crate::remote_model::Read { lf, summary, notes } = match read {
9836 Ok(read) => read,
9837 Err(crate::model_files::RangeError::NoRanges) => {
9838 return Ok(Answer::Load(Box::new(LoadAnswer::NoRanges { options })));
9839 }
9840 Err(crate::model_files::RangeError::Failed(message)) => {
9842 return Err(crate::logging::redact(&message, &[]));
9843 }
9844 };
9845 let opened = Arc::new(crate::model_files::opened(&summary));
9846 let options = OpenOptions {
9847 format: Some(format),
9848 opened: Some(opened.clone()),
9849 ..options
9850 };
9851 let state = Self::schema_state_from_full_scan(
9853 lf,
9854 None,
9855 &OpenOptions {
9856 hive: false,
9857 ..options.clone()
9858 },
9859 )
9860 .map_err(|e| crate::error_display::user_message_from_report(&e, Some(&url)))?
9861 .with_open(OpenFacts {
9862 detail: opened.detail.clone(),
9863 open_notes: notes,
9864 read_as: Some(format),
9865 ..Default::default()
9866 });
9867 Ok(Answer::Load(Box::new(LoadAnswer::SchemaRead {
9868 state: Box::new(state),
9869 path: Some(url),
9870 options,
9871 debug_label: Some("model headers (ranged)".to_string()),
9872 })))
9873 });
9874 }
9875 #[cfg(any(feature = "http", feature = "cloud"))]
9876 Step::Probe(pending) => {
9877 #[cfg(feature = "cloud")]
9878 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
9879 self.spawn_job(job, Some("Checking size..."), move |_| {
9880 #[cfg(feature = "cloud")]
9883 if let loading::PendingDownload::Arrow { url, .. } = &pending {
9884 let (_, _, options) = pending.parts();
9885 let (objects, options) = crate::cloud_arrow::list(
9886 url, options, &cloud, &runtime,
9887 )
9888 .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
9889 let size = crate::cloud_arrow::stream_bytes(&objects);
9890 return Ok(Answer::Load(Box::new(LoadAnswer::Sized(
9891 loading::PendingDownload::Arrow {
9892 url: url.clone(),
9893 objects,
9894 size: Some(size),
9895 options,
9896 },
9897 ))));
9898 }
9899 let size = match &pending {
9900 #[cfg(feature = "http")]
9901 loading::PendingDownload::Http { url, .. } => {
9902 Self::fetch_remote_size_http(url).map_err(|gone| gone.message)?
9906 }
9907 #[cfg(feature = "cloud")]
9908 loading::PendingDownload::S3 { url, .. }
9909 | loading::PendingDownload::Gcs { url, .. }
9910 | loading::PendingDownload::Azure { url, .. } => {
9911 Self::fetch_remote_size_cloud(url, &cloud, &runtime).unwrap_or(None)
9912 }
9913 #[cfg(feature = "cloud")]
9914 loading::PendingDownload::Arrow { size, .. } => *size,
9915 };
9916 Ok(Answer::Load(Box::new(LoadAnswer::Sized(
9917 pending.with_size(size),
9918 ))))
9919 });
9920 }
9921 #[cfg(any(feature = "http", feature = "cloud"))]
9922 Step::Download { pending, writer } => {
9923 #[cfg(feature = "cloud")]
9928 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
9929 let status = match &pending {
9930 #[cfg(feature = "http")]
9931 loading::PendingDownload::Http { .. } => "Downloading...",
9932 #[cfg(feature = "cloud")]
9933 loading::PendingDownload::S3 { .. } => "Downloading from S3...",
9934 #[cfg(feature = "cloud")]
9935 loading::PendingDownload::Gcs { .. } => "Downloading from GCS...",
9936 #[cfg(feature = "cloud")]
9937 loading::PendingDownload::Azure { .. } => "Downloading from Azure...",
9938 #[cfg(feature = "cloud")]
9939 loading::PendingDownload::Arrow { url, .. } => {
9940 match source::input_source(Path::new(url)) {
9941 source::InputSource::Gcs(_) => "Downloading from GCS...",
9942 source::InputSource::Azure(_) => "Downloading from Azure...",
9943 _ => "Downloading from S3...",
9944 }
9945 }
9946 };
9947 let sized = pending
9950 .parts()
9951 .1
9952 .filter(|_| status == "Downloading...")
9953 .map(|size| format!("Downloading {}...", discover::format_size(size)));
9954 let status = sized.as_deref().unwrap_or(status);
9955 self.spawn_job(job, Some(status), move |_| {
9956 let (url, _, options) = pending.parts();
9957 let fetched = match &pending {
9958 #[cfg(feature = "http")]
9959 loading::PendingDownload::Http { .. } => {
9960 let ext = source::download_suffix(url);
9961 let limit = options
9965 .download_unasked
9966 .filter(|_| pending.parts().1.is_none())
9967 .map(|unasked| unasked.limit);
9968 Self::download_http_to_temp(
9969 url,
9970 options.temp_dir.as_deref(),
9971 ext.as_deref(),
9972 limit,
9973 &writer,
9974 )
9975 .map(|file| (file, options.clone()))
9976 }
9977 #[cfg(feature = "cloud")]
9978 loading::PendingDownload::S3 { .. }
9979 | loading::PendingDownload::Gcs { .. }
9980 | loading::PendingDownload::Azure { .. } => {
9981 Self::download_cloud_to_temp(url, &cloud, options, &runtime, &writer)
9982 .map(|file| (file, options.clone()))
9983 }
9984 #[cfg(feature = "cloud")]
9986 loading::PendingDownload::Arrow { objects, .. } => {
9987 crate::cloud_arrow::download(
9988 objects, options, &cloud, &runtime, &writer,
9989 )
9990 .map(|(file, parts)| {
9991 let options = OpenOptions {
9992 format: Some(FileFormat::Arrow),
9993 hive: false,
9994 arrow_parts: Some(Arc::new(parts)),
9995 ..options.clone()
9996 };
9997 (file, options)
9998 })
9999 }
10000 };
10001 let (download, options) = match fetched {
10002 Err(e) if e.downcast_ref::<crate::download::PastLimit>().is_some() => {
10003 return Ok(Answer::Load(Box::new(LoadAnswer::PastLimit(pending))));
10004 }
10005 fetched => fetched.map_err(|e| {
10006 crate::error_display::user_message_from_report(&e, None)
10007 })?,
10008 };
10009 Ok(Answer::Load(Box::new(LoadAnswer::Downloaded {
10010 download,
10011 options,
10012 })))
10013 });
10014 }
10015 Step::Spool {
10016 options,
10017 writer,
10018 read,
10019 } => {
10020 let piped = self.stdin_reader.take();
10023 let stdout = self.stdout_pass.take();
10024 self.spawn_job(job, Some("Reading stdin..."), move |_| {
10025 let open = move || -> crate::download::Opened<Box<dyn std::io::Read + Send>> {
10026 Ok((piped.unwrap_or_else(|| Box::new(std::io::stdin())), None))
10027 };
10028 let (download, options) = if options.follow
10032 || options.tee.is_some()
10033 || crate::stdin::may_read_as_it_arrives(&options)
10034 {
10035 match crate::follow::spool(open, options, &writer, &read, stdout)? {
10036 (crate::follow::Spooled::Temp(download), options) => {
10037 (download, options)
10038 }
10039 (crate::follow::Spooled::Kept(file), options) => {
10040 return Ok(Answer::Load(Box::new(LoadAnswer::Recorded {
10041 file,
10042 options,
10043 })));
10044 }
10045 }
10046 } else {
10047 crate::stdin::spool(open, options, &writer, &read)?
10048 };
10049 Ok(Answer::Load(Box::new(LoadAnswer::Spooled {
10050 download,
10051 options,
10052 })))
10053 });
10054 }
10055 Step::FetchSpec {
10056 url,
10057 options,
10058 writer,
10059 } => {
10060 #[cfg(any(feature = "http", feature = "cloud"))]
10061 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
10062 self.spawn_job(job, Some("Reading spec..."), move |_| {
10063 #[cfg(any(feature = "http", feature = "cloud"))]
10064 let fetched = crate::remote_model::fetch_small(
10065 &url,
10066 crate::formats::MAX_SPEC_BYTES,
10067 &cloud,
10068 &runtime,
10069 &|| writer.stopped(),
10070 );
10071 #[cfg(not(any(feature = "http", feature = "cloud")))]
10072 let fetched: std::result::Result<Option<Vec<u8>>, String> = {
10073 let _ = &writer;
10074 Err(crate::error_display::file_message(
10075 &url,
10076 "this build reads no URLs",
10077 ))
10078 };
10079 let bytes = fetched
10081 .map_err(|message| crate::logging::redact(&message, &[]))?
10082 .ok_or_else(|| {
10083 crate::logging::redact(
10084 &crate::error_display::file_message(
10085 &url,
10086 &format!(
10087 "a format spec is at most {}",
10088 crate::formats::MAX_SPEC_SAID
10089 ),
10090 ),
10091 &[],
10092 )
10093 })?;
10094 let spec = crate::formats::Spec::from_bytes(&bytes, &url)
10095 .map_err(|e| crate::logging::redact(&e.to_string(), &[]))?;
10096 Ok(Answer::Load(Box::new(LoadAnswer::SpecFetched {
10097 spec: Arc::new(spec),
10098 options,
10099 })))
10100 });
10101 }
10102 Step::DecompressRecords {
10103 file,
10104 path,
10105 choice,
10106 options,
10107 writer,
10108 } => {
10109 self.spawn_job(job, Some("Decompressing..."), move |_| {
10110 let failed = |e: color_eyre::Report| {
10111 crate::error_display::user_message_from_report(&e, Some(path.as_path()))
10112 };
10113 let compression = options
10114 .compression
10115 .or_else(|| CompressionFormat::from_extension(&file))
10116 .ok_or_else(|| format!("{} is not compressed", path.display()))?;
10117 let temp_dir = options.temp_dir.clone().unwrap_or_else(std::env::temp_dir);
10118 let copy =
10119 DataTableState::decompress_to_copy(&file, compression, &temp_dir, &writer)
10120 .map_err(failed)?;
10121 Ok(Answer::Load(Box::new(LoadAnswer::DecompressedRecords {
10122 copy,
10123 path,
10124 choice,
10125 options,
10126 })))
10127 });
10128 }
10129 Step::ReadRecords {
10130 copy,
10131 path,
10132 choice,
10133 options,
10134 } => {
10135 self.spawn_job(job, Some("Reading records..."), move |_| {
10136 let named = path
10137 .file_name()
10138 .map(|n| n.to_string_lossy().into_owned())
10139 .unwrap_or_default();
10140 let read = crate::formats::read(©, &named, choice)?;
10141 let lf = Arc::clone(&read.records).into_lazy().map_err(|e| {
10142 crate::error_display::user_message_from_report(
10143 &color_eyre::eyre::eyre!(e),
10144 Some(path.as_path()),
10145 )
10146 })?;
10147 Ok(Answer::Load(Box::new(LoadAnswer::Scanned {
10148 lf: Box::new(lf),
10149 path: Some(path),
10150 options: OpenOptions {
10151 format_read: Some(Arc::new(read)),
10152 ..options
10153 },
10154 })))
10155 });
10156 }
10157 Step::Decompress {
10158 file,
10159 path,
10160 options,
10161 writer,
10162 download,
10163 } => {
10164 let options = OpenOptions {
10167 format: options.format.or(Some(FileFormat::TEXT)),
10168 ..options
10169 };
10170 let formats = self.formats.clone();
10171 self.spawn_job(job, Some("Decompressing..."), move |_| {
10172 let failed = |e: color_eyre::Report| {
10173 crate::error_display::user_message_from_report(&e, Some(path.as_path()))
10174 };
10175 let options =
10176 Self::with_delimited_spec(&file, options, &formats).map_err(failed)?;
10177 let lines = options.delimited.is_none()
10178 && options.format.is_some_and(FileFormat::is_lines);
10179 let (state, opened) = if lines {
10180 let (state, opened) =
10181 DataTableState::from_lines_decompressed(&file, &options, &writer)
10182 .map_err(failed)?;
10183 (state, Some(opened))
10184 } else {
10185 let state = Self::decompressed_delimited_state(&file, &options, &writer)
10186 .map_err(failed)?;
10187 (state, None)
10188 };
10189 let mut open_notes = options
10190 .delimited
10191 .as_ref()
10192 .map(|read| read.notes())
10193 .unwrap_or_default();
10194 open_notes.extend(opened.iter().flat_map(|o| o.notes.iter().cloned()));
10195 let state = state.with_open(OpenFacts {
10196 fetched: Self::fetched(download.as_ref(), Some(&path)),
10197 download,
10198 open_notes,
10199 records: opened.and_then(|o| o.window),
10200 delimited: options.delimited.clone(),
10201 read_as: options.format,
10202 read_mode: options.format.and_then(|f| {
10204 f.read_mode(crate::Stored::Compressed {
10205 in_memory: options.decompress_in_memory,
10206 })
10207 }),
10208 ..Default::default()
10209 });
10210 Ok(Answer::Load(Box::new(LoadAnswer::SchemaRead {
10211 state: Box::new(state),
10212 path: Some(path),
10213 options,
10214 debug_label: Some("decompressed delimited".to_string()),
10215 })))
10216 });
10217 }
10218 Step::Convert {
10219 what,
10220 files,
10221 path,
10222 options,
10223 writer,
10224 read,
10225 } => {
10226 let formats = self.formats.clone();
10230 self.spawn_job(job, Some(what.status()), move |_| {
10231 let named = |e: color_eyre::Report| {
10232 crate::error_display::user_message_from_report(&e, path.as_deref())
10233 };
10234 let converted = match what {
10235 loading::Conversion::Streams => {
10236 let converted = crate::ipc_stream::convert(
10237 &files,
10238 options.temp_dir.as_deref(),
10239 &writer,
10240 &read,
10241 )
10242 .map_err(named)?;
10243 loading::Converted::Streams {
10244 file: converted.file,
10245 parts: converted.parts,
10246 }
10247 }
10248 loading::Conversion::Text(format) => {
10249 let display = path.clone().unwrap_or_else(|| files[0].clone());
10250 let (converted, detail) =
10251 crate::readers::convert(&crate::readers::ConvertIn {
10252 files: &files,
10253 display: &display,
10254 format,
10255 options: &options,
10256 formats: &formats,
10257 writer: &writer,
10258 read: &read,
10259 })
10260 .map_err(named)?;
10261 loading::Converted::Frame {
10262 files: converted.files,
10263 lf: Box::new(converted.lf),
10264 notes: converted.notes,
10265 other_tables: converted.other_tables,
10266 detail,
10267 }
10268 }
10269 };
10270 Ok(Answer::Load(Box::new(LoadAnswer::Converted {
10271 converted,
10272 path,
10273 options,
10274 })))
10275 });
10276 }
10277 Step::Scan {
10278 paths,
10279 options,
10280 display,
10281 status,
10282 } => {
10283 let cloud = self.app_config.cloud.clone();
10284 let formats = self.formats.clone();
10285 let path = display.or_else(|| paths.first().cloned());
10288 self.reads.scans += 1;
10289 self.spawn_job(job, Some(status), move |_| {
10290 Self::scan_for_open(&cloud, &formats, &paths, options, path)
10291 .map(|answer| Answer::Load(Box::new(answer)))
10292 });
10293 }
10294 Step::ReadSchema {
10295 lf,
10296 path,
10297 options,
10298 progress,
10299 made,
10300 } => {
10301 self.debug.schema_load = None;
10302 let cloud = self.app_config.cloud.clone();
10303 let runtime = self.runtime.clone();
10304 let report = crate::measurements::OpenReport {
10305 progress,
10306 meter: Arc::new(crate::measurements::Meter::default()),
10307 remembered: Some(self.cache.clone()),
10308 writes: self.cache_writes.clone(),
10309 };
10310 self.spawn_job(job, Some("Reading schema..."), move |_| {
10311 Self::read_schema_for_open(*lf, path, options, &cloud, &runtime, &report, made)
10312 .map(|answer| Answer::Load(Box::new(answer)))
10313 });
10314 }
10315 Step::Nothing
10316 | Step::Crash(_)
10317 | Step::Install(_)
10318 | Step::Failed(_)
10319 | Step::Tables(_)
10320 | Step::Hex(_) => {
10321 unreachable!("not a phase with a worker")
10322 }
10323 #[cfg(any(feature = "http", feature = "cloud"))]
10324 Step::Ask(_) => unreachable!("not a phase with a worker"),
10325 Step::AskRead(_) => unreachable!("not a phase with a worker"),
10326 }
10327 }
10328
10329 fn in_memory_confirmation_message(read: &loading::InMemory) -> String {
10332 let what = match read.files {
10333 1 => format!(
10334 "{}: {} reads",
10335 read.name
10336 .file_name()
10337 .map(|n| n.to_string_lossy().into_owned())
10338 .unwrap_or_else(|| read.name.display().to_string()),
10339 read.format.title()
10340 ),
10341 n => format!("{n} {} files read", read.format.title()),
10342 };
10343 format!(
10344 "{what} {} into memory before the table appears.\n\nRead it?",
10345 Self::format_bytes(read.bytes)
10346 )
10347 }
10348
10349 #[cfg(any(feature = "http", feature = "cloud"))]
10351 fn download_confirmation_message(
10352 pending: &loading::PendingDownload,
10353 note: Option<&str>,
10354 ) -> String {
10355 let (url, size, options) = pending.parts();
10356 let size_str = size
10357 .map(Self::format_bytes)
10358 .unwrap_or_else(|| "unknown".to_string());
10359 let dest_dir = options
10360 .temp_dir
10361 .as_deref()
10362 .map(|p| p.display().to_string())
10363 .unwrap_or_else(|| std::env::temp_dir().display().to_string());
10364 let note = note.map(|note| format!("{note}\n\n")).unwrap_or_default();
10365 let files = match pending.arrow_files() {
10367 Some((1, 0)) => "Arrow stream: converted as it downloads\n".to_string(),
10368 Some((streams, 0)) => {
10369 format!("Files: {streams} Arrow streams, converted as they download\n")
10370 }
10371 Some((streams, in_place)) => {
10372 let streams = match streams {
10373 1 => "1 Arrow stream, converted as it downloads".to_string(),
10374 n => format!("{n} Arrow streams, converted as they download"),
10375 };
10376 let in_place = match in_place {
10377 1 => "1 IPC file read in place".to_string(),
10378 n => format!("{n} IPC files read in place"),
10379 };
10380 format!("Files: {streams}; {in_place}\n")
10381 }
10382 None => String::new(),
10383 };
10384 format!(
10385 "{note}URL: {url}\n{files}File size: {size_str}\nDestination: {dest_dir} (temporary file)\n\nContinue with download?"
10386 )
10387 }
10388
10389 fn read_the_selected_note_s_column_as_text(&mut self) {
10396 let Some(state) = self.data_table_state.as_mut() else {
10397 return;
10398 };
10399 let notes = state.notes();
10400 let Some(column) = notes
10401 .get(self.info_modal.notes_selected_index)
10402 .and_then(|note| note.read_as_text.clone())
10403 else {
10404 return;
10405 };
10406 if let Ok(true) = state.deferred(|s| s.read_column_as_text(&column)) {
10408 let notes = state.notes().len();
10413 self.info_modal.notes_selected_index = self
10414 .info_modal
10415 .notes_selected_index
10416 .min(notes.saturating_sub(1));
10417 self.info_modal.notes_scroll_offset = 0;
10418 self.spawn_async_collect(Self::LOADING_BUFFER);
10419 }
10420 }
10421
10422 fn hoist_partition_columns(
10423 lf: LazyFrame,
10424 schema: &Schema,
10425 partition_columns: &[String],
10426 drifts: bool,
10427 ) -> LazyFrame {
10428 hoist_partition_columns(lf, schema, partition_columns, drifts)
10429 }
10430}
10431
10432pub(crate) fn hoist_partition_columns(
10439 lf: LazyFrame,
10440 schema: &Schema,
10441 partition_columns: &[String],
10442 drifts: bool,
10443) -> LazyFrame {
10444 if partition_columns.is_empty() {
10445 return lf;
10446 }
10447 let mut exprs: Vec<_> = partition_columns
10448 .iter()
10449 .map(|s| col(s.as_str()))
10450 .chain(
10451 schema
10452 .iter_names()
10453 .map(|s| s.to_string())
10454 .filter(|c| !partition_columns.contains(c))
10455 .map(|s| col(s.as_str())),
10456 )
10457 .collect();
10458 if drifts {
10459 exprs.push(col(crate::schema_union::DRIFT_COLUMN));
10460 }
10461 lf.select(exprs)
10462}
10463
10464fn next_search_epoch() -> u64 {
10467 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
10468 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
10469}
10470
10471const INDEX_STEP: usize = 16 << 20;
10477
10478type FootersReported = Option<(u64, Option<crate::widgets::datatable::FootersFound>)>;
10479
10480impl App {
10481 fn schema_state_from_local_hive(
10493 path: Option<&Path>,
10494 options: &OpenOptions,
10495 report: &crate::measurements::OpenReport,
10496 ) -> Option<(DataTableState, OpenFacts)> {
10497 if !options.single_spine_schema {
10498 return None;
10499 }
10500 let p = path.filter(|p| p.is_dir() && options.hive)?;
10501 dataset_files::open(Arc::new(dataset_files::LocalFiles::new(p)), options, report)
10502 }
10503
10504 #[cfg(feature = "cloud")]
10507 fn schema_state_from_cloud_hive(
10508 path: Option<&Path>,
10509 options: &OpenOptions,
10510 cloud: &crate::config::CloudConfig,
10511 runtime: &tokio::runtime::Handle,
10512 report: &crate::measurements::OpenReport,
10513 ) -> Option<(DataTableState, OpenFacts)> {
10514 if !options.single_spine_schema || options.format == Some(FileFormat::Arrow) {
10516 return None;
10517 }
10518 let p = path.filter(|p| {
10521 let s = p.as_os_str().to_string_lossy();
10522 home::is_object_store_url(p) && (options.hive || source::is_prefix_or_glob(&s))
10523 })?;
10524
10525 let (full, cloud_opts, store) = Self::cloud_store_for(p, cloud, runtime).ok()?;
10526 let (_bucket, key) = Self::cloud_bucket_and_key(&full).ok()?;
10527 Self::schema_state_from_cloud_hive_with(
10528 full, key, store, cloud_opts, options, runtime, report,
10529 )
10530 }
10531
10532 #[cfg(feature = "cloud")]
10538 fn schema_state_from_cloud_hive_with(
10539 full: String,
10540 key: String,
10541 store: Arc<dyn object_store::ObjectStore>,
10542 cloud_opts: CloudOptions,
10543 options: &OpenOptions,
10544 runtime: &tokio::runtime::Handle,
10545 report: &crate::measurements::OpenReport,
10546 ) -> Option<(DataTableState, OpenFacts)> {
10547 let pattern = full.contains('*').then(|| {
10559 globset::GlobBuilder::new(&key)
10560 .literal_separator(true)
10561 .build()
10562 .map(|g| g.compile_matcher())
10563 });
10564 let pattern = match pattern {
10565 Some(Err(_)) => return None,
10567 Some(Ok(matcher)) => Some(matcher),
10568 None => None,
10569 };
10570 let listed = cloud_hive::prefix_of_glob(&key).to_string();
10571 Self::schema_state_from_cloud_files(
10572 CloudTarget {
10573 full: &full,
10574 key: listed,
10575 pattern: pattern.as_ref(),
10576 },
10577 store,
10578 cloud_opts,
10579 options,
10580 runtime,
10581 report,
10582 )
10583 }
10584
10585 #[cfg(feature = "cloud")]
10592 fn schema_state_from_cloud_files(
10593 target: CloudTarget<'_>,
10594 store: Arc<dyn object_store::ObjectStore>,
10595 cloud_opts: CloudOptions,
10596 options: &OpenOptions,
10597 runtime: &tokio::runtime::Handle,
10598 report: &crate::measurements::OpenReport,
10599 ) -> Option<(DataTableState, OpenFacts)> {
10600 let CloudTarget { full, key, pattern } = target;
10601 dataset_files::open(
10602 Arc::new(dataset_files::StoreFiles::new(
10603 full,
10604 key,
10605 pattern.cloned(),
10606 store,
10607 cloud_opts,
10608 runtime,
10609 )),
10610 options,
10611 report,
10612 )
10613 }
10614
10615 #[cfg(feature = "cloud")]
10622 fn record_cloud_object_facts(
10623 cache: Option<&crate::cache::CacheManager>,
10624 full: &str,
10625 footer: &cloud_hive::FileFooter,
10626 ) {
10627 let Some(cache) = cache else {
10628 return;
10629 };
10630 let columns: Vec<String> = footer
10631 .schema
10632 .iter_names()
10633 .map(|name| name.to_string())
10634 .collect();
10635 cache.record_dataset_facts(&[(
10636 PathBuf::from(full),
10637 crate::cache::DatasetFacts {
10638 mtime: std::time::SystemTime::now()
10639 .duration_since(std::time::UNIX_EPOCH)
10640 .map(|d| d.as_secs())
10641 .unwrap_or_default(),
10642 size: 0,
10643 rows: Some(footer.rows()),
10644 cols: Some(columns.len()),
10645 cols_sampled: false,
10646 columns,
10647 kind: Some(discover::EntryKind::File),
10648 classified_by: discover::CLASSIFIER_VERSION,
10649 cost: discover::Cost {
10650 row_groups: Some(footer.row_group_rows.len()),
10651 ..Default::default()
10652 },
10653 holds: Default::default(),
10654 },
10655 )]);
10656 }
10657
10658 fn schema_state_from_full_scan(
10661 mut lf: LazyFrame,
10662 path: Option<&Path>,
10663 options: &OpenOptions,
10664 ) -> Result<DataTableState> {
10665 let schema = lf
10666 .collect_schema()
10667 .map_err(color_eyre::eyre::Report::from)?;
10668 let partition_columns =
10669 match path.filter(|p| options.hive && (p.is_dir() || source::expands_as_glob(p))) {
10670 Some(p) => DataTableState::discover_hive_partition_columns(p)
10671 .into_iter()
10672 .filter(|c| schema.contains(c.as_str()))
10673 .collect::<Vec<_>>(),
10674 None => Vec::new(),
10675 };
10676 let lf = Self::hoist_partition_columns(lf, &schema, &partition_columns, false);
10677 let part_cols = (!partition_columns.is_empty()).then_some(partition_columns);
10678 DataTableState::from_schema_and_lazyframe(schema, lf, options, part_cols)
10679 }
10680
10681 fn build_schema_state(
10686 lf: LazyFrame,
10687 path: Option<&Path>,
10688 options: &OpenOptions,
10689 cloud: &crate::config::CloudConfig,
10690 runtime: &tokio::runtime::Handle,
10691 report: &crate::measurements::OpenReport,
10692 ) -> Result<(DataTableState, OpenFacts, String)> {
10693 let (state, mut facts, label) =
10698 Self::schema_state_by_route(lf, path, options, cloud, runtime, report)?;
10699 let names_look_like_data = options.format.and_then(FileFormat::separator).is_some()
10706 && options.has_header != Some(false)
10707 && !crate::schema_union::names_are_names(
10708 &state
10709 .schema()
10710 .iter_names()
10711 .map(|n| n.to_string())
10712 .collect::<Vec<_>>(),
10713 );
10714 facts.open_notes = crate::notes::from_the_open(
10715 &options.left_out,
10716 options.read_as_plain_files_of,
10717 options.files_disagree,
10718 names_look_like_data,
10719 );
10720 facts.not_the_table = options.read_as_plain_files_of;
10723 if let Some(splits) = &options.splits {
10724 facts.other_tables = splits.others.clone();
10725 facts
10726 .open_notes
10727 .extend(crate::notes::map_caches(splits.caches));
10728 }
10729 if let Some(read) = &options.format_read {
10730 facts.open_notes.extend(read.notes());
10731 facts.format_read = Some(read.clone());
10732 }
10733 if let Some(opened) = &options.opened {
10734 facts.records = opened.window.clone();
10735 facts.detail = opened.detail.clone();
10736 facts.other_tables = opened.other_tables.clone();
10737 facts.open_notes.extend(opened.notes.iter().cloned());
10738 facts.units = opened.units.clone();
10739 facts.indexing = opened.indexing.clone();
10740 facts.numbering = opened.numbering.clone();
10741 }
10742 if let Some(sqlite) = &options.sqlite {
10743 facts.pushdown = Some(sqlite.pushdown.clone());
10744 facts.hold = sqlite.hold.lock().ok().and_then(|mut hold| hold.take());
10745 facts.other_tables = sqlite.other_tables.clone();
10746 }
10747 if let Some(read) = &options.delimited {
10748 facts.open_notes.extend(read.notes());
10749 facts.delimited = Some(read.clone());
10750 }
10751 facts.open_notes.extend(options.read_notes.iter().cloned());
10752 facts.typing = options.typing.clone();
10753 facts.read_mode = options.read_mode;
10754 facts.read_as = options.format;
10755 facts.remote_source = match &options.arrow_parts {
10760 Some(parts) => parts.iter().any(|part| {
10761 matches!(part, crate::ipc_stream::Part::InPlace(p) if source::is_remote_url(p))
10762 }),
10763 None => path.is_some_and(source::scans_in_place),
10764 };
10765 if options.hive
10771 && facts.remote_files.is_none()
10772 && options.format.is_none_or(|f| f == FileFormat::Parquet)
10773 && let Some(dir) = path.filter(|p| !source::is_remote_url(p) && p.is_dir())
10774 {
10775 facts.parquet_count_dir = Some(dir.to_path_buf());
10776 }
10777 Ok((state, facts, label))
10778 }
10779
10780 #[cfg(feature = "cloud")]
10795 fn scan_cloud_prefix(
10796 url: &str,
10797 cloud_opts: CloudOptions,
10798 format: FileFormat,
10799 glob: bool,
10800 options: &OpenOptions,
10801 ) -> Option<Result<LazyFrame>> {
10802 if !format.reads_bucket_prefix() {
10805 return None;
10806 }
10807 let pl_path = if url.ends_with('/') && !url.contains('*') {
10811 PlRefPath::new(format!("{url}**/*.*").as_str())
10812 } else {
10813 PlRefPath::new(url)
10814 };
10815 let scan = crate::readers::of(format).bucket_scan?;
10816 Some(scan(crate::readers::BucketIn {
10817 url,
10818 path: pl_path,
10819 cloud: cloud_opts,
10820 glob,
10821 options,
10822 format,
10823 }))
10824 }
10825
10826 #[cfg(feature = "cloud")]
10829 fn cloud_glob_format(url: &str, options: &OpenOptions) -> Option<FileFormat> {
10830 options
10831 .format
10832 .or_else(|| {
10833 url.contains('*')
10834 .then(|| FileFormat::from_path(Path::new(url)))
10835 .flatten()
10836 })
10837 .filter(|f| *f != FileFormat::Parquet)
10838 }
10839
10840 #[cfg(feature = "cloud")]
10843 fn resolve_cloud_url(
10844 path: &Path,
10845 cloud: &crate::config::CloudConfig,
10846 ) -> Result<(String, CloudOptions)> {
10847 let text = path.to_string_lossy();
10848 let resolved = crate::cloud_sources::resolve_for_open(&text, cloud)
10849 .map_err(|e| color_eyre::eyre::eyre!(e))?;
10850 use object_store::azure::AzureConfigKey;
10851 use polars::io::cloud::GoogleConfigKey;
10852 let gcs_agent = (
10853 GoogleConfigKey::Client(crate::user_agent::CLIENT_KEY),
10854 crate::user_agent::get(),
10855 );
10856 let options = match resolved.kind {
10857 crate::cloud_browse::ProviderKind::S3 => Self::build_s3_cloud_options(&resolved.s3),
10858 crate::cloud_browse::ProviderKind::Gcs
10859 if resolved.signing == crate::cloud_sources::Signing::Unsigned =>
10860 {
10861 CloudOptions::default()
10862 .with_gcp([(GoogleConfigKey::SkipSignature, "true".into()), gcs_agent])
10863 }
10864 crate::cloud_browse::ProviderKind::Gcs => match &resolved.gcloud {
10865 Some((configuration, _)) => CloudOptions::default()
10868 .with_gcp([gcs_agent])
10869 .with_credential_provider(Some(crate::gcloud::polars_provider(configuration))),
10870 None => match &resolved.google_credentials {
10871 Some(file) => CloudOptions::default().with_gcp([
10872 (
10873 GoogleConfigKey::ApplicationCredentials,
10874 file.to_string_lossy().into_owned(),
10875 ),
10876 gcs_agent,
10877 ]),
10878 None => CloudOptions::default().with_gcp([gcs_agent]),
10879 },
10880 },
10881 crate::cloud_browse::ProviderKind::Azure => {
10882 let (account, _, _) = source::azure_parts(&resolved.url)
10883 .ok_or_else(|| color_eyre::eyre::eyre!("not an Azure URL"))?;
10884 let mut azure = crate::azure::polars_options(&account, &resolved.azure);
10885 azure.push((
10886 AzureConfigKey::Client(crate::user_agent::CLIENT_KEY),
10887 crate::user_agent::get(),
10888 ));
10889 CloudOptions::default().with_azure(azure)
10890 }
10891 };
10892 Ok((resolved.url, options))
10893 }
10894
10895 #[cfg(feature = "cloud")]
10898 fn cloud_store_for(
10899 path: &Path,
10900 cloud: &crate::config::CloudConfig,
10901 runtime: &tokio::runtime::Handle,
10902 ) -> Result<(String, CloudOptions, Arc<dyn object_store::ObjectStore>)> {
10903 let (full, cloud_opts) = Self::resolve_cloud_url(path, cloud)?;
10904 let store = Self::polars_object_store(&full, &cloud_opts, runtime)?;
10905 Ok((full, cloud_opts, store))
10906 }
10907
10908 #[cfg(feature = "cloud")]
10920 fn schema_state_from_cloud_object(
10921 path: &Path,
10922 options: &OpenOptions,
10923 cloud: &crate::config::CloudConfig,
10924 runtime: &tokio::runtime::Handle,
10925 report: &crate::measurements::OpenReport,
10926 ) -> Result<(DataTableState, OpenFacts)> {
10927 let (full, cloud_opts, store) = Self::cloud_store_for(path, cloud, runtime)?;
10928 let (_bucket, key) = Self::cloud_bucket_and_key(&full)?;
10929 if key.is_empty() {
10930 return Err(color_eyre::eyre::eyre!("a bucket, not an object"));
10931 }
10932 let meter = report.meter.clone();
10933 let (footer, etag) = wait_on_runtime(runtime, async move {
10934 cloud_hive::footer_of_cloud_parquet(store, &key, &meter).await
10935 })
10936 .ok_or_else(|| color_eyre::eyre::eyre!("cancelled"))??;
10937 let args = ScanArgsParquet {
10938 schema: Some(footer.schema.clone()),
10939 cloud_options: Some(cloud_opts),
10940 hive_options: polars::io::HiveOptions::default(),
10941 glob: false,
10942 ..Default::default()
10943 };
10944 let lf = LazyFrame::scan_parquet(PlRefPath::new(full.as_str()), args)?;
10945 let state =
10946 DataTableState::from_schema_and_lazyframe(footer.schema.clone(), lf, options, None)?;
10947 Self::record_cloud_object_facts(report.remembered.as_ref(), &full, &footer);
10950 let column_bytes = crate::schema_union::column_bytes_per_row(&[Some(footer.clone())]);
10951 let facts = OpenFacts {
10952 remote_objects: vec![crate::local_copy::RemoteObject {
10953 url: full,
10954 size: footer.file_bytes as u64,
10955 etag,
10956 }],
10957 row_groups: vec![footer.row_group_rows],
10958 column_bytes,
10959 ..Default::default()
10960 };
10961 Ok((state, facts))
10962 }
10963
10964 fn schema_state_by_route(
10967 lf: LazyFrame,
10968 path: Option<&Path>,
10969 options: &OpenOptions,
10970 cloud: &crate::config::CloudConfig,
10971 runtime: &tokio::runtime::Handle,
10972 report: &crate::measurements::OpenReport,
10973 ) -> Result<(DataTableState, OpenFacts, String)> {
10974 #[cfg(not(feature = "cloud"))]
10975 let _ = (cloud, runtime);
10976
10977 let attempt = |report: &crate::measurements::OpenReport| crate::measurements::OpenReport {
10992 progress: report.progress.clone(),
10993 meter: Arc::new(crate::measurements::Meter::default()),
10994 remembered: report.remembered.clone(),
10995 writes: report.writes.clone(),
10996 };
10997
10998 let local = attempt(report);
10999 if let Some((state, facts)) = Self::schema_state_from_local_hive(path, options, &local) {
11000 let facts = OpenFacts {
11001 measurements: local.meter,
11002 ..facts
11003 };
11004 return Ok((state, facts, "one-file (local)".to_string()));
11005 }
11006 if report.progress.is_cancelled() {
11008 return Err(color_eyre::eyre::eyre!("cancelled"));
11009 }
11010 #[cfg(feature = "cloud")]
11011 let cloud_hive_attempt = attempt(report);
11012 #[cfg(feature = "cloud")]
11013 if let Some((state, facts)) =
11014 Self::schema_state_from_cloud_hive(path, options, cloud, runtime, &cloud_hive_attempt)
11015 {
11016 let facts = OpenFacts {
11017 measurements: cloud_hive_attempt.meter,
11018 ..facts
11019 };
11020 return Ok((state, facts, "one-file (cloud)".to_string()));
11021 }
11022 #[cfg(feature = "cloud")]
11023 if let Some(p) = path.filter(|p| {
11024 source::scans_in_place(p)
11025 && !options.hive
11026 && !source::is_prefix_or_glob(&p.to_string_lossy())
11027 }) {
11028 let object = attempt(report);
11029 match Self::schema_state_from_cloud_object(p, options, cloud, runtime, &object) {
11030 Ok((state, facts)) => {
11031 let facts = OpenFacts {
11032 measurements: object.meter,
11033 ..facts
11034 };
11035 return Ok((state, facts, "footer (cloud)".to_string()));
11036 }
11037 Err(e) => {
11040 return Self::schema_state_from_full_scan(lf, path, options).map(|state| {
11044 (
11045 state,
11046 OpenFacts::default(),
11047 format!("full scan (cloud footer: {e})"),
11048 )
11049 });
11050 }
11051 }
11052 }
11053 Self::schema_state_from_full_scan(lf, path, options)
11054 .map(|state| (state, OpenFacts::default(), "full scan".to_string()))
11055 }
11056
11057 fn hugging_face_split(
11061 dir: &Path,
11062 format: FileFormat,
11063 files: Vec<PathBuf>,
11064 options: &OpenOptions,
11065 report: &mut ReadReport,
11066 ) -> Result<Vec<PathBuf>> {
11067 let metadata = || {
11068 ["dataset_info.json", "state.json"]
11069 .iter()
11070 .any(|name| dir.join(name).is_file())
11071 };
11072 if format != FileFormat::Arrow || !metadata() {
11073 return Ok(files);
11074 }
11075 let names: Vec<&str> = files
11076 .iter()
11077 .map(|f| f.file_name().and_then(|n| n.to_str()).unwrap_or_default())
11078 .collect();
11079 let (chosen, splits) = crate::hf_splits::choose(&names, options.table.as_deref())
11080 .map_err(|e| color_eyre::eyre::eyre!("{}: {e}", dir.display()))?;
11081 report.splits = Some(Arc::new(splits));
11082 Ok(chosen.into_iter().map(|i| files[i].clone()).collect())
11083 }
11084
11085 fn one_table(path: Option<&Path>, format: Option<FileFormat>) -> color_eyre::Report {
11087 match path {
11088 Some(path) => crate::error_display::FileError::new(path, cli::one_table(format)).into(),
11089 None => color_eyre::eyre::eyre!(cli::one_table(format)),
11090 }
11091 }
11092
11093 #[cfg(feature = "cloud")]
11095 fn cloud_scan_failed(url: &str, e: &polars::prelude::PolarsError) -> color_eyre::Report {
11096 let said = crate::error_display::user_message_from_polars(e);
11097 let (first, rest) = said.split_once('\n').unwrap_or((&said, ""));
11098 let first = first.trim_end().trim_end_matches('.');
11099 let what = format!("could not read it: {first}. Check the credentials and the URL.");
11100 let what = match rest {
11101 "" => what,
11102 rest => format!("{what}\n{rest}"),
11103 };
11104 crate::error_display::FileError::new(Path::new(url), what).into()
11105 }
11106
11107 fn scan_arrow_parts(
11112 cloud: &crate::config::CloudConfig,
11113 converted: Option<&PathBuf>,
11114 parts: &[crate::ipc_stream::Part],
11115 ) -> Result<LazyFrame> {
11116 use crate::ipc_stream::Part;
11117 #[cfg(not(feature = "cloud"))]
11118 let _ = cloud;
11119 let scan = |path: &Path| -> Result<LazyFrame> {
11120 #[cfg(feature = "cloud")]
11121 if source::is_remote_url(path) {
11122 let (url, cloud_options) = Self::resolve_cloud_url(path, cloud)?;
11123 let args = polars::prelude::UnifiedScanArgs {
11124 cloud_options: Some(cloud_options),
11125 ..Default::default()
11126 };
11127 return Ok(LazyFrame::scan_ipc(
11128 PlRefPath::new(url.as_str()),
11129 Default::default(),
11130 args,
11131 )?);
11132 }
11133 let args = polars::prelude::UnifiedScanArgs {
11136 glob: source::expands_as_glob(path),
11137 ..Default::default()
11138 };
11139 Ok(LazyFrame::scan_ipc(
11140 polars::prelude::PlRefPath::try_from_path(path)?,
11141 Default::default(),
11142 args,
11143 )?)
11144 };
11145 let streams = |offset: u64, rows: u64| -> Result<LazyFrame> {
11146 let file = converted
11147 .ok_or_else(|| color_eyre::eyre::eyre!("No converted Arrow file to read."))?;
11148 let lf = scan(file)?;
11149 let whole = offset == 0
11151 && parts
11152 .iter()
11153 .all(|part| matches!(part, Part::Converted { .. }));
11154 Ok(if whole {
11155 lf
11156 } else {
11157 lf.slice(offset as i64, rows as polars::prelude::IdxSize)
11158 })
11159 };
11160 let mut frames = Vec::new();
11161 let mut run: Option<(u64, u64)> = None;
11162 for part in parts {
11163 match part {
11164 Part::Converted { offset, rows, .. } => {
11165 run = Some(match run {
11166 Some((start, n)) if start + n == *offset => (start, n + rows),
11167 Some((start, n)) => {
11168 frames.push(streams(start, n)?);
11169 (*offset, *rows)
11170 }
11171 None => (*offset, *rows),
11172 });
11173 }
11174 Part::InPlace(path) => {
11175 if let Some((start, n)) = run.take() {
11176 frames.push(streams(start, n)?);
11177 }
11178 frames.push(scan(path)?);
11179 }
11180 }
11181 }
11182 if let Some((start, n)) = run {
11183 frames.push(streams(start, n)?);
11184 }
11185 match frames.len() {
11186 0 => Err(color_eyre::eyre::eyre!("No Arrow files to read.")),
11187 1 => Ok(frames.remove(0)),
11188 _ => Ok(polars::prelude::concat(
11189 frames.as_slice(),
11190 DataTableState::union_of_files(),
11191 )?),
11192 }
11193 }
11194
11195 fn dataset_dict_split(
11199 dir: &Path,
11200 splits: &[String],
11201 options: &OpenOptions,
11202 report: &mut ReadReport,
11203 formats: &crate::formats::Registry,
11204 ) -> Result<Scan> {
11205 let listed: Vec<&str> = splits.iter().map(String::as_str).collect();
11206 let mut picked = crate::hf_splits::pick(&listed, options.table.as_deref())
11207 .map_err(|e| color_eyre::eyre::eyre!("{}: {e}", dir.display()))?;
11208 let split = dir.join(picked.split.as_deref().unwrap_or_default());
11209 let inner = OpenOptions {
11210 table: None,
11211 splits: None,
11212 ..options.clone()
11213 };
11214 let scan = Self::build_local_lazyframe(&[split], &inner, report, formats)?;
11215 picked.caches = report.splits.as_ref().map_or(0, |inner| inner.caches);
11217 report.splits = Some(Arc::new(picked));
11218 Ok(scan)
11219 }
11220
11221 fn files_disagree(
11237 files: &[PathBuf],
11238 options: &OpenOptions,
11239 found: FileFormat,
11240 ) -> crate::schema_union::Disagreement {
11241 let format = options.format.unwrap_or(found);
11245 if format == FileFormat::Parquet {
11246 return Default::default();
11247 }
11248 if options.null_values.is_some() {
11254 return Default::default();
11255 }
11256 crate::schema_union::sample_files(files, format, &Self::read_as(options)).disagreement()
11257 }
11258
11259 fn read_as(options: &OpenOptions) -> crate::schema_union::ReadAs {
11267 crate::schema_union::ReadAs {
11268 delimiter: options.delimiter,
11269 has_header: options.has_header,
11270 skip_rows: options.skip_rows,
11271 skip_lines: options.skip_lines,
11272 infer_schema_length: options.infer_schema_length,
11273 ignore_errors: options.ignore_errors,
11274 try_parse_dates: options.csv_try_parse_dates(),
11275 comment_char: options.comment_char.clone(),
11276 header_rows: options.header_rows.clone(),
11277 header_join: options.header_join.clone(),
11278 }
11279 }
11280
11281 fn refuse_spec_in_place(path: &Path, options: &OpenOptions) -> Result<()> {
11285 if source::is_remote_url(path)
11286 && (options.spec_file.is_some() || options.spec_name.is_some())
11287 {
11288 return Err(crate::error_display::FileError::new(
11289 path,
11290 "a format spec reads one remote object at a time, not a prefix or a glob; name the object",
11291 )
11292 .into());
11293 }
11294 Ok(())
11295 }
11296
11297 fn build_lazyframe_from_paths_with(
11303 cloud: &crate::config::CloudConfig,
11304 paths: &[PathBuf],
11305 options: &OpenOptions,
11306 report: &mut ReadReport,
11307 formats: &crate::formats::Registry,
11308 ) -> Result<Scan> {
11309 if let Some(parts) = &options.arrow_parts {
11312 if options.table.is_some() && options.splits.is_none() {
11313 let named = parts.first().map(|part| match part {
11315 crate::ipc_stream::Part::InPlace(path) => path.as_path(),
11316 crate::ipc_stream::Part::Converted { source, .. } => source.as_path(),
11317 });
11318 return Err(Self::one_table(named, Some(FileFormat::Arrow)));
11319 }
11320 return Self::scan_arrow_parts(cloud, paths.first(), parts).map(Scan::from);
11321 }
11322 #[cfg(not(feature = "cloud"))]
11324 let _ = cloud;
11325 let path = &paths[0];
11326 Self::refuse_spec_in_place(path, options)?;
11327 match source::input_source(path) {
11328 source::InputSource::Http(_url) => {
11329 #[cfg(feature = "http")]
11330 {
11331 return Err(color_eyre::eyre::eyre!(
11332 "HTTP/HTTPS load is handled in the event loop; this path should not be reached."
11333 ));
11334 }
11335 #[cfg(not(feature = "http"))]
11336 {
11337 return Err(color_eyre::eyre::eyre!(
11338 "HTTP/HTTPS URLs are not supported in this build. Rebuild with default features."
11339 ));
11340 }
11341 }
11342 source::InputSource::S3(url) => {
11343 #[cfg(feature = "cloud")]
11344 {
11345 let (full, cloud_opts) =
11346 Self::resolve_cloud_url(Path::new(&format!("s3://{url}")), cloud)?;
11347 let is_glob = source::is_prefix_or_glob(&full);
11348 if let Some(format) = Self::cloud_glob_format(&full, options)
11351 && let Some(lf) = Self::scan_cloud_prefix(
11352 &full,
11353 cloud_opts.clone(),
11354 format,
11355 is_glob,
11356 options,
11357 )
11358 {
11359 return lf.map(Scan::from);
11360 }
11361 let pl_path = PlRefPath::new(full.as_str());
11362 let hive_options = if is_glob {
11363 polars::io::HiveOptions::new_enabled()
11364 } else {
11365 polars::io::HiveOptions::default()
11366 };
11367 let args = ScanArgsParquet {
11368 cloud_options: Some(cloud_opts),
11369 hive_options,
11370 glob: is_glob,
11371 ..Default::default()
11372 };
11373 let lf = LazyFrame::scan_parquet(pl_path, args)
11374 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
11375 return Ok(lf.into());
11379 }
11380 #[cfg(not(feature = "cloud"))]
11381 {
11382 let _ = url;
11383 return Err(color_eyre::eyre::eyre!(
11384 "S3 is not supported in this build. Rebuild with default features and set AWS credentials (e.g. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)."
11385 ));
11386 }
11387 }
11388 source::InputSource::Gcs(url) => {
11389 #[cfg(feature = "cloud")]
11390 {
11391 let (full, cloud_opts) =
11392 Self::resolve_cloud_url(Path::new(&format!("gs://{url}")), cloud)?;
11393 let is_glob = source::is_prefix_or_glob(&full);
11394 if let Some(format) = Self::cloud_glob_format(&full, options)
11397 && let Some(lf) = Self::scan_cloud_prefix(
11398 &full,
11399 cloud_opts.clone(),
11400 format,
11401 is_glob,
11402 options,
11403 )
11404 {
11405 return lf.map(Scan::from);
11406 }
11407 let pl_path = PlRefPath::new(full.as_str());
11408 let hive_options = if is_glob {
11409 polars::io::HiveOptions::new_enabled()
11410 } else {
11411 polars::io::HiveOptions::default()
11412 };
11413 let args = ScanArgsParquet {
11414 cloud_options: Some(cloud_opts),
11415 hive_options,
11416 glob: is_glob,
11417 ..Default::default()
11418 };
11419 let lf = LazyFrame::scan_parquet(pl_path, args)
11420 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
11421 return Ok(lf.into());
11422 }
11423 #[cfg(not(feature = "cloud"))]
11424 {
11425 let _ = url;
11426 return Err(color_eyre::eyre::eyre!(
11427 "GCS (gs://) is not supported in this build. Rebuild with default features."
11428 ));
11429 }
11430 }
11431 source::InputSource::Azure(url) => {
11432 #[cfg(feature = "cloud")]
11433 {
11434 let (full, cloud_opts) = Self::resolve_cloud_url(Path::new(&url), cloud)?;
11435 let is_glob = source::is_prefix_or_glob(&full);
11436 if let Some(format) = Self::cloud_glob_format(&full, options)
11439 && let Some(lf) = Self::scan_cloud_prefix(
11440 &full,
11441 cloud_opts.clone(),
11442 format,
11443 is_glob,
11444 options,
11445 )
11446 {
11447 return lf.map(Scan::from);
11448 }
11449 let args = ScanArgsParquet {
11450 cloud_options: Some(cloud_opts),
11451 hive_options: if is_glob {
11452 polars::io::HiveOptions::new_enabled()
11453 } else {
11454 polars::io::HiveOptions::default()
11455 },
11456 glob: is_glob,
11457 ..Default::default()
11458 };
11459 let lf = LazyFrame::scan_parquet(PlRefPath::new(full.as_str()), args)
11460 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
11461 return Ok(lf.into());
11462 }
11463 #[cfg(not(feature = "cloud"))]
11464 {
11465 let _ = url;
11466 return Err(color_eyre::eyre::eyre!(
11467 "Azure is not supported in this build. Rebuild with default features."
11468 ));
11469 }
11470 }
11471 source::InputSource::Local(_) => {}
11472 }
11473 Self::build_local_lazyframe(paths, options, report, formats)
11474 }
11475
11476 fn read_directory_files(
11479 files: &[PathBuf],
11480 options: &OpenOptions,
11481 found: FileFormat,
11482 report: &mut ReadReport,
11483 formats: &crate::formats::Registry,
11484 ) -> Result<Scan> {
11485 if options.delimited.is_none()
11486 && options.format.is_none()
11487 && found.separator().is_some()
11488 && let Some(first) = files.iter().find(|f| !crate::nul_tail::holds_nothing(f))
11489 && let Some(choice) = Self::delimited_spec_of(first, options, formats)?
11490 {
11491 let nested = OpenOptions {
11492 hive: false,
11493 format: Some(found),
11494 splits: report.splits.clone(),
11495 ..options.clone()
11496 };
11497 return Self::read_with_delimited_spec(files, &nested, report, formats, choice);
11498 }
11499 report.files_disagree = Self::files_disagree(files, options, found);
11501 let nested = OpenOptions {
11502 hive: false,
11503 format: Some(options.format.unwrap_or(found)),
11504 splits: report.splits.clone(),
11505 ..options.clone()
11506 };
11507 Self::build_local_lazyframe(files, &nested, report, formats)
11508 }
11509
11510 fn delimited_spec_of(
11512 file: &Path,
11513 options: &OpenOptions,
11514 formats: &crate::formats::Registry,
11515 ) -> Result<Option<crate::formats::Choice>> {
11516 let asked = crate::formats::Asked {
11517 compression: options.compression,
11518 text_only: true,
11519 ..Default::default()
11520 };
11521 match crate::formats::route(file, &asked, formats)
11522 .map_err(|e| color_eyre::eyre::eyre!(e))?
11523 {
11524 crate::formats::Route::Delimited(choice) => Ok(Some(choice)),
11525 _ => Ok(None),
11526 }
11527 }
11528
11529 fn read_with_delimited_spec(
11532 paths: &[PathBuf],
11533 options: &OpenOptions,
11534 report: &mut ReadReport,
11535 formats: &crate::formats::Registry,
11536 choice: crate::formats::Choice,
11537 ) -> Result<Scan> {
11538 let mut nested = options.clone();
11539 let Some(delimited) = choice.spec.delimited.clone() else {
11540 return Err(color_eyre::eyre::eyre!(
11541 "{} is not a delimited spec",
11542 choice.spec.name
11543 ));
11544 };
11545 delimited.apply(&mut nested);
11546 nested.delimited = Some(Arc::new(crate::delimited_spec::DelimitedRead::chosen(
11547 choice.spec,
11548 choice.by,
11549 choice.also,
11550 )));
11551 Self::build_local_lazyframe(paths, &nested, report, formats)
11552 }
11553
11554 fn build_local_lazyframe(
11557 paths: &[PathBuf],
11558 options: &OpenOptions,
11559 report: &mut ReadReport,
11560 formats: &crate::formats::Registry,
11561 ) -> Result<Scan> {
11562 let path = &paths[0];
11563
11564 if options.hex
11566 && let [one] = paths
11567 && one.is_file()
11568 {
11569 return Ok(Scan::Hex {
11570 file: one.clone(),
11571 asked: true,
11572 });
11573 }
11574
11575 if let [pattern] = paths
11579 && !options.hive
11580 && options.delimited.is_none()
11581 && options.format.is_none()
11582 && source::expands_as_glob(pattern)
11583 {
11584 let files = crate::local_glob::expand(pattern);
11585 if let Some(first) = files.iter().find(|f| !crate::nul_tail::holds_nothing(f))
11586 && let Some(choice) = Self::delimited_spec_of(first, options, formats)?
11587 {
11588 let format = FileFormat::from_path(first).filter(|f| f.separator().is_some());
11589 let nested = OpenOptions {
11590 format: format.or(Some(FileFormat::Csv)),
11591 ..options.clone()
11592 };
11593 return Self::read_with_delimited_spec(&files, &nested, report, formats, choice);
11594 }
11595 }
11596
11597 if !options.hive && options.delimited.is_none() {
11602 let asked = crate::formats::Asked {
11603 spec_file: options.spec_file.clone(),
11604 spec_name: options.spec_name.clone(),
11605 variant: options.table.clone(),
11606 spec: options.spec_fetched.clone(),
11607 builtin: options.format.is_some(),
11608 compression: options.compression,
11609 text_only: paths.len() > 1,
11610 };
11611 match crate::formats::route(path, &asked, formats)
11612 .map_err(|e| color_eyre::eyre::eyre!(e))?
11613 {
11614 crate::formats::Route::Elsewhere => {}
11615 crate::formats::Route::Delimited(choice) => {
11616 return Self::read_with_delimited_spec(paths, options, report, formats, choice);
11617 }
11618 crate::formats::Route::Read(read) => {
11619 let lf = Arc::clone(&read.records).into_lazy()?;
11620 report.format_read = Some(Arc::new(*read));
11621 return Ok(lf.into());
11622 }
11623 crate::formats::Route::Decompress(choice) => {
11624 return Ok(Scan::DecompressSpec {
11625 file: path.clone(),
11626 choice,
11627 });
11628 }
11629 }
11630 } else if options.hive && (options.spec_file.is_some() || options.spec_name.is_some()) {
11631 return Err(color_eyre::eyre::eyre!(
11632 "a format spec reads one file, or one directory of column files"
11633 ));
11634 }
11635
11636 if let Some(read) = &options.delimited
11638 && report.delimited.is_none()
11639 && path.is_file()
11640 {
11641 report.delimited = Some(Arc::new(crate::delimited_spec::read_facts(
11642 read, paths, options,
11643 )?));
11644 }
11645
11646 if paths.len() == 1 && (options.hive || path.is_dir()) {
11652 let is_single_file = path.is_file();
11654 if !is_single_file {
11655 if path.is_dir()
11661 && let Some(splits) = crate::hf_splits::dataset_dict(path)
11662 {
11663 return Self::dataset_dict_split(path, &splits, options, report, formats);
11664 }
11665 if path.is_dir() {
11666 match crate::discover::directory_format(path) {
11667 crate::discover::DirectoryFormat::One(FileFormat::Parquet, _) => {}
11669 crate::discover::DirectoryFormat::Deeper => {
11677 if let crate::discover::DirectoryFormat::One(found, files) =
11678 crate::discover::hive_leaf_format(path)
11679 && found != FileFormat::Parquet
11680 {
11681 let named = files
11684 .first()
11685 .and_then(|f| crate::discover::data_extension(f))
11686 .unwrap_or_else(|| format!("{found:?}").to_lowercase());
11687 return Err(color_eyre::eyre::eyre!(
11688 "{} is partitioned into key=value directories of .{} \
11689 files. datui reads hive partitioning for Parquet \
11690 only — open one partition instead.",
11691 path.display(),
11692 named
11693 ));
11694 }
11695 }
11696 crate::discover::DirectoryFormat::One(found, files) => {
11697 let format = options.format.unwrap_or(found);
11702 let files =
11703 Self::hugging_face_split(path, format, files, options, report)?;
11704 return Self::read_directory_files(
11705 &files, options, found, report, formats,
11706 );
11707 }
11708 crate::discover::DirectoryFormat::Mixed {
11709 format: found,
11710 files,
11711 passed_over,
11712 } => {
11713 let format = options.format.unwrap_or(found);
11719 let files =
11720 Self::hugging_face_split(path, format, files, options, report)?;
11721 let lf = Self::read_directory_files(
11722 &files, options, found, report, formats,
11723 )?;
11724 if !matches!(found, FileFormat::Safetensors | FileFormat::Gguf) {
11730 report.left_out = passed_over;
11731 }
11732 return Ok(lf);
11733 }
11734 }
11735 }
11736 let use_parquet_hive =
11737 path.is_dir() || path.as_os_str().to_string_lossy().contains(".parquet");
11738 if use_parquet_hive {
11739 return DataTableState::scan_parquet_hive(path).map(Scan::from);
11742 }
11743 return Err(color_eyre::eyre::eyre!(
11744 "With --hive use a directory or a glob pattern for Parquet (e.g. path/to/dir or path/**/*.parquet)"
11745 ));
11746 }
11747 }
11748
11749 let compressed = options
11754 .compression
11755 .or_else(|| CompressionFormat::from_extension(path))
11756 .is_some();
11757 let named = FileFormat::from_path(path).or_else(|| {
11760 compressed
11761 .then(|| FileFormat::from_path(Path::new(path.file_stem()?)))
11762 .flatten()
11763 .filter(|f| f.decompressed_once())
11764 });
11765 let mut effective_format = options
11766 .format
11767 .or_else(|| {
11770 named.filter(|f| !f.is_lines()).map(|f| {
11771 (!compressed)
11772 .then(|| crate::readers::refined(path, f))
11773 .flatten()
11774 .unwrap_or(f)
11775 })
11776 })
11777 .or_else(|| {
11778 (path.extension().is_none()
11779 && crate::discover::is_parquet_key(&path.to_string_lossy()))
11780 .then_some(FileFormat::Parquet)
11781 })
11782 .or_else(|| crate::readers::sniff_open(path, options.compression))
11785 .or(named);
11786 if effective_format.is_none()
11789 && let [file] = paths
11790 && file.is_file()
11791 {
11792 effective_format = crate::lines::guess_file(file, options.compression)
11793 .map(|f| crate::lines::as_asked(f, options));
11794 report.guessed = effective_format.is_some();
11795 }
11796 report.format = effective_format;
11797
11798 if options.table.is_some()
11801 && !effective_format.is_some_and(FileFormat::takes_table)
11802 && options.splits.is_none()
11803 {
11804 return Err(Self::one_table(Some(path), effective_format));
11805 }
11806
11807 if let [file] = paths
11812 && compressed
11813 && let Some(format) = effective_format.filter(|f| f.decompressed_once())
11814 {
11815 return Ok(Scan::Decompress {
11816 file: file.clone(),
11817 format,
11818 });
11819 }
11820
11821 let Some(format) = effective_format else {
11822 if !path.exists() {
11823 return Err(std::io::Error::new(
11824 std::io::ErrorKind::NotFound,
11825 format!("File not found: {}", path.display()),
11826 )
11827 .into());
11828 }
11829 if paths.len() == 1 && path.is_file() {
11831 return Ok(Scan::Hex {
11832 file: path.clone(),
11833 asked: false,
11834 });
11835 }
11836 return Err(color_eyre::eyre::eyre!(match paths.len() {
11837 1 => UNSUPPORTED.to_string(),
11838 _ => crate::readers::many_files_refused(),
11839 }));
11840 };
11841 if paths.len() > 1 && !format.reads_many_files() {
11844 if !path.exists() {
11845 return Err(std::io::Error::new(
11846 std::io::ErrorKind::NotFound,
11847 format!("File not found: {}", path.display()),
11848 )
11849 .into());
11850 }
11851 return Err(color_eyre::eyre::eyre!(crate::readers::many_files_refused()));
11852 }
11853 let guessed;
11854 let options = if report.guessed {
11855 guessed = OpenOptions {
11856 format_guessed: true,
11857 ..options.clone()
11858 };
11859 &guessed
11860 } else {
11861 options
11862 };
11863 crate::readers::scan(crate::readers::ScanIn {
11864 format,
11865 paths,
11866 options,
11867 report,
11868 formats,
11869 })
11870 }
11871
11872 pub fn help_visible(&self) -> bool {
11874 self.help.is_open()
11875 }
11876
11877 pub fn help_context(&self) -> Option<datui_cli::keys::Context> {
11879 self.help.context()
11880 }
11881
11882 fn open_view_list(&mut self) {
11884 if self.view_dataset().is_none() {
11885 return;
11886 }
11887 self.view_modal.table_state.select(Some(0));
11888 self.refresh_view_list();
11889 self.view_modal.active = true;
11890 self.view_modal.mode = ViewModalMode::List;
11891 }
11892
11893 fn refresh_view_list(&mut self) {
11896 let (Some(state), Some(dataset)) = (&self.data_table_state, self.view_dataset()) else {
11897 return;
11898 };
11899 let rows: Vec<ViewRow> = self
11900 .view_manager
11901 .find_relevant_views(dataset, state.source_schema())
11902 .into_iter()
11903 .map(|(view, score)| {
11904 let reason = view::match_reason(&view, dataset, state.source_schema());
11905 ViewRow {
11906 view,
11907 score,
11908 reason,
11909 }
11910 })
11911 .collect();
11912 self.view_modal.broken_views = self.view_manager.broken_views.clone();
11913 let selected = self.view_modal.table_state.selected().unwrap_or(0);
11914 self.view_modal.table_state.select(if rows.is_empty() {
11915 None
11916 } else {
11917 Some(selected.min(rows.len() - 1))
11918 });
11919 self.view_modal.rows = rows;
11920 }
11921
11922 fn open_save_view_form(&mut self) {
11927 self.view_modal
11928 .enter_create_mode(self.history_limit, &self.theme);
11929
11930 let query = self.data_table_state.as_ref().and_then(|state| {
11931 let (query, sql_query, fuzzy_query) = active_query_settings(
11932 state.get_active_query(),
11933 state.get_active_sql_query(),
11934 state.get_active_fuzzy_query(),
11935 );
11936 sql_query.or(fuzzy_query).or(query)
11937 });
11938 self.view_modal.name_input.suggest(
11939 self.view_manager
11940 .suggest_name(self.path.as_deref(), query.as_deref()),
11941 );
11942
11943 if let Some(path) = self.path.as_ref().filter(|_| !self.reads_stdin()) {
11945 let absolute_path = view::exact_location(path);
11948 self.view_modal
11949 .exact_path_input
11950 .suggest(absolute_path.to_string_lossy());
11951 if let Some(relative) = view::relative_location(path) {
11952 self.view_modal.relative_path_input.suggest(relative);
11953 }
11954
11955 if let Some(parent) = absolute_path.parent()
11960 && let Some(parent_str) = parent.to_str()
11961 && !parent_str.is_empty()
11962 && let Some(ext) = absolute_path.extension()
11963 {
11964 let separator = if crate::source::is_remote_url(path) {
11965 '/'
11966 } else {
11967 std::path::MAIN_SEPARATOR
11968 };
11969 self.view_modal.path_pattern_input.suggest(format!(
11970 "{}{separator}*.{}",
11971 parent_str.trim_end_matches(separator),
11972 ext.to_string_lossy()
11973 ));
11974 }
11975
11976 if let Some(filename) = path.file_name()
11979 && let Some(filename_str) = filename.to_str()
11980 {
11981 use regex::Regex;
11982 let pattern = match Regex::new(r"\d+") {
11983 Ok(re) => re.replace_all(filename_str, "*").to_string(),
11984 Err(_) => filename_str.to_string(),
11985 };
11986 self.view_modal.filename_pattern_input.suggest(pattern);
11987 }
11988 }
11989
11990 self.view_modal.table = self.view_table().map(str::to_string);
11991
11992 if let Some(ref state) = self.data_table_state
11996 && !state.source_schema().is_empty()
11997 {
11998 self.view_modal.schema_match_enabled = true;
11999 }
12000 }
12001
12002 fn save_view_form(&mut self) {
12006 self.view_modal.name_error = None;
12007 let name = self.view_modal.name_input.value().trim().to_string();
12008 if name.is_empty() {
12009 self.view_modal.name_error = Some("name is required".to_string());
12010 self.view_modal.form_focus = FormFocus::Name;
12011 return;
12012 }
12013 let renaming_to_taken = match &self.view_modal.editing_view_id {
12014 None => self.view_manager.view_exists(&name),
12015 Some(id) => self
12016 .view_manager
12017 .get_view_by_name(&name)
12018 .is_some_and(|other| other.id != *id),
12019 };
12020 if renaming_to_taken {
12021 self.view_modal.name_error = Some("name already exists".to_string());
12022 self.view_modal.form_focus = FormFocus::Name;
12023 return;
12024 }
12025
12026 let non_empty = |input: &widgets::text_input::TextInput| {
12027 let value = input.value().trim();
12028 (!value.is_empty()).then(|| value.to_string())
12029 };
12030 let match_criteria = view::MatchCriteria {
12031 exact_path: non_empty(&self.view_modal.exact_path_input).map(std::path::PathBuf::from),
12032 relative_path: non_empty(&self.view_modal.relative_path_input),
12033 path_pattern: non_empty(&self.view_modal.path_pattern_input),
12034 filename_pattern: non_empty(&self.view_modal.filename_pattern_input),
12035 schema_columns: if self.view_modal.schema_match_enabled {
12038 self.data_table_state.as_ref().map(|state| {
12039 state
12040 .source_schema()
12041 .iter_names()
12042 .map(|s| s.to_string())
12043 .collect()
12044 })
12045 } else {
12046 None
12047 },
12048 schema_types: None,
12049 table: self.view_modal.table.clone(),
12050 };
12051 let description = {
12052 let value = self.view_modal.description_input.value();
12053 (!value.is_empty()).then(|| value.to_string())
12054 };
12055
12056 let saved = if let Some(editing_id) = self.view_modal.editing_view_id.clone() {
12057 let Some(mut view) = self.view_manager.get_view_by_id(&editing_id).cloned() else {
12058 return;
12059 };
12060 view.name = name;
12061 view.description = description;
12062 let stored_schema = view.match_criteria.schema_columns.take();
12063 view.match_criteria = match_criteria;
12064 let editing_the_active_view =
12065 self.active_view_id.as_deref() == Some(editing_id.as_str());
12066 if !editing_the_active_view
12071 && self.view_modal.schema_match_enabled
12072 && stored_schema.is_some()
12073 {
12074 view.match_criteria.schema_columns = stored_schema;
12075 }
12076 if editing_the_active_view && let Some(state) = &self.data_table_state {
12081 view.settings = view_settings_of(state);
12082 view.settings.chart = self.saved_chart();
12083 }
12084 match self.view_manager.update_view(&view) {
12085 Ok(()) => true,
12086 Err(e) => {
12087 if self.view_manager.get_view_by_id(&editing_id).is_none() {
12090 self.refresh_view_list();
12091 self.view_modal.exit_form();
12092 }
12093 self.error_modal.show(format!("Error saving view: {e}"));
12094 return;
12095 }
12096 }
12097 } else {
12098 self.create_view_from_current_state(name, description, match_criteria)
12099 .is_ok()
12100 };
12101 if saved {
12102 self.refresh_view_list();
12103 self.view_modal.exit_form();
12104 }
12105 }
12106
12107 fn view_score_details(&self) -> Option<(String, String)> {
12109 let state = self.data_table_state.as_ref()?;
12110 let path = self.view_dataset()?;
12111 let idx = self.view_modal.table_state.selected()?;
12112 let row = self.view_modal.rows.get(idx)?;
12113 let view = &row.view;
12114
12115 let exact_path_match = view::exact_path_matches(&view.match_criteria, path);
12116 let relative_path_match = view::relative_path_matches(&view.match_criteria, path);
12117 let file_cols: std::collections::HashSet<&str> = state
12118 .source_schema()
12119 .iter_names()
12120 .map(|s| s.as_str())
12121 .collect();
12122 let exact_schema_match =
12123 view.match_criteria
12124 .schema_columns
12125 .as_ref()
12126 .is_some_and(|required| {
12127 let required: std::collections::HashSet<&str> =
12128 required.iter().map(|s| s.as_str()).collect();
12129 required.is_subset(&file_cols) && file_cols.len() == required.len()
12130 });
12131
12132 let mut details = format!("Total score: {:.1}\n\n", row.score);
12133 if exact_path_match && exact_schema_match {
12134 details.push_str("Exact path + exact schema: 2000.0\n");
12135 } else if exact_path_match {
12136 details.push_str("Exact path: 1000.0\n");
12137 } else if relative_path_match && exact_schema_match {
12138 details.push_str("Relative path + exact schema: 1950.0\n");
12139 } else if relative_path_match {
12140 details.push_str("Relative path: 950.0\n");
12141 } else if exact_schema_match {
12142 details.push_str("Exact schema: 900.0\n");
12143 } else {
12144 if view::path_pattern_matches(&view.match_criteria, path) {
12145 details.push_str("Path pattern match: 50.0+\n");
12146 }
12147 if view::filename_pattern_matches(&view.match_criteria, path) {
12148 details.push_str("Filename pattern match: 30.0+\n");
12149 }
12150 if let Some(required_cols) = &view.match_criteria.schema_columns {
12151 let matching_count = required_cols
12152 .iter()
12153 .filter(|col| file_cols.contains(col.as_str()))
12154 .count();
12155 if matching_count > 0 {
12156 details.push_str(&format!(
12157 "Partial schema match: {:.1} ({} columns)\n",
12158 matching_count as f64 * 2.0,
12159 matching_count
12160 ));
12161 }
12162 }
12163 }
12164 if view.usage_count > 0 {
12165 details.push_str(&format!(
12166 "Usage count: {:.1}\n",
12167 (view.usage_count.min(10) as f64) * 1.0
12168 ));
12169 }
12170 if let Some(last_used) = view.last_used
12171 && let Ok(duration) = std::time::SystemTime::now().duration_since(last_used)
12172 {
12173 let days_since = duration.as_secs() / 86400;
12174 if days_since <= 7 {
12175 details.push_str("Recent usage: 5.0\n");
12176 } else if days_since <= 30 {
12177 details.push_str("Recent usage: 2.0\n");
12178 }
12179 }
12180 Some((format!("Score: {}", view.name), details))
12181 }
12182
12183 pub(crate) fn open_help_overlay(&mut self) {
12186 if self.help.is_open() || self.confirmation_modal.active || self.error_modal.active {
12188 return;
12189 }
12190 let context = self.keys_context();
12191 let typing = self.text_field_focused()
12193 || (self.input_mode == InputMode::Home
12194 && !self.documentation.is_open()
12195 && (!self.home.filter.is_empty() || self.home.path_input_active));
12196 self.help.open(context, typing);
12197 }
12198
12199 fn close_help_left_behind(&mut self) {
12204 let left = self
12205 .help
12206 .context()
12207 .is_some_and(|shown| shown != self.keys_context());
12208 if left || self.confirmation_modal.active || self.error_modal.active {
12209 self.help.close();
12210 }
12211 }
12212
12213 pub fn keys_context(&self) -> datui_cli::keys::Context {
12215 use crate::analysis_modal::{AnalysisTool, AnalysisView};
12216 use datui_cli::keys::Context;
12217 if self.analysis_modal.active {
12218 return match self.analysis_modal.view {
12219 AnalysisView::DistributionDetail => Context::DistributionDetail,
12220 AnalysisView::CorrelationDetail => Context::CorrelationDetail,
12221 AnalysisView::Main => match self.analysis_modal.selected_tool {
12222 Some(AnalysisTool::DistributionAnalysis) => Context::Distribution,
12223 Some(AnalysisTool::CorrelationMatrix) => Context::Correlation,
12224 Some(AnalysisTool::DataQuality) => Context::DataQuality,
12225 Some(AnalysisTool::Describe) | None => Context::Describe,
12226 },
12227 };
12228 }
12229 if self.view_modal.active {
12230 return Context::Views;
12231 }
12232 match self.input_mode {
12233 InputMode::Normal => Context::Table,
12234 InputMode::Editing => match self.input_type {
12235 Some(InputType::Find) => Context::Find,
12236 _ => Context::Query,
12237 },
12238 InputMode::SortFilter => Context::SortFilter,
12239 InputMode::PivotMelt => Context::PivotMelt,
12240 InputMode::Export => Context::Export,
12241 InputMode::Copy => Context::Copy,
12242 InputMode::Inspect => Context::Inspector,
12243 InputMode::GoToColumn => Context::GoToColumn,
12244 InputMode::PickFormat => Context::FormatPicker,
12245 InputMode::Retype => Context::Retype,
12246 InputMode::Combine => Context::Combine,
12247 InputMode::PickTable => Context::TablePicker,
12248 InputMode::Sample => Context::Sample,
12249 InputMode::Info => Context::Info,
12250 InputMode::Chart => Context::Chart,
12251 InputMode::Home if self.documentation.is_open() => Context::Documentation,
12252 InputMode::Home => Context::Home,
12253 InputMode::Hex => Context::Hex,
12254 InputMode::ValueCounts => Context::ValueCounts,
12255 }
12256 }
12257
12258 pub fn awaiting_open_confirmation(&self) -> bool {
12265 self.confirmation_modal.active && self.loading.asking()
12266 }
12267
12268 fn key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
12269 self.debug.on_key(event);
12270
12271 self.flash = None;
12274 self.cancel_drag();
12276
12277 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
12278 if ctrl && event.code == KeyCode::Char('q') {
12282 return Some(AppEvent::Exit);
12283 }
12284 if ctrl && event.code == KeyCode::Char('c') {
12287 return Some(AppEvent::Exit);
12288 }
12289
12290 if !self.menu_showing() {
12295 self.context_menu = None;
12296 }
12297 if let Some(menu) = self.context_menu.as_mut() {
12298 match menu.key(event) {
12299 context_menu::MenuKey::Moved => return None,
12300 context_menu::MenuKey::Close => {
12301 self.context_menu = None;
12302 return None;
12303 }
12304 context_menu::MenuKey::Run(key) => {
12305 self.context_menu = None;
12306 return Some(AppEvent::Press(key));
12307 }
12308 context_menu::MenuKey::Do(action) => {
12309 self.context_menu = None;
12310 self.menu_action(action);
12311 return None;
12312 }
12313 context_menu::MenuKey::Other => self.context_menu = None,
12314 }
12315 }
12316
12317 if event.code == KeyCode::Esc && self.view_applying() {
12320 self.cancel_view();
12321 return None;
12322 }
12323 if event.code == KeyCode::Esc && self.finding() {
12325 self.cancel_find();
12326 return None;
12327 }
12328 if event.code == KeyCode::Esc
12330 && self.input_mode == InputMode::Normal
12331 && self.in_normal_table_view()
12332 && self.footers_counted().is_some()
12333 {
12334 self.stop_count();
12335 return None;
12336 }
12337
12338 if event.code == KeyCode::Esc
12339 && self.input_mode == InputMode::Normal
12340 && !self.analysis_modal.active
12341 && !self.error_modal.active
12342 && !self.confirmation_modal.active
12343 && self.return_from_quality_evidence(true)
12344 {
12345 return None;
12346 }
12347
12348 if event.code == KeyCode::F(1) {
12351 if self.help.is_open() {
12352 self.help.close();
12353 } else {
12354 self.open_help_overlay();
12355 }
12356 return None;
12357 }
12358
12359 if self.input_mode == InputMode::Home
12364 && !self.confirmation_modal.active
12365 && !self.error_modal.active
12366 && !self.help.is_open()
12367 {
12368 return self.home_key(event);
12369 }
12370
12371 if event.code == KeyCode::Char('o')
12375 && event.modifiers.contains(KeyModifiers::CONTROL)
12376 && (!self.confirmation_modal.active || self.awaiting_open_confirmation())
12377 {
12378 self.help.close();
12379 self.enter_home();
12380 return None;
12381 }
12382
12383 if self.confirmation_modal.active {
12386 match event.code {
12387 KeyCode::Left | KeyCode::Char('h') => {
12388 self.confirmation_modal.focus_yes = true;
12389 }
12390 KeyCode::Right | KeyCode::Char('l') => {
12391 self.confirmation_modal.focus_yes = false;
12392 }
12393 KeyCode::Tab => {
12394 self.confirmation_modal.focus_yes = !self.confirmation_modal.focus_yes;
12396 }
12397 KeyCode::Up | KeyCode::Char('k') => {
12400 self.confirmation_modal.scroll =
12401 self.confirmation_modal.scroll.saturating_sub(1);
12402 }
12403 KeyCode::Down | KeyCode::Char('j') => {
12404 self.confirmation_modal.scroll =
12405 self.confirmation_modal.scroll.saturating_add(1);
12406 }
12407 KeyCode::Enter if self.pending_leave.is_some() => {
12408 let stop = self.confirmation_modal.focus_yes;
12409 self.confirmation_modal.hide();
12410 return self.leave_recording(stop);
12411 }
12412 KeyCode::Enter => {
12413 if self.confirmation_modal.focus_yes {
12414 if std::mem::take(&mut self.pending_read_all) {
12417 self.confirmation_modal.hide();
12418 let sample = sampling::Sample {
12421 method: sampling::SampleMethod::EveryRow,
12422 ..self.analysis_modal.sample.clone()
12423 };
12424 return self.apply_sample(sample);
12425 }
12426 if let Some(url) = self.pending_link.take() {
12427 self.confirmation_modal.hide();
12428 return Some(AppEvent::OpenLink(url));
12429 }
12430 if self.pending_clear_recents {
12431 self.pending_clear_recents = false;
12432 self.confirmation_modal.hide();
12433 self.cache.clear_recents();
12434 self.home_refresh();
12435 self.home.status = None;
12436 return None;
12437 }
12438 if self.analysis_modal.data_quality_confirm_run {
12440 self.confirmation_modal.hide();
12441 let event = self.run_quality_setup();
12442 self.analysis_modal.data_quality_confirm_run = false;
12444 return event;
12445 }
12446 if std::mem::take(&mut self.pending_hide_examples) {
12447 self.confirmation_modal.hide();
12448 self.cache.hide_examples();
12449 self.home_refresh();
12450 self.home.select_first_entry();
12451 return None;
12452 }
12453 if let Some(id) = self.pending_delete_view.take() {
12454 self.confirmation_modal.hide();
12455 if self.view_manager.delete_view(&id).is_ok() {
12456 self.refresh_view_list();
12457 }
12458 return None;
12459 }
12460 if let Some(place) = self.pending_forget_place.take() {
12461 self.confirmation_modal.hide();
12462 let paths = self.home.recents_in(&place);
12463 self.cache.forget_recents(&paths);
12464 self.home_refresh();
12465 self.home.status = None;
12466 return None;
12467 }
12468 if let Some((path, format)) = self.pending_quality_export.take() {
12471 self.confirmation_modal.hide();
12472 return Some(AppEvent::QualityReportExport(
12473 path,
12474 format,
12475 Overwrite::Replace,
12476 ));
12477 }
12478 if let Some(request) = self.pending_chart_export.take() {
12479 self.confirmation_modal.hide();
12480 return Some(AppEvent::ChartExport(ChartExportRequest {
12481 overwrite: Overwrite::Replace,
12482 ..request
12483 }));
12484 }
12485 if let Some(request) = self.pending_export.take() {
12486 self.confirmation_modal.hide();
12487 return Some(AppEvent::Export(ExportRequest {
12488 overwrite: Overwrite::Replace,
12489 ..request
12490 }));
12491 }
12492 if let Some((format, header)) = self.pending_copy.take() {
12493 self.confirmation_modal.hide();
12494 return Some(AppEvent::CopyTable { format, header });
12495 }
12496 if self.loading.asking() {
12497 self.confirmation_modal.hide();
12498 let step = self.loading.confirmed();
12502 return self.run_load_step(step);
12503 }
12504 } else {
12505 self.pending_clear_recents = false;
12506 self.pending_link = None;
12507 self.pending_read_all = false;
12508 self.pending_forget_place = None;
12509 self.pending_delete_view = None;
12510 self.pending_hide_examples = false;
12511 self.analysis_modal.data_quality_confirm_run = false;
12514 if self.pending_chart_export.take().is_some() {
12517 self.chart_export_modal.resume();
12518 }
12519 self.pending_quality_export = None;
12521 if self.pending_export.take().is_some() {
12522 self.export_modal.resume();
12523 self.input_mode = InputMode::Export;
12524 }
12525 self.pending_copy = None;
12526 if self.loading.asking() {
12527 self.enter_home();
12528 return None;
12529 }
12530 self.confirmation_modal.hide();
12531 }
12532 }
12533 KeyCode::Esc => {
12534 self.pending_clear_recents = false;
12537 self.pending_link = None;
12538 self.pending_read_all = false;
12539 self.pending_forget_place = None;
12540 self.pending_delete_view = None;
12541 self.pending_hide_examples = false;
12542 self.analysis_modal.data_quality_confirm_run = false;
12543 self.pending_leave = None;
12545 if self.pending_chart_export.take().is_some() {
12548 self.chart_export_modal.resume();
12549 }
12550 self.pending_quality_export = None;
12551 if self.pending_export.take().is_some() {
12552 self.export_modal.resume();
12553 self.input_mode = InputMode::Export;
12554 }
12555 self.pending_copy = None;
12556 if self.loading.asking() {
12557 self.enter_home();
12561 return None;
12562 }
12563 self.confirmation_modal.hide();
12564 }
12565 _ => {}
12566 }
12567 return None;
12568 }
12569 if self.error_modal.active {
12571 match event.code {
12572 KeyCode::Up | KeyCode::Char('k') => {
12574 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(1);
12575 return None;
12576 }
12577 KeyCode::Down | KeyCode::Char('j') => {
12578 self.error_modal.scroll = self.error_modal.scroll.saturating_add(1);
12579 return None;
12580 }
12581 KeyCode::PageUp => {
12582 self.error_modal.scroll = self.error_modal.scroll.saturating_sub(8);
12583 return None;
12584 }
12585 KeyCode::PageDown => {
12586 self.error_modal.scroll = self.error_modal.scroll.saturating_add(8);
12587 return None;
12588 }
12589 KeyCode::Esc | KeyCode::Enter => {
12590 self.error_modal.hide();
12591 if self.data_table_state.is_none() {
12596 let reason = self.last_load_error.take();
12597 self.enter_home();
12598 self.home.status = reason;
12599 }
12600 }
12601 _ => {}
12602 }
12603 return None;
12604 }
12605
12606 let in_main_table = !(self.input_mode != InputMode::Normal
12611 || self.help.is_open()
12612 || self.view_modal.active
12613 || self.analysis_modal.active);
12614 if in_main_table && event.is_press() {
12617 self.column_hints = Self::column_cursor_key(event).is_some()
12618 || (self.column_hints
12619 && matches!(
12620 event.code,
12621 KeyCode::Char(
12622 '+' | '-' | 'F' | '[' | ']' | 'H' | 'L' | '<' | '>' | '=' | 'w'
12623 )
12624 ));
12625 }
12626 if in_main_table
12627 && let Some(mv) = Self::column_cursor_key(event)
12628 && let Some(state) = self.data_table_state.as_mut()
12629 {
12630 state.move_cursor(mv);
12631 if self.debug.enabled {
12632 self.debug.last_action = format!("move_cursor({mv:?})");
12633 }
12634 return None;
12635 }
12636
12637 self.close_help_left_behind();
12641 if self.help.is_open() {
12642 return match self.help.key(event) {
12643 help::HelpKey::Press(key) => Some(AppEvent::Press(key)),
12644 help::HelpKey::Stay | help::HelpKey::Closed => None,
12645 };
12646 }
12647
12648 if event.code == KeyCode::Char('?') {
12649 let ctrl_help = event.modifiers.contains(KeyModifiers::CONTROL);
12650 let in_text_input = self.text_field_focused() || self.input_mode == InputMode::Home;
12652 if ctrl_help || !in_text_input {
12654 self.open_help_overlay();
12655 return None;
12656 }
12657 }
12658
12659 if self.input_mode == InputMode::SortFilter {
12660 return self.sort_filter_key(event);
12661 }
12662
12663 if self.input_mode == InputMode::Export {
12664 return self.export_key(event);
12665 }
12666
12667 if self.input_mode == InputMode::Sample {
12668 return self.table_sample_form_key(event);
12669 }
12670
12671 if self.input_mode == InputMode::Inspect {
12672 return self.inspector_key(event);
12673 }
12674
12675 if self.input_mode == InputMode::ValueCounts {
12676 return self.value_counts_key(event);
12677 }
12678
12679 if self.input_mode == InputMode::Hex {
12680 return self.hex_key(event);
12681 }
12682
12683 if self.input_mode == InputMode::GoToColumn {
12684 self.go_to_column_key(event);
12685 return None;
12686 }
12687
12688 if self.input_mode == InputMode::PickFormat {
12689 return self.format_picker_key(event);
12690 }
12691
12692 if self.input_mode == InputMode::Retype {
12693 return self.retype_key(event);
12694 }
12695
12696 if self.input_mode == InputMode::Combine {
12697 return self.combine_key(event);
12698 }
12699
12700 if self.input_mode == InputMode::PickTable {
12701 return self.table_picker_key(event);
12702 }
12703
12704 if self.input_mode == InputMode::Copy {
12705 return self.copy_key(event);
12706 }
12707
12708 if self.input_mode == InputMode::PivotMelt {
12709 return self.pivot_melt_key(event);
12710 }
12711
12712 if self.input_mode == InputMode::Info {
12713 return self.info_key(event);
12714 }
12715
12716 if self.input_mode == InputMode::Chart {
12717 return self.chart_key(event);
12718 }
12719
12720 if self.analysis_modal.active {
12721 return self.analysis_key(event);
12722 }
12723
12724 if self.view_modal.active {
12725 return self.view_key(event);
12726 }
12727
12728 if self.input_mode == InputMode::Editing {
12729 return self.editing_key(event);
12730 }
12731
12732 const RIGHT_KEYS: [KeyCode; 2] = [KeyCode::Right, KeyCode::Char('l')];
12733
12734 const LEFT_KEYS: [KeyCode; 2] = [KeyCode::Left, KeyCode::Char('h')];
12735
12736 const DOWN_KEYS: [KeyCode; 2] = [KeyCode::Down, KeyCode::Char('j')];
12737
12738 const UP_KEYS: [KeyCode; 2] = [KeyCode::Up, KeyCode::Char('k')];
12739
12740 if event
12746 .modifiers
12747 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
12748 && !matches!(event.code, KeyCode::Char('f' | 'b' | 'd' | 'u'))
12749 {
12750 return None;
12751 }
12752
12753 match event.code {
12754 KeyCode::Char('q') => {
12758 if self.opened_from_home {
12759 self.enter_home();
12760 None
12761 } else {
12762 Some(AppEvent::Exit)
12763 }
12764 }
12765 KeyCode::Char('Q') => Some(AppEvent::Exit),
12766 KeyCode::Char('R') => Some(AppEvent::Reset),
12767 KeyCode::Char('H' | 'L') if event.is_press() => {
12768 self.move_cursor_column(event.code == KeyCode::Char('L'))
12769 }
12770 KeyCode::Char('+' | '-') if event.is_press() => {
12771 self.quick_filter(event.code == KeyCode::Char('+'))
12772 }
12773 KeyCode::Char('#') => {
12774 let renumbered = self
12775 .data_table_state
12776 .as_mut()
12777 .is_some_and(|state| state.deferred(|s| s.toggle_row_numbers()));
12778 if renumbered {
12779 self.spawn_async_collect(Self::LOADING_BUFFER);
12780 }
12781 None
12782 }
12783 KeyCode::Char('<' | '>' | '=' | 'w')
12785 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
12786 {
12787 if let Some(state) = self.data_table_state.as_mut()
12788 && let Some(name) = state.current_column().map(str::to_string)
12789 {
12790 let (choice, shown) = (state.width_choice(&name), state.on_screen_width(&name));
12793 let width = match event.code {
12794 KeyCode::Char('<') => choice.narrower(shown),
12795 KeyCode::Char('>') => choice.wider(shown),
12796 KeyCode::Char('=') => WidthChoice::Fit,
12797 _ => WidthChoice::Auto,
12798 };
12799 state.set_width_choices([(name, width)]);
12800 }
12801 None
12802 }
12803 KeyCode::Char('/' | 'f')
12805 if event.is_press() && !event.modifiers.contains(KeyModifiers::CONTROL) =>
12806 {
12807 self.open_find();
12808 None
12809 }
12810 KeyCode::Char('[' | ']') if event.is_press() => {
12811 self.sort_by_cursor_column(event.code == KeyCode::Char(']'))
12812 }
12813 KeyCode::Char('n') if event.is_press() => {
12814 self.find_again(find::Direction::Next);
12815 None
12816 }
12817 KeyCode::Char('N') if event.is_press() => {
12818 self.find_again(find::Direction::Previous);
12819 None
12820 }
12821 KeyCode::Char('D') => {
12822 self.dtype_row = !self.dtype_row;
12825 if self.debug.enabled {
12826 self.debug.last_action = format!(
12827 "toggle_dtype_row({})",
12828 if self.dtype_row { "on" } else { "off" }
12829 );
12830 }
12831 None
12832 }
12833 KeyCode::Char('F') => {
12834 self.open_value_counts();
12835 None
12836 }
12837 KeyCode::Char(',') => {
12838 self.number_format.enabled = !self.number_format.enabled;
12842 if self.debug.enabled {
12843 self.debug.last_action = format!(
12844 "toggle_number_format({})",
12845 if self.number_format.enabled {
12846 "on"
12847 } else {
12848 "off"
12849 }
12850 );
12851 }
12852 None
12853 }
12854 KeyCode::Esc => {
12855 if self.find_shown() {
12857 self.find.active = None;
12858 return None;
12859 }
12860 if self.sample_drawing() {
12862 self.stop_sample_draw();
12863 return None;
12864 }
12865 let mut from_counts = false;
12866 let drilled_up = if let Some(ref mut state) = self.data_table_state {
12867 if state.is_drilled_down() {
12868 from_counts = state.drilled_into_value();
12869 let _ = state.deferred(|s| s.drill_up());
12870 true
12871 } else {
12872 false
12873 }
12874 } else {
12875 false
12876 };
12877 if drilled_up {
12878 self.sync_sort_filter_modal();
12879 }
12880 if from_counts
12883 && std::mem::take(&mut self.value_counts.drill_return)
12884 && let Some(state) = self.data_table_state.as_ref()
12885 {
12886 self.value_counts.rebase(state.len_generation());
12887 self.input_mode = InputMode::ValueCounts;
12888 }
12889 if drilled_up {
12890 self.spawn_async_collect(Self::LOADING_BUFFER);
12891 return None;
12892 }
12893 if let Some(state) = self.data_table_state.as_mut()
12895 && state.follow().is_some()
12896 {
12897 state.stop_following();
12898 self.flash_note("Stopped following".to_string());
12899 }
12900 None
12903 }
12904 KeyCode::Char('t') if event.is_press() => self.toggle_follow(),
12905 code if RIGHT_KEYS.contains(&code) || LEFT_KEYS.contains(&code) => {
12906 if let Some(ref mut state) = self.data_table_state {
12907 state.move_cursor(if RIGHT_KEYS.contains(&code) {
12908 crate::widgets::column_paging::CursorMove::Right
12909 } else {
12910 crate::widgets::column_paging::CursorMove::Left
12911 });
12912 }
12913 None
12914 }
12915 code if event.is_press() && DOWN_KEYS.contains(&code) => {
12916 let would_collect = self
12917 .data_table_state
12918 .as_ref()
12919 .map(|s| s.scroll_would_trigger_collect(1))
12920 .unwrap_or(false);
12921 if would_collect {
12922 self.busy = true;
12923 Some(AppEvent::DoScrollNext)
12924 } else {
12925 if let Some(ref mut s) = self.data_table_state {
12926 s.select_next();
12927 }
12928 None
12929 }
12930 }
12931 code if event.is_press() && UP_KEYS.contains(&code) => {
12932 let would_collect = self
12933 .data_table_state
12934 .as_ref()
12935 .map(|s| s.scroll_would_trigger_collect(-1))
12936 .unwrap_or(false);
12937 if would_collect {
12938 self.busy = true;
12939 Some(AppEvent::DoScrollPrev)
12940 } else {
12941 if let Some(ref mut s) = self.data_table_state {
12942 s.select_previous();
12943 }
12944 None
12945 }
12946 }
12947 KeyCode::PageDown if event.is_press() => {
12948 let would_collect = self
12949 .data_table_state
12950 .as_ref()
12951 .map(|s| s.scroll_would_trigger_collect(s.visible_rows as i64))
12952 .unwrap_or(false);
12953 if would_collect {
12954 self.busy = true;
12955 Some(AppEvent::DoScrollDown)
12956 } else {
12957 if let Some(ref mut s) = self.data_table_state {
12958 s.page_down();
12959 }
12960 None
12961 }
12962 }
12963 KeyCode::Home if event.is_press() => self.jump_key(AppEvent::DoScrollHome),
12964 KeyCode::End | KeyCode::Char('G') if event.is_press() => {
12965 self.jump_key(AppEvent::DoScrollEnd)
12966 }
12967 KeyCode::Char('f')
12968 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
12969 {
12970 let would_collect = self
12971 .data_table_state
12972 .as_ref()
12973 .map(|s| s.scroll_would_trigger_collect(s.visible_rows as i64))
12974 .unwrap_or(false);
12975 if would_collect {
12976 self.busy = true;
12977 Some(AppEvent::DoScrollDown)
12978 } else {
12979 if let Some(ref mut s) = self.data_table_state {
12980 s.page_down();
12981 }
12982 None
12983 }
12984 }
12985 KeyCode::Char('b')
12986 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
12987 {
12988 let would_collect = self
12989 .data_table_state
12990 .as_ref()
12991 .map(|s| s.scroll_would_trigger_collect(-(s.visible_rows as i64)))
12992 .unwrap_or(false);
12993 if would_collect {
12994 self.busy = true;
12995 Some(AppEvent::DoScrollUp)
12996 } else {
12997 if let Some(ref mut s) = self.data_table_state {
12998 s.page_up();
12999 }
13000 None
13001 }
13002 }
13003 KeyCode::Char('d')
13004 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
13005 {
13006 let half = self
13007 .data_table_state
13008 .as_ref()
13009 .map(|s| (s.visible_rows / 2).max(1) as i64)
13010 .unwrap_or(1);
13011 let would_collect = self
13012 .data_table_state
13013 .as_ref()
13014 .map(|s| s.scroll_would_trigger_collect(half))
13015 .unwrap_or(false);
13016 if would_collect {
13017 self.busy = true;
13018 Some(AppEvent::DoScrollHalfDown)
13019 } else {
13020 if let Some(ref mut s) = self.data_table_state {
13021 s.half_page_down();
13022 }
13023 None
13024 }
13025 }
13026 KeyCode::Char('u')
13027 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
13028 {
13029 let half = self
13030 .data_table_state
13031 .as_ref()
13032 .map(|s| (s.visible_rows / 2).max(1) as i64)
13033 .unwrap_or(1);
13034 let would_collect = self
13035 .data_table_state
13036 .as_ref()
13037 .map(|s| s.scroll_would_trigger_collect(-half))
13038 .unwrap_or(false);
13039 if would_collect {
13040 self.busy = true;
13041 Some(AppEvent::DoScrollHalfUp)
13042 } else {
13043 if let Some(ref mut s) = self.data_table_state {
13044 s.half_page_up();
13045 }
13046 None
13047 }
13048 }
13049 KeyCode::PageUp if event.is_press() => {
13050 let would_collect = self
13051 .data_table_state
13052 .as_ref()
13053 .map(|s| s.scroll_would_trigger_collect(-(s.visible_rows as i64)))
13054 .unwrap_or(false);
13055 if would_collect {
13056 self.busy = true;
13057 Some(AppEvent::DoScrollUp)
13058 } else {
13059 if let Some(ref mut s) = self.data_table_state {
13060 s.page_up();
13061 }
13062 None
13063 }
13064 }
13065 KeyCode::Enter if event.is_press() => {
13066 if self.input_mode != InputMode::Normal {
13067 return None;
13068 }
13069 if self.enter_inspects() {
13071 self.open_inspector();
13072 return None;
13073 }
13074 self.drill_selected_row();
13075 None
13076 }
13077 KeyCode::Char('i') if event.is_press() => {
13078 if let Some(state) = self.data_table_state.as_mut() {
13079 let unseen = state.notes_unseen();
13083 state.mark_notes_seen();
13084 if unseen {
13085 self.info_modal
13086 .open_on(crate::widgets::info::InfoTab::Notes);
13087 } else if state.format_detail().is_some_and(|d| d.first) {
13088 self.info_modal
13092 .open_on(crate::widgets::info::InfoTab::Format);
13093 } else {
13094 self.info_modal.open();
13095 }
13096 if let Some(detail) = state.format_detail()
13098 && let Some(at) = detail.list.iter().position(|(key, _)| {
13099 detail.table.as_ref() == Some(key) && detail.tables.contains(key)
13100 })
13101 {
13102 self.info_modal.detail_selected = at;
13103 }
13104 self.input_mode = InputMode::Info;
13105 self.read_file_facts();
13106 self.count_unfit();
13107 }
13108 None
13109 }
13110 KeyCode::Char(':') if event.is_press() => {
13111 self.open_command_line();
13112 None
13113 }
13114 KeyCode::Char('V') => {
13115 if let Some(ref state) = self.data_table_state
13119 && let Some(dataset) = self.view_dataset()
13120 {
13121 match self
13122 .view_manager
13123 .get_most_relevant(dataset, state.source_schema())
13124 {
13125 Some((view, why)) => {
13126 if let Err(e) = self.apply_matched_view(&view, why) {
13127 self.error_modal.show(format!("Error applying view: {}", e));
13128 }
13129 }
13130 None => self.open_view_list(),
13131 }
13132 }
13133 None
13134 }
13135 KeyCode::Char('v') => {
13136 self.open_view_list();
13137 None
13138 }
13139 KeyCode::Char('S') => {
13140 if self.input_mode == InputMode::Normal {
13141 self.open_table_sample_form();
13142 }
13143 None
13144 }
13145 KeyCode::Char('s') => {
13146 if self.data_table_state.is_some() {
13147 self.sync_sort_filter_modal();
13151 let current = self
13152 .data_table_state
13153 .as_ref()
13154 .and_then(|state| state.current_column())
13155 .map(str::to_string);
13156 self.sort_filter_modal.open(
13157 self.history_limit,
13158 &self.theme,
13159 current.as_deref(),
13160 );
13161 self.input_mode = InputMode::SortFilter;
13162 }
13163 None
13164 }
13165 KeyCode::Char('r') => {
13166 if let Some(state) = &mut self.data_table_state {
13167 state.deferred(DataTableState::reverse);
13168 self.spawn_async_collect("Sorting...");
13169 }
13170 None
13171 }
13172 KeyCode::Char('a') => {
13173 if self.data_table_state.is_some()
13175 && self.input_mode == InputMode::Normal
13176 && self.quality_evidence_return.is_none()
13177 {
13178 let view = self.data_table_state.as_ref().map(|s| s.len_generation());
13180 self.analysis_modal.open(view);
13181 if self.analysis_modal.sample_dataset != Some(self.dataset_generation) {
13184 self.analysis_modal.sample.scope = data_quality::QualityScope::CurrentView;
13185 self.analysis_modal.sample_dataset = Some(self.dataset_generation);
13186 }
13187 let sampled = self
13189 .data_table_state
13190 .as_ref()
13191 .is_some_and(|state| state.sampled().is_some());
13192 self.analysis_modal.follow_view_sample(sampled);
13193 self.sync_quality_plan();
13194 }
13195 None
13196 }
13197 KeyCode::Char('c') => {
13198 if let Some(state) = &self.data_table_state
13199 && self.input_mode == InputMode::Normal
13200 {
13201 let numeric_columns: Vec<String> = state
13202 .schema()
13203 .iter()
13204 .filter(|(_, dtype)| dtype.is_numeric())
13205 .map(|(name, _)| name.to_string())
13206 .collect();
13207 let datetime_columns: Vec<String> = state
13208 .schema()
13209 .iter()
13210 .filter(|(_, dtype)| {
13211 matches!(
13212 dtype,
13213 DataType::Datetime(_, _) | DataType::Date | DataType::Time
13214 )
13215 })
13216 .map(|(name, _)| name.to_string())
13217 .collect();
13218 let category_columns: Vec<String> = state
13219 .schema()
13220 .iter()
13221 .filter(|(_, dtype)| chart_data::is_category_dtype(dtype))
13222 .map(|(name, _)| name.to_string())
13223 .collect();
13224 let cursor = state.current_column().and_then(|name| {
13226 let dtype = state.schema().get(name)?.clone();
13227 Some((name.to_string(), dtype))
13228 });
13229 let bucketable_columns: Vec<String> = state
13231 .schema()
13232 .iter()
13233 .filter(|(_, dtype)| {
13234 matches!(dtype, DataType::Datetime(_, _) | DataType::Date)
13235 })
13236 .map(|(name, _)| name.to_string())
13237 .collect();
13238 self.chart_modal.series_cap = Some(self.theme.series_colors().len());
13239 self.chart_modal.row_order = self.view_state().sort;
13240 let sampled = state.sampled().is_some();
13241 self.chart_modal.open(
13242 ChartColumns {
13243 numeric: &numeric_columns,
13244 datetime: &datetime_columns,
13245 bucketable: &bucketable_columns,
13246 category: &category_columns,
13247 },
13248 cursor.as_ref().map(|(name, dtype)| (name.as_str(), dtype)),
13249 Some(self.app_config.analysis.chart_rows),
13250 self.app_config.analysis.chart_grid,
13251 self.dataset_generation,
13252 );
13253 if sampled {
13255 self.chart_modal.row_limit = None;
13256 } else if self.chart_modal.view_sampled {
13257 self.chart_modal.row_limit = Some(self.chart_modal.sample_rows);
13258 }
13259 self.chart_modal.view_sampled = sampled;
13260 self.chart_cache.clear();
13261 self.input_mode = InputMode::Chart;
13262 }
13263 None
13264 }
13265 KeyCode::Char('p') => {
13266 if self.data_table_state.is_some() && self.input_mode == InputMode::Normal {
13267 self.open_pivot_builder();
13268 }
13269 None
13270 }
13271 KeyCode::Char('e') => {
13272 if self.data_table_state.is_some() && self.input_mode == InputMode::Normal {
13273 self.export_counts = None;
13274 self.export_modal.open(
13275 self.original_file_format,
13276 self.history_limit,
13277 &self.theme,
13278 self.original_file_delimiter,
13279 );
13280 let stem = self.dataset_stem();
13282 self.export_modal.suggest_path(&format!("{stem}-export"));
13283 if let Some(state) = self.data_table_state.as_ref() {
13284 self.export_modal.offer_source_file = state.can_name_source_files();
13285 self.export_modal.nested_columns = state
13286 .get_column_order()
13287 .iter()
13288 .filter_map(|name| state.schema().get(name))
13289 .any(crate::nested_json::is_nested);
13290 self.export_modal.avro_renames =
13291 state.get_column_order().iter().any(|name| {
13292 state
13293 .schema()
13294 .get(name)
13295 .is_some_and(|dtype| crate::avro_types::renames(name, dtype))
13296 });
13297 }
13298 self.input_mode = InputMode::Export;
13299 }
13300 None
13301 }
13302 KeyCode::Char(' ') if event.is_press() => {
13303 if self.input_mode == InputMode::Normal {
13304 self.open_inspector();
13305 }
13306 None
13307 }
13308 KeyCode::Char('g') if event.is_press() => {
13309 if self.input_mode == InputMode::Normal {
13310 self.open_go_to_column();
13311 }
13312 None
13313 }
13314 KeyCode::Char('b') if event.is_press() => {
13315 if self.input_mode == InputMode::Normal {
13316 self.open_format_picker();
13317 }
13318 None
13319 }
13320 KeyCode::Char('T') if event.is_press() => {
13321 if self.input_mode == InputMode::Normal {
13322 self.open_table_picker();
13323 }
13324 None
13325 }
13326 KeyCode::Char('y') => {
13327 if self.input_mode == InputMode::Normal
13328 && let Some(state) = self.data_table_state.as_ref()
13329 {
13330 let columns = state.get_column_order().to_vec();
13331 let context = copy_modal::CopyContext {
13332 row_number: state.selected_display_row().unwrap_or(0),
13333 view_rows: state.copy_view_df().map(|d| d.height()).unwrap_or(0),
13334 view_cols: columns.len(),
13335 total_rows: state.num_rows_if_valid(),
13336 };
13337 let current = state.current_column().map(str::to_string);
13338 self.copy_modal.open(columns, current.as_deref(), context);
13339 self.input_mode = InputMode::Copy;
13340 }
13341 None
13342 }
13343 _ => None,
13344 }
13345 }
13346
13347 pub fn handle(&mut self, event: &AppEvent) -> EventOutcome {
13352 if let AppEvent::Press(key) = event {
13354 return self.handle(&AppEvent::Key(*key));
13355 }
13356 if let AppEvent::Key(key) = event
13357 && self.is_busy()
13358 && !self.key_acts_while_busy(key)
13359 {
13360 return Err(*key);
13361 }
13362 let out = self.dispatch_event(event);
13363 if out.is_none() {
13370 self.let_waiting_errands_in();
13371 }
13372 self.ensure_chart_data();
13373 self.home_score_search();
13374 self.refresh_stale_live_matches();
13376 Ok(out)
13377 }
13378
13379 pub(crate) fn let_waiting_errands_in(&mut self) {
13382 if self.join_held_footers() {
13386 self.reread_after_the_footers_joined();
13387 }
13388 self.join_followed_fields();
13389 self.describe_ended_journal();
13390 self.reread_when_the_work_allows();
13393 self.collect_when_the_work_allows();
13394 }
13395
13396 pub fn event(&mut self, event: &AppEvent) -> Option<AppEvent> {
13397 self.handle(event).unwrap_or(None)
13398 }
13399
13400 pub fn chart_preparing(&self) -> bool {
13405 match self.chart_inflight.as_ref() {
13406 Some(inflight) if !inflight.stale => true,
13407 Some(_) => self.chart_request_pending(),
13408 None => self.chart_settling(),
13409 }
13410 }
13411
13412 fn chart_settling(&self) -> bool {
13415 self.chart_asked
13416 .as_ref()
13417 .and_then(|(_, until)| *until)
13418 .is_some_and(|until| std::time::Instant::now() < until)
13419 }
13420
13421 fn chart_request_pending(&self) -> bool {
13424 if self.input_mode != InputMode::Chart || !self.chart_modal.active {
13425 return false;
13426 }
13427 ChartRequest::from_modal(&self.chart_modal)
13428 .is_some_and(|request| self.chart_cache.get(&request).is_none())
13429 }
13430
13431 fn reset_chart_state(&mut self) {
13439 self.chart_cache.clear();
13440 self.chart_asked = None;
13441 if let Some(inflight) = self.chart_inflight.as_mut() {
13442 inflight.stale = true;
13443 inflight
13444 .cancel
13445 .store(true, std::sync::atomic::Ordering::Relaxed);
13446 }
13447 self.chart_export_modal.close();
13450 *self
13451 .pending_chart_result
13452 .lock()
13453 .unwrap_or_else(|e| e.into_inner()) = None;
13454 let writing = self
13455 .jobs
13456 .supersede(|job| matches!(job, Job::ChartExport { .. }));
13457 let waiting = self.chart_export_waiting.take().is_some();
13458 if writing || waiting {
13459 self.export_progress = None;
13460 self.status_message = None;
13461 self.busy = false;
13462 }
13463 }
13464
13465 pub(crate) fn chart_status(&self) -> String {
13468 let aggregating = self
13469 .chart_inflight
13470 .as_ref()
13471 .filter(|i| !i.stale)
13472 .map(|i| i.request.aggregates())
13473 .or_else(|| ChartRequest::from_modal(&self.chart_modal).map(|r| r.aggregates()))
13474 .unwrap_or(false);
13475 if !aggregating {
13476 return "Preparing chart...".to_string();
13477 }
13478 match self
13479 .data_table_state
13480 .as_ref()
13481 .and_then(|s| s.num_rows_if_valid())
13482 {
13483 Some(rows) => format!("Grouping {} rows...", crate::discover::format_rows(rows)),
13484 None => "Grouping every row...".to_string(),
13485 }
13486 }
13487
13488 pub fn chart_names(&self) -> Option<Vec<String>> {
13491 let request = ChartRequest::from_modal(&self.chart_modal)?;
13492 match self.chart_cache.prepared(&request)? {
13493 ChartPrepared::XY(xy) => Some(xy.names.clone()),
13494 _ => None,
13495 }
13496 }
13497
13498 pub fn chart_data_ready(&self) -> bool {
13500 ChartRequest::from_modal(&self.chart_modal).is_some_and(|r| self.chart_cache.satisfies(&r))
13501 }
13502
13503 fn ensure_chart_data(&mut self) {
13508 const CHART_AGGREGATE_SETTLE: std::time::Duration = std::time::Duration::from_millis(150);
13509 if self.input_mode != InputMode::Chart || !self.chart_modal.active {
13510 return;
13511 }
13512 self.chart_modal.view_rows = self
13514 .data_table_state
13515 .as_ref()
13516 .and_then(|state| state.num_rows_if_valid());
13517 let request = ChartRequest::from_modal(&self.chart_modal);
13518 if let Some(inflight) = self.chart_inflight.as_ref()
13519 && !request
13520 .as_ref()
13521 .is_some_and(|r| r.reads_as(&inflight.request))
13522 {
13523 inflight
13526 .cancel
13527 .store(true, std::sync::atomic::Ordering::Relaxed);
13528 }
13529 let Some(request) = request else {
13530 return;
13531 };
13532 let settle = match self.chart_asked.take() {
13536 Some((asked, until)) if asked == request => until,
13537 Some((asked, _)) if request.steps_aggregate_from(&asked) => {
13538 let tx = self.events.clone();
13539 std::thread::spawn(move || {
13540 std::thread::sleep(CHART_AGGREGATE_SETTLE);
13541 let _ = tx.send(AppEvent::Wake);
13542 });
13543 Some(std::time::Instant::now() + CHART_AGGREGATE_SETTLE)
13544 }
13545 _ => None,
13546 };
13547 self.chart_asked = Some((request.clone(), settle));
13548 if self.chart_cache.get(&request).is_some() {
13549 self.chart_cache.touch(&request, self.chart_modal.log_scale);
13550 if let Some(colors) = request
13552 .spec
13553 .encoding
13554 .color
13555 .field
13556 .as_deref()
13557 .and_then(|c| self.chart_cache.colors(c))
13558 {
13559 self.chart_modal.color_counts = Some(colors.clone());
13560 }
13561 return;
13562 }
13563 if self.chart_inflight.is_some() || self.chart_settling() {
13564 return;
13565 }
13566 let Some(state) = self.data_table_state.as_ref() else {
13567 return;
13568 };
13569 let lf = if request.sorted {
13573 state.lf().clone()
13574 } else {
13575 state.analysis_lf()
13576 };
13577 let schema = state.schema().clone();
13578 let dataset = Some(state.len_generation());
13579 let sampling = chart_data::ChartSampling {
13580 limit: request.row_limit,
13582 known_total: state.num_rows_if_valid(),
13583 seed: self.analysis_modal.sample.seed,
13584 streaming: self.app_config.performance.streaming,
13585 full_passes: !state.is_remote_source(),
13586 held: self.chart_cache.held_rows(dataset),
13587 cancel: Arc::default(),
13588 };
13589 self.chart_inflight = Some(ChartInflight {
13590 dataset,
13591 request: request.clone(),
13592 stale: false,
13593 cancel: Arc::clone(&sampling.cancel),
13594 });
13595 let slot = self.pending_chart_result.clone();
13596 let tx = self.events.clone();
13597 self.runtime.spawn_blocking(move || {
13598 let result = logging::catch_panic(|| request.prepare(&lf, &schema, &sampling))
13602 .unwrap_or_else(|_| Err(color_eyre::eyre::eyre!("Chart preparation panicked")))
13603 .map_err(|e| crate::error_display::user_message_from_report(&e, None));
13604 *slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(result);
13605 let _ = tx.send(AppEvent::BackgroundChartReady);
13606 });
13607 }
13608
13609 fn dispatch_event(&mut self, event: &AppEvent) -> Option<AppEvent> {
13610 self.debug.num_events += 1;
13611
13612 match event {
13613 AppEvent::Key(key) => {
13614 if let Some(leaving) = self.leaves(key)
13616 && !self.confirmation_modal.active
13617 && self.recording().is_some_and(|spool| spool.live())
13618 {
13619 self.ask_about_recording(leaving);
13620 return None;
13621 }
13622 self.key(key)
13623 }
13624 AppEvent::Open(paths, options) => {
13625 if paths.is_empty() {
13626 return Some(AppEvent::Crash("No paths provided".to_string()));
13627 }
13628 if self.input_mode == InputMode::Home {
13631 self.opened_from_home = true;
13632 }
13633 #[cfg(feature = "cloud")]
13636 let expanded = match paths
13637 .iter()
13638 .map(|p| {
13639 crate::cloud_sources::expand_azure_short_url(
13640 p,
13641 &self.app_config.cloud,
13642 self.home.browsing.as_deref(),
13643 )
13644 })
13645 .collect::<std::result::Result<Vec<_>, _>>()
13646 {
13647 Ok(expanded) => expanded,
13648 Err(message) => return Some(AppEvent::Crash(message)),
13649 };
13650 #[cfg(feature = "cloud")]
13651 if &expanded != paths {
13652 return Some(AppEvent::Open(expanded, options.clone()));
13653 }
13654 let mut request =
13657 loading::OpenRequest::named(paths.clone(), options.clone(), &self.formats);
13658 request.warn_in_memory_above = self.app_config.read.memory_warning();
13659 self.begin_new_dataset();
13660 let step = self.loading.open(request);
13661 self.run_load_step(step)
13662 }
13663 AppEvent::OpenLazyFrame(lf, options) => {
13664 self.begin_new_dataset();
13665 let step = self.loading.open_frame((**lf).clone(), options.clone());
13666 self.run_load_step(step)
13667 }
13668 AppEvent::HomeListingReady {
13669 generation,
13670 listing,
13671 known,
13672 visits,
13673 newest,
13674 folds,
13675 } => {
13676 if *generation == self.home_generation {
13680 self.home.listing_in_flight = false;
13681 }
13682 self.home.known = known.clone();
13686 self.home.visits = visits.clone();
13687 self.home.newest_recent = newest.clone();
13688 if let Some(folds) = folds {
13689 self.home.folds = folds.clone();
13690 }
13691 if *generation != self.home_generation {
13694 return None;
13695 }
13696 self.home.apply_listing((**listing).clone());
13697 self.spawn_home_probes();
13700 #[cfg(feature = "cloud")]
13701 self.spawn_cloud_discovery();
13702 self.request_home_measurements();
13703 self.request_home_classifications();
13704 None
13705 }
13706 AppEvent::HomeListingFailed => {
13707 self.home.listing_in_flight = false;
13709 None
13710 }
13711 AppEvent::HomeMeasured { measured, done } => {
13712 for (path, m) in measured {
13713 self.home.enriched.insert(path.clone(), m.clone());
13714 }
13715 self.home.apply_measurements();
13716 if *done {
13717 self.home.measure_in_flight = false;
13718 self.request_home_measurements();
13719 }
13720 None
13721 }
13722 AppEvent::HomeSized { path, measured } => {
13723 match self.home.enriched.get_mut(path) {
13725 Some(known) => known.size = measured.size,
13726 None => {
13727 self.home.enriched.insert(path.clone(), measured.clone());
13728 }
13729 }
13730 self.home.apply_measurements();
13731 None
13732 }
13733 AppEvent::HomeWebGone { path, gone } => {
13734 self.home.web_gone.insert(path.clone(), gone.clone());
13735 None
13736 }
13737 AppEvent::HomeClassified { measured, done } => {
13738 for (path, m) in measured {
13743 self.home.enriched.insert(path.clone(), m.clone());
13744 }
13745 self.home.apply_measurements();
13751 if *done {
13755 self.home.classify_in_flight = false;
13756 self.request_home_classifications();
13757 }
13758 None
13759 }
13760 AppEvent::HomePathListed { listing } => {
13761 if self.home.path_input_active
13764 && home::typed_dir(&self.home.path_input) == listing.dir
13765 {
13766 self.home.path_listing = Some((**listing).clone());
13767 if self.home.path_pick.is_none() {
13768 self.home.pick_first_path();
13769 }
13770 }
13771 None
13772 }
13773 AppEvent::HomePathCompleted {
13774 generation,
13775 typed,
13776 completed,
13777 candidates,
13778 } => {
13779 if *generation != self.home_generation || &self.home.path_input != typed {
13782 return None;
13783 }
13784 if *candidates == 0 {
13785 self.home.status = Some("No such path".to_string());
13786 } else {
13787 self.home.status = None;
13788 if *candidates > 1 {
13789 self.flash_note(format!("{candidates} matches"));
13790 }
13791 self.home.path_input = completed.clone();
13792 self.home.pick_first_path();
13793 }
13794 None
13795 }
13796 AppEvent::HomePreviewReady {
13797 path,
13798 stamp,
13799 read_at,
13800 rows,
13801 prepared,
13802 } => {
13803 let prepared = prepared.lock().ok().and_then(|mut p| p.take());
13804 if let Some(prepared) = &prepared {
13806 let schema = prepared
13807 .state
13808 .schema()
13809 .iter()
13810 .map(|(name, dtype)| (name.to_string(), dtype.clone()))
13811 .collect();
13812 self.home_schema_cache.insert(path.clone(), Some(schema));
13813 }
13814 let prepared = prepared.filter(|_| read_at.is_some());
13815 self.home_previews.landed(
13816 path.clone(),
13817 *stamp,
13818 read_at.unwrap_or(*stamp),
13819 rows.clone(),
13820 prepared,
13821 );
13822 None
13823 }
13824 AppEvent::HomeSchemaReady {
13825 generation,
13826 path,
13827 preview,
13828 } => {
13829 self.home_schema_inflight.retain(|p| p != path);
13830 let known = self
13832 .home_schema_cache
13833 .get(path)
13834 .is_some_and(Option::is_some);
13835 if *generation == self.home_generation && (preview.is_some() || !known) {
13836 self.home_schema_cache.insert(path.clone(), preview.clone());
13837 }
13838 None
13839 }
13840 AppEvent::HomeSearchBatch {
13841 generation,
13842 root,
13843 found,
13844 scanned,
13845 } => {
13846 if *generation == self.home_search_generation {
13851 self.home.search_batch(root, found.clone(), *scanned);
13852 }
13853 None
13854 }
13855 AppEvent::HomeSearchScored { epoch, matches } => {
13856 if let Some(matches) = matches {
13859 self.home.search_scored(*epoch, (**matches).clone());
13860 }
13861 None
13862 }
13863 AppEvent::HomeSearchDone {
13864 generation,
13865 root,
13866 scanned,
13867 limited,
13868 } => {
13869 if *generation == self.home_search_generation {
13870 self.home.search_finished(root, *scanned, limited.clone());
13871 }
13872 self.home_search_inflight = false;
13873 if !self.home.filter.is_empty() && self.home.search.root.is_none() {
13875 self.spawn_home_search();
13876 }
13877 None
13878 }
13879 #[cfg(feature = "cloud")]
13880 AppEvent::HomeCloudSources { sources } => {
13881 self.home.cloud = sources.clone();
13882 self.home_refresh();
13883 None
13884 }
13885 #[cfg(feature = "cloud")]
13886 AppEvent::HomeCloudListed {
13887 id,
13888 buckets,
13889 details,
13890 failure,
13891 listed_at,
13892 } => {
13893 if let Some(source) = self.home.cloud.iter_mut().find(|s| &s.id == id) {
13894 source.refreshing = false;
13895 for (place, lines) in details {
13896 source.place_details.insert(place.clone(), lines.clone());
13897 }
13898 match failure {
13899 Some((short, detail)) => {
13902 for bucket in buckets {
13903 if !source.buckets.contains(bucket) {
13904 source.buckets.push(bucket.clone());
13905 }
13906 }
13907 source.status = home::CloudStatus::Failed {
13908 short: short.clone(),
13909 detail: detail.clone(),
13910 };
13911 }
13912 None => {
13913 source.buckets = buckets.clone();
13914 source.status = home::CloudStatus::Listed;
13915 source.listed_at = Some(*listed_at);
13916 }
13917 }
13918 }
13919 self.home_refresh();
13920 None
13921 }
13922 AppEvent::HomeNarrowed {
13923 dir,
13924 prefix,
13925 listed,
13926 } => {
13927 let asked = self
13930 .home_narrowing
13931 .as_ref()
13932 .is_some_and(|(d, p, _)| d == dir && p == prefix);
13933 if asked {
13934 self.home_narrowing = None;
13935 }
13936 let wanted = asked
13938 && self.home.browsing.as_ref() == Some(dir)
13939 && !self.home.filter.is_empty();
13940 if let (Some((rows, truncated)), true) = (listed, wanted) {
13941 self.home.narrowed = Some(home::Narrowed {
13942 dir: dir.clone(),
13943 prefix: prefix.clone(),
13944 rows: rows.clone(),
13945 truncated: *truncated,
13946 });
13947 self.home_refresh();
13948 }
13949 None
13950 }
13951 AppEvent::HomeProbeCancelled { root } => {
13952 self.home_probes_inflight.retain(|p| p != root);
13953 self.home_listing_cancels.remove(root);
13954 self.home.listing_so_far.remove(root);
13955 if self.home.browsing.as_ref() == Some(root) {
13957 self.home_refresh();
13958 }
13959 None
13960 }
13961 AppEvent::HomeProbeFailed { root, message } => {
13962 self.home_probes_inflight.retain(|p| p != root);
13963 self.home_listing_cancels.remove(root);
13964 self.home.probe_failed(root.clone());
13965 self.home.probe_errors.insert(root.clone(), message.clone());
13966 self.home_refresh();
13967 None
13968 }
13969 AppEvent::HomeProbeProgress { root, rows } => {
13970 if self.home_probes_inflight.contains(root) && !self.home.probed.contains_key(root)
13973 {
13974 self.home.listing_so_far.insert(root.clone(), rows.clone());
13975 self.home_refresh();
13976 }
13977 None
13978 }
13979 AppEvent::HomeProbeReady {
13980 root,
13981 rows,
13982 cut_short,
13983 } => {
13984 self.home_probes_inflight.retain(|p| p != root);
13990 self.home_listing_cancels.remove(root);
13991 let landed = rows.is_some();
13992 match rows {
13993 Some(rows) => self.home.probe_ready(root.clone(), rows.clone()),
13994 None => self.home.probe_failed(root.clone()),
13995 }
13996 if *cut_short {
13997 self.home.cut_short.insert(root.clone());
13998 }
13999 #[cfg(feature = "cloud")]
14001 if *cut_short && !self.home.filter.is_empty() {
14002 self.narrow_cloud_listing();
14003 }
14004 #[cfg(feature = "cloud")]
14007 if let Some((account, _, _)) = source::azure_parts(&root.to_string_lossy())
14008 && crate::azure::remembered_key(&account).is_some()
14009 {
14010 for source in &mut self.home.cloud {
14011 let place = source
14012 .buckets
14013 .iter()
14014 .find(|b| home::cloud_account(b).is_some_and(|(_, a)| a == account))
14015 .cloned();
14016 if let Some(place) = place {
14017 let lines = source.place_details.entry(place).or_default();
14018 if !lines.iter().any(|(k, _)| k == "access") {
14019 lines.push(("access".to_string(), "access key".to_string()));
14020 }
14021 }
14022 }
14023 }
14024 self.home_refresh();
14027 #[cfg(feature = "cloud")]
14036 if landed {
14037 self.peek_cloud_directories();
14038 }
14039 #[cfg(not(feature = "cloud"))]
14040 let _ = landed;
14041 None
14042 }
14043 AppEvent::HomeCloudKinds { kinds, failed } => {
14044 for directory in failed {
14045 self.home.peeking.remove(directory);
14046 self.home.peek_failed.insert(directory.clone());
14047 }
14048 let roots: Vec<PathBuf> = self.home.probed.keys().cloned().collect();
14049 for (directory, kind) in kinds {
14050 self.home.peeking.remove(directory);
14054 self.home
14055 .cloud_kinds
14056 .insert(directory.clone(), kind.clone());
14057 }
14058 for root in roots {
14059 self.home.apply_cloud_kinds(&root);
14060 }
14061 self.home_refresh();
14062 None
14063 }
14064 AppEvent::Resize(_cols, _rows) => {
14065 None
14068 }
14069 AppEvent::Collect => {
14070 self.spawn_async_collect(Self::LOADING_BUFFER);
14071 None
14072 }
14073 AppEvent::DoScrollDown => self.handle_scroll(|s| s.page_down()),
14074 AppEvent::DoScrollUp => self.handle_scroll(|s| s.page_up()),
14075 AppEvent::DoScrollNext => self.handle_scroll(|s| s.select_next()),
14076 AppEvent::DoScrollPrev => self.handle_scroll(|s| s.select_previous()),
14077 AppEvent::DoScrollEnd => self.handle_scroll(|s| s.scroll_to_end()),
14078 AppEvent::DoScrollHome => self.handle_scroll(|s| s.scroll_to_start()),
14079 AppEvent::DoScrollHalfDown => self.handle_scroll(|s| s.half_page_down()),
14080 AppEvent::DoScrollHalfUp => self.handle_scroll(|s| s.half_page_up()),
14081 AppEvent::GoToLine(n) => {
14082 let n = *n;
14083 if let Some(state) = self.data_table_state.as_ref()
14085 && state.indexing().is_some()
14086 && (n >= state.num_rows()
14087 || state.changes_rows()
14088 || !state.view_sort_columns().is_empty()
14089 || !state.view_sort_ascending())
14090 {
14091 self.goto_when_indexed = Some((self.dataset_generation, n));
14092 self.status_message = Some(Self::INDEXING_FOR_ROW.to_string());
14093 self.busy = false;
14094 return None;
14095 }
14096 self.handle_scroll(|s| s.scroll_to_row_centered(n))
14097 }
14098 AppEvent::AnalysisChunk => {
14099 let (source, known_total) = match &self.data_table_state {
14102 Some(state) => self.sample_source(state),
14103 None => {
14104 self.analysis_computation = None;
14105 self.analysis_modal.computing = None;
14106 self.busy = false;
14107 return None;
14108 }
14109 };
14110 let comp = self.analysis_computation.take()?;
14111 if comp.df.is_none() {
14112 let sample = self.analysis_modal.sample.clone();
14113 let streaming = self.app_config.performance.streaming;
14114 self.spawn_job(
14115 Job::Analysis(jobs::AnalysisRun::default()),
14116 Some("Running analysis..."),
14117 move |_| {
14118 let results = source
14119 .cut(&sample.scope)
14120 .and_then(|lf| {
14121 crate::statistics::compute_describe_from_lazy(
14122 &lf,
14123 known_total,
14124 &sample,
14125 streaming,
14126 )
14127 })
14128 .map_err(|e| format!("{e}"))?;
14129 Ok(Answer::Described(results))
14130 },
14131 );
14132 }
14133 None
14134 }
14135 AppEvent::AnalysisDistributionCompute => {
14136 if let Some(state) = &self.data_table_state {
14137 let (source, known_total) = self.sample_source(state);
14138 let sample = self.analysis_modal.sample.clone();
14139 let streaming = self.app_config.performance.streaming;
14140 self.spawn_job(
14141 Job::Analysis(jobs::AnalysisRun::default()),
14142 Some("Analyzing distributions..."),
14143 move |_| {
14144 let options = crate::statistics::ComputeOptions {
14145 include_distribution_info: true,
14146 include_distribution_analyses: true,
14147 include_correlation_matrix: false,
14148 include_skewness_kurtosis_outliers: true,
14149 polars_streaming: streaming,
14150 };
14151 let results = source
14152 .cut(&sample.scope)
14153 .and_then(|lf| {
14154 crate::statistics::compute_statistics_for_sample(
14155 &lf,
14156 &sample,
14157 known_total,
14158 options,
14159 )
14160 })
14161 .map_err(|e| format!("{e}"))?;
14162 Ok(Answer::Distributions(results))
14163 },
14164 );
14165 } else {
14166 self.analysis_modal.computing = None;
14167 self.busy = false;
14168 }
14169 None
14170 }
14171 AppEvent::AnalysisCorrelationCompute => {
14172 if let Some(state) = &self.data_table_state {
14173 let (source, known_total) = self.sample_source(state);
14174 let streaming = state.polars_streaming();
14175 let sample = self.analysis_modal.sample.clone();
14176 let seed = sample.seed;
14177 self.spawn_job(
14178 Job::Analysis(jobs::AnalysisRun::default()),
14179 Some("Computing correlation matrix..."),
14180 move |_| {
14181 let result = source
14184 .cut(&sample.scope)
14185 .and_then(|lf| {
14186 let schema = lf.clone().collect_schema()?;
14187 let numeric: Vec<polars::prelude::Expr> = schema
14188 .iter()
14189 .filter(|(_, dtype)| dtype.is_numeric())
14190 .map(|(name, _)| col(name.clone()))
14191 .collect();
14192 crate::sampling::read(
14193 &lf.select(numeric),
14194 &sample,
14195 known_total,
14196 streaming,
14197 )
14198 })
14199 .map(|rows| {
14200 let matrix =
14201 crate::statistics::compute_correlation_matrix(&rows.df)
14202 .ok();
14203 crate::statistics::AnalysisResults {
14204 column_statistics: vec![],
14205 total_rows: rows.total_rows,
14206 sample_size: rows.sample_size,
14207 per_value: rows.per_value.map(|per_value| per_value.kept),
14208 sample_seed: seed,
14209 correlation_matrix: matrix,
14210 distribution_analyses: vec![],
14211 }
14212 });
14213 let results = result.map_err(|e| format!("{e}"))?;
14214 Ok(Answer::Correlations(results))
14215 },
14216 );
14217 } else {
14218 self.analysis_modal.computing = None;
14219 self.busy = false;
14220 }
14221 None
14222 }
14223 AppEvent::AnalysisDataQualityCompute => {
14224 if let Some(state) = &self.data_table_state {
14226 let plan = self.analysis_modal.data_quality_plan.clone();
14227 let source_scope = plan.scope.uses_source();
14228 let (lf, source, cached_rows) = if source_scope {
14229 let (lf, source) = state.data_quality_source_scan();
14230 (lf, source, None)
14231 } else {
14232 let ordered = matches!(
14233 plan.scope,
14234 data_quality::QualityScope::FirstRows(_)
14235 | data_quality::QualityScope::ViewRows { .. }
14236 );
14237 let (lf, source) = state.data_quality_scan(ordered);
14238 let rows = state.num_rows_if_valid().map(|rows| match &plan.scope {
14239 data_quality::QualityScope::CurrentView => rows,
14240 data_quality::QualityScope::FirstRows(limit) => rows.min(*limit),
14241 data_quality::QualityScope::ViewRows { start, end } => {
14242 rows.min(*end).saturating_sub(start.saturating_sub(1))
14243 }
14244 _ => unreachable!(),
14245 });
14246 (lf, source, rows)
14247 };
14248 let streaming = state.polars_streaming();
14249 let audio = (plan.compute == data_quality::QualityCompute::Full)
14251 .then(|| state.window_for_quality(&plan.scope))
14252 .flatten()
14253 .and_then(crate::audio::recording);
14254 let view_generation = state.len_generation();
14255 let dataset_generation = self.dataset_generation;
14256 let kept_entry = self.kept_quality_entry(&plan.sample());
14257 let kept = kept_entry.map(|kept| kept.rows.clone());
14258 let kept_source = kept_entry
14261 .filter(|_| plan.compute == data_quality::QualityCompute::Sample)
14262 .map(|kept| kept.source.clone());
14263 let mut identity = self.quality_source_identity(state, &plan.scope);
14264 let copy_job = match self.quality_copy_plan(&plan) {
14265 data_quality::CopyPlan::Kept { .. } => self
14266 .quality_copy_kept()
14267 .cloned()
14268 .map_or(QualityCopyJob::Source, QualityCopyJob::Kept),
14269 data_quality::CopyPlan::Fetch { .. } => match state.remote_objects() {
14270 Some(objects) => QualityCopyJob::Fetch {
14271 objects,
14272 root: self.quality_copies_root(),
14273 },
14274 None => QualityCopyJob::Source,
14275 },
14276 _ => QualityCopyJob::Source,
14277 };
14278 #[cfg(feature = "cloud")]
14279 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
14280 let mut source = source;
14283 if plan.compute == data_quality::QualityCompute::Full
14284 && let Some(source) = source.as_mut()
14285 {
14286 source.conflict_scan = state.quality_conflict_scan();
14287 }
14288 let started = self.start_job(
14292 Job::Analysis(jobs::AnalysisRun::default()),
14293 Some("Profiling data quality..."),
14294 );
14295 let ticket = started.ticket();
14296 let phases = self.events.clone();
14297 let watch = data_quality::QualityWatch::new(move |phase| {
14298 let _ = phases.send(AppEvent::JobProgress {
14299 ticket,
14300 progress: Progress::QualityPhase(phase),
14301 });
14302 });
14303 if let Some(progress) = self.analysis_modal.computing.as_mut() {
14304 progress.read = Some(watch.read().clone());
14305 }
14306 if let Some(Job::Analysis(run)) = self.jobs.job_mut(ticket) {
14307 run.watch = Some(watch.clone());
14308 }
14309 started.run(&self.runtime, move |worker| {
14310 match kept_source {
14312 Some(source) => identity = source,
14313 None => identity.stat(),
14314 }
14315 let lf = if source_scope {
14316 data_quality::prepare_source_quality_scan(lf, source.as_ref())
14317 .map_err(|error| format!("{error}"))?
14318 } else {
14319 lf
14320 };
14321 let lf =
14322 data_quality::apply_quality_scope(lf, &plan.scope, source.as_ref())
14323 .map_err(|error| format!("{error}"))?;
14324 let fetch = |objects: &[crate::local_copy::RemoteObject], root: &Path| {
14327 #[cfg(feature = "cloud")]
14328 {
14329 Self::fetch_quality_copy(
14330 objects,
14331 root,
14332 &cloud,
14333 &runtime,
14334 watch.read(),
14335 )
14336 }
14337 #[cfg(not(feature = "cloud"))]
14338 {
14339 let _ = (objects, root);
14340 Err(color_eyre::eyre::eyre!("Built without cloud support"))
14341 }
14342 };
14343 let kept_copy = |copy: Option<Arc<crate::local_copy::LocalCopy>>| {
14344 worker.send(AppEvent::BackgroundQualityCopyKept {
14345 dataset_generation,
14346 copy,
14347 });
14348 };
14349 let (lf, held) =
14350 Self::quality_scope_on_copy(lf, copy_job, &watch, fetch, kept_copy)
14351 .map_err(|error| format!("{error}"))?;
14352 let (results, rows) = crate::data_quality::compute_data_quality_watched(
14353 &lf,
14354 cached_rows,
14355 &plan,
14356 source.as_ref(),
14357 streaming,
14358 kept.as_deref(),
14359 &watch,
14360 );
14361 let results = match (results, audio) {
14362 (Ok(mut results), Some(audio)) => {
14363 crate::data_quality::add_signal_observations(
14364 &mut results,
14365 &audio,
14366 &watch,
14367 )
14368 .map(|()| results)
14369 }
14370 (results, _) => results,
14371 };
14372 drop(held);
14375 let kept = rows.map(|rows| KeptQualitySample {
14376 dataset_generation,
14377 view_generation,
14378 sample: plan.sample(),
14379 rows: std::sync::Arc::new(rows),
14380 source: identity.clone(),
14381 });
14382 match results {
14383 Ok(mut results) => {
14384 results.source = Some(Box::new(identity));
14385 Ok(Answer::DataQuality {
14386 results: Box::new(results),
14387 kept,
14388 plan: Box::new(plan),
14389 })
14390 }
14391 Err(error) => {
14392 if let Some(kept) = kept {
14394 worker.send(AppEvent::BackgroundQualitySampleKept { kept });
14395 }
14396 Err(format!("{error}"))
14397 }
14398 }
14399 });
14400 } else {
14401 self.analysis_modal.computing = None;
14402 self.busy = false;
14403 }
14404 None
14405 }
14406 AppEvent::BackgroundLenReady {
14407 len_generation,
14408 num_rows,
14409 file_row_groups,
14410 } => {
14411 if self.len_count_inflight == Some(*len_generation) {
14412 self.len_count_inflight = None;
14413 }
14414 if self.len_count_failed == Some(*len_generation) {
14415 self.len_count_failed = None;
14416 }
14417 if let Some(run) = self.query_running.as_mut() {
14419 run.rollback.count_landed(
14420 *len_generation,
14421 *num_rows,
14422 file_row_groups.as_deref(),
14423 );
14424 if run.len_count_inflight == Some(*len_generation) {
14425 run.len_count_inflight = None;
14426 }
14427 }
14428 if let Some(state) = self.data_table_state.as_mut()
14433 && state.count_landed(*len_generation, *num_rows, file_row_groups.as_deref())
14434 {
14435 if self.end_after_count == Some(*len_generation) {
14437 self.end_after_count = None;
14438 self.status_message = None;
14439 return self.jump_key(AppEvent::DoScrollEnd);
14440 }
14441 } else if self.end_after_count == Some(*len_generation) {
14442 self.retire_the_end_that_was_waiting();
14457 }
14458 self.remember_a_downloads_shape();
14459 None
14460 }
14461 AppEvent::FramePainted => {
14462 self.frame_painted();
14463 None
14464 }
14465 AppEvent::BackgroundLenFailed { len_generation } => {
14466 if self.len_count_inflight == Some(*len_generation) {
14467 self.len_count_inflight = None;
14468 }
14469 if self.count_after_stop.take() == Some(*len_generation) {
14470 self.count_exactly();
14471 return None;
14472 }
14473 if let Some(run) = self.query_running.as_mut()
14474 && run.len_count_inflight == Some(*len_generation)
14475 {
14476 run.len_count_inflight = None;
14477 run.len_count_failed = Some(*len_generation);
14478 }
14479 if self
14493 .data_table_state
14494 .as_ref()
14495 .is_some_and(|state| state.len_generation() == *len_generation)
14496 {
14497 self.len_count_failed = Some(*len_generation);
14498 }
14499 if self.end_after_count == Some(*len_generation) {
14504 self.end_after_count = None;
14505 if self
14506 .data_table_state
14507 .as_ref()
14508 .is_some_and(|state| state.len_generation() == *len_generation)
14509 {
14510 self.status_message =
14511 Some("Could not count the rows to find the end".to_string());
14512 } else {
14513 self.take_down_the_counting_status();
14517 }
14518 }
14519 None
14520 }
14521 AppEvent::LinesIndexed { generation, rows } => {
14522 self.lines_indexed(*generation, *rows);
14523 None
14524 }
14525 AppEvent::BackgroundFootersJoined { .. } => {
14526 let taken = self
14534 .pending_footers_result
14535 .lock()
14536 .unwrap_or_else(|e| e.into_inner())
14537 .take();
14538 if let Some((slot_generation, found)) = taken
14543 && slot_generation == self.dataset_generation
14544 {
14545 let Some(found) = found else {
14546 if let Some(state) = self.data_table_state.as_mut() {
14552 state.give_up_on_pending_footers();
14553 }
14554 self.reread_owed = Some(slot_generation);
14561 self.reread_when_the_work_allows();
14562 return None;
14563 };
14564 self.footers_held = Some((slot_generation, found));
14565 if self.join_held_footers() {
14566 self.reread_after_the_footers_joined();
14567 }
14568 }
14569 None
14570 }
14571 AppEvent::BackgroundQualitySampleKept { kept } => {
14572 self.retain_quality_sample(kept);
14573 None
14574 }
14575 AppEvent::BackgroundQualityCopyKept {
14576 dataset_generation,
14577 copy,
14578 } => {
14579 self.retain_quality_copy(*dataset_generation, copy.clone());
14580 None
14581 }
14582 AppEvent::OpenNamed(paths, options) => {
14583 if let Some(event) = Self::route_named_without_looking(paths, options) {
14584 return Some(event);
14585 }
14586 let (paths, options) = (paths.clone(), options.clone());
14587 let formats = self.formats.clone();
14588 self.make_way_for_an_open();
14591 let load = self.loading.look_at_paths();
14592 self.spawn_job(Job::OpenNamed(load), Some("Scanning input..."), move |_| {
14593 if let Some(missing) = Self::missing_named_path(&paths, &formats) {
14594 return Ok(Answer::NamedPathMissing(missing));
14595 }
14596 let (paths, options, directory) =
14597 match Self::route_named_paths_with(paths, options, &formats) {
14598 AppEvent::LookThenOpenDirectory(dir, options) => {
14599 (Vec::new(), options, Some(dir))
14600 }
14601 AppEvent::Open(paths, options) => (paths, options, None),
14602 _ => unreachable!("a named path is opened or looked at"),
14603 };
14604 Ok(Answer::NamedPaths {
14605 paths,
14606 options: Box::new(options),
14607 directory,
14608 })
14609 });
14610 None
14611 }
14612 AppEvent::LookThenOpenDirectory(dir, options) => {
14613 let looking = dir.clone();
14618 let options = options.clone();
14619 self.make_way_for_an_open();
14620 let load = self.loading.look_at_directory(looking.clone());
14621 self.jobs
14623 .supersede(|job| matches!(job, Job::LookAtDirectory { .. }));
14624 #[cfg(feature = "cloud")]
14635 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
14636 let job = Job::LookAtDirectory {
14637 load,
14638 path: looking.clone(),
14639 };
14640 self.spawn_job(job, Some(Self::LOOKING_AT_A_DIRECTORY), move |_| {
14641 #[cfg(feature = "cloud")]
14642 if home::is_object_store_url(&looking) {
14643 let url = looking.to_string_lossy().into_owned();
14644 let peeked = wait_on_runtime(&runtime, async move {
14645 crate::cloud_browse::peek_kind(&url, &cloud).await
14646 })
14647 .and_then(Result::ok);
14648 let (kind, holds) = match peeked {
14649 Some((kind, holds)) => (kind, Some(Box::new(holds))),
14650 None => (discover::EntryKind::Unknown, None),
14651 };
14652 return Ok(Answer::LookedAt {
14653 kind,
14654 holds,
14655 options: Box::new(options),
14656 });
14657 }
14658 let as_read = Self::read_as(&options);
14667 let looked = logging::catch_panic(|| {
14668 let mut entry = discover::Entry::directory(&looking);
14669 entry.kind = discover::EntryKind::Unknown;
14670 home::look_into_as(&entry, &as_read)
14671 });
14672 let kind = match looked {
14673 Ok(entry) => entry.kind,
14674 Err(_) => discover::EntryKind::Directory,
14675 };
14676 Ok(Answer::LookedAt {
14677 kind,
14678 holds: None,
14679 options: Box::new(options),
14680 })
14681 });
14682 None
14683 }
14684 AppEvent::ClassifyThenOpen { path, jump } => {
14685 let looking = path.clone();
14691 self.jobs.supersede(|job| matches!(job, Job::Classify(_)));
14692 let look = Job::Classify(jobs::Classify {
14693 path: looking.clone(),
14694 browsing: self.home.browsing.clone(),
14695 jump: *jump,
14696 });
14697 let name = looking
14698 .file_name()
14699 .map(|n| n.to_string_lossy().into_owned())
14700 .unwrap_or_else(|| looking.display().to_string());
14701 self.home.status = Some(format!("Looking at {name}..."));
14703 self.spawn_job(look, Some(Self::LOOKING), move |_| {
14704 let found = if !looking.exists() {
14707 None
14708 } else if looking.is_dir() {
14709 Some(crate::discover::classify_directory(&looking))
14710 } else {
14711 Some(crate::discover::EntryKind::File)
14712 };
14713 Ok(Answer::Kind(found))
14714 });
14715 None
14716 }
14717 AppEvent::JobEnded(ticket) => self.job_ended(*ticket),
14718 AppEvent::JobProgress { ticket, progress } => {
14719 self.job_progress(*ticket, progress);
14720 None
14721 }
14722 AppEvent::QQuery(query) => {
14723 self.run_query(QueryMode::Q, query, "Applying query...");
14724 None
14725 }
14726 AppEvent::SqlQuery(sql) => {
14727 self.run_query(QueryMode::Sql, sql, "Applying SQL query...");
14728 None
14729 }
14730 AppEvent::Filter(statements) => {
14731 if let Some(state) = &mut self.data_table_state {
14732 state.deferred(|s| s.filter(statements.clone()));
14733 }
14734 self.spawn_async_collect("Filtering...");
14735 None
14736 }
14737 AppEvent::Sort(columns, descending) => {
14738 if let Some(state) = &mut self.data_table_state {
14739 state.deferred(|s| s.sort_by(columns.clone(), descending.clone()));
14740 }
14741 self.spawn_async_collect("Sorting...");
14742 None
14743 }
14744 AppEvent::Reset => {
14745 if self
14747 .data_table_state
14748 .as_ref()
14749 .is_some_and(|state| state.sampled().is_some())
14750 {
14751 self.put_down_sample_draw();
14752 if let Some(state) = self.data_table_state.take() {
14753 self.data_table_state = Some(state.into_unsampled());
14754 }
14755 self.sample_changed();
14756 }
14757 if let Some(state) = &mut self.data_table_state {
14758 state.deferred(|s| s.reset());
14759 }
14760 self.spawn_async_collect(Self::LOADING_BUFFER);
14761 self.active_view_id = None;
14763 None
14764 }
14765 AppEvent::ColumnOrder(order, locked_count) => {
14766 if let Some(state) = &mut self.data_table_state {
14767 state.deferred(|s| {
14768 s.set_column_order(order.clone());
14769 s.set_locked_columns(*locked_count);
14770 });
14771 self.spawn_async_collect(Self::LOADING_BUFFER);
14772 }
14773 None
14774 }
14775 AppEvent::Pivot(spec) => {
14776 let job = self.data_table_state.as_ref()?.plan_pivot(spec);
14779 let spec = spec.clone();
14780 self.spawn_job(Job::Pivot, Some(Self::COMPUTING_PIVOT), move |_| {
14781 let pivoted = job
14782 .run()
14783 .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
14784 Ok(Answer::Pivoted { spec, pivoted })
14785 });
14786 None
14787 }
14788 AppEvent::Melt(spec) => {
14789 self.busy = true;
14790 if let Some(state) = &mut self.data_table_state {
14791 let result = state.deferred(|s| s.melt(spec));
14792 match result {
14793 Ok(()) => {
14794 self.pivot_melt_modal.close();
14795 self.input_mode = InputMode::Normal;
14796 self.spawn_async_collect("Computing melt...");
14797 None
14798 }
14799 Err(e) => {
14800 self.busy = false;
14801 self.error_modal
14802 .show(crate::error_display::user_message_from_report(&e, None));
14803 None
14804 }
14805 }
14806 } else {
14807 self.busy = false;
14808 None
14809 }
14810 }
14811 AppEvent::QualityReportExport(path, format, overwrite) => {
14812 let results = self.analysis_modal.data_quality_results.clone()?;
14815 let plan = self.analysis_modal.quality_result_plan().clone();
14816 let (path, format, overwrite) = (path.clone(), *format, *overwrite);
14817 self.spawn_job(
14818 Job::QualityReport,
14819 Some("Writing the report..."),
14820 move |_| {
14821 crate::quality_export::write(&path, &results, &plan, format, overwrite)
14822 .map_err(|error| Self::format_export_error(&error))?;
14823 Ok(Answer::QualityReportWritten(path))
14824 },
14825 );
14826 None
14827 }
14828 AppEvent::ChartExport(request) => {
14829 self.busy = true;
14830 self.export_progress = Some(ExportProgress {
14831 file_path: request.path.clone(),
14832 current_phase: "Exporting chart".to_string(),
14833 written: None,
14834 });
14835 Some(AppEvent::DoChartExport(request.clone()))
14836 }
14837 AppEvent::DoChartExport(request) => {
14838 if self.input_mode != InputMode::Chart || !self.chart_modal.active {
14843 self.export_progress = None;
14844 self.status_message = None;
14845 self.busy = false;
14846 return None;
14847 }
14848 self.start_chart_export(request.clone());
14849 None
14850 }
14851 AppEvent::BackgroundChartReady => {
14852 let inflight = self.chart_inflight.take()?;
14858 let outcome = self
14859 .pending_chart_result
14860 .lock()
14861 .unwrap_or_else(|e| e.into_inner())
14862 .take()
14863 .unwrap_or_else(|| Err("Chart preparation produced no result".to_string()));
14864 if inflight.stale {
14865 return None;
14866 }
14867 if outcome.is_err() && inflight.cancel.load(std::sync::atomic::Ordering::Relaxed) {
14870 return None;
14871 }
14872 let dataset = self.data_table_state.as_ref().map(|s| s.len_generation());
14873 if dataset != inflight.dataset {
14874 return None;
14875 }
14876 let outcome = outcome.map(|(prepared, colors)| {
14877 if let Some(colors) = colors {
14878 self.chart_cache.hold_colors(colors.clone());
14879 self.chart_modal.color_counts = Some(colors);
14880 }
14881 prepared
14882 });
14883 self.chart_cache.insert(inflight.request, outcome);
14884 if let Some(request) = self.chart_export_waiting.take() {
14890 self.start_chart_export(request);
14891 }
14892 None
14893 }
14894 AppEvent::Export(request) => {
14895 if self.data_table_state.is_some() {
14896 self.busy = true;
14897 self.export_progress = Some(ExportProgress {
14898 file_path: request.path.clone(),
14899 current_phase: "Preparing export".to_string(),
14900 written: None,
14901 });
14902 Some(AppEvent::DoExport(request.clone()))
14904 } else {
14905 None
14906 }
14907 }
14908 AppEvent::Followed(news) => {
14909 self.followed(news);
14910 None
14911 }
14912 AppEvent::FollowedDetail {
14913 dataset_generation,
14914 detail,
14915 } => {
14916 if *dataset_generation == self.dataset_generation
14917 && let Some(state) = self.data_table_state.as_mut()
14918 {
14919 state.set_format_detail((**detail).clone());
14920 }
14921 None
14922 }
14923 AppEvent::DoExport(request) => {
14924 let Some(state) = &self.data_table_state else {
14925 self.export_progress = None;
14926 self.busy = false;
14927 return None;
14928 };
14929 let frame = match self.export_counts.clone() {
14931 Some(counts) => crate::widgets::datatable::ExportFrame::of(
14932 polars::prelude::IntoLazy::lazy(counts),
14933 ),
14934 None => state.export_frame(request.options.source_file),
14935 };
14936 let streaming = state.polars_streaming();
14937 let phase = match request.route(streaming) {
14940 crate::export::Route::Streamed => Self::export_write_phase(request),
14941 crate::export::Route::Collected => "Collecting data",
14942 };
14943 self.export_progress = Some(ExportProgress {
14944 file_path: request.path.clone(),
14945 current_phase: phase.to_string(),
14946 written: None,
14947 });
14948 let writing = Self::export_write_phase(request);
14949 let request = request.clone();
14950 self.spawn_job(Job::Export, Some("Exporting..."), move |worker| {
14951 let report = worker.reporter();
14952 let written = move |bytes| {
14953 report(Progress::ExportWriting {
14954 phase: writing,
14955 bytes,
14956 })
14957 };
14958 frame
14959 .into_lazy()
14960 .map_err(color_eyre::eyre::Report::from)
14961 .and_then(|lf| crate::export::run(lf, &request, streaming, written))
14962 .map_err(|e| Self::format_export_error(&e))?;
14963 Ok(Answer::Exported(request.path))
14965 });
14966 None
14967 }
14968 AppEvent::OpenLink(url) => {
14969 if link_open::open(url).is_err() {
14972 self.flash_note("Couldn't open the link; y copies it".to_string());
14973 }
14974 None
14975 }
14976 AppEvent::CopyTable { format, header } => {
14977 let accepts = match self.copy_destination() {
14978 Ok(destination) => destination.accepts(),
14979 Err(e) => {
14980 self.busy = false;
14981 self.error_modal.show(e);
14982 return None;
14983 }
14984 };
14985 if let Some(state) = &self.data_table_state {
14986 let lf = state.visible_lf();
14987 let streaming = state.polars_streaming();
14988 let (format, header) = (*format, *header);
14989 self.spawn_job(Job::Copy, Some("Collecting data for copy..."), move |_| {
14990 let (payload, rows) = match accepts.base64_limit {
14993 Some(limit) => crate::clipboard::bounded_table_text(
14994 lf, format, header, limit,
14995 )
14996 .map(|(text, rows)| (crate::clipboard::Payload::text(text), rows)),
14997 None => crate::statistics::collect_lazy(lf, streaming)
14998 .map_err(|e| crate::error_display::user_message_from_polars(&e))
14999 .and_then(|df| {
15000 crate::clipboard::tabular_payload(
15001 &df,
15002 format,
15003 header,
15004 accepts.html,
15005 )
15006 .map(|payload| (payload, df.height()))
15007 }),
15008 }
15009 .map_err(|message| format!("Copy failed: {message}"))?;
15010 Ok(Answer::Copied {
15012 payload,
15013 message: format!(
15014 "Copied {} rows as {}",
15015 copy_modal::thousands(rows),
15016 format.as_str()
15017 ),
15018 })
15019 });
15020 } else {
15021 self.busy = false;
15022 }
15023 None
15024 }
15025 AppEvent::TerminalBackground(mode) => {
15026 self.terminal_answered(*mode);
15027 None
15028 }
15029 AppEvent::TerminalFocused => {
15030 self.background_query |= self.app_config.theme.follow;
15031 None
15032 }
15033 _ => None,
15034 }
15035 }
15036
15037 fn axis_numbers(&self, column: &str) -> chart_data::AxisNumbers {
15039 let schema = self.data_table_state.as_ref().map(|s| s.schema().as_ref());
15040 chart_data::AxisNumbers::column(&self.number_format, schema, column)
15041 }
15042
15043 fn axes_numbers(&self, columns: &[String]) -> chart_data::AxisNumbers {
15045 let schema = self.data_table_state.as_ref().map(|s| s.schema().as_ref());
15046 chart_data::AxisNumbers::columns(&self.number_format, schema, columns)
15047 }
15048
15049 fn build_chart_figure(&self) -> Result<Option<chart_export::Figure>> {
15052 use chart_export::{Axis, Figure, Plot, Series};
15053 if self.data_table_state.is_none() {
15054 return Err(color_eyre::eyre::eyre!("No data loaded"));
15055 }
15056 let modal = &self.chart_modal;
15057 let Some(request) = ChartRequest::from_modal(modal).filter(|r| !r.x_only) else {
15058 return Err(color_eyre::eyre::eyre!(
15059 "Pick the columns the chart needs first"
15060 ));
15061 };
15062 let prepared = match self.chart_cache.get(&request) {
15063 Some(Ok(prepared)) => prepared,
15064 Some(Err(message)) => return Err(color_eyre::eyre::eyre!("{}", message)),
15067 None => return Ok(None),
15068 };
15069 let no_points = || color_eyre::eyre::eyre!("No valid data points to export");
15070 let spec = &request.spec;
15071 let x_name = spec.encoding.x.field.clone().unwrap_or_default();
15072 let ys = &spec.encoding.y.field;
15073 let title = |column: &str| modal.axis_title(column);
15074 let numbers_of = |column: &str| self.axis_numbers(column);
15075 let y_axis = || {
15076 use chart_modal::Aggregate;
15077 let aggregate = spec.encoding.y.aggregate;
15078 let numbers = match aggregate {
15079 Aggregate::Count | Aggregate::Distinct => {
15080 chart_data::AxisNumbers::count(&self.number_format)
15081 }
15082 a if a.is_fractional() => self.axes_numbers(ys).fractional(),
15083 _ => self.axes_numbers(ys),
15084 };
15085 let names = if aggregate == Aggregate::Count {
15086 "count".to_string()
15087 } else {
15088 ys.iter().map(|y| title(y)).collect::<Vec<_>>().join(", ")
15089 };
15090 let title = match aggregate {
15091 Aggregate::None | Aggregate::Count => names,
15092 _ => format!("{} {names}", spec.encoding.y.aggregate_name()),
15093 };
15094 Axis {
15095 title,
15096 numbers,
15097 log: modal.log_scale,
15098 ..Default::default()
15099 }
15100 };
15101 let plot = match prepared {
15102 ChartPrepared::XY(cache) => {
15103 let points = if modal.log_scale {
15104 cache
15105 .series_log
15106 .clone()
15107 .unwrap_or_else(|| log_series(&cache.series))
15108 } else {
15109 cache.series.clone()
15110 };
15111 let last = cache.names.len().saturating_sub(1);
15112 let series: Vec<Series> = points
15113 .into_iter()
15114 .zip(&cache.names)
15115 .zip(&cache.breaks)
15116 .enumerate()
15117 .filter(|(_, ((points, _), _))| !points.is_empty())
15118 .map(|(i, ((points, name), breaks))| Series {
15119 name: name.clone(),
15120 points,
15121 breaks: breaks.clone(),
15122 other: cache.other && i == last,
15123 })
15124 .collect();
15125 if series.is_empty() {
15126 return Err(no_points());
15127 }
15128 Plot::Lines {
15129 series,
15130 scatter: spec.mark == chart_modal::Mark::Scatter,
15131 x: Axis {
15132 title: title(&cache.x_column),
15133 numbers: numbers_of(&cache.x_column),
15134 kind: cache.x_axis_kind,
15135 log: false,
15136 },
15137 y: y_axis(),
15138 y_from_zero: modal.y_starts_at_zero,
15139 }
15140 }
15141 ChartPrepared::Histogram(data) => {
15142 if data.bins.is_empty() {
15143 return Err(no_points());
15144 }
15145 Plot::Histogram {
15146 data: data.clone(),
15147 x: Axis {
15148 title: title(&data.column),
15149 numbers: numbers_of(&data.column),
15150 ..Default::default()
15151 },
15152 y: Axis {
15153 title: if data.share { "share" } else { "count" }.to_string(),
15154 numbers: if data.share {
15155 chart_data::AxisNumbers::measure(&self.number_format, "Share")
15156 } else {
15157 chart_data::AxisNumbers::count(&self.number_format)
15158 },
15159 ..Default::default()
15160 },
15161 }
15162 }
15163 ChartPrepared::BoxPlot(data) => {
15164 if data.stats.is_empty() {
15165 return Err(no_points());
15166 }
15167 Plot::Box {
15168 data: data.clone(),
15169 x_title: x_name.clone(),
15170 y: Axis {
15171 title: ys.first().map(|y| title(y)).unwrap_or_default(),
15172 numbers: self.axes_numbers(ys),
15173 ..Default::default()
15174 },
15175 }
15176 }
15177 ChartPrepared::Kde(data) => {
15178 if data.series.is_empty() {
15179 return Err(no_points());
15180 }
15181 Plot::Kde {
15182 data: data.clone(),
15183 x: Axis {
15184 title: title(&x_name),
15185 numbers: numbers_of(&x_name).fractional(),
15186 ..Default::default()
15187 },
15188 y: Axis {
15189 title: "density".to_string(),
15190 numbers: chart_data::AxisNumbers::measure(&self.number_format, "Density"),
15191 ..Default::default()
15192 },
15193 }
15194 }
15195 ChartPrepared::Heatmap(data) => {
15196 if data.counts.is_empty() || data.max_count <= 0.0 {
15197 return Err(no_points());
15198 }
15199 Plot::Heatmap {
15200 data: data.clone(),
15201 x: Axis {
15202 title: title(&data.x_column),
15203 numbers: numbers_of(&data.x_column),
15204 ..Default::default()
15205 },
15206 y: Axis {
15207 title: title(&data.y_column),
15208 numbers: numbers_of(&data.y_column),
15209 ..Default::default()
15210 },
15211 }
15212 }
15213 ChartPrepared::Bar(data) => {
15214 if data.bars.is_empty() {
15215 return Err(no_points());
15216 }
15217 Plot::Bars {
15218 value: Axis {
15219 title: data.value_column.clone(),
15220 numbers: chart_data::AxisNumbers {
15221 format: data.value_format(&self.number_format),
15222 whole: data.value_dtype.is_integer(),
15223 },
15224 ..Default::default()
15225 },
15226 data: data.clone(),
15227 }
15228 }
15229 ChartPrepared::XRange(_) => return Err(no_points()),
15231 };
15232 Ok(Some(Figure {
15233 plot,
15234 chart_notes: self.chart_notes_of(prepared, "·"),
15236 grid: modal.grid,
15237 }))
15238 }
15239
15240 fn start_chart_export(&mut self, mut request: ChartExportRequest) {
15243 request.options.recipe = if request.recipe {
15246 self.chart_recipe()
15247 } else {
15248 None
15249 };
15250 match self.build_chart_figure() {
15251 Ok(Some(figure)) => {
15252 self.chart_export_waiting = None;
15253 let write = Job::ChartExport {
15254 path: request.path.clone(),
15255 format: request.format,
15256 };
15257 self.spawn_job(write, Some("Exporting chart..."), move |_| {
15258 let ChartExportRequest {
15259 path,
15260 format,
15261 options,
15262 overwrite,
15263 ..
15264 } = request;
15265 ChartExportJob { figure, options }
15266 .write(&path, format, overwrite)
15267 .map_err(|e| Self::format_export_error(&e))?;
15268 Ok(Answer::ChartExported)
15269 });
15270 }
15271 Ok(None) => self.chart_export_waiting = Some(request),
15273 Err(e) => {
15274 let message = Self::format_export_error(&e);
15275 self.finish_chart_export(&request.path, request.format, Err(message));
15276 }
15277 }
15278 }
15279
15280 fn finish_chart_export(
15281 &mut self,
15282 path: &Path,
15283 format: ChartExportFormat,
15284 result: Result<(), String>,
15285 ) {
15286 self.chart_export_waiting = None;
15287 self.export_progress = None;
15288 self.status_message = None;
15289 self.busy = false;
15290 match result {
15291 Ok(()) => {
15292 self.flash_path("Chart exported to ", path);
15293 self.chart_export_modal.close();
15294 }
15295 Err(message) => {
15297 self.chart_export_modal.reopen_with_path(path, format);
15298 self.chart_export_modal.error = Some(message);
15299 }
15300 }
15301 }
15302
15303 fn filter_problem(&self) -> Option<String> {
15306 let schema = self.data_table_state.as_ref()?.schema();
15307 self.sort_filter_modal
15308 .filter
15309 .statements
15310 .iter()
15311 .find_map(|f| crate::python_script::SidebarFilter::problem(f, schema.get(&f.column)))
15312 }
15313
15314 pub fn header_toggle_offered(&self) -> bool {
15317 self.opened
15318 .as_ref()
15319 .and_then(|(_, options)| options.format)
15320 .and_then(FileFormat::separator)
15321 .is_some()
15322 }
15323
15324 pub(crate) fn toggle_header(&mut self) -> Option<AppEvent> {
15328 if !self.header_toggle_offered() {
15329 return None;
15330 }
15331 let (paths, options) = self.opened.clone()?;
15332 let options = OpenOptions {
15333 has_header: Some(!options.has_header.unwrap_or(true)),
15334 ..options
15335 };
15336 self.set_loading_phase("Scanning input", 10);
15337 self.name_what_is_loading(paths[0].clone());
15338 Some(AppEvent::Open(paths, options))
15339 }
15340
15341 fn move_cursor_column(&mut self, right: bool) -> Option<AppEvent> {
15346 let state = self.data_table_state.as_ref()?;
15347 let at = state.current_column_index()?;
15348 let mut order = state.headers();
15349 let locked = state.locked_columns_count().min(order.len());
15350 let to = if right { at + 1 } else { at.checked_sub(1)? };
15351 if to >= order.len() || (at < locked) != (to < locked) {
15352 return None;
15353 }
15354 let moving = order[at].clone();
15356 self.data_table_state.as_mut()?.set_current_column(&moving);
15357 order.swap(at, to);
15358 let applied = &mut self.sort_filter_modal.sort.applied_order;
15361 if let (Some(i), Some(j)) = (
15362 applied.iter().position(|c| *c == order[at]),
15363 applied.iter().position(|c| *c == order[to]),
15364 ) {
15365 applied.swap(i, j);
15366 }
15367 Some(AppEvent::ColumnOrder(order, locked))
15368 }
15369
15370 fn sort_by_cursor_column(&mut self, descending: bool) -> Option<AppEvent> {
15378 let state = self.data_table_state.as_ref()?;
15379 let column = state.current_column()?.to_string();
15380 let already = state.view_sort_columns() == std::slice::from_ref(&column)
15381 && state.view_sort_descending() == [descending];
15382 if already {
15383 if let Some(state) = self.data_table_state.as_mut() {
15386 state.deferred(|s| s.sort(Vec::new(), true));
15387 }
15388 self.spawn_async_collect("Sorting...");
15389 return None;
15390 }
15391 Some(AppEvent::Sort(vec![column], vec![descending]))
15392 }
15393
15394 fn quick_filter(&mut self, keep: bool) -> Option<AppEvent> {
15395 let state = self.data_table_state.as_ref()?;
15396 let column = state.current_column()?.to_string();
15397 let row = state.copy_row_df()?;
15398 let series = row.column(&column).ok()?.as_materialized_series().clone();
15399 let value = series.get(0).ok()?;
15400 let dtype = state.schema().get(&column)?.clone();
15402 let (operator, text) = if value.is_null() {
15403 let operator = if keep {
15404 FilterOperator::IsNull
15405 } else {
15406 FilterOperator::IsNotNull
15407 };
15408 (operator, String::new())
15409 } else {
15410 let operator = if keep {
15411 FilterOperator::Eq
15412 } else {
15413 FilterOperator::NotEq
15414 };
15415 let text = crate::typed_value::text_of(&value, &dtype);
15418 let Some(text) = text else {
15419 let kind = match dtype {
15420 DataType::List(_) => "lists",
15421 DataType::Array(..) => "arrays",
15422 DataType::Struct(_) => "structs",
15423 DataType::Binary | DataType::BinaryOffset => "binary",
15424 _ => "this type",
15425 };
15426 self.flash_note(format!("+ and - filter on plain values, not {kind}"));
15427 return None;
15428 };
15429 (operator, text)
15430 };
15431 let statement = FilterStatement {
15432 columns: Vec::new(),
15433 column,
15434 operator,
15435 value: text,
15436 logical_op: LogicalOperator::And,
15437 };
15438 let mut statements = state.view_filters().to_vec();
15439 if statements.contains(&statement) {
15440 return None;
15441 }
15442 statements.push(statement);
15443 Some(AppEvent::Filter(statements))
15444 }
15445
15446 fn sync_sort_filter_modal(&mut self) {
15453 let Some(state) = self.data_table_state.as_ref() else {
15454 return;
15455 };
15456 let filters = state.view_filters().to_vec();
15457 let sort_columns = state.view_sort_columns().to_vec();
15458 let sort_descending = state.view_sort_descending().to_vec();
15459 let headers: Vec<String> = state.schema().iter_names().map(|s| s.to_string()).collect();
15460 let schema = state.schema().clone();
15461 let order = state.headers();
15462 let locked = state.locked_columns_count();
15463
15464 let modal = &mut self.sort_filter_modal;
15465 modal.filter.applied = filters.clone();
15466 modal.filter.statements = filters;
15467 modal.filter.operands = order
15468 .iter()
15469 .map(|name| {
15470 schema
15471 .get(name)
15472 .map(crate::filter_modal::Operand::of)
15473 .unwrap_or_default()
15474 })
15475 .collect();
15476 modal.filter.available_columns = order.clone();
15477 modal.filter.cursor = modal.filter.statements.len();
15479 modal.filter.editor = None;
15480 let shown: std::collections::HashSet<&str> = order.iter().map(String::as_str).collect();
15485 let applied = &modal.sort.applied_order;
15486 let current = applied
15487 .iter()
15488 .filter(|name| shown.contains(name.as_str()))
15489 .eq(order.iter());
15490 let reference: &[String] = if current { applied } else { &[] };
15491 let full = order_with_hidden(&order, &headers, reference);
15492 let places: HashMap<&str, usize> = full
15493 .iter()
15494 .enumerate()
15495 .map(|(i, name)| (name.as_str(), i))
15496 .collect();
15497 let place = |name: &String| places.get(name.as_str()).copied();
15498 let last_locked = applied
15501 .get(..modal.sort.applied_locked)
15502 .filter(|span| {
15503 current && span.iter().filter(|n| shown.contains(n.as_str())).count() == locked
15504 })
15505 .and_then(|span| span.iter().rev().find_map(place))
15506 .or_else(|| {
15507 locked
15508 .checked_sub(1)
15509 .and_then(|i| order.get(i))
15510 .and_then(place)
15511 });
15512 modal.sort.columns = headers
15513 .iter()
15514 .map(|name| {
15515 let display_order = places[name.as_str()];
15516 SortColumn {
15517 name: name.clone(),
15518 sort_order: sort_columns.iter().position(|c| c == name).map(|o| o + 1),
15521 sort_descending: sort_columns
15522 .iter()
15523 .position(|c| c == name)
15524 .and_then(|i| sort_descending.get(i).copied())
15525 .unwrap_or(false),
15526 display_order,
15527 is_locked: last_locked.is_some_and(|l| display_order <= l),
15528 is_to_be_locked: false,
15529 is_visible: shown.contains(name.as_str()),
15530 width: state.width_choice(name),
15531 shown_width: state.on_screen_width(name),
15532 }
15533 })
15534 .collect();
15535 modal.sort.has_unapplied_changes = false;
15536 }
15537
15538 fn apply_sort_filter(&mut self) -> Option<AppEvent> {
15542 if self.sort_filter_modal.filter.editor.is_some() {
15544 self.sort_filter_modal.filter.commit_editor();
15545 }
15546 if let Some(why) = self.filter_problem() {
15548 self.sort_filter_modal.sort.status = Some(why);
15549 return None;
15550 }
15551 let (columns, descending) = self.sort_filter_modal.sort.sorted_columns_and_directions();
15552 let column_order = self.sort_filter_modal.sort.get_column_order();
15553 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
15554 self.sort_filter_modal.sort.applied_order =
15555 self.sort_filter_modal.sort.get_full_column_order();
15556 self.sort_filter_modal.sort.applied_locked = self.sort_filter_modal.sort.get_locked_span();
15557 let statements = self.sort_filter_modal.filter.statements.clone();
15558 let view_unchanged = self.data_table_state.as_mut().is_some_and(|state| {
15563 state.set_width_choices(self.sort_filter_modal.sort.width_choices());
15564 state.headers() == column_order
15565 && state.locked_columns_count() == locked_count
15566 && state.view_filters() == statements.as_slice()
15567 && state.view_sort_columns() == columns.as_slice()
15568 && state.view_sort_descending() == descending.as_slice()
15569 });
15570 for col in &mut self.sort_filter_modal.sort.columns {
15571 col.is_to_be_locked = false;
15572 }
15573 self.sort_filter_modal.sort.has_unapplied_changes = false;
15574 self.sort_filter_modal.close();
15575 self.input_mode = InputMode::Normal;
15576 if view_unchanged {
15577 return None;
15578 }
15579 let _ = self.send_event(AppEvent::ColumnOrder(column_order, locked_count));
15580 let _ = self.send_event(AppEvent::Filter(statements));
15581 Some(AppEvent::Sort(columns, descending))
15582 }
15583
15584 fn info_tabs_on_offer(&self) -> crate::widgets::info::TabsOffered {
15586 let facts_tab = self.info_facts_tab();
15587 self.data_table_state
15588 .as_ref()
15589 .map(|state| crate::widgets::info::TabsOffered {
15590 documentation: self.info_documentation.is_open(),
15591 ..crate::widgets::info::TabsOffered::of(state, facts_tab)
15592 })
15593 .unwrap_or_default()
15594 }
15595
15596 pub(crate) fn opened_format(&self) -> Option<FileFormat> {
15598 self.opened
15599 .as_ref()
15600 .and_then(|(_, options)| options.format)
15601 .or_else(|| self.path.as_deref().and_then(FileFormat::from_path))
15602 }
15603
15604 pub(crate) fn info_facts_tab(&self) -> Option<&'static str> {
15608 self.info_facts()
15609 .and_then(|(format, _)| format.summary_tab())
15610 }
15611
15612 pub(crate) fn info_facts(&self) -> Option<(FileFormat, crate::readers::Facts)> {
15615 match self.file_facts()? {
15616 FileFacts::Read {
15618 size: None,
15619 detail: None,
15620 ..
15621 } => None,
15622 _ => self.facts_of_open(),
15623 }
15624 }
15625
15626 fn facts_of_open(&self) -> Option<(FileFormat, crate::readers::Facts)> {
15629 let hive = self
15630 .opened
15631 .as_ref()
15632 .is_some_and(|(_, options)| options.hive);
15633 let format = self.opened_format()?;
15634 let facts = crate::readers::of(format).facts?;
15635 let state = self.data_table_state.as_ref()?;
15636 let plain = state
15637 .read_mode()
15638 .is_none_or(|mode| Some(mode) == format.read_mode(crate::Stored::Plain));
15639 let one_file = state.dataset_schema().is_none();
15641 (!hive && plain && one_file).then_some((format, facts))
15642 }
15643
15644 fn apply_view(&mut self, view: &SavedView) -> Result<()> {
15649 self.apply_view_with(view, None)
15650 }
15651
15652 fn apply_matched_view(&mut self, view: &SavedView, why: view::MatchReason) -> Result<()> {
15655 self.apply_view_with(view, Some(why))
15656 }
15657
15658 fn apply_view_with(&mut self, view: &SavedView, why: Option<view::MatchReason>) -> Result<()> {
15659 self.jobs.supersede(|job| matches!(job, Job::ViewPivot(_)));
15660 if let Some(saved) = &view.settings.sample {
15661 return self.apply_sampled_view(view, saved, why);
15662 }
15663 let Some(state) = self.data_table_state.as_mut() else {
15664 return Ok(());
15665 };
15666 match state.try_transition(|s| Self::replay_view(s, &view.settings, None))? {
15667 (Replayed::Planned, rollback) => {
15668 self.view_planned(view, rollback, why);
15669 Ok(())
15670 }
15671 (Replayed::Pivot(job), rollback) => {
15672 state.roll_back(rollback);
15674 self.jobs.try_advance();
15677 let pivot_view = Job::ViewPivot(Box::new((view.clone(), why)));
15678 self.spawn_job(pivot_view, Some(Self::APPLYING_VIEW), move |_| {
15679 let pivoted = job
15680 .run()
15681 .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
15682 Ok(Answer::ViewPivoted(pivoted))
15683 });
15684 Ok(())
15685 }
15686 }
15687 }
15688
15689 fn view_planned(
15693 &mut self,
15694 view: &SavedView,
15695 rollback: crate::widgets::datatable::ViewRollback,
15696 why: Option<view::MatchReason>,
15697 ) {
15698 if let Some(path) = &self.path {
15699 use crate::logging::LogFailure;
15700 self.view_manager
15701 .record_use(&view.id, path)
15702 .or_log("record a view's use");
15703 }
15704 let previous = self.active_view_id.replace(view.id.clone());
15705 self.restore_view_chart(view.settings.chart.as_ref());
15706 let Some(state) = self.data_table_state.as_ref() else {
15707 return;
15708 };
15709 self.query_running = Some(QueryRun {
15710 origin: RunOrigin::View {
15711 previous,
15712 matched: why.map(|why| (view.name.clone(), why)),
15713 },
15714 frame: state.len_generation(),
15715 rollback,
15716 len_count_inflight: self.len_count_inflight,
15717 count_after_paint: self.count_after_paint,
15718 len_count_failed: self.len_count_failed,
15719 rows: None,
15720 });
15721 if !self.spawn_async_collect(Self::APPLYING_VIEW) {
15722 if let Some(why) = why {
15725 self.flash_view_applied(&view.name, why);
15726 }
15727 self.query_running = None;
15728 self.busy = false;
15729 self.status_message = None;
15730 self.first_rows_settled();
15731 }
15732 }
15733
15734 pub(crate) fn view_applying(&self) -> bool {
15737 if !self.is_busy() || !self.in_normal_table_view() {
15738 return false;
15739 }
15740 let pivot = self
15741 .jobs
15742 .current(|job| matches!(job, Job::ViewPivot(_)))
15743 .is_some();
15744 let rows = self.query_running.as_ref().is_some_and(|run| {
15745 matches!(run.origin, RunOrigin::View { .. })
15746 && self
15747 .data_table_state
15748 .as_ref()
15749 .is_some_and(|state| state.len_generation() == run.frame)
15750 });
15751 pivot || rows
15752 }
15753
15754 pub(crate) fn query_reading(&self) -> Option<&str> {
15758 let run = self.query_running.as_ref()?;
15759 let frame = self.data_table_state.as_ref()?.len_generation();
15760 if !matches!(run.origin, RunOrigin::Query(_)) || run.frame != frame {
15761 return None;
15762 }
15763 self.jobs
15764 .waiting_status(|job| Self::reading_rows(job) || Self::owed_rows(job))
15765 }
15766
15767 fn cancel_view(&mut self) {
15770 self.jobs.advance();
15771 self.screen_generation = self.screen_generation.wrapping_add(1);
15772 if let Some(run) = self.take_query_run() {
15773 self.roll_back_query_run(run);
15774 }
15775 self.forget_the_rows_read();
15777 self.read_after_view_rollback();
15778 self.flash_note("View cancelled".to_string());
15779 }
15780
15781 fn job_ended(&mut self, ticket: Ticket) -> Option<AppEvent> {
15790 let jobs::Ended {
15791 job,
15792 current,
15793 keys,
15794 outcome,
15795 ..
15796 } = self.jobs.end(ticket)?;
15797 let cancelled_analysis = !current && Self::reads_for_analysis(&job);
15798 let waited = keys.is_some();
15799 let out = match outcome {
15800 Outcome::Answered(answer) => self.answered(job, current, waited, *answer),
15801 Outcome::Failed { message, panicked } => {
15802 self.background_failed(&job, current, waited, &message, panicked);
15803 None
15804 }
15805 };
15806 if let Some(status) = keys {
15807 if out.is_some() {
15808 self.busy = true;
15809 } else {
15810 self.busy = false;
15811 if self.status_message.as_deref() == Some(status.as_str())
15814 && !self.jobs.shows(&status)
15815 {
15816 self.status_message = None;
15817 }
15818 }
15819 }
15820 if cancelled_analysis
15823 && self.cancelled_analysis().is_none()
15824 && self.analysis_modal.data_quality_setup_note.as_deref() == Some(QUALITY_RUN_WAITS)
15825 {
15826 self.analysis_modal.data_quality_setup_note = None;
15827 }
15828 out
15829 }
15830
15831 fn job_progress(&mut self, ticket: Ticket, progress: &Progress) {
15833 if !self.jobs.is_current(ticket) {
15834 return;
15835 }
15836 match progress {
15837 Progress::ExportWriting { phase, bytes } => {
15838 if let Some(export) = self.export_progress.as_mut() {
15839 export.current_phase = phase.to_string();
15840 export.written = Some(*bytes);
15841 }
15842 }
15843 Progress::QualityPhase(phase) => {
15844 if let Some(progress) = self.analysis_modal.computing.as_mut() {
15845 progress.phase = phase.stage.label().to_string();
15846 progress.reads_source = Some(phase.reads_source);
15847 progress.interruptible = Some(phase.interruptible);
15848 }
15849 }
15850 Progress::Finding { rows } => self.find_progress(*rows),
15851 Progress::HexFinding { read, total } => self.hex_find_progress(*read, *total),
15852 Progress::SampleBegun(schema) => self.sample_begun(schema),
15853 Progress::SampleGrew => self.sample_grew(),
15854 }
15855 }
15856
15857 fn answered(
15861 &mut self,
15862 job: Job,
15863 current: bool,
15864 waited: bool,
15865 answer: Answer,
15866 ) -> Option<AppEvent> {
15867 match answer {
15868 Answer::Load(answer) => {
15869 let Job::Load(load) = job else {
15874 return None;
15875 };
15876 let step = self.loading.answered(
15877 load,
15878 *answer,
15879 #[cfg(any(feature = "http", feature = "cloud"))]
15880 &self.jobs,
15881 );
15882 self.run_load_step(step)
15883 }
15884 Answer::NamedPaths {
15885 paths,
15886 options,
15887 directory,
15888 } => {
15889 let Job::OpenNamed(load) = job else {
15892 return None;
15893 };
15894 if !self.loading.looking_at_paths(load) {
15895 return None;
15896 }
15897 Some(match directory {
15899 Some(dir) => AppEvent::LookThenOpenDirectory(dir, *options),
15900 None => AppEvent::Open(paths, *options),
15901 })
15902 }
15903 Answer::NamedPathMissing(path) => {
15904 let Job::OpenNamed(load) = job else {
15905 return None;
15906 };
15907 if !self.loading.looking_at_paths(load) {
15908 return None;
15909 }
15910 if let Some(retired) = self.loading.retire() {
15912 self.put_down_load(retired);
15913 }
15914 Some(AppEvent::NamedPathMissing(path))
15915 }
15916 Answer::LookedAt {
15917 kind,
15918 holds,
15919 options,
15920 } => {
15921 let Job::LookAtDirectory { load, path } = job else {
15926 return None;
15927 };
15928 if !self.loading.looking_at_directory(load) {
15929 return None;
15930 }
15931 self.open_the_directory_looked_at(path, kind, holds.as_deref(), *options)
15933 }
15934 Answer::Kind(found) => {
15935 let Job::Classify(asked) = job else {
15938 return None;
15939 };
15940 if !current {
15941 return None;
15942 }
15943 self.home.status = None;
15944
15945 if self.input_mode != InputMode::Home || self.home.browsing != asked.browsing {
15950 return None;
15951 }
15952
15953 let path = asked.path;
15954 let Some(kind) = found else {
15955 self.home.status = Some(format!("No such path: {}", path.display()));
15956 if asked.jump {
15957 self.home.path_input = path.display().to_string();
15959 self.home.path_input_active = true;
15960 self.list_the_typed_directory();
15961 }
15962 return None;
15963 };
15964 self.open_what_it_is(path, kind, asked.jump)
15965 }
15966 Answer::Rows(result) => {
15967 let Job::Rows(inflight) = job else {
15969 return None;
15970 };
15971 if !current {
15972 return None;
15973 }
15974 if let Some(state) = self.data_table_state.as_ref() {
15978 let took = inflight.began.elapsed();
15979 log::debug!(
15980 target: "datui",
15981 "rows {}..{} of {}: read in {took:.1?}",
15982 inflight.start,
15983 inflight.end,
15984 inflight.dataset
15985 );
15986 state.measurements().read_page(took, inflight.files);
15987 }
15988 if let Some(state) = &mut self.data_table_state {
15989 state.apply_async_collect(result);
15990 }
15991 self.retire_a_count_the_rows_answered();
15992 self.remember_a_downloads_shape();
15993 self.catch_up_follow();
15995 let ran = self.take_query_run();
15997 if waited {
16000 self.first_rows_settled();
16001 match ran.map(|run| run.origin) {
16002 Some(RunOrigin::Query(mode)) if self.query_prompt_mode() == Some(mode) => {
16003 self.leave_query_prompt_after_run();
16004 }
16005 Some(RunOrigin::View {
16007 matched: Some((name, why)),
16008 ..
16009 }) => self.flash_view_applied(&name, why),
16010 _ => {}
16011 }
16012 }
16013 None
16014 }
16015 Answer::RowsFailed {
16016 message,
16017 conversion,
16018 } => {
16019 self.rows_failed(current, waited, &message, conversion.as_deref());
16020 None
16021 }
16022 Answer::Described(results) => {
16023 if current {
16024 self.analysis_modal.describe_results = Some(results);
16025 self.analysis_modal.computing = None;
16026 }
16027 None
16028 }
16029 Answer::Distributions(results) => {
16030 if current {
16031 self.analysis_modal.distribution_results = Some(results);
16032 self.analysis_modal.computing = None;
16033 }
16034 None
16035 }
16036 Answer::Correlations(results) => {
16037 if current {
16038 self.analysis_modal.install_correlations(results);
16039 self.analysis_modal.computing = None;
16040 }
16041 None
16042 }
16043 Answer::DataQuality {
16044 results,
16045 kept,
16046 plan,
16047 } => {
16048 if let Some(kept) = kept {
16051 self.retain_quality_sample(&kept);
16052 }
16053 if current
16054 && self.analysis_modal.active
16055 && self.analysis_modal.selected_tool
16056 == Some(analysis_modal::AnalysisTool::DataQuality)
16057 {
16058 self.cache_quality_result(&results, (*plan).clone());
16061 self.analysis_modal.data_quality_last_plan = Some(*plan);
16062 self.analysis_modal.data_quality_results = Some(*results);
16063 self.analysis_modal.data_quality_from_cache = false;
16064 self.analysis_modal
16065 .set_quality_page(crate::data_quality::QualityPage::Overview);
16066 self.analysis_modal.computing = None;
16067 }
16068 None
16069 }
16070 Answer::SampleDrawn(drawn) => self.sample_drawn(job, current, drawn),
16071 Answer::Sample { df, label } => {
16072 if current {
16073 self.analysis_modal.computing = None;
16074 self.show_sample_view(df, label);
16075 }
16076 None
16077 }
16078 Answer::Pivoted { spec, pivoted } => {
16079 if !current {
16081 return None;
16082 }
16083 let installed = self.data_table_state.as_mut().map(|state| {
16084 state
16085 .deferred(|s| s.install_pivot(&spec, pivoted))
16086 .map_err(|e| crate::error_display::user_message_from_report(&e, None))
16087 });
16088 match installed {
16089 Some(Ok(())) => {
16090 self.pivot_melt_modal.close();
16091 if self.input_mode == InputMode::PivotMelt {
16093 self.input_mode = InputMode::Normal;
16094 }
16095 self.spawn_async_collect(Self::LOADING_BUFFER);
16097 }
16098 Some(Err(message)) => self.error_modal.show(message),
16099 None => {}
16100 }
16101 None
16102 }
16103 Answer::ReshapePreviewed { input, result } => {
16104 if let Job::ReshapePreview { epoch, token } = job {
16105 self.reshape_preview_ended(epoch, token, input, result);
16106 }
16107 None
16108 }
16109 Answer::ViewPivoted(pivoted) => {
16110 let Job::ViewPivot(pivot) = job else {
16113 return None;
16114 };
16115 let (view, why) = *pivot;
16116 if !current {
16117 return None;
16118 }
16119 let planned = self.data_table_state.as_mut().map(|state| {
16120 state
16123 .try_transition(|s| Self::replay_view(s, &view.settings, Some(pivoted)))
16124 .map(|(_, rollback)| rollback)
16125 .map_err(|e| e.to_string())
16126 });
16127 match planned {
16128 Some(Ok(rollback)) => self.view_planned(&view, rollback, why),
16130 Some(Err(message)) => self.view_pivot_failed(&message),
16131 None => {}
16132 }
16133 None
16134 }
16135 Answer::DrillRow { group_index, row } => {
16136 if current {
16138 self.drill_into(group_index, &row);
16139 }
16140 None
16141 }
16142 Answer::FieldsRead(values) => {
16143 let Job::InspectRow { frame, row } = job else {
16145 return None;
16146 };
16147 if !current {
16148 return None;
16149 }
16150 let asked = self
16151 .inspector_modal
16152 .read
16153 .as_ref()
16154 .is_some_and(|read| read.key() == (frame, row));
16155 if self.inspector_modal.active && asked {
16156 self.inspector_modal.read =
16157 Some(inspector_modal::FieldRead::Read { frame, row, values });
16158 }
16159 None
16160 }
16161 Answer::JsonParsed(root) => {
16162 let Job::InspectJson { token } = job else {
16164 return None;
16165 };
16166 if !current || !self.inspector_modal.active {
16167 return None;
16168 }
16169 let modal = &mut self.inspector_modal;
16170 if let Some(wait) = modal.json_wait.take_if(|w| w.token == token) {
16171 let node = inspector_drill::Node::Json {
16172 root,
16173 path: Vec::new(),
16174 };
16175 modal.drill_in(wait.frame, wait.row, wait.label, node);
16176 }
16177 None
16178 }
16179 Answer::Indented(text) => {
16180 let Job::InspectPretty { token } = job else {
16181 return None;
16182 };
16183 let modal = &mut self.inspector_modal;
16184 if current
16185 && let Some(inspector_modal::Pretty::Pending { token: t, place }) =
16186 modal.pretty.as_ref()
16187 && *t == token
16188 {
16189 modal.pretty = Some(inspector_modal::Pretty::Ready {
16190 place: place.clone(),
16191 text,
16192 });
16193 }
16194 None
16195 }
16196 Answer::Unpacked(decoded) => {
16197 let Job::InspectUnpack { token } = job else {
16198 return None;
16199 };
16200 let modal = &mut self.inspector_modal;
16201 if current
16202 && let Some(inspector_modal::Unpack::Pending { token: t, place }) =
16203 modal.unpack.as_ref()
16204 && *t == token
16205 {
16206 modal.unpack = Some(inspector_modal::Unpack::Ready {
16207 place: place.clone(),
16208 text: std::sync::Arc::new(decoded),
16209 });
16210 }
16211 None
16212 }
16213 Answer::ValueWritten(open) => {
16214 if current && self.inspector_modal.active {
16215 self.external_open = Some(open);
16216 }
16217 None
16218 }
16219 Answer::Exported(path) => {
16220 self.export_modal.close();
16222 self.export_counts = None;
16223 if current {
16224 self.export_progress = None;
16225 self.flash_path("Exported to ", &path);
16226 }
16227 None
16228 }
16229 Answer::Copied { payload, message } => {
16230 if current {
16231 self.export_progress = None;
16232 self.finish_copy(payload, message);
16233 }
16234 None
16235 }
16236 Answer::QualityReportWritten(path) => {
16237 self.analysis_modal.data_quality_export = None;
16238 if current {
16239 self.flash_path("Report written to ", &path);
16240 }
16241 None
16242 }
16243 Answer::ChartExported => {
16244 if let Job::ChartExport { path, format } = job
16247 && current
16248 {
16249 self.finish_chart_export(&path, format, Ok(()));
16250 }
16251 None
16252 }
16253 Answer::FileFacts(facts) => {
16254 if let Job::FileFacts { dataset } = job {
16255 self.file_facts_landed(dataset, facts);
16256 }
16257 None
16258 }
16259 Answer::UnfitCounted(unfit) => {
16260 let columns: Vec<&str> = unfit.iter().map(|u| u.column.as_str()).collect();
16262 let said = if columns.is_empty() {
16263 "none".to_string()
16264 } else {
16265 columns.join(", ")
16266 };
16267 log::debug!(target: "datui", "values column types made null, by column: {said}");
16268 if let Job::UnfitCount { dataset, version } = job
16269 && dataset == self.dataset_generation
16270 && let Some(state) = self.data_table_state.as_mut()
16271 {
16272 match version {
16273 None => state.unfit_counted(&unfit),
16274 Some(version) => state.changes_unfit_counted(version, &unfit),
16275 }
16276 }
16277 None
16278 }
16279 Answer::Found(found) => {
16280 if let Job::Find(run) = job {
16281 self.find_answered(run, current, found);
16282 }
16283 None
16284 }
16285 Answer::HexOpened(source) => {
16286 self.hex_opened(job, current, *source);
16287 None
16288 }
16289 Answer::HexFound(hit) => {
16290 self.hex_found(job, current, hit);
16291 None
16292 }
16293 Answer::ValueCounts(counts) => {
16294 if current {
16297 self.value_counts.computing = None;
16298 self.value_counts.hold(*counts);
16299 }
16300 None
16301 }
16302 #[cfg(test)]
16304 Answer::Probe(held) => {
16305 drop(held);
16306 None
16307 }
16308 }
16309 }
16310
16311 fn background_failed(
16318 &mut self,
16319 job: &Job,
16320 current: bool,
16321 waited: bool,
16322 message: &str,
16323 panicked: bool,
16324 ) {
16325 match job {
16326 Job::Load(load) | Job::OpenNamed(load) | Job::LookAtDirectory { load, .. } => {
16329 if let loading::Step::Failed(failed) = self.loading.failed(*load, message) {
16330 self.load_failed(failed);
16331 }
16332 }
16333 Job::Classify(_) => {
16334 if current {
16335 self.home.status = None;
16336 self.error_modal.show(message.to_string());
16337 }
16338 }
16339 Job::Rows(_) | Job::OwedRows { .. } => self.rows_failed(current, waited, message, None),
16340 Job::Analysis(_) | Job::SampleRows => {
16341 if current {
16342 self.analysis_modal.computing = None;
16343 self.error_modal.show(message.to_string());
16344 }
16345 }
16346 Job::SampleDraw(_) => self.sample_draw_failed(job, current, message),
16347 Job::Pivot | Job::Copy => {
16349 if current {
16350 self.error_modal.show(message.to_string());
16351 }
16352 }
16353 Job::QualityReport => {
16355 if current {
16356 match self.analysis_modal.data_quality_export.as_mut() {
16357 Some(form) => form.error = Some(message.to_string()),
16358 None => self.error_modal.show(message.to_string()),
16359 }
16360 }
16361 }
16362 Job::ViewPivot(_) => {
16363 if current {
16364 self.view_pivot_failed(message);
16365 }
16366 }
16367 Job::ReshapePreview { epoch, token } => {
16369 let message = if panicked {
16370 "Could not preview; see the log".to_string()
16371 } else {
16372 message.to_string()
16373 };
16374 self.reshape_preview_ended(*epoch, *token, None, Err(message));
16375 }
16376 Job::DrillRow => {
16377 if current {
16381 self.flash_note(if panicked {
16382 "Could not drill in; see the log".to_string()
16383 } else {
16384 format!("Could not drill in: {message}")
16385 });
16386 }
16387 }
16388 Job::InspectJson { token } => {
16389 let modal = &mut self.inspector_modal;
16390 if current && let Some(wait) = modal.json_wait.take_if(|w| w.token == *token) {
16391 modal.not_json = Some((wait.frame, wait.row, wait.path));
16392 self.flash_note(if panicked {
16393 "Could not read the JSON; see the log".to_string()
16394 } else {
16395 sentence(message)
16396 });
16397 }
16398 }
16399 Job::InspectPretty { token } => {
16400 let modal = &mut self.inspector_modal;
16401 if let Some(inspector_modal::Pretty::Pending { token: t, place }) =
16402 modal.pretty.as_ref()
16403 && t == token
16404 {
16405 modal.pretty = Some(inspector_modal::Pretty::Failed {
16406 place: place.clone(),
16407 });
16408 }
16409 }
16410 Job::InspectUnpack { token } => {
16411 let modal = &mut self.inspector_modal;
16412 if let Some(inspector_modal::Unpack::Pending { token: t, place }) =
16413 modal.unpack.as_ref()
16414 && t == token
16415 {
16416 modal.unpack = Some(inspector_modal::Unpack::Failed {
16417 place: place.clone(),
16418 });
16419 }
16420 }
16421 Job::OpenValue => {
16422 if current {
16423 self.flash_note(if panicked {
16424 "Could not open the value; see the log".to_string()
16425 } else {
16426 format!("Could not open the value: {message}")
16427 });
16428 }
16429 }
16430 Job::InspectRow { frame, row } => {
16431 if current {
16432 let asked = self
16433 .inspector_modal
16434 .read
16435 .as_ref()
16436 .is_some_and(|read| read.key() == (*frame, *row));
16437 if asked {
16438 let message = if panicked {
16440 "Could not read the field; see the log".to_string()
16441 } else {
16442 format!("Could not read the field: {message}")
16443 };
16444 self.inspector_modal.read = Some(inspector_modal::FieldRead::Failed {
16445 frame: *frame,
16446 row: *row,
16447 message,
16448 });
16449 }
16450 }
16451 }
16452 Job::Export => {
16455 if current {
16456 self.export_progress = None;
16457 self.export_modal.resume();
16458 self.export_modal.path_error = Some(message.to_string());
16459 self.input_mode = InputMode::Export;
16460 } else {
16461 self.export_modal.close();
16462 self.export_counts = None;
16463 }
16464 }
16465 Job::ChartExport { path, format } => {
16466 if current {
16467 self.finish_chart_export(path, *format, Err(message.to_string()));
16468 }
16469 }
16470 Job::Find(_) => self.find_failed(current, message),
16471 Job::HexOpen { .. } => {
16472 if current {
16473 self.error_modal.show(message.to_string());
16474 }
16475 }
16476 Job::HexFind(_) => {
16477 if current {
16478 self.status_message = None;
16479 self.flash_note(message.to_string());
16480 }
16481 }
16482 Job::ValueCounts => {
16483 if current && let Some(computing) = self.value_counts.computing.take() {
16485 let why = if panicked {
16486 "could not count; see the log".to_string()
16487 } else {
16488 message.to_string()
16489 };
16490 self.value_counts.failed = Some((computing.column, why));
16491 }
16492 }
16493 Job::FileFacts { dataset } => {
16495 let why = if panicked {
16498 "could not read; see the log".to_string()
16499 } else {
16500 message.to_string()
16501 };
16502 self.file_facts_landed(*dataset, FileFacts::Failed(why));
16503 }
16504 Job::UnfitCount { .. } => {
16506 log::warn!(target: "datui", "counting values that did not fit their type failed: {message}");
16507 }
16508 }
16509 }
16510
16511 fn read_file_facts(&mut self) {
16519 let dataset = self.dataset_generation;
16520 if self.data_table_state.is_none()
16521 || self
16522 .file_facts
16523 .as_ref()
16524 .is_some_and(|(read, _)| *read == dataset)
16525 || self.file_facts_reading()
16526 {
16527 return;
16528 }
16529 let several = self
16532 .opened
16533 .as_ref()
16534 .is_some_and(|(paths, _)| paths.len() > 1);
16535 let piped = self.reads_stdin();
16536 let Some(path) = self.path.clone().filter(|path| {
16537 !several
16538 && !piped
16539 && !source::is_remote_url(path)
16540 && !source::is_prefix_or_glob(&path.to_string_lossy())
16541 }) else {
16542 return;
16543 };
16544 let facts = self.facts_of_open().map(|(_, facts)| facts);
16545 #[cfg(test)]
16546 let read: FileFactsReader = self
16547 .file_facts_reader
16548 .clone()
16549 .unwrap_or_else(|| Arc::new(FileFacts::read));
16550 #[cfg(not(test))]
16551 let read = FileFacts::read;
16552 self.spawn_job(Job::FileFacts { dataset }, None, move |_| {
16553 Ok(Answer::FileFacts(read(&path, facts)?))
16554 });
16555 }
16556
16557 fn count_unfit(&mut self) {
16561 let dataset = self.dataset_generation;
16562 let Some(state) = self.data_table_state.as_ref() else {
16563 return;
16564 };
16565 let read = state
16566 .unfit_to_count()
16567 .map(|(source, typed)| (source, typed, None));
16568 let view = state
16569 .changes_unfit_to_count()
16570 .map(|(source, typed, version)| (source, typed, Some(version)));
16571 let streaming = self.app_config.performance.streaming;
16572 for (source, typed, version) in [read, view].into_iter().flatten() {
16573 let running = self
16574 .jobs
16575 .current(|job| {
16576 matches!(job, Job::UnfitCount { dataset: d, version: v }
16577 if *d == dataset && *v == version)
16578 })
16579 .is_some();
16580 if running {
16581 continue;
16582 }
16583 self.spawn_job(Job::UnfitCount { dataset, version }, None, move |_| {
16584 let counted = crate::statistics::collect_lazy(
16585 crate::column_types::unfit_frame(source, &typed),
16586 streaming,
16587 )
16588 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
16589 Ok(Answer::UnfitCounted(crate::column_types::unfit_counts(
16590 &counted, &typed,
16591 )))
16592 });
16593 }
16594 }
16595
16596 pub fn unfit_count_pending(&self) -> bool {
16598 self.jobs
16599 .current(|job| matches!(job, Job::UnfitCount { .. }))
16600 .is_some()
16601 }
16602
16603 pub(crate) fn file_facts_reading(&self) -> bool {
16605 let dataset = self.dataset_generation;
16606 self.jobs
16607 .current(|job| matches!(job, Job::FileFacts { dataset: asked } if *asked == dataset))
16608 .is_some()
16609 }
16610
16611 fn file_facts_landed(&mut self, dataset: u64, facts: FileFacts) {
16614 if dataset == self.dataset_generation {
16615 self.file_facts = Some((dataset, facts));
16616 }
16617 }
16618
16619 pub fn file_facts(&self) -> Option<&FileFacts> {
16623 Self::facts_shown(
16624 &self.file_facts,
16625 self.dataset_generation,
16626 self.file_facts_reading(),
16627 )
16628 }
16629
16630 pub(crate) fn facts_shown(
16633 read: &Option<(u64, FileFacts)>,
16634 dataset: u64,
16635 reading: bool,
16636 ) -> Option<&FileFacts> {
16637 static READING: FileFacts = FileFacts::Reading;
16638 match read {
16639 Some((read_for, facts)) if *read_for == dataset => Some(facts),
16640 _ => reading.then_some(&READING),
16641 }
16642 }
16643
16644 fn land_on_tables(&mut self, tables: loading::Tables) {
16647 let loading::Tables {
16648 database,
16649 from_home,
16650 } = tables;
16651 self.status_message = None;
16652 self.busy = false;
16653 self.enter_home();
16654 if from_home {
16655 self.home_browse_into(database);
16656 } else {
16657 self.home_jump_into(database);
16658 }
16659 }
16660
16661 fn load_failed(&mut self, failed: loading::Failed) {
16665 let loading::Failed { message, from_home } = failed;
16666 self.status_message = None;
16667 self.busy = false;
16668 if from_home {
16673 self.last_load_error = None;
16674 self.enter_home();
16675 } else {
16676 self.last_load_error = Some(message.clone());
16677 }
16678 self.error_modal.show(message);
16679 }
16680
16681 fn rows_failed(
16685 &mut self,
16686 current: bool,
16687 waited: bool,
16688 message: &str,
16689 conversion: Option<&crate::error_display::ConversionFailure>,
16690 ) {
16691 if !current {
16692 return;
16693 }
16694 let message = &self.named_by_source(message);
16695 if let Some(run) = self.take_query_run() {
16696 self.fail_query_run(run, message, conversion);
16697 return;
16698 }
16699 if !waited {
16700 return;
16701 }
16702 if let Some(generation) = self.count_after_paint.take() {
16705 if self.len_count_inflight == Some(generation) {
16706 self.len_count_inflight = None;
16707 }
16708 if self
16709 .data_table_state
16710 .as_ref()
16711 .is_some_and(|state| state.len_generation() == generation)
16712 {
16713 self.len_count_failed = Some(generation);
16714 }
16715 }
16716 self.first_rows_settled();
16717 self.error_modal.show(message.to_string());
16718 }
16719
16720 fn named_by_source(&self, message: &str) -> String {
16723 let (Some(state), Some(source)) = (self.data_table_state.as_ref(), self.path.as_deref())
16724 else {
16725 return message.to_string();
16726 };
16727 state
16728 .temp_files()
16729 .into_iter()
16730 .fold(message.to_string(), |message, file| {
16731 crate::error_display::named_by_source(&message, file, source)
16732 })
16733 }
16734
16735 fn view_pivot_failed(&mut self, message: &str) {
16737 self.error_modal
16738 .show(format!("Error applying view: {message}"));
16739 self.read_after_view_rollback();
16740 }
16741
16742 fn fail_query_run(
16745 &mut self,
16746 run: QueryRun,
16747 message: &str,
16748 conversion: Option<&crate::error_display::ConversionFailure>,
16749 ) {
16750 let rows = run.rows;
16751 let origin = self.roll_back_query_run(run);
16752 self.first_rows_settled();
16753 self.status_message = None;
16754 self.busy = false;
16755 let mode = match origin {
16759 RunOrigin::View { .. } => {
16760 self.error_modal
16761 .show(format!("Error applying view: {message}"));
16762 self.read_after_view_rollback();
16763 return;
16764 }
16765 RunOrigin::Query(mode) if self.query_prompt_mode() == Some(mode) => mode,
16766 RunOrigin::Query(_) => {
16767 self.error_modal.show(message.to_string());
16768 return;
16769 }
16770 };
16771 let sql = mode == QueryMode::Sql;
16772 self.query_run_error = Some(match conversion {
16773 Some(failure) if sql => failure.sql_message(rows),
16774 _ => message.to_string(),
16775 });
16776 self.inline_failures = self.inline_failures.wrapping_add(1);
16777 }
16778
16779 fn roll_back_query_run(&mut self, run: QueryRun) -> RunOrigin {
16782 if let Some(state) = self.data_table_state.as_mut() {
16783 state.roll_back(run.rollback);
16784 }
16785 self.len_count_inflight = run.len_count_inflight;
16786 self.count_after_paint = run.count_after_paint;
16787 self.len_count_failed = run.len_count_failed;
16788 if let RunOrigin::View { previous, .. } = &run.origin {
16789 self.active_view_id = previous.clone();
16790 }
16791 run.origin
16792 }
16793
16794 fn read_after_view_rollback(&mut self) {
16797 self.busy = false;
16798 self.status_message = None;
16799 if !self.spawn_async_collect(Self::LOADING_BUFFER) {
16800 self.first_rows_settled();
16801 }
16802 }
16803
16804 fn replay_view(
16809 state: &mut DataTableState,
16810 settings: &view::ViewSettings,
16811 pivoted: Option<DataFrame>,
16812 ) -> Result<Replayed> {
16813 if settings.pivot.is_some() || settings.melt.is_some() {
16814 if let Some(source) = &settings.reshape_source {
16815 Self::replay_query(
16816 state,
16817 source.sql_query.as_deref(),
16818 source.query.as_deref(),
16819 source.fuzzy_query.as_deref(),
16820 )?;
16821 Self::replay_filters_and_sort(
16822 state,
16823 &source.filters,
16824 &source.sort_columns,
16825 source.sort_directions(),
16826 )?;
16827 }
16828 let reshaped = match (&settings.pivot, &settings.melt, pivoted) {
16829 (Some(spec), _, Some(pivoted)) => state.install_pivot(spec, pivoted),
16830 (Some(spec), _, None) => {
16831 Self::check_plan(state)?;
16832 return Ok(Replayed::Pivot(Box::new(state.plan_pivot(spec))));
16833 }
16834 (None, Some(spec), _) => state.melt(spec),
16835 (None, None, _) => Ok(()),
16836 };
16837 reshaped.map_err(|e| {
16838 color_eyre::eyre::eyre!(
16839 "{}",
16840 crate::error_display::user_message_from_report(&e, None)
16841 )
16842 })?;
16843 }
16844 Self::replay_query(
16845 state,
16846 settings.sql_query.as_deref(),
16847 settings.query.as_deref(),
16848 settings.fuzzy_query.as_deref(),
16849 )?;
16850 if !settings.columns.is_empty() {
16852 state.set_column_changes(&settings.columns);
16853 }
16854 Self::replay_filters_and_sort(
16855 state,
16856 &settings.filters,
16857 &settings.sort_columns,
16858 settings.sort_directions(),
16859 )?;
16860 if !settings.column_order.is_empty() {
16861 state.set_column_order(settings.column_order.clone());
16862 state.set_locked_columns(settings.locked_columns_count);
16863 }
16864 Self::check_plan(state)?;
16865 Ok(Replayed::Planned)
16866 }
16867
16868 fn check_plan(state: &DataTableState) -> Result<()> {
16870 state.check_plan().map_err(|e| {
16871 color_eyre::eyre::eyre!("{}", crate::error_display::user_message_from_polars(&e))
16872 })
16873 }
16874
16875 fn replay_query(
16877 state: &mut DataTableState,
16878 sql: Option<&str>,
16879 dsl: Option<&str>,
16880 fuzzy: Option<&str>,
16881 ) -> Result<()> {
16882 let stated = |q: Option<&str>| q.filter(|q| !q.trim().is_empty()).map(str::to_string);
16883 if let Some(sql) = stated(sql) {
16884 state.sql_query(sql);
16885 } else if let Some(query) = stated(dsl) {
16886 state.query(query);
16887 }
16888 if state.error().is_none()
16889 && let Some(fuzzy) = stated(fuzzy)
16890 {
16891 state.fuzzy_search(fuzzy);
16892 }
16893 match state.error().cloned() {
16894 Some(error) => Err(color_eyre::eyre::eyre!(
16895 "{}",
16896 crate::error_display::user_message_from_polars(&error)
16897 )),
16898 None => Ok(()),
16899 }
16900 }
16901
16902 fn replay_filters_and_sort(
16904 state: &mut DataTableState,
16905 filters: &[FilterStatement],
16906 sort_columns: &[String],
16907 descending: Vec<bool>,
16908 ) -> Result<()> {
16909 if !filters.is_empty() {
16910 state.filter(filters.to_vec());
16911 if let Some(error) = state.error().cloned() {
16912 return Err(color_eyre::eyre::eyre!("{}", error));
16913 }
16914 }
16915 if !sort_columns.is_empty() {
16916 state.sort_by(sort_columns.to_vec(), descending);
16917 if let Some(error) = state.error().cloned() {
16918 return Err(color_eyre::eyre::eyre!("{}", error));
16919 }
16920 }
16921 Ok(())
16922 }
16923
16924 fn export_write_phase(request: &ExportRequest) -> &'static str {
16926 if request.options.compression(request.format).is_some() {
16927 "Writing and compressing file"
16928 } else {
16929 "Writing file"
16930 }
16931 }
16932
16933 fn format_export_error(error: &color_eyre::eyre::Report) -> String {
16937 use std::io::{self, ErrorKind};
16938
16939 for cause in error.chain() {
16940 if let Some(io_err) = cause.downcast_ref::<io::Error>() {
16941 let msg = match (crate::output_file::Refused::of(io_err), io_err.kind()) {
16944 (Some(refused), _) => format!("{refused}."),
16945 (None, _) if crate::error_display::held_by_another_program(io_err) => {
16947 "it is open in another program; close it there and try again.".to_string()
16948 }
16949 (None, ErrorKind::PermissionDenied) => "permission denied.".to_string(),
16950 (None, ErrorKind::IsADirectory) => "it is a directory.".to_string(),
16951 (None, _) => crate::error_display::user_message_from_io(io_err, None),
16952 };
16953 return format!("Cannot write: {msg}");
16954 }
16955 if let Some(pe) = cause.downcast_ref::<polars::prelude::PolarsError>() {
16956 let msg = crate::error_display::user_message_from_polars(pe);
16957 return format!("Export failed: {}", msg);
16958 }
16959 }
16960 let error_str = error.to_string();
16961 let first_line = error_str.lines().next().unwrap_or("Unknown error").trim();
16962 format!("Export failed: {}", first_line)
16963 }
16964
16965 const COPY_CONFIRM_BYTES: usize = 10 * 1024 * 1024;
16968 const COPY_REFUSE_BYTES: usize = 200 * 1024 * 1024;
16971
16972 fn perform_copy(&mut self) -> Option<AppEvent> {
16975 use copy_modal::{CopyScope, thousands};
16976 enum Planned {
16979 Copy(clipboard::Payload, String),
16980 Collect,
16981 Confirm(Option<usize>),
16984 }
16985 let format = self.copy_modal.format;
16986 let header = self.copy_modal.header();
16987 let scope = self.copy_modal.scope;
16988 let accepts = match self.copy_destination() {
16991 Ok(destination) => destination.accepts(),
16992 Err(e) => {
16993 self.copy_modal.close();
16994 self.input_mode = InputMode::Normal;
16995 self.error_modal.show(e);
16996 return None;
16997 }
16998 };
16999 let planned: Result<Planned, String> = match self.data_table_state.as_ref() {
17000 None => Err("Nothing to copy: no table is open".to_string()),
17001 Some(state) => match scope {
17002 CopyScope::Cell => {
17003 let column = self.copy_modal.column.clone().unwrap_or_default();
17004 match state.copy_cell_value(&column) {
17005 Some(value) => {
17006 let row = state.selected_display_row().unwrap_or(0);
17007 Ok(Planned::Copy(
17008 clipboard::Payload::text(value),
17009 format!("Copied cell {column} of row {}", thousands(row)),
17010 ))
17011 }
17012 None => Err("Nothing to copy: the current row is not buffered".to_string()),
17013 }
17014 }
17015 CopyScope::Row => match state.copy_row_df() {
17016 Some(df) => clipboard::tabular_payload(&df, format, header, accepts.html).map(
17017 |payload| {
17018 let row = state.selected_display_row().unwrap_or(0);
17019 Planned::Copy(
17020 payload,
17021 format!("Copied row {} as {}", thousands(row), format.as_str()),
17022 )
17023 },
17024 ),
17025 None => Err("Nothing to copy: the current row is not buffered".to_string()),
17026 },
17027 CopyScope::View => match state.copy_view_df() {
17028 Some(df) => clipboard::tabular_payload(&df, format, header, accepts.html).map(
17029 |payload| {
17030 Planned::Copy(
17031 payload,
17032 format!(
17033 "Copied {} rows as {}",
17034 thousands(df.height()),
17035 format.as_str()
17036 ),
17037 )
17038 },
17039 ),
17040 None => Err("Nothing to copy: no rows are on screen".to_string()),
17041 },
17042 CopyScope::Python => Ok(Planned::Copy(
17043 clipboard::Payload::text(self.python_script(state)),
17044 "Copied the view as Python".to_string(),
17045 )),
17046 CopyScope::Table => {
17047 let cap = accepts
17050 .base64_limit
17051 .map_or(usize::MAX, |limit| limit / 4 * 3);
17052 match state.estimated_copy_bytes() {
17053 Some(bytes) if bytes > Self::COPY_REFUSE_BYTES => Err(format!(
17054 "The table is about {} — too much to hold on a clipboard. \
17055 Export it to a file instead (e).",
17056 Self::format_bytes(bytes as u64)
17057 )),
17058 Some(bytes) if bytes.min(cap) > Self::COPY_CONFIRM_BYTES => {
17059 Ok(Planned::Confirm(Some(bytes)))
17060 }
17061 Some(_) => Ok(Planned::Collect),
17062 None if cap <= Self::COPY_CONFIRM_BYTES => Ok(Planned::Collect),
17063 None => Ok(Planned::Confirm(None)),
17067 }
17068 }
17069 },
17070 };
17071 self.copy_modal.close();
17072 self.input_mode = InputMode::Normal;
17073 match planned {
17074 Ok(Planned::Copy(payload, message)) => {
17075 self.finish_copy(payload, message);
17076 None
17077 }
17078 Ok(Planned::Collect) => Some(AppEvent::CopyTable { format, header }),
17079 Ok(Planned::Confirm(bytes)) => {
17080 self.pending_copy = Some((format, header));
17081 let counting = self
17082 .data_table_state
17083 .as_ref()
17084 .is_some_and(|state| state.num_rows_if_valid().is_none());
17085 self.confirmation_modal.show(match bytes {
17086 Some(bytes) => format!(
17087 "This copies about {} to the clipboard.\n\nCopy the whole table?",
17088 Self::format_bytes(bytes as u64)
17089 ),
17090 None if counting => "The table's size is not known yet — the row count \
17091 is still being read.\n\nCopy the whole table anyway?"
17092 .to_string(),
17093 None => "The size of the table's binary columns is not known.\n\n\
17094 Copy the whole table anyway?"
17095 .to_string(),
17096 });
17097 None
17098 }
17099 Err(message) => {
17100 self.error_modal.show(message);
17101 None
17102 }
17103 }
17104 }
17105
17106 pub fn python_script(&self, state: &DataTableState) -> String {
17109 let (paths, options) = match &self.opened {
17110 Some((paths, options)) => (Some(paths.as_slice()), options.clone()),
17111 None => (None, OpenOptions::default()),
17112 };
17113 let cloud = options.effective_cloud(&self.app_config.cloud);
17114 let mut options = options;
17115 if options.read_python.is_empty() {
17116 options.read_python = state.read_python().to_vec();
17118 }
17119 let remote = paths
17121 .and_then(|paths| paths.first())
17122 .map(|p| p.to_string_lossy().into_owned())
17123 .filter(|p| source::is_remote_url(Path::new(p)));
17124 let source_of = remote
17125 .as_deref()
17126 .and_then(|url| source::split_source_id(url).0)
17127 .and_then(|id| cloud.connections.iter().find(|c| c.name == id));
17128 #[cfg(feature = "cloud")]
17130 let unsigned = remote
17131 .as_deref()
17132 .and_then(crate::cloud_sources::known_access)
17133 .unwrap_or(false);
17134 #[cfg(not(feature = "cloud"))]
17135 let unsigned = false;
17136 let record = python_script::OpenRecord {
17137 paths,
17138 options: &options,
17139 format: state.read_as().or(options.format),
17140 read_mode: state.read_mode(),
17141 schema: state.source_schema(),
17142 remote_objects: state
17143 .remote_objects()
17144 .unwrap_or_default()
17145 .into_iter()
17146 .map(|object| object.url)
17147 .collect(),
17148 s3_endpoint: source_of
17149 .and_then(|c| c.endpoint_url.clone())
17150 .or(cloud.s3_endpoint_url.clone())
17151 .filter(|s| !s.trim().is_empty()),
17152 s3_region: source_of
17153 .and_then(|c| c.region.clone())
17154 .or(cloud.s3_region.clone())
17155 .filter(|s| !s.trim().is_empty()),
17156 unsigned,
17157 read_as_text: state.read_as_text().iter().map(|c| c.to_string()).collect(),
17158 spec: state.format_read().map(|read| read.spec.name.clone()),
17159 };
17160 python_script::Script {
17161 source: python_script::source(&record),
17162 steps: state.python_steps(),
17163 }
17164 .render()
17165 }
17166
17167 fn finish_copy(&mut self, payload: clipboard::Payload, message: String) {
17171 let written = self
17172 .copy_destination()
17173 .and_then(|destination| destination.write(payload));
17174 match written {
17175 Ok(()) => self.flash_note(message),
17176 Err(e) => self.error_modal.show(e),
17177 }
17178 }
17179
17180 fn copy_destination(&mut self) -> Result<&mut dyn clipboard::Destination, String> {
17182 if self.clipboard.is_none() {
17183 let choice = clipboard::BackendChoice::parse(&self.app_config.clipboard.backend)
17184 .unwrap_or_default();
17185 let limit = usize::try_from(self.app_config.clipboard.osc52_limit.bytes())
17186 .unwrap_or(usize::MAX);
17187 self.clipboard = Some(clipboard::destination(choice, limit)?);
17188 }
17189 Ok(self
17190 .clipboard
17191 .as_deref_mut()
17192 .expect("destination just built"))
17193 }
17194
17195 pub fn enter_inspects(&self) -> bool {
17199 self.input_mode == InputMode::Normal
17200 && self
17201 .data_table_state
17202 .as_ref()
17203 .is_some_and(|state| !state.can_drill_down())
17204 }
17205
17206 fn open_inspector(&mut self) {
17209 let Some(state) = self.data_table_state.as_ref() else {
17210 return;
17211 };
17212 if state.inspect_row().is_none() {
17213 self.flash_note("No row to inspect".to_string());
17214 return;
17215 }
17216 self.inspector_modal
17217 .open(state.inspect_fields(), state.current_column());
17218 self.input_mode = InputMode::Inspect;
17219 }
17220
17221 fn open_value_counts(&mut self) {
17223 let Some(state) = self.data_table_state.as_ref() else {
17224 return;
17225 };
17226 let names = state.get_column_order().to_vec();
17227 let Some(at) = state
17228 .current_column()
17229 .and_then(|current| names.iter().position(|n| n == current))
17230 else {
17231 return;
17232 };
17233 self.value_counts.open(names, at, state.len_generation());
17234 self.input_mode = InputMode::ValueCounts;
17235 self.count_values(false);
17236 }
17237
17238 pub(crate) fn value_counts_shown(&self) -> bool {
17241 self.input_mode == InputMode::ValueCounts
17242 || (self.input_mode == InputMode::Export && self.export_counts.is_some())
17243 }
17244
17245 pub(crate) fn value_counts_computing(&self) -> bool {
17247 self.input_mode == InputMode::ValueCounts && self.value_counts.computing.is_some()
17248 }
17249
17250 fn export_returns_to(&self) -> InputMode {
17252 if self.export_counts.is_some() {
17253 InputMode::ValueCounts
17254 } else {
17255 InputMode::Normal
17256 }
17257 }
17258
17259 fn count_values(&mut self, exact: bool) {
17263 let Some(column) = self.value_counts.column().map(str::to_string) else {
17264 return;
17265 };
17266 let held_sample = self.value_counts.current().map(|c| c.is_sample());
17267 if held_sample == Some(false) || (held_sample == Some(true) && !exact) {
17268 return;
17269 }
17270 if self
17271 .value_counts
17272 .computing
17273 .as_ref()
17274 .is_some_and(|c| c.column == column && (c.exact || !exact))
17275 {
17276 return;
17277 }
17278 self.stop_value_count();
17279 let Some(state) = self.data_table_state.as_ref() else {
17280 return;
17281 };
17282 let read = if exact {
17283 value_counts::Read::Exact
17284 } else {
17285 value_counts::Read::Quick {
17286 sample_rows: self.app_config.analysis.sample_rows,
17287 seed: self.analysis_modal.sample.seed,
17288 remote: state.is_remote_source(),
17289 }
17290 };
17291 let plan = value_counts::Plan {
17292 lf: state.analysis_lf(),
17293 column: column.clone(),
17294 read,
17295 known_total: state.num_rows_if_valid(),
17296 streaming: state.polars_streaming(),
17297 };
17298 let watch = crate::sampling::ReadWatch::default();
17299 self.value_counts.failed = None;
17300 self.value_counts.computing = Some(value_counts_modal::Computing {
17301 column,
17302 exact,
17303 watch: watch.clone(),
17304 file_starts: state.file_row_starts().map(Arc::new),
17305 });
17306 self.spawn_job(Job::ValueCounts, None, move |_| {
17307 plan.run(&watch)
17308 .map(|counts| Answer::ValueCounts(Box::new(counts)))
17309 .map_err(|e| crate::error_display::user_message_from_report(&e, None))
17310 });
17311 }
17312
17313 fn stop_value_count(&mut self) {
17316 if let Some(computing) = self.value_counts.computing.take() {
17317 computing.watch.stop();
17318 self.jobs.cancel(|job| matches!(job, Job::ValueCounts));
17319 }
17320 }
17321
17322 fn value_counts_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17324 if !event.is_press() {
17325 return None;
17326 }
17327 let page = self.value_counts_page() as isize;
17328 let listing = !self.value_counts.shows_histogram();
17330 match event.code {
17331 KeyCode::Esc => {
17334 let counting = self.value_counts.counting();
17335 self.stop_value_count();
17336 if counting && self.value_counts.current().is_some() {
17337 self.flash_note("Count stopped".to_string());
17338 } else {
17339 self.input_mode = InputMode::Normal;
17340 }
17341 }
17342 KeyCode::Down | KeyCode::Char('j') if listing => self.value_counts.move_by(1),
17343 KeyCode::Up | KeyCode::Char('k') if listing => self.value_counts.move_by(-1),
17344 KeyCode::PageDown if listing => self.value_counts.move_by(page),
17345 KeyCode::PageUp if listing => self.value_counts.move_by(-page),
17346 KeyCode::Home if listing => self.value_counts.move_to_start(),
17347 KeyCode::End | KeyCode::Char('G') if listing => self.value_counts.move_to_end(),
17348 KeyCode::Left | KeyCode::Right | KeyCode::Char('h') | KeyCode::Char('l') => {
17349 let by = if matches!(event.code, KeyCode::Left | KeyCode::Char('h')) {
17350 -1
17351 } else {
17352 1
17353 };
17354 if self.value_counts.step(by) {
17355 if let (Some(state), Some(column)) =
17357 (self.data_table_state.as_mut(), self.value_counts.column())
17358 {
17359 state.set_current_column(column);
17360 }
17361 self.count_values(false);
17362 }
17363 }
17364 KeyCode::Char('s') if listing => self.value_counts.toggle_order(),
17365 KeyCode::Char('c') => self.value_counts.toggle_view(),
17366 KeyCode::Char('a') => {
17367 if self.value_counts.current().is_some_and(|c| c.is_sample()) {
17368 self.count_values(true);
17369 }
17370 }
17371 KeyCode::Enter if listing => self.drill_into_counted_value(),
17372 KeyCode::Char('y') => self.copy_value_counts(),
17373 KeyCode::Char('e') => self.export_value_counts(),
17374 KeyCode::Char('t') if self.follow_rows_waiting() => {
17376 self.stop_value_count();
17377 self.take_follow_rows(false);
17378 if let Some(state) = self.data_table_state.as_ref() {
17379 let names = self.value_counts.columns.clone();
17380 let at = self.value_counts.at;
17381 self.value_counts.open(names, at, state.len_generation());
17382 }
17383 self.count_values(false);
17384 }
17385 _ => {}
17386 }
17387 None
17388 }
17389
17390 fn value_counts_page(&self) -> usize {
17392 self.value_counts.page.max(1)
17393 }
17394
17395 fn drill_into_counted_value(&mut self) {
17397 let Some(kind) = self.value_counts.selected_kind() else {
17398 return;
17399 };
17400 let (Some(counts), Some(column)) = (
17401 self.value_counts.current().cloned(),
17402 self.value_counts.column().map(str::to_string),
17403 ) else {
17404 return;
17405 };
17406 let value = match kind {
17407 value_counts::LineKind::Value(at) => match counts.value(at) {
17408 Ok(value) => value,
17409 Err(_) => return,
17410 },
17411 value_counts::LineKind::Null => polars::prelude::AnyValue::Null,
17412 value_counts::LineKind::Other(_) => {
17413 self.flash_note("Other is many values: pick one to see its rows".to_string());
17414 return;
17415 }
17416 };
17417 let Some(state) = self.data_table_state.as_mut() else {
17418 return;
17419 };
17420 let nested = state.is_drilled_down();
17421 match state.deferred(|s| s.drill_into_value(&column, value)) {
17422 Ok(()) => {
17423 self.stop_value_count();
17424 self.value_counts.drill_return = !nested;
17427 self.input_mode = InputMode::Normal;
17428 self.sync_sort_filter_modal();
17429 self.spawn_async_collect(Self::LOADING_BUFFER);
17430 }
17431 Err(e) => self.flash_note(format!(
17432 "Could not drill in: {}",
17433 crate::error_display::user_message_from_report(&e, None)
17434 )),
17435 }
17436 }
17437
17438 fn copy_value_counts(&mut self) {
17440 let Some(counts) = self.value_counts.current().cloned() else {
17441 return;
17442 };
17443 let order = self.value_counts.order;
17444 let html = match self.copy_destination() {
17445 Ok(destination) => destination.accepts().html,
17446 Err(e) => {
17447 self.error_modal.show(e);
17448 return;
17449 }
17450 };
17451 self.spawn_job(Job::Copy, Some("Copying..."), move |_| {
17452 let table = counts
17453 .table(order)
17454 .map_err(|e| format!("Copy failed: {e}"))?;
17455 let payload =
17456 crate::clipboard::tabular_payload(&table, clipboard::CopyFormat::Tsv, true, html)
17457 .map_err(|e| format!("Copy failed: {e}"))?;
17458 Ok(Answer::Copied {
17459 payload,
17460 message: format!(
17461 "Copied {} values as TSV",
17462 copy_modal::thousands(table.height())
17463 ),
17464 })
17465 });
17466 }
17467
17468 fn export_value_counts(&mut self) {
17470 let Some(counts) = self.value_counts.current() else {
17471 return;
17472 };
17473 let table = match counts.table(self.value_counts.order) {
17474 Ok(table) => table,
17475 Err(e) => {
17476 self.error_modal
17477 .show(format!("Cannot export the counts: {e}"));
17478 return;
17479 }
17480 };
17481 self.export_modal.open(
17482 self.original_file_format,
17483 self.history_limit,
17484 &self.theme,
17485 self.original_file_delimiter,
17486 );
17487 let stem = self.dataset_stem();
17488 self.export_modal.suggest_path(&format!("{stem}-counts"));
17489 self.export_modal.offer_source_file = false;
17490 self.export_modal.nested_columns = false;
17491 self.export_modal.avro_renames = table
17492 .columns()
17493 .iter()
17494 .any(|c| crate::avro_types::renames(c.name(), c.dtype()));
17495 self.export_counts = Some(table);
17496 self.input_mode = InputMode::Export;
17497 }
17498
17499 fn open_go_to_column(&mut self) {
17502 let Some(state) = self.data_table_state.as_ref() else {
17503 return;
17504 };
17505 let names = state.get_column_order().to_vec();
17506 if names.is_empty() {
17507 return;
17508 }
17509 let at = state
17510 .current_column()
17511 .and_then(|current| names.iter().position(|n| n == current))
17512 .unwrap_or(0);
17513 self.go_to_column = crate::widgets::ui::PickerState::new(names);
17514 self.go_to_column.select_original(at);
17515 self.input_mode = InputMode::GoToColumn;
17516 }
17517
17518 fn go_to_column_key(&mut self, event: &KeyEvent) {
17521 match event.code {
17522 KeyCode::Esc => self.input_mode = InputMode::Normal,
17523 KeyCode::Enter => {
17524 let Some(index) = self.go_to_column.selected_original() else {
17525 return;
17527 };
17528 let name = self.go_to_column.items()[index].clone();
17530 if let Some(state) = self.data_table_state.as_mut() {
17531 state.go_to_column(&name);
17532 }
17533 self.input_mode = InputMode::Normal;
17534 }
17535 KeyCode::Up => self.go_to_column.move_up(),
17536 KeyCode::Down => self.go_to_column.move_down(),
17537 KeyCode::Backspace => self.go_to_column.backspace(),
17538 KeyCode::Char(c) => self.go_to_column.filter_key(c, event.modifiers),
17539 _ => {}
17540 }
17541 }
17542
17543 fn open_format_picker(&mut self) {
17546 let Some(read) = self
17547 .data_table_state
17548 .as_ref()
17549 .and_then(|s| s.format_read())
17550 .cloned()
17551 else {
17552 return;
17554 };
17555 let mut names = vec![read.spec.name.clone()];
17556 names.extend(read.also.iter().cloned());
17557 for found in &self.formats.specs {
17558 if found.spec.layout == read.spec.layout
17559 && !found.spec.is_delimited()
17560 && !names.contains(&found.spec.name)
17561 {
17562 names.push(found.spec.name.clone());
17563 }
17564 }
17565 self.format_picker = crate::widgets::ui::PickerState::new(names);
17566 self.input_mode = InputMode::PickFormat;
17567 }
17568
17569 fn format_picker_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17572 match event.code {
17573 KeyCode::Esc => self.input_mode = InputMode::Normal,
17574 KeyCode::Enter => {
17575 let index = self.format_picker.selected_original()?;
17576 let name = self.format_picker.items()[index].clone();
17577 self.input_mode = InputMode::Normal;
17578 let current = self
17579 .data_table_state
17580 .as_ref()
17581 .and_then(|s| s.format_read())
17582 .map(|read| read.spec.name.clone());
17583 if current.as_deref() == Some(name.as_str()) {
17584 return None;
17585 }
17586 let (paths, options) = self.opened.clone()?;
17587 let options = OpenOptions {
17588 spec_name: Some(name),
17589 spec_file: None,
17590 spec_fetched: None,
17591 table: None,
17592 format_read: None,
17593 sqlite: None,
17594 format: None,
17595 ..options
17596 };
17597 self.set_loading_phase("Scanning input", 10);
17598 self.name_what_is_loading(paths[0].clone());
17599 return Some(AppEvent::Open(paths, options));
17600 }
17601 KeyCode::Up => self.format_picker.move_up(),
17602 KeyCode::Down => self.format_picker.move_down(),
17603 KeyCode::Backspace => self.format_picker.backspace(),
17604 KeyCode::Char(c) => self.format_picker.filter_key(c, event.modifiers),
17605 _ => {}
17606 }
17607 None
17608 }
17609
17610 pub fn sibling_tables(&self) -> Option<table_switch::Tables> {
17613 let state = self.data_table_state.as_ref()?;
17614 let (paths, options) = self.opened.as_ref()?;
17615 table_switch::of(state, paths, options)
17616 }
17617
17618 pub fn offers_other_tables(&self) -> bool {
17620 match (self.data_table_state.as_ref(), self.opened.as_ref()) {
17621 (Some(state), Some((paths, options))) => table_switch::several(state, paths, options),
17622 _ => false,
17623 }
17624 }
17625
17626 fn open_table_picker(&mut self) {
17629 let Some(tables) = self.sibling_tables().filter(table_switch::Tables::several) else {
17630 self.flash_note("Only one table here".to_string());
17631 return;
17632 };
17633 let labels = tables.tables.iter().map(|t| t.label.clone()).collect();
17634 self.table_picker = crate::widgets::ui::PickerState::new(labels);
17635 if let Some(at) = tables.current {
17636 self.table_picker.select_original(at);
17637 }
17638 self.table_choices = Some(tables);
17639 self.input_mode = InputMode::PickTable;
17640 }
17641
17642 fn table_picker_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17645 match event.code {
17646 KeyCode::Esc => {
17647 self.input_mode = InputMode::Normal;
17648 self.table_choices = None;
17649 }
17650 KeyCode::Enter => {
17651 let index = self.table_picker.selected_original()?;
17652 let tables = self.table_choices.take()?;
17653 self.input_mode = InputMode::Normal;
17654 if tables.current == Some(index) {
17655 return None;
17656 }
17657 let table = tables.tables.get(index)?.table.clone();
17658 return self.switch_table(table);
17659 }
17660 KeyCode::Up => self.table_picker.move_up(),
17661 KeyCode::Down => self.table_picker.move_down(),
17662 KeyCode::Backspace => self.table_picker.backspace(),
17663 KeyCode::Char(c) => self.table_picker.filter_key(c, event.modifiers),
17664 _ => {}
17665 }
17666 None
17667 }
17668
17669 pub(crate) fn switch_table(&mut self, table: Option<String>) -> Option<AppEvent> {
17673 let (paths, options) = self.opened.clone()?;
17674 let shown = match &table {
17675 Some(name) => crate::members::place(&paths[0], name),
17676 None => paths[0].clone(),
17677 };
17678 let options = OpenOptions {
17679 table,
17680 view: None,
17683 prepared: None,
17684 ..options
17685 };
17686 self.set_loading_phase("Scanning input", 10);
17687 self.name_what_is_loading(shown);
17688 Some(AppEvent::Open(paths, options))
17689 }
17690
17691 fn close_inspector(&mut self) {
17692 self.inspector_modal.close();
17693 self.input_mode = InputMode::Normal;
17694 }
17695
17696 fn drill_selected_row(&mut self) {
17699 let Some(state) = self.data_table_state.as_ref() else {
17700 return;
17701 };
17702 let drill = state
17704 .table_state
17705 .selected()
17706 .map(|selected| state.start_row() + selected)
17707 .and_then(|index| Some((index, state.drill_row(index)?)));
17708 match drill {
17709 None => self.flash_note("No group to drill down into".to_string()),
17710 Some((group_index, DrillRow::Buffered(row))) => self.drill_into(group_index, &row),
17711 Some((group_index, DrillRow::Read(lf))) => {
17712 let streaming = state.polars_streaming();
17713 self.spawn_job(Job::DrillRow, Some(Self::READING_GROUP), move |_| {
17714 let row = crate::statistics::collect_lazy(*lf, streaming)
17715 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
17716 Ok(Answer::DrillRow { group_index, row })
17717 });
17718 }
17719 }
17720 }
17721
17722 fn refresh_inspector_list(&mut self) {
17725 if let Some(state) = self.data_table_state.as_ref() {
17726 let visible = crate::widgets::inspector::visible_fields(&self.inspector_modal, state);
17727 self.inspector_modal.set_visible(visible);
17728 }
17729 }
17730
17731 fn inspector_pane(&self) -> Option<crate::widgets::inspector::Pane> {
17735 let modal = &self.inspector_modal;
17736 if modal.drill.is_some() {
17737 return modal.pane.as_ref().map(|(_, pane)| pane.clone());
17738 }
17739 let state = self.data_table_state.as_ref()?;
17740 let row = state.inspect_row()?;
17741 let field = modal.focused()?;
17742 if let Some(pane) = modal.pane_for(row.frame, row.row, &field.name) {
17743 return Some(pane.clone());
17744 }
17745 let shown = crate::widgets::inspector::shown(field, &row, modal.read.as_ref(), state);
17746 Some(crate::widgets::inspector::pane(
17747 &field.dtype,
17748 &shown,
17749 &crate::widgets::inspector::PaneAsk {
17750 choice: modal.view,
17751 width: modal
17752 .pane
17753 .as_ref()
17754 .map_or(80, |(key, _)| key.width as usize),
17755 table: None,
17756 indented: crate::widgets::inspector::Indented::None,
17757 not_json: modal.known_not_json(row.frame, row.row, &field.name),
17758 unpacked: modal.unpacked(&(row.frame, row.row, field.name.clone())),
17759 read_key: "Enter",
17760 },
17761 ))
17762 }
17763
17764 fn inspector_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17767 if !event.is_press() {
17768 return None;
17769 }
17770 let modal = &mut self.inspector_modal;
17771 if modal.finding {
17772 match event.code {
17773 KeyCode::Esc => modal.clear_find(),
17774 KeyCode::Enter | KeyCode::Tab | KeyCode::Down => modal.finding = false,
17775 KeyCode::Up => {
17776 modal.finding = false;
17777 self.refresh_inspector_list();
17778 self.inspector_modal.prev_field();
17779 return None;
17780 }
17781 KeyCode::Backspace => modal.find_backspace(),
17782 KeyCode::Char(c) => modal.find_key(c, event.modifiers),
17783 _ => {}
17784 }
17785 self.refresh_inspector_list();
17788 return None;
17789 }
17790 if modal.value_find.as_ref().is_some_and(|f| f.editing) {
17791 self.value_find_key(event);
17792 return None;
17793 }
17794 if event
17795 .modifiers
17796 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
17797 {
17798 return None;
17799 }
17800 if modal.focus == inspector_modal::Focus::Value {
17801 return self.inspector_value_key(event);
17802 }
17803 if modal.drill.is_some() {
17804 return self.drill_key(event);
17805 }
17806 self.refresh_inspector_list();
17807 let modal = &mut self.inspector_modal;
17808 match event.code {
17809 KeyCode::Esc if !modal.filter.is_empty() => modal.clear_find(),
17812 KeyCode::Esc if modal.compare => {
17813 modal.compare = false;
17814 modal.filled_only = false;
17815 }
17816 KeyCode::Esc | KeyCode::Char(' ') => self.close_inspector(),
17817 KeyCode::Down | KeyCode::Char('j') => modal.next_field(),
17818 KeyCode::Up | KeyCode::Char('k') => modal.prev_field(),
17819 KeyCode::Home => modal.first_field(),
17820 KeyCode::End => modal.last_field(),
17821 KeyCode::PageDown => modal.page_fields(1),
17822 KeyCode::PageUp => modal.page_fields(-1),
17823 KeyCode::Tab | KeyCode::BackTab => {
17825 if modal.focused().is_some() {
17826 modal.focus = inspector_modal::Focus::Value;
17827 }
17828 }
17829 KeyCode::Char('/') => modal.finding = true,
17830 KeyCode::Char('e') => self.inspector_view(),
17831 KeyCode::Char('w') => self.inspector_wrap(),
17832 KeyCode::Char('f') => {
17833 modal.filled_only = !modal.filled_only;
17834 modal.list_offset = 0;
17835 }
17836 KeyCode::Char('s') => {
17837 modal.order = modal.order.next();
17838 modal.list_offset = 0;
17839 }
17840 KeyCode::Char('c') => {
17841 modal.compare = !modal.compare;
17842 if !modal.compare {
17843 modal.filled_only = false;
17844 }
17845 }
17846 KeyCode::Char('m') => self.toggle_inspector_pin(),
17847 KeyCode::Right | KeyCode::Char('l') => return self.step_row(1),
17848 KeyCode::Left | KeyCode::Char('h') => return self.step_row(-1),
17849 KeyCode::Char('y') => self.copy_inspected_field(),
17850 KeyCode::Char('Y') => self.copy_inspected_row(),
17851 KeyCode::Char('o') => self.open_inspected_value(),
17852 KeyCode::Char('r') => self.read_focused_field(),
17853 KeyCode::Enter => return self.inspector_enter(),
17854 _ => {}
17855 }
17856 None
17857 }
17858
17859 fn inspector_value_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17861 let Some(pane) = self.inspector_pane() else {
17862 self.inspector_modal.focus = inspector_modal::Focus::List;
17863 return None;
17864 };
17865 let modal = &mut self.inspector_modal;
17866 let h = modal.page.max(1);
17867 let content = &pane.content;
17868 let page = h.saturating_sub(1).max(1) as isize;
17869 match event.code {
17870 KeyCode::Esc
17871 if modal
17872 .value_find
17873 .as_ref()
17874 .is_some_and(|f| !f.text.is_empty()) =>
17875 {
17876 modal.value_find = None;
17877 }
17878 KeyCode::Esc | KeyCode::Tab | KeyCode::BackTab => {
17879 modal.focus = inspector_modal::Focus::List;
17880 }
17881 KeyCode::Char(' ') => self.close_inspector(),
17882 KeyCode::Down | KeyCode::Char('j') => modal.reader.scroll(content, h, 1),
17883 KeyCode::Up | KeyCode::Char('k') => modal.reader.scroll(content, h, -1),
17884 KeyCode::PageDown => modal.reader.scroll(content, h, page),
17885 KeyCode::PageUp => modal.reader.scroll(content, h, -page),
17886 KeyCode::Home => modal.reader.home(),
17887 KeyCode::End => modal.reader.end(content, h),
17888 KeyCode::Char('/') => {
17889 modal.value_find = Some(inspector_modal::ValueFind {
17890 editing: true,
17891 pane: pane.id,
17892 ..Default::default()
17893 });
17894 }
17895 KeyCode::Char('n') => self.next_value_hit(1),
17896 KeyCode::Char('N') => self.next_value_hit(-1),
17897 KeyCode::Char('e') => self.inspector_view(),
17898 KeyCode::Char('w') => self.inspector_wrap(),
17899 KeyCode::Char('y') => {
17900 if modal.drill.is_some() {
17901 self.copy_drilled_item();
17902 } else {
17903 self.copy_inspected_field();
17904 }
17905 }
17906 KeyCode::Char('o') if modal.drill.is_none() => self.open_inspected_value(),
17907 KeyCode::Right | KeyCode::Char('l') if modal.drill.is_none() => {
17908 return self.step_row(1);
17909 }
17910 KeyCode::Left | KeyCode::Char('h') if modal.drill.is_none() => {
17911 return self.step_row(-1);
17912 }
17913 _ => {}
17914 }
17915 None
17916 }
17917
17918 fn value_find_key(&mut self, event: &KeyEvent) {
17921 let pane = self.inspector_pane();
17922 let modal = &mut self.inspector_modal;
17923 let Some(find) = modal.value_find.as_mut() else {
17924 return;
17925 };
17926 match event.code {
17927 KeyCode::Esc => modal.value_find = None,
17928 KeyCode::Enter => {
17929 find.editing = false;
17930 let Some(pane) = pane else {
17931 return;
17932 };
17933 find.hits = inspector_reader::find_hits(&pane.content, &find.text);
17934 find.pane = pane.id;
17935 let h = modal.page.max(1);
17936 let from = modal.reader.window(&pane.content, h).from;
17937 let at = find.hits.partition_point(|&p| p < from);
17938 find.current = (!find.hits.is_empty()).then(|| at % find.hits.len());
17939 if let Some(at) = find.current {
17940 let pos = find.hits[at];
17941 modal.reader.jump(&pane.content, h, pos);
17942 }
17943 }
17944 KeyCode::Backspace => {
17945 find.text.pop();
17946 }
17947 KeyCode::Char(c) => inspector_modal::edit_find(&mut find.text, c, event.modifiers),
17948 _ => {}
17949 }
17950 }
17951
17952 fn next_value_hit(&mut self, step: isize) {
17954 let Some(pane) = self.inspector_pane() else {
17955 return;
17956 };
17957 let modal = &mut self.inspector_modal;
17958 let Some(find) = modal.value_find.as_mut() else {
17959 return;
17960 };
17961 if find.pane != pane.id && !find.text.is_empty() {
17962 find.hits = inspector_reader::find_hits(&pane.content, &find.text);
17963 find.pane = pane.id;
17964 find.current = None;
17965 }
17966 let n = find.hits.len();
17967 if n == 0 {
17968 return;
17969 }
17970 let at = match find.current {
17971 Some(at) => (at as isize + step).rem_euclid(n as isize) as usize,
17972 None if step > 0 => 0,
17973 None => n - 1,
17974 };
17975 find.current = Some(at);
17976 let pos = find.hits[at];
17977 let h = modal.page.max(1);
17978 modal.reader.jump(&pane.content, h, pos);
17979 }
17980
17981 fn drill_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
17984 let modal = &mut self.inspector_modal;
17985 match event.code {
17986 KeyCode::Esc | KeyCode::Left | KeyCode::Char('h') => {
17987 modal.drill_out();
17988 }
17989 KeyCode::Char(' ') => self.close_inspector(),
17990 KeyCode::Down | KeyCode::Char('j') => modal.next_field(),
17991 KeyCode::Up | KeyCode::Char('k') => modal.prev_field(),
17992 KeyCode::Home => modal.first_field(),
17993 KeyCode::End => modal.last_field(),
17994 KeyCode::PageDown => modal.page_fields(1),
17995 KeyCode::PageUp => modal.page_fields(-1),
17996 KeyCode::Tab | KeyCode::BackTab
17998 if modal
17999 .drill
18000 .as_ref()
18001 .is_some_and(|drill| drill.level().focused().is_some()) =>
18002 {
18003 modal.focus = inspector_modal::Focus::Value;
18004 }
18005 KeyCode::Char('e') => self.inspector_view(),
18006 KeyCode::Char('w') => self.inspector_wrap(),
18007 KeyCode::Char('y') => self.copy_drilled_item(),
18008 KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => {
18009 let drill = modal.drill.as_ref()?;
18010 let (frame, row) = (drill.frame, drill.row);
18011 let (label, node) = drill.level().focused()?;
18012 let path = drill.item_key(&label);
18013 if node.opens() && !modal.known_not_json(frame, row, &path) {
18014 self.inspector_open(frame, row, label, path, node);
18015 }
18016 }
18017 _ => {}
18018 }
18019 None
18020 }
18021
18022 fn inspector_view(&mut self) {
18025 if let Some(view) = self.inspector_pane().and_then(|pane| pane.next_view()) {
18026 self.inspector_modal.choose_view(view);
18027 }
18028 }
18029
18030 fn inspector_wrap(&mut self) {
18032 let modal = &mut self.inspector_modal;
18033 modal.wrap = match modal.wrap {
18034 inspector_reader::Wrap::Word => inspector_reader::Wrap::Hard,
18035 inspector_reader::Wrap::Hard => inspector_reader::Wrap::Word,
18036 };
18037 }
18038
18039 fn toggle_inspector_pin(&mut self) {
18041 let Some(row) = self.data_table_state.as_ref().and_then(|s| s.inspect_row()) else {
18042 return;
18043 };
18044 let modal = &mut self.inspector_modal;
18045 let here = modal
18046 .pinned
18047 .as_ref()
18048 .is_some_and(|p| (p.frame, p.row) == (row.frame, row.row));
18049 if here {
18050 modal.pinned = None;
18051 self.flash_note("Unpinned".to_string());
18052 } else {
18053 let n = row.display_row;
18054 modal.pinned = Some(row);
18055 modal.compare = true;
18056 self.flash_note(format!(
18057 "Pinned row {}; Compare shows it",
18058 copy_modal::thousands(n)
18059 ));
18060 }
18061 }
18062
18063 fn inspector_enter(&mut self) -> Option<AppEvent> {
18066 let state = self.data_table_state.as_ref()?;
18067 if state.can_drill_down() {
18068 self.close_inspector();
18069 self.drill_selected_row();
18070 return None;
18071 }
18072 let row = state.inspect_row()?;
18073 let field = self.inspector_modal.focused().cloned()?;
18074 let shown = crate::widgets::inspector::shown(
18075 &field,
18076 &row,
18077 self.inspector_modal.read.as_ref(),
18078 state,
18079 );
18080 use crate::widgets::inspector::Shown;
18081 match shown {
18082 Shown::Unread | Shown::Failed(_) => self.read_focused_field(),
18084 Shown::Value(ref v)
18085 if crate::widgets::inspector::value_opens(v)
18086 && !self
18087 .inspector_modal
18088 .known_not_json(row.frame, row.row, &field.name) =>
18089 {
18090 let column = if field.buffered() {
18091 row.values.column(&field.name).ok()
18092 } else {
18093 self.inspector_modal
18094 .read_values(row.frame, row.row)
18095 .and_then(|values| values.column(&field.name).ok())
18096 };
18097 if let Some(column) = column {
18098 let node =
18099 inspector_drill::Node::Native(column.as_materialized_series().clone());
18100 let path = inspector_drill::path_key([field.name.as_str()]);
18101 self.inspector_open(row.frame, row.row, field.name.clone(), path, node);
18102 }
18103 }
18104 _ => {}
18105 }
18106 None
18107 }
18108
18109 fn read_focused_field(&mut self) {
18112 let Some(state) = self.data_table_state.as_ref() else {
18113 return;
18114 };
18115 let Some(row) = state.inspect_row() else {
18116 return;
18117 };
18118 let Some(field) = self.inspector_modal.focused().cloned() else {
18119 return;
18120 };
18121 let shown = crate::widgets::inspector::shown(
18122 &field,
18123 &row,
18124 self.inspector_modal.read.as_ref(),
18125 state,
18126 );
18127 if matches!(
18128 shown,
18129 crate::widgets::inspector::Shown::Unread | crate::widgets::inspector::Shown::Failed(_)
18130 ) {
18131 self.inspector_modal.follow = Some(field.name.clone());
18132 self.read_inspected_fields(&row, true);
18133 }
18134 }
18135
18136 fn inspector_needs(&mut self) {
18141 if self.input_mode != InputMode::Inspect || !self.inspector_modal.active {
18142 return;
18143 }
18144 let Some(state) = self.data_table_state.as_ref() else {
18145 return;
18146 };
18147 let Some(row) = state.inspect_row() else {
18148 return;
18149 };
18150 let modal = &self.inspector_modal;
18151 if modal.drill.is_some() {
18152 return;
18153 }
18154 let Some(field) = modal.focused().cloned() else {
18155 return;
18156 };
18157 let read_here = modal
18158 .read
18159 .as_ref()
18160 .is_some_and(|r| r.key() == (row.frame, row.row));
18161 if modal.follow.as_deref() == Some(field.name.as_str()) && !field.buffered() && !read_here {
18162 self.read_inspected_fields(&row, false);
18163 return;
18164 }
18165 let pane = modal.pane_for(row.frame, row.row, &field.name);
18168 let place = (row.frame, row.row, field.name.clone());
18169 let indent = pane.is_some_and(|pane| pane.indent)
18170 && !modal.pretty.as_ref().is_some_and(|p| *p.place() == place);
18171 let unpack = pane.is_some_and(|pane| pane.unpack)
18172 && !modal.unpack.as_ref().is_some_and(|u| *u.place() == place);
18173 if !indent && !unpack {
18174 return;
18175 }
18176 let column = if field.buffered() {
18177 row.values.column(&field.name).ok().cloned()
18178 } else {
18179 modal
18180 .read_values(row.frame, row.row)
18181 .and_then(|values| values.column(&field.name).ok().cloned())
18182 };
18183 let Some(column) = column else {
18184 return;
18185 };
18186 let modal = &mut self.inspector_modal;
18187 if unpack {
18188 modal.unpack_token += 1;
18189 let token = modal.unpack_token;
18190 modal.unpack = Some(inspector_modal::Unpack::Pending { token, place });
18191 self.spawn_job(Job::InspectUnpack { token }, None, move |_| {
18192 let value = column.get(0).map_err(|e| e.to_string())?;
18193 let bytes = match &value {
18194 polars::prelude::AnyValue::Binary(b) => *b,
18195 polars::prelude::AnyValue::BinaryOwned(b) => b.as_slice(),
18196 _ => return Err("not bytes".to_string()),
18197 };
18198 inspector_bytes::decode_text(bytes, inspector_bytes::sniff(bytes))
18199 .map(Answer::Unpacked)
18200 .ok_or_else(|| "not text".to_string())
18201 });
18202 return;
18203 }
18204 modal.pretty_token += 1;
18205 let token = modal.pretty_token;
18206 modal.pretty = Some(inspector_modal::Pretty::Pending { token, place });
18207 self.spawn_job(Job::InspectPretty { token }, None, move |_| {
18208 let value = column.get(0).map_err(|e| e.to_string())?;
18209 let text = match &value {
18210 polars::prelude::AnyValue::String(s) => *s,
18211 polars::prelude::AnyValue::StringOwned(s) => s.as_str(),
18212 _ => return Err("not text".to_string()),
18213 };
18214 let json = inspector_drill::parse_json(text)?;
18215 let (pretty, _) = inspector_drill::json_text(&json, true, usize::MAX);
18216 Ok(Answer::Indented(std::sync::Arc::from(pretty)))
18217 });
18218 }
18219
18220 pub fn follow_terminal_background(&mut self, mode: ThemeMode) {
18224 let theme = &self.app_config.theme;
18225 if !theme.follow || theme.mode == Some(mode) {
18226 return;
18227 }
18228 let mut next = theme.clone();
18229 let built = theme.palette_for(mode).and_then(|colors| {
18230 next.colors = colors;
18231 next.mode = Some(mode);
18232 Theme::from_config(&next)
18233 });
18234 match built {
18235 Ok(built) => {
18236 self.theme = built;
18237 self.chart_modal.series_cap = Some(self.theme.series_colors().len());
18238 self.app_config.theme = next;
18239 for input in [
18242 &mut self.query_input,
18243 &mut self.sql_input,
18244 &mut self.find.input,
18245 ] {
18246 *input = std::mem::take(input).with_theme(&self.theme);
18247 }
18248 }
18249 Err(e) => log::warn!("cannot switch to the {mode:?} palette: {e}"),
18252 }
18253 }
18254
18255 fn terminal_answered(&mut self, mode: ThemeMode) {
18258 if self.app_config.theme.follow {
18259 self.cache
18260 .remember_terminal_mode(&terminal_color::terminal_key(), mode);
18261 }
18262 self.follow_terminal_background(mode);
18263 }
18264
18265 pub fn settle_first_palette(&mut self, answered: Option<ThemeMode>) {
18269 if let Some(mode) = answered {
18270 self.terminal_answered(mode);
18271 } else if self.app_config.theme.follow
18272 && let Some(mode) = self.cache.terminal_mode(&terminal_color::terminal_key())
18273 {
18274 self.follow_terminal_background(mode);
18275 }
18276 }
18277
18278 pub fn take_background_query(&mut self) -> bool {
18281 std::mem::take(&mut self.background_query)
18282 }
18283
18284 pub fn theme(&self) -> &Theme {
18286 &self.theme
18287 }
18288
18289 pub fn follows_terminal(&self) -> bool {
18291 self.app_config.theme.follow
18292 }
18293
18294 pub fn take_external_open(&mut self) -> Option<external_open::ExternalOpen> {
18296 self.external_open.take()
18297 }
18298
18299 pub fn mouse_enabled(&self) -> bool {
18302 self.app_config.display.mouse
18303 }
18304
18305 pub fn external_opened(&mut self, open: &external_open::ExternalOpen, failed: Option<String>) {
18308 let program = external_open::program_for(open.document, |name| std::env::var(name).ok());
18309 if matches!(program, external_open::Program::Wait(_)) {
18310 let _ = std::fs::remove_file(&open.path);
18311 }
18312 match failed {
18313 Some(e) => self.flash_note(format!("Could not open the value: {e}")),
18314 None if matches!(program, external_open::Program::Opener(_)) => {
18315 self.flash_note("Opened in the system viewer".to_string())
18316 }
18317 None => {}
18318 }
18319 }
18320
18321 fn copy_inspected_field(&mut self) {
18327 use copy_modal::thousands;
18328 let Some(state) = self.data_table_state.as_ref() else {
18329 return;
18330 };
18331 let (Some(row), Some(field)) = (state.inspect_row(), self.inspector_modal.focused()) else {
18332 return;
18333 };
18334 let field = field.clone();
18335 let column = if field.buffered() {
18336 row.values.column(&field.name).ok().cloned()
18337 } else {
18338 self.inspector_modal
18339 .read_values(row.frame, row.row)
18340 .and_then(|values| values.column(&field.name).ok().cloned())
18341 };
18342 let Some(column) = column else {
18343 self.flash_note(format!("{} is not read yet; Enter reads it", field.name));
18344 return;
18345 };
18346 let message = format!(
18347 "Copied {} of row {}",
18348 field.name,
18349 thousands(row.display_row)
18350 );
18351 use crate::widgets::inspector::CopyAs;
18352 match self.inspector_pane().map(|pane| pane.copy) {
18353 Some(CopyAs::Text(text)) => self.copy_string(text.to_string(), message),
18354 Some(CopyAs::Escaped) => {
18355 let text = column
18356 .get(0)
18357 .map(|v| crate::exact::escaped(&crate::exact::value_text(&v)))
18358 .unwrap_or_default();
18359 self.copy_string(text, message);
18360 }
18361 _ => self.copy_value(column, message),
18362 }
18363 }
18364
18365 fn copy_string(&mut self, text: String, message: String) {
18367 let limit = match self.copy_destination() {
18368 Ok(destination) => destination.accepts().base64_limit,
18369 Err(e) => {
18370 self.error_modal.show(e);
18371 return;
18372 }
18373 };
18374 if let Some(limit) = limit
18375 && text.len() > limit / 4 * 3
18376 {
18377 self.error_modal
18378 .show(clipboard::over_osc52_limit(None, limit));
18379 return;
18380 }
18381 self.finish_copy(clipboard::Payload::text(text), message);
18382 }
18383
18384 fn copy_inspected_row(&mut self) {
18387 use copy_modal::thousands;
18388 let Some(state) = self.data_table_state.as_ref() else {
18389 return;
18390 };
18391 let Some(row) = state.inspect_row() else {
18392 return;
18393 };
18394 let fields = self.inspector_modal.fields.clone();
18395 let read = self.inspector_modal.read.clone();
18396 let display = row.display_row;
18397 let size = row.values.estimated_size()
18398 + self
18399 .inspector_modal
18400 .read_values(row.frame, row.row)
18401 .map_or(0, |v| v.estimated_size());
18402 let build = move || {
18403 let (json, kept, unread) =
18404 crate::widgets::inspector::row_json(&fields, &row, read.as_ref());
18405 let message = if unread > 0 {
18406 format!(
18407 "Copied row {}: {} fields, {} not read",
18408 thousands(display),
18409 thousands(kept),
18410 thousands(unread)
18411 )
18412 } else {
18413 format!("Copied row {} as JSON", thousands(display))
18414 };
18415 (json, message)
18416 };
18417 if size <= Self::FIELD_COPY_INLINE_BYTES {
18418 let (json, message) = build();
18419 self.copy_string(json, message);
18420 return;
18421 }
18422 let limit = match self.copy_destination() {
18423 Ok(destination) => destination.accepts().base64_limit,
18424 Err(e) => {
18425 self.error_modal.show(e);
18426 return;
18427 }
18428 };
18429 self.spawn_job(Job::Copy, Some("Copying..."), move |_| {
18430 let (json, message) = build();
18431 if let Some(limit) = limit
18432 && json.len() > limit / 4 * 3
18433 {
18434 return Err(clipboard::over_osc52_limit(None, limit));
18435 }
18436 Ok(Answer::Copied {
18437 payload: clipboard::Payload::text(json),
18438 message,
18439 })
18440 });
18441 }
18442
18443 fn open_inspected_value(&mut self) {
18446 let Some(state) = self.data_table_state.as_ref() else {
18447 return;
18448 };
18449 let Some(row) = state.inspect_row() else {
18450 return;
18451 };
18452 let Some(field) = self.inspector_modal.focused().cloned() else {
18453 return;
18454 };
18455 let column = if field.buffered() {
18456 row.values.column(&field.name).ok().cloned()
18457 } else {
18458 self.inspector_modal
18459 .read_values(row.frame, row.row)
18460 .and_then(|values| values.column(&field.name).ok().cloned())
18461 };
18462 let Some(column) = column else {
18463 self.flash_note(format!("{} is not read yet; Enter reads it", field.name));
18464 return;
18465 };
18466 if self.open_dir.is_none() {
18467 match tempfile::Builder::new().prefix("datui-values-").tempdir() {
18468 Ok(dir) => self.open_dir = Some(dir),
18469 Err(e) => {
18470 self.flash_note(format!("Could not open the value: {e}"));
18471 return;
18472 }
18473 }
18474 }
18475 let dir = self
18476 .open_dir
18477 .as_ref()
18478 .map(|d| d.path().to_path_buf())
18479 .unwrap_or_default();
18480 let shown_as = self.inspector_pane().map(|pane| pane.copy);
18481 let name = field.name.clone();
18482 let display = row.display_row;
18483 self.spawn_job(Job::OpenValue, Some("Writing the value..."), move |_| {
18484 use crate::widgets::inspector::CopyAs;
18485 let value = column.get(0).map_err(|e| e.to_string())?;
18486 let (bytes, extension, document): (Vec<u8>, &str, bool) = match (&shown_as, &value) {
18487 (Some(CopyAs::Text(text)), _) => {
18488 let ext = if inspector_drill::looks_like_json(text) {
18489 "json"
18490 } else {
18491 "txt"
18492 };
18493 (text.as_bytes().to_vec(), ext, false)
18494 }
18495 (_, polars::prelude::AnyValue::Binary(b)) => {
18496 let kind = inspector_bytes::sniff(b);
18497 (
18498 b.to_vec(),
18499 kind.map_or("bin", |k| k.extension()),
18500 kind.is_some_and(|k| k.is_document()),
18501 )
18502 }
18503 (_, polars::prelude::AnyValue::BinaryOwned(b)) => {
18504 let kind = inspector_bytes::sniff(b);
18505 (
18506 b.clone(),
18507 kind.map_or("bin", |k| k.extension()),
18508 kind.is_some_and(|k| k.is_document()),
18509 )
18510 }
18511 (_, v) if crate::exact::is_nested_value(v) => {
18512 let text = crate::exact::copy_text(&column).map_err(|e| e.to_string())?;
18513 (text.into_bytes(), "json", false)
18514 }
18515 (_, v) => {
18516 let text = crate::exact::value_text(v);
18517 let trimmed = text.trim_start();
18518 let ext = if inspector_drill::looks_like_json(&text) {
18519 "json"
18520 } else if trimmed.starts_with('<') {
18521 "xml"
18522 } else {
18523 "txt"
18524 };
18525 (text.into_bytes(), ext, false)
18526 }
18527 };
18528 let file = external_open::file_name(&name, display, extension);
18529 let path =
18530 external_open::write_value(&dir, &file, &bytes).map_err(|e| e.to_string())?;
18531 Ok(Answer::ValueWritten(external_open::ExternalOpen {
18532 path,
18533 document,
18534 }))
18535 });
18536 }
18537
18538 fn step_row(&mut self, delta: i64) -> Option<AppEvent> {
18541 let state = self.data_table_state.as_mut()?;
18542 if state.scroll_would_trigger_collect(delta) {
18543 self.busy = true;
18544 return Some(if delta > 0 {
18545 AppEvent::DoScrollNext
18546 } else {
18547 AppEvent::DoScrollPrev
18548 });
18549 }
18550 if delta > 0 {
18551 state.select_next();
18552 } else {
18553 state.select_previous();
18554 }
18555 None
18556 }
18557
18558 fn inspector_open(
18564 &mut self,
18565 frame: u64,
18566 row: usize,
18567 label: String,
18568 path: String,
18569 node: inspector_drill::Node,
18570 ) {
18571 use inspector_drill::{JSON_INLINE_BYTES, Node, Shape};
18572 if node.shape() != Shape::Leaf {
18573 self.inspector_modal.drill_in(frame, row, label, node);
18574 return;
18575 }
18576 let Some(len) = node
18577 .with_text(|s| inspector_drill::opens_as_json(s).then_some(s.len()))
18578 .flatten()
18579 else {
18580 return;
18581 };
18582 if len <= JSON_INLINE_BYTES {
18584 match node.with_text(inspector_drill::parse_json) {
18585 Some(Ok(value)) => {
18586 let node = Node::Json {
18587 root: std::sync::Arc::new(value),
18588 path: Vec::new(),
18589 };
18590 self.inspector_modal.drill_in(frame, row, label, node);
18591 }
18592 Some(Err(e)) => {
18593 self.inspector_modal.not_json = Some((frame, row, path));
18594 self.flash_note(sentence(&e));
18595 }
18596 None => {}
18597 }
18598 return;
18599 }
18600 let token = self.inspector_modal.wait_for_json(frame, row, label, path);
18601 self.spawn_job(
18604 Job::InspectJson { token },
18605 Some(Self::READING_JSON),
18606 move |_| match node.with_text(inspector_drill::parse_json) {
18607 Some(parsed) => parsed.map(|value| Answer::JsonParsed(std::sync::Arc::new(value))),
18608 None => Err("not JSON: not text".to_string()),
18609 },
18610 );
18611 }
18612
18613 fn copy_drilled_item(&mut self) {
18616 use inspector_drill::Node;
18617 let Some(drill) = self.inspector_modal.drill.as_ref() else {
18618 return;
18619 };
18620 let Some((label, node)) = drill.level().focused() else {
18621 return;
18622 };
18623 let g = crate::glyphs::get();
18624 let mut path: Vec<&str> = drill.levels.iter().map(|l| l.label.as_str()).collect();
18625 path.push(&label);
18626 let display_row = self
18627 .data_table_state
18628 .as_ref()
18629 .and_then(|s| s.inspect_row())
18630 .map_or(drill.row + 1, |r| r.display_row);
18631 let message = format!(
18632 "Copied {} of row {}",
18633 path.join(&format!(" {} ", g.trail)),
18634 copy_modal::thousands(display_row)
18635 );
18636 match node {
18637 Node::Native(series) => self.copy_value(polars::prelude::Column::from(series), message),
18638 Node::Json { .. } => {
18639 let limit = match self.copy_destination() {
18640 Ok(destination) => destination.accepts().base64_limit,
18641 Err(e) => {
18642 self.error_modal.show(e);
18643 return;
18644 }
18645 };
18646 let cap = limit.map_or(Self::JSON_COPY_MAX_BYTES, |limit| limit / 4 * 3);
18648 let quick = cap.min(Self::FIELD_COPY_INLINE_BYTES);
18649 let null = serde_json::Value::Null;
18650 let value = node.json().unwrap_or(&null);
18651 if let Some(text) = inspector_drill::json_copy_text(value, quick) {
18652 self.finish_copy(clipboard::Payload::text(text), message);
18653 return;
18654 }
18655 if let Some(limit) = limit.filter(|_| cap <= Self::FIELD_COPY_INLINE_BYTES) {
18656 self.error_modal
18657 .show(clipboard::over_osc52_limit(None, limit));
18658 return;
18659 }
18660 self.spawn_job(Job::Copy, Some("Copying..."), move |_| {
18662 let value = node.json().unwrap_or(&serde_json::Value::Null);
18663 let text =
18664 inspector_drill::json_copy_text(value, cap).ok_or_else(|| match limit {
18665 Some(limit) => clipboard::over_osc52_limit(None, limit),
18666 None => "Copy failed: the value is too large to copy".to_string(),
18667 })?;
18668 Ok(Answer::Copied {
18669 payload: clipboard::Payload::text(text),
18670 message,
18671 })
18672 });
18673 }
18674 }
18675 }
18676
18677 fn read_inspected_fields(&mut self, row: &crate::widgets::datatable::InspectRow, wait: bool) {
18683 let Some(state) = self.data_table_state.as_ref() else {
18684 return;
18685 };
18686 let fields = state.inspect_fields();
18687 let wanted: Vec<String> = fields
18688 .iter()
18689 .filter(|f| !f.buffered())
18690 .map(|f| f.name.clone())
18691 .collect();
18692 let check: Vec<String> = fields
18693 .iter()
18694 .filter(|f| f.buffered())
18695 .map(|f| f.name.clone())
18696 .collect();
18697 let columns: Vec<String> = wanted.iter().chain(check.iter()).cloned().collect();
18698 let lf = match state.inspect_read_lf(row.row, &columns) {
18699 Ok(lf) => lf,
18700 Err(e) => {
18701 self.inspector_modal.read = Some(inspector_modal::FieldRead::Failed {
18702 frame: row.frame,
18703 row: row.row,
18704 message: crate::error_display::user_message_from_polars(&e),
18705 });
18706 return;
18707 }
18708 };
18709 let expected = row.values.select(check.iter().map(String::as_str)).ok();
18710 let streaming = state.polars_streaming();
18711 let (frame, index) = (row.frame, row.row);
18712 self.inspector_modal.read = Some(inspector_modal::FieldRead::Reading { frame, row: index });
18713 self.spawn_job(
18714 Job::InspectRow { frame, row: index },
18715 wait.then_some(Self::READING_FIELDS),
18716 move |_| {
18717 let read = crate::statistics::collect_lazy(lf, streaming)
18718 .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
18719 if read.height() != 1 {
18720 return Err("the row is no longer in the view".to_string());
18721 }
18722 let same = expected.is_some_and(|expected| {
18723 read.select(check.iter().map(String::as_str))
18724 .is_ok_and(|again| again.equals_missing(&expected))
18725 });
18726 if !same {
18727 return Err("the view's order of equal rows changed on reading again; \
18728 sort by a column that tells the rows apart"
18729 .to_string());
18730 }
18731 let values = read
18732 .select(wanted.iter().map(String::as_str))
18733 .map_err(|e| e.to_string())?;
18734 Ok(Answer::FieldsRead(values))
18735 },
18736 );
18737 }
18738
18739 fn copy_value(&mut self, column: polars::prelude::Column, message: String) {
18741 let limit = match self.copy_destination() {
18744 Ok(destination) => destination.accepts().base64_limit,
18745 Err(e) => {
18746 self.error_modal.show(e);
18747 return;
18748 }
18749 };
18750 if let Some(limit) = limit {
18751 let fits = limit / 4 * 3;
18752 let over = column
18753 .get(0)
18754 .is_ok_and(|value| crate::exact::copy_len_floor(&value, fits) > fits);
18755 if over {
18756 self.error_modal
18757 .show(clipboard::over_osc52_limit(None, limit));
18758 return;
18759 }
18760 }
18761 if column.as_materialized_series().estimated_size() <= Self::FIELD_COPY_INLINE_BYTES {
18762 match crate::exact::copy_text(&column) {
18763 Ok(text) => self.finish_copy(clipboard::Payload::text(text), message),
18764 Err(e) => self.error_modal.show(format!("Copy failed: {e}")),
18765 }
18766 return;
18767 }
18768 self.spawn_job(Job::Copy, Some("Copying..."), move |_| {
18769 let text = crate::exact::copy_text(&column).map_err(|e| format!("Copy failed: {e}"))?;
18770 Ok(Answer::Copied {
18771 payload: clipboard::Payload::text(text),
18772 message,
18773 })
18774 });
18775 }
18776
18777 pub fn set_clipboard_destination(&mut self, destination: Box<dyn clipboard::Destination>) {
18780 self.clipboard = Some(destination);
18781 }
18782
18783 pub fn create_view_from_current_state(
18784 &mut self,
18785 name: String,
18786 description: Option<String>,
18787 match_criteria: view::MatchCriteria,
18788 ) -> Result<view::SavedView> {
18789 let settings = match &self.data_table_state {
18790 Some(state) => view::ViewSettings {
18791 chart: self.saved_chart(),
18792 ..view_settings_of(state)
18793 },
18794 None => view::ViewSettings {
18795 chart: None,
18796 sample: None,
18797 query: None,
18798 sql_query: None,
18799 fuzzy_query: None,
18800 filters: Vec::new(),
18801 sort_columns: Vec::new(),
18802 sort_descending: Vec::new(),
18803 sort_ascending: true,
18804 column_order: Vec::new(),
18805 locked_columns_count: 0,
18806 pivot: None,
18807 melt: None,
18808 reshape_source: None,
18809 columns: Vec::new(),
18810 },
18811 };
18812
18813 self.view_manager
18814 .create_view(name, description, match_criteria, settings)
18815 }
18816
18817 pub fn query_prompt_mode(&self) -> Option<QueryMode> {
18819 (self.input_mode == InputMode::Editing && self.input_type == Some(InputType::Query))
18820 .then_some(self.query_mode)
18821 }
18822
18823 pub(crate) fn open_command_line(&mut self) {
18826 let Some(state) = self.data_table_state.as_mut() else {
18827 return;
18828 };
18829 self.input_mode = InputMode::Editing;
18830 self.input_type = Some(InputType::Query);
18831 self.query_run_error = None;
18832 self.sql_completion = None;
18833 self.query_input.set_value(state.get_active_query());
18834 self.sql_input.set_value(state.get_active_sql_query());
18835 self.query_input.select_all();
18836 self.sql_input.select_all();
18837 state.suppress_error_display = true;
18838 self.sql_columns = state.sql_table_columns();
18839 self.query_mode = self.opening_query_mode();
18840 self.query_text_restored = !self.query_input_shown().is_empty();
18841 self.sync_query_focus();
18842 }
18843
18844 fn opening_query_mode(&self) -> QueryMode {
18847 let active = self.data_table_state.as_ref().and_then(|state| {
18848 if !state.get_active_sql_query().trim().is_empty() {
18849 Some(QueryMode::Sql)
18850 } else if !state.get_active_query().trim().is_empty() {
18851 Some(QueryMode::Q)
18852 } else {
18853 None
18854 }
18855 });
18856 active
18857 .or(self.query_mode_chosen)
18858 .unwrap_or(self.app_config.query.default_mode)
18859 .resolve()
18860 }
18861
18862 fn query_input_mut(&mut self) -> &mut TextInput {
18864 match self.query_mode {
18865 QueryMode::Sql => &mut self.sql_input,
18866 QueryMode::Q => &mut self.query_input,
18867 }
18868 }
18869
18870 pub(crate) fn query_input_shown(&self) -> &TextInput {
18872 match self.query_mode {
18873 QueryMode::Sql => &self.sql_input,
18874 QueryMode::Q => &self.query_input,
18875 }
18876 }
18877
18878 fn set_query_mode(&mut self, mode: QueryMode) {
18881 self.query_mode = mode.resolve();
18882 if let Some(state) = &mut self.data_table_state {
18883 state.dismiss_error();
18884 }
18885 self.query_run_error = None;
18886 self.sync_query_focus();
18887 }
18888
18889 fn complete_column_name(&mut self) {
18893 let sql = self.query_mode == QueryMode::Sql;
18894 let columns = std::mem::take(&mut self.sql_columns);
18895 let mut cycle = self.sql_completion.take();
18896 let input = self.query_input_mut();
18897 let line = input
18898 .line_at(input.cursor_line())
18899 .unwrap_or_default()
18900 .to_string();
18901 let value = input.value().to_string();
18902 let complete = if sql {
18903 sql_assist::tab
18904 } else {
18905 sql_assist::q_tab
18906 };
18907 if let Some(step) = complete(
18908 &columns,
18909 &line,
18910 input.cursor_col(),
18911 &value,
18912 input.cursor(),
18913 &mut cycle,
18914 ) {
18915 input.replace_before_cursor(step.span, &step.insert);
18916 sql_assist::landed(&mut cycle, input.value(), input.cursor());
18917 }
18918 self.sql_completion = cycle;
18919 self.sql_columns = columns;
18920 }
18921
18922 pub(crate) fn sql_column_matches(&self) -> Vec<&(String, DataType)> {
18925 let input = self.query_input_shown();
18926 let line = input.line_at(input.cursor_line()).unwrap_or_default();
18927 let word = match self.query_mode {
18928 QueryMode::Sql => sql_assist::word_before(line, input.cursor_col()),
18929 QueryMode::Q => sql_assist::q_word_before(line, input.cursor_col()),
18930 }
18931 .map(|w| w.text)
18932 .unwrap_or_default();
18933 sql_assist::matching(&self.sql_columns, &word)
18934 }
18935
18936 pub fn query_prompt_text(&self) -> Option<&str> {
18938 self.query_prompt_mode()?;
18939 Some(self.query_input_shown().value())
18940 }
18941
18942 pub fn query_prompt_error(&self) -> Option<String> {
18945 if let Some(error) = &self.query_run_error {
18946 return Some(error.clone());
18947 }
18948 let state = self.data_table_state.as_ref()?;
18949 let error = state.error()?;
18950 Some(if self.query_mode == QueryMode::Sql {
18951 crate::error_display::sql_error_message(error, state.sql_table_rows())
18952 } else {
18953 crate::error_display::user_message_from_polars(error)
18954 })
18955 }
18956
18957 pub fn inline_failures(&self) -> u64 {
18959 self.inline_failures
18960 }
18961
18962 fn run_query(&mut self, mode: QueryMode, text: &str, status: &str) {
18967 self.query_run_error = None;
18968 let Some(state) = self.data_table_state.as_mut() else {
18969 return;
18970 };
18971 let rollback = state.rollback_point();
18972 let rows = state.sql_table_rows();
18973 state.deferred(|s| match mode {
18975 QueryMode::Sql => s.sql_query(text.to_string()),
18976 QueryMode::Q => s.query(text.to_string()),
18977 });
18978 if state.error().is_some() {
18979 return;
18980 }
18981 self.query_running = Some(QueryRun {
18982 origin: RunOrigin::Query(mode),
18983 frame: state.len_generation(),
18984 rollback,
18985 len_count_inflight: self.len_count_inflight,
18986 count_after_paint: self.count_after_paint,
18987 len_count_failed: self.len_count_failed,
18988 rows,
18989 });
18990 if !self.spawn_async_collect(status) {
18991 self.query_running = None;
18993 if self.query_prompt_mode() == Some(mode) {
18994 self.leave_query_prompt_after_run();
18995 }
18996 }
18997 }
18998
18999 fn take_query_run(&mut self) -> Option<QueryRun> {
19002 let run = self.query_running.take()?;
19003 let frame = self.data_table_state.as_ref()?.len_generation();
19004 (run.frame == frame).then_some(run)
19005 }
19006
19007 fn leave_query_prompt_after_run(&mut self) {
19009 self.sql_completion = None;
19010 self.input_mode = InputMode::Normal;
19011 self.input_type = None;
19012 self.sql_input.set_focused(false);
19013 self.query_input.set_focused(false);
19014 if let Some(state) = &mut self.data_table_state {
19015 state.suppress_error_display = false;
19016 }
19017 }
19018
19019 fn sync_query_focus(&mut self) {
19021 let mode = self.query_mode;
19022 self.sql_input.set_focused(mode == QueryMode::Sql);
19023 self.query_input.set_focused(mode == QueryMode::Q);
19024 }
19025
19026 fn close_query_prompt(&mut self) {
19028 self.query_run_error = None;
19029 self.sql_completion = None;
19030 self.query_input.clear();
19031 self.sql_input.clear();
19032 self.query_input.set_focused(false);
19033 self.sql_input.set_focused(false);
19034 self.input_mode = InputMode::Normal;
19035 self.input_type = None;
19036 if let Some(state) = &mut self.data_table_state {
19037 state.dismiss_error();
19038 state.suppress_error_display = false;
19039 }
19040 }
19041}
19042
19043impl Widget for &mut App {
19044 fn render(self, area: Rect, buf: &mut Buffer) {
19045 self.begin_frame();
19046 self.debug.num_frames += 1;
19047 if self.debug.enabled {
19048 self.debug.show_help_at_render = self.help.is_open();
19049 }
19050
19051 use crate::render::context::RenderContext;
19052 use crate::render::layout::app_layout;
19053 use crate::render::main_view::MainViewContent;
19054
19055 let ctx = RenderContext::from_theme_and_config(
19056 &self.theme,
19057 self.table_cell_padding,
19058 self.column_colors,
19059 self.number_format.clone(),
19060 )
19061 .with_dtype_row(self.dtype_row);
19062
19063 let main_view_content = MainViewContent::current(self);
19064
19065 Clear.render(area, buf);
19066 let background_color = self.color("background");
19067 Block::default()
19068 .style(Style::default().bg(background_color))
19069 .render(area, buf);
19070
19071 let progress = self.footer_progress_line(main_view_content);
19074 let prompt_room = crate::render::footer::MAX_LINES - 1;
19075 let prompt_rows = if main_view_content == MainViewContent::Datatable {
19076 crate::render::input_strip::rows(self, area.width, prompt_room)
19077 } else {
19078 0
19079 };
19080 let progress_rows = u16::from(progress.is_some() && prompt_rows < prompt_room);
19081 let footer_lines = 1 + prompt_rows + progress_rows;
19082 let rule = self.input_mode != InputMode::Inspect;
19084 let app_layout = app_layout(area, self.debug.enabled, footer_lines, rule);
19085 let room = app_layout.footer.height.saturating_sub(1);
19088 let prompt_rows = prompt_rows.min(room);
19089 let progress_rows = progress_rows.min(room - prompt_rows);
19090 let main_area = app_layout.main_view;
19091 Clear.render(main_area, buf);
19092
19093 crate::render::main_view_render::render_main_view(area, main_area, buf, self, &ctx);
19094 if self.in_normal_table_view() {
19095 self.render_drop_mark(buf, &ctx);
19096 }
19097 if self.menu_showing()
19098 && let Some(menu) = self.context_menu.clone()
19099 {
19100 menu.render(main_area, buf, &ctx);
19101 }
19102
19103 if self.confirmation_modal.active {
19106 crate::render::overlays::render_confirmation_modal(
19107 area,
19108 buf,
19109 &mut self.confirmation_modal,
19110 &ctx,
19111 );
19112 }
19113 if self.error_modal.active {
19114 crate::render::overlays::render_error_modal(area, buf, &mut self.error_modal, &ctx);
19115 }
19116 self.close_help_left_behind();
19117 if self.help.is_open() {
19118 crate::render::help::render_help(app_layout.main_view, buf, &mut self.help, &ctx);
19121 }
19122
19123 let footer = self.footer(main_view_content, progress_rows > 0);
19124 crate::render::footer::render_rule(app_layout.rule, buf, &ctx);
19125 let line = Rect {
19126 height: 1,
19127 ..app_layout.footer
19128 };
19129 let drawn = footer.render_line(line, buf, &ctx);
19130 if prompt_rows > 0 {
19131 crate::render::input_strip::render(
19132 Rect {
19133 y: line.y + 1,
19134 height: prompt_rows,
19135 ..line
19136 },
19137 buf,
19138 self,
19139 &ctx,
19140 );
19141 }
19142 if let Some(progress) = progress.filter(|_| progress_rows > 0) {
19143 crate::render::footer::render_progress(
19144 &progress,
19145 Rect {
19146 y: line.y + 1 + prompt_rows,
19147 height: 1,
19148 ..line
19149 },
19150 buf,
19151 &ctx,
19152 );
19153 }
19154 self.pointer.chips_drawn(
19155 drawn
19156 .iter()
19157 .map(|(rect, key)| (*rect, key.as_str()))
19158 .collect(),
19159 );
19160 if let Some(debug_area) = app_layout.debug {
19161 self.debug.render(debug_area, buf);
19162 }
19163 self.pointer.drawn();
19164
19165 crate::sanitize::sanitize_buffer(buf);
19177 }
19178}
19179
19180impl App {
19181 pub fn capture_view(&self) -> Result<Option<LazyFrame>> {
19191 let Some(state) = &self.data_table_state else {
19192 return Ok(None);
19193 };
19194 if state.scans_a_download() {
19195 return Err(color_eyre::eyre::eyre!(
19196 "cannot return this view: the data was downloaded to a temporary file \
19197 that is removed when datui exits. Export it from inside datui (press \
19198 e) instead."
19199 ));
19200 }
19201 if state.scans_a_temp_file() {
19202 return Err(color_eyre::eyre::eyre!(
19203 "cannot return this view: the data was decompressed or converted into a \
19204 temporary file that is removed when datui exits. Export it from \
19205 inside datui (press e) instead."
19206 ));
19207 }
19208 Ok(Some(state.visible_lf()))
19209 }
19210}
19211
19212impl Drop for App {
19213 fn drop(&mut self) {
19214 self.footer_progress.cancel();
19221 self.indexing_stop
19224 .store(true, std::sync::atomic::Ordering::Relaxed);
19225 if let Some(lines) = self.indexing_lines.take() {
19226 lines.stop_indexing();
19227 }
19228 }
19229}
19230
19231#[cfg(feature = "cloud")]
19233fn home_cloud_source(
19234 source: &crate::cloud_sources::Source,
19235 cached: Option<&crate::cache::CloudListing>,
19236 listing: bool,
19237) -> home::CloudSource {
19238 let mut details: Vec<(String, String)> = vec![
19239 ("source".to_string(), source.id.clone()),
19240 (
19241 "api".to_string(),
19242 match source.kind {
19243 crate::cloud_browse::ProviderKind::S3 => "s3",
19244 crate::cloud_browse::ProviderKind::Gcs => "gcs",
19245 crate::cloud_browse::ProviderKind::Azure => "azure",
19246 }
19247 .to_string(),
19248 ),
19249 ];
19250 if let Some(endpoint) = &source.s3.endpoint {
19251 details.push(("endpoint".to_string(), endpoint.clone()));
19252 }
19253 if let Some(region) = &source.s3.region {
19254 details.push(("region".to_string(), region.clone()));
19255 }
19256 if let Some(project) = &source.project {
19257 details.push(("project".to_string(), project.clone()));
19258 }
19259 if let Some(profile) = &source.profile {
19260 details.push(("profile".to_string(), profile.clone()));
19261 }
19262 if let Some(configuration) = &source.gcloud {
19263 details.push(("configuration".to_string(), configuration.clone()));
19264 }
19265 if source.s3.virtual_hosted.is_some() {
19266 let style = if source.s3.virtual_hosted_style() {
19267 "virtual-hosted"
19268 } else {
19269 "path-style"
19270 };
19271 details.push(("addressing".to_string(), style.to_string()));
19272 }
19273 details.push(("login".to_string(), source.origin.clone()));
19274
19275 let short = source.problem.as_deref().map(|problem| {
19276 if problem.starts_with("not signed in") {
19277 "not signed in"
19278 } else if problem.starts_with("unsupported login") {
19279 "unsupported login"
19280 } else {
19281 "not configured"
19282 }
19283 });
19284 let note = match (&source.problem, short) {
19288 (Some(problem), Some(short)) => problem
19289 .strip_prefix(short)
19290 .map(|rest| rest.trim_start_matches([':', ' ']))
19291 .filter(|rest| !rest.is_empty())
19292 .unwrap_or(problem)
19293 .to_string(),
19294 _ => [source.detail(), Some(source.origin.clone())]
19295 .into_iter()
19296 .flatten()
19297 .filter(|n| !n.is_empty())
19298 .collect::<Vec<_>>()
19299 .join(&format!(" {} ", crate::glyphs::get().middot)),
19300 };
19301 let mut names: Vec<String> = cached.map(|c| c.buckets.clone()).unwrap_or_default();
19302 for bucket in &source.buckets {
19303 if !names.contains(bucket) {
19304 names.push(bucket.clone());
19305 }
19306 }
19307 let status = match (&source.problem, short) {
19308 (Some(problem), Some(short)) => home::CloudStatus::Failed {
19309 short: short.to_string(),
19310 detail: problem.clone(),
19311 },
19312 _ if cached.is_some() => home::CloudStatus::Listed,
19313 _ if listing => home::CloudStatus::Listing,
19314 _ => home::CloudStatus::Unlisted,
19315 };
19316 home::CloudSource {
19317 id: source.id.clone(),
19318 label: source.label.clone(),
19319 api: match source.kind {
19320 crate::cloud_browse::ProviderKind::S3 => "s3",
19321 crate::cloud_browse::ProviderKind::Gcs => "gcs",
19322 crate::cloud_browse::ProviderKind::Azure => "azure",
19323 }
19324 .to_string(),
19325 note,
19326 buckets: names
19327 .iter()
19328 .map(|b| PathBuf::from(source.bucket_url(b)))
19329 .collect(),
19330 refreshing: listing && cached.is_some() && source.problem.is_none(),
19331 asked: listing || source.problem.is_some(),
19333 listed_at: cached
19334 .map(|c| std::time::UNIX_EPOCH + std::time::Duration::from_secs(c.listed_at)),
19335 status,
19336 details,
19337 place_details: Default::default(),
19338 }
19339}
19340
19341#[cfg(feature = "cloud")]
19343fn summarize_cloud_failure(error: &str) -> (String, String) {
19344 let lower = error.to_lowercase();
19345 if let Some(start) = lower.find("needs ") {
19347 return (error[start..].to_string(), error.to_string());
19348 }
19349 let short = if lower.contains("403")
19350 || lower.contains("forbidden")
19351 || lower.contains("accessdenied")
19352 || lower.contains("access denied")
19353 {
19354 "403"
19355 } else if lower.contains("401")
19356 || lower.contains("unauthorized")
19357 || lower.contains("credential")
19358 || lower.contains("invalidaccesskeyid")
19359 || lower.contains("expired")
19360 || lower.contains("sso")
19361 || lower.contains("az login")
19362 {
19363 "not logged in"
19364 } else if lower.contains("unsupported login") {
19365 "unsupported login"
19366 } else if lower.contains("no gcp project") {
19367 "no project"
19368 } else if lower.contains("is not set") {
19369 "not configured"
19370 } else if lower.contains("timed out")
19371 || lower.contains("timeout")
19372 || lower.contains("connection")
19373 || lower.contains("dns")
19374 || lower.contains("resolve")
19375 {
19376 "unavailable"
19377 } else {
19378 "error"
19379 };
19380 (short.to_string(), error.to_string())
19381}
19382
19383#[cfg(feature = "cloud")]
19393pub(crate) fn wait_on_runtime<F>(runtime: &tokio::runtime::Handle, future: F) -> Option<F::Output>
19394where
19395 F: std::future::Future + Send + 'static,
19396 F::Output: Send + 'static,
19397{
19398 let (tx, rx) = std::sync::mpsc::sync_channel(1);
19399 runtime.spawn(async move {
19400 let _ = tx.send(future.await);
19401 });
19402 rx.recv().ok()
19403}
19404
19405fn conclude(
19410 end: event_pump::Ended,
19411 app: &App,
19412 capture: bool,
19413 reader: &mut terminal_input::TerminalInput,
19414 screen: &mut TakenTerminal,
19415) -> Result<Option<LazyFrame>> {
19416 reader.stop();
19417 screen.restore();
19418 match end {
19419 event_pump::Ended::Quit if capture => app.capture_view(),
19420 event_pump::Ended::Quit => Ok(None),
19421 event_pump::Ended::Crash(msg) => Err(color_eyre::eyre::eyre!(msg)),
19422 event_pump::Ended::NotFound(path) => Err(std::io::Error::new(
19423 std::io::ErrorKind::NotFound,
19424 format!("File not found: {}", path.display()),
19425 )
19426 .into()),
19427 }
19428}
19429
19430static ENDED_BY_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
19433
19434pub fn ended_by_signal() -> Option<i32> {
19439 let status = ENDED_BY_SIGNAL.load(std::sync::atomic::Ordering::SeqCst);
19440 (status != 0).then_some(status)
19441}
19442
19443fn end_session(status: i32, tx: &std::sync::mpsc::Sender<AppEvent>) {
19448 use std::sync::atomic::Ordering;
19449 const STRAGGLE: std::time::Duration = std::time::Duration::from_secs(3);
19452 fn end_now(status: i32) -> ! {
19453 restore_terminal();
19454 std::process::exit(status)
19455 }
19456 if ENDED_BY_SIGNAL
19457 .compare_exchange(0, status, Ordering::SeqCst, Ordering::SeqCst)
19458 .is_err()
19459 {
19460 end_now(ENDED_BY_SIGNAL.load(Ordering::SeqCst));
19461 }
19462 let _ = tx.send(AppEvent::Exit);
19463 tokio::spawn(async move {
19464 tokio::time::sleep(STRAGGLE).await;
19465 end_now(status);
19466 });
19467}
19468
19469#[cfg(unix)]
19472fn quit_on_signals(runtime: &tokio::runtime::Handle, tx: &std::sync::mpsc::Sender<AppEvent>) {
19473 use tokio::signal::unix::{SignalKind, signal};
19474 let _runtime = runtime.enter();
19476 for kind in [
19477 SignalKind::terminate(),
19478 SignalKind::hangup(),
19479 SignalKind::interrupt(),
19480 ] {
19481 let Ok(mut arrivals) = signal(kind) else {
19482 continue;
19483 };
19484 let tx = tx.clone();
19485 runtime.spawn(async move {
19486 while arrivals.recv().await.is_some() {
19487 end_session(128 + kind.as_raw_value(), &tx);
19488 }
19489 });
19490 }
19491}
19492
19493#[cfg(windows)]
19497fn quit_on_signals(runtime: &tokio::runtime::Handle, tx: &std::sync::mpsc::Sender<AppEvent>) {
19498 use tokio::signal::windows::{ctrl_close, ctrl_logoff, ctrl_shutdown};
19499 const CLOSED: i32 = 0xC000_013A_u32 as i32;
19501 let _runtime = runtime.enter();
19503 macro_rules! quit_on {
19505 ($listen:expr) => {
19506 if let Ok(mut arrivals) = $listen {
19507 let tx = tx.clone();
19508 runtime.spawn(async move {
19509 while arrivals.recv().await.is_some() {
19510 end_session(CLOSED, &tx);
19511 }
19512 });
19513 }
19514 };
19515 }
19516 quit_on!(ctrl_close());
19517 quit_on!(ctrl_logoff());
19518 quit_on!(ctrl_shutdown());
19519}
19520
19521pub fn run(input: RunInput, config: Option<AppConfig>) -> Result<()> {
19524 run_impl(input, config, false).map(|_| ())
19525}
19526
19527pub fn run_captured(input: RunInput, config: Option<AppConfig>) -> Result<Option<LazyFrame>> {
19531 run_impl(input, config, true)
19532}
19533
19534fn run_impl(
19535 input: RunInput,
19536 config: Option<AppConfig>,
19537 capture: bool,
19538) -> Result<Option<LazyFrame>> {
19539 use event_pump::EventPump;
19540 use std::io::Write;
19541
19542 let input = startup::expand_home(input);
19544 use std::sync::{Mutex, Once, mpsc};
19545
19546 let views = Views::read_in_background();
19549
19550 static COLOR_EYRE_INIT: Once = Once::new();
19553 static INSTALL_RESULT: Mutex<Option<Result<(), color_eyre::Report>>> = Mutex::new(None);
19554 COLOR_EYRE_INIT.call_once(|| {
19555 *INSTALL_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some(color_eyre::install());
19556 });
19557 if let Some(Err(e)) = INSTALL_RESULT
19558 .lock()
19559 .unwrap_or_else(|e| e.into_inner())
19560 .as_ref()
19561 {
19562 return Err(color_eyre::eyre::eyre!(e.to_string()));
19563 }
19564 let rt = tokio::runtime::Builder::new_multi_thread()
19565 .worker_threads(2)
19566 .enable_all()
19567 .build()
19568 .map_err(|e| color_eyre::eyre::eyre!("Failed to create tokio runtime: {}", e))?;
19569
19570 struct RtGuard(Option<tokio::runtime::Runtime>);
19577 impl Drop for RtGuard {
19578 fn drop(&mut self) {
19579 if let Some(rt) = self.0.take() {
19580 rt.shutdown_background();
19581 }
19582 }
19583 }
19584 let rt_guard = RtGuard(Some(rt));
19585 let rt_handle = rt_guard
19586 .0
19587 .as_ref()
19588 .expect("runtime present")
19589 .handle()
19590 .clone();
19591
19592 let passed = match &input {
19595 RunInput::Cli(args) if args.tee.as_deref().is_some_and(crate::stdin::is_stdin) => {
19596 Some(crate::tee::pass_stdout_on().map_err(|e| color_eyre::eyre::eyre!(e))?)
19597 }
19598 _ => None,
19599 };
19600 let mut terminal = match ratatui::try_init() {
19601 Ok(terminal) => QuietTerminal(Some(terminal)),
19602 Err(e) => {
19603 if config.is_none() {
19607 startup::load_config(&input)?;
19608 }
19609 if let Some(missing) =
19611 App::missing_named_path(startup::named_paths(&input), &Default::default())
19612 {
19613 return Err(std::io::Error::new(
19614 std::io::ErrorKind::NotFound,
19615 format!("File not found: {}", missing.display()),
19616 )
19617 .into());
19618 }
19619 return Err(color_eyre::eyre::eyre!(
19620 "datui requires an interactive terminal (TTY). No terminal detected: {}. \
19621 There is no TTY inside a Jupyter notebook or when output is piped or \
19622 redirected; run from a terminal with stdout connected to it.",
19623 e
19624 ));
19625 }
19626 };
19627 let mut screen = TakenTerminal { restored: false };
19629 let session = logging::TuiSession::begin(restore_terminal);
19632 push_keyboard_flags();
19633 let asked = terminal_color::supported()
19637 && config.as_ref().is_none_or(|c| c.theme.follow)
19638 && terminal_color::ask(&mut std::io::stdout());
19639 let mut background = None;
19640 let (tx, rx) = mpsc::channel::<AppEvent>();
19641 {
19642 let tx = tx.clone();
19643 session.wake_with(move || {
19644 let _ = tx.send(AppEvent::Wake);
19645 });
19646 }
19647 let mut reader = terminal_input::TerminalInput::start(tx.clone())?;
19648 #[cfg(any(unix, windows))]
19651 if matches!(input, RunInput::Cli(_)) {
19652 quit_on_signals(&rt_handle, &tx);
19653 }
19654
19655 let waiting_on = startup::named(&input);
19658 {
19659 let tx = tx.clone();
19660 std::thread::Builder::new()
19661 .name("datui-settings".into())
19662 .spawn(move || {
19663 let read = logging::catch_panic(|| startup::read(input, config))
19664 .unwrap_or_else(|panic| Err(color_eyre::eyre::eyre!(panic)));
19665 let _ = tx.send(AppEvent::SettingsRead(Box::new(read)));
19666 })?;
19667 }
19668 let mut backlog = Vec::new();
19669 let grace_ends = std::time::Instant::now() + startup::GRACE;
19670 let mut waiting_shown = false;
19671 let settings = loop {
19672 let timeout = if waiting_shown {
19673 std::time::Duration::MAX
19674 } else {
19675 grace_ends.saturating_duration_since(std::time::Instant::now())
19676 };
19677 match rx.recv_timeout(timeout) {
19678 Ok(AppEvent::SettingsRead(read)) => break *read,
19679 Ok(AppEvent::TerminalBackground(mode)) => background = Some(mode),
19680 Ok(AppEvent::Terminal(crossterm::event::Event::Key(key)))
19681 if key.modifiers.contains(KeyModifiers::CONTROL)
19682 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('q')) =>
19683 {
19684 reader.stop();
19685 screen.restore();
19686 return Ok(None);
19687 }
19688 Ok(AppEvent::Crash(msg)) => {
19689 reader.stop();
19690 screen.restore();
19691 return Err(color_eyre::eyre::eyre!(msg));
19692 }
19693 Ok(AppEvent::Exit) => {
19695 reader.stop();
19696 screen.restore();
19697 return Ok(None);
19698 }
19699 Ok(event) => {
19700 if waiting_shown
19701 && matches!(
19702 event,
19703 AppEvent::Terminal(crossterm::event::Event::Resize(..))
19704 )
19705 {
19706 terminal
19707 .get()
19708 .draw(|frame| startup::draw_waiting(frame, waiting_on.as_deref()))?;
19709 }
19710 backlog.push(event);
19712 }
19713 Err(_) => {
19714 terminal
19715 .get()
19716 .draw(|frame| startup::draw_waiting(frame, waiting_on.as_deref()))?;
19717 let _ = std::io::stdout().flush();
19718 waiting_shown = true;
19719 }
19720 }
19721 };
19722 let startup::Settings {
19723 config,
19724 theme,
19725 input,
19726 opts,
19727 notes,
19728 } = match settings {
19729 Ok(settings) => settings,
19730 Err(e) => {
19731 reader.stop();
19732 screen.restore();
19733 return Err(e);
19734 }
19735 };
19736
19737 let background = (asked && config.theme.follow)
19740 .then(|| startup::take_answer(&rx, background, &mut backlog))
19741 .flatten();
19742 if config.theme.follow && terminal_color::supported() {
19743 follow_focus(&mut std::io::stdout());
19744 }
19745
19746 glyphs::init_with_overrides(config.display.unicode, &config.glyphs.overrides);
19750
19751 pointer::capture(config.display.mouse, &mut std::io::stdout());
19753
19754 let mut app = App::new_with_views(tx.clone(), rt_handle, theme, config, views);
19755 app.settle_first_palette(background);
19756 if let Some(out) = passed {
19757 app.pass_stdout_to(out);
19758 }
19759 app.startup_view = opts.view.clone();
19760 let debug_env = std::env::var_os("DATUI_DEBUG").is_some_and(|v| !v.is_empty() && v != "0");
19762 if opts.debug || debug_env {
19763 app.enable_debug();
19764 }
19765
19766 let open = match input {
19768 RunInput::Paths(paths, _) if paths.is_empty() => {
19770 app.enter_home();
19771 None
19772 }
19773 RunInput::Paths(paths, opts) => {
19774 app.set_loading_phase("Scanning input", 10);
19777 if let [path] = paths.as_slice() {
19778 app.name_what_is_loading(path.clone());
19779 }
19780 Some(AppEvent::OpenNamed(paths, opts))
19781 }
19782 RunInput::LazyFrame(lf, opts) => {
19783 app.set_loading_phase("Scanning input", 10);
19784 Some(AppEvent::OpenLazyFrame(lf, opts))
19785 }
19786 RunInput::Cli(_) | RunInput::Host(..) => {
19787 unreachable!("read_settings resolves the command line")
19788 }
19789 };
19790 let _sweep = app.exit_sweep();
19793 let input_tx = tx.clone();
19794 let mut pump = EventPump::new(app, tx, rx);
19795 pump.handle_first(backlog.into_iter().chain(open));
19800 let end = pump.run(|app| {
19801 if let Some(open) = app.take_external_open() {
19802 let mouse = app.mouse_enabled();
19803 let focus = app.follows_terminal() && terminal_color::supported();
19804 let note = open_externally(&open, &mut reader, &input_tx, mouse, focus, terminal.get());
19805 app.external_opened(&open, note);
19806 }
19807 if app.take_background_query() && terminal_color::supported() {
19809 terminal_color::ask(&mut std::io::stdout());
19810 }
19811 terminal
19812 .get()
19813 .draw(|frame| frame.render_widget(app, frame.area()))?;
19814 let _ = std::io::stdout().flush();
19815 Ok(())
19816 })?;
19817 let result = conclude(end, &pump.app, capture, &mut reader, &mut screen);
19818 drop(session);
19820 for note in notes {
19822 let _ = writeln!(std::io::stderr(), "datui: {note}");
19823 }
19824 if let Some((tee, handle)) = pump.app.recording_after_exit() {
19827 let to = if tee.to_stdout() {
19828 format!("passing standard input on to {}", tee.name())
19829 } else {
19830 format!("recording standard input to {}", tee.path.display())
19831 };
19832 let _ = writeln!(
19833 std::io::stderr(),
19834 "datui: {to} until it ends (Ctrl+C stops it)"
19835 );
19836 handle.spool().wait();
19837 let done = if tee.to_stdout() {
19838 "datui: standard input ended".to_string()
19839 } else {
19840 format!("datui: saved {}", tee.path.display())
19841 };
19842 let _ = writeln!(std::io::stderr(), "{done}");
19843 }
19844 result
19845}
19846
19847fn open_externally(
19851 open: &external_open::ExternalOpen,
19852 reader: &mut terminal_input::TerminalInput,
19853 tx: &std::sync::mpsc::Sender<AppEvent>,
19854 mouse: bool,
19855 focus: bool,
19856 terminal: &mut ratatui::DefaultTerminal,
19857) -> Option<String> {
19858 let program = external_open::program_for(open.document, |name| std::env::var(name).ok());
19859 let result = match &program {
19860 external_open::Program::Opener(_) => external_open::run(&program, &open.path),
19861 external_open::Program::Wait(_) => {
19862 reader.stop();
19863 restore_terminal();
19864 let result = external_open::run(&program, &open.path);
19865 let _ = crossterm::terminal::enable_raw_mode();
19866 let _ = crossterm::execute!(
19867 std::io::stdout(),
19868 crossterm::terminal::EnterAlternateScreen,
19869 crossterm::cursor::Hide
19870 );
19871 push_keyboard_flags();
19872 pointer::capture(mouse, &mut std::io::stdout());
19873 if focus {
19874 follow_focus(&mut std::io::stdout());
19875 }
19876 let _ = terminal.clear();
19877 match terminal_input::TerminalInput::start(tx.clone()) {
19878 Ok(started) => *reader = started,
19879 Err(e) => {
19880 let _ = tx.send(AppEvent::Crash(format!("Could not read keys again: {e}")));
19881 }
19882 }
19883 result
19884 }
19885 };
19886 result.err().map(|e| e.to_string())
19887}
19888
19889fn sentence(text: &str) -> String {
19891 let mut chars = text.chars();
19892 match chars.next() {
19893 Some(first) => first.to_uppercase().chain(chars).collect(),
19894 None => String::new(),
19895 }
19896}