1use crate::export_modal::{ExportFocus, ExportFormat, ExportModal};
4use crate::CompressionFormat;
5use ratatui::layout::{Constraint, Direction, Layout, Rect};
6use ratatui::style::{Color, Style};
7use ratatui::text::{Line, Span};
8use ratatui::widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph, Widget};
9
10pub fn render_export_modal(
12 area: Rect,
13 buf: &mut ratatui::buffer::Buffer,
14 modal: &mut ExportModal,
15 border_color: Color,
16 active_color: Color,
17 text_primary: Color,
18 text_inverse: Color,
19) {
20 Clear.render(area, buf);
21 let block = Block::default()
22 .borders(Borders::ALL)
23 .border_type(BorderType::Rounded)
24 .border_style(Style::default().fg(border_color))
25 .title("Export Data");
26 let inner = block.inner(area);
27 block.render(area, buf);
28
29 let chunks = Layout::default()
31 .direction(Direction::Horizontal)
32 .constraints([
33 Constraint::Length(20), Constraint::Min(40), ])
36 .split(inner);
37
38 render_format_list(chunks[0], buf, modal, border_color, active_color);
40
41 let right_chunks = Layout::default()
43 .direction(Direction::Vertical)
44 .constraints([
45 Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
49 .split(chunks[1]);
50
51 render_path_input(
53 right_chunks[0],
54 buf,
55 modal,
56 border_color,
57 active_color,
58 text_primary,
59 text_inverse,
60 );
61
62 render_format_options(
64 right_chunks[1],
65 buf,
66 modal,
67 border_color,
68 active_color,
69 text_primary,
70 text_inverse,
71 );
72
73 render_footer(right_chunks[2], buf, modal, border_color, active_color);
75}
76
77fn render_format_list(
78 area: Rect,
79 buf: &mut ratatui::buffer::Buffer,
80 modal: &mut ExportModal,
81 border_color: Color,
82 active_color: Color,
83) {
84 let is_focused = modal.focus == ExportFocus::FormatSelector;
85 let border_style = if is_focused {
86 Style::default().fg(active_color)
87 } else {
88 Style::default().fg(border_color)
89 };
90
91 let block = Block::default()
92 .borders(Borders::ALL)
93 .border_type(BorderType::Rounded)
94 .border_style(border_style)
95 .title("Format");
96 let inner = block.inner(area);
97 block.render(area, buf);
98
99 let items: Vec<ListItem> = ExportFormat::ALL
100 .iter()
101 .map(|format| {
102 let marker = if modal.selected_format == *format {
103 "●"
104 } else {
105 "○"
106 };
107 let style = if modal.selected_format == *format {
108 Style::default().fg(active_color)
109 } else {
110 Style::default().fg(border_color)
111 };
112 ListItem::new(Line::from(vec![Span::styled(
113 format!("{} {}", marker, format.as_str()),
114 style,
115 )]))
116 })
117 .collect();
118
119 let list = List::new(items).style(if is_focused {
120 Style::default().fg(active_color)
121 } else {
122 Style::default()
123 });
124 list.render(inner, buf);
125}
126
127fn render_path_input(
128 area: Rect,
129 buf: &mut ratatui::buffer::Buffer,
130 modal: &mut ExportModal,
131 border_color: Color,
132 active_color: Color,
133 _text_primary: Color,
134 _text_inverse: Color,
135) {
136 let is_focused = modal.focus == ExportFocus::PathInput;
137 let border_style = if is_focused {
138 Style::default().fg(active_color)
139 } else {
140 Style::default().fg(border_color)
141 };
142
143 let block = Block::default()
144 .borders(Borders::ALL)
145 .border_type(BorderType::Rounded)
146 .border_style(border_style)
147 .title("File Path");
148 let inner = block.inner(area);
149 block.render(area, buf);
150
151 modal.path_input.set_focused(is_focused);
153 (&modal.path_input).render(inner, buf);
154}
155
156fn render_format_options(
157 area: Rect,
158 buf: &mut ratatui::buffer::Buffer,
159 modal: &mut ExportModal,
160 border_color: Color,
161 active_color: Color,
162 text_primary: Color,
163 text_inverse: Color,
164) {
165 let block = Block::default()
166 .borders(Borders::ALL)
167 .border_type(BorderType::Rounded)
168 .border_style(Style::default().fg(border_color))
169 .title("Options");
170 let inner = block.inner(area);
171 block.render(area, buf);
172
173 match modal.selected_format {
174 ExportFormat::Csv => render_csv_options(
175 inner,
176 buf,
177 modal,
178 border_color,
179 active_color,
180 text_primary,
181 text_inverse,
182 ),
183 ExportFormat::Json => render_json_options(inner, buf, modal, border_color, active_color),
184 ExportFormat::Ndjson => {
185 render_ndjson_options(inner, buf, modal, border_color, active_color)
186 }
187 ExportFormat::Parquet | ExportFormat::Ipc | ExportFormat::Avro => {
188 render_no_format_options(inner, buf, modal, border_color, active_color)
189 }
190 }
191}
192
193fn render_csv_options(
194 area: Rect,
195 buf: &mut ratatui::buffer::Buffer,
196 modal: &mut ExportModal,
197 border_color: Color,
198 active_color: Color,
199 _text_primary: Color,
200 _text_inverse: Color,
201) {
202 let rows = Layout::default()
209 .direction(Direction::Vertical)
210 .constraints([
211 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), ])
216 .split(area);
217
218 let delimiter_row = Layout::default()
221 .direction(Direction::Horizontal)
222 .constraints([
223 Constraint::Length(15), Constraint::Length(2), Constraint::Min(1), ])
227 .split(rows[0]);
228
229 let is_delimiter_focused = modal.focus == ExportFocus::CsvDelimiter;
230 let delimiter_label_style = if is_delimiter_focused {
231 Style::default().fg(active_color)
232 } else {
233 Style::default().fg(border_color)
234 };
235
236 Paragraph::new("Delimiter:")
237 .style(delimiter_label_style)
238 .render(delimiter_row[0], buf);
239
240 modal.csv_delimiter_input.set_focused(is_delimiter_focused);
242 (&modal.csv_delimiter_input).render(delimiter_row[2], buf);
243
244 let header_row = Layout::default()
247 .direction(Direction::Horizontal)
248 .constraints([
249 Constraint::Length(15), Constraint::Length(2), Constraint::Min(1), ])
253 .split(rows[1]);
254
255 let is_header_focused = modal.focus == ExportFocus::CsvIncludeHeader;
256 let header_label_style = if is_header_focused {
257 Style::default().fg(active_color)
258 } else {
259 Style::default().fg(border_color)
260 };
261
262 Paragraph::new("Include Header:")
263 .style(header_label_style)
264 .render(header_row[0], buf);
265
266 let marker = if modal.csv_include_header {
268 "☑"
269 } else {
270 "☐"
271 };
272 let checkbox_style = if is_header_focused {
273 Style::default().fg(active_color)
274 } else {
275 Style::default().fg(border_color)
276 };
277
278 Paragraph::new(Line::from(vec![Span::styled(marker, checkbox_style)]))
279 .render(header_row[2], buf);
280
281 let compression_label_row = Layout::default()
284 .direction(Direction::Horizontal)
285 .constraints([
286 Constraint::Length(15), Constraint::Min(1), ])
289 .split(rows[2]);
290
291 let is_compression_focused = modal.focus == ExportFocus::CsvCompression;
292 let compression_label_style = if is_compression_focused {
293 Style::default().fg(active_color)
294 } else {
295 Style::default().fg(border_color)
296 };
297
298 Paragraph::new("Compression:")
299 .style(compression_label_style)
300 .render(compression_label_row[0], buf);
301
302 if rows.len() > 3 && rows[3].height > 0 {
304 render_compression_grid(
305 rows[3],
306 buf,
307 modal,
308 ExportFocus::CsvCompression,
309 modal.csv_compression,
310 border_color,
311 active_color,
312 );
313 }
314}
315
316fn render_json_options(
317 area: Rect,
318 buf: &mut ratatui::buffer::Buffer,
319 modal: &mut ExportModal,
320 border_color: Color,
321 active_color: Color,
322) {
323 let rows = Layout::default()
325 .direction(Direction::Vertical)
326 .constraints([
327 Constraint::Length(1), Constraint::Min(1), ])
330 .split(area);
331
332 let compression_label_row = Layout::default()
334 .direction(Direction::Horizontal)
335 .constraints([
336 Constraint::Length(15), Constraint::Min(1), ])
339 .split(rows[0]);
340
341 let is_compression_focused = modal.focus == ExportFocus::JsonCompression;
342 let compression_label_style = if is_compression_focused {
343 Style::default().fg(active_color)
344 } else {
345 Style::default().fg(border_color)
346 };
347
348 Paragraph::new("Compression:")
349 .style(compression_label_style)
350 .render(compression_label_row[0], buf);
351
352 if rows.len() > 1 && rows[1].height > 0 {
354 render_compression_grid(
355 rows[1],
356 buf,
357 modal,
358 ExportFocus::JsonCompression,
359 modal.json_compression,
360 border_color,
361 active_color,
362 );
363 }
364}
365
366fn render_ndjson_options(
367 area: Rect,
368 buf: &mut ratatui::buffer::Buffer,
369 modal: &mut ExportModal,
370 border_color: Color,
371 active_color: Color,
372) {
373 let rows = Layout::default()
375 .direction(Direction::Vertical)
376 .constraints([
377 Constraint::Length(1), Constraint::Min(1), ])
380 .split(area);
381
382 let compression_label_row = Layout::default()
384 .direction(Direction::Horizontal)
385 .constraints([
386 Constraint::Length(15), Constraint::Min(1), ])
389 .split(rows[0]);
390
391 let is_compression_focused = modal.focus == ExportFocus::NdjsonCompression;
392 let compression_label_style = if is_compression_focused {
393 Style::default().fg(active_color)
394 } else {
395 Style::default().fg(border_color)
396 };
397
398 Paragraph::new("Compression:")
399 .style(compression_label_style)
400 .render(compression_label_row[0], buf);
401
402 if rows.len() > 1 && rows[1].height > 0 {
404 render_compression_grid(
405 rows[1],
406 buf,
407 modal,
408 ExportFocus::NdjsonCompression,
409 modal.ndjson_compression,
410 border_color,
411 active_color,
412 );
413 }
414}
415
416fn render_no_format_options(
417 area: Rect,
418 buf: &mut ratatui::buffer::Buffer,
419 modal: &mut ExportModal,
420 border_color: Color,
421 _active_color: Color,
422) {
423 let msg = format!(
424 "No additional options for {} format",
425 modal.selected_format.as_str()
426 );
427 Paragraph::new(msg)
428 .style(Style::default().fg(border_color))
429 .centered()
430 .render(area, buf);
431}
432
433fn render_compression_grid(
435 area: Rect,
436 buf: &mut ratatui::buffer::Buffer,
437 modal: &mut ExportModal,
438 focus: ExportFocus,
439 compression: Option<CompressionFormat>,
440 border_color: Color,
441 active_color: Color,
442) {
443 let is_focused = modal.focus == focus;
444
445 let compression_options = [
446 (None, "None"),
447 (Some(CompressionFormat::Gzip), "Gzip"),
448 (Some(CompressionFormat::Zstd), "Zstd"),
449 (Some(CompressionFormat::Bzip2), "Bzip2"),
450 (Some(CompressionFormat::Xz), "XZ"),
451 ];
452
453 const ITEMS_PER_ROW: usize = 3;
455 let num_rows = (compression_options.len() as u16).div_ceil(ITEMS_PER_ROW as u16);
456
457 let item_width = area.width / ITEMS_PER_ROW as u16;
459 let item_height = 1;
460
461 let mut option_idx = 0;
462 for row in 0..num_rows.min(area.height) {
463 let y = area.y + row;
464 if y >= area.bottom() {
465 break;
466 }
467
468 for col in 0..ITEMS_PER_ROW {
469 if option_idx >= compression_options.len() {
470 break;
471 }
472
473 let x = area.x + (col as u16 * item_width);
474 let item_area = Rect {
475 x,
476 y,
477 width: item_width,
478 height: item_height,
479 };
480
481 let (opt, label) = &compression_options[option_idx];
482 let is_selected = *opt == compression;
483 let is_option_focused = is_focused && option_idx == modal.compression_selection_idx;
484
485 let marker = if is_selected { "●" } else { "○" };
486 let style = if is_selected || is_option_focused {
487 Style::default().fg(active_color)
488 } else {
489 Style::default().fg(border_color)
490 };
491
492 let text = format!("{} {}", marker, label);
493 Paragraph::new(Line::from(vec![Span::styled(text, style)])).render(item_area, buf);
494
495 option_idx += 1;
496 }
497 }
498}
499
500fn render_footer(
501 area: Rect,
502 buf: &mut ratatui::buffer::Buffer,
503 modal: &mut ExportModal,
504 border_color: Color,
505 active_color: Color,
506) {
507 let chunks = Layout::default()
508 .direction(Direction::Horizontal)
509 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
510 .split(area);
511
512 let is_focused = modal.focus == ExportFocus::ExportButton;
514 let text_style = if is_focused {
515 Style::default().fg(active_color)
516 } else {
517 Style::default().fg(border_color)
518 };
519 let border_style = if is_focused {
520 Style::default().fg(active_color)
521 } else {
522 Style::default().fg(border_color)
523 };
524
525 Paragraph::new("Export")
526 .style(text_style)
527 .block(
528 Block::default()
529 .borders(Borders::ALL)
530 .border_type(BorderType::Rounded)
531 .border_style(border_style),
532 )
533 .centered()
534 .render(chunks[0], buf);
535
536 let is_focused = modal.focus == ExportFocus::CancelButton;
538 let text_style = if is_focused {
539 Style::default().fg(active_color)
540 } else {
541 Style::default().fg(border_color)
542 };
543 let border_style = if is_focused {
544 Style::default().fg(active_color)
545 } else {
546 Style::default().fg(border_color)
547 };
548
549 Paragraph::new("Cancel")
550 .style(text_style)
551 .block(
552 Block::default()
553 .borders(Borders::ALL)
554 .border_type(BorderType::Rounded)
555 .border_style(border_style),
556 )
557 .centered()
558 .render(chunks[1], buf);
559}