1use color_eyre::Result;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use polars::datatypes::AnyValue;
4use polars::datatypes::DataType;
5#[cfg(feature = "cloud")]
6use polars::io::cloud::{AmazonS3ConfigKey, CloudOptions};
7use polars::prelude::{col, len, DataFrame, LazyFrame, Schema};
8#[cfg(feature = "cloud")]
9use polars::prelude::{PlPathRef, ScanArgsParquet};
10use std::path::{Path, PathBuf};
11use std::sync::{mpsc::Sender, Arc};
12use widgets::info::{read_parquet_metadata, InfoFocus, InfoModal, InfoTab, ParquetMetadataCache};
13
14use ratatui::style::{Color, Style};
15use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
16
17use ratatui::widgets::{Block, Clear};
18
19pub mod analysis_modal;
20pub mod cache;
21pub mod chart_data;
22pub mod chart_export;
23pub mod chart_export_modal;
24pub mod chart_modal;
25pub mod cli;
26#[cfg(feature = "cloud")]
27mod cloud_hive;
28pub mod config;
29pub mod error_display;
30pub mod export_modal;
31pub mod filter_modal;
32pub(crate) mod help_strings;
33pub mod pivot_melt_modal;
34mod query;
35mod render;
36pub mod sort_filter_modal;
37pub mod sort_modal;
38mod source;
39pub mod statistics;
40pub mod template;
41pub mod widgets;
42
43pub use cache::CacheManager;
44pub use cli::Args;
45pub use config::{
46 rgb_to_256_color, rgb_to_basic_ansi, AppConfig, ColorParser, ConfigManager, Theme,
47};
48
49use analysis_modal::{AnalysisModal, AnalysisProgress};
50use chart_export::{
51 write_box_plot_eps, write_box_plot_png, write_chart_eps, write_chart_png, write_heatmap_eps,
52 write_heatmap_png, BoxPlotExportBounds, ChartExportBounds, ChartExportFormat,
53 ChartExportSeries,
54};
55use chart_export_modal::{ChartExportFocus, ChartExportModal};
56use chart_modal::{ChartFocus, ChartKind, ChartModal, ChartType};
57pub use error_display::{error_for_python, ErrorKindForPython};
58use export_modal::{ExportFocus, ExportFormat, ExportModal};
59use filter_modal::{FilterFocus, FilterOperator, FilterStatement, LogicalOperator};
60use pivot_melt_modal::{MeltSpec, PivotMeltFocus, PivotMeltModal, PivotMeltTab, PivotSpec};
61use sort_filter_modal::{SortFilterFocus, SortFilterModal, SortFilterTab};
62use sort_modal::{SortColumn, SortFocus};
63pub use template::{Template, TemplateManager};
64use widgets::controls::Controls;
65use widgets::datatable::DataTableState;
66use widgets::debug::DebugState;
67use widgets::template_modal::{CreateFocus, TemplateFocus, TemplateModal, TemplateModalMode};
68use widgets::text_input::{TextInput, TextInputEvent};
69
70pub const APP_NAME: &str = "datui";
72
73pub use cli::{CompressionFormat, FileFormat};
75
76fn file_format_to_export_format(f: FileFormat) -> Option<ExportFormat> {
78 match f {
79 FileFormat::Parquet => Some(ExportFormat::Parquet),
80 FileFormat::Csv | FileFormat::Tsv | FileFormat::Psv => Some(ExportFormat::Csv),
81 FileFormat::Json => Some(ExportFormat::Json),
82 FileFormat::Jsonl => Some(ExportFormat::Ndjson),
83 FileFormat::Arrow => Some(ExportFormat::Ipc),
84 FileFormat::Avro => Some(ExportFormat::Avro),
85 FileFormat::Orc | FileFormat::Excel => None,
86 }
87}
88
89#[cfg(test)]
90pub mod tests {
91 use std::path::Path;
92 use std::process::Command;
93 use std::sync::Once;
94
95 static INIT: Once = Once::new();
96
97 pub fn ensure_sample_data() {
101 INIT.call_once(|| {
102 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
104 let sample_data_dir = repo_root.join("tests/sample-data");
105
106 let key_files = [
109 "people.parquet",
110 "sales.parquet",
111 "large_dataset.parquet",
112 "empty.parquet",
113 "pivot_long.parquet",
114 "melt_wide.parquet",
115 "infer_schema_length_data.csv",
116 ];
117
118 let needs_generation = !sample_data_dir.exists()
119 || key_files
120 .iter()
121 .any(|file| !sample_data_dir.join(file).exists());
122
123 if needs_generation {
124 let script_path = repo_root.join("scripts/generate_sample_data.py");
126 if !script_path.exists() {
127 panic!(
128 "Sample data generation script not found at: {}. \
129 Please ensure you're running tests from the repository root.",
130 script_path.display()
131 );
132 }
133
134 let python_cmd = if Command::new("python3").arg("--version").output().is_ok() {
136 "python3"
137 } else if Command::new("python").arg("--version").output().is_ok() {
138 "python"
139 } else {
140 panic!(
141 "Python not found. Please install Python 3 to generate test data. \
142 The script requires: polars>=0.20.0 and numpy>=1.24.0"
143 );
144 };
145
146 let output = Command::new(python_cmd)
148 .arg(script_path)
149 .output()
150 .unwrap_or_else(|e| {
151 panic!(
152 "Failed to run sample data generation script: {}. \
153 Make sure Python is installed and the script is executable.",
154 e
155 );
156 });
157
158 if !output.status.success() {
159 let stderr = String::from_utf8_lossy(&output.stderr);
160 let stdout = String::from_utf8_lossy(&output.stdout);
161 panic!(
162 "Sample data generation failed!\n\
163 Exit code: {:?}\n\
164 stdout:\n{}\n\
165 stderr:\n{}",
166 output.status.code(),
167 stdout,
168 stderr
169 );
170 }
171 }
172 });
173 }
174
175 pub fn sample_data_dir() -> std::path::PathBuf {
177 ensure_sample_data();
178 Path::new(env!("CARGO_MANIFEST_DIR"))
179 .join("../..")
180 .join("tests/sample-data")
181 }
182
183 #[test]
185 fn test_active_query_settings_only_one_set() {
186 use super::active_query_settings;
187
188 let (q, sql, fuzzy) = active_query_settings("", "", "");
189 assert!(q.is_none() && sql.is_none() && fuzzy.is_none());
190
191 let (q, sql, fuzzy) = active_query_settings("select a", "SELECT 1", "foo");
192 assert!(q.is_none() && sql.as_deref() == Some("SELECT 1") && fuzzy.is_none());
193
194 let (q, sql, fuzzy) = active_query_settings("select a", "", "foo bar");
195 assert!(q.is_none() && sql.is_none() && fuzzy.as_deref() == Some("foo bar"));
196
197 let (q, sql, fuzzy) = active_query_settings(" select a ", "", "");
198 assert!(q.as_deref() == Some("select a") && sql.is_none() && fuzzy.is_none());
199 }
200}
201
202#[derive(Clone, Debug)]
204pub enum ParseStringsTarget {
205 All,
207 Columns(Vec<String>),
209}
210
211#[derive(Clone)]
212pub struct OpenOptions {
213 pub delimiter: Option<u8>,
214 pub has_header: Option<bool>,
215 pub skip_lines: Option<usize>,
216 pub skip_rows: Option<usize>,
217 pub skip_tail_rows: Option<usize>,
219 pub compression: Option<CompressionFormat>,
220 pub format: Option<FileFormat>,
222 pub pages_lookahead: Option<usize>,
223 pub pages_lookback: Option<usize>,
224 pub max_buffered_rows: Option<usize>,
225 pub max_buffered_mb: Option<usize>,
226 pub row_numbers: bool,
227 pub row_start_index: usize,
228 pub hive: bool,
230 pub single_spine_schema: bool,
232 pub parse_dates: bool,
234 pub parse_strings: Option<ParseStringsTarget>,
236 pub parse_strings_sample_rows: usize,
238 pub decompress_in_memory: bool,
240 pub temp_dir: Option<std::path::PathBuf>,
242 pub excel_sheet: Option<String>,
244 pub s3_endpoint_url_override: Option<String>,
246 pub s3_access_key_id_override: Option<String>,
247 pub s3_secret_access_key_override: Option<String>,
248 pub s3_region_override: Option<String>,
249 pub polars_streaming: bool,
251 pub workaround_pivot_date_index: bool,
253 pub null_values: Option<Vec<String>>,
255 pub infer_schema_length: Option<usize>,
257 pub ignore_errors: bool,
259 pub debug: bool,
261}
262
263impl OpenOptions {
264 pub fn new() -> Self {
265 Self {
266 delimiter: None,
267 has_header: None,
268 skip_lines: None,
269 skip_rows: None,
270 skip_tail_rows: None,
271 compression: None,
272 format: None,
273 pages_lookahead: None,
274 pages_lookback: None,
275 max_buffered_rows: None,
276 max_buffered_mb: None,
277 row_numbers: false,
278 row_start_index: 1,
279 hive: false,
280 single_spine_schema: true,
281 parse_dates: true,
282 parse_strings: None,
283 parse_strings_sample_rows: 1000,
284 decompress_in_memory: false,
285 temp_dir: None,
286 excel_sheet: None,
287 s3_endpoint_url_override: None,
288 s3_access_key_id_override: None,
289 s3_secret_access_key_override: None,
290 s3_region_override: None,
291 polars_streaming: true,
292 workaround_pivot_date_index: true,
293 null_values: None,
294 infer_schema_length: None,
295 ignore_errors: false,
296 debug: false,
297 }
298 }
299}
300
301impl Default for OpenOptions {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307impl OpenOptions {
308 pub fn with_skip_lines(mut self, skip_lines: usize) -> Self {
309 self.skip_lines = Some(skip_lines);
310 self
311 }
312
313 pub fn with_skip_rows(mut self, skip_rows: usize) -> Self {
314 self.skip_rows = Some(skip_rows);
315 self
316 }
317
318 pub fn with_delimiter(mut self, delimiter: u8) -> Self {
319 self.delimiter = Some(delimiter);
320 self
321 }
322
323 pub fn with_has_header(mut self, has_header: bool) -> Self {
324 self.has_header = Some(has_header);
325 self
326 }
327
328 pub fn with_compression(mut self, compression: CompressionFormat) -> Self {
329 self.compression = Some(compression);
330 self
331 }
332
333 pub fn with_workaround_pivot_date_index(mut self, workaround_pivot_date_index: bool) -> Self {
334 self.workaround_pivot_date_index = workaround_pivot_date_index;
335 self
336 }
337
338 pub fn csv_try_parse_dates(&self) -> bool {
342 self.parse_strings.is_none() && self.parse_dates
343 }
344}
345
346impl OpenOptions {
347 pub fn from_args_and_config(args: &cli::Args, config: &AppConfig) -> Self {
349 let mut opts = OpenOptions::new();
350
351 opts.delimiter = args.delimiter.or(config.file_loading.delimiter);
353 opts.skip_lines = args.skip_lines.or(config.file_loading.skip_lines);
354 opts.skip_rows = args.skip_rows.or(config.file_loading.skip_rows);
355 opts.skip_tail_rows = args.skip_tail_rows.or(config.file_loading.skip_tail_rows);
356
357 opts.has_header = if let Some(no_header) = args.no_header {
359 Some(!no_header)
360 } else {
361 config.file_loading.has_header
362 };
363
364 opts.compression = args.compression;
366
367 opts.format = args.format;
369
370 opts.pages_lookahead = args
372 .pages_lookahead
373 .or(Some(config.display.pages_lookahead));
374 opts.pages_lookback = args.pages_lookback.or(Some(config.display.pages_lookback));
375 opts.max_buffered_rows = Some(config.display.max_buffered_rows);
376 opts.max_buffered_mb = Some(config.display.max_buffered_mb);
377
378 opts.row_numbers = args.row_numbers || config.display.row_numbers;
380
381 opts.row_start_index = args
383 .row_start_index
384 .unwrap_or(config.display.row_start_index);
385
386 opts.hive = args.hive;
388
389 opts.single_spine_schema = args
391 .single_spine_schema
392 .or(config.file_loading.single_spine_schema)
393 .unwrap_or(true);
394
395 opts.parse_dates = args
397 .parse_dates
398 .or(config.file_loading.parse_dates)
399 .unwrap_or(true);
400
401 if args.no_parse_strings {
403 opts.parse_strings = None;
404 } else if !args.parse_strings.is_empty() {
405 let has_all = args.parse_strings.iter().any(|s| s.is_empty());
406 opts.parse_strings = Some(if has_all {
407 ParseStringsTarget::All
408 } else {
409 let cols: Vec<String> = args
410 .parse_strings
411 .iter()
412 .filter(|s| !s.is_empty())
413 .cloned()
414 .collect::<std::collections::HashSet<_>>()
415 .into_iter()
416 .collect();
417 ParseStringsTarget::Columns(cols)
418 });
419 } else if config.file_loading.parse_strings == Some(false) {
420 opts.parse_strings = None;
421 } else {
422 opts.parse_strings = Some(ParseStringsTarget::All);
423 }
424 opts.parse_strings_sample_rows = config
425 .file_loading
426 .parse_strings_sample_rows
427 .unwrap_or(1000);
428
429 opts.decompress_in_memory = args
431 .decompress_in_memory
432 .or(config.file_loading.decompress_in_memory)
433 .unwrap_or(false);
434
435 opts.temp_dir = args.temp_dir.clone().or_else(|| {
437 config
438 .file_loading
439 .temp_dir
440 .as_ref()
441 .map(std::path::PathBuf::from)
442 });
443
444 opts.excel_sheet = args.excel_sheet.clone();
446
447 opts.s3_endpoint_url_override = args
449 .s3_endpoint_url
450 .clone()
451 .or_else(|| std::env::var("AWS_ENDPOINT_URL_S3").ok())
452 .or_else(|| std::env::var("AWS_ENDPOINT_URL").ok());
453 opts.s3_access_key_id_override = args
454 .s3_access_key_id
455 .clone()
456 .or_else(|| std::env::var("AWS_ACCESS_KEY_ID").ok());
457 opts.s3_secret_access_key_override = args
458 .s3_secret_access_key
459 .clone()
460 .or_else(|| std::env::var("AWS_SECRET_ACCESS_KEY").ok());
461 opts.s3_region_override = args
462 .s3_region
463 .clone()
464 .or_else(|| std::env::var("AWS_REGION").ok())
465 .or_else(|| std::env::var("AWS_DEFAULT_REGION").ok());
466
467 opts.polars_streaming = config.performance.polars_streaming;
468
469 opts.workaround_pivot_date_index = args.workaround_pivot_date_index.unwrap_or(true);
470
471 opts.debug = args.debug || config.debug.enabled;
473
474 let config_nulls = config.file_loading.null_values.as_deref().unwrap_or(&[]);
476 let cli_nulls = &args.null_value;
477 if config_nulls.is_empty() && cli_nulls.is_empty() {
478 opts.null_values = None;
479 } else {
480 opts.null_values = Some(
481 config_nulls
482 .iter()
483 .chain(cli_nulls.iter())
484 .cloned()
485 .collect(),
486 );
487 }
488
489 opts.infer_schema_length = args
491 .infer_schema_length
492 .or(config.file_loading.infer_schema_length)
493 .or(Some(1000));
494
495 opts.ignore_errors = args
497 .ignore_errors
498 .or(config.file_loading.ignore_errors)
499 .unwrap_or(false);
500
501 opts
502 }
503}
504
505impl From<&cli::Args> for OpenOptions {
506 fn from(args: &cli::Args) -> Self {
507 let config = AppConfig::default();
509 Self::from_args_and_config(args, &config)
510 }
511}
512
513pub enum AppEvent {
514 Key(KeyEvent),
515 Open(Vec<PathBuf>, OpenOptions),
516 OpenLazyFrame(Box<LazyFrame>, OpenOptions),
518 DoLoad(Vec<PathBuf>, OpenOptions), DoLoadScanPaths(Vec<PathBuf>, OpenOptions),
521 DoLoadCsvWithParseStrings(Vec<PathBuf>, OpenOptions),
523 #[cfg(feature = "http")]
525 DoDownloadHttp(String, OpenOptions),
526 #[cfg(feature = "cloud")]
528 DoDownloadS3ToTemp(String, OpenOptions),
529 #[cfg(feature = "cloud")]
531 DoDownloadGcsToTemp(String, OpenOptions),
532 #[cfg(any(feature = "http", feature = "cloud"))]
534 DoLoadFromHttpTemp(PathBuf, OpenOptions),
535 DoLoadSchema(Box<LazyFrame>, Option<PathBuf>, OpenOptions),
537 DoLoadSchemaBlocking(Box<LazyFrame>, Option<PathBuf>, OpenOptions),
539 DoLoadBuffer,
541 DoDecompress(Vec<PathBuf>, OpenOptions), DoExport(PathBuf, ExportFormat, ExportOptions), DoExportCollect(PathBuf, ExportFormat, ExportOptions), DoExportWrite(PathBuf, ExportFormat, ExportOptions), DoLoadParquetMetadata, Exit,
547 Crash(String),
548 Search(String),
549 SqlSearch(String),
550 FuzzySearch(String),
551 Filter(Vec<FilterStatement>),
552 Sort(Vec<String>, bool), ColumnOrder(Vec<String>, usize), Pivot(PivotSpec),
555 Melt(MeltSpec),
556 Export(PathBuf, ExportFormat, ExportOptions), ChartExport(PathBuf, ChartExportFormat, String, u32, u32), DoChartExport(PathBuf, ChartExportFormat, String, u32, u32), Collect,
560 Update,
561 Reset,
562 Resize(u16, u16), DoScrollDown, DoScrollUp, DoScrollNext, DoScrollPrev, DoScrollEnd, DoScrollHalfDown, DoScrollHalfUp, GoToLine(usize), AnalysisChunk,
573 AnalysisDistributionCompute,
575 AnalysisCorrelationCompute,
577}
578
579#[derive(Clone)]
581pub enum RunInput {
582 Paths(Vec<PathBuf>, OpenOptions),
583 LazyFrame(Box<LazyFrame>, OpenOptions),
584}
585
586#[derive(Debug, Clone)]
587pub struct ExportOptions {
588 pub csv_delimiter: u8,
589 pub csv_include_header: bool,
590 pub csv_compression: Option<CompressionFormat>,
591 pub json_compression: Option<CompressionFormat>,
592 pub ndjson_compression: Option<CompressionFormat>,
593 pub parquet_compression: Option<CompressionFormat>, }
595
596#[derive(Debug, Default, PartialEq, Eq)]
597pub enum InputMode {
598 #[default]
599 Normal,
600 SortFilter,
601 PivotMelt,
602 Editing,
603 Export,
604 Info,
605 Chart,
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum InputType {
610 Search,
611 Filter,
612 GoToLine,
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
617pub enum QueryTab {
618 #[default]
619 SqlLike,
620 Fuzzy,
621 Sql,
622}
623
624impl QueryTab {
625 fn next(self) -> Self {
626 match self {
627 QueryTab::SqlLike => QueryTab::Fuzzy,
628 QueryTab::Fuzzy => QueryTab::Sql,
629 QueryTab::Sql => QueryTab::SqlLike,
630 }
631 }
632 fn prev(self) -> Self {
633 match self {
634 QueryTab::SqlLike => QueryTab::Sql,
635 QueryTab::Fuzzy => QueryTab::SqlLike,
636 QueryTab::Sql => QueryTab::Fuzzy,
637 }
638 }
639 fn index(self) -> usize {
640 match self {
641 QueryTab::SqlLike => 0,
642 QueryTab::Fuzzy => 1,
643 QueryTab::Sql => 2,
644 }
645 }
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
650pub enum QueryFocus {
651 TabBar,
652 #[default]
653 Input,
654}
655
656#[derive(Default)]
657pub struct ErrorModal {
658 pub active: bool,
659 pub message: String,
660}
661
662impl ErrorModal {
663 pub fn new() -> Self {
664 Self::default()
665 }
666
667 pub fn show(&mut self, message: String) {
668 self.active = true;
669 self.message = message;
670 }
671
672 pub fn hide(&mut self) {
673 self.active = false;
674 self.message.clear();
675 }
676}
677
678#[derive(Default)]
679pub struct SuccessModal {
680 pub active: bool,
681 pub message: String,
682}
683
684impl SuccessModal {
685 pub fn new() -> Self {
686 Self::default()
687 }
688
689 pub fn show(&mut self, message: String) {
690 self.active = true;
691 self.message = message;
692 }
693
694 pub fn hide(&mut self) {
695 self.active = false;
696 self.message.clear();
697 }
698}
699
700#[derive(Default)]
701pub struct ConfirmationModal {
702 pub active: bool,
703 pub message: String,
704 pub focus_yes: bool, }
706
707impl ConfirmationModal {
708 pub fn new() -> Self {
709 Self::default()
710 }
711
712 pub fn show(&mut self, message: String) {
713 self.active = true;
714 self.message = message;
715 self.focus_yes = true; }
717
718 pub fn hide(&mut self) {
719 self.active = false;
720 self.message.clear();
721 self.focus_yes = true;
722 }
723}
724
725#[cfg(any(feature = "http", feature = "cloud"))]
727#[derive(Clone)]
728pub enum PendingDownload {
729 #[cfg(feature = "http")]
730 Http {
731 url: String,
732 size: Option<u64>,
733 options: OpenOptions,
734 },
735 #[cfg(feature = "cloud")]
736 S3 {
737 url: String,
738 size: Option<u64>,
739 options: OpenOptions,
740 },
741 #[cfg(feature = "cloud")]
742 Gcs {
743 url: String,
744 size: Option<u64>,
745 options: OpenOptions,
746 },
747}
748
749#[derive(Clone, Debug, Default)]
750pub enum LoadingState {
751 #[default]
752 Idle,
753 Loading {
754 file_path: Option<PathBuf>,
756 file_size: u64, current_phase: String, progress_percent: u16, },
760 Exporting {
761 file_path: PathBuf,
762 current_phase: String, progress_percent: u16, },
765}
766
767impl LoadingState {
768 pub fn is_loading(&self) -> bool {
769 matches!(
770 self,
771 LoadingState::Loading { .. } | LoadingState::Exporting { .. }
772 )
773 }
774}
775
776#[allow(dead_code)]
778struct AnalysisComputationState {
779 df: Option<DataFrame>,
780 schema: Option<Arc<Schema>>,
781 partial_stats: Vec<crate::statistics::ColumnStatistics>,
782 current: usize,
783 total: usize,
784 total_rows: usize,
785 sample_seed: u64,
786 sample_size: Option<usize>,
787}
788
789fn active_query_settings(
792 dsl_query: &str,
793 sql_query: &str,
794 fuzzy_query: &str,
795) -> (Option<String>, Option<String>, Option<String>) {
796 let sql_trimmed = sql_query.trim();
797 let fuzzy_trimmed = fuzzy_query.trim();
798 let dsl_trimmed = dsl_query.trim();
799 if !sql_trimmed.is_empty() {
800 (None, Some(sql_trimmed.to_string()), None)
801 } else if !fuzzy_trimmed.is_empty() {
802 (None, None, Some(fuzzy_trimmed.to_string()))
803 } else if !dsl_trimmed.is_empty() {
804 (Some(dsl_trimmed.to_string()), None, None)
805 } else {
806 (None, None, None)
807 }
808}
809
810struct TemplateApplicationState {
812 lf: LazyFrame,
813 schema: Arc<Schema>,
814 active_query: String,
815 active_sql_query: String,
816 active_fuzzy_query: String,
817 filters: Vec<FilterStatement>,
818 sort_columns: Vec<String>,
819 sort_ascending: bool,
820 column_order: Vec<String>,
821 locked_columns_count: usize,
822}
823
824#[derive(Default)]
825pub(crate) struct ChartCache {
826 pub(crate) xy: Option<ChartCacheXY>,
827 pub(crate) x_range: Option<ChartCacheXRange>,
828 pub(crate) histogram: Option<ChartCacheHistogram>,
829 pub(crate) box_plot: Option<ChartCacheBoxPlot>,
830 pub(crate) kde: Option<ChartCacheKde>,
831 pub(crate) heatmap: Option<ChartCacheHeatmap>,
832}
833
834impl ChartCache {
835 fn clear(&mut self) {
836 *self = Self::default();
837 }
838}
839
840pub(crate) struct ChartCacheXY {
841 pub(crate) x_column: String,
842 pub(crate) y_columns: Vec<String>,
843 pub(crate) row_limit: Option<usize>,
844 pub(crate) series: Vec<Vec<(f64, f64)>>,
845 pub(crate) series_log: Option<Vec<Vec<(f64, f64)>>>,
846 pub(crate) x_axis_kind: chart_data::XAxisTemporalKind,
847}
848
849pub(crate) struct ChartCacheXRange {
850 pub(crate) x_column: String,
851 pub(crate) row_limit: Option<usize>,
852 pub(crate) x_min: f64,
853 pub(crate) x_max: f64,
854 pub(crate) x_axis_kind: chart_data::XAxisTemporalKind,
855}
856
857pub(crate) struct ChartCacheHistogram {
858 pub(crate) column: String,
859 pub(crate) bins: usize,
860 pub(crate) row_limit: Option<usize>,
861 pub(crate) data: chart_data::HistogramData,
862}
863
864pub(crate) struct ChartCacheBoxPlot {
865 pub(crate) column: String,
866 pub(crate) row_limit: Option<usize>,
867 pub(crate) data: chart_data::BoxPlotData,
868}
869
870pub(crate) struct ChartCacheKde {
871 pub(crate) column: String,
872 pub(crate) bandwidth_factor: f64,
873 pub(crate) row_limit: Option<usize>,
874 pub(crate) data: chart_data::KdeData,
875}
876
877pub(crate) struct ChartCacheHeatmap {
878 pub(crate) x_column: String,
879 pub(crate) y_column: String,
880 pub(crate) bins: usize,
881 pub(crate) row_limit: Option<usize>,
882 pub(crate) data: chart_data::HeatmapData,
883}
884
885pub struct App {
886 pub data_table_state: Option<DataTableState>,
887 path: Option<PathBuf>,
888 original_file_format: Option<ExportFormat>, original_file_delimiter: Option<u8>, events: Sender<AppEvent>,
891 focus: u32,
892 debug: DebugState,
893 info_modal: InfoModal,
894 parquet_metadata_cache: Option<ParquetMetadataCache>,
895 query_input: TextInput, sql_input: TextInput, fuzzy_input: TextInput, pub input_mode: InputMode,
899 input_type: Option<InputType>,
900 query_tab: QueryTab,
901 query_focus: QueryFocus,
902 pub sort_filter_modal: SortFilterModal,
903 pub pivot_melt_modal: PivotMeltModal,
904 pub template_modal: TemplateModal,
905 pub analysis_modal: AnalysisModal,
906 pub chart_modal: ChartModal,
907 pub chart_export_modal: ChartExportModal,
908 pub export_modal: ExportModal,
909 pub(crate) chart_cache: ChartCache,
910 error_modal: ErrorModal,
911 success_modal: SuccessModal,
912 confirmation_modal: ConfirmationModal,
913 pending_export: Option<(PathBuf, ExportFormat, ExportOptions)>, export_df: Option<DataFrame>,
916 pending_chart_export: Option<(PathBuf, ChartExportFormat, String, u32, u32)>,
917 #[cfg(any(feature = "http", feature = "cloud"))]
919 pending_download: Option<PendingDownload>,
920 show_help: bool,
921 help_scroll: usize, cache: CacheManager,
923 template_manager: TemplateManager,
924 active_template_id: Option<String>, loading_state: LoadingState, theme: Theme, sampling_threshold: Option<usize>, history_limit: usize, table_cell_padding: u16, column_colors: bool, busy: bool, throbber_frame: u8, drain_keys_on_next_loop: bool, analysis_computation: Option<AnalysisComputationState>,
935 app_config: AppConfig,
936 #[cfg(feature = "http")]
938 http_temp_path: Option<PathBuf>,
939}
940
941impl App {
942 pub fn should_drain_keys(&self) -> bool {
944 self.drain_keys_on_next_loop
945 }
946
947 pub fn clear_drain_keys_request(&mut self) {
949 self.drain_keys_on_next_loop = false;
950 }
951
952 pub fn send_event(&mut self, event: AppEvent) -> Result<()> {
953 self.events.send(event)?;
954 Ok(())
955 }
956
957 pub fn set_loading_phase(&mut self, phase: impl Into<String>, progress_percent: u16) {
960 self.busy = true;
961 self.loading_state = LoadingState::Loading {
962 file_path: None,
963 file_size: 0,
964 current_phase: phase.into(),
965 progress_percent,
966 };
967 }
968
969 fn ensure_file_extension(
973 path: &Path,
974 format: ExportFormat,
975 compression: Option<CompressionFormat>,
976 ) -> PathBuf {
977 let current_ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
978 let mut new_path = path.to_path_buf();
979
980 if current_ext.is_empty() {
981 let desired_ext = if let Some(comp) = compression {
983 format!("{}.{}", format.extension(), comp.extension())
984 } else {
985 format.extension().to_string()
986 };
987 new_path.set_extension(&desired_ext);
988 } else {
989 let is_compression_only = matches!(
991 current_ext.to_lowercase().as_str(),
992 "gz" | "zst" | "bz2" | "xz"
993 ) && ExportFormat::from_extension(current_ext).is_none();
994
995 if is_compression_only {
996 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
998 let stem_has_format = stem
999 .split('.')
1000 .next_back()
1001 .and_then(ExportFormat::from_extension)
1002 .is_some();
1003 if stem_has_format {
1004 if let Some(comp) = compression {
1005 if let Some(format_ext) = stem
1006 .split('.')
1007 .next_back()
1008 .and_then(ExportFormat::from_extension)
1009 .map(|f| f.extension())
1010 {
1011 new_path =
1012 PathBuf::from(stem.rsplit_once('.').map(|x| x.0).unwrap_or(stem));
1013 new_path.set_extension(format!("{}.{}", format_ext, comp.extension()));
1014 }
1015 }
1016 } else if let Some(comp) = compression {
1017 new_path.set_extension(format!("{}.{}", format.extension(), comp.extension()));
1018 } else {
1019 new_path.set_extension(format.extension());
1020 }
1021 } else if let Some(comp) = compression {
1022 if format.supports_compression() {
1023 new_path.set_extension(format!("{}.{}", current_ext, comp.extension()));
1024 }
1025 }
1027 }
1029
1030 new_path
1031 }
1032
1033 pub fn new(events: Sender<AppEvent>) -> App {
1034 let theme = Theme::from_config(&AppConfig::default().theme).unwrap_or_else(|_| {
1036 Theme {
1038 colors: std::collections::HashMap::new(),
1039 }
1040 });
1041
1042 Self::new_with_config(events, theme, AppConfig::default())
1043 }
1044
1045 pub fn new_with_theme(events: Sender<AppEvent>, theme: Theme) -> App {
1046 Self::new_with_config(events, theme, AppConfig::default())
1047 }
1048
1049 pub fn new_with_config(events: Sender<AppEvent>, theme: Theme, app_config: AppConfig) -> App {
1050 let cache = CacheManager::new(APP_NAME).unwrap_or_else(|_| CacheManager {
1051 cache_dir: std::env::temp_dir().join(APP_NAME),
1052 });
1053
1054 let config_manager = ConfigManager::new(APP_NAME).unwrap_or_else(|_| ConfigManager {
1055 config_dir: std::env::temp_dir().join(APP_NAME).join("config"),
1056 });
1057
1058 let template_manager = TemplateManager::new(&config_manager).unwrap_or_else(|_| {
1059 let temp_config = ConfigManager::new("datui").unwrap_or_else(|_| ConfigManager {
1060 config_dir: std::env::temp_dir().join("datui").join("config"),
1061 });
1062 TemplateManager::new(&temp_config).unwrap_or_else(|_| {
1063 let last_resort = ConfigManager {
1064 config_dir: std::env::temp_dir().join("datui_config"),
1065 };
1066 TemplateManager::new(&last_resort)
1067 .unwrap_or_else(|_| TemplateManager::empty(&last_resort))
1068 })
1069 });
1070
1071 App {
1072 path: None,
1073 data_table_state: None,
1074 original_file_format: None,
1075 original_file_delimiter: None,
1076 events,
1077 focus: 0,
1078 debug: DebugState::default(),
1079 info_modal: InfoModal::new(),
1080 parquet_metadata_cache: None,
1081 query_input: TextInput::new()
1082 .with_history_limit(app_config.query.history_limit)
1083 .with_theme(&theme)
1084 .with_history("query".to_string()),
1085 sql_input: TextInput::new()
1086 .with_history_limit(app_config.query.history_limit)
1087 .with_theme(&theme)
1088 .with_history("sql".to_string()),
1089 fuzzy_input: TextInput::new()
1090 .with_history_limit(app_config.query.history_limit)
1091 .with_theme(&theme)
1092 .with_history("fuzzy".to_string()),
1093 input_mode: InputMode::Normal,
1094 input_type: None,
1095 query_tab: QueryTab::SqlLike,
1096 query_focus: QueryFocus::Input,
1097 sort_filter_modal: SortFilterModal::new(),
1098 pivot_melt_modal: PivotMeltModal::new(),
1099 template_modal: TemplateModal::new(),
1100 analysis_modal: AnalysisModal::new(),
1101 chart_modal: ChartModal::new(),
1102 chart_export_modal: ChartExportModal::new(),
1103 export_modal: ExportModal::new(),
1104 chart_cache: ChartCache::default(),
1105 error_modal: ErrorModal::new(),
1106 success_modal: SuccessModal::new(),
1107 confirmation_modal: ConfirmationModal::new(),
1108 pending_export: None,
1109 export_df: None,
1110 pending_chart_export: None,
1111 #[cfg(any(feature = "http", feature = "cloud"))]
1112 pending_download: None,
1113 show_help: false,
1114 help_scroll: 0,
1115 cache,
1116 template_manager,
1117 active_template_id: None,
1118 loading_state: LoadingState::Idle,
1119 theme,
1120 sampling_threshold: app_config.performance.sampling_threshold,
1121 history_limit: app_config.query.history_limit,
1122 table_cell_padding: app_config.display.table_cell_padding.min(u16::MAX as usize) as u16,
1123 column_colors: app_config.display.column_colors,
1124 busy: false,
1125 throbber_frame: 0,
1126 drain_keys_on_next_loop: false,
1127 analysis_computation: None,
1128 app_config,
1129 #[cfg(feature = "http")]
1130 http_temp_path: None,
1131 }
1132 }
1133
1134 pub fn enable_debug(&mut self) {
1135 self.debug.enabled = true;
1136 }
1137
1138 fn color(&self, name: &str) -> Color {
1140 self.theme.get(name)
1141 }
1142
1143 fn load(&mut self, paths: &[PathBuf], options: &OpenOptions) -> Result<()> {
1144 self.parquet_metadata_cache = None;
1145 self.export_df = None;
1146 let path = &paths[0]; let compression = options
1149 .compression
1150 .or_else(|| CompressionFormat::from_extension(path));
1151 let is_csv = options.format == Some(FileFormat::Csv)
1152 || path
1153 .file_stem()
1154 .and_then(|stem| stem.to_str())
1155 .map(|stem| {
1156 stem.ends_with(".csv")
1157 || path
1158 .extension()
1159 .and_then(|e| e.to_str())
1160 .map(|e| e.eq_ignore_ascii_case("csv"))
1161 .unwrap_or(false)
1162 })
1163 .unwrap_or(false);
1164 let is_compressed_csv = paths.len() == 1 && compression.is_some() && is_csv;
1165
1166 if is_compressed_csv {
1169 if let LoadingState::Loading {
1171 file_path,
1172 file_size,
1173 ..
1174 } = &self.loading_state
1175 {
1176 self.loading_state = LoadingState::Loading {
1177 file_path: file_path.clone(),
1178 file_size: *file_size,
1179 current_phase: if options.parse_strings.is_some() {
1180 "Scanning string columns".to_string()
1181 } else {
1182 "Reading data".to_string()
1183 },
1184 progress_percent: if options.parse_strings.is_some() {
1185 55
1186 } else {
1187 50
1188 },
1189 };
1190 }
1191 let lf = DataTableState::from_csv(path, options)?; if let LoadingState::Loading {
1195 file_path,
1196 file_size,
1197 ..
1198 } = &self.loading_state
1199 {
1200 self.loading_state = LoadingState::Loading {
1201 file_path: file_path.clone(),
1202 file_size: *file_size,
1203 current_phase: "Building lazyframe".to_string(),
1204 progress_percent: 60,
1205 };
1206 }
1207
1208 if let LoadingState::Loading {
1210 file_path,
1211 file_size,
1212 ..
1213 } = &self.loading_state
1214 {
1215 self.loading_state = LoadingState::Loading {
1216 file_path: file_path.clone(),
1217 file_size: *file_size,
1218 current_phase: "Loading buffer".to_string(),
1219 progress_percent: 70,
1220 };
1221 }
1222
1223 self.data_table_state = Some(lf);
1224 self.path = Some(path.clone());
1225 let original_format =
1226 path.file_stem()
1227 .and_then(|stem| stem.to_str())
1228 .and_then(|stem| {
1229 if stem.ends_with(".csv") {
1230 Some(ExportFormat::Csv)
1231 } else {
1232 None
1233 }
1234 });
1235 self.original_file_format = original_format;
1236 self.original_file_delimiter = Some(options.delimiter.unwrap_or(b','));
1237 self.sort_filter_modal = SortFilterModal::new();
1238 self.pivot_melt_modal = PivotMeltModal::new();
1239 return Ok(());
1240 }
1241
1242 if paths.len() == 1 && options.hive {
1245 let path_str = path.as_os_str().to_string_lossy();
1246 let is_single_file = path.exists()
1247 && path.is_file()
1248 && !path_str.contains('*')
1249 && !path_str.contains("**");
1250 if !is_single_file {
1251 let use_parquet_hive = path.is_dir()
1253 || path_str.contains(".parquet")
1254 || path_str.contains("*.parquet");
1255 if use_parquet_hive {
1256 if let LoadingState::Loading {
1257 file_path,
1258 file_size,
1259 ..
1260 } = &self.loading_state
1261 {
1262 self.loading_state = LoadingState::Loading {
1263 file_path: file_path.clone(),
1264 file_size: *file_size,
1265 current_phase: "Scanning partitioned dataset".to_string(),
1266 progress_percent: 60,
1267 };
1268 }
1269 let lf = DataTableState::from_parquet_hive(
1270 path,
1271 options.pages_lookahead,
1272 options.pages_lookback,
1273 options.max_buffered_rows,
1274 options.max_buffered_mb,
1275 options.row_numbers,
1276 options.row_start_index,
1277 )?;
1278 if let LoadingState::Loading {
1279 file_path,
1280 file_size,
1281 ..
1282 } = &self.loading_state
1283 {
1284 self.loading_state = LoadingState::Loading {
1285 file_path: file_path.clone(),
1286 file_size: *file_size,
1287 current_phase: "Rendering data".to_string(),
1288 progress_percent: 90,
1289 };
1290 }
1291 self.loading_state = LoadingState::Idle;
1292 self.data_table_state = Some(lf);
1293 self.path = Some(path.clone());
1294 self.original_file_format = Some(ExportFormat::Parquet);
1295 self.original_file_delimiter = None;
1296 self.sort_filter_modal = SortFilterModal::new();
1297 self.pivot_melt_modal = PivotMeltModal::new();
1298 return Ok(());
1299 }
1300 self.loading_state = LoadingState::Idle;
1301 return Err(color_eyre::eyre::eyre!(
1302 "With --hive use a directory or a glob pattern for Parquet (e.g. path/to/dir or path/**/*.parquet)"
1303 ));
1304 }
1305 }
1306
1307 let effective_format = options.format.or_else(|| FileFormat::from_path(path));
1310 let csv_parse_strings =
1311 effective_format == Some(FileFormat::Csv) && options.parse_strings.is_some();
1312 if let LoadingState::Loading {
1313 file_path,
1314 file_size,
1315 ..
1316 } = &self.loading_state
1317 {
1318 self.loading_state = LoadingState::Loading {
1319 file_path: file_path.clone(),
1320 file_size: *file_size,
1321 current_phase: if csv_parse_strings {
1322 "Scanning string columns".to_string()
1323 } else {
1324 "Building lazyframe".to_string()
1325 },
1326 progress_percent: if csv_parse_strings { 55 } else { 60 },
1327 };
1328 }
1329
1330 let original_format = effective_format
1332 .and_then(file_format_to_export_format)
1333 .or_else(|| {
1334 path.extension().and_then(|e| e.to_str()).and_then(|ext| {
1335 if ext.eq_ignore_ascii_case("parquet") {
1336 Some(ExportFormat::Parquet)
1337 } else if ext.eq_ignore_ascii_case("csv") {
1338 Some(ExportFormat::Csv)
1339 } else if ext.eq_ignore_ascii_case("json") {
1340 Some(ExportFormat::Json)
1341 } else if ext.eq_ignore_ascii_case("jsonl")
1342 || ext.eq_ignore_ascii_case("ndjson")
1343 {
1344 Some(ExportFormat::Ndjson)
1345 } else if ext.eq_ignore_ascii_case("arrow")
1346 || ext.eq_ignore_ascii_case("ipc")
1347 || ext.eq_ignore_ascii_case("feather")
1348 {
1349 Some(ExportFormat::Ipc)
1350 } else if ext.eq_ignore_ascii_case("avro") {
1351 Some(ExportFormat::Avro)
1352 } else {
1353 None
1354 }
1355 })
1356 });
1357
1358 let lf = if paths.len() > 1 {
1359 match effective_format {
1361 Some(FileFormat::Parquet) => DataTableState::from_parquet_paths(
1362 paths,
1363 options.pages_lookahead,
1364 options.pages_lookback,
1365 options.max_buffered_rows,
1366 options.max_buffered_mb,
1367 options.row_numbers,
1368 options.row_start_index,
1369 )?,
1370 Some(FileFormat::Csv) => DataTableState::from_csv_paths(paths, options)?,
1371 Some(FileFormat::Json) => DataTableState::from_json_paths(
1372 paths,
1373 options.pages_lookahead,
1374 options.pages_lookback,
1375 options.max_buffered_rows,
1376 options.max_buffered_mb,
1377 options.row_numbers,
1378 options.row_start_index,
1379 )?,
1380 Some(FileFormat::Jsonl) => DataTableState::from_json_lines_paths(
1381 paths,
1382 options.pages_lookahead,
1383 options.pages_lookback,
1384 options.max_buffered_rows,
1385 options.max_buffered_mb,
1386 options.row_numbers,
1387 options.row_start_index,
1388 )?,
1389 Some(FileFormat::Arrow) => DataTableState::from_ipc_paths(
1390 paths,
1391 options.pages_lookahead,
1392 options.pages_lookback,
1393 options.max_buffered_rows,
1394 options.max_buffered_mb,
1395 options.row_numbers,
1396 options.row_start_index,
1397 )?,
1398 Some(FileFormat::Avro) => DataTableState::from_avro_paths(
1399 paths,
1400 options.pages_lookahead,
1401 options.pages_lookback,
1402 options.max_buffered_rows,
1403 options.max_buffered_mb,
1404 options.row_numbers,
1405 options.row_start_index,
1406 )?,
1407 Some(FileFormat::Orc) => DataTableState::from_orc_paths(
1408 paths,
1409 options.pages_lookahead,
1410 options.pages_lookback,
1411 options.max_buffered_rows,
1412 options.max_buffered_mb,
1413 options.row_numbers,
1414 options.row_start_index,
1415 )?,
1416 Some(FileFormat::Tsv) | Some(FileFormat::Psv) | Some(FileFormat::Excel) | None => {
1417 self.loading_state = LoadingState::Idle;
1418 if !paths.is_empty() && !path.exists() {
1419 return Err(std::io::Error::new(
1420 std::io::ErrorKind::NotFound,
1421 format!("File not found: {}", path.display()),
1422 )
1423 .into());
1424 }
1425 return Err(color_eyre::eyre::eyre!(
1426 "Unsupported file type for multiple files (parquet, csv, json, jsonl, ndjson, arrow/ipc/feather, avro, orc only)"
1427 ));
1428 }
1429 }
1430 } else {
1431 match effective_format {
1432 Some(FileFormat::Parquet) => DataTableState::from_parquet(
1433 path,
1434 options.pages_lookahead,
1435 options.pages_lookback,
1436 options.max_buffered_rows,
1437 options.max_buffered_mb,
1438 options.row_numbers,
1439 options.row_start_index,
1440 )?,
1441 Some(FileFormat::Csv) => DataTableState::from_csv(path, options)?,
1442 Some(FileFormat::Tsv) => DataTableState::from_delimited(path, b'\t', options)?,
1443 Some(FileFormat::Psv) => DataTableState::from_delimited(path, b'|', options)?,
1444 Some(FileFormat::Json) => DataTableState::from_json(
1445 path,
1446 options.pages_lookahead,
1447 options.pages_lookback,
1448 options.max_buffered_rows,
1449 options.max_buffered_mb,
1450 options.row_numbers,
1451 options.row_start_index,
1452 )?,
1453 Some(FileFormat::Jsonl) => DataTableState::from_json_lines(
1454 path,
1455 options.pages_lookahead,
1456 options.pages_lookback,
1457 options.max_buffered_rows,
1458 options.max_buffered_mb,
1459 options.row_numbers,
1460 options.row_start_index,
1461 )?,
1462 Some(FileFormat::Arrow) => DataTableState::from_ipc(
1463 path,
1464 options.pages_lookahead,
1465 options.pages_lookback,
1466 options.max_buffered_rows,
1467 options.max_buffered_mb,
1468 options.row_numbers,
1469 options.row_start_index,
1470 )?,
1471 Some(FileFormat::Avro) => DataTableState::from_avro(
1472 path,
1473 options.pages_lookahead,
1474 options.pages_lookback,
1475 options.max_buffered_rows,
1476 options.max_buffered_mb,
1477 options.row_numbers,
1478 options.row_start_index,
1479 )?,
1480 Some(FileFormat::Excel) => DataTableState::from_excel(
1481 path,
1482 options.pages_lookahead,
1483 options.pages_lookback,
1484 options.max_buffered_rows,
1485 options.max_buffered_mb,
1486 options.row_numbers,
1487 options.row_start_index,
1488 options.excel_sheet.as_deref(),
1489 )?,
1490 Some(FileFormat::Orc) => DataTableState::from_orc(
1491 path,
1492 options.pages_lookahead,
1493 options.pages_lookback,
1494 options.max_buffered_rows,
1495 options.max_buffered_mb,
1496 options.row_numbers,
1497 options.row_start_index,
1498 )?,
1499 None => {
1500 self.loading_state = LoadingState::Idle;
1501 if paths.len() == 1 && !path.exists() {
1502 return Err(std::io::Error::new(
1503 std::io::ErrorKind::NotFound,
1504 format!("File not found: {}", path.display()),
1505 )
1506 .into());
1507 }
1508 return Err(color_eyre::eyre::eyre!("Unsupported file type"));
1509 }
1510 }
1511 };
1512
1513 if let LoadingState::Loading {
1515 file_path,
1516 file_size,
1517 ..
1518 } = &self.loading_state
1519 {
1520 self.loading_state = LoadingState::Loading {
1521 file_path: file_path.clone(),
1522 file_size: *file_size,
1523 current_phase: "Rendering data".to_string(),
1524 progress_percent: 90,
1525 };
1526 }
1527
1528 self.loading_state = LoadingState::Idle;
1530 self.data_table_state = Some(lf);
1531 self.path = Some(path.clone());
1532 self.original_file_format = original_format;
1533 self.original_file_delimiter = match effective_format {
1535 Some(FileFormat::Csv) => Some(options.delimiter.unwrap_or(b',')),
1536 Some(FileFormat::Tsv) => Some(b'\t'),
1537 Some(FileFormat::Psv) => Some(b'|'),
1538 _ => path.extension().and_then(|e| e.to_str()).and_then(|ext| {
1539 if ext.eq_ignore_ascii_case("csv") {
1540 Some(options.delimiter.unwrap_or(b','))
1541 } else if ext.eq_ignore_ascii_case("tsv") {
1542 Some(b'\t')
1543 } else if ext.eq_ignore_ascii_case("psv") {
1544 Some(b'|')
1545 } else {
1546 None
1547 }
1548 }),
1549 };
1550 self.sort_filter_modal = SortFilterModal::new();
1551 self.pivot_melt_modal = PivotMeltModal::new();
1552 Ok(())
1553 }
1554
1555 #[cfg(feature = "cloud")]
1556 fn build_s3_cloud_options(
1557 cloud: &crate::config::CloudConfig,
1558 options: &OpenOptions,
1559 ) -> CloudOptions {
1560 let mut opts = CloudOptions::default();
1561 let mut configs: Vec<(AmazonS3ConfigKey, String)> = Vec::new();
1562 let e = options
1563 .s3_endpoint_url_override
1564 .as_ref()
1565 .or(cloud.s3_endpoint_url.as_ref());
1566 let k = options
1567 .s3_access_key_id_override
1568 .as_ref()
1569 .or(cloud.s3_access_key_id.as_ref());
1570 let s = options
1571 .s3_secret_access_key_override
1572 .as_ref()
1573 .or(cloud.s3_secret_access_key.as_ref());
1574 let r = options
1575 .s3_region_override
1576 .as_ref()
1577 .or(cloud.s3_region.as_ref());
1578 if let Some(e) = e {
1579 configs.push((AmazonS3ConfigKey::Endpoint, e.clone()));
1580 }
1581 if let Some(k) = k {
1582 configs.push((AmazonS3ConfigKey::AccessKeyId, k.clone()));
1583 }
1584 if let Some(s) = s {
1585 configs.push((AmazonS3ConfigKey::SecretAccessKey, s.clone()));
1586 }
1587 if let Some(r) = r {
1588 configs.push((AmazonS3ConfigKey::Region, r.clone()));
1589 }
1590 if !configs.is_empty() {
1591 opts = opts.with_aws(configs);
1592 }
1593 opts
1594 }
1595
1596 #[cfg(feature = "cloud")]
1597 fn build_s3_object_store(
1598 s3_url: &str,
1599 cloud: &crate::config::CloudConfig,
1600 options: &OpenOptions,
1601 ) -> Result<Arc<dyn object_store::ObjectStore>> {
1602 let (path_part, _ext) = source::url_path_extension(s3_url);
1603 let (bucket, _key) = path_part
1604 .split_once('/')
1605 .ok_or_else(|| color_eyre::eyre::eyre!("S3 URL must be s3://bucket/key"))?;
1606 let mut builder = object_store::aws::AmazonS3Builder::from_env()
1607 .with_url(s3_url)
1608 .with_bucket_name(bucket);
1609 let e = options
1610 .s3_endpoint_url_override
1611 .as_ref()
1612 .or(cloud.s3_endpoint_url.as_ref());
1613 let k = options
1614 .s3_access_key_id_override
1615 .as_ref()
1616 .or(cloud.s3_access_key_id.as_ref());
1617 let s = options
1618 .s3_secret_access_key_override
1619 .as_ref()
1620 .or(cloud.s3_secret_access_key.as_ref());
1621 let r = options
1622 .s3_region_override
1623 .as_ref()
1624 .or(cloud.s3_region.as_ref());
1625 if let Some(e) = e {
1626 builder = builder.with_endpoint(e);
1627 }
1628 if let Some(k) = k {
1629 builder = builder.with_access_key_id(k);
1630 }
1631 if let Some(s) = s {
1632 builder = builder.with_secret_access_key(s);
1633 }
1634 if let Some(r) = r {
1635 builder = builder.with_region(r);
1636 }
1637 let store = builder
1638 .build()
1639 .map_err(|e| color_eyre::eyre::eyre!("S3 config failed: {}", e))?;
1640 Ok(Arc::new(store))
1641 }
1642
1643 #[cfg(feature = "cloud")]
1644 fn build_gcs_object_store(gs_url: &str) -> Result<Arc<dyn object_store::ObjectStore>> {
1645 let (path_part, _ext) = source::url_path_extension(gs_url);
1646 let (bucket, _key) = path_part
1647 .split_once('/')
1648 .ok_or_else(|| color_eyre::eyre::eyre!("GCS URL must be gs://bucket/key"))?;
1649 let store = object_store::gcp::GoogleCloudStorageBuilder::from_env()
1650 .with_url(gs_url)
1651 .with_bucket_name(bucket)
1652 .build()
1653 .map_err(|e| color_eyre::eyre::eyre!("GCS config failed: {}", e))?;
1654 Ok(Arc::new(store))
1655 }
1656
1657 fn format_bytes(n: u64) -> String {
1659 const KB: u64 = 1024;
1660 const MB: u64 = KB * 1024;
1661 const GB: u64 = MB * 1024;
1662 const TB: u64 = GB * 1024;
1663 if n >= TB {
1664 format!("{:.2} TB", n as f64 / TB as f64)
1665 } else if n >= GB {
1666 format!("{:.2} GB", n as f64 / GB as f64)
1667 } else if n >= MB {
1668 format!("{:.2} MB", n as f64 / MB as f64)
1669 } else if n >= KB {
1670 format!("{:.2} KB", n as f64 / KB as f64)
1671 } else {
1672 format!("{} bytes", n)
1673 }
1674 }
1675
1676 #[cfg(feature = "http")]
1677 fn fetch_remote_size_http(url: &str) -> Result<Option<u64>> {
1678 let response = ureq::request("HEAD", url)
1679 .timeout(std::time::Duration::from_secs(15))
1680 .call();
1681 match response {
1682 Ok(r) => Ok(r
1683 .header("Content-Length")
1684 .and_then(|s| s.parse::<u64>().ok())),
1685 Err(_) => Ok(None),
1686 }
1687 }
1688
1689 #[cfg(feature = "cloud")]
1690 fn fetch_remote_size_s3(
1691 s3_url: &str,
1692 cloud: &crate::config::CloudConfig,
1693 options: &OpenOptions,
1694 ) -> Result<Option<u64>> {
1695 use object_store::path::Path as OsPath;
1696 use object_store::ObjectStore;
1697
1698 let (path_part, _ext) = source::url_path_extension(s3_url);
1699 let (bucket, key) = path_part
1700 .split_once('/')
1701 .ok_or_else(|| color_eyre::eyre::eyre!("S3 URL must be s3://bucket/key"))?;
1702 if key.is_empty() {
1703 return Ok(None);
1704 }
1705 let mut builder = object_store::aws::AmazonS3Builder::from_env()
1706 .with_url(s3_url)
1707 .with_bucket_name(bucket);
1708 let e = options
1709 .s3_endpoint_url_override
1710 .as_ref()
1711 .or(cloud.s3_endpoint_url.as_ref());
1712 let k = options
1713 .s3_access_key_id_override
1714 .as_ref()
1715 .or(cloud.s3_access_key_id.as_ref());
1716 let s = options
1717 .s3_secret_access_key_override
1718 .as_ref()
1719 .or(cloud.s3_secret_access_key.as_ref());
1720 let r = options
1721 .s3_region_override
1722 .as_ref()
1723 .or(cloud.s3_region.as_ref());
1724 if let Some(e) = e {
1725 builder = builder.with_endpoint(e);
1726 }
1727 if let Some(k) = k {
1728 builder = builder.with_access_key_id(k);
1729 }
1730 if let Some(s) = s {
1731 builder = builder.with_secret_access_key(s);
1732 }
1733 if let Some(r) = r {
1734 builder = builder.with_region(r);
1735 }
1736 let store = builder
1737 .build()
1738 .map_err(|e| color_eyre::eyre::eyre!("S3 config failed: {}", e))?;
1739 let rt = tokio::runtime::Runtime::new()
1740 .map_err(|e| color_eyre::eyre::eyre!("Could not start runtime: {}", e))?;
1741 let path = OsPath::from(key);
1742 match rt.block_on(store.head(&path)) {
1743 Ok(meta) => Ok(Some(meta.size)),
1744 Err(_) => Ok(None),
1745 }
1746 }
1747
1748 #[cfg(feature = "cloud")]
1749 fn fetch_remote_size_gcs(gs_url: &str, _options: &OpenOptions) -> Result<Option<u64>> {
1750 use object_store::path::Path as OsPath;
1751 use object_store::ObjectStore;
1752
1753 let (path_part, _ext) = source::url_path_extension(gs_url);
1754 let (bucket, key) = path_part
1755 .split_once('/')
1756 .ok_or_else(|| color_eyre::eyre::eyre!("GCS URL must be gs://bucket/key"))?;
1757 if key.is_empty() {
1758 return Ok(None);
1759 }
1760 let store = object_store::gcp::GoogleCloudStorageBuilder::from_env()
1761 .with_url(gs_url)
1762 .with_bucket_name(bucket)
1763 .build()
1764 .map_err(|e| color_eyre::eyre::eyre!("GCS config failed: {}", e))?;
1765 let rt = tokio::runtime::Runtime::new()
1766 .map_err(|e| color_eyre::eyre::eyre!("Could not start runtime: {}", e))?;
1767 let path = OsPath::from(key);
1768 match rt.block_on(store.head(&path)) {
1769 Ok(meta) => Ok(Some(meta.size)),
1770 Err(_) => Ok(None),
1771 }
1772 }
1773
1774 #[cfg(feature = "http")]
1775 fn download_http_to_temp(
1776 url: &str,
1777 temp_dir: Option<&Path>,
1778 extension: Option<&str>,
1779 ) -> Result<PathBuf> {
1780 let dir = temp_dir
1781 .map(Path::to_path_buf)
1782 .unwrap_or_else(std::env::temp_dir);
1783 let suffix = extension
1784 .map(|e| format!(".{e}"))
1785 .unwrap_or_else(|| ".tmp".to_string());
1786 let mut temp = tempfile::Builder::new()
1787 .suffix(&suffix)
1788 .tempfile_in(&dir)
1789 .map_err(|_| color_eyre::eyre::eyre!("Could not create a temporary file."))?;
1790 let response = ureq::get(url)
1791 .timeout(std::time::Duration::from_secs(300))
1792 .call()
1793 .map_err(|e| {
1794 color_eyre::eyre::eyre!("Download failed. Check the URL and your connection: {}", e)
1795 })?;
1796 let status = response.status();
1797 if status >= 400 {
1798 return Err(color_eyre::eyre::eyre!(
1799 "Server returned {} {}. Check the URL.",
1800 status,
1801 response.status_text()
1802 ));
1803 }
1804 std::io::copy(&mut response.into_reader(), &mut temp)
1805 .map_err(|_| color_eyre::eyre::eyre!("Download failed while saving the file."))?;
1806 let (_file, path) = temp
1807 .keep()
1808 .map_err(|_| color_eyre::eyre::eyre!("Could not save the downloaded file."))?;
1809 Ok(path)
1810 }
1811
1812 #[cfg(feature = "cloud")]
1813 fn download_s3_to_temp(
1814 s3_url: &str,
1815 cloud: &crate::config::CloudConfig,
1816 options: &OpenOptions,
1817 ) -> Result<PathBuf> {
1818 use object_store::path::Path as OsPath;
1819 use object_store::ObjectStore;
1820
1821 let (path_part, ext) = source::url_path_extension(s3_url);
1822 let (bucket, key) = path_part
1823 .split_once('/')
1824 .ok_or_else(|| color_eyre::eyre::eyre!("S3 URL must be s3://bucket/key"))?;
1825 if key.is_empty() {
1826 return Err(color_eyre::eyre::eyre!(
1827 "S3 URL must point to an object (e.g. s3://bucket/path/file.csv)"
1828 ));
1829 }
1830
1831 let mut builder = object_store::aws::AmazonS3Builder::from_env()
1832 .with_url(s3_url)
1833 .with_bucket_name(bucket);
1834 let e = options
1835 .s3_endpoint_url_override
1836 .as_ref()
1837 .or(cloud.s3_endpoint_url.as_ref());
1838 let k = options
1839 .s3_access_key_id_override
1840 .as_ref()
1841 .or(cloud.s3_access_key_id.as_ref());
1842 let s = options
1843 .s3_secret_access_key_override
1844 .as_ref()
1845 .or(cloud.s3_secret_access_key.as_ref());
1846 let r = options
1847 .s3_region_override
1848 .as_ref()
1849 .or(cloud.s3_region.as_ref());
1850 if let Some(e) = e {
1851 builder = builder.with_endpoint(e);
1852 }
1853 if let Some(k) = k {
1854 builder = builder.with_access_key_id(k);
1855 }
1856 if let Some(s) = s {
1857 builder = builder.with_secret_access_key(s);
1858 }
1859 if let Some(r) = r {
1860 builder = builder.with_region(r);
1861 }
1862 let store = builder
1863 .build()
1864 .map_err(|e| color_eyre::eyre::eyre!("S3 config failed: {}", e))?;
1865
1866 let rt = tokio::runtime::Runtime::new()
1867 .map_err(|e| color_eyre::eyre::eyre!("Could not start runtime: {}", e))?;
1868 let path = OsPath::from(key);
1869 let get_result = rt.block_on(store.get(&path)).map_err(|e| {
1870 color_eyre::eyre::eyre!("Could not read from S3. Check credentials and URL: {}", e)
1871 })?;
1872 let bytes = rt
1873 .block_on(get_result.bytes())
1874 .map_err(|e| color_eyre::eyre::eyre!("Could not read S3 object body: {}", e))?;
1875
1876 let dir = options.temp_dir.clone().unwrap_or_else(std::env::temp_dir);
1877 let suffix = ext
1878 .as_ref()
1879 .map(|e| format!(".{e}"))
1880 .unwrap_or_else(|| ".tmp".to_string());
1881 let mut temp = tempfile::Builder::new()
1882 .suffix(&suffix)
1883 .tempfile_in(&dir)
1884 .map_err(|_| color_eyre::eyre::eyre!("Could not create a temporary file."))?;
1885 std::io::copy(&mut std::io::Cursor::new(bytes.as_ref()), &mut temp)
1886 .map_err(|_| color_eyre::eyre::eyre!("Could not write downloaded file."))?;
1887 let (_file, path_buf) = temp
1888 .keep()
1889 .map_err(|_| color_eyre::eyre::eyre!("Could not save the downloaded file."))?;
1890 Ok(path_buf)
1891 }
1892
1893 #[cfg(feature = "cloud")]
1894 fn download_gcs_to_temp(gs_url: &str, options: &OpenOptions) -> Result<PathBuf> {
1895 use object_store::path::Path as OsPath;
1896 use object_store::ObjectStore;
1897
1898 let (path_part, ext) = source::url_path_extension(gs_url);
1899 let (bucket, key) = path_part
1900 .split_once('/')
1901 .ok_or_else(|| color_eyre::eyre::eyre!("GCS URL must be gs://bucket/key"))?;
1902 if key.is_empty() {
1903 return Err(color_eyre::eyre::eyre!(
1904 "GCS URL must point to an object (e.g. gs://bucket/path/file.csv)"
1905 ));
1906 }
1907
1908 let store = object_store::gcp::GoogleCloudStorageBuilder::from_env()
1909 .with_url(gs_url)
1910 .with_bucket_name(bucket)
1911 .build()
1912 .map_err(|e| color_eyre::eyre::eyre!("GCS config failed: {}", e))?;
1913
1914 let rt = tokio::runtime::Runtime::new()
1915 .map_err(|e| color_eyre::eyre::eyre!("Could not start runtime: {}", e))?;
1916 let path = OsPath::from(key);
1917 let get_result = rt.block_on(store.get(&path)).map_err(|e| {
1918 color_eyre::eyre::eyre!("Could not read from GCS. Check credentials and URL: {}", e)
1919 })?;
1920 let bytes = rt
1921 .block_on(get_result.bytes())
1922 .map_err(|e| color_eyre::eyre::eyre!("Could not read GCS object body: {}", e))?;
1923
1924 let dir = options.temp_dir.clone().unwrap_or_else(std::env::temp_dir);
1925 let suffix = ext
1926 .as_ref()
1927 .map(|e| format!(".{e}"))
1928 .unwrap_or_else(|| ".tmp".to_string());
1929 let mut temp = tempfile::Builder::new()
1930 .suffix(&suffix)
1931 .tempfile_in(&dir)
1932 .map_err(|_| color_eyre::eyre::eyre!("Could not create a temporary file."))?;
1933 std::io::copy(&mut std::io::Cursor::new(bytes.as_ref()), &mut temp)
1934 .map_err(|_| color_eyre::eyre::eyre!("Could not write downloaded file."))?;
1935 let (_file, path_buf) = temp
1936 .keep()
1937 .map_err(|_| color_eyre::eyre::eyre!("Could not save the downloaded file."))?;
1938 Ok(path_buf)
1939 }
1940
1941 fn build_lazyframe_from_paths(
1943 &mut self,
1944 paths: &[PathBuf],
1945 options: &OpenOptions,
1946 ) -> Result<LazyFrame> {
1947 let path = &paths[0];
1948 match source::input_source(path) {
1949 source::InputSource::Http(_url) => {
1950 #[cfg(feature = "http")]
1951 {
1952 return Err(color_eyre::eyre::eyre!(
1953 "HTTP/HTTPS load is handled in the event loop; this path should not be reached."
1954 ));
1955 }
1956 #[cfg(not(feature = "http"))]
1957 {
1958 return Err(color_eyre::eyre::eyre!(
1959 "HTTP/HTTPS URLs are not supported in this build. Rebuild with default features."
1960 ));
1961 }
1962 }
1963 source::InputSource::S3(url) => {
1964 #[cfg(feature = "cloud")]
1965 {
1966 let full = format!("s3://{url}");
1967 let cloud_opts = Self::build_s3_cloud_options(&self.app_config.cloud, options);
1968 let pl_path = PlPathRef::new(&full).into_owned();
1969 let is_glob = full.contains('*') || full.ends_with('/');
1970 let hive_options = if is_glob {
1971 polars::io::HiveOptions::new_enabled()
1972 } else {
1973 polars::io::HiveOptions::default()
1974 };
1975 let args = ScanArgsParquet {
1976 cloud_options: Some(cloud_opts),
1977 hive_options,
1978 glob: is_glob,
1979 ..Default::default()
1980 };
1981 let lf = LazyFrame::scan_parquet(pl_path, args).map_err(|e| {
1982 color_eyre::eyre::eyre!(
1983 "Could not read from S3. Check credentials and URL: {}",
1984 e
1985 )
1986 })?;
1987 let state = DataTableState::from_lazyframe(lf, options)?;
1988 return Ok(state.lf);
1989 }
1990 #[cfg(not(feature = "cloud"))]
1991 {
1992 return Err(color_eyre::eyre::eyre!(
1993 "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)."
1994 ));
1995 }
1996 }
1997 source::InputSource::Gcs(url) => {
1998 #[cfg(feature = "cloud")]
1999 {
2000 let full = format!("gs://{url}");
2001 let pl_path = PlPathRef::new(&full).into_owned();
2002 let is_glob = full.contains('*') || full.ends_with('/');
2003 let hive_options = if is_glob {
2004 polars::io::HiveOptions::new_enabled()
2005 } else {
2006 polars::io::HiveOptions::default()
2007 };
2008 let args = ScanArgsParquet {
2009 cloud_options: Some(CloudOptions::default()),
2010 hive_options,
2011 glob: is_glob,
2012 ..Default::default()
2013 };
2014 let lf = LazyFrame::scan_parquet(pl_path, args).map_err(|e| {
2015 color_eyre::eyre::eyre!(
2016 "Could not read from GCS. Check credentials and URL: {}",
2017 e
2018 )
2019 })?;
2020 let state = DataTableState::from_lazyframe(lf, options)?;
2021 return Ok(state.lf);
2022 }
2023 #[cfg(not(feature = "cloud"))]
2024 {
2025 return Err(color_eyre::eyre::eyre!(
2026 "GCS (gs://) is not supported in this build. Rebuild with default features."
2027 ));
2028 }
2029 }
2030 source::InputSource::Local(_) => {}
2031 }
2032
2033 if paths.len() == 1 && options.hive {
2034 let path_str = path.as_os_str().to_string_lossy();
2035 let is_single_file = path.exists()
2036 && path.is_file()
2037 && !path_str.contains('*')
2038 && !path_str.contains("**");
2039 if !is_single_file {
2040 let use_parquet_hive = path.is_dir()
2041 || path_str.contains(".parquet")
2042 || path_str.contains("*.parquet");
2043 if use_parquet_hive {
2044 return DataTableState::scan_parquet_hive(path);
2046 }
2047 return Err(color_eyre::eyre::eyre!(
2048 "With --hive use a directory or a glob pattern for Parquet (e.g. path/to/dir or path/**/*.parquet)"
2049 ));
2050 }
2051 }
2052
2053 let effective_format = options.format.or_else(|| FileFormat::from_path(path));
2054
2055 let lf = if paths.len() > 1 {
2056 match effective_format {
2057 Some(FileFormat::Parquet) => DataTableState::from_parquet_paths(
2058 paths,
2059 options.pages_lookahead,
2060 options.pages_lookback,
2061 options.max_buffered_rows,
2062 options.max_buffered_mb,
2063 options.row_numbers,
2064 options.row_start_index,
2065 )?,
2066 Some(FileFormat::Csv) => DataTableState::from_csv_paths(paths, options)?,
2067 Some(FileFormat::Json) => DataTableState::from_json_paths(
2068 paths,
2069 options.pages_lookahead,
2070 options.pages_lookback,
2071 options.max_buffered_rows,
2072 options.max_buffered_mb,
2073 options.row_numbers,
2074 options.row_start_index,
2075 )?,
2076 Some(FileFormat::Jsonl) => DataTableState::from_json_lines_paths(
2077 paths,
2078 options.pages_lookahead,
2079 options.pages_lookback,
2080 options.max_buffered_rows,
2081 options.max_buffered_mb,
2082 options.row_numbers,
2083 options.row_start_index,
2084 )?,
2085 Some(FileFormat::Arrow) => DataTableState::from_ipc_paths(
2086 paths,
2087 options.pages_lookahead,
2088 options.pages_lookback,
2089 options.max_buffered_rows,
2090 options.max_buffered_mb,
2091 options.row_numbers,
2092 options.row_start_index,
2093 )?,
2094 Some(FileFormat::Avro) => DataTableState::from_avro_paths(
2095 paths,
2096 options.pages_lookahead,
2097 options.pages_lookback,
2098 options.max_buffered_rows,
2099 options.max_buffered_mb,
2100 options.row_numbers,
2101 options.row_start_index,
2102 )?,
2103 Some(FileFormat::Orc) => DataTableState::from_orc_paths(
2104 paths,
2105 options.pages_lookahead,
2106 options.pages_lookback,
2107 options.max_buffered_rows,
2108 options.max_buffered_mb,
2109 options.row_numbers,
2110 options.row_start_index,
2111 )?,
2112 Some(FileFormat::Tsv) | Some(FileFormat::Psv) | Some(FileFormat::Excel) | None => {
2113 if !paths.is_empty() && !path.exists() {
2114 return Err(std::io::Error::new(
2115 std::io::ErrorKind::NotFound,
2116 format!("File not found: {}", path.display()),
2117 )
2118 .into());
2119 }
2120 return Err(color_eyre::eyre::eyre!(
2121 "Unsupported file type for multiple files (parquet, csv, json, jsonl, ndjson, arrow/ipc/feather, avro, orc only)"
2122 ));
2123 }
2124 }
2125 } else {
2126 match effective_format {
2127 Some(FileFormat::Parquet) => DataTableState::from_parquet(
2128 path,
2129 options.pages_lookahead,
2130 options.pages_lookback,
2131 options.max_buffered_rows,
2132 options.max_buffered_mb,
2133 options.row_numbers,
2134 options.row_start_index,
2135 )?,
2136 Some(FileFormat::Csv) => DataTableState::from_csv(path, options)?,
2137 Some(FileFormat::Tsv) => DataTableState::from_delimited(path, b'\t', options)?,
2138 Some(FileFormat::Psv) => DataTableState::from_delimited(path, b'|', options)?,
2139 Some(FileFormat::Json) => DataTableState::from_json(
2140 path,
2141 options.pages_lookahead,
2142 options.pages_lookback,
2143 options.max_buffered_rows,
2144 options.max_buffered_mb,
2145 options.row_numbers,
2146 options.row_start_index,
2147 )?,
2148 Some(FileFormat::Jsonl) => DataTableState::from_json_lines(
2149 path,
2150 options.pages_lookahead,
2151 options.pages_lookback,
2152 options.max_buffered_rows,
2153 options.max_buffered_mb,
2154 options.row_numbers,
2155 options.row_start_index,
2156 )?,
2157 Some(FileFormat::Arrow) => DataTableState::from_ipc(
2158 path,
2159 options.pages_lookahead,
2160 options.pages_lookback,
2161 options.max_buffered_rows,
2162 options.max_buffered_mb,
2163 options.row_numbers,
2164 options.row_start_index,
2165 )?,
2166 Some(FileFormat::Avro) => DataTableState::from_avro(
2167 path,
2168 options.pages_lookahead,
2169 options.pages_lookback,
2170 options.max_buffered_rows,
2171 options.max_buffered_mb,
2172 options.row_numbers,
2173 options.row_start_index,
2174 )?,
2175 Some(FileFormat::Excel) => DataTableState::from_excel(
2176 path,
2177 options.pages_lookahead,
2178 options.pages_lookback,
2179 options.max_buffered_rows,
2180 options.max_buffered_mb,
2181 options.row_numbers,
2182 options.row_start_index,
2183 options.excel_sheet.as_deref(),
2184 )?,
2185 Some(FileFormat::Orc) => DataTableState::from_orc(
2186 path,
2187 options.pages_lookahead,
2188 options.pages_lookback,
2189 options.max_buffered_rows,
2190 options.max_buffered_mb,
2191 options.row_numbers,
2192 options.row_start_index,
2193 )?,
2194 None => {
2195 if paths.len() == 1 && !path.exists() {
2196 return Err(std::io::Error::new(
2197 std::io::ErrorKind::NotFound,
2198 format!("File not found: {}", path.display()),
2199 )
2200 .into());
2201 }
2202 return Err(color_eyre::eyre::eyre!("Unsupported file type"));
2203 }
2204 }
2205 };
2206 Ok(lf.lf)
2207 }
2208
2209 fn open_help_overlay(&mut self) {
2211 let already = self.show_help
2212 || (self.template_modal.active && self.template_modal.show_help)
2213 || (self.analysis_modal.active && self.analysis_modal.show_help);
2214 if already {
2215 return;
2216 }
2217 if self.analysis_modal.active {
2218 self.analysis_modal.show_help = true;
2219 } else if self.template_modal.active {
2220 self.template_modal.show_help = true;
2221 } else {
2222 self.show_help = true;
2223 }
2224 }
2225
2226 fn key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
2227 self.debug.on_key(event);
2228
2229 if event.code == KeyCode::F(1) {
2231 self.open_help_overlay();
2232 return None;
2233 }
2234
2235 if self.confirmation_modal.active {
2238 match event.code {
2239 KeyCode::Left | KeyCode::Char('h') => {
2240 self.confirmation_modal.focus_yes = true;
2241 }
2242 KeyCode::Right | KeyCode::Char('l') => {
2243 self.confirmation_modal.focus_yes = false;
2244 }
2245 KeyCode::Tab => {
2246 self.confirmation_modal.focus_yes = !self.confirmation_modal.focus_yes;
2248 }
2249 KeyCode::Enter => {
2250 if self.confirmation_modal.focus_yes {
2251 if let Some((path, format, title, width, height)) =
2253 self.pending_chart_export.take()
2254 {
2255 self.confirmation_modal.hide();
2256 return Some(AppEvent::ChartExport(path, format, title, width, height));
2257 }
2258 if let Some((path, format, options)) = self.pending_export.take() {
2259 self.confirmation_modal.hide();
2260 return Some(AppEvent::Export(path, format, options));
2261 }
2262 #[cfg(any(feature = "http", feature = "cloud"))]
2263 if let Some(pending) = self.pending_download.take() {
2264 self.confirmation_modal.hide();
2265 if let LoadingState::Loading {
2266 file_path,
2267 file_size,
2268 ..
2269 } = &self.loading_state
2270 {
2271 self.loading_state = LoadingState::Loading {
2272 file_path: file_path.clone(),
2273 file_size: *file_size,
2274 current_phase: "Downloading".to_string(),
2275 progress_percent: 20,
2276 };
2277 }
2278 return Some(match pending {
2279 #[cfg(feature = "http")]
2280 PendingDownload::Http { url, options, .. } => {
2281 AppEvent::DoDownloadHttp(url, options)
2282 }
2283 #[cfg(feature = "cloud")]
2284 PendingDownload::S3 { url, options, .. } => {
2285 AppEvent::DoDownloadS3ToTemp(url, options)
2286 }
2287 #[cfg(feature = "cloud")]
2288 PendingDownload::Gcs { url, options, .. } => {
2289 AppEvent::DoDownloadGcsToTemp(url, options)
2290 }
2291 });
2292 }
2293 } else {
2294 if let Some((path, format, _, _, _)) = self.pending_chart_export.take() {
2296 self.chart_export_modal.reopen_with_path(&path, format);
2297 }
2298 self.pending_export = None;
2299 #[cfg(any(feature = "http", feature = "cloud"))]
2300 if self.pending_download.take().is_some() {
2301 self.confirmation_modal.hide();
2302 return Some(AppEvent::Exit);
2303 }
2304 self.confirmation_modal.hide();
2305 }
2306 }
2307 KeyCode::Esc => {
2308 if let Some((path, format, _, _, _)) = self.pending_chart_export.take() {
2310 self.chart_export_modal.reopen_with_path(&path, format);
2311 }
2312 self.pending_export = None;
2313 #[cfg(any(feature = "http", feature = "cloud"))]
2314 if self.pending_download.take().is_some() {
2315 self.confirmation_modal.hide();
2316 return Some(AppEvent::Exit);
2317 }
2318 self.confirmation_modal.hide();
2319 }
2320 _ => {}
2321 }
2322 return None;
2323 }
2324 if self.success_modal.active {
2326 match event.code {
2327 KeyCode::Esc | KeyCode::Enter => {
2328 self.success_modal.hide();
2329 }
2330 _ => {}
2331 }
2332 return None;
2333 }
2334 if self.error_modal.active {
2336 match event.code {
2337 KeyCode::Esc | KeyCode::Enter => {
2338 self.error_modal.hide();
2339 }
2340 _ => {}
2341 }
2342 return None;
2343 }
2344
2345 let in_main_table = !(self.input_mode != InputMode::Normal
2349 || self.show_help
2350 || self.template_modal.active
2351 || self.analysis_modal.active);
2352 if in_main_table {
2353 let did_scroll = match event.code {
2354 KeyCode::Right | KeyCode::Char('l') => {
2355 if let Some(ref mut state) = self.data_table_state {
2356 state.scroll_right();
2357 if self.debug.enabled {
2358 self.debug.last_action = "scroll_right".to_string();
2359 }
2360 true
2361 } else {
2362 false
2363 }
2364 }
2365 KeyCode::Left | KeyCode::Char('h') => {
2366 if let Some(ref mut state) = self.data_table_state {
2367 state.scroll_left();
2368 if self.debug.enabled {
2369 self.debug.last_action = "scroll_left".to_string();
2370 }
2371 true
2372 } else {
2373 false
2374 }
2375 }
2376 _ => false,
2377 };
2378 if did_scroll {
2379 return None;
2380 }
2381 }
2382
2383 if self.show_help
2384 || (self.template_modal.active && self.template_modal.show_help)
2385 || (self.analysis_modal.active && self.analysis_modal.show_help)
2386 {
2387 match event.code {
2388 KeyCode::Esc => {
2389 if self.analysis_modal.active && self.analysis_modal.show_help {
2390 self.analysis_modal.show_help = false;
2391 } else if self.template_modal.active && self.template_modal.show_help {
2392 self.template_modal.show_help = false;
2393 } else {
2394 self.show_help = false;
2395 }
2396 self.help_scroll = 0;
2397 }
2398 KeyCode::Char('?') => {
2399 if self.analysis_modal.active && self.analysis_modal.show_help {
2400 self.analysis_modal.show_help = false;
2401 } else if self.template_modal.active && self.template_modal.show_help {
2402 self.template_modal.show_help = false;
2403 } else {
2404 self.show_help = false;
2405 }
2406 self.help_scroll = 0;
2407 }
2408 KeyCode::Down | KeyCode::Char('j') => {
2409 self.help_scroll = self.help_scroll.saturating_add(1);
2410 }
2411 KeyCode::Up | KeyCode::Char('k') => {
2412 self.help_scroll = self.help_scroll.saturating_sub(1);
2413 }
2414 KeyCode::PageDown => {
2415 self.help_scroll = self.help_scroll.saturating_add(10);
2416 }
2417 KeyCode::PageUp => {
2418 self.help_scroll = self.help_scroll.saturating_sub(10);
2419 }
2420 KeyCode::Home => {
2421 self.help_scroll = 0;
2422 }
2423 KeyCode::End => {
2424 }
2426 _ => {}
2427 }
2428 return None;
2429 }
2430
2431 if event.code == KeyCode::Char('?') {
2432 let ctrl_help = event.modifiers.contains(KeyModifiers::CONTROL);
2433 let in_text_input = match self.input_mode {
2434 InputMode::Editing => true,
2435 InputMode::Export => matches!(
2436 self.export_modal.focus,
2437 ExportFocus::PathInput | ExportFocus::CsvDelimiter
2438 ),
2439 InputMode::SortFilter => {
2440 let on_body = self.sort_filter_modal.focus == SortFilterFocus::Body;
2441 let filter_tab = self.sort_filter_modal.active_tab == SortFilterTab::Filter;
2442 on_body
2443 && filter_tab
2444 && self.sort_filter_modal.filter.focus == FilterFocus::Value
2445 }
2446 InputMode::PivotMelt => matches!(
2447 self.pivot_melt_modal.focus,
2448 PivotMeltFocus::PivotFilter
2449 | PivotMeltFocus::MeltFilter
2450 | PivotMeltFocus::MeltPattern
2451 | PivotMeltFocus::MeltVarName
2452 | PivotMeltFocus::MeltValName
2453 ),
2454 InputMode::Info | InputMode::Chart => false,
2455 InputMode::Normal => {
2456 if self.template_modal.active
2457 && self.template_modal.mode != TemplateModalMode::List
2458 {
2459 matches!(
2460 self.template_modal.create_focus,
2461 CreateFocus::Name
2462 | CreateFocus::Description
2463 | CreateFocus::ExactPath
2464 | CreateFocus::RelativePath
2465 | CreateFocus::PathPattern
2466 | CreateFocus::FilenamePattern
2467 )
2468 } else {
2469 false
2470 }
2471 }
2472 };
2473 if ctrl_help || !in_text_input {
2475 self.open_help_overlay();
2476 return None;
2477 }
2478 }
2479
2480 if self.input_mode == InputMode::SortFilter {
2481 let on_tab_bar = self.sort_filter_modal.focus == SortFilterFocus::TabBar;
2482 let on_body = self.sort_filter_modal.focus == SortFilterFocus::Body;
2483 let on_apply = self.sort_filter_modal.focus == SortFilterFocus::Apply;
2484 let on_cancel = self.sort_filter_modal.focus == SortFilterFocus::Cancel;
2485 let on_clear = self.sort_filter_modal.focus == SortFilterFocus::Clear;
2486 let sort_tab = self.sort_filter_modal.active_tab == SortFilterTab::Sort;
2487 let filter_tab = self.sort_filter_modal.active_tab == SortFilterTab::Filter;
2488
2489 match event.code {
2490 KeyCode::Esc => {
2491 for col in &mut self.sort_filter_modal.sort.columns {
2492 col.is_to_be_locked = false;
2493 }
2494 self.sort_filter_modal.sort.has_unapplied_changes = false;
2495 self.sort_filter_modal.close();
2496 self.input_mode = InputMode::Normal;
2497 }
2498 KeyCode::Tab => self.sort_filter_modal.next_focus(),
2499 KeyCode::BackTab => self.sort_filter_modal.prev_focus(),
2500 KeyCode::Left | KeyCode::Char('h') if on_tab_bar => {
2501 self.sort_filter_modal.switch_tab();
2502 }
2503 KeyCode::Right | KeyCode::Char('l') if on_tab_bar => {
2504 self.sort_filter_modal.switch_tab();
2505 }
2506 KeyCode::Enter if event.modifiers.contains(KeyModifiers::CONTROL) && sort_tab => {
2507 let columns = self.sort_filter_modal.sort.get_sorted_columns();
2508 let column_order = self.sort_filter_modal.sort.get_column_order();
2509 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
2510 let ascending = self.sort_filter_modal.sort.ascending;
2511 self.sort_filter_modal.sort.has_unapplied_changes = false;
2512 self.sort_filter_modal.close();
2513 self.input_mode = InputMode::Normal;
2514 let _ = self.send_event(AppEvent::ColumnOrder(column_order, locked_count));
2515 return Some(AppEvent::Sort(columns, ascending));
2516 }
2517 KeyCode::Enter if on_apply => {
2518 if sort_tab {
2519 let columns = self.sort_filter_modal.sort.get_sorted_columns();
2520 let column_order = self.sort_filter_modal.sort.get_column_order();
2521 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
2522 let ascending = self.sort_filter_modal.sort.ascending;
2523 self.sort_filter_modal.sort.has_unapplied_changes = false;
2524 self.sort_filter_modal.close();
2525 self.input_mode = InputMode::Normal;
2526 let _ = self.send_event(AppEvent::ColumnOrder(column_order, locked_count));
2527 return Some(AppEvent::Sort(columns, ascending));
2528 } else {
2529 let statements = self.sort_filter_modal.filter.statements.clone();
2530 self.sort_filter_modal.close();
2531 self.input_mode = InputMode::Normal;
2532 return Some(AppEvent::Filter(statements));
2533 }
2534 }
2535 KeyCode::Enter if on_cancel => {
2536 for col in &mut self.sort_filter_modal.sort.columns {
2537 col.is_to_be_locked = false;
2538 }
2539 self.sort_filter_modal.sort.has_unapplied_changes = false;
2540 self.sort_filter_modal.close();
2541 self.input_mode = InputMode::Normal;
2542 }
2543 KeyCode::Enter if on_clear => {
2544 if sort_tab {
2545 self.sort_filter_modal.sort.clear_selection();
2546 } else {
2547 self.sort_filter_modal.filter.statements.clear();
2548 self.sort_filter_modal.filter.list_state.select(None);
2549 }
2550 }
2551 KeyCode::Char(' ')
2552 if on_body
2553 && sort_tab
2554 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2555 {
2556 self.sort_filter_modal.sort.toggle_selection();
2557 }
2558 KeyCode::Char(' ')
2559 if on_body
2560 && sort_tab
2561 && self.sort_filter_modal.sort.focus == SortFocus::Order =>
2562 {
2563 self.sort_filter_modal.sort.ascending = !self.sort_filter_modal.sort.ascending;
2564 self.sort_filter_modal.sort.has_unapplied_changes = true;
2565 }
2566 KeyCode::Char(' ') if on_apply && sort_tab => {
2567 let columns = self.sort_filter_modal.sort.get_sorted_columns();
2568 let column_order = self.sort_filter_modal.sort.get_column_order();
2569 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
2570 let ascending = self.sort_filter_modal.sort.ascending;
2571 self.sort_filter_modal.sort.has_unapplied_changes = false;
2572 let _ = self.send_event(AppEvent::ColumnOrder(column_order, locked_count));
2573 return Some(AppEvent::Sort(columns, ascending));
2574 }
2575 KeyCode::Enter if on_body && filter_tab => {
2576 match self.sort_filter_modal.filter.focus {
2577 FilterFocus::Add => {
2578 self.sort_filter_modal.filter.add_statement();
2579 }
2580 FilterFocus::Statements => {
2581 let m = &mut self.sort_filter_modal.filter;
2582 if let Some(idx) = m.list_state.selected() {
2583 if idx < m.statements.len() {
2584 m.statements.remove(idx);
2585 if m.statements.is_empty() {
2586 m.list_state.select(None);
2587 m.focus = FilterFocus::Column;
2588 } else {
2589 m.list_state
2590 .select(Some(m.statements.len().saturating_sub(1)));
2591 }
2592 }
2593 }
2594 }
2595 _ => {}
2596 }
2597 }
2598 KeyCode::Enter if on_body && sort_tab => match self.sort_filter_modal.sort.focus {
2599 SortFocus::Filter => {
2600 self.sort_filter_modal.sort.focus = SortFocus::ColumnList;
2601 }
2602 SortFocus::ColumnList => {
2603 self.sort_filter_modal.sort.toggle_selection();
2604 let columns = self.sort_filter_modal.sort.get_sorted_columns();
2605 let column_order = self.sort_filter_modal.sort.get_column_order();
2606 let locked_count = self.sort_filter_modal.sort.get_locked_columns_count();
2607 let ascending = self.sort_filter_modal.sort.ascending;
2608 self.sort_filter_modal.sort.has_unapplied_changes = false;
2609 let _ = self.send_event(AppEvent::ColumnOrder(column_order, locked_count));
2610 return Some(AppEvent::Sort(columns, ascending));
2611 }
2612 SortFocus::Order => {
2613 self.sort_filter_modal.sort.ascending =
2614 !self.sort_filter_modal.sort.ascending;
2615 self.sort_filter_modal.sort.has_unapplied_changes = true;
2616 }
2617 _ => {}
2618 },
2619 KeyCode::Left
2620 | KeyCode::Right
2621 | KeyCode::Char('h')
2622 | KeyCode::Char('l')
2623 | KeyCode::Up
2624 | KeyCode::Down
2625 | KeyCode::Char('j')
2626 | KeyCode::Char('k')
2627 if on_body
2628 && sort_tab
2629 && self.sort_filter_modal.sort.focus == SortFocus::Order =>
2630 {
2631 let s = &mut self.sort_filter_modal.sort;
2632 match event.code {
2633 KeyCode::Left | KeyCode::Char('h') | KeyCode::Up | KeyCode::Char('k') => {
2634 s.ascending = true;
2635 }
2636 KeyCode::Right
2637 | KeyCode::Char('l')
2638 | KeyCode::Down
2639 | KeyCode::Char('j') => {
2640 s.ascending = false;
2641 }
2642 _ => {}
2643 }
2644 s.has_unapplied_changes = true;
2645 }
2646 KeyCode::Down
2647 if on_body
2648 && filter_tab
2649 && self.sort_filter_modal.filter.focus == FilterFocus::Statements =>
2650 {
2651 let m = &mut self.sort_filter_modal.filter;
2652 let i = match m.list_state.selected() {
2653 Some(i) => {
2654 if i >= m.statements.len().saturating_sub(1) {
2655 0
2656 } else {
2657 i + 1
2658 }
2659 }
2660 None => 0,
2661 };
2662 m.list_state.select(Some(i));
2663 }
2664 KeyCode::Up
2665 if on_body
2666 && filter_tab
2667 && self.sort_filter_modal.filter.focus == FilterFocus::Statements =>
2668 {
2669 let m = &mut self.sort_filter_modal.filter;
2670 let i = match m.list_state.selected() {
2671 Some(i) => {
2672 if i == 0 {
2673 m.statements.len().saturating_sub(1)
2674 } else {
2675 i - 1
2676 }
2677 }
2678 None => 0,
2679 };
2680 m.list_state.select(Some(i));
2681 }
2682 KeyCode::Down | KeyCode::Char('j') if on_body && sort_tab => {
2683 let s = &mut self.sort_filter_modal.sort;
2684 if s.focus == SortFocus::ColumnList {
2685 let i = match s.table_state.selected() {
2686 Some(i) => {
2687 if i >= s.filtered_columns().len().saturating_sub(1) {
2688 0
2689 } else {
2690 i + 1
2691 }
2692 }
2693 None => 0,
2694 };
2695 s.table_state.select(Some(i));
2696 } else {
2697 let _ = s.next_body_focus();
2698 }
2699 }
2700 KeyCode::Up | KeyCode::Char('k') if on_body && sort_tab => {
2701 let s = &mut self.sort_filter_modal.sort;
2702 if s.focus == SortFocus::ColumnList {
2703 let i = match s.table_state.selected() {
2704 Some(i) => {
2705 if i == 0 {
2706 s.filtered_columns().len().saturating_sub(1)
2707 } else {
2708 i - 1
2709 }
2710 }
2711 None => 0,
2712 };
2713 s.table_state.select(Some(i));
2714 } else {
2715 let _ = s.prev_body_focus();
2716 }
2717 }
2718 KeyCode::Char(']')
2719 if on_body
2720 && sort_tab
2721 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2722 {
2723 self.sort_filter_modal.sort.move_selection_down();
2724 }
2725 KeyCode::Char('[')
2726 if on_body
2727 && sort_tab
2728 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2729 {
2730 self.sort_filter_modal.sort.move_selection_up();
2731 }
2732 KeyCode::Char('+') | KeyCode::Char('=')
2733 if on_body
2734 && sort_tab
2735 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2736 {
2737 self.sort_filter_modal.sort.move_column_display_up();
2738 self.sort_filter_modal.sort.has_unapplied_changes = true;
2739 }
2740 KeyCode::Char('-') | KeyCode::Char('_')
2741 if on_body
2742 && sort_tab
2743 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2744 {
2745 self.sort_filter_modal.sort.move_column_display_down();
2746 self.sort_filter_modal.sort.has_unapplied_changes = true;
2747 }
2748 KeyCode::Char('L')
2749 if on_body
2750 && sort_tab
2751 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2752 {
2753 self.sort_filter_modal.sort.toggle_lock_at_column();
2754 self.sort_filter_modal.sort.has_unapplied_changes = true;
2755 }
2756 KeyCode::Char('v')
2757 if on_body
2758 && sort_tab
2759 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList =>
2760 {
2761 self.sort_filter_modal.sort.toggle_visibility();
2762 self.sort_filter_modal.sort.has_unapplied_changes = true;
2763 }
2764 KeyCode::Char(c)
2765 if on_body
2766 && sort_tab
2767 && self.sort_filter_modal.sort.focus == SortFocus::ColumnList
2768 && c.is_ascii_digit() =>
2769 {
2770 if let Some(digit) = c.to_digit(10) {
2771 self.sort_filter_modal
2772 .sort
2773 .jump_selection_to_order(digit as usize);
2774 }
2775 }
2776 _ if on_body
2780 && sort_tab
2781 && self.sort_filter_modal.sort.focus == SortFocus::Filter
2782 && !matches!(
2783 event.code,
2784 KeyCode::Tab
2785 | KeyCode::BackTab
2786 | KeyCode::Esc
2787 | KeyCode::Enter
2788 | KeyCode::Up
2789 | KeyCode::Down
2790 ) =>
2791 {
2792 let _ = self
2794 .sort_filter_modal
2795 .sort
2796 .filter_input
2797 .handle_key(event, Some(&self.cache));
2798 }
2799 KeyCode::Char(c)
2800 if on_body
2801 && filter_tab
2802 && self.sort_filter_modal.filter.focus == FilterFocus::Value =>
2803 {
2804 self.sort_filter_modal.filter.new_value.push(c);
2805 }
2806 KeyCode::Backspace
2807 if on_body
2808 && filter_tab
2809 && self.sort_filter_modal.filter.focus == FilterFocus::Value =>
2810 {
2811 self.sort_filter_modal.filter.new_value.pop();
2812 }
2813 KeyCode::Right | KeyCode::Char('l') if on_body && filter_tab => {
2814 let m = &mut self.sort_filter_modal.filter;
2815 match m.focus {
2816 FilterFocus::Column => {
2817 m.new_column_idx =
2818 (m.new_column_idx + 1) % m.available_columns.len().max(1);
2819 }
2820 FilterFocus::Operator => {
2821 m.new_operator_idx =
2822 (m.new_operator_idx + 1) % FilterOperator::iterator().count();
2823 }
2824 FilterFocus::Logical => {
2825 m.new_logical_idx =
2826 (m.new_logical_idx + 1) % LogicalOperator::iterator().count();
2827 }
2828 _ => {}
2829 }
2830 }
2831 KeyCode::Left | KeyCode::Char('h') if on_body && filter_tab => {
2832 let m = &mut self.sort_filter_modal.filter;
2833 match m.focus {
2834 FilterFocus::Column => {
2835 m.new_column_idx = if m.new_column_idx == 0 {
2836 m.available_columns.len().saturating_sub(1)
2837 } else {
2838 m.new_column_idx - 1
2839 };
2840 }
2841 FilterFocus::Operator => {
2842 m.new_operator_idx = if m.new_operator_idx == 0 {
2843 FilterOperator::iterator().count() - 1
2844 } else {
2845 m.new_operator_idx - 1
2846 };
2847 }
2848 FilterFocus::Logical => {
2849 m.new_logical_idx = if m.new_logical_idx == 0 {
2850 LogicalOperator::iterator().count() - 1
2851 } else {
2852 m.new_logical_idx - 1
2853 };
2854 }
2855 _ => {}
2856 }
2857 }
2858 _ => {}
2859 }
2860 return None;
2861 }
2862
2863 if self.input_mode == InputMode::Export {
2864 match event.code {
2865 KeyCode::Esc => {
2866 self.export_modal.close();
2867 self.input_mode = InputMode::Normal;
2868 }
2869 KeyCode::Tab => self.export_modal.next_focus(),
2870 KeyCode::BackTab => self.export_modal.prev_focus(),
2871 KeyCode::Up | KeyCode::Char('k') => {
2872 match self.export_modal.focus {
2873 ExportFocus::FormatSelector => {
2874 let current_idx = ExportFormat::ALL
2876 .iter()
2877 .position(|&f| f == self.export_modal.selected_format)
2878 .unwrap_or(0);
2879 let prev_idx = if current_idx == 0 {
2880 ExportFormat::ALL.len() - 1
2881 } else {
2882 current_idx - 1
2883 };
2884 self.export_modal.selected_format = ExportFormat::ALL[prev_idx];
2885 }
2886 ExportFocus::PathInput => {
2887 self.export_modal.path_input.handle_key(event, None);
2889 }
2890 ExportFocus::CsvDelimiter => {
2891 self.export_modal
2893 .csv_delimiter_input
2894 .handle_key(event, None);
2895 }
2896 ExportFocus::CsvCompression
2897 | ExportFocus::JsonCompression
2898 | ExportFocus::NdjsonCompression => {
2899 self.export_modal.cycle_compression_backward();
2901 }
2902 _ => {
2903 self.export_modal.prev_focus();
2904 }
2905 }
2906 }
2907 KeyCode::Down | KeyCode::Char('j') => {
2908 match self.export_modal.focus {
2909 ExportFocus::FormatSelector => {
2910 let current_idx = ExportFormat::ALL
2912 .iter()
2913 .position(|&f| f == self.export_modal.selected_format)
2914 .unwrap_or(0);
2915 let next_idx = (current_idx + 1) % ExportFormat::ALL.len();
2916 self.export_modal.selected_format = ExportFormat::ALL[next_idx];
2917 }
2918 ExportFocus::PathInput => {
2919 self.export_modal.path_input.handle_key(event, None);
2921 }
2922 ExportFocus::CsvDelimiter => {
2923 self.export_modal
2925 .csv_delimiter_input
2926 .handle_key(event, None);
2927 }
2928 ExportFocus::CsvCompression
2929 | ExportFocus::JsonCompression
2930 | ExportFocus::NdjsonCompression => {
2931 self.export_modal.cycle_compression();
2933 }
2934 _ => {
2935 self.export_modal.next_focus();
2936 }
2937 }
2938 }
2939 KeyCode::Left | KeyCode::Char('h') => {
2940 match self.export_modal.focus {
2941 ExportFocus::PathInput => {
2942 self.export_modal.path_input.handle_key(event, None);
2943 }
2944 ExportFocus::CsvDelimiter => {
2945 self.export_modal
2946 .csv_delimiter_input
2947 .handle_key(event, None);
2948 }
2949 ExportFocus::FormatSelector => {
2950 }
2952 ExportFocus::CsvCompression
2953 | ExportFocus::JsonCompression
2954 | ExportFocus::NdjsonCompression => {
2955 self.export_modal.cycle_compression_backward();
2957 }
2958 _ => self.export_modal.prev_focus(),
2959 }
2960 }
2961 KeyCode::Right | KeyCode::Char('l') => {
2962 match self.export_modal.focus {
2963 ExportFocus::PathInput => {
2964 self.export_modal.path_input.handle_key(event, None);
2965 }
2966 ExportFocus::CsvDelimiter => {
2967 self.export_modal
2968 .csv_delimiter_input
2969 .handle_key(event, None);
2970 }
2971 ExportFocus::FormatSelector => {
2972 }
2974 ExportFocus::CsvCompression
2975 | ExportFocus::JsonCompression
2976 | ExportFocus::NdjsonCompression => {
2977 self.export_modal.cycle_compression();
2979 }
2980 _ => self.export_modal.next_focus(),
2981 }
2982 }
2983 KeyCode::Enter => {
2984 match self.export_modal.focus {
2985 ExportFocus::PathInput => {
2986 let path_str = self.export_modal.path_input.value.trim();
2988 if !path_str.is_empty() {
2989 let mut path = PathBuf::from(path_str);
2990 let format = self.export_modal.selected_format;
2991 let compression = match format {
2993 ExportFormat::Csv => self.export_modal.csv_compression,
2994 ExportFormat::Json => self.export_modal.json_compression,
2995 ExportFormat::Ndjson => self.export_modal.ndjson_compression,
2996 ExportFormat::Parquet
2997 | ExportFormat::Ipc
2998 | ExportFormat::Avro => None,
2999 };
3000 let path_with_ext =
3002 Self::ensure_file_extension(&path, format, compression);
3003 if path_with_ext != path {
3005 self.export_modal
3006 .path_input
3007 .set_value(path_with_ext.display().to_string());
3008 }
3009 path = path_with_ext;
3010 let delimiter =
3011 self.export_modal
3012 .csv_delimiter_input
3013 .value
3014 .chars()
3015 .next()
3016 .unwrap_or(',') as u8;
3017 let options = ExportOptions {
3018 csv_delimiter: delimiter,
3019 csv_include_header: self.export_modal.csv_include_header,
3020 csv_compression: self.export_modal.csv_compression,
3021 json_compression: self.export_modal.json_compression,
3022 ndjson_compression: self.export_modal.ndjson_compression,
3023 parquet_compression: None,
3024 };
3025 if path.exists() {
3027 let path_display = path.display().to_string();
3028 self.pending_export = Some((path, format, options));
3029 self.confirmation_modal.show(format!(
3030 "File already exists:\n{}\n\nDo you wish to overwrite this file?",
3031 path_display
3032 ));
3033 self.export_modal.close();
3034 self.input_mode = InputMode::Normal;
3035 } else {
3036 self.export_modal.close();
3038 self.input_mode = InputMode::Normal;
3039 return Some(AppEvent::Export(path, format, options));
3040 }
3041 }
3042 }
3043 ExportFocus::ExportButton => {
3044 if !self.export_modal.path_input.value.is_empty() {
3045 let mut path = PathBuf::from(&self.export_modal.path_input.value);
3046 let format = self.export_modal.selected_format;
3047 let compression = match format {
3049 ExportFormat::Csv => self.export_modal.csv_compression,
3050 ExportFormat::Json => self.export_modal.json_compression,
3051 ExportFormat::Ndjson => self.export_modal.ndjson_compression,
3052 ExportFormat::Parquet
3053 | ExportFormat::Ipc
3054 | ExportFormat::Avro => None,
3055 };
3056 let path_with_ext =
3058 Self::ensure_file_extension(&path, format, compression);
3059 if path_with_ext != path {
3061 self.export_modal
3062 .path_input
3063 .set_value(path_with_ext.display().to_string());
3064 }
3065 path = path_with_ext;
3066 let delimiter =
3067 self.export_modal
3068 .csv_delimiter_input
3069 .value
3070 .chars()
3071 .next()
3072 .unwrap_or(',') as u8;
3073 let options = ExportOptions {
3074 csv_delimiter: delimiter,
3075 csv_include_header: self.export_modal.csv_include_header,
3076 csv_compression: self.export_modal.csv_compression,
3077 json_compression: self.export_modal.json_compression,
3078 ndjson_compression: self.export_modal.ndjson_compression,
3079 parquet_compression: None,
3080 };
3081 if path.exists() {
3083 let path_display = path.display().to_string();
3084 self.pending_export = Some((path, format, options));
3085 self.confirmation_modal.show(format!(
3086 "File already exists:\n{}\n\nDo you wish to overwrite this file?",
3087 path_display
3088 ));
3089 self.export_modal.close();
3090 self.input_mode = InputMode::Normal;
3091 } else {
3092 self.export_modal.close();
3094 self.input_mode = InputMode::Normal;
3095 return Some(AppEvent::Export(path, format, options));
3096 }
3097 }
3098 }
3099 ExportFocus::CancelButton => {
3100 self.export_modal.close();
3101 self.input_mode = InputMode::Normal;
3102 }
3103 ExportFocus::CsvIncludeHeader => {
3104 self.export_modal.csv_include_header =
3105 !self.export_modal.csv_include_header;
3106 }
3107 ExportFocus::CsvCompression
3108 | ExportFocus::JsonCompression
3109 | ExportFocus::NdjsonCompression => {
3110 }
3113 _ => {}
3114 }
3115 }
3116 KeyCode::Char(' ') => {
3117 match self.export_modal.focus {
3119 ExportFocus::PathInput => {
3120 self.export_modal.path_input.handle_key(event, None);
3122 }
3123 ExportFocus::CsvDelimiter => {
3124 self.export_modal
3126 .csv_delimiter_input
3127 .handle_key(event, None);
3128 }
3129 ExportFocus::CsvIncludeHeader => {
3130 self.export_modal.csv_include_header =
3132 !self.export_modal.csv_include_header;
3133 }
3134 _ => {}
3135 }
3136 }
3137 KeyCode::Char(_)
3138 | KeyCode::Backspace
3139 | KeyCode::Delete
3140 | KeyCode::Home
3141 | KeyCode::End => {
3142 match self.export_modal.focus {
3143 ExportFocus::PathInput => {
3144 self.export_modal.path_input.handle_key(event, None);
3145 }
3146 ExportFocus::CsvDelimiter => {
3147 self.export_modal
3148 .csv_delimiter_input
3149 .handle_key(event, None);
3150 }
3151 ExportFocus::FormatSelector => {
3152 }
3154 _ => {}
3155 }
3156 }
3157 _ => {}
3158 }
3159 return None;
3160 }
3161
3162 if self.input_mode == InputMode::PivotMelt {
3163 let pivot_melt_text_focus = matches!(
3164 self.pivot_melt_modal.focus,
3165 PivotMeltFocus::PivotFilter
3166 | PivotMeltFocus::MeltFilter
3167 | PivotMeltFocus::MeltPattern
3168 | PivotMeltFocus::MeltVarName
3169 | PivotMeltFocus::MeltValName
3170 );
3171 let ctrl_help = event.modifiers.contains(KeyModifiers::CONTROL);
3172 if event.code == KeyCode::Char('?') && (ctrl_help || !pivot_melt_text_focus) {
3173 self.show_help = true;
3174 return None;
3175 }
3176 match event.code {
3177 KeyCode::Esc => {
3178 self.pivot_melt_modal.close();
3179 self.input_mode = InputMode::Normal;
3180 }
3181 KeyCode::Tab => self.pivot_melt_modal.next_focus(),
3182 KeyCode::BackTab => self.pivot_melt_modal.prev_focus(),
3183 KeyCode::Left => {
3184 if self.pivot_melt_modal.focus == PivotMeltFocus::PivotAggregation {
3185 self.pivot_melt_modal.pivot_move_aggregation_step(4, 0, -1);
3186 } else if self.pivot_melt_modal.focus == PivotMeltFocus::PivotFilter {
3187 self.pivot_melt_modal
3188 .pivot_filter_input
3189 .handle_key(event, None);
3190 self.pivot_melt_modal.pivot_index_table.select(None);
3191 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltFilter {
3192 self.pivot_melt_modal
3193 .melt_filter_input
3194 .handle_key(event, None);
3195 self.pivot_melt_modal.melt_index_table.select(None);
3196 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern
3197 && self.pivot_melt_modal.melt_pattern_cursor > 0
3198 {
3199 self.pivot_melt_modal.melt_pattern_cursor -= 1;
3200 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName
3201 && self.pivot_melt_modal.melt_variable_cursor > 0
3202 {
3203 self.pivot_melt_modal.melt_variable_cursor -= 1;
3204 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName
3205 && self.pivot_melt_modal.melt_value_cursor > 0
3206 {
3207 self.pivot_melt_modal.melt_value_cursor -= 1;
3208 } else if self.pivot_melt_modal.focus == PivotMeltFocus::TabBar {
3209 self.pivot_melt_modal.switch_tab();
3210 } else {
3211 self.pivot_melt_modal.prev_focus();
3212 }
3213 }
3214 KeyCode::Right => {
3215 if self.pivot_melt_modal.focus == PivotMeltFocus::PivotAggregation {
3216 self.pivot_melt_modal.pivot_move_aggregation_step(4, 0, 1);
3217 } else if self.pivot_melt_modal.focus == PivotMeltFocus::PivotFilter {
3218 self.pivot_melt_modal
3219 .pivot_filter_input
3220 .handle_key(event, None);
3221 self.pivot_melt_modal.pivot_index_table.select(None);
3222 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltFilter {
3223 self.pivot_melt_modal
3224 .melt_filter_input
3225 .handle_key(event, None);
3226 self.pivot_melt_modal.melt_index_table.select(None);
3227 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern {
3228 let n = self.pivot_melt_modal.melt_pattern.chars().count();
3229 if self.pivot_melt_modal.melt_pattern_cursor < n {
3230 self.pivot_melt_modal.melt_pattern_cursor += 1;
3231 }
3232 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName {
3233 let n = self.pivot_melt_modal.melt_variable_name.chars().count();
3234 if self.pivot_melt_modal.melt_variable_cursor < n {
3235 self.pivot_melt_modal.melt_variable_cursor += 1;
3236 }
3237 } else if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName {
3238 let n = self.pivot_melt_modal.melt_value_name.chars().count();
3239 if self.pivot_melt_modal.melt_value_cursor < n {
3240 self.pivot_melt_modal.melt_value_cursor += 1;
3241 }
3242 } else if self.pivot_melt_modal.focus == PivotMeltFocus::TabBar {
3243 self.pivot_melt_modal.switch_tab();
3244 } else {
3245 self.pivot_melt_modal.next_focus();
3246 }
3247 }
3248 KeyCode::Enter => match self.pivot_melt_modal.focus {
3249 PivotMeltFocus::Apply => {
3250 return match self.pivot_melt_modal.active_tab {
3251 PivotMeltTab::Pivot => {
3252 if let Some(err) = self.pivot_melt_modal.pivot_validation_error() {
3253 self.error_modal.show(err);
3254 None
3255 } else {
3256 self.pivot_melt_modal
3257 .build_pivot_spec()
3258 .map(AppEvent::Pivot)
3259 }
3260 }
3261 PivotMeltTab::Melt => {
3262 if let Some(err) = self.pivot_melt_modal.melt_validation_error() {
3263 self.error_modal.show(err);
3264 None
3265 } else {
3266 self.pivot_melt_modal.build_melt_spec().map(AppEvent::Melt)
3267 }
3268 }
3269 };
3270 }
3271 PivotMeltFocus::Cancel => {
3272 self.pivot_melt_modal.close();
3273 self.input_mode = InputMode::Normal;
3274 }
3275 PivotMeltFocus::Clear => {
3276 self.pivot_melt_modal.reset_form();
3277 }
3278 _ => {}
3279 },
3280 KeyCode::Up | KeyCode::Char('k') if !pivot_melt_text_focus => {
3281 match self.pivot_melt_modal.focus {
3282 PivotMeltFocus::PivotIndexList => {
3283 self.pivot_melt_modal.pivot_move_index_selection(false);
3284 }
3285 PivotMeltFocus::PivotPivotCol => {
3286 self.pivot_melt_modal.pivot_move_pivot_selection(false);
3287 }
3288 PivotMeltFocus::PivotValueCol => {
3289 self.pivot_melt_modal.pivot_move_value_selection(false);
3290 }
3291 PivotMeltFocus::PivotAggregation => {
3292 self.pivot_melt_modal.pivot_move_aggregation_step(4, -1, 0);
3293 }
3294 PivotMeltFocus::MeltIndexList => {
3295 self.pivot_melt_modal.melt_move_index_selection(false);
3296 }
3297 PivotMeltFocus::MeltStrategy => {
3298 self.pivot_melt_modal.melt_move_strategy(false);
3299 }
3300 PivotMeltFocus::MeltType => {
3301 self.pivot_melt_modal.melt_move_type_filter(false);
3302 }
3303 PivotMeltFocus::MeltExplicitList => {
3304 self.pivot_melt_modal.melt_move_explicit_selection(false);
3305 }
3306 _ => {}
3307 }
3308 }
3309 KeyCode::Down | KeyCode::Char('j') if !pivot_melt_text_focus => {
3310 match self.pivot_melt_modal.focus {
3311 PivotMeltFocus::PivotIndexList => {
3312 self.pivot_melt_modal.pivot_move_index_selection(true);
3313 }
3314 PivotMeltFocus::PivotPivotCol => {
3315 self.pivot_melt_modal.pivot_move_pivot_selection(true);
3316 }
3317 PivotMeltFocus::PivotValueCol => {
3318 self.pivot_melt_modal.pivot_move_value_selection(true);
3319 }
3320 PivotMeltFocus::PivotAggregation => {
3321 self.pivot_melt_modal.pivot_move_aggregation_step(4, 1, 0);
3322 }
3323 PivotMeltFocus::MeltIndexList => {
3324 self.pivot_melt_modal.melt_move_index_selection(true);
3325 }
3326 PivotMeltFocus::MeltStrategy => {
3327 self.pivot_melt_modal.melt_move_strategy(true);
3328 }
3329 PivotMeltFocus::MeltType => {
3330 self.pivot_melt_modal.melt_move_type_filter(true);
3331 }
3332 PivotMeltFocus::MeltExplicitList => {
3333 self.pivot_melt_modal.melt_move_explicit_selection(true);
3334 }
3335 _ => {}
3336 }
3337 }
3338 KeyCode::Char(' ') if !pivot_melt_text_focus => match self.pivot_melt_modal.focus {
3339 PivotMeltFocus::PivotIndexList => {
3340 self.pivot_melt_modal.pivot_toggle_index_at_selection();
3341 }
3342 PivotMeltFocus::MeltIndexList => {
3343 self.pivot_melt_modal.melt_toggle_index_at_selection();
3344 }
3345 PivotMeltFocus::MeltExplicitList => {
3346 self.pivot_melt_modal.melt_toggle_explicit_at_selection();
3347 }
3348 _ => {}
3349 },
3350 KeyCode::Home
3351 | KeyCode::End
3352 | KeyCode::Char(_)
3353 | KeyCode::Backspace
3354 | KeyCode::Delete
3355 if self.pivot_melt_modal.focus == PivotMeltFocus::PivotFilter =>
3356 {
3357 self.pivot_melt_modal
3358 .pivot_filter_input
3359 .handle_key(event, None);
3360 self.pivot_melt_modal.pivot_index_table.select(None);
3361 }
3362 KeyCode::Home
3363 | KeyCode::End
3364 | KeyCode::Char(_)
3365 | KeyCode::Backspace
3366 | KeyCode::Delete
3367 if self.pivot_melt_modal.focus == PivotMeltFocus::MeltFilter =>
3368 {
3369 self.pivot_melt_modal
3370 .melt_filter_input
3371 .handle_key(event, None);
3372 self.pivot_melt_modal.melt_index_table.select(None);
3373 }
3374 KeyCode::Home if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern => {
3375 self.pivot_melt_modal.melt_pattern_cursor = 0;
3376 }
3377 KeyCode::End if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern => {
3378 self.pivot_melt_modal.melt_pattern_cursor =
3379 self.pivot_melt_modal.melt_pattern.chars().count();
3380 }
3381 KeyCode::Char(c) if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern => {
3382 let byte_pos: usize = self
3383 .pivot_melt_modal
3384 .melt_pattern
3385 .chars()
3386 .take(self.pivot_melt_modal.melt_pattern_cursor)
3387 .map(|ch| ch.len_utf8())
3388 .sum();
3389 self.pivot_melt_modal.melt_pattern.insert(byte_pos, c);
3390 self.pivot_melt_modal.melt_pattern_cursor += 1;
3391 }
3392 KeyCode::Backspace
3393 if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern =>
3394 {
3395 if self.pivot_melt_modal.melt_pattern_cursor > 0 {
3396 let prev_byte: usize = self
3397 .pivot_melt_modal
3398 .melt_pattern
3399 .chars()
3400 .take(self.pivot_melt_modal.melt_pattern_cursor - 1)
3401 .map(|ch| ch.len_utf8())
3402 .sum();
3403 if let Some(ch) = self.pivot_melt_modal.melt_pattern[prev_byte..]
3404 .chars()
3405 .next()
3406 {
3407 self.pivot_melt_modal
3408 .melt_pattern
3409 .drain(prev_byte..prev_byte + ch.len_utf8());
3410 self.pivot_melt_modal.melt_pattern_cursor -= 1;
3411 }
3412 }
3413 }
3414 KeyCode::Delete if self.pivot_melt_modal.focus == PivotMeltFocus::MeltPattern => {
3415 let n = self.pivot_melt_modal.melt_pattern.chars().count();
3416 if self.pivot_melt_modal.melt_pattern_cursor < n {
3417 let byte_pos: usize = self
3418 .pivot_melt_modal
3419 .melt_pattern
3420 .chars()
3421 .take(self.pivot_melt_modal.melt_pattern_cursor)
3422 .map(|ch| ch.len_utf8())
3423 .sum();
3424 if let Some(ch) = self.pivot_melt_modal.melt_pattern[byte_pos..]
3425 .chars()
3426 .next()
3427 {
3428 self.pivot_melt_modal
3429 .melt_pattern
3430 .drain(byte_pos..byte_pos + ch.len_utf8());
3431 }
3432 }
3433 }
3434 KeyCode::Home if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName => {
3435 self.pivot_melt_modal.melt_variable_cursor = 0;
3436 }
3437 KeyCode::End if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName => {
3438 self.pivot_melt_modal.melt_variable_cursor =
3439 self.pivot_melt_modal.melt_variable_name.chars().count();
3440 }
3441 KeyCode::Char(c) if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName => {
3442 let byte_pos: usize = self
3443 .pivot_melt_modal
3444 .melt_variable_name
3445 .chars()
3446 .take(self.pivot_melt_modal.melt_variable_cursor)
3447 .map(|ch| ch.len_utf8())
3448 .sum();
3449 self.pivot_melt_modal.melt_variable_name.insert(byte_pos, c);
3450 self.pivot_melt_modal.melt_variable_cursor += 1;
3451 }
3452 KeyCode::Backspace
3453 if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName =>
3454 {
3455 if self.pivot_melt_modal.melt_variable_cursor > 0 {
3456 let prev_byte: usize = self
3457 .pivot_melt_modal
3458 .melt_variable_name
3459 .chars()
3460 .take(self.pivot_melt_modal.melt_variable_cursor - 1)
3461 .map(|ch| ch.len_utf8())
3462 .sum();
3463 if let Some(ch) = self.pivot_melt_modal.melt_variable_name[prev_byte..]
3464 .chars()
3465 .next()
3466 {
3467 self.pivot_melt_modal
3468 .melt_variable_name
3469 .drain(prev_byte..prev_byte + ch.len_utf8());
3470 self.pivot_melt_modal.melt_variable_cursor -= 1;
3471 }
3472 }
3473 }
3474 KeyCode::Delete if self.pivot_melt_modal.focus == PivotMeltFocus::MeltVarName => {
3475 let n = self.pivot_melt_modal.melt_variable_name.chars().count();
3476 if self.pivot_melt_modal.melt_variable_cursor < n {
3477 let byte_pos: usize = self
3478 .pivot_melt_modal
3479 .melt_variable_name
3480 .chars()
3481 .take(self.pivot_melt_modal.melt_variable_cursor)
3482 .map(|ch| ch.len_utf8())
3483 .sum();
3484 if let Some(ch) = self.pivot_melt_modal.melt_variable_name[byte_pos..]
3485 .chars()
3486 .next()
3487 {
3488 self.pivot_melt_modal
3489 .melt_variable_name
3490 .drain(byte_pos..byte_pos + ch.len_utf8());
3491 }
3492 }
3493 }
3494 KeyCode::Home if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName => {
3495 self.pivot_melt_modal.melt_value_cursor = 0;
3496 }
3497 KeyCode::End if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName => {
3498 self.pivot_melt_modal.melt_value_cursor =
3499 self.pivot_melt_modal.melt_value_name.chars().count();
3500 }
3501 KeyCode::Char(c) if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName => {
3502 let byte_pos: usize = self
3503 .pivot_melt_modal
3504 .melt_value_name
3505 .chars()
3506 .take(self.pivot_melt_modal.melt_value_cursor)
3507 .map(|ch| ch.len_utf8())
3508 .sum();
3509 self.pivot_melt_modal.melt_value_name.insert(byte_pos, c);
3510 self.pivot_melt_modal.melt_value_cursor += 1;
3511 }
3512 KeyCode::Backspace
3513 if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName =>
3514 {
3515 if self.pivot_melt_modal.melt_value_cursor > 0 {
3516 let prev_byte: usize = self
3517 .pivot_melt_modal
3518 .melt_value_name
3519 .chars()
3520 .take(self.pivot_melt_modal.melt_value_cursor - 1)
3521 .map(|ch| ch.len_utf8())
3522 .sum();
3523 if let Some(ch) = self.pivot_melt_modal.melt_value_name[prev_byte..]
3524 .chars()
3525 .next()
3526 {
3527 self.pivot_melt_modal
3528 .melt_value_name
3529 .drain(prev_byte..prev_byte + ch.len_utf8());
3530 self.pivot_melt_modal.melt_value_cursor -= 1;
3531 }
3532 }
3533 }
3534 KeyCode::Delete if self.pivot_melt_modal.focus == PivotMeltFocus::MeltValName => {
3535 let n = self.pivot_melt_modal.melt_value_name.chars().count();
3536 if self.pivot_melt_modal.melt_value_cursor < n {
3537 let byte_pos: usize = self
3538 .pivot_melt_modal
3539 .melt_value_name
3540 .chars()
3541 .take(self.pivot_melt_modal.melt_value_cursor)
3542 .map(|ch| ch.len_utf8())
3543 .sum();
3544 if let Some(ch) = self.pivot_melt_modal.melt_value_name[byte_pos..]
3545 .chars()
3546 .next()
3547 {
3548 self.pivot_melt_modal
3549 .melt_value_name
3550 .drain(byte_pos..byte_pos + ch.len_utf8());
3551 }
3552 }
3553 }
3554 _ => {}
3555 }
3556 return None;
3557 }
3558
3559 if self.input_mode == InputMode::Info {
3560 let on_tab_bar = self.info_modal.focus == InfoFocus::TabBar;
3561 let on_body = self.info_modal.focus == InfoFocus::Body;
3562 let schema_tab = self.info_modal.active_tab == InfoTab::Schema;
3563 let total_rows = self
3564 .data_table_state
3565 .as_ref()
3566 .map(|s| s.schema.len())
3567 .unwrap_or(0);
3568 let visible = self.info_modal.schema_visible_height;
3569
3570 match event.code {
3571 KeyCode::Esc | KeyCode::Char('i') if event.is_press() => {
3572 self.info_modal.close();
3573 self.input_mode = InputMode::Normal;
3574 }
3575 KeyCode::Tab if event.is_press() => {
3576 if schema_tab {
3577 self.info_modal.next_focus();
3578 }
3579 }
3580 KeyCode::BackTab if event.is_press() => {
3581 if schema_tab {
3582 self.info_modal.prev_focus();
3583 }
3584 }
3585 KeyCode::Left | KeyCode::Char('h') if event.is_press() && on_tab_bar => {
3586 let has_partitions = self
3587 .data_table_state
3588 .as_ref()
3589 .and_then(|s| s.partition_columns.as_ref())
3590 .map(|v| !v.is_empty())
3591 .unwrap_or(false);
3592 self.info_modal.switch_tab_prev(has_partitions);
3593 }
3594 KeyCode::Right | KeyCode::Char('l') if event.is_press() && on_tab_bar => {
3595 let has_partitions = self
3596 .data_table_state
3597 .as_ref()
3598 .and_then(|s| s.partition_columns.as_ref())
3599 .map(|v| !v.is_empty())
3600 .unwrap_or(false);
3601 self.info_modal.switch_tab(has_partitions);
3602 }
3603 KeyCode::Down | KeyCode::Char('j') if event.is_press() && on_body && schema_tab => {
3604 self.info_modal.schema_table_down(total_rows, visible);
3605 }
3606 KeyCode::Up | KeyCode::Char('k') if event.is_press() && on_body && schema_tab => {
3607 self.info_modal.schema_table_up(total_rows, visible);
3608 }
3609 _ => {}
3610 }
3611 return None;
3612 }
3613
3614 if self.input_mode == InputMode::Chart {
3615 if self.chart_export_modal.active {
3617 match event.code {
3618 KeyCode::Esc if event.is_press() => {
3619 self.chart_export_modal.close();
3620 }
3621 KeyCode::Tab if event.is_press() => {
3622 self.chart_export_modal.next_focus();
3623 }
3624 KeyCode::BackTab if event.is_press() => {
3625 self.chart_export_modal.prev_focus();
3626 }
3627 KeyCode::Up | KeyCode::Char('k')
3628 if event.is_press()
3629 && self.chart_export_modal.focus
3630 == ChartExportFocus::FormatSelector =>
3631 {
3632 let idx = ChartExportFormat::ALL
3633 .iter()
3634 .position(|&f| f == self.chart_export_modal.selected_format)
3635 .unwrap_or(0);
3636 let prev = if idx == 0 {
3637 ChartExportFormat::ALL.len() - 1
3638 } else {
3639 idx - 1
3640 };
3641 self.chart_export_modal.selected_format = ChartExportFormat::ALL[prev];
3642 }
3643 KeyCode::Down | KeyCode::Char('j')
3644 if event.is_press()
3645 && self.chart_export_modal.focus
3646 == ChartExportFocus::FormatSelector =>
3647 {
3648 let idx = ChartExportFormat::ALL
3649 .iter()
3650 .position(|&f| f == self.chart_export_modal.selected_format)
3651 .unwrap_or(0);
3652 let next = (idx + 1) % ChartExportFormat::ALL.len();
3653 self.chart_export_modal.selected_format = ChartExportFormat::ALL[next];
3654 }
3655 KeyCode::Left | KeyCode::Char('h')
3656 if event.is_press()
3657 && self.chart_export_modal.focus
3658 == ChartExportFocus::FormatSelector =>
3659 {
3660 let idx = ChartExportFormat::ALL
3661 .iter()
3662 .position(|&f| f == self.chart_export_modal.selected_format)
3663 .unwrap_or(0);
3664 let prev = if idx == 0 {
3665 ChartExportFormat::ALL.len() - 1
3666 } else {
3667 idx - 1
3668 };
3669 self.chart_export_modal.selected_format = ChartExportFormat::ALL[prev];
3670 }
3671 KeyCode::Right | KeyCode::Char('l')
3672 if event.is_press()
3673 && self.chart_export_modal.focus
3674 == ChartExportFocus::FormatSelector =>
3675 {
3676 let idx = ChartExportFormat::ALL
3677 .iter()
3678 .position(|&f| f == self.chart_export_modal.selected_format)
3679 .unwrap_or(0);
3680 let next = (idx + 1) % ChartExportFormat::ALL.len();
3681 self.chart_export_modal.selected_format = ChartExportFormat::ALL[next];
3682 }
3683 KeyCode::Enter if event.is_press() => match self.chart_export_modal.focus {
3684 ChartExportFocus::PathInput | ChartExportFocus::ExportButton => {
3685 let path_str = self.chart_export_modal.path_input.value.trim();
3686 if !path_str.is_empty() {
3687 let title =
3688 self.chart_export_modal.title_input.value.trim().to_string();
3689 let (width, height) = self.chart_export_modal.export_dimensions();
3690 let mut path = PathBuf::from(path_str);
3691 let format = self.chart_export_modal.selected_format;
3692 if path.extension().is_none() {
3694 path.set_extension(format.extension());
3695 }
3696 let path_display = path.display().to_string();
3697 if path.exists() {
3698 self.pending_chart_export =
3699 Some((path, format, title, width, height));
3700 self.chart_export_modal.close();
3701 self.confirmation_modal.show(format!(
3702 "File already exists:\n{}\n\nDo you wish to overwrite this file?",
3703 path_display
3704 ));
3705 } else {
3706 self.chart_export_modal.close();
3707 return Some(AppEvent::ChartExport(
3708 path, format, title, width, height,
3709 ));
3710 }
3711 }
3712 }
3713 ChartExportFocus::CancelButton => {
3714 self.chart_export_modal.close();
3715 }
3716 _ => {}
3717 },
3718 _ => {
3719 if event.is_press() {
3720 if self.chart_export_modal.focus == ChartExportFocus::TitleInput {
3721 let _ = self.chart_export_modal.title_input.handle_key(event, None);
3722 } else if self.chart_export_modal.focus == ChartExportFocus::PathInput {
3723 let _ = self.chart_export_modal.path_input.handle_key(event, None);
3724 } else if self.chart_export_modal.focus == ChartExportFocus::WidthInput
3725 {
3726 let allow = match event.code {
3727 KeyCode::Char(c) if c.is_ascii_digit() => true,
3728 KeyCode::Backspace
3729 | KeyCode::Delete
3730 | KeyCode::Left
3731 | KeyCode::Right
3732 | KeyCode::Home
3733 | KeyCode::End => true,
3734 _ => false,
3735 };
3736 if allow {
3737 let _ =
3738 self.chart_export_modal.width_input.handle_key(event, None);
3739 }
3740 } else if self.chart_export_modal.focus == ChartExportFocus::HeightInput
3741 {
3742 let allow = match event.code {
3743 KeyCode::Char(c) if c.is_ascii_digit() => true,
3744 KeyCode::Backspace
3745 | KeyCode::Delete
3746 | KeyCode::Left
3747 | KeyCode::Right
3748 | KeyCode::Home
3749 | KeyCode::End => true,
3750 _ => false,
3751 };
3752 if allow {
3753 let _ = self
3754 .chart_export_modal
3755 .height_input
3756 .handle_key(event, None);
3757 }
3758 }
3759 }
3760 }
3761 }
3762 return None;
3763 }
3764
3765 match event.code {
3766 KeyCode::Char('e')
3767 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3768 {
3769 if self.data_table_state.is_some() && self.chart_modal.can_export() {
3771 self.chart_export_modal
3772 .open(&self.theme, self.history_limit);
3773 }
3774 }
3775 KeyCode::Char('?') if event.is_press() => {
3777 self.show_help = true;
3778 }
3779 KeyCode::Esc if event.is_press() => {
3780 self.chart_modal.close();
3781 self.chart_cache.clear();
3782 self.input_mode = InputMode::Normal;
3783 }
3784 KeyCode::Tab if event.is_press() => {
3785 self.chart_modal.next_focus();
3786 }
3787 KeyCode::BackTab if event.is_press() => {
3788 self.chart_modal.prev_focus();
3789 }
3790 KeyCode::Enter | KeyCode::Char(' ') if event.is_press() => {
3791 match self.chart_modal.focus {
3792 ChartFocus::YStartsAtZero => self.chart_modal.toggle_y_starts_at_zero(),
3793 ChartFocus::LogScale => self.chart_modal.toggle_log_scale(),
3794 ChartFocus::ShowLegend => self.chart_modal.toggle_show_legend(),
3795 ChartFocus::XList => self.chart_modal.x_list_toggle(),
3796 ChartFocus::YList => self.chart_modal.y_list_toggle(),
3797 ChartFocus::ChartType => self.chart_modal.next_chart_type(),
3798 ChartFocus::HistList => self.chart_modal.hist_list_toggle(),
3799 ChartFocus::BoxList => self.chart_modal.box_list_toggle(),
3800 ChartFocus::KdeList => self.chart_modal.kde_list_toggle(),
3801 ChartFocus::HeatmapXList => self.chart_modal.heatmap_x_list_toggle(),
3802 ChartFocus::HeatmapYList => self.chart_modal.heatmap_y_list_toggle(),
3803 _ => {}
3804 }
3805 }
3806 KeyCode::Char('+') | KeyCode::Char('=')
3807 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3808 {
3809 match self.chart_modal.focus {
3810 ChartFocus::HistBins => self.chart_modal.adjust_hist_bins(1),
3811 ChartFocus::HeatmapBins => self.chart_modal.adjust_heatmap_bins(1),
3812 ChartFocus::KdeBandwidth => self
3813 .chart_modal
3814 .adjust_kde_bandwidth_factor(chart_modal::KDE_BANDWIDTH_STEP),
3815 ChartFocus::LimitRows => self.chart_modal.adjust_row_limit(1),
3816 _ => {}
3817 }
3818 }
3819 KeyCode::Char('-')
3820 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3821 {
3822 match self.chart_modal.focus {
3823 ChartFocus::HistBins => self.chart_modal.adjust_hist_bins(-1),
3824 ChartFocus::HeatmapBins => self.chart_modal.adjust_heatmap_bins(-1),
3825 ChartFocus::KdeBandwidth => self
3826 .chart_modal
3827 .adjust_kde_bandwidth_factor(-chart_modal::KDE_BANDWIDTH_STEP),
3828 ChartFocus::LimitRows => self.chart_modal.adjust_row_limit(-1),
3829 _ => {}
3830 }
3831 }
3832 KeyCode::Left | KeyCode::Char('h')
3833 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3834 {
3835 match self.chart_modal.focus {
3836 ChartFocus::TabBar => self.chart_modal.prev_chart_kind(),
3837 ChartFocus::ChartType => self.chart_modal.prev_chart_type(),
3838 ChartFocus::HistBins => self.chart_modal.adjust_hist_bins(-1),
3839 ChartFocus::HeatmapBins => self.chart_modal.adjust_heatmap_bins(-1),
3840 ChartFocus::KdeBandwidth => self
3841 .chart_modal
3842 .adjust_kde_bandwidth_factor(-chart_modal::KDE_BANDWIDTH_STEP),
3843 ChartFocus::LimitRows => self.chart_modal.adjust_row_limit(-1),
3844 _ => {}
3845 }
3846 }
3847 KeyCode::Right | KeyCode::Char('l')
3848 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3849 {
3850 match self.chart_modal.focus {
3851 ChartFocus::TabBar => self.chart_modal.next_chart_kind(),
3852 ChartFocus::ChartType => self.chart_modal.next_chart_type(),
3853 ChartFocus::HistBins => self.chart_modal.adjust_hist_bins(1),
3854 ChartFocus::HeatmapBins => self.chart_modal.adjust_heatmap_bins(1),
3855 ChartFocus::KdeBandwidth => self
3856 .chart_modal
3857 .adjust_kde_bandwidth_factor(chart_modal::KDE_BANDWIDTH_STEP),
3858 ChartFocus::LimitRows => self.chart_modal.adjust_row_limit(1),
3859 _ => {}
3860 }
3861 }
3862 KeyCode::PageUp
3863 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3864 {
3865 if self.chart_modal.focus == ChartFocus::LimitRows {
3866 self.chart_modal.adjust_row_limit_page(1);
3867 }
3868 }
3869 KeyCode::PageDown
3870 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3871 {
3872 if self.chart_modal.focus == ChartFocus::LimitRows {
3873 self.chart_modal.adjust_row_limit_page(-1);
3874 }
3875 }
3876 KeyCode::Up | KeyCode::Char('k')
3877 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3878 {
3879 match self.chart_modal.focus {
3880 ChartFocus::ChartType => self.chart_modal.prev_chart_type(),
3881 ChartFocus::XList => self.chart_modal.x_list_up(),
3882 ChartFocus::YList => self.chart_modal.y_list_up(),
3883 ChartFocus::HistList => self.chart_modal.hist_list_up(),
3884 ChartFocus::BoxList => self.chart_modal.box_list_up(),
3885 ChartFocus::KdeList => self.chart_modal.kde_list_up(),
3886 ChartFocus::HeatmapXList => self.chart_modal.heatmap_x_list_up(),
3887 ChartFocus::HeatmapYList => self.chart_modal.heatmap_y_list_up(),
3888 _ => {}
3889 }
3890 }
3891 KeyCode::Down | KeyCode::Char('j')
3892 if event.is_press() && !self.chart_modal.is_text_input_focused() =>
3893 {
3894 match self.chart_modal.focus {
3895 ChartFocus::ChartType => self.chart_modal.next_chart_type(),
3896 ChartFocus::XList => self.chart_modal.x_list_down(),
3897 ChartFocus::YList => self.chart_modal.y_list_down(),
3898 ChartFocus::HistList => self.chart_modal.hist_list_down(),
3899 ChartFocus::BoxList => self.chart_modal.box_list_down(),
3900 ChartFocus::KdeList => self.chart_modal.kde_list_down(),
3901 ChartFocus::HeatmapXList => self.chart_modal.heatmap_x_list_down(),
3902 ChartFocus::HeatmapYList => self.chart_modal.heatmap_y_list_down(),
3903 _ => {}
3904 }
3905 }
3906 _ => {
3907 if event.is_press() {
3909 if self.chart_modal.focus == ChartFocus::XInput {
3910 let _ = self.chart_modal.x_input.handle_key(event, None);
3911 } else if self.chart_modal.focus == ChartFocus::YInput {
3912 let _ = self.chart_modal.y_input.handle_key(event, None);
3913 } else if self.chart_modal.focus == ChartFocus::HistInput {
3914 let _ = self.chart_modal.hist_input.handle_key(event, None);
3915 } else if self.chart_modal.focus == ChartFocus::BoxInput {
3916 let _ = self.chart_modal.box_input.handle_key(event, None);
3917 } else if self.chart_modal.focus == ChartFocus::KdeInput {
3918 let _ = self.chart_modal.kde_input.handle_key(event, None);
3919 } else if self.chart_modal.focus == ChartFocus::HeatmapXInput {
3920 let _ = self.chart_modal.heatmap_x_input.handle_key(event, None);
3921 } else if self.chart_modal.focus == ChartFocus::HeatmapYInput {
3922 let _ = self.chart_modal.heatmap_y_input.handle_key(event, None);
3923 }
3924 }
3925 }
3926 }
3927 return None;
3928 }
3929
3930 if self.analysis_modal.active {
3931 match event.code {
3932 KeyCode::Esc => {
3933 if self.analysis_modal.show_help {
3934 self.analysis_modal.show_help = false;
3935 } else if self.analysis_modal.view != analysis_modal::AnalysisView::Main {
3936 self.analysis_modal.close_detail();
3938 } else {
3939 self.analysis_modal.close();
3940 }
3941 }
3942 KeyCode::Char('?') => {
3943 self.analysis_modal.show_help = !self.analysis_modal.show_help;
3944 }
3945 KeyCode::Char('r') => {
3946 if self.sampling_threshold.is_some() {
3947 self.analysis_modal.recalculate();
3948 match self.analysis_modal.selected_tool {
3949 Some(analysis_modal::AnalysisTool::Describe) => {
3950 self.analysis_modal.describe_results = None;
3951 self.analysis_modal.computing = Some(AnalysisProgress {
3952 phase: "Describing data".to_string(),
3953 current: 0,
3954 total: 1,
3955 });
3956 self.analysis_computation = Some(AnalysisComputationState {
3957 df: None,
3958 schema: None,
3959 partial_stats: Vec::new(),
3960 current: 0,
3961 total: 0,
3962 total_rows: 0,
3963 sample_seed: self.analysis_modal.random_seed,
3964 sample_size: None,
3965 });
3966 self.busy = true;
3967 return Some(AppEvent::AnalysisChunk);
3968 }
3969 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
3970 self.analysis_modal.distribution_results = None;
3971 self.analysis_modal.computing = Some(AnalysisProgress {
3972 phase: "Distribution".to_string(),
3973 current: 0,
3974 total: 1,
3975 });
3976 self.busy = true;
3977 return Some(AppEvent::AnalysisDistributionCompute);
3978 }
3979 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
3980 self.analysis_modal.correlation_results = None;
3981 self.analysis_modal.computing = Some(AnalysisProgress {
3982 phase: "Correlation".to_string(),
3983 current: 0,
3984 total: 1,
3985 });
3986 self.busy = true;
3987 return Some(AppEvent::AnalysisCorrelationCompute);
3988 }
3989 None => {}
3990 }
3991 }
3992 }
3993 KeyCode::Tab => {
3994 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
3995 self.analysis_modal.switch_focus();
3997 } else if self.analysis_modal.view
3998 == analysis_modal::AnalysisView::DistributionDetail
3999 {
4000 } else {
4003 self.analysis_modal.next_detail_section();
4005 }
4006 }
4007 KeyCode::Enter => {
4008 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4009 if self.analysis_modal.focus == analysis_modal::AnalysisFocus::Sidebar {
4010 self.analysis_modal.select_tool();
4012 match self.analysis_modal.selected_tool {
4014 Some(analysis_modal::AnalysisTool::Describe)
4015 if self.analysis_modal.describe_results.is_none() =>
4016 {
4017 self.analysis_modal.computing = Some(AnalysisProgress {
4018 phase: "Describing data".to_string(),
4019 current: 0,
4020 total: 1,
4021 });
4022 self.analysis_computation = Some(AnalysisComputationState {
4023 df: None,
4024 schema: None,
4025 partial_stats: Vec::new(),
4026 current: 0,
4027 total: 0,
4028 total_rows: 0,
4029 sample_seed: self.analysis_modal.random_seed,
4030 sample_size: None,
4031 });
4032 self.busy = true;
4033 return Some(AppEvent::AnalysisChunk);
4034 }
4035 Some(analysis_modal::AnalysisTool::DistributionAnalysis)
4036 if self.analysis_modal.distribution_results.is_none() =>
4037 {
4038 self.analysis_modal.computing = Some(AnalysisProgress {
4039 phase: "Distribution".to_string(),
4040 current: 0,
4041 total: 1,
4042 });
4043 self.busy = true;
4044 return Some(AppEvent::AnalysisDistributionCompute);
4045 }
4046 Some(analysis_modal::AnalysisTool::CorrelationMatrix)
4047 if self.analysis_modal.correlation_results.is_none() =>
4048 {
4049 self.analysis_modal.computing = Some(AnalysisProgress {
4050 phase: "Correlation".to_string(),
4051 current: 0,
4052 total: 1,
4053 });
4054 self.busy = true;
4055 return Some(AppEvent::AnalysisCorrelationCompute);
4056 }
4057 _ => {}
4058 }
4059 } else {
4060 match self.analysis_modal.selected_tool {
4062 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4063 self.analysis_modal.open_distribution_detail();
4064 }
4065 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4066 self.analysis_modal.open_correlation_detail();
4067 }
4068 _ => {}
4069 }
4070 }
4071 }
4072 }
4073 KeyCode::Down | KeyCode::Char('j') => {
4074 match self.analysis_modal.view {
4075 analysis_modal::AnalysisView::Main => {
4076 match self.analysis_modal.focus {
4077 analysis_modal::AnalysisFocus::Sidebar => {
4078 self.analysis_modal.next_tool();
4080 }
4081 analysis_modal::AnalysisFocus::Main => {
4082 match self.analysis_modal.selected_tool {
4084 Some(analysis_modal::AnalysisTool::Describe) => {
4085 if let Some(state) = &self.data_table_state {
4086 let max_rows = state.schema.len();
4087 self.analysis_modal.next_row(max_rows);
4088 }
4089 }
4090 Some(
4091 analysis_modal::AnalysisTool::DistributionAnalysis,
4092 ) => {
4093 if let Some(results) =
4094 self.analysis_modal.current_results()
4095 {
4096 let max_rows = results.distribution_analyses.len();
4097 self.analysis_modal.next_row(max_rows);
4098 }
4099 }
4100 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4101 if let Some(results) =
4102 self.analysis_modal.current_results()
4103 {
4104 if let Some(corr) = &results.correlation_matrix {
4105 let max_rows = corr.columns.len();
4106 let row_header_width = 20u16;
4108 let cell_width = 12u16;
4109 let column_spacing = 1u16;
4110 let estimated_width = 80u16;
4111 let available_width = estimated_width
4112 .saturating_sub(row_header_width);
4113 let mut calculated_visible = 0usize;
4114 let mut used = 0u16;
4115 let max_cols = corr.columns.len();
4116 loop {
4117 let needed = if calculated_visible == 0 {
4118 cell_width
4119 } else {
4120 column_spacing + cell_width
4121 };
4122 if used + needed <= available_width
4123 && calculated_visible < max_cols
4124 {
4125 used += needed;
4126 calculated_visible += 1;
4127 } else {
4128 break;
4129 }
4130 }
4131 let visible_cols =
4132 calculated_visible.max(1).min(max_cols);
4133 self.analysis_modal.move_correlation_cell(
4134 (1, 0),
4135 max_rows,
4136 max_rows,
4137 visible_cols,
4138 );
4139 }
4140 }
4141 }
4142 None => {}
4143 }
4144 }
4145 _ => {}
4146 }
4147 }
4148 analysis_modal::AnalysisView::DistributionDetail => {
4149 if self.analysis_modal.focus
4150 == analysis_modal::AnalysisFocus::DistributionSelector
4151 {
4152 self.analysis_modal.next_distribution();
4153 }
4154 }
4155 _ => {}
4156 }
4157 }
4158 KeyCode::Char('s') => {
4159 if self.analysis_modal.view == analysis_modal::AnalysisView::DistributionDetail
4161 {
4162 self.analysis_modal.histogram_scale =
4163 match self.analysis_modal.histogram_scale {
4164 analysis_modal::HistogramScale::Linear => {
4165 analysis_modal::HistogramScale::Log
4166 }
4167 analysis_modal::HistogramScale::Log => {
4168 analysis_modal::HistogramScale::Linear
4169 }
4170 };
4171 }
4172 }
4173 KeyCode::Up | KeyCode::Char('k') => {
4174 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4175 self.analysis_modal.previous_row();
4176 } else if self.analysis_modal.view
4177 == analysis_modal::AnalysisView::DistributionDetail
4178 && self.analysis_modal.focus
4179 == analysis_modal::AnalysisFocus::DistributionSelector
4180 {
4181 self.analysis_modal.previous_distribution();
4182 }
4183 }
4184 KeyCode::Left | KeyCode::Char('h')
4185 if !event.modifiers.contains(KeyModifiers::CONTROL) =>
4186 {
4187 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4188 match self.analysis_modal.focus {
4189 analysis_modal::AnalysisFocus::Sidebar => {
4190 }
4192 analysis_modal::AnalysisFocus::DistributionSelector => {
4193 }
4195 analysis_modal::AnalysisFocus::Main => {
4196 match self.analysis_modal.selected_tool {
4197 Some(analysis_modal::AnalysisTool::Describe) => {
4198 self.analysis_modal.scroll_left();
4199 }
4200 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4201 self.analysis_modal.scroll_left();
4202 }
4203 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4204 if let Some(results) = self.analysis_modal.current_results()
4205 {
4206 if let Some(corr) = &results.correlation_matrix {
4207 let max_cols = corr.columns.len();
4208 let row_header_width = 20u16;
4211 let cell_width = 12u16;
4212 let column_spacing = 1u16;
4213 let estimated_width = 80u16; let available_width = estimated_width
4218 .saturating_sub(row_header_width);
4219 let mut calculated_visible = 0usize;
4221 let mut used = 0u16;
4222 loop {
4223 let needed = if calculated_visible == 0 {
4224 cell_width
4225 } else {
4226 column_spacing + cell_width
4227 };
4228 if used + needed <= available_width
4229 && calculated_visible < max_cols
4230 {
4231 used += needed;
4232 calculated_visible += 1;
4233 } else {
4234 break;
4235 }
4236 }
4237 let visible_cols =
4238 calculated_visible.max(1).min(max_cols);
4239 self.analysis_modal.move_correlation_cell(
4240 (0, -1),
4241 max_cols,
4242 max_cols,
4243 visible_cols,
4244 );
4245 }
4246 }
4247 }
4248 None => {}
4249 }
4250 }
4251 }
4252 }
4253 }
4254 KeyCode::Right | KeyCode::Char('l') => {
4255 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4256 match self.analysis_modal.focus {
4257 analysis_modal::AnalysisFocus::Sidebar => {
4258 }
4260 analysis_modal::AnalysisFocus::DistributionSelector => {
4261 }
4263 analysis_modal::AnalysisFocus::Main => {
4264 match self.analysis_modal.selected_tool {
4265 Some(analysis_modal::AnalysisTool::Describe) => {
4266 let max_stats = 12;
4268 let visible_stats = 8; self.analysis_modal.scroll_right(max_stats, visible_stats);
4271 }
4272 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4273 let max_stats = 8;
4275 let visible_stats = 6; self.analysis_modal.scroll_right(max_stats, visible_stats);
4278 }
4279 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4280 if let Some(results) = self.analysis_modal.current_results()
4281 {
4282 if let Some(corr) = &results.correlation_matrix {
4283 let max_cols = corr.columns.len();
4284 let row_header_width = 20u16;
4286 let cell_width = 12u16;
4287 let column_spacing = 1u16;
4288 let estimated_width = 80u16; let available_width = estimated_width
4290 .saturating_sub(row_header_width);
4291 let mut calculated_visible = 0usize;
4292 let mut used = 0u16;
4293 loop {
4294 let needed = if calculated_visible == 0 {
4295 cell_width
4296 } else {
4297 column_spacing + cell_width
4298 };
4299 if used + needed <= available_width
4300 && calculated_visible < max_cols
4301 {
4302 used += needed;
4303 calculated_visible += 1;
4304 } else {
4305 break;
4306 }
4307 }
4308 let visible_cols =
4309 calculated_visible.max(1).min(max_cols);
4310 self.analysis_modal.move_correlation_cell(
4311 (0, 1),
4312 max_cols,
4313 max_cols,
4314 visible_cols,
4315 );
4316 }
4317 }
4318 }
4319 None => {}
4320 }
4321 }
4322 }
4323 }
4324 }
4325 KeyCode::PageDown => {
4326 if self.analysis_modal.view == analysis_modal::AnalysisView::Main
4327 && self.analysis_modal.focus == analysis_modal::AnalysisFocus::Main
4328 {
4329 match self.analysis_modal.selected_tool {
4330 Some(analysis_modal::AnalysisTool::Describe) => {
4331 if let Some(state) = &self.data_table_state {
4332 let max_rows = state.schema.len();
4333 let page_size = 10;
4334 self.analysis_modal.page_down(max_rows, page_size);
4335 }
4336 }
4337 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4338 if let Some(results) = self.analysis_modal.current_results() {
4339 let max_rows = results.distribution_analyses.len();
4340 let page_size = 10;
4341 self.analysis_modal.page_down(max_rows, page_size);
4342 }
4343 }
4344 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4345 if let Some(results) = self.analysis_modal.current_results() {
4346 if let Some(corr) = &results.correlation_matrix {
4347 let max_rows = corr.columns.len();
4348 let page_size = 10;
4349 self.analysis_modal.page_down(max_rows, page_size);
4350 }
4351 }
4352 }
4353 None => {}
4354 }
4355 }
4356 }
4357 KeyCode::PageUp => {
4358 if self.analysis_modal.view == analysis_modal::AnalysisView::Main
4359 && self.analysis_modal.focus == analysis_modal::AnalysisFocus::Main
4360 {
4361 let page_size = 10;
4362 self.analysis_modal.page_up(page_size);
4363 }
4364 }
4365 KeyCode::Home => {
4366 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4367 match self.analysis_modal.focus {
4368 analysis_modal::AnalysisFocus::Sidebar => {
4369 self.analysis_modal.sidebar_state.select(Some(0));
4370 }
4371 analysis_modal::AnalysisFocus::DistributionSelector => {
4372 self.analysis_modal
4373 .distribution_selector_state
4374 .select(Some(0));
4375 }
4376 analysis_modal::AnalysisFocus::Main => {
4377 match self.analysis_modal.selected_tool {
4378 Some(analysis_modal::AnalysisTool::Describe) => {
4379 self.analysis_modal.table_state.select(Some(0));
4380 }
4381 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4382 self.analysis_modal
4383 .distribution_table_state
4384 .select(Some(0));
4385 }
4386 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4387 self.analysis_modal.correlation_table_state.select(Some(0));
4388 self.analysis_modal.selected_correlation = Some((0, 0));
4389 }
4390 None => {}
4391 }
4392 }
4393 }
4394 }
4395 }
4396 KeyCode::End => {
4397 if self.analysis_modal.view == analysis_modal::AnalysisView::Main {
4398 match self.analysis_modal.focus {
4399 analysis_modal::AnalysisFocus::Sidebar => {
4400 self.analysis_modal.sidebar_state.select(Some(2));
4401 }
4403 analysis_modal::AnalysisFocus::DistributionSelector => {
4404 self.analysis_modal
4405 .distribution_selector_state
4406 .select(Some(13)); }
4408 analysis_modal::AnalysisFocus::Main => {
4409 match self.analysis_modal.selected_tool {
4410 Some(analysis_modal::AnalysisTool::Describe) => {
4411 if let Some(state) = &self.data_table_state {
4412 let max_rows = state.schema.len();
4413 if max_rows > 0 {
4414 self.analysis_modal
4415 .table_state
4416 .select(Some(max_rows - 1));
4417 }
4418 }
4419 }
4420 Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
4421 if let Some(results) = self.analysis_modal.current_results()
4422 {
4423 let max_rows = results.distribution_analyses.len();
4424 if max_rows > 0 {
4425 self.analysis_modal
4426 .distribution_table_state
4427 .select(Some(max_rows - 1));
4428 }
4429 }
4430 }
4431 Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
4432 if let Some(results) = self.analysis_modal.current_results()
4433 {
4434 if let Some(corr) = &results.correlation_matrix {
4435 let max_rows = corr.columns.len();
4436 if max_rows > 0 {
4437 self.analysis_modal
4438 .correlation_table_state
4439 .select(Some(max_rows - 1));
4440 self.analysis_modal.selected_correlation =
4441 Some((max_rows - 1, max_rows - 1));
4442 }
4443 }
4444 }
4445 }
4446 None => {}
4447 }
4448 }
4449 }
4450 }
4451 }
4452 _ => {}
4453 }
4454 return None;
4455 }
4456
4457 if self.template_modal.active {
4458 match event.code {
4459 KeyCode::Esc => {
4460 if self.template_modal.show_score_details {
4461 self.template_modal.show_score_details = false;
4463 } else if self.template_modal.delete_confirm {
4464 self.template_modal.delete_confirm = false;
4466 } else if self.template_modal.mode == TemplateModalMode::Create
4467 || self.template_modal.mode == TemplateModalMode::Edit
4468 {
4469 self.template_modal.exit_create_mode();
4471 } else {
4472 if self.template_modal.show_help {
4474 self.template_modal.show_help = false;
4475 } else {
4476 self.template_modal.active = false;
4477 self.template_modal.show_help = false;
4478 self.template_modal.delete_confirm = false;
4479 }
4480 }
4481 }
4482 KeyCode::BackTab if self.template_modal.delete_confirm => {
4483 self.template_modal.delete_confirm_focus =
4484 !self.template_modal.delete_confirm_focus;
4485 }
4486 KeyCode::Left
4487 | KeyCode::Right
4488 | KeyCode::Up
4489 | KeyCode::Down
4490 | KeyCode::Char('h')
4491 | KeyCode::Char('l')
4492 | KeyCode::Char('j')
4493 | KeyCode::Char('k')
4494 if self.template_modal.delete_confirm =>
4495 {
4496 self.template_modal.delete_confirm_focus =
4497 !self.template_modal.delete_confirm_focus;
4498 }
4499 KeyCode::Tab if !self.template_modal.delete_confirm => {
4500 self.template_modal.next_focus();
4501 }
4502 KeyCode::BackTab => {
4503 self.template_modal.prev_focus();
4504 }
4505 KeyCode::Char('s') if self.template_modal.mode == TemplateModalMode::List => {
4506 self.template_modal
4508 .enter_create_mode(self.history_limit, &self.theme);
4509 if let Some(ref path) = self.path {
4511 self.template_modal.create_name_input.value =
4513 self.template_manager.generate_next_template_name();
4514 self.template_modal.create_name_input.cursor =
4515 self.template_modal.create_name_input.value.chars().count();
4516
4517 let absolute_path = if path.is_absolute() {
4519 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
4520 } else {
4521 if let Ok(cwd) = std::env::current_dir() {
4523 let abs = cwd.join(path);
4524 abs.canonicalize().unwrap_or(abs)
4525 } else {
4526 path.to_path_buf()
4527 }
4528 };
4529 self.template_modal.create_exact_path_input.value =
4530 absolute_path.to_string_lossy().to_string();
4531 self.template_modal.create_exact_path_input.cursor = self
4532 .template_modal
4533 .create_exact_path_input
4534 .value
4535 .chars()
4536 .count();
4537
4538 if let Ok(cwd) = std::env::current_dir() {
4540 let abs_path = if path.is_absolute() {
4541 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
4542 } else {
4543 let abs = cwd.join(path);
4544 abs.canonicalize().unwrap_or(abs)
4545 };
4546 if let Ok(canonical_cwd) = cwd.canonicalize() {
4547 if let Ok(rel_path) = abs_path.strip_prefix(&canonical_cwd) {
4548 let rel_str = rel_path.to_string_lossy().to_string();
4550 self.template_modal.create_relative_path_input.value =
4551 rel_str.strip_prefix('/').unwrap_or(&rel_str).to_string();
4552 self.template_modal.create_relative_path_input.cursor = self
4553 .template_modal
4554 .create_relative_path_input
4555 .value
4556 .chars()
4557 .count();
4558 } else {
4559 self.template_modal.create_relative_path_input.clear();
4561 }
4562 } else {
4563 if let Ok(rel_path) = abs_path.strip_prefix(&cwd) {
4565 let rel_str = rel_path.to_string_lossy().to_string();
4566 self.template_modal.create_relative_path_input.value =
4567 rel_str.strip_prefix('/').unwrap_or(&rel_str).to_string();
4568 self.template_modal.create_relative_path_input.cursor = self
4569 .template_modal
4570 .create_relative_path_input
4571 .value
4572 .chars()
4573 .count();
4574 } else {
4575 self.template_modal.create_relative_path_input.clear();
4576 }
4577 }
4578 } else {
4579 self.template_modal.create_relative_path_input.clear();
4580 }
4581
4582 if let Some(parent) = path.parent() {
4584 if let Some(parent_str) = parent.to_str() {
4585 if path.file_name().is_some() {
4586 if let Some(ext) = path.extension() {
4587 self.template_modal.create_path_pattern_input.value =
4588 format!("{}/*.{}", parent_str, ext.to_string_lossy());
4589 self.template_modal.create_path_pattern_input.cursor = self
4590 .template_modal
4591 .create_path_pattern_input
4592 .value
4593 .chars()
4594 .count();
4595 }
4596 }
4597 }
4598 }
4599
4600 if let Some(filename) = path.file_name() {
4602 if let Some(filename_str) = filename.to_str() {
4603 let mut pattern = filename_str.to_string();
4605 use regex::Regex;
4607 if let Ok(re) = Regex::new(r"\d+") {
4608 pattern = re.replace_all(&pattern, "*").to_string();
4609 }
4610 self.template_modal.create_filename_pattern_input.value = pattern;
4611 self.template_modal.create_filename_pattern_input.cursor = self
4612 .template_modal
4613 .create_filename_pattern_input
4614 .value
4615 .chars()
4616 .count();
4617 }
4618 }
4619 }
4620
4621 if let Some(ref state) = self.data_table_state {
4623 if !state.schema.is_empty() {
4624 self.template_modal.create_schema_match_enabled = false;
4625 }
4627 }
4628 }
4629 KeyCode::Char('e') if self.template_modal.mode == TemplateModalMode::List => {
4630 if let Some(idx) = self.template_modal.table_state.selected() {
4632 if let Some((template, _)) = self.template_modal.templates.get(idx) {
4633 let template_clone = template.clone();
4634 self.template_modal.enter_edit_mode(
4635 &template_clone,
4636 self.history_limit,
4637 &self.theme,
4638 );
4639 }
4640 }
4641 }
4642 KeyCode::Char('d')
4643 if self.template_modal.mode == TemplateModalMode::List
4644 && !self.template_modal.delete_confirm =>
4645 {
4646 if let Some(_idx) = self.template_modal.table_state.selected() {
4648 self.template_modal.delete_confirm = true;
4649 self.template_modal.delete_confirm_focus = false; }
4651 }
4652 KeyCode::Char('?')
4653 if self.template_modal.mode == TemplateModalMode::List
4654 && !self.template_modal.delete_confirm =>
4655 {
4656 self.template_modal.show_score_details = true;
4658 }
4659 KeyCode::Char('D') if self.template_modal.delete_confirm => {
4660 if let Some(idx) = self.template_modal.table_state.selected() {
4662 if let Some((template, _)) = self.template_modal.templates.get(idx) {
4663 if self.template_manager.delete_template(&template.id).is_err() {
4664 } else {
4666 if let Some(ref state) = self.data_table_state {
4668 if let Some(ref path) = self.path {
4669 self.template_modal.templates = self
4670 .template_manager
4671 .find_relevant_templates(path, &state.schema);
4672 if !self.template_modal.templates.is_empty() {
4673 let new_idx = idx.min(
4674 self.template_modal
4675 .templates
4676 .len()
4677 .saturating_sub(1),
4678 );
4679 self.template_modal.table_state.select(Some(new_idx));
4680 } else {
4681 self.template_modal.table_state.select(None);
4682 }
4683 }
4684 }
4685 }
4686 self.template_modal.delete_confirm = false;
4687 }
4688 }
4689 }
4690 KeyCode::Tab if self.template_modal.delete_confirm => {
4691 self.template_modal.delete_confirm_focus =
4693 !self.template_modal.delete_confirm_focus;
4694 }
4695 KeyCode::Enter if self.template_modal.delete_confirm => {
4696 if self.template_modal.delete_confirm_focus {
4698 if let Some(idx) = self.template_modal.table_state.selected() {
4700 if let Some((template, _)) = self.template_modal.templates.get(idx) {
4701 if self.template_manager.delete_template(&template.id).is_err() {
4702 } else {
4704 if let Some(ref state) = self.data_table_state {
4706 if let Some(ref path) = self.path {
4707 self.template_modal.templates = self
4708 .template_manager
4709 .find_relevant_templates(path, &state.schema);
4710 if !self.template_modal.templates.is_empty() {
4711 let new_idx = idx.min(
4712 self.template_modal
4713 .templates
4714 .len()
4715 .saturating_sub(1),
4716 );
4717 self.template_modal
4718 .table_state
4719 .select(Some(new_idx));
4720 } else {
4721 self.template_modal.table_state.select(None);
4722 }
4723 }
4724 }
4725 }
4726 self.template_modal.delete_confirm = false;
4727 }
4728 }
4729 } else {
4730 self.template_modal.delete_confirm = false;
4732 }
4733 }
4734 KeyCode::Enter => {
4735 match self.template_modal.mode {
4736 TemplateModalMode::List => {
4737 match self.template_modal.focus {
4738 TemplateFocus::TemplateList => {
4739 let template_idx = self.template_modal.table_state.selected();
4741 if let Some(idx) = template_idx {
4742 if let Some((template, _)) =
4743 self.template_modal.templates.get(idx)
4744 {
4745 let template_clone = template.clone();
4746 if let Err(e) = self.apply_template(&template_clone) {
4747 self.error_modal.show(format!(
4749 "Error applying template: {}",
4750 e
4751 ));
4752 } else {
4754 self.template_modal.active = false;
4756 }
4757 }
4758 }
4759 }
4760 TemplateFocus::CreateButton => {
4761 }
4764 _ => {}
4765 }
4766 }
4767 TemplateModalMode::Create | TemplateModalMode::Edit => {
4768 if self.template_modal.create_focus == CreateFocus::Description {
4770 let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
4771 self.template_modal
4772 .create_description_input
4773 .handle_key(&event, None);
4774 let area_height = 10; self.template_modal
4777 .create_description_input
4778 .ensure_cursor_visible(area_height, 80);
4779 return None;
4780 }
4781 match self.template_modal.create_focus {
4782 CreateFocus::SaveButton => {
4783 self.template_modal.name_error = None;
4785 if self
4786 .template_modal
4787 .create_name_input
4788 .value
4789 .trim()
4790 .is_empty()
4791 {
4792 self.template_modal.name_error =
4793 Some("(required)".to_string());
4794 self.template_modal.create_focus = CreateFocus::Name;
4795 return None;
4796 }
4797
4798 if self.template_modal.editing_template_id.is_none()
4800 && self.template_manager.template_exists(
4801 self.template_modal.create_name_input.value.trim(),
4802 )
4803 {
4804 self.template_modal.name_error =
4805 Some("(name already exists)".to_string());
4806 self.template_modal.create_focus = CreateFocus::Name;
4807 return None;
4808 }
4809
4810 let match_criteria = template::MatchCriteria {
4812 exact_path: if !self
4813 .template_modal
4814 .create_exact_path_input
4815 .value
4816 .trim()
4817 .is_empty()
4818 {
4819 Some(std::path::PathBuf::from(
4820 self.template_modal
4821 .create_exact_path_input
4822 .value
4823 .trim(),
4824 ))
4825 } else {
4826 None
4827 },
4828 relative_path: if !self
4829 .template_modal
4830 .create_relative_path_input
4831 .value
4832 .trim()
4833 .is_empty()
4834 {
4835 Some(
4836 self.template_modal
4837 .create_relative_path_input
4838 .value
4839 .trim()
4840 .to_string(),
4841 )
4842 } else {
4843 None
4844 },
4845 path_pattern: if !self
4846 .template_modal
4847 .create_path_pattern_input
4848 .value
4849 .is_empty()
4850 {
4851 Some(
4852 self.template_modal
4853 .create_path_pattern_input
4854 .value
4855 .clone(),
4856 )
4857 } else {
4858 None
4859 },
4860 filename_pattern: if !self
4861 .template_modal
4862 .create_filename_pattern_input
4863 .value
4864 .is_empty()
4865 {
4866 Some(
4867 self.template_modal
4868 .create_filename_pattern_input
4869 .value
4870 .clone(),
4871 )
4872 } else {
4873 None
4874 },
4875 schema_columns: if self
4876 .template_modal
4877 .create_schema_match_enabled
4878 {
4879 self.data_table_state.as_ref().map(|state| {
4880 state
4881 .schema
4882 .iter_names()
4883 .map(|s| s.to_string())
4884 .collect()
4885 })
4886 } else {
4887 None
4888 },
4889 schema_types: None, };
4891
4892 let description = if !self
4893 .template_modal
4894 .create_description_input
4895 .value
4896 .is_empty()
4897 {
4898 Some(
4899 self.template_modal
4900 .create_description_input
4901 .value
4902 .clone(),
4903 )
4904 } else {
4905 None
4906 };
4907
4908 if let Some(ref editing_id) =
4909 self.template_modal.editing_template_id
4910 {
4911 if let Some(mut template) = self
4913 .template_manager
4914 .get_template_by_id(editing_id)
4915 .cloned()
4916 {
4917 template.name = self
4918 .template_modal
4919 .create_name_input
4920 .value
4921 .trim()
4922 .to_string();
4923 template.description = description;
4924 template.match_criteria = match_criteria;
4925 if let Some(state) = &self.data_table_state {
4927 let (query, sql_query, fuzzy_query) =
4928 active_query_settings(
4929 state.get_active_query(),
4930 state.get_active_sql_query(),
4931 state.get_active_fuzzy_query(),
4932 );
4933 template.settings = template::TemplateSettings {
4934 query,
4935 sql_query,
4936 fuzzy_query,
4937 filters: state.get_filters().to_vec(),
4938 sort_columns: state.get_sort_columns().to_vec(),
4939 sort_ascending: state.get_sort_ascending(),
4940 column_order: state.get_column_order().to_vec(),
4941 locked_columns_count: state
4942 .locked_columns_count(),
4943 pivot: state.last_pivot_spec().cloned(),
4944 melt: state.last_melt_spec().cloned(),
4945 };
4946 }
4947
4948 match self.template_manager.update_template(&template) {
4949 Ok(_) => {
4950 if let Some(ref state) = self.data_table_state {
4952 if let Some(ref path) = self.path {
4953 self.template_modal.templates = self
4954 .template_manager
4955 .find_relevant_templates(
4956 path,
4957 &state.schema,
4958 );
4959 self.template_modal.table_state.select(
4960 if self
4961 .template_modal
4962 .templates
4963 .is_empty()
4964 {
4965 None
4966 } else {
4967 Some(0)
4968 },
4969 );
4970 }
4971 }
4972 self.template_modal.exit_create_mode();
4973 }
4974 Err(_) => {
4975 }
4977 }
4978 }
4979 } else {
4980 match self.create_template_from_current_state(
4982 self.template_modal
4983 .create_name_input
4984 .value
4985 .trim()
4986 .to_string(),
4987 description,
4988 match_criteria,
4989 ) {
4990 Ok(_) => {
4991 if let Some(ref state) = self.data_table_state {
4993 if let Some(ref path) = self.path {
4994 self.template_modal.templates = self
4995 .template_manager
4996 .find_relevant_templates(
4997 path,
4998 &state.schema,
4999 );
5000 self.template_modal.table_state.select(
5001 if self
5002 .template_modal
5003 .templates
5004 .is_empty()
5005 {
5006 None
5007 } else {
5008 Some(0)
5009 },
5010 );
5011 }
5012 }
5013 self.template_modal.exit_create_mode();
5014 }
5015 Err(_) => {
5016 }
5018 }
5019 }
5020 }
5021 CreateFocus::CancelButton => {
5022 self.template_modal.exit_create_mode();
5023 }
5024 _ => {
5025 self.template_modal.next_focus();
5027 }
5028 }
5029 }
5030 }
5031 }
5032 KeyCode::Up => {
5033 match self.template_modal.mode {
5034 TemplateModalMode::List => {
5035 if self.template_modal.focus == TemplateFocus::TemplateList {
5036 let i = match self.template_modal.table_state.selected() {
5037 Some(i) => {
5038 if i == 0 {
5039 self.template_modal.templates.len().saturating_sub(1)
5040 } else {
5041 i - 1
5042 }
5043 }
5044 None => 0,
5045 };
5046 self.template_modal.table_state.select(Some(i));
5047 }
5048 }
5049 TemplateModalMode::Create | TemplateModalMode::Edit => {
5050 if self.template_modal.create_focus == CreateFocus::Description {
5052 let event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
5053 self.template_modal
5054 .create_description_input
5055 .handle_key(&event, None);
5056 let area_height = 10; self.template_modal
5059 .create_description_input
5060 .ensure_cursor_visible(area_height, 80);
5061 } else {
5062 self.template_modal.prev_focus();
5064 }
5065 }
5066 }
5067 }
5068 KeyCode::Down => {
5069 match self.template_modal.mode {
5070 TemplateModalMode::List => {
5071 if self.template_modal.focus == TemplateFocus::TemplateList {
5072 let i = match self.template_modal.table_state.selected() {
5073 Some(i) => {
5074 if i >= self
5075 .template_modal
5076 .templates
5077 .len()
5078 .saturating_sub(1)
5079 {
5080 0
5081 } else {
5082 i + 1
5083 }
5084 }
5085 None => 0,
5086 };
5087 self.template_modal.table_state.select(Some(i));
5088 }
5089 }
5090 TemplateModalMode::Create | TemplateModalMode::Edit => {
5091 if self.template_modal.create_focus == CreateFocus::Description {
5093 let event = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
5094 self.template_modal
5095 .create_description_input
5096 .handle_key(&event, None);
5097 let area_height = 10; self.template_modal
5100 .create_description_input
5101 .ensure_cursor_visible(area_height, 80);
5102 } else {
5103 self.template_modal.next_focus();
5105 }
5106 }
5107 }
5108 }
5109 KeyCode::Char('j')
5110 if self.template_modal.mode == TemplateModalMode::List
5111 && self.template_modal.focus == TemplateFocus::TemplateList
5112 && !self.template_modal.delete_confirm =>
5113 {
5114 let i = match self.template_modal.table_state.selected() {
5115 Some(i) => {
5116 if i >= self.template_modal.templates.len().saturating_sub(1) {
5117 0
5118 } else {
5119 i + 1
5120 }
5121 }
5122 None => 0,
5123 };
5124 self.template_modal.table_state.select(Some(i));
5125 }
5126 KeyCode::Char('k')
5127 if self.template_modal.mode == TemplateModalMode::List
5128 && self.template_modal.focus == TemplateFocus::TemplateList
5129 && !self.template_modal.delete_confirm =>
5130 {
5131 let i = match self.template_modal.table_state.selected() {
5132 Some(i) => {
5133 if i == 0 {
5134 self.template_modal.templates.len().saturating_sub(1)
5135 } else {
5136 i - 1
5137 }
5138 }
5139 None => 0,
5140 };
5141 self.template_modal.table_state.select(Some(i));
5142 }
5143 KeyCode::Char(c)
5144 if self.template_modal.mode == TemplateModalMode::Create
5145 || self.template_modal.mode == TemplateModalMode::Edit =>
5146 {
5147 match self.template_modal.create_focus {
5148 CreateFocus::Name => {
5149 self.template_modal.name_error = None;
5151 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5152 self.template_modal
5153 .create_name_input
5154 .handle_key(&event, None);
5155 }
5156 CreateFocus::Description => {
5157 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5158 self.template_modal
5159 .create_description_input
5160 .handle_key(&event, None);
5161 let area_height = 10; self.template_modal
5164 .create_description_input
5165 .ensure_cursor_visible(area_height, 80);
5166 }
5167 CreateFocus::ExactPath => {
5168 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5169 self.template_modal
5170 .create_exact_path_input
5171 .handle_key(&event, None);
5172 }
5173 CreateFocus::RelativePath => {
5174 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5175 self.template_modal
5176 .create_relative_path_input
5177 .handle_key(&event, None);
5178 }
5179 CreateFocus::PathPattern => {
5180 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5181 self.template_modal
5182 .create_path_pattern_input
5183 .handle_key(&event, None);
5184 }
5185 CreateFocus::FilenamePattern => {
5186 let event = KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty());
5187 self.template_modal
5188 .create_filename_pattern_input
5189 .handle_key(&event, None);
5190 }
5191 CreateFocus::SchemaMatch => {
5192 if c == ' ' {
5194 self.template_modal.create_schema_match_enabled =
5195 !self.template_modal.create_schema_match_enabled;
5196 }
5197 }
5198 _ => {}
5199 }
5200 }
5201 KeyCode::Left | KeyCode::Right | KeyCode::Home | KeyCode::End
5202 if self.template_modal.mode == TemplateModalMode::Create
5203 || self.template_modal.mode == TemplateModalMode::Edit =>
5204 {
5205 match self.template_modal.create_focus {
5206 CreateFocus::Name => {
5207 self.template_modal
5208 .create_name_input
5209 .handle_key(event, None);
5210 }
5211 CreateFocus::Description => {
5212 self.template_modal
5213 .create_description_input
5214 .handle_key(event, None);
5215 let area_height = 10;
5217 self.template_modal
5218 .create_description_input
5219 .ensure_cursor_visible(area_height, 80);
5220 }
5221 CreateFocus::ExactPath => {
5222 self.template_modal
5223 .create_exact_path_input
5224 .handle_key(event, None);
5225 }
5226 CreateFocus::RelativePath => {
5227 self.template_modal
5228 .create_relative_path_input
5229 .handle_key(event, None);
5230 }
5231 CreateFocus::PathPattern => {
5232 self.template_modal
5233 .create_path_pattern_input
5234 .handle_key(event, None);
5235 }
5236 CreateFocus::FilenamePattern => {
5237 self.template_modal
5238 .create_filename_pattern_input
5239 .handle_key(event, None);
5240 }
5241 _ => {}
5242 }
5243 }
5244 KeyCode::PageUp | KeyCode::PageDown
5245 if self.template_modal.mode == TemplateModalMode::Create
5246 || self.template_modal.mode == TemplateModalMode::Edit =>
5247 {
5248 if self.template_modal.create_focus == CreateFocus::Description {
5251 let lines: Vec<&str> = self
5252 .template_modal
5253 .create_description_input
5254 .value
5255 .lines()
5256 .collect();
5257 let current_line = self.template_modal.create_description_input.cursor_line;
5258 let current_col = self.template_modal.create_description_input.cursor_col;
5259
5260 let target_line = if event.code == KeyCode::PageUp {
5261 current_line.saturating_sub(5)
5262 } else {
5263 (current_line + 5).min(lines.len().saturating_sub(1))
5264 };
5265
5266 if target_line < lines.len() {
5267 let target_line_str = lines.get(target_line).unwrap_or(&"");
5268 let new_col = current_col.min(target_line_str.chars().count());
5269 self.template_modal.create_description_input.cursor = self
5270 .template_modal
5271 .create_description_input
5272 .line_col_to_cursor(target_line, new_col);
5273 self.template_modal
5274 .create_description_input
5275 .update_line_col_from_cursor();
5276 let area_height = 10;
5278 self.template_modal
5279 .create_description_input
5280 .ensure_cursor_visible(area_height, 80);
5281 }
5282 }
5283 }
5284 KeyCode::Backspace
5285 | KeyCode::Delete
5286 | KeyCode::Left
5287 | KeyCode::Right
5288 | KeyCode::Home
5289 | KeyCode::End
5290 if self.template_modal.mode == TemplateModalMode::Create
5291 || self.template_modal.mode == TemplateModalMode::Edit =>
5292 {
5293 match self.template_modal.create_focus {
5294 CreateFocus::Name => {
5295 self.template_modal
5296 .create_name_input
5297 .handle_key(event, None);
5298 }
5299 CreateFocus::Description => {
5300 self.template_modal
5301 .create_description_input
5302 .handle_key(event, None);
5303 let area_height = 10;
5305 self.template_modal
5306 .create_description_input
5307 .ensure_cursor_visible(area_height, 80);
5308 }
5309 CreateFocus::ExactPath => {
5310 self.template_modal
5311 .create_exact_path_input
5312 .handle_key(event, None);
5313 }
5314 CreateFocus::RelativePath => {
5315 self.template_modal
5316 .create_relative_path_input
5317 .handle_key(event, None);
5318 }
5319 CreateFocus::PathPattern => {
5320 self.template_modal
5321 .create_path_pattern_input
5322 .handle_key(event, None);
5323 }
5324 CreateFocus::FilenamePattern => {
5325 self.template_modal
5326 .create_filename_pattern_input
5327 .handle_key(event, None);
5328 }
5329 _ => {}
5330 }
5331 }
5332 _ => {}
5333 }
5334 return None;
5335 }
5336
5337 if self.input_mode == InputMode::Editing {
5338 if self.input_type == Some(InputType::Search) {
5339 const RIGHT_KEYS: [KeyCode; 2] = [KeyCode::Right, KeyCode::Char('l')];
5340 const LEFT_KEYS: [KeyCode; 2] = [KeyCode::Left, KeyCode::Char('h')];
5341
5342 if self.query_focus == QueryFocus::TabBar && event.is_press() {
5343 if event.code == KeyCode::BackTab
5344 || (event.code == KeyCode::Tab
5345 && !event.modifiers.contains(KeyModifiers::SHIFT))
5346 {
5347 self.query_focus = QueryFocus::Input;
5348 if let Some(state) = &self.data_table_state {
5349 if self.query_tab == QueryTab::SqlLike {
5350 self.query_input.value = state.get_active_query().to_string();
5351 self.query_input.cursor = self.query_input.value.chars().count();
5352 self.sql_input.set_focused(false);
5353 self.fuzzy_input.set_focused(false);
5354 self.query_input.set_focused(true);
5355 } else if self.query_tab == QueryTab::Fuzzy {
5356 self.fuzzy_input.value = state.get_active_fuzzy_query().to_string();
5357 self.fuzzy_input.cursor = self.fuzzy_input.value.chars().count();
5358 self.query_input.set_focused(false);
5359 self.sql_input.set_focused(false);
5360 self.fuzzy_input.set_focused(true);
5361 } else if self.query_tab == QueryTab::Sql {
5362 self.sql_input.value = state.get_active_sql_query().to_string();
5363 self.sql_input.cursor = self.sql_input.value.chars().count();
5364 self.query_input.set_focused(false);
5365 self.fuzzy_input.set_focused(false);
5366 self.sql_input.set_focused(true);
5367 }
5368 }
5369 return None;
5370 }
5371 if RIGHT_KEYS.contains(&event.code) {
5372 self.query_tab = self.query_tab.next();
5373 if let Some(state) = &self.data_table_state {
5374 if self.query_tab == QueryTab::SqlLike {
5375 self.query_input.value = state.get_active_query().to_string();
5376 self.query_input.cursor = self.query_input.value.chars().count();
5377 } else if self.query_tab == QueryTab::Fuzzy {
5378 self.fuzzy_input.value = state.get_active_fuzzy_query().to_string();
5379 self.fuzzy_input.cursor = self.fuzzy_input.value.chars().count();
5380 } else if self.query_tab == QueryTab::Sql {
5381 self.sql_input.value = state.get_active_sql_query().to_string();
5382 self.sql_input.cursor = self.sql_input.value.chars().count();
5383 }
5384 }
5385 self.query_input.set_focused(false);
5386 self.sql_input.set_focused(false);
5387 self.fuzzy_input.set_focused(false);
5388 return None;
5389 }
5390 if LEFT_KEYS.contains(&event.code) {
5391 self.query_tab = self.query_tab.prev();
5392 if let Some(state) = &self.data_table_state {
5393 if self.query_tab == QueryTab::SqlLike {
5394 self.query_input.value = state.get_active_query().to_string();
5395 self.query_input.cursor = self.query_input.value.chars().count();
5396 } else if self.query_tab == QueryTab::Fuzzy {
5397 self.fuzzy_input.value = state.get_active_fuzzy_query().to_string();
5398 self.fuzzy_input.cursor = self.fuzzy_input.value.chars().count();
5399 } else if self.query_tab == QueryTab::Sql {
5400 self.sql_input.value = state.get_active_sql_query().to_string();
5401 self.sql_input.cursor = self.sql_input.value.chars().count();
5402 }
5403 }
5404 self.query_input.set_focused(false);
5405 self.sql_input.set_focused(false);
5406 self.fuzzy_input.set_focused(false);
5407 return None;
5408 }
5409 if event.code == KeyCode::Esc {
5410 self.query_input.clear();
5411 self.sql_input.clear();
5412 self.fuzzy_input.clear();
5413 self.query_input.set_focused(false);
5414 self.sql_input.set_focused(false);
5415 self.fuzzy_input.set_focused(false);
5416 self.input_mode = InputMode::Normal;
5417 self.input_type = None;
5418 if let Some(state) = &mut self.data_table_state {
5419 state.error = None;
5420 state.suppress_error_display = false;
5421 }
5422 return None;
5423 }
5424 return None;
5425 }
5426
5427 if event.is_press()
5428 && event.code == KeyCode::Tab
5429 && !event.modifiers.contains(KeyModifiers::SHIFT)
5430 {
5431 self.query_focus = QueryFocus::TabBar;
5432 self.query_input.set_focused(false);
5433 self.sql_input.set_focused(false);
5434 self.fuzzy_input.set_focused(false);
5435 return None;
5436 }
5437
5438 if self.query_focus != QueryFocus::Input {
5439 return None;
5440 }
5441
5442 if self.query_tab == QueryTab::Sql {
5443 self.query_input.set_focused(false);
5444 self.fuzzy_input.set_focused(false);
5445 self.sql_input.set_focused(true);
5446 let result = self.sql_input.handle_key(event, Some(&self.cache));
5447 match result {
5448 TextInputEvent::Submit => {
5449 let _ = self.sql_input.save_to_history(&self.cache);
5450 let sql = self.sql_input.value.clone();
5451 self.sql_input.set_focused(false);
5452 return Some(AppEvent::SqlSearch(sql));
5453 }
5454 TextInputEvent::Cancel => {
5455 self.sql_input.clear();
5456 self.sql_input.set_focused(false);
5457 self.input_mode = InputMode::Normal;
5458 self.input_type = None;
5459 if let Some(state) = &mut self.data_table_state {
5460 state.error = None;
5461 state.suppress_error_display = false;
5462 }
5463 }
5464 TextInputEvent::HistoryChanged | TextInputEvent::None => {}
5465 }
5466 return None;
5467 }
5468
5469 if self.query_tab == QueryTab::Fuzzy {
5470 self.query_input.set_focused(false);
5471 self.sql_input.set_focused(false);
5472 self.fuzzy_input.set_focused(true);
5473 let result = self.fuzzy_input.handle_key(event, Some(&self.cache));
5474 match result {
5475 TextInputEvent::Submit => {
5476 let _ = self.fuzzy_input.save_to_history(&self.cache);
5477 let query = self.fuzzy_input.value.clone();
5478 self.fuzzy_input.set_focused(false);
5479 return Some(AppEvent::FuzzySearch(query));
5480 }
5481 TextInputEvent::Cancel => {
5482 self.fuzzy_input.clear();
5483 self.fuzzy_input.set_focused(false);
5484 self.input_mode = InputMode::Normal;
5485 self.input_type = None;
5486 if let Some(state) = &mut self.data_table_state {
5487 state.error = None;
5488 state.suppress_error_display = false;
5489 }
5490 }
5491 TextInputEvent::HistoryChanged | TextInputEvent::None => {}
5492 }
5493 return None;
5494 }
5495
5496 if self.query_tab != QueryTab::SqlLike {
5497 return None;
5498 }
5499
5500 self.sql_input.set_focused(false);
5501 self.fuzzy_input.set_focused(false);
5502 self.query_input.set_focused(true);
5503 let result = self.query_input.handle_key(event, Some(&self.cache));
5504
5505 match result {
5506 TextInputEvent::Submit => {
5507 let _ = self.query_input.save_to_history(&self.cache);
5509 let query = self.query_input.value.clone();
5510 self.query_input.set_focused(false);
5511 return Some(AppEvent::Search(query));
5512 }
5513 TextInputEvent::Cancel => {
5514 self.query_input.clear();
5516 self.query_input.set_focused(false);
5517 self.input_mode = InputMode::Normal;
5518 if let Some(state) = &mut self.data_table_state {
5519 state.error = None;
5521 state.suppress_error_display = false;
5522 }
5523 }
5524 TextInputEvent::HistoryChanged => {
5525 }
5527 TextInputEvent::None => {
5528 }
5530 }
5531 return None;
5532 }
5533
5534 if self.input_type == Some(InputType::GoToLine) {
5536 self.query_input.set_focused(true);
5537 let result = self.query_input.handle_key(event, None);
5538 match result {
5539 TextInputEvent::Submit => {
5540 let value = self.query_input.value.trim().to_string();
5541 self.query_input.clear();
5542 self.query_input.set_focused(false);
5543 self.input_mode = InputMode::Normal;
5544 self.input_type = None;
5545 if let Some(state) = &mut self.data_table_state {
5546 if let Ok(display_line) = value.parse::<usize>() {
5547 let row_index =
5548 display_line.saturating_sub(state.row_start_index());
5549 let would_collect = state.scroll_would_trigger_collect(
5550 row_index as i64 - state.start_row as i64,
5551 );
5552 if would_collect {
5553 self.busy = true;
5554 return Some(AppEvent::GoToLine(row_index));
5555 }
5556 state.scroll_to_row_centered(row_index);
5557 }
5558 }
5559 }
5560 TextInputEvent::Cancel => {
5561 self.query_input.clear();
5562 self.query_input.set_focused(false);
5563 self.input_mode = InputMode::Normal;
5564 self.input_type = None;
5565 }
5566 TextInputEvent::HistoryChanged | TextInputEvent::None => {}
5567 }
5568 return None;
5569 }
5570
5571 return None;
5574 }
5575
5576 const RIGHT_KEYS: [KeyCode; 2] = [KeyCode::Right, KeyCode::Char('l')];
5577
5578 const LEFT_KEYS: [KeyCode; 2] = [KeyCode::Left, KeyCode::Char('h')];
5579
5580 const DOWN_KEYS: [KeyCode; 2] = [KeyCode::Down, KeyCode::Char('j')];
5581
5582 const UP_KEYS: [KeyCode; 2] = [KeyCode::Up, KeyCode::Char('k')];
5583
5584 match event.code {
5585 KeyCode::Char('q') | KeyCode::Char('Q') => Some(AppEvent::Exit),
5586 KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
5587 Some(AppEvent::Exit)
5588 }
5589 KeyCode::Char('R') => Some(AppEvent::Reset),
5590 KeyCode::Char('N') => {
5591 if let Some(ref mut state) = self.data_table_state {
5592 state.toggle_row_numbers();
5593 }
5594 None
5595 }
5596 KeyCode::Esc => {
5597 if let Some(ref mut state) = self.data_table_state {
5599 if state.is_drilled_down() {
5600 let _ = state.drill_up();
5601 return None;
5602 }
5603 }
5604 None
5607 }
5608 code if RIGHT_KEYS.contains(&code) => {
5609 if let Some(ref mut state) = self.data_table_state {
5610 state.scroll_right();
5611 if self.debug.enabled {
5612 self.debug.last_action = "scroll_right".to_string();
5613 }
5614 }
5615 None
5616 }
5617 code if LEFT_KEYS.contains(&code) => {
5618 if let Some(ref mut state) = self.data_table_state {
5619 state.scroll_left();
5620 if self.debug.enabled {
5621 self.debug.last_action = "scroll_left".to_string();
5622 }
5623 }
5624 None
5625 }
5626 code if event.is_press() && DOWN_KEYS.contains(&code) => {
5627 let would_collect = self
5628 .data_table_state
5629 .as_ref()
5630 .map(|s| s.scroll_would_trigger_collect(1))
5631 .unwrap_or(false);
5632 if would_collect {
5633 self.busy = true;
5634 Some(AppEvent::DoScrollNext)
5635 } else {
5636 if let Some(ref mut s) = self.data_table_state {
5637 s.select_next();
5638 }
5639 None
5640 }
5641 }
5642 code if event.is_press() && UP_KEYS.contains(&code) => {
5643 let would_collect = self
5644 .data_table_state
5645 .as_ref()
5646 .map(|s| s.scroll_would_trigger_collect(-1))
5647 .unwrap_or(false);
5648 if would_collect {
5649 self.busy = true;
5650 Some(AppEvent::DoScrollPrev)
5651 } else {
5652 if let Some(ref mut s) = self.data_table_state {
5653 s.select_previous();
5654 }
5655 None
5656 }
5657 }
5658 KeyCode::PageDown if event.is_press() => {
5659 let would_collect = self
5660 .data_table_state
5661 .as_ref()
5662 .map(|s| s.scroll_would_trigger_collect(s.visible_rows as i64))
5663 .unwrap_or(false);
5664 if would_collect {
5665 self.busy = true;
5666 Some(AppEvent::DoScrollDown)
5667 } else {
5668 if let Some(ref mut s) = self.data_table_state {
5669 s.page_down();
5670 }
5671 None
5672 }
5673 }
5674 KeyCode::Home if event.is_press() => {
5675 if let Some(ref mut state) = self.data_table_state {
5676 if state.start_row > 0 {
5677 state.scroll_to(0);
5678 }
5679 state.table_state.select(Some(0));
5680 }
5681 None
5682 }
5683 KeyCode::End if event.is_press() => {
5684 if self.data_table_state.is_some() {
5685 self.busy = true;
5686 Some(AppEvent::DoScrollEnd)
5687 } else {
5688 None
5689 }
5690 }
5691 KeyCode::Char('G') if event.is_press() => {
5692 if self.data_table_state.is_some() {
5693 self.busy = true;
5694 Some(AppEvent::DoScrollEnd)
5695 } else {
5696 None
5697 }
5698 }
5699 KeyCode::Char('f')
5700 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
5701 {
5702 let would_collect = self
5703 .data_table_state
5704 .as_ref()
5705 .map(|s| s.scroll_would_trigger_collect(s.visible_rows as i64))
5706 .unwrap_or(false);
5707 if would_collect {
5708 self.busy = true;
5709 Some(AppEvent::DoScrollDown)
5710 } else {
5711 if let Some(ref mut s) = self.data_table_state {
5712 s.page_down();
5713 }
5714 None
5715 }
5716 }
5717 KeyCode::Char('b')
5718 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
5719 {
5720 let would_collect = self
5721 .data_table_state
5722 .as_ref()
5723 .map(|s| s.scroll_would_trigger_collect(-(s.visible_rows as i64)))
5724 .unwrap_or(false);
5725 if would_collect {
5726 self.busy = true;
5727 Some(AppEvent::DoScrollUp)
5728 } else {
5729 if let Some(ref mut s) = self.data_table_state {
5730 s.page_up();
5731 }
5732 None
5733 }
5734 }
5735 KeyCode::Char('d')
5736 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
5737 {
5738 let half = self
5739 .data_table_state
5740 .as_ref()
5741 .map(|s| (s.visible_rows / 2).max(1) as i64)
5742 .unwrap_or(1);
5743 let would_collect = self
5744 .data_table_state
5745 .as_ref()
5746 .map(|s| s.scroll_would_trigger_collect(half))
5747 .unwrap_or(false);
5748 if would_collect {
5749 self.busy = true;
5750 Some(AppEvent::DoScrollHalfDown)
5751 } else {
5752 if let Some(ref mut s) = self.data_table_state {
5753 s.half_page_down();
5754 }
5755 None
5756 }
5757 }
5758 KeyCode::Char('u')
5759 if event.modifiers.contains(KeyModifiers::CONTROL) && event.is_press() =>
5760 {
5761 let half = self
5762 .data_table_state
5763 .as_ref()
5764 .map(|s| (s.visible_rows / 2).max(1) as i64)
5765 .unwrap_or(1);
5766 let would_collect = self
5767 .data_table_state
5768 .as_ref()
5769 .map(|s| s.scroll_would_trigger_collect(-half))
5770 .unwrap_or(false);
5771 if would_collect {
5772 self.busy = true;
5773 Some(AppEvent::DoScrollHalfUp)
5774 } else {
5775 if let Some(ref mut s) = self.data_table_state {
5776 s.half_page_up();
5777 }
5778 None
5779 }
5780 }
5781 KeyCode::PageUp if event.is_press() => {
5782 let would_collect = self
5783 .data_table_state
5784 .as_ref()
5785 .map(|s| s.scroll_would_trigger_collect(-(s.visible_rows as i64)))
5786 .unwrap_or(false);
5787 if would_collect {
5788 self.busy = true;
5789 Some(AppEvent::DoScrollUp)
5790 } else {
5791 if let Some(ref mut s) = self.data_table_state {
5792 s.page_up();
5793 }
5794 None
5795 }
5796 }
5797 KeyCode::Enter if event.is_press() => {
5798 if self.input_mode == InputMode::Normal {
5800 if let Some(ref mut state) = self.data_table_state {
5801 if state.is_grouped() && !state.is_drilled_down() {
5802 if let Some(selected) = state.table_state.selected() {
5803 let group_index = state.start_row + selected;
5804 let _ = state.drill_down_into_group(group_index);
5805 }
5806 }
5807 }
5808 }
5809 None
5810 }
5811 KeyCode::Tab if event.is_press() => {
5812 self.focus = (self.focus + 1) % 2;
5813 None
5814 }
5815 KeyCode::BackTab if event.is_press() => {
5816 self.focus = (self.focus + 1) % 2;
5817 None
5818 }
5819 KeyCode::Char('i') if event.is_press() => {
5820 if self.data_table_state.is_some() {
5821 self.info_modal.open();
5822 self.input_mode = InputMode::Info;
5823 if self.path.is_some()
5825 && self.original_file_format == Some(ExportFormat::Parquet)
5826 && self.parquet_metadata_cache.is_none()
5827 {
5828 self.busy = true;
5829 return Some(AppEvent::DoLoadParquetMetadata);
5830 }
5831 }
5832 None
5833 }
5834 KeyCode::Char('/') => {
5835 self.input_mode = InputMode::Editing;
5836 self.input_type = Some(InputType::Search);
5837 self.query_tab = QueryTab::SqlLike;
5838 self.query_focus = QueryFocus::Input;
5839 if let Some(state) = &mut self.data_table_state {
5840 self.query_input.value = state.active_query.clone();
5841 self.query_input.cursor = self.query_input.value.chars().count();
5842 self.sql_input.value = state.get_active_sql_query().to_string();
5843 self.fuzzy_input.value = state.get_active_fuzzy_query().to_string();
5844 self.fuzzy_input.cursor = self.fuzzy_input.value.chars().count();
5845 self.sql_input.cursor = self.sql_input.value.chars().count();
5846 state.suppress_error_display = true;
5847 } else {
5848 self.query_input.clear();
5849 self.sql_input.clear();
5850 self.fuzzy_input.clear();
5851 }
5852 self.sql_input.set_focused(false);
5853 self.fuzzy_input.set_focused(false);
5854 self.query_input.set_focused(true);
5855 None
5856 }
5857 KeyCode::Char(':') if event.is_press() => {
5858 if self.data_table_state.is_some() {
5859 self.input_mode = InputMode::Editing;
5860 self.input_type = Some(InputType::GoToLine);
5861 self.query_input.value.clear();
5862 self.query_input.cursor = 0;
5863 self.query_input.set_focused(true);
5864 }
5865 None
5866 }
5867 KeyCode::Char('T') => {
5868 if let Some(ref state) = self.data_table_state {
5870 if let Some(ref path) = self.path {
5871 if let Some(template) =
5872 self.template_manager.get_most_relevant(path, &state.schema)
5873 {
5874 if let Err(e) = self.apply_template(&template) {
5876 self.error_modal
5878 .show(format!("Error applying template: {}", e));
5879 }
5880 }
5881 }
5882 }
5883 None
5884 }
5885 KeyCode::Char('t') => {
5886 if let Some(ref state) = self.data_table_state {
5888 if let Some(ref path) = self.path {
5889 self.template_modal.templates = self
5891 .template_manager
5892 .find_relevant_templates(path, &state.schema);
5893 self.template_modal.table_state.select(
5894 if self.template_modal.templates.is_empty() {
5895 None
5896 } else {
5897 Some(0)
5898 },
5899 );
5900 self.template_modal.active = true;
5901 self.template_modal.mode = TemplateModalMode::List;
5902 self.template_modal.focus = TemplateFocus::TemplateList;
5903 }
5904 }
5905 None
5906 }
5907 KeyCode::Char('s') => {
5908 if let Some(state) = &self.data_table_state {
5909 let headers: Vec<String> =
5910 state.schema.iter_names().map(|s| s.to_string()).collect();
5911 let locked_count = state.locked_columns_count();
5912
5913 let mut existing_columns: std::collections::HashMap<String, SortColumn> = self
5915 .sort_filter_modal
5916 .sort
5917 .columns
5918 .iter()
5919 .map(|c| (c.name.clone(), c.clone()))
5920 .collect();
5921 self.sort_filter_modal.sort.columns = headers
5922 .iter()
5923 .enumerate()
5924 .map(|(i, h)| {
5925 if let Some(mut col) = existing_columns.remove(h) {
5926 col.display_order = i;
5927 col.is_locked = i < locked_count;
5928 col.is_to_be_locked = false;
5929 col
5930 } else {
5931 SortColumn {
5932 name: h.clone(),
5933 sort_order: None,
5934 display_order: i,
5935 is_locked: i < locked_count,
5936 is_to_be_locked: false,
5937 is_visible: true,
5938 }
5939 }
5940 })
5941 .collect();
5942 self.sort_filter_modal.sort.filter_input.clear();
5943 self.sort_filter_modal.sort.focus = SortFocus::ColumnList;
5944
5945 self.sort_filter_modal.filter.available_columns = state.headers();
5947 if !self.sort_filter_modal.filter.available_columns.is_empty() {
5948 self.sort_filter_modal.filter.new_column_idx =
5949 self.sort_filter_modal.filter.new_column_idx.min(
5950 self.sort_filter_modal
5951 .filter
5952 .available_columns
5953 .len()
5954 .saturating_sub(1),
5955 );
5956 } else {
5957 self.sort_filter_modal.filter.new_column_idx = 0;
5958 }
5959
5960 self.sort_filter_modal.open(self.history_limit, &self.theme);
5961 self.input_mode = InputMode::SortFilter;
5962 }
5963 None
5964 }
5965 KeyCode::Char('r') => {
5966 if let Some(state) = &mut self.data_table_state {
5967 state.reverse();
5968 }
5969 None
5970 }
5971 KeyCode::Char('a') => {
5972 if self.data_table_state.is_some() && self.input_mode == InputMode::Normal {
5974 self.analysis_modal.open();
5975 }
5976 None
5977 }
5978 KeyCode::Char('c') => {
5979 if let Some(state) = &self.data_table_state {
5980 if self.input_mode == InputMode::Normal {
5981 let numeric_columns: Vec<String> = state
5982 .schema
5983 .iter()
5984 .filter(|(_, dtype)| dtype.is_numeric())
5985 .map(|(name, _)| name.to_string())
5986 .collect();
5987 let datetime_columns: Vec<String> = state
5988 .schema
5989 .iter()
5990 .filter(|(_, dtype)| {
5991 matches!(
5992 dtype,
5993 DataType::Datetime(_, _) | DataType::Date | DataType::Time
5994 )
5995 })
5996 .map(|(name, _)| name.to_string())
5997 .collect();
5998 self.chart_modal.open(
5999 &numeric_columns,
6000 &datetime_columns,
6001 self.app_config.chart.row_limit,
6002 );
6003 self.chart_modal.x_input =
6004 std::mem::take(&mut self.chart_modal.x_input).with_theme(&self.theme);
6005 self.chart_modal.y_input =
6006 std::mem::take(&mut self.chart_modal.y_input).with_theme(&self.theme);
6007 self.chart_modal.hist_input =
6008 std::mem::take(&mut self.chart_modal.hist_input)
6009 .with_theme(&self.theme);
6010 self.chart_modal.box_input =
6011 std::mem::take(&mut self.chart_modal.box_input).with_theme(&self.theme);
6012 self.chart_modal.kde_input =
6013 std::mem::take(&mut self.chart_modal.kde_input).with_theme(&self.theme);
6014 self.chart_modal.heatmap_x_input =
6015 std::mem::take(&mut self.chart_modal.heatmap_x_input)
6016 .with_theme(&self.theme);
6017 self.chart_modal.heatmap_y_input =
6018 std::mem::take(&mut self.chart_modal.heatmap_y_input)
6019 .with_theme(&self.theme);
6020 self.chart_cache.clear();
6021 self.input_mode = InputMode::Chart;
6022 }
6023 }
6024 None
6025 }
6026 KeyCode::Char('p') => {
6027 if let Some(state) = &self.data_table_state {
6028 if self.input_mode == InputMode::Normal {
6029 self.pivot_melt_modal.available_columns =
6030 state.schema.iter_names().map(|s| s.to_string()).collect();
6031 self.pivot_melt_modal.column_dtypes = state
6032 .schema
6033 .iter()
6034 .map(|(n, d)| (n.to_string(), d.clone()))
6035 .collect();
6036 self.pivot_melt_modal.open(self.history_limit, &self.theme);
6037 self.input_mode = InputMode::PivotMelt;
6038 }
6039 }
6040 None
6041 }
6042 KeyCode::Char('e') => {
6043 if self.data_table_state.is_some() && self.input_mode == InputMode::Normal {
6044 let config_delimiter = AppConfig::load(APP_NAME)
6046 .ok()
6047 .and_then(|config| config.file_loading.delimiter);
6048 self.export_modal.open(
6049 self.original_file_format,
6050 self.history_limit,
6051 &self.theme,
6052 self.original_file_delimiter,
6053 config_delimiter,
6054 );
6055 self.input_mode = InputMode::Export;
6056 }
6057 None
6058 }
6059 _ => None,
6060 }
6061 }
6062
6063 pub fn event(&mut self, event: &AppEvent) -> Option<AppEvent> {
6064 self.debug.num_events += 1;
6065
6066 match event {
6067 AppEvent::Key(key) => {
6068 let is_column_scroll = matches!(
6069 key.code,
6070 KeyCode::Left | KeyCode::Right | KeyCode::Char('h') | KeyCode::Char('l')
6071 );
6072 let is_help_key = key.code == KeyCode::F(1);
6073 if self.busy && !is_column_scroll && !is_help_key && !self.confirmation_modal.active
6075 {
6076 return None;
6077 }
6078 self.key(key)
6079 }
6080 AppEvent::Open(paths, options) => {
6081 if paths.is_empty() {
6082 return Some(AppEvent::Crash("No paths provided".to_string()));
6083 }
6084 #[cfg(feature = "http")]
6085 if let Some(ref p) = self.http_temp_path.take() {
6086 let _ = std::fs::remove_file(p);
6087 }
6088 self.busy = true;
6089 let first = &paths[0];
6090 let file_size = match source::input_source(first) {
6091 source::InputSource::Local(_) => {
6092 std::fs::metadata(first).map(|m| m.len()).unwrap_or(0)
6093 }
6094 source::InputSource::S3(_)
6095 | source::InputSource::Gcs(_)
6096 | source::InputSource::Http(_) => 0,
6097 };
6098 let path_str = first.as_os_str().to_string_lossy();
6099 let _is_partitioned_path = paths.len() == 1
6100 && options.hive
6101 && (first.is_dir() || path_str.contains('*') || path_str.contains("**"));
6102 let phase = "Scanning input";
6103
6104 self.loading_state = LoadingState::Loading {
6105 file_path: Some(first.clone()),
6106 file_size,
6107 current_phase: phase.to_string(),
6108 progress_percent: 10,
6109 };
6110
6111 Some(AppEvent::DoLoadScanPaths(paths.clone(), options.clone()))
6112 }
6113 AppEvent::OpenLazyFrame(lf, options) => {
6114 self.busy = true;
6115 self.loading_state = LoadingState::Loading {
6116 file_path: None,
6117 file_size: 0,
6118 current_phase: "Scanning input".to_string(),
6119 progress_percent: 10,
6120 };
6121 Some(AppEvent::DoLoadSchema(lf.clone(), None, options.clone()))
6122 }
6123 AppEvent::DoLoadScanPaths(paths, options) => {
6124 let first = &paths[0];
6125 let src = source::input_source(first);
6126 if paths.len() > 1 {
6127 match &src {
6128 source::InputSource::S3(_) => {
6129 return Some(AppEvent::Crash(
6130 "Only one S3 URL at a time. Open a single s3:// path.".to_string(),
6131 ));
6132 }
6133 source::InputSource::Gcs(_) => {
6134 return Some(AppEvent::Crash(
6135 "Only one GCS URL at a time. Open a single gs:// path.".to_string(),
6136 ));
6137 }
6138 source::InputSource::Http(_) => {
6139 return Some(AppEvent::Crash(
6140 "Only one HTTP/HTTPS URL at a time. Open a single URL.".to_string(),
6141 ));
6142 }
6143 source::InputSource::Local(_) => {}
6144 }
6145 }
6146 let compression = options
6147 .compression
6148 .or_else(|| CompressionFormat::from_extension(first));
6149 let is_csv = options.format == Some(FileFormat::Csv)
6150 || first
6151 .file_stem()
6152 .and_then(|stem| stem.to_str())
6153 .map(|stem| {
6154 stem.ends_with(".csv")
6155 || first
6156 .extension()
6157 .and_then(|e| e.to_str())
6158 .map(|e| e.eq_ignore_ascii_case("csv"))
6159 .unwrap_or(false)
6160 })
6161 .unwrap_or(false);
6162 let is_compressed_csv = matches!(src, source::InputSource::Local(_))
6163 && paths.len() == 1
6164 && compression.is_some()
6165 && is_csv;
6166 if is_compressed_csv {
6167 if let LoadingState::Loading {
6168 file_path,
6169 file_size,
6170 ..
6171 } = &self.loading_state
6172 {
6173 self.loading_state = LoadingState::Loading {
6174 file_path: file_path.clone(),
6175 file_size: *file_size,
6176 current_phase: "Decompressing".to_string(),
6177 progress_percent: 30,
6178 };
6179 }
6180 Some(AppEvent::DoLoad(paths.clone(), options.clone()))
6181 } else {
6182 #[cfg(feature = "http")]
6183 if let source::InputSource::Http(ref url) = src {
6184 let size = Self::fetch_remote_size_http(url).unwrap_or(None);
6185 let size_str = size
6186 .map(Self::format_bytes)
6187 .unwrap_or_else(|| "unknown".to_string());
6188 let dest_dir = options
6189 .temp_dir
6190 .as_deref()
6191 .map(|p| p.display().to_string())
6192 .unwrap_or_else(|| std::env::temp_dir().display().to_string());
6193 let message = format!(
6194 "URL: {}\nFile size: {}\nDestination: {} (temporary file)\n\nContinue with download?",
6195 url, size_str, dest_dir
6196 );
6197 self.pending_download = Some(PendingDownload::Http {
6198 url: url.clone(),
6199 size,
6200 options: options.clone(),
6201 });
6202 self.confirmation_modal.show(message);
6203 return None;
6204 }
6205 #[cfg(feature = "cloud")]
6206 if let source::InputSource::S3(ref url) = src {
6207 let full = format!("s3://{url}");
6208 let (_, ext) = source::url_path_extension(&full);
6209 let is_glob = full.contains('*') || full.ends_with('/');
6210 if source::cloud_path_should_download(ext.as_deref(), is_glob) {
6211 let size =
6212 Self::fetch_remote_size_s3(&full, &self.app_config.cloud, options)
6213 .unwrap_or(None);
6214 let size_str = size
6215 .map(Self::format_bytes)
6216 .unwrap_or_else(|| "unknown".to_string());
6217 let dest_dir = options
6218 .temp_dir
6219 .as_deref()
6220 .map(|p| p.display().to_string())
6221 .unwrap_or_else(|| std::env::temp_dir().display().to_string());
6222 let message = format!(
6223 "URL: {}\nFile size: {}\nDestination: {} (temporary file)\n\nContinue with download?",
6224 full, size_str, dest_dir
6225 );
6226 self.pending_download = Some(PendingDownload::S3 {
6227 url: full,
6228 size,
6229 options: options.clone(),
6230 });
6231 self.confirmation_modal.show(message);
6232 return None;
6233 }
6234 }
6235 #[cfg(feature = "cloud")]
6236 if let source::InputSource::Gcs(ref url) = src {
6237 let full = format!("gs://{url}");
6238 let (_, ext) = source::url_path_extension(&full);
6239 let is_glob = full.contains('*') || full.ends_with('/');
6240 if source::cloud_path_should_download(ext.as_deref(), is_glob) {
6241 let size = Self::fetch_remote_size_gcs(&full, options).unwrap_or(None);
6242 let size_str = size
6243 .map(Self::format_bytes)
6244 .unwrap_or_else(|| "unknown".to_string());
6245 let dest_dir = options
6246 .temp_dir
6247 .as_deref()
6248 .map(|p| p.display().to_string())
6249 .unwrap_or_else(|| std::env::temp_dir().display().to_string());
6250 let message = format!(
6251 "URL: {}\nFile size: {}\nDestination: {} (temporary file)\n\nContinue with download?",
6252 full, size_str, dest_dir
6253 );
6254 self.pending_download = Some(PendingDownload::Gcs {
6255 url: full,
6256 size,
6257 options: options.clone(),
6258 });
6259 self.confirmation_modal.show(message);
6260 return None;
6261 }
6262 }
6263 let first = paths[0].clone();
6264 if paths.len() == 1 && is_csv && options.parse_strings.is_some() {
6266 if let LoadingState::Loading {
6267 file_path,
6268 file_size,
6269 ..
6270 } = &self.loading_state
6271 {
6272 self.loading_state = LoadingState::Loading {
6273 file_path: file_path.clone(),
6274 file_size: *file_size,
6275 current_phase: "Scanning string columns".to_string(),
6276 progress_percent: 55,
6277 };
6278 }
6279 return Some(AppEvent::DoLoadCsvWithParseStrings(
6280 paths.clone(),
6281 options.clone(),
6282 ));
6283 }
6284 #[allow(clippy::needless_borrow)]
6285 match self.build_lazyframe_from_paths(&paths, options) {
6286 Ok(lf) => {
6287 if let LoadingState::Loading {
6288 file_path,
6289 file_size,
6290 ..
6291 } = &self.loading_state
6292 {
6293 self.loading_state = LoadingState::Loading {
6294 file_path: file_path.clone(),
6295 file_size: *file_size,
6296 current_phase: "Caching schema".to_string(),
6297 progress_percent: 40,
6298 };
6299 }
6300 Some(AppEvent::DoLoadSchema(
6301 Box::new(lf),
6302 Some(first),
6303 options.clone(),
6304 ))
6305 }
6306 Err(e) => {
6307 self.loading_state = LoadingState::Idle;
6308 self.busy = false;
6309 self.drain_keys_on_next_loop = true;
6310 let msg = crate::error_display::user_message_from_report(
6311 &e,
6312 paths.first().map(|p| p.as_path()),
6313 );
6314 Some(AppEvent::Crash(msg))
6315 }
6316 }
6317 }
6318 }
6319 AppEvent::DoLoadCsvWithParseStrings(paths, options) => {
6320 let first = paths[0].clone();
6321 #[allow(clippy::needless_borrow)]
6322 match self.build_lazyframe_from_paths(&paths, options) {
6323 Ok(lf) => {
6324 if let LoadingState::Loading {
6325 file_path,
6326 file_size,
6327 ..
6328 } = &self.loading_state
6329 {
6330 self.loading_state = LoadingState::Loading {
6331 file_path: file_path.clone(),
6332 file_size: *file_size,
6333 current_phase: "Caching schema".to_string(),
6334 progress_percent: 40,
6335 };
6336 }
6337 Some(AppEvent::DoLoadSchema(
6338 Box::new(lf),
6339 Some(first),
6340 options.clone(),
6341 ))
6342 }
6343 Err(e) => {
6344 self.loading_state = LoadingState::Idle;
6345 self.busy = false;
6346 self.drain_keys_on_next_loop = true;
6347 let msg = crate::error_display::user_message_from_report(
6348 &e,
6349 paths.first().map(|p| p.as_path()),
6350 );
6351 Some(AppEvent::Crash(msg))
6352 }
6353 }
6354 }
6355 #[cfg(feature = "http")]
6356 AppEvent::DoDownloadHttp(url, options) => {
6357 let (_, ext) = source::url_path_extension(url.as_str());
6358 match Self::download_http_to_temp(
6359 url.as_str(),
6360 options.temp_dir.as_deref(),
6361 ext.as_deref(),
6362 ) {
6363 Ok(temp_path) => {
6364 self.http_temp_path = Some(temp_path.clone());
6365 if let LoadingState::Loading {
6366 file_path,
6367 file_size,
6368 ..
6369 } = &self.loading_state
6370 {
6371 self.loading_state = LoadingState::Loading {
6372 file_path: file_path.clone(),
6373 file_size: *file_size,
6374 current_phase: "Scanning".to_string(),
6375 progress_percent: 30,
6376 };
6377 }
6378 Some(AppEvent::DoLoadFromHttpTemp(temp_path, options.clone()))
6379 }
6380 Err(e) => {
6381 self.loading_state = LoadingState::Idle;
6382 self.busy = false;
6383 self.drain_keys_on_next_loop = true;
6384 let msg = crate::error_display::user_message_from_report(&e, None);
6385 Some(AppEvent::Crash(msg))
6386 }
6387 }
6388 }
6389 #[cfg(feature = "cloud")]
6390 AppEvent::DoDownloadS3ToTemp(s3_url, options) => {
6391 match Self::download_s3_to_temp(s3_url, &self.app_config.cloud, options) {
6392 Ok(temp_path) => {
6393 self.http_temp_path = Some(temp_path.clone());
6394 if let LoadingState::Loading {
6395 file_path,
6396 file_size,
6397 ..
6398 } = &self.loading_state
6399 {
6400 self.loading_state = LoadingState::Loading {
6401 file_path: file_path.clone(),
6402 file_size: *file_size,
6403 current_phase: "Scanning".to_string(),
6404 progress_percent: 30,
6405 };
6406 }
6407 Some(AppEvent::DoLoadFromHttpTemp(temp_path, options.clone()))
6408 }
6409 Err(e) => {
6410 self.loading_state = LoadingState::Idle;
6411 self.busy = false;
6412 self.drain_keys_on_next_loop = true;
6413 let msg = crate::error_display::user_message_from_report(&e, None);
6414 Some(AppEvent::Crash(msg))
6415 }
6416 }
6417 }
6418 #[cfg(feature = "cloud")]
6419 AppEvent::DoDownloadGcsToTemp(gs_url, options) => {
6420 match Self::download_gcs_to_temp(gs_url, options) {
6421 Ok(temp_path) => {
6422 self.http_temp_path = Some(temp_path.clone());
6423 if let LoadingState::Loading {
6424 file_path,
6425 file_size,
6426 ..
6427 } = &self.loading_state
6428 {
6429 self.loading_state = LoadingState::Loading {
6430 file_path: file_path.clone(),
6431 file_size: *file_size,
6432 current_phase: "Scanning".to_string(),
6433 progress_percent: 30,
6434 };
6435 }
6436 Some(AppEvent::DoLoadFromHttpTemp(temp_path, options.clone()))
6437 }
6438 Err(e) => {
6439 self.loading_state = LoadingState::Idle;
6440 self.busy = false;
6441 self.drain_keys_on_next_loop = true;
6442 let msg = crate::error_display::user_message_from_report(&e, None);
6443 Some(AppEvent::Crash(msg))
6444 }
6445 }
6446 }
6447 #[cfg(any(feature = "http", feature = "cloud"))]
6448 AppEvent::DoLoadFromHttpTemp(temp_path, options) => {
6449 self.http_temp_path = Some(temp_path.clone());
6450 let display_path = match &self.loading_state {
6451 LoadingState::Loading { file_path, .. } => file_path.clone(),
6452 _ => None,
6453 };
6454 if let LoadingState::Loading {
6455 file_path,
6456 file_size,
6457 ..
6458 } = &self.loading_state
6459 {
6460 self.loading_state = LoadingState::Loading {
6461 file_path: file_path.clone(),
6462 file_size: *file_size,
6463 current_phase: "Scanning".to_string(),
6464 progress_percent: 30,
6465 };
6466 }
6467 #[allow(clippy::cloned_ref_to_slice_refs)]
6468 match self.build_lazyframe_from_paths(&[temp_path.clone()], options) {
6469 Ok(lf) => {
6470 if let LoadingState::Loading {
6471 file_path,
6472 file_size,
6473 ..
6474 } = &self.loading_state
6475 {
6476 self.loading_state = LoadingState::Loading {
6477 file_path: file_path.clone(),
6478 file_size: *file_size,
6479 current_phase: "Caching schema".to_string(),
6480 progress_percent: 40,
6481 };
6482 }
6483 Some(AppEvent::DoLoadSchema(
6484 Box::new(lf),
6485 display_path,
6486 options.clone(),
6487 ))
6488 }
6489 Err(e) => {
6490 self.loading_state = LoadingState::Idle;
6491 self.busy = false;
6492 self.drain_keys_on_next_loop = true;
6493 let msg = crate::error_display::user_message_from_report(
6494 &e,
6495 Some(temp_path.as_path()),
6496 );
6497 Some(AppEvent::Crash(msg))
6498 }
6499 }
6500 }
6501 AppEvent::DoLoadSchema(lf, path, options) => {
6502 if let LoadingState::Loading {
6504 file_path,
6505 file_size,
6506 ..
6507 } = &self.loading_state
6508 {
6509 self.loading_state = LoadingState::Loading {
6510 file_path: file_path.clone(),
6511 file_size: *file_size,
6512 current_phase: "Caching schema".to_string(),
6513 progress_percent: 40,
6514 };
6515 }
6516 Some(AppEvent::DoLoadSchemaBlocking(
6517 lf.clone(),
6518 path.clone(),
6519 options.clone(),
6520 ))
6521 }
6522 AppEvent::DoLoadSchemaBlocking(lf, path, options) => {
6523 self.debug.schema_load = None;
6524 if options.single_spine_schema
6526 && path.as_ref().is_some_and(|p| p.is_dir() && options.hive)
6527 {
6528 let p = path.as_ref().expect("path set by caller");
6529 if let Ok((merged_schema, partition_columns)) =
6530 DataTableState::schema_from_one_hive_parquet(p)
6531 {
6532 if let Ok(lf_owned) =
6533 DataTableState::scan_parquet_hive_with_schema(p, merged_schema.clone())
6534 {
6535 match DataTableState::from_schema_and_lazyframe(
6536 merged_schema,
6537 lf_owned,
6538 options,
6539 Some(partition_columns),
6540 ) {
6541 Ok(state) => {
6542 self.debug.schema_load = Some("one-file (local)".to_string());
6543 self.parquet_metadata_cache = None;
6544 self.export_df = None;
6545 self.data_table_state = Some(state);
6546 self.path = path.clone();
6547 if let Some(ref path_p) = path {
6548 self.original_file_format = path_p
6549 .extension()
6550 .and_then(|e| e.to_str())
6551 .and_then(|ext| {
6552 if ext.eq_ignore_ascii_case("parquet") {
6553 Some(ExportFormat::Parquet)
6554 } else if ext.eq_ignore_ascii_case("csv") {
6555 Some(ExportFormat::Csv)
6556 } else if ext.eq_ignore_ascii_case("json") {
6557 Some(ExportFormat::Json)
6558 } else if ext.eq_ignore_ascii_case("jsonl")
6559 || ext.eq_ignore_ascii_case("ndjson")
6560 {
6561 Some(ExportFormat::Ndjson)
6562 } else if ext.eq_ignore_ascii_case("arrow")
6563 || ext.eq_ignore_ascii_case("ipc")
6564 || ext.eq_ignore_ascii_case("feather")
6565 {
6566 Some(ExportFormat::Ipc)
6567 } else if ext.eq_ignore_ascii_case("avro") {
6568 Some(ExportFormat::Avro)
6569 } else {
6570 None
6571 }
6572 });
6573 self.original_file_delimiter =
6574 Some(options.delimiter.unwrap_or(b','));
6575 } else {
6576 self.original_file_format = None;
6577 self.original_file_delimiter = None;
6578 }
6579 self.sort_filter_modal = SortFilterModal::new();
6580 self.pivot_melt_modal = PivotMeltModal::new();
6581 if let LoadingState::Loading {
6582 file_path,
6583 file_size,
6584 ..
6585 } = &self.loading_state
6586 {
6587 self.loading_state = LoadingState::Loading {
6588 file_path: file_path.clone(),
6589 file_size: *file_size,
6590 current_phase: "Loading buffer".to_string(),
6591 progress_percent: 70,
6592 };
6593 }
6594 return Some(AppEvent::DoLoadBuffer);
6595 }
6596 Err(e) => {
6597 self.loading_state = LoadingState::Idle;
6598 self.busy = false;
6599 self.drain_keys_on_next_loop = true;
6600 let msg =
6601 crate::error_display::user_message_from_report(&e, None);
6602 return Some(AppEvent::Crash(msg));
6603 }
6604 }
6605 }
6606 }
6607 }
6608
6609 #[cfg(feature = "cloud")]
6610 {
6611 if options.single_spine_schema
6614 && path.as_ref().is_some_and(|p| {
6615 let s = p.as_os_str().to_string_lossy();
6616 let is_cloud = s.starts_with("s3://") || s.starts_with("gs://");
6617 let looks_like_hive = s.ends_with('/') || s.contains('*');
6618 is_cloud && (options.hive || looks_like_hive)
6619 })
6620 {
6621 self.debug.schema_load = Some("trying one-file (cloud)".to_string());
6622 let src = source::input_source(path.as_ref().expect("path set by caller"));
6623 let try_cloud = match &src {
6624 source::InputSource::S3(url) => {
6625 let full = format!("s3://{url}");
6626 let (path_part, _) = source::url_path_extension(&full);
6627 let key = path_part
6628 .split_once('/')
6629 .map(|(_, k)| k.trim_end_matches('/'))
6630 .unwrap_or("");
6631 let cloud_opts =
6632 Self::build_s3_cloud_options(&self.app_config.cloud, options);
6633 Self::build_s3_object_store(&full, &self.app_config.cloud, options)
6634 .ok()
6635 .and_then(|store| {
6636 let rt = tokio::runtime::Runtime::new().ok()?;
6637 let (merged_schema, partition_columns) = rt
6638 .block_on(cloud_hive::schema_from_one_cloud_hive(
6639 store, key,
6640 ))
6641 .ok()?;
6642 let pl_path = PlPathRef::new(&full).into_owned();
6643 let args = ScanArgsParquet {
6644 schema: Some(merged_schema.clone()),
6645 cloud_options: Some(cloud_opts),
6646 hive_options: polars::io::HiveOptions::new_enabled(),
6647 glob: true,
6648 ..Default::default()
6649 };
6650 let mut lf_owned =
6651 LazyFrame::scan_parquet(pl_path, args).ok()?;
6652 if !partition_columns.is_empty() {
6653 let exprs: Vec<_> = partition_columns
6654 .iter()
6655 .map(|s| col(s.as_str()))
6656 .chain(
6657 merged_schema
6658 .iter_names()
6659 .map(|s| s.to_string())
6660 .filter(|c| !partition_columns.contains(c))
6661 .map(|s| col(s.as_str())),
6662 )
6663 .collect();
6664 lf_owned = lf_owned.select(exprs);
6665 }
6666 DataTableState::from_schema_and_lazyframe(
6667 merged_schema,
6668 lf_owned,
6669 options,
6670 Some(partition_columns),
6671 )
6672 .ok()
6673 })
6674 }
6675 source::InputSource::Gcs(url) => {
6676 let full = format!("gs://{url}");
6677 let (path_part, _) = source::url_path_extension(&full);
6678 let key = path_part
6679 .split_once('/')
6680 .map(|(_, k)| k.trim_end_matches('/'))
6681 .unwrap_or("");
6682 Self::build_gcs_object_store(&full).ok().and_then(|store| {
6683 let rt = tokio::runtime::Runtime::new().ok()?;
6684 let (merged_schema, partition_columns) = rt
6685 .block_on(cloud_hive::schema_from_one_cloud_hive(
6686 store, key,
6687 ))
6688 .ok()?;
6689 let pl_path = PlPathRef::new(&full).into_owned();
6690 let args = ScanArgsParquet {
6691 schema: Some(merged_schema.clone()),
6692 cloud_options: Some(CloudOptions::default()),
6693 hive_options: polars::io::HiveOptions::new_enabled(),
6694 glob: true,
6695 ..Default::default()
6696 };
6697 let mut lf_owned =
6698 LazyFrame::scan_parquet(pl_path, args).ok()?;
6699 if !partition_columns.is_empty() {
6700 let exprs: Vec<_> = partition_columns
6701 .iter()
6702 .map(|s| col(s.as_str()))
6703 .chain(
6704 merged_schema
6705 .iter_names()
6706 .map(|s| s.to_string())
6707 .filter(|c| !partition_columns.contains(c))
6708 .map(|s| col(s.as_str())),
6709 )
6710 .collect();
6711 lf_owned = lf_owned.select(exprs);
6712 }
6713 DataTableState::from_schema_and_lazyframe(
6714 merged_schema,
6715 lf_owned,
6716 options,
6717 Some(partition_columns),
6718 )
6719 .ok()
6720 })
6721 }
6722 _ => None,
6723 };
6724 if let Some(state) = try_cloud {
6725 self.debug.schema_load = Some("one-file (cloud)".to_string());
6726 self.parquet_metadata_cache = None;
6727 self.export_df = None;
6728 self.data_table_state = Some(state);
6729 self.path = path.clone();
6730 if let Some(ref path_p) = path {
6731 self.original_file_format =
6732 path_p.extension().and_then(|e| e.to_str()).and_then(|ext| {
6733 if ext.eq_ignore_ascii_case("parquet") {
6734 Some(ExportFormat::Parquet)
6735 } else if ext.eq_ignore_ascii_case("csv") {
6736 Some(ExportFormat::Csv)
6737 } else if ext.eq_ignore_ascii_case("json") {
6738 Some(ExportFormat::Json)
6739 } else if ext.eq_ignore_ascii_case("jsonl")
6740 || ext.eq_ignore_ascii_case("ndjson")
6741 {
6742 Some(ExportFormat::Ndjson)
6743 } else if ext.eq_ignore_ascii_case("arrow")
6744 || ext.eq_ignore_ascii_case("ipc")
6745 || ext.eq_ignore_ascii_case("feather")
6746 {
6747 Some(ExportFormat::Ipc)
6748 } else if ext.eq_ignore_ascii_case("avro") {
6749 Some(ExportFormat::Avro)
6750 } else {
6751 None
6752 }
6753 });
6754 self.original_file_delimiter =
6755 Some(options.delimiter.unwrap_or(b','));
6756 } else {
6757 self.original_file_format = None;
6758 self.original_file_delimiter = None;
6759 }
6760 self.sort_filter_modal = SortFilterModal::new();
6761 self.pivot_melt_modal = PivotMeltModal::new();
6762 if let LoadingState::Loading {
6763 file_path,
6764 file_size,
6765 ..
6766 } = &self.loading_state
6767 {
6768 self.loading_state = LoadingState::Loading {
6769 file_path: file_path.clone(),
6770 file_size: *file_size,
6771 current_phase: "Loading buffer".to_string(),
6772 progress_percent: 70,
6773 };
6774 }
6775 return Some(AppEvent::DoLoadBuffer);
6776 } else {
6777 self.debug.schema_load = Some("fallback (cloud)".to_string());
6778 }
6779 }
6780 }
6781
6782 if self.debug.schema_load.is_none() {
6783 self.debug.schema_load = Some("full scan".to_string());
6784 }
6785 let mut lf_owned = (**lf).clone();
6786 match lf_owned.collect_schema() {
6787 Ok(schema) => {
6788 let partition_columns = if path.as_ref().is_some_and(|p| {
6789 options.hive
6790 && (p.is_dir() || p.as_os_str().to_string_lossy().contains('*'))
6791 }) {
6792 let discovered = DataTableState::discover_hive_partition_columns(
6793 path.as_ref().expect("path set by caller"),
6794 );
6795 discovered
6796 .into_iter()
6797 .filter(|c| schema.contains(c.as_str()))
6798 .collect::<Vec<_>>()
6799 } else {
6800 Vec::new()
6801 };
6802 if !partition_columns.is_empty() {
6803 let exprs: Vec<_> = partition_columns
6804 .iter()
6805 .map(|s| col(s.as_str()))
6806 .chain(
6807 schema
6808 .iter_names()
6809 .map(|s| s.to_string())
6810 .filter(|c| !partition_columns.contains(c))
6811 .map(|s| col(s.as_str())),
6812 )
6813 .collect();
6814 lf_owned = lf_owned.select(exprs);
6815 }
6816 let part_cols_opt = if partition_columns.is_empty() {
6817 None
6818 } else {
6819 Some(partition_columns)
6820 };
6821 match DataTableState::from_schema_and_lazyframe(
6822 schema,
6823 lf_owned,
6824 options,
6825 part_cols_opt,
6826 ) {
6827 Ok(state) => {
6828 self.parquet_metadata_cache = None;
6829 self.export_df = None;
6830 self.data_table_state = Some(state);
6831 self.path = path.clone();
6832 if let Some(ref p) = path {
6833 self.original_file_format =
6834 p.extension().and_then(|e| e.to_str()).and_then(|ext| {
6835 if ext.eq_ignore_ascii_case("parquet") {
6836 Some(ExportFormat::Parquet)
6837 } else if ext.eq_ignore_ascii_case("csv") {
6838 Some(ExportFormat::Csv)
6839 } else if ext.eq_ignore_ascii_case("json") {
6840 Some(ExportFormat::Json)
6841 } else if ext.eq_ignore_ascii_case("jsonl")
6842 || ext.eq_ignore_ascii_case("ndjson")
6843 {
6844 Some(ExportFormat::Ndjson)
6845 } else if ext.eq_ignore_ascii_case("arrow")
6846 || ext.eq_ignore_ascii_case("ipc")
6847 || ext.eq_ignore_ascii_case("feather")
6848 {
6849 Some(ExportFormat::Ipc)
6850 } else if ext.eq_ignore_ascii_case("avro") {
6851 Some(ExportFormat::Avro)
6852 } else {
6853 None
6854 }
6855 });
6856 self.original_file_delimiter =
6857 Some(options.delimiter.unwrap_or(b','));
6858 } else {
6859 self.original_file_format = None;
6860 self.original_file_delimiter = None;
6861 }
6862 self.sort_filter_modal = SortFilterModal::new();
6863 self.pivot_melt_modal = PivotMeltModal::new();
6864 if let LoadingState::Loading {
6865 file_path,
6866 file_size,
6867 ..
6868 } = &self.loading_state
6869 {
6870 self.loading_state = LoadingState::Loading {
6871 file_path: file_path.clone(),
6872 file_size: *file_size,
6873 current_phase: "Loading buffer".to_string(),
6874 progress_percent: 70,
6875 };
6876 }
6877 Some(AppEvent::DoLoadBuffer)
6878 }
6879 Err(e) => {
6880 self.loading_state = LoadingState::Idle;
6881 self.busy = false;
6882 self.drain_keys_on_next_loop = true;
6883 let msg = crate::error_display::user_message_from_report(&e, None);
6884 Some(AppEvent::Crash(msg))
6885 }
6886 }
6887 }
6888 Err(e) => {
6889 self.loading_state = LoadingState::Idle;
6890 self.busy = false;
6891 self.drain_keys_on_next_loop = true;
6892 let report = color_eyre::eyre::Report::from(e);
6893 let msg = crate::error_display::user_message_from_report(&report, None);
6894 Some(AppEvent::Crash(msg))
6895 }
6896 }
6897 }
6898 AppEvent::DoLoadBuffer => {
6899 if let Some(state) = &mut self.data_table_state {
6900 state.collect();
6901 if let Some(e) = state.error.take() {
6902 self.loading_state = LoadingState::Idle;
6903 self.busy = false;
6904 self.drain_keys_on_next_loop = true;
6905 let msg = crate::error_display::user_message_from_polars(&e);
6906 return Some(AppEvent::Crash(msg));
6907 }
6908 }
6909 self.loading_state = LoadingState::Idle;
6910 self.busy = false;
6911 self.drain_keys_on_next_loop = true;
6912 Some(AppEvent::Collect)
6913 }
6914 AppEvent::DoLoad(paths, options) => {
6915 let first = &paths[0];
6916 let compression = options
6918 .compression
6919 .or_else(|| CompressionFormat::from_extension(first));
6920 let is_csv = first
6921 .file_stem()
6922 .and_then(|stem| stem.to_str())
6923 .map(|stem| {
6924 stem.ends_with(".csv")
6925 || first
6926 .extension()
6927 .and_then(|e| e.to_str())
6928 .map(|e| e.eq_ignore_ascii_case("csv"))
6929 .unwrap_or(false)
6930 })
6931 .unwrap_or(false);
6932 let is_compressed_csv = paths.len() == 1 && compression.is_some() && is_csv;
6933
6934 if is_compressed_csv {
6935 if let LoadingState::Loading {
6937 file_path,
6938 file_size,
6939 ..
6940 } = &self.loading_state
6941 {
6942 self.loading_state = LoadingState::Loading {
6943 file_path: file_path.clone(),
6944 file_size: *file_size,
6945 current_phase: "Decompressing".to_string(),
6946 progress_percent: 30,
6947 };
6948 }
6949 Some(AppEvent::DoDecompress(paths.clone(), options.clone()))
6951 } else {
6952 match self.load(paths, options) {
6954 Ok(_) => {
6955 self.busy = false;
6956 self.drain_keys_on_next_loop = true;
6957 Some(AppEvent::Collect)
6958 }
6959 Err(e) => {
6960 self.loading_state = LoadingState::Idle;
6961 self.busy = false;
6962 self.drain_keys_on_next_loop = true;
6963 let msg = crate::error_display::user_message_from_report(
6964 &e,
6965 paths.first().map(|p| p.as_path()),
6966 );
6967 Some(AppEvent::Crash(msg))
6968 }
6969 }
6970 }
6971 }
6972 AppEvent::DoDecompress(paths, options) => {
6973 match self.load(paths, options) {
6975 Ok(_) => Some(AppEvent::DoLoadBuffer),
6976 Err(e) => {
6977 self.loading_state = LoadingState::Idle;
6978 self.busy = false;
6979 self.drain_keys_on_next_loop = true;
6980 let msg = crate::error_display::user_message_from_report(
6981 &e,
6982 paths.first().map(|p| p.as_path()),
6983 );
6984 Some(AppEvent::Crash(msg))
6985 }
6986 }
6987 }
6988 AppEvent::Resize(_cols, rows) => {
6989 self.busy = true;
6990 if let Some(state) = &mut self.data_table_state {
6991 state.visible_rows = *rows as usize;
6992 state.collect();
6993 }
6994 self.busy = false;
6995 self.drain_keys_on_next_loop = true;
6996 None
6997 }
6998 AppEvent::Collect => {
6999 self.busy = true;
7000 if let Some(ref mut state) = self.data_table_state {
7001 state.collect();
7002 }
7003 self.busy = false;
7004 self.drain_keys_on_next_loop = true;
7005 None
7006 }
7007 AppEvent::DoScrollDown => {
7008 if let Some(state) = &mut self.data_table_state {
7009 state.page_down();
7010 }
7011 self.busy = false;
7012 self.drain_keys_on_next_loop = true;
7013 None
7014 }
7015 AppEvent::DoScrollUp => {
7016 if let Some(state) = &mut self.data_table_state {
7017 state.page_up();
7018 }
7019 self.busy = false;
7020 self.drain_keys_on_next_loop = true;
7021 None
7022 }
7023 AppEvent::DoScrollNext => {
7024 if let Some(state) = &mut self.data_table_state {
7025 state.select_next();
7026 }
7027 self.busy = false;
7028 self.drain_keys_on_next_loop = true;
7029 None
7030 }
7031 AppEvent::DoScrollPrev => {
7032 if let Some(state) = &mut self.data_table_state {
7033 state.select_previous();
7034 }
7035 self.busy = false;
7036 self.drain_keys_on_next_loop = true;
7037 None
7038 }
7039 AppEvent::DoScrollEnd => {
7040 if let Some(state) = &mut self.data_table_state {
7041 state.scroll_to_end();
7042 }
7043 self.busy = false;
7044 self.drain_keys_on_next_loop = true;
7045 None
7046 }
7047 AppEvent::DoScrollHalfDown => {
7048 if let Some(state) = &mut self.data_table_state {
7049 state.half_page_down();
7050 }
7051 self.busy = false;
7052 self.drain_keys_on_next_loop = true;
7053 None
7054 }
7055 AppEvent::DoScrollHalfUp => {
7056 if let Some(state) = &mut self.data_table_state {
7057 state.half_page_up();
7058 }
7059 self.busy = false;
7060 self.drain_keys_on_next_loop = true;
7061 None
7062 }
7063 AppEvent::GoToLine(n) => {
7064 if let Some(state) = &mut self.data_table_state {
7065 state.scroll_to_row_centered(*n);
7066 }
7067 self.busy = false;
7068 self.drain_keys_on_next_loop = true;
7069 None
7070 }
7071 AppEvent::AnalysisChunk => {
7072 let lf = match &self.data_table_state {
7073 Some(state) => state.lf.clone(),
7074 None => {
7075 self.analysis_computation = None;
7076 self.analysis_modal.computing = None;
7077 self.busy = false;
7078 return None;
7079 }
7080 };
7081 let comp = self.analysis_computation.take()?;
7082 if comp.df.is_none() {
7083 let total_rows = match self
7086 .data_table_state
7087 .as_ref()
7088 .and_then(|s| s.num_rows_if_valid())
7089 {
7090 Some(n) => n,
7091 None => match crate::statistics::collect_lazy(
7092 lf.clone().select([len()]),
7093 self.app_config.performance.polars_streaming,
7094 ) {
7095 Ok(count_df) => {
7096 if let Some(col) = count_df.get(0) {
7097 match col.first() {
7098 Some(AnyValue::UInt32(n)) => *n as usize,
7099 _ => 0,
7100 }
7101 } else {
7102 0
7103 }
7104 }
7105 Err(_e) => {
7106 self.analysis_modal.computing = None;
7107 self.busy = false;
7108 self.drain_keys_on_next_loop = true;
7109 return None;
7110 }
7111 },
7112 };
7113 match crate::statistics::compute_describe_from_lazy(
7114 &lf,
7115 total_rows,
7116 self.sampling_threshold,
7117 comp.sample_seed,
7118 self.app_config.performance.polars_streaming,
7119 ) {
7120 Ok(results) => {
7121 self.analysis_modal.describe_results = Some(results);
7122 self.analysis_modal.computing = None;
7123 self.busy = false;
7124 self.drain_keys_on_next_loop = true;
7125 None
7126 }
7127 Err(_e) => {
7128 self.analysis_modal.computing = None;
7129 self.busy = false;
7130 self.drain_keys_on_next_loop = true;
7131 None
7132 }
7133 }
7134 } else {
7135 None
7136 }
7137 }
7138 AppEvent::AnalysisDistributionCompute => {
7139 if let Some(state) = &self.data_table_state {
7140 let options = crate::statistics::ComputeOptions {
7141 include_distribution_info: true,
7142 include_distribution_analyses: true,
7143 include_correlation_matrix: false,
7144 include_skewness_kurtosis_outliers: true,
7145 polars_streaming: self.app_config.performance.polars_streaming,
7146 };
7147 if let Ok(results) = crate::statistics::compute_statistics_with_options(
7148 &state.lf,
7149 self.sampling_threshold,
7150 self.analysis_modal.random_seed,
7151 options,
7152 ) {
7153 self.analysis_modal.distribution_results = Some(results);
7154 }
7155 }
7156 self.analysis_modal.computing = None;
7157 self.busy = false;
7158 self.drain_keys_on_next_loop = true;
7159 None
7160 }
7161 AppEvent::AnalysisCorrelationCompute => {
7162 if let Some(state) = &self.data_table_state {
7163 if let Ok(df) =
7164 crate::statistics::collect_lazy(state.lf.clone(), state.polars_streaming)
7165 {
7166 if let Ok(matrix) = crate::statistics::compute_correlation_matrix(&df) {
7167 self.analysis_modal.correlation_results =
7168 Some(crate::statistics::AnalysisResults {
7169 column_statistics: vec![],
7170 total_rows: df.height(),
7171 sample_size: None,
7172 sample_seed: self.analysis_modal.random_seed,
7173 correlation_matrix: Some(matrix),
7174 distribution_analyses: vec![],
7175 });
7176 }
7177 }
7178 }
7179 self.analysis_modal.computing = None;
7180 self.busy = false;
7181 self.drain_keys_on_next_loop = true;
7182 None
7183 }
7184 AppEvent::Search(query) => {
7185 let query_succeeded = if let Some(state) = &mut self.data_table_state {
7186 state.query(query.clone());
7187 state.error.is_none()
7188 } else {
7189 false
7190 };
7191
7192 if query_succeeded {
7194 self.input_mode = InputMode::Normal;
7196 self.query_input.set_focused(false);
7197 if let Some(state) = &mut self.data_table_state {
7199 state.suppress_error_display = false;
7200 }
7201 }
7202 None
7205 }
7206 AppEvent::SqlSearch(sql) => {
7207 let sql_succeeded = if let Some(state) = &mut self.data_table_state {
7208 state.sql_query(sql.clone());
7209 state.error.is_none()
7210 } else {
7211 false
7212 };
7213 if sql_succeeded {
7214 self.input_mode = InputMode::Normal;
7215 self.sql_input.set_focused(false);
7216 if let Some(state) = &mut self.data_table_state {
7217 state.suppress_error_display = false;
7218 }
7219 Some(AppEvent::Collect)
7220 } else {
7221 None
7222 }
7223 }
7224 AppEvent::FuzzySearch(query) => {
7225 let fuzzy_succeeded = if let Some(state) = &mut self.data_table_state {
7226 state.fuzzy_search(query.clone());
7227 state.error.is_none()
7228 } else {
7229 false
7230 };
7231 if fuzzy_succeeded {
7232 self.input_mode = InputMode::Normal;
7233 self.fuzzy_input.set_focused(false);
7234 if let Some(state) = &mut self.data_table_state {
7235 state.suppress_error_display = false;
7236 }
7237 Some(AppEvent::Collect)
7238 } else {
7239 None
7240 }
7241 }
7242 AppEvent::Filter(statements) => {
7243 if let Some(state) = &mut self.data_table_state {
7244 state.filter(statements.clone());
7245 }
7246 None
7247 }
7248 AppEvent::Sort(columns, ascending) => {
7249 if let Some(state) = &mut self.data_table_state {
7250 state.sort(columns.clone(), *ascending);
7251 }
7252 None
7253 }
7254 AppEvent::Reset => {
7255 if let Some(state) = &mut self.data_table_state {
7256 state.reset();
7257 }
7258 self.active_template_id = None;
7260 None
7261 }
7262 AppEvent::ColumnOrder(order, locked_count) => {
7263 if let Some(state) = &mut self.data_table_state {
7264 state.set_column_order(order.clone());
7265 state.set_locked_columns(*locked_count);
7266 }
7267 None
7268 }
7269 AppEvent::Pivot(spec) => {
7270 self.busy = true;
7271 if let Some(state) = &mut self.data_table_state {
7272 match state.pivot(spec) {
7273 Ok(()) => {
7274 self.pivot_melt_modal.close();
7275 self.input_mode = InputMode::Normal;
7276 Some(AppEvent::Collect)
7277 }
7278 Err(e) => {
7279 self.busy = false;
7280 self.error_modal
7281 .show(crate::error_display::user_message_from_report(&e, None));
7282 None
7283 }
7284 }
7285 } else {
7286 self.busy = false;
7287 None
7288 }
7289 }
7290 AppEvent::Melt(spec) => {
7291 self.busy = true;
7292 if let Some(state) = &mut self.data_table_state {
7293 match state.melt(spec) {
7294 Ok(()) => {
7295 self.pivot_melt_modal.close();
7296 self.input_mode = InputMode::Normal;
7297 Some(AppEvent::Collect)
7298 }
7299 Err(e) => {
7300 self.busy = false;
7301 self.error_modal
7302 .show(crate::error_display::user_message_from_report(&e, None));
7303 None
7304 }
7305 }
7306 } else {
7307 self.busy = false;
7308 None
7309 }
7310 }
7311 AppEvent::ChartExport(path, format, title, width, height) => {
7312 self.busy = true;
7313 self.loading_state = LoadingState::Exporting {
7314 file_path: path.clone(),
7315 current_phase: "Exporting chart".to_string(),
7316 progress_percent: 0,
7317 };
7318 Some(AppEvent::DoChartExport(
7319 path.clone(),
7320 *format,
7321 title.clone(),
7322 *width,
7323 *height,
7324 ))
7325 }
7326 AppEvent::DoChartExport(path, format, title, width, height) => {
7327 let result = self.do_chart_export(path, *format, title, *width, *height);
7328 self.loading_state = LoadingState::Idle;
7329 self.busy = false;
7330 self.drain_keys_on_next_loop = true;
7331 match result {
7332 Ok(()) => {
7333 self.success_modal.show(format!(
7334 "Chart exported successfully to\n{}",
7335 path.display()
7336 ));
7337 self.chart_export_modal.close();
7338 }
7339 Err(e) => {
7340 self.error_modal
7341 .show(crate::error_display::user_message_from_report(
7342 &e,
7343 Some(path),
7344 ));
7345 self.chart_export_modal.reopen_with_path(path, *format);
7346 }
7347 }
7348 None
7349 }
7350 AppEvent::Export(path, format, options) => {
7351 if let Some(_state) = &self.data_table_state {
7352 self.busy = true;
7353 self.loading_state = LoadingState::Exporting {
7355 file_path: path.clone(),
7356 current_phase: "Preparing export".to_string(),
7357 progress_percent: 0,
7358 };
7359 Some(AppEvent::DoExport(path.clone(), *format, options.clone()))
7361 } else {
7362 None
7363 }
7364 }
7365 AppEvent::DoExport(path, format, options) => {
7366 if let Some(_state) = &self.data_table_state {
7367 self.loading_state = LoadingState::Exporting {
7369 file_path: path.clone(),
7370 current_phase: "Collecting data".to_string(),
7371 progress_percent: 10,
7372 };
7373 Some(AppEvent::DoExportCollect(
7374 path.clone(),
7375 *format,
7376 options.clone(),
7377 ))
7378 } else {
7379 self.busy = false;
7380 None
7381 }
7382 }
7383 AppEvent::DoExportCollect(path, format, options) => {
7384 if let Some(state) = &self.data_table_state {
7385 match crate::statistics::collect_lazy(state.lf.clone(), state.polars_streaming)
7386 {
7387 Ok(df) => {
7388 self.export_df = Some(df);
7389 let has_compression = match format {
7390 ExportFormat::Csv => options.csv_compression.is_some(),
7391 ExportFormat::Json => options.json_compression.is_some(),
7392 ExportFormat::Ndjson => options.ndjson_compression.is_some(),
7393 ExportFormat::Parquet | ExportFormat::Ipc | ExportFormat::Avro => {
7394 false
7395 }
7396 };
7397 let phase = if has_compression {
7398 "Writing and compressing file"
7399 } else {
7400 "Writing file"
7401 };
7402 self.loading_state = LoadingState::Exporting {
7403 file_path: path.clone(),
7404 current_phase: phase.to_string(),
7405 progress_percent: 50,
7406 };
7407 Some(AppEvent::DoExportWrite(
7408 path.clone(),
7409 *format,
7410 options.clone(),
7411 ))
7412 }
7413 Err(e) => {
7414 self.loading_state = LoadingState::Idle;
7415 self.busy = false;
7416 self.drain_keys_on_next_loop = true;
7417 self.error_modal.show(format!(
7418 "Export failed: {}",
7419 crate::error_display::user_message_from_polars(&e)
7420 ));
7421 None
7422 }
7423 }
7424 } else {
7425 self.busy = false;
7426 None
7427 }
7428 }
7429 AppEvent::DoExportWrite(path, format, options) => {
7430 let result = self
7431 .export_df
7432 .take()
7433 .map(|mut df| Self::export_data_from_df(&mut df, path, *format, options));
7434 self.loading_state = LoadingState::Idle;
7435 self.busy = false;
7436 self.drain_keys_on_next_loop = true;
7437 match result {
7438 Some(Ok(())) => {
7439 self.success_modal
7440 .show(format!("Data exported successfully to\n{}", path.display()));
7441 }
7442 Some(Err(e)) => {
7443 let error_msg = Self::format_export_error(&e, path);
7444 self.error_modal.show(error_msg);
7445 }
7446 None => {}
7447 }
7448 None
7449 }
7450 AppEvent::DoLoadParquetMetadata => {
7451 let path = self.path.clone();
7452 if let Some(p) = &path {
7453 if let Some(meta) = read_parquet_metadata(p) {
7454 self.parquet_metadata_cache = Some(meta);
7455 }
7456 }
7457 self.busy = false;
7458 self.drain_keys_on_next_loop = true;
7459 None
7460 }
7461 _ => None,
7462 }
7463 }
7464
7465 fn do_chart_export(
7469 &self,
7470 path: &Path,
7471 format: ChartExportFormat,
7472 title: &str,
7473 width: u32,
7474 height: u32,
7475 ) -> color_eyre::Result<()> {
7476 let state = self
7477 .data_table_state
7478 .as_ref()
7479 .ok_or_else(|| color_eyre::eyre::eyre!("No data loaded"))?;
7480 let chart_title = title.trim();
7481 let chart_title = if chart_title.is_empty() {
7482 None
7483 } else {
7484 Some(chart_title.to_string())
7485 };
7486
7487 match self.chart_modal.chart_kind {
7488 ChartKind::XY => {
7489 let x_column = self
7490 .chart_modal
7491 .effective_x_column()
7492 .ok_or_else(|| color_eyre::eyre::eyre!("No X axis column selected"))?;
7493 let y_columns = self.chart_modal.effective_y_columns();
7494 if y_columns.is_empty() {
7495 return Err(color_eyre::eyre::eyre!("No Y axis columns selected"));
7496 }
7497
7498 let row_limit_opt = self.chart_modal.row_limit;
7499 let row_limit = self.chart_modal.effective_row_limit();
7500 let cache_matches = self.chart_cache.xy.as_ref().is_some_and(|c| {
7501 c.x_column == *x_column
7502 && c.y_columns == y_columns
7503 && c.row_limit == row_limit_opt
7504 });
7505
7506 let (series_vec, x_axis_kind_export, from_cache) = if cache_matches {
7507 if let Some(cache) = self.chart_cache.xy.as_ref() {
7508 let pts = if self.chart_modal.log_scale {
7509 cache.series_log.as_ref().cloned().unwrap_or_else(|| {
7510 cache
7511 .series
7512 .iter()
7513 .map(|s| {
7514 s.iter().map(|&(x, y)| (x, y.max(0.0).ln_1p())).collect()
7515 })
7516 .collect()
7517 })
7518 } else {
7519 cache.series.clone()
7520 };
7521 (pts, cache.x_axis_kind, true)
7522 } else {
7523 let r = chart_data::prepare_chart_data(
7524 &state.lf,
7525 &state.schema,
7526 x_column,
7527 &y_columns,
7528 row_limit,
7529 )?;
7530 (r.series, r.x_axis_kind, false)
7531 }
7532 } else {
7533 let r = chart_data::prepare_chart_data(
7534 &state.lf,
7535 &state.schema,
7536 x_column,
7537 &y_columns,
7538 row_limit,
7539 )?;
7540 (r.series, r.x_axis_kind, false)
7541 };
7542
7543 let log_scale = self.chart_modal.log_scale;
7544 let series: Vec<ChartExportSeries> = series_vec
7545 .iter()
7546 .zip(y_columns.iter())
7547 .filter(|(points, _)| !points.is_empty())
7548 .map(|(points, name)| {
7549 let pts = if log_scale && !from_cache {
7550 points
7551 .iter()
7552 .map(|&(x, y)| (x, y.max(0.0).ln_1p()))
7553 .collect()
7554 } else {
7555 points.clone()
7556 };
7557 ChartExportSeries {
7558 name: name.clone(),
7559 points: pts,
7560 }
7561 })
7562 .collect();
7563
7564 if series.is_empty() {
7565 return Err(color_eyre::eyre::eyre!("No valid data points to export"));
7566 }
7567
7568 let mut all_x_min = f64::INFINITY;
7569 let mut all_x_max = f64::NEG_INFINITY;
7570 let mut all_y_min = f64::INFINITY;
7571 let mut all_y_max = f64::NEG_INFINITY;
7572 for s in &series {
7573 for &(x, y) in &s.points {
7574 all_x_min = all_x_min.min(x);
7575 all_x_max = all_x_max.max(x);
7576 all_y_min = all_y_min.min(y);
7577 all_y_max = all_y_max.max(y);
7578 }
7579 }
7580
7581 let chart_type = self.chart_modal.chart_type;
7582 let y_starts_at_zero = self.chart_modal.y_starts_at_zero;
7583 let y_min_bounds = if chart_type == ChartType::Bar {
7584 0.0_f64.min(all_y_min)
7585 } else if y_starts_at_zero {
7586 0.0
7587 } else {
7588 all_y_min
7589 };
7590 let y_max_bounds = if all_y_max > y_min_bounds {
7591 all_y_max
7592 } else {
7593 y_min_bounds + 1.0
7594 };
7595 let x_min_bounds = if all_x_max > all_x_min {
7596 all_x_min
7597 } else {
7598 all_x_min - 0.5
7599 };
7600 let x_max_bounds = if all_x_max > all_x_min {
7601 all_x_max
7602 } else {
7603 all_x_min + 0.5
7604 };
7605
7606 let x_label = x_column.to_string();
7607 let y_label = y_columns.join(", ");
7608 let bounds = ChartExportBounds {
7609 x_min: x_min_bounds,
7610 x_max: x_max_bounds,
7611 y_min: y_min_bounds,
7612 y_max: y_max_bounds,
7613 x_label: x_label.clone(),
7614 y_label: y_label.clone(),
7615 x_axis_kind: x_axis_kind_export,
7616 log_scale: self.chart_modal.log_scale,
7617 chart_title,
7618 };
7619
7620 match format {
7621 ChartExportFormat::Png => {
7622 write_chart_png(path, &series, chart_type, &bounds, (width, height))
7623 }
7624 ChartExportFormat::Eps => write_chart_eps(path, &series, chart_type, &bounds),
7625 }
7626 }
7627 ChartKind::Histogram => {
7628 let column = self
7629 .chart_modal
7630 .effective_hist_column()
7631 .ok_or_else(|| color_eyre::eyre::eyre!("No histogram column selected"))?;
7632 let row_limit = self.chart_modal.effective_row_limit();
7633 let data = if let Some(c) = self.chart_cache.histogram.as_ref().filter(|c| {
7634 c.column == column
7635 && c.bins == self.chart_modal.hist_bins
7636 && c.row_limit == self.chart_modal.row_limit
7637 }) {
7638 c.data.clone()
7639 } else {
7640 chart_data::prepare_histogram_data(
7641 &state.lf,
7642 &column,
7643 self.chart_modal.hist_bins,
7644 row_limit,
7645 )?
7646 };
7647 if data.bins.is_empty() {
7648 return Err(color_eyre::eyre::eyre!("No valid data points to export"));
7649 }
7650 let points: Vec<(f64, f64)> =
7651 data.bins.iter().map(|b| (b.center, b.count)).collect();
7652 let series = vec![ChartExportSeries {
7653 name: column.clone(),
7654 points,
7655 }];
7656 let x_max = if data.x_max > data.x_min {
7657 data.x_max
7658 } else {
7659 data.x_min + 1.0
7660 };
7661 let y_max = if data.max_count > 0.0 {
7662 data.max_count
7663 } else {
7664 1.0
7665 };
7666 let bounds = ChartExportBounds {
7667 x_min: data.x_min,
7668 x_max,
7669 y_min: 0.0,
7670 y_max,
7671 x_label: column.clone(),
7672 y_label: "Count".to_string(),
7673 x_axis_kind: chart_data::XAxisTemporalKind::Numeric,
7674 log_scale: false,
7675 chart_title,
7676 };
7677 match format {
7678 ChartExportFormat::Png => {
7679 write_chart_png(path, &series, ChartType::Bar, &bounds, (width, height))
7680 }
7681 ChartExportFormat::Eps => {
7682 write_chart_eps(path, &series, ChartType::Bar, &bounds)
7683 }
7684 }
7685 }
7686 ChartKind::BoxPlot => {
7687 let column = self
7688 .chart_modal
7689 .effective_box_column()
7690 .ok_or_else(|| color_eyre::eyre::eyre!("No box plot column selected"))?;
7691 let row_limit = self.chart_modal.effective_row_limit();
7692 let data = if let Some(c) = self
7693 .chart_cache
7694 .box_plot
7695 .as_ref()
7696 .filter(|c| c.column == column && c.row_limit == self.chart_modal.row_limit)
7697 {
7698 c.data.clone()
7699 } else {
7700 chart_data::prepare_box_plot_data(
7701 &state.lf,
7702 std::slice::from_ref(&column),
7703 row_limit,
7704 )?
7705 };
7706 if data.stats.is_empty() {
7707 return Err(color_eyre::eyre::eyre!("No valid data points to export"));
7708 }
7709 let bounds = BoxPlotExportBounds {
7710 y_min: data.y_min,
7711 y_max: data.y_max,
7712 x_labels: vec![column.clone()],
7713 x_label: "Columns".to_string(),
7714 y_label: "Value".to_string(),
7715 chart_title,
7716 };
7717 match format {
7718 ChartExportFormat::Png => {
7719 write_box_plot_png(path, &data, &bounds, (width, height))
7720 }
7721 ChartExportFormat::Eps => write_box_plot_eps(path, &data, &bounds),
7722 }
7723 }
7724 ChartKind::Kde => {
7725 let column = self
7726 .chart_modal
7727 .effective_kde_column()
7728 .ok_or_else(|| color_eyre::eyre::eyre!("No KDE column selected"))?;
7729 let row_limit = self.chart_modal.effective_row_limit();
7730 let data = if let Some(c) = self.chart_cache.kde.as_ref().filter(|c| {
7731 c.column == column
7732 && c.bandwidth_factor == self.chart_modal.kde_bandwidth_factor
7733 && c.row_limit == self.chart_modal.row_limit
7734 }) {
7735 c.data.clone()
7736 } else {
7737 chart_data::prepare_kde_data(
7738 &state.lf,
7739 std::slice::from_ref(&column),
7740 self.chart_modal.kde_bandwidth_factor,
7741 row_limit,
7742 )?
7743 };
7744 if data.series.is_empty() {
7745 return Err(color_eyre::eyre::eyre!("No valid data points to export"));
7746 }
7747 let series: Vec<ChartExportSeries> = data
7748 .series
7749 .iter()
7750 .map(|s| ChartExportSeries {
7751 name: s.name.clone(),
7752 points: s.points.clone(),
7753 })
7754 .collect();
7755 let bounds = ChartExportBounds {
7756 x_min: data.x_min,
7757 x_max: data.x_max,
7758 y_min: 0.0,
7759 y_max: data.y_max,
7760 x_label: column.clone(),
7761 y_label: "Density".to_string(),
7762 x_axis_kind: chart_data::XAxisTemporalKind::Numeric,
7763 log_scale: false,
7764 chart_title,
7765 };
7766 match format {
7767 ChartExportFormat::Png => {
7768 write_chart_png(path, &series, ChartType::Line, &bounds, (width, height))
7769 }
7770 ChartExportFormat::Eps => {
7771 write_chart_eps(path, &series, ChartType::Line, &bounds)
7772 }
7773 }
7774 }
7775 ChartKind::Heatmap => {
7776 let x_column = self
7777 .chart_modal
7778 .effective_heatmap_x_column()
7779 .ok_or_else(|| color_eyre::eyre::eyre!("No heatmap X column selected"))?;
7780 let y_column = self
7781 .chart_modal
7782 .effective_heatmap_y_column()
7783 .ok_or_else(|| color_eyre::eyre::eyre!("No heatmap Y column selected"))?;
7784 let row_limit = self.chart_modal.effective_row_limit();
7785 let data = if let Some(c) = self.chart_cache.heatmap.as_ref().filter(|c| {
7786 c.x_column == *x_column
7787 && c.y_column == *y_column
7788 && c.bins == self.chart_modal.heatmap_bins
7789 && c.row_limit == self.chart_modal.row_limit
7790 }) {
7791 c.data.clone()
7792 } else {
7793 chart_data::prepare_heatmap_data(
7794 &state.lf,
7795 &x_column,
7796 &y_column,
7797 self.chart_modal.heatmap_bins,
7798 row_limit,
7799 )?
7800 };
7801 if data.counts.is_empty() || data.max_count <= 0.0 {
7802 return Err(color_eyre::eyre::eyre!("No valid data points to export"));
7803 }
7804 let bounds = ChartExportBounds {
7805 x_min: data.x_min,
7806 x_max: data.x_max,
7807 y_min: data.y_min,
7808 y_max: data.y_max,
7809 x_label: x_column.clone(),
7810 y_label: y_column.clone(),
7811 x_axis_kind: chart_data::XAxisTemporalKind::Numeric,
7812 log_scale: false,
7813 chart_title,
7814 };
7815 match format {
7816 ChartExportFormat::Png => {
7817 write_heatmap_png(path, &data, &bounds, (width, height))
7818 }
7819 ChartExportFormat::Eps => write_heatmap_eps(path, &data, &bounds),
7820 }
7821 }
7822 }
7823 }
7824
7825 fn apply_template(&mut self, template: &Template) -> Result<()> {
7826 let saved_state = self
7828 .data_table_state
7829 .as_ref()
7830 .map(|state| TemplateApplicationState {
7831 lf: state.lf.clone(),
7832 schema: state.schema.clone(),
7833 active_query: state.active_query.clone(),
7834 active_sql_query: state.get_active_sql_query().to_string(),
7835 active_fuzzy_query: state.get_active_fuzzy_query().to_string(),
7836 filters: state.get_filters().to_vec(),
7837 sort_columns: state.get_sort_columns().to_vec(),
7838 sort_ascending: state.get_sort_ascending(),
7839 column_order: state.get_column_order().to_vec(),
7840 locked_columns_count: state.locked_columns_count(),
7841 });
7842 let saved_active_template_id = self.active_template_id.clone();
7843
7844 if let Some(state) = &mut self.data_table_state {
7845 state.error = None;
7846
7847 let sql_trimmed = template.settings.sql_query.as_deref().unwrap_or("").trim();
7849 let query_opt = template.settings.query.as_deref().filter(|s| !s.is_empty());
7850 let fuzzy_trimmed = template
7851 .settings
7852 .fuzzy_query
7853 .as_deref()
7854 .unwrap_or("")
7855 .trim();
7856
7857 if !sql_trimmed.is_empty() {
7858 state.sql_query(template.settings.sql_query.clone().unwrap_or_default());
7859 } else if let Some(q) = query_opt {
7860 state.query(q.to_string());
7861 }
7862 if let Some(error) = state.error.clone() {
7863 if let Some(saved) = saved_state {
7864 self.restore_state(saved);
7865 }
7866 self.active_template_id = saved_active_template_id;
7867 return Err(color_eyre::eyre::eyre!(
7868 "{}",
7869 crate::error_display::user_message_from_polars(&error)
7870 ));
7871 }
7872
7873 if !fuzzy_trimmed.is_empty() {
7874 state.fuzzy_search(template.settings.fuzzy_query.clone().unwrap_or_default());
7875 if let Some(error) = state.error.clone() {
7876 if let Some(saved) = saved_state {
7877 self.restore_state(saved);
7878 }
7879 self.active_template_id = saved_active_template_id;
7880 return Err(color_eyre::eyre::eyre!(
7881 "{}",
7882 crate::error_display::user_message_from_polars(&error)
7883 ));
7884 }
7885 }
7886
7887 if !template.settings.filters.is_empty() {
7889 state.filter(template.settings.filters.clone());
7890 let error_opt = state.error.clone();
7892 if let Some(error) = error_opt {
7893 if let Some(saved) = saved_state {
7895 self.restore_state(saved);
7896 }
7897 self.active_template_id = saved_active_template_id;
7898 return Err(color_eyre::eyre::eyre!("{}", error));
7899 }
7900 }
7901
7902 if !template.settings.sort_columns.is_empty() {
7904 state.sort(
7905 template.settings.sort_columns.clone(),
7906 template.settings.sort_ascending,
7907 );
7908 let error_opt = state.error.clone();
7910 if let Some(error) = error_opt {
7911 if let Some(saved) = saved_state {
7913 self.restore_state(saved);
7914 }
7915 self.active_template_id = saved_active_template_id;
7916 return Err(color_eyre::eyre::eyre!("{}", error));
7917 }
7918 }
7919
7920 if let Some(ref spec) = template.settings.pivot {
7922 if let Err(e) = state.pivot(spec) {
7923 if let Some(saved) = saved_state {
7924 self.restore_state(saved);
7925 }
7926 self.active_template_id = saved_active_template_id;
7927 return Err(color_eyre::eyre::eyre!(
7928 "{}",
7929 crate::error_display::user_message_from_report(&e, None)
7930 ));
7931 }
7932 } else if let Some(ref spec) = template.settings.melt {
7933 if let Err(e) = state.melt(spec) {
7934 if let Some(saved) = saved_state {
7935 self.restore_state(saved);
7936 }
7937 self.active_template_id = saved_active_template_id;
7938 return Err(color_eyre::eyre::eyre!(
7939 "{}",
7940 crate::error_display::user_message_from_report(&e, None)
7941 ));
7942 }
7943 }
7944
7945 if !template.settings.column_order.is_empty() {
7947 state.set_column_order(template.settings.column_order.clone());
7948 let error_opt = state.error.clone();
7950 if let Some(error) = error_opt {
7951 if let Some(saved) = saved_state {
7953 self.restore_state(saved);
7954 }
7955 self.active_template_id = saved_active_template_id;
7956 return Err(color_eyre::eyre::eyre!("{}", error));
7957 }
7958 state.set_locked_columns(template.settings.locked_columns_count);
7959 let error_opt = state.error.clone();
7961 if let Some(error) = error_opt {
7962 if let Some(saved) = saved_state {
7964 self.restore_state(saved);
7965 }
7966 self.active_template_id = saved_active_template_id;
7967 return Err(color_eyre::eyre::eyre!("{}", error));
7968 }
7969 }
7970 }
7971
7972 if let Some(path) = &self.path {
7977 let mut updated_template = template.clone();
7978 updated_template.last_used = Some(std::time::SystemTime::now());
7979 updated_template.usage_count += 1;
7980 updated_template.last_matched_file = Some(path.clone());
7981
7982 let _ = self.template_manager.save_template(&updated_template);
7984 }
7985
7986 self.active_template_id = Some(template.id.clone());
7988
7989 Ok(())
7990 }
7991
7992 fn format_export_error(error: &color_eyre::eyre::Report, path: &Path) -> String {
7994 use std::io;
7995
7996 for cause in error.chain() {
7997 if let Some(io_err) = cause.downcast_ref::<io::Error>() {
7998 let msg = crate::error_display::user_message_from_io(io_err, None);
7999 return format!("Cannot write to {}: {}", path.display(), msg);
8000 }
8001 if let Some(pe) = cause.downcast_ref::<polars::prelude::PolarsError>() {
8002 let msg = crate::error_display::user_message_from_polars(pe);
8003 return format!("Export failed: {}", msg);
8004 }
8005 }
8006 let error_str = error.to_string();
8007 let first_line = error_str.lines().next().unwrap_or("Unknown error").trim();
8008 format!("Export failed: {}", first_line)
8009 }
8010
8011 fn export_data_from_df(
8013 df: &mut DataFrame,
8014 path: &Path,
8015 format: ExportFormat,
8016 options: &ExportOptions,
8017 ) -> Result<()> {
8018 use polars::prelude::*;
8019 use std::fs::File;
8020 use std::io::{BufWriter, Write};
8021
8022 match format {
8023 ExportFormat::Csv => {
8024 use polars::prelude::CsvWriter;
8025 if let Some(compression) = options.csv_compression {
8026 let file = File::create(path)?;
8028 let writer: Box<dyn Write> = match compression {
8029 CompressionFormat::Gzip => Box::new(flate2::write::GzEncoder::new(
8030 file,
8031 flate2::Compression::default(),
8032 )),
8033 CompressionFormat::Zstd => {
8034 Box::new(zstd::Encoder::new(file, 0)?.auto_finish())
8035 }
8036 CompressionFormat::Bzip2 => Box::new(bzip2::write::BzEncoder::new(
8037 file,
8038 bzip2::Compression::default(),
8039 )),
8040 CompressionFormat::Xz => {
8041 Box::new(xz2::write::XzEncoder::new(
8042 file, 6, ))
8044 }
8045 };
8046 CsvWriter::new(writer)
8047 .with_separator(options.csv_delimiter)
8048 .include_header(options.csv_include_header)
8049 .finish(df)?;
8050 } else {
8051 let file = File::create(path)?;
8053 CsvWriter::new(file)
8054 .with_separator(options.csv_delimiter)
8055 .include_header(options.csv_include_header)
8056 .finish(df)?;
8057 }
8058 }
8059 ExportFormat::Parquet => {
8060 use polars::prelude::ParquetWriter;
8061 let file = File::create(path)?;
8062 let mut writer = BufWriter::new(file);
8063 ParquetWriter::new(&mut writer).finish(df)?;
8064 }
8065 ExportFormat::Json => {
8066 use polars::prelude::JsonWriter;
8067 if let Some(compression) = options.json_compression {
8068 let file = File::create(path)?;
8070 let writer: Box<dyn Write> = match compression {
8071 CompressionFormat::Gzip => Box::new(flate2::write::GzEncoder::new(
8072 file,
8073 flate2::Compression::default(),
8074 )),
8075 CompressionFormat::Zstd => {
8076 Box::new(zstd::Encoder::new(file, 0)?.auto_finish())
8077 }
8078 CompressionFormat::Bzip2 => Box::new(bzip2::write::BzEncoder::new(
8079 file,
8080 bzip2::Compression::default(),
8081 )),
8082 CompressionFormat::Xz => {
8083 Box::new(xz2::write::XzEncoder::new(
8084 file, 6, ))
8086 }
8087 };
8088 JsonWriter::new(writer)
8089 .with_json_format(JsonFormat::Json)
8090 .finish(df)?;
8091 } else {
8092 let file = File::create(path)?;
8094 JsonWriter::new(file)
8095 .with_json_format(JsonFormat::Json)
8096 .finish(df)?;
8097 }
8098 }
8099 ExportFormat::Ndjson => {
8100 use polars::prelude::{JsonFormat, JsonWriter};
8101 if let Some(compression) = options.ndjson_compression {
8102 let file = File::create(path)?;
8104 let writer: Box<dyn Write> = match compression {
8105 CompressionFormat::Gzip => Box::new(flate2::write::GzEncoder::new(
8106 file,
8107 flate2::Compression::default(),
8108 )),
8109 CompressionFormat::Zstd => {
8110 Box::new(zstd::Encoder::new(file, 0)?.auto_finish())
8111 }
8112 CompressionFormat::Bzip2 => Box::new(bzip2::write::BzEncoder::new(
8113 file,
8114 bzip2::Compression::default(),
8115 )),
8116 CompressionFormat::Xz => {
8117 Box::new(xz2::write::XzEncoder::new(
8118 file, 6, ))
8120 }
8121 };
8122 JsonWriter::new(writer)
8123 .with_json_format(JsonFormat::JsonLines)
8124 .finish(df)?;
8125 } else {
8126 let file = File::create(path)?;
8128 JsonWriter::new(file)
8129 .with_json_format(JsonFormat::JsonLines)
8130 .finish(df)?;
8131 }
8132 }
8133 ExportFormat::Ipc => {
8134 use polars::prelude::IpcWriter;
8135 let file = File::create(path)?;
8136 let mut writer = BufWriter::new(file);
8137 IpcWriter::new(&mut writer).finish(df)?;
8138 }
8139 ExportFormat::Avro => {
8140 use polars::io::avro::AvroWriter;
8141 let file = File::create(path)?;
8142 let mut writer = BufWriter::new(file);
8143 AvroWriter::new(&mut writer).finish(df)?;
8144 }
8145 }
8146
8147 Ok(())
8148 }
8149
8150 #[allow(dead_code)] fn export_data(
8152 state: &DataTableState,
8153 path: &Path,
8154 format: ExportFormat,
8155 options: &ExportOptions,
8156 ) -> Result<()> {
8157 let mut df = crate::statistics::collect_lazy(state.lf.clone(), state.polars_streaming)?;
8158 Self::export_data_from_df(&mut df, path, format, options)
8159 }
8160
8161 fn restore_state(&mut self, saved: TemplateApplicationState) {
8162 if let Some(state) = &mut self.data_table_state {
8163 let saved_lf = saved.lf.clone();
8165 let saved_schema = saved.schema.clone();
8166
8167 state.lf = saved.lf;
8170 state.schema = saved.schema;
8171 state.active_query = saved.active_query;
8172 state.active_sql_query = saved.active_sql_query;
8173 state.active_fuzzy_query = saved.active_fuzzy_query;
8174 state.error = None;
8176 state.filter(saved.filters.clone());
8180 if state.error.is_none() {
8181 state.sort(saved.sort_columns.clone(), saved.sort_ascending);
8182 }
8183 if state.error.is_none() {
8184 state.set_column_order(saved.column_order.clone());
8185 }
8186 if state.error.is_none() {
8187 state.set_locked_columns(saved.locked_columns_count);
8188 }
8189 state.lf = saved_lf;
8191 state.schema = saved_schema;
8192 state.collect();
8193 }
8194 }
8195
8196 pub fn create_template_from_current_state(
8197 &mut self,
8198 name: String,
8199 description: Option<String>,
8200 match_criteria: template::MatchCriteria,
8201 ) -> Result<template::Template> {
8202 let settings = if let Some(state) = &self.data_table_state {
8203 let (query, sql_query, fuzzy_query) = active_query_settings(
8204 state.get_active_query(),
8205 state.get_active_sql_query(),
8206 state.get_active_fuzzy_query(),
8207 );
8208 template::TemplateSettings {
8209 query,
8210 sql_query,
8211 fuzzy_query,
8212 filters: state.get_filters().to_vec(),
8213 sort_columns: state.get_sort_columns().to_vec(),
8214 sort_ascending: state.get_sort_ascending(),
8215 column_order: state.get_column_order().to_vec(),
8216 locked_columns_count: state.locked_columns_count(),
8217 pivot: state.last_pivot_spec().cloned(),
8218 melt: state.last_melt_spec().cloned(),
8219 }
8220 } else {
8221 template::TemplateSettings {
8222 query: None,
8223 sql_query: None,
8224 fuzzy_query: None,
8225 filters: Vec::new(),
8226 sort_columns: Vec::new(),
8227 sort_ascending: true,
8228 column_order: Vec::new(),
8229 locked_columns_count: 0,
8230 pivot: None,
8231 melt: None,
8232 }
8233 };
8234
8235 self.template_manager
8236 .create_template(name, description, match_criteria, settings)
8237 }
8238
8239 fn get_help_info(&self) -> (String, String) {
8240 let (title, content) = match self.input_mode {
8241 InputMode::Normal => ("Main View Help", help_strings::main_view()),
8242 InputMode::Editing => match self.input_type {
8243 Some(InputType::Search) => ("Query Help", help_strings::query()),
8244 _ => ("Editing Help", help_strings::editing()),
8245 },
8246 InputMode::SortFilter => ("Sort & Filter Help", help_strings::sort_filter()),
8247 InputMode::PivotMelt => ("Pivot / Melt Help", help_strings::pivot_melt()),
8248 InputMode::Export => ("Export Help", help_strings::export()),
8249 InputMode::Info => ("Info Panel Help", help_strings::info_panel()),
8250 InputMode::Chart => ("Chart Help", help_strings::chart()),
8251 };
8252 (title.to_string(), content.to_string())
8253 }
8254}
8255
8256impl Widget for &mut App {
8257 fn render(self, area: Rect, buf: &mut Buffer) {
8258 self.debug.num_frames += 1;
8259 if self.debug.enabled {
8260 self.debug.show_help_at_render = self.show_help;
8261 }
8262
8263 use crate::render::context::RenderContext;
8264 use crate::render::layout::{app_layout, centered_rect_loading};
8265 use crate::render::main_view::MainViewContent;
8266
8267 let ctx = RenderContext::from_theme_and_config(
8268 &self.theme,
8269 self.table_cell_padding,
8270 self.column_colors,
8271 );
8272
8273 let main_view_content = MainViewContent::from_app_state(
8274 self.analysis_modal.active,
8275 self.input_mode == InputMode::Chart,
8276 );
8277
8278 Clear.render(area, buf);
8279 let background_color = self.color("background");
8280 Block::default()
8281 .style(Style::default().bg(background_color))
8282 .render(area, buf);
8283
8284 let app_layout = app_layout(area, self.debug.enabled);
8285 let main_area = app_layout.main_view;
8286 Clear.render(main_area, buf);
8287
8288 crate::render::main_view_render::render_main_view(area, main_area, buf, self, &ctx);
8289
8290 if matches!(self.loading_state, LoadingState::Loading { .. }) {
8292 if let LoadingState::Loading {
8293 current_phase,
8294 progress_percent,
8295 ..
8296 } = &self.loading_state
8297 {
8298 let popover_rect = centered_rect_loading(area);
8299 crate::render::overlays::render_loading_gauge(
8300 popover_rect,
8301 buf,
8302 "Loading",
8303 current_phase,
8304 *progress_percent,
8305 ctx.modal_border,
8306 ctx.primary_chart_series_color,
8307 );
8308 }
8309 }
8310 if matches!(self.loading_state, LoadingState::Exporting { .. }) {
8311 if let LoadingState::Exporting {
8312 file_path,
8313 current_phase,
8314 progress_percent,
8315 } = &self.loading_state
8316 {
8317 let label = format!("{}: {}", current_phase, file_path.display());
8318 crate::render::overlays::render_loading_gauge(
8319 area,
8320 buf,
8321 "Exporting",
8322 &label,
8323 *progress_percent,
8324 ctx.modal_border,
8325 ctx.primary_chart_series_color,
8326 );
8327 }
8328 }
8329
8330 if self.confirmation_modal.active {
8331 crate::render::overlays::render_confirmation_modal(
8332 area,
8333 buf,
8334 &self.confirmation_modal,
8335 &ctx,
8336 );
8337 }
8338 if self.success_modal.active {
8339 crate::render::overlays::render_success_modal(area, buf, &self.success_modal, &ctx);
8340 }
8341 if self.error_modal.active {
8342 crate::render::overlays::render_error_modal(area, buf, &self.error_modal, &ctx);
8343 }
8344 if self.show_help
8345 || (self.template_modal.active && self.template_modal.show_help)
8346 || (self.analysis_modal.active && self.analysis_modal.show_help)
8347 {
8348 let (title, text): (String, String) =
8349 if self.analysis_modal.active && self.analysis_modal.show_help {
8350 crate::render::analysis_view::help_title_and_text(&self.analysis_modal)
8351 } else if self.template_modal.active {
8352 (
8353 "Template Help".to_string(),
8354 help_strings::template().to_string(),
8355 )
8356 } else {
8357 let (t, txt) = self.get_help_info();
8358 (t.to_string(), txt.to_string())
8359 };
8360 crate::render::overlays::render_help_overlay(
8361 area,
8362 buf,
8363 &title,
8364 &text,
8365 &mut self.help_scroll,
8366 &ctx,
8367 );
8368 }
8369
8370 let row_count = self.data_table_state.as_ref().map(|s| s.num_rows);
8371 let use_unicode_throbber = std::env::var("LANG")
8372 .map(|l| l.to_uppercase().contains("UTF-8"))
8373 .unwrap_or(false);
8374 let mut controls = Controls::from_context(row_count.unwrap_or(0), &ctx)
8375 .with_unicode_throbber(use_unicode_throbber);
8376
8377 match crate::render::main_view::control_bar_spec(self, main_view_content) {
8378 crate::render::main_view::ControlBarSpec::Datatable {
8379 dimmed,
8380 query_active,
8381 } => {
8382 controls = controls.with_dimmed(dimmed).with_query_active(query_active);
8383 }
8384 crate::render::main_view::ControlBarSpec::Custom(pairs) => {
8385 controls = controls.with_custom_controls(pairs);
8386 }
8387 }
8388
8389 if self.busy {
8390 self.throbber_frame = self.throbber_frame.wrapping_add(1);
8391 }
8392 controls = controls.with_busy(self.busy, self.throbber_frame);
8393 controls.render(app_layout.control_bar, buf);
8394 if let Some(debug_area) = app_layout.debug {
8395 self.debug.render(debug_area, buf);
8396 }
8397 }
8398}
8399
8400pub fn run(input: RunInput, config: Option<AppConfig>) -> Result<()> {
8402 use std::io::Write;
8403 use std::sync::{mpsc, Mutex, Once};
8404
8405 let config = match config {
8406 Some(c) => c,
8407 None => AppConfig::load(APP_NAME)?,
8408 };
8409
8410 let opts = match &input {
8411 RunInput::Paths(_, o) => o.clone(),
8412 RunInput::LazyFrame(_, o) => o.clone(),
8413 };
8414
8415 let theme = Theme::from_config(&config.theme)
8416 .or_else(|e| Theme::from_config(&AppConfig::default().theme).map_err(|_| e))?;
8417
8418 static COLOR_EYRE_INIT: Once = Once::new();
8421 static INSTALL_RESULT: Mutex<Option<Result<(), color_eyre::Report>>> = Mutex::new(None);
8422 COLOR_EYRE_INIT.call_once(|| {
8423 *INSTALL_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some(color_eyre::install());
8424 });
8425 if let Some(Err(e)) = INSTALL_RESULT
8426 .lock()
8427 .unwrap_or_else(|e| e.into_inner())
8428 .as_ref()
8429 {
8430 return Err(color_eyre::eyre::eyre!(e.to_string()));
8431 }
8432 if let RunInput::Paths(ref paths, _) = input {
8434 if paths.is_empty() {
8435 return Err(color_eyre::eyre::eyre!("At least one path is required"));
8436 }
8437 for path in paths {
8438 let s = path.to_string_lossy();
8439 let is_remote = s.starts_with("s3://")
8440 || s.starts_with("gs://")
8441 || s.starts_with("http://")
8442 || s.starts_with("https://");
8443 let is_glob = s.contains('*');
8444 if !is_remote && !is_glob && !path.exists() {
8445 return Err(std::io::Error::new(
8446 std::io::ErrorKind::NotFound,
8447 format!("File not found: {}", path.display()),
8448 )
8449 .into());
8450 }
8451 }
8452 }
8453 let mut terminal = ratatui::try_init().map_err(|e| {
8454 color_eyre::eyre::eyre!(
8455 "datui requires an interactive terminal (TTY). No terminal detected: {}. \
8456 Run from a terminal or ensure stdout is connected to a TTY.",
8457 e
8458 )
8459 })?;
8460 let (tx, rx) = mpsc::channel::<AppEvent>();
8461 let mut app = App::new_with_config(tx.clone(), theme, config.clone());
8462 if opts.debug {
8463 app.enable_debug();
8464 }
8465
8466 terminal.draw(|frame| frame.render_widget(&mut app, frame.area()))?;
8467
8468 match input {
8469 RunInput::Paths(paths, opts) => {
8470 tx.send(AppEvent::Open(paths, opts))?;
8471 }
8472 RunInput::LazyFrame(lf, opts) => {
8473 app.set_loading_phase("Scanning input", 10);
8476 terminal.draw(|frame| frame.render_widget(&mut app, frame.area()))?;
8477 let _ = std::io::stdout().flush();
8478 std::thread::sleep(std::time::Duration::from_millis(150));
8480 tx.send(AppEvent::OpenLazyFrame(lf, opts))?;
8481 }
8482 }
8483
8484 loop {
8487 let event = match rx.recv_timeout(std::time::Duration::from_millis(50)) {
8488 Ok(ev) => ev,
8489 Err(mpsc::RecvTimeoutError::Timeout) => break,
8490 Err(mpsc::RecvTimeoutError::Disconnected) => break,
8491 };
8492 match event {
8493 AppEvent::Exit => break,
8494 AppEvent::Crash(msg) => {
8495 ratatui::restore();
8496 return Err(color_eyre::eyre::eyre!(msg));
8497 }
8498 ev => {
8499 if let Some(next) = app.event(&ev) {
8500 let _ = tx.send(next);
8501 }
8502 terminal.draw(|frame| frame.render_widget(&mut app, frame.area()))?;
8503 let _ = std::io::stdout().flush();
8504 if matches!(ev, AppEvent::DoLoadSchema(..)) {
8507 break;
8508 }
8509 }
8510 }
8511 }
8512
8513 loop {
8514 if crossterm::event::poll(std::time::Duration::from_millis(
8515 config.performance.event_poll_interval_ms,
8516 ))? {
8517 match crossterm::event::read()? {
8518 crossterm::event::Event::Key(key) => {
8519 if key.is_press() {
8520 tx.send(AppEvent::Key(key))?
8521 }
8522 }
8523 crossterm::event::Event::Resize(cols, rows) => {
8524 tx.send(AppEvent::Resize(cols, rows))?
8525 }
8526 _ => {}
8527 }
8528 }
8529
8530 let updated = match rx.recv_timeout(std::time::Duration::from_millis(0)) {
8531 Ok(event) => {
8532 match event {
8533 AppEvent::Exit => break,
8534 AppEvent::Crash(msg) => {
8535 ratatui::restore();
8536 return Err(color_eyre::eyre::eyre!(msg));
8537 }
8538 event => {
8539 if let Some(next) = app.event(&event) {
8540 tx.send(next)?;
8541 }
8542 }
8543 }
8544 true
8545 }
8546 Err(mpsc::RecvTimeoutError::Timeout) => false,
8547 Err(mpsc::RecvTimeoutError::Disconnected) => break,
8548 };
8549
8550 if updated {
8551 terminal.draw(|frame| frame.render_widget(&mut app, frame.area()))?;
8552 if app.should_drain_keys() {
8553 while crossterm::event::poll(std::time::Duration::from_millis(0))? {
8554 let _ = crossterm::event::read();
8555 }
8556 app.clear_drain_keys_request();
8557 }
8558 }
8559 }
8560
8561 ratatui::restore();
8562 Ok(())
8563}