1use crate::widgets::text_input::TextInput;
4use crate::CompressionFormat;
5
6#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
7pub enum ExportFormat {
8 #[default]
9 Csv,
10 Parquet,
11 Json,
12 Ndjson,
13 Ipc,
15 Avro,
16}
17
18impl ExportFormat {
19 pub const ALL: [Self; 6] = [
20 Self::Csv,
21 Self::Parquet,
22 Self::Json,
23 Self::Ndjson,
24 Self::Ipc,
25 Self::Avro,
26 ];
27
28 pub fn as_str(self) -> &'static str {
29 match self {
30 Self::Csv => "CSV",
31 Self::Parquet => "Parquet",
32 Self::Json => "JSON",
33 Self::Ndjson => "NDJSON",
34 Self::Ipc => "Arrow",
35 Self::Avro => "Avro",
36 }
37 }
38
39 pub fn extension(self) -> &'static str {
40 match self {
41 Self::Csv => "csv",
42 Self::Parquet => "parquet",
43 Self::Json => "json",
44 Self::Ndjson => "jsonl",
45 Self::Ipc => "arrow",
46 Self::Avro => "avro",
47 }
48 }
49
50 pub fn from_extension(ext: &str) -> Option<Self> {
51 match ext.to_lowercase().as_str() {
52 "csv" => Some(Self::Csv),
53 "parquet" => Some(Self::Parquet),
54 "json" => Some(Self::Json),
55 "ndjson" | "jsonl" => Some(Self::Ndjson),
56 "arrow" | "ipc" | "feather" => Some(Self::Ipc),
57 "avro" => Some(Self::Avro),
58 _ => None,
59 }
60 }
61
62 pub fn supports_compression(self) -> bool {
63 matches!(self, Self::Csv | Self::Json | Self::Ndjson)
64 }
65}
66
67#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
68pub enum ExportFocus {
69 #[default]
70 FormatSelector,
71 PathInput,
72 CsvDelimiter,
74 CsvIncludeHeader,
75 CsvCompression,
76 JsonCompression,
78 NdjsonCompression,
80 ExportButton,
82 CancelButton,
83}
84
85pub struct ExportModal {
86 pub active: bool,
87 pub focus: ExportFocus,
88 pub selected_format: ExportFormat,
89 pub path_input: TextInput,
90 pub csv_delimiter_input: TextInput,
92 pub csv_include_header: bool,
93 pub csv_compression: Option<CompressionFormat>,
94 pub json_compression: Option<CompressionFormat>,
96 pub ndjson_compression: Option<CompressionFormat>,
98 pub compression_selection_idx: usize,
100 pub history_limit: usize,
101}
102
103impl ExportModal {
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn open(
109 &mut self,
110 default_format: Option<ExportFormat>,
111 history_limit: usize,
112 theme: &crate::config::Theme,
113 file_delimiter: Option<u8>,
114 config_delimiter: Option<u8>,
115 ) {
116 self.active = true;
117 self.focus = ExportFocus::PathInput;
118 self.history_limit = history_limit;
119 if let Some(format) = default_format {
120 self.selected_format = format;
121 }
122 self.path_input = TextInput::new()
123 .with_history_limit(history_limit)
124 .with_theme(theme);
125 self.path_input.clear();
126 self.csv_delimiter_input = TextInput::new()
127 .with_history_limit(history_limit)
128 .with_theme(theme);
129 let delimiter_char = config_delimiter.or(file_delimiter).unwrap_or(b',');
131 self.csv_delimiter_input
133 .set_value(format!("{}", delimiter_char as char));
134 self.csv_include_header = true;
135 self.csv_compression = None;
136 self.json_compression = None;
137 self.ndjson_compression = None;
138 self.compression_selection_idx = 0;
139 }
140
141 pub fn close(&mut self) {
142 self.active = false;
143 self.focus = ExportFocus::FormatSelector;
144 self.path_input.clear();
145 }
146
147 pub fn next_focus(&mut self) {
148 let new_focus = match self.focus {
149 ExportFocus::FormatSelector => ExportFocus::PathInput,
150 ExportFocus::PathInput => match self.selected_format {
151 ExportFormat::Csv => ExportFocus::CsvDelimiter,
152 ExportFormat::Json => ExportFocus::JsonCompression,
153 ExportFormat::Ndjson => ExportFocus::NdjsonCompression,
154 ExportFormat::Parquet | ExportFormat::Ipc | ExportFormat::Avro => {
155 ExportFocus::ExportButton
156 }
157 },
158 ExportFocus::CsvDelimiter => ExportFocus::CsvIncludeHeader,
159 ExportFocus::CsvIncludeHeader => ExportFocus::CsvCompression,
160 ExportFocus::CsvCompression => ExportFocus::ExportButton,
161 ExportFocus::JsonCompression => ExportFocus::ExportButton,
162 ExportFocus::NdjsonCompression => ExportFocus::ExportButton,
163 ExportFocus::ExportButton => ExportFocus::CancelButton,
164 ExportFocus::CancelButton => ExportFocus::FormatSelector,
165 };
166 self.focus = new_focus;
167 if matches!(
169 self.focus,
170 ExportFocus::CsvCompression
171 | ExportFocus::JsonCompression
172 | ExportFocus::NdjsonCompression
173 ) {
174 self.init_compression_selection();
175 }
176 }
177
178 pub fn prev_focus(&mut self) {
179 let new_focus = match self.focus {
180 ExportFocus::FormatSelector => ExportFocus::CancelButton,
181 ExportFocus::PathInput => ExportFocus::FormatSelector,
182 ExportFocus::CsvDelimiter => ExportFocus::PathInput,
183 ExportFocus::CsvIncludeHeader => ExportFocus::CsvDelimiter,
184 ExportFocus::CsvCompression => ExportFocus::CsvIncludeHeader,
185 ExportFocus::JsonCompression => ExportFocus::PathInput,
186 ExportFocus::NdjsonCompression => ExportFocus::PathInput,
187 ExportFocus::ExportButton => match self.selected_format {
188 ExportFormat::Csv => ExportFocus::CsvCompression,
189 ExportFormat::Json => ExportFocus::JsonCompression,
190 ExportFormat::Ndjson => ExportFocus::NdjsonCompression,
191 ExportFormat::Parquet | ExportFormat::Ipc | ExportFormat::Avro => {
192 ExportFocus::PathInput
193 }
194 },
195 ExportFocus::CancelButton => ExportFocus::ExportButton,
196 };
197 self.focus = new_focus;
198 if matches!(
200 self.focus,
201 ExportFocus::CsvCompression
202 | ExportFocus::JsonCompression
203 | ExportFocus::NdjsonCompression
204 ) {
205 self.init_compression_selection();
206 }
207 }
208
209 pub fn init_compression_selection(&mut self) {
210 const COMPRESSION_OPTIONS: [Option<CompressionFormat>; 5] = [
211 None,
212 Some(CompressionFormat::Gzip),
213 Some(CompressionFormat::Zstd),
214 Some(CompressionFormat::Bzip2),
215 Some(CompressionFormat::Xz),
216 ];
217
218 let compression = match self.focus {
219 ExportFocus::CsvCompression => self.csv_compression,
220 ExportFocus::JsonCompression => self.json_compression,
221 ExportFocus::NdjsonCompression => self.ndjson_compression,
222 _ => return,
223 };
224
225 self.compression_selection_idx = COMPRESSION_OPTIONS
227 .iter()
228 .position(|&opt| opt == compression)
229 .unwrap_or(0);
230 }
231
232 pub fn cycle_compression(&mut self) {
233 const COMPRESSION_OPTIONS: [Option<CompressionFormat>; 5] = [
234 None,
235 Some(CompressionFormat::Gzip),
236 Some(CompressionFormat::Zstd),
237 Some(CompressionFormat::Bzip2),
238 Some(CompressionFormat::Xz),
239 ];
240
241 let compression = match self.focus {
242 ExportFocus::CsvCompression => &mut self.csv_compression,
243 ExportFocus::JsonCompression => &mut self.json_compression,
244 ExportFocus::NdjsonCompression => &mut self.ndjson_compression,
245 _ => return,
246 };
247
248 self.compression_selection_idx =
250 (self.compression_selection_idx + 1) % COMPRESSION_OPTIONS.len();
251 *compression = COMPRESSION_OPTIONS[self.compression_selection_idx];
252 }
253
254 pub fn cycle_compression_backward(&mut self) {
255 const COMPRESSION_OPTIONS: [Option<CompressionFormat>; 5] = [
256 None,
257 Some(CompressionFormat::Gzip),
258 Some(CompressionFormat::Zstd),
259 Some(CompressionFormat::Bzip2),
260 Some(CompressionFormat::Xz),
261 ];
262
263 let compression = match self.focus {
264 ExportFocus::CsvCompression => &mut self.csv_compression,
265 ExportFocus::JsonCompression => &mut self.json_compression,
266 ExportFocus::NdjsonCompression => &mut self.ndjson_compression,
267 _ => return,
268 };
269
270 self.compression_selection_idx = if self.compression_selection_idx == 0 {
272 COMPRESSION_OPTIONS.len() - 1
273 } else {
274 self.compression_selection_idx - 1
275 };
276 *compression = COMPRESSION_OPTIONS[self.compression_selection_idx];
277 }
278
279 pub fn select_compression(&mut self, compression: Option<CompressionFormat>) {
280 match self.focus {
281 ExportFocus::CsvCompression => {
282 self.csv_compression = compression;
283 }
284 ExportFocus::JsonCompression => {
285 self.json_compression = compression;
286 }
287 ExportFocus::NdjsonCompression => {
288 self.ndjson_compression = compression;
289 }
290 _ => {}
291 }
292 }
293}
294
295impl Default for ExportModal {
296 fn default() -> Self {
297 Self {
298 active: false,
299 focus: ExportFocus::FormatSelector,
300 selected_format: ExportFormat::Csv,
301 path_input: TextInput::new(),
302 csv_delimiter_input: TextInput::new(),
303 csv_include_header: true,
304 csv_compression: None,
305 json_compression: None,
306 ndjson_compression: None,
307 compression_selection_idx: 0,
308 history_limit: 1000,
309 }
310 }
311}