1use super::{adapter::DatasetAdapter, format::truncate_string, scroll::ScrollState};
6
7#[derive(Debug, Clone)]
31pub struct DatasetViewer {
32 adapter: DatasetAdapter,
34 scroll: ScrollState,
36 column_widths: Vec<u16>,
38 display_width: u16,
40 visible_rows: u16,
42}
43
44impl DatasetViewer {
45 pub fn new(adapter: DatasetAdapter) -> Self {
50 Self::with_dimensions(adapter, 80, 24)
51 }
52
53 pub fn with_dimensions(adapter: DatasetAdapter, width: u16, height: u16) -> Self {
60 let visible_rows = height.saturating_sub(1); let column_widths = adapter.calculate_column_widths(width, 20);
62 let scroll = ScrollState::new(adapter.row_count(), visible_rows as usize);
63
64 Self {
65 adapter,
66 scroll,
67 column_widths,
68 display_width: width,
69 visible_rows,
70 }
71 }
72
73 pub fn set_dimensions(&mut self, width: u16, height: u16) {
77 self.display_width = width;
78 self.visible_rows = height.saturating_sub(1);
79 self.column_widths = self.adapter.calculate_column_widths(width, 20);
80 self.scroll.set_visible_rows(self.visible_rows as usize);
81 }
82
83 #[inline]
85 pub fn scroll_offset(&self) -> usize {
86 self.scroll.offset()
87 }
88
89 pub fn set_scroll_offset(&mut self, offset: usize) {
91 self.scroll.set_offset(offset);
92 }
93
94 #[inline]
96 pub fn row_count(&self) -> usize {
97 self.adapter.row_count()
98 }
99
100 #[inline]
102 pub fn visible_row_count(&self) -> u16 {
103 self.visible_rows
104 }
105
106 #[inline]
108 pub fn selected_row(&self) -> Option<usize> {
109 self.scroll.selected()
110 }
111
112 pub fn select_row(&mut self, row: usize) {
114 self.scroll.set_selected(Some(row));
115 }
116
117 pub fn clear_selection(&mut self) {
119 self.scroll.set_selected(None);
120 }
121
122 #[inline]
124 pub fn is_empty(&self) -> bool {
125 self.adapter.is_empty()
126 }
127
128 #[inline]
130 pub fn adapter(&self) -> &DatasetAdapter {
131 &self.adapter
132 }
133
134 #[inline]
136 pub fn column_widths(&self) -> &[u16] {
137 &self.column_widths
138 }
139
140 pub fn scroll_down(&mut self) {
144 self.scroll.scroll_down();
145 }
146
147 pub fn scroll_up(&mut self) {
149 self.scroll.scroll_up();
150 }
151
152 pub fn page_down(&mut self) {
154 self.scroll.page_down();
155 }
156
157 pub fn page_up(&mut self) {
159 self.scroll.page_up();
160 }
161
162 pub fn home(&mut self) {
164 self.scroll.home();
165 }
166
167 pub fn end(&mut self) {
169 self.scroll.end();
170 }
171
172 pub fn select_next(&mut self) {
174 self.scroll.select_next();
175 }
176
177 pub fn select_prev(&mut self) {
179 self.scroll.select_prev();
180 }
181
182 pub fn headers(&self) -> Vec<String> {
186 self.adapter
187 .field_names()
188 .into_iter()
189 .enumerate()
190 .map(|(i, name)| {
191 let width = self.column_widths.get(i).copied().unwrap_or(10) as usize;
192 truncate_string(name, width)
193 })
194 .collect()
195 }
196
197 pub fn visible_rows_data(&self) -> Vec<Vec<String>> {
201 let start = self.scroll.offset();
202 let end = (start + self.visible_rows as usize).min(self.adapter.row_count());
203
204 (start..end)
205 .map(|row_idx| self.format_row(row_idx))
206 .collect()
207 }
208
209 fn format_row(&self, row_idx: usize) -> Vec<String> {
211 (0..self.adapter.column_count())
212 .map(|col_idx| {
213 let width = self.column_widths.get(col_idx).copied().unwrap_or(10) as usize;
214 match self.adapter.get_cell(row_idx, col_idx) {
215 Ok(Some(value)) => truncate_string(&value, width),
216 Ok(None) => String::new(),
217 Err(_) => "<error>".to_string(),
218 }
219 })
220 .collect()
221 }
222
223 pub fn is_row_selected(&self, global_row: usize) -> bool {
225 self.scroll.selected() == Some(global_row)
226 }
227
228 pub fn needs_scrollbar(&self) -> bool {
230 self.scroll.needs_scrollbar()
231 }
232
233 pub fn scrollbar_position(&self) -> f32 {
235 self.scroll.scrollbar_position()
236 }
237
238 pub fn scrollbar_size(&self) -> f32 {
240 self.scroll.scrollbar_size()
241 }
242
243 pub fn render_header_line(&self) -> String {
245 let headers = self.headers();
246 headers.join(" ")
247 }
248
249 pub fn render_row_line(&self, viewport_row: usize) -> Option<String> {
251 let global_row = self.scroll.to_global_row(viewport_row);
252 if global_row >= self.adapter.row_count() {
253 return None;
254 }
255
256 let cells = self.format_row(global_row);
257 Some(cells.join(" "))
258 }
259
260 pub fn viewport_to_data_row(&self, viewport_row: usize) -> usize {
262 self.scroll.to_global_row(viewport_row)
263 }
264
265 pub fn search(&mut self, query: &str) -> Option<usize> {
272 let result = self.adapter.search(query);
273 if let Some(row) = result {
274 self.select_row(row);
275 self.scroll.ensure_visible(row);
276 }
277 result
278 }
279
280 pub fn search_next(&mut self, query: &str) -> Option<usize> {
284 let start = self.scroll.selected().map(|r| r + 1).unwrap_or(0);
285 let result = self.adapter.search_from(query, start);
286 if let Some(row) = result {
287 self.select_row(row);
288 self.scroll.ensure_visible(row);
289 }
290 result
291 }
292
293 pub fn render_lines(&self) -> Vec<String> {
297 let mut lines = Vec::with_capacity(self.visible_rows as usize + 1);
298
299 lines.push(self.render_header_line());
301
302 for vrow in 0..self.visible_rows as usize {
304 if let Some(line) = self.render_row_line(vrow) {
305 lines.push(line);
306 }
307 }
308
309 lines
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use std::sync::Arc;
316
317 use arrow::{
318 array::{Float32Array, Int32Array, RecordBatch, StringArray},
319 datatypes::{DataType, Field, Schema},
320 };
321
322 use super::*;
323
324 fn create_test_adapter(rows: usize) -> DatasetAdapter {
325 let schema = Arc::new(Schema::new(vec![
326 Field::new("id", DataType::Utf8, false),
327 Field::new("value", DataType::Int32, false),
328 Field::new("score", DataType::Float32, false),
329 ]));
330
331 let ids: Vec<String> = (0..rows).map(|i| format!("id_{i}")).collect();
332 let values: Vec<i32> = (0..rows).map(|i| i as i32 * 10).collect();
333 let scores: Vec<f32> = (0..rows).map(|i| i as f32 * 0.1).collect();
334
335 let batch = RecordBatch::try_new(
336 schema.clone(),
337 vec![
338 Arc::new(StringArray::from(ids)),
339 Arc::new(Int32Array::from(values)),
340 Arc::new(Float32Array::from(scores)),
341 ],
342 )
343 .unwrap();
344
345 DatasetAdapter::from_batches(vec![batch], schema).unwrap()
346 }
347
348 fn create_test_viewer() -> DatasetViewer {
349 let adapter = create_test_adapter(80);
350 DatasetViewer::with_dimensions(adapter, 80, 24)
351 }
352
353 #[test]
354 fn f026_viewer_new() {
355 let viewer = create_test_viewer();
356 assert_eq!(viewer.row_count(), 80);
357 assert_eq!(viewer.scroll_offset(), 0);
358 }
359
360 #[test]
361 fn f027_viewer_scroll_down() {
362 let mut viewer = create_test_viewer();
363 viewer.scroll_down();
364 assert_eq!(viewer.scroll_offset(), 1);
365 }
366
367 #[test]
368 fn f028_viewer_scroll_up() {
369 let mut viewer = create_test_viewer();
370 viewer.set_scroll_offset(10);
371 viewer.scroll_up();
372 assert_eq!(viewer.scroll_offset(), 9);
373 }
374
375 #[test]
376 fn f029_viewer_scroll_bounds_top() {
377 let mut viewer = create_test_viewer();
378 viewer.scroll_up();
379 assert_eq!(viewer.scroll_offset(), 0);
380 }
381
382 #[test]
383 fn f030_viewer_page_down() {
384 let mut viewer = create_test_viewer();
385 let visible = viewer.visible_row_count() as usize;
386 viewer.page_down();
387 assert!(viewer.scroll_offset() >= visible / 2);
388 }
389
390 #[test]
391 fn f031_viewer_page_up() {
392 let mut viewer = create_test_viewer();
393 viewer.set_scroll_offset(50);
394 viewer.page_up();
395 assert!(viewer.scroll_offset() < 50);
396 }
397
398 #[test]
399 fn f032_viewer_home() {
400 let mut viewer = create_test_viewer();
401 viewer.set_scroll_offset(50);
402 viewer.home();
403 assert_eq!(viewer.scroll_offset(), 0);
404 }
405
406 #[test]
407 fn f033_viewer_end() {
408 let mut viewer = create_test_viewer();
409 viewer.end();
410 let max_offset = viewer.row_count() - viewer.visible_row_count() as usize;
412 assert_eq!(viewer.scroll_offset(), max_offset);
413 }
414
415 #[test]
416 fn f034_viewer_select_row() {
417 let mut viewer = create_test_viewer();
418 viewer.select_row(5);
419 assert_eq!(viewer.selected_row(), Some(5));
420 }
421
422 #[test]
423 fn f035_viewer_select_next() {
424 let mut viewer = create_test_viewer();
425 viewer.select_row(0);
426 viewer.select_next();
427 assert_eq!(viewer.selected_row(), Some(1));
428 }
429
430 #[test]
431 fn f036_viewer_select_prev() {
432 let mut viewer = create_test_viewer();
433 viewer.select_row(5);
434 viewer.select_prev();
435 assert_eq!(viewer.selected_row(), Some(4));
436 }
437
438 #[test]
439 fn f037_viewer_clear_selection() {
440 let mut viewer = create_test_viewer();
441 viewer.select_row(5);
442 viewer.clear_selection();
443 assert_eq!(viewer.selected_row(), None);
444 }
445
446 #[test]
447 fn f038_viewer_headers() {
448 let viewer = create_test_viewer();
449 let headers = viewer.headers();
450 assert_eq!(headers.len(), 3);
451 assert!(headers[0].contains("id"));
452 }
453
454 #[test]
455 fn f039_viewer_visible_rows() {
456 let viewer = create_test_viewer();
457 let rows = viewer.visible_rows_data();
458 assert!(rows.len() <= viewer.visible_row_count() as usize);
459 }
460
461 #[test]
462 fn f040_viewer_needs_scrollbar() {
463 let viewer = create_test_viewer();
464 assert!(viewer.needs_scrollbar());
465 }
466
467 #[test]
468 fn f041_viewer_no_scrollbar_small() {
469 let adapter = create_test_adapter(5);
470 let viewer = DatasetViewer::with_dimensions(adapter, 80, 24);
471 assert!(!viewer.needs_scrollbar());
472 }
473
474 #[test]
475 fn f042_viewer_scrollbar_position() {
476 let mut viewer = create_test_viewer();
477 viewer.set_scroll_offset(40);
478 let pos = viewer.scrollbar_position();
479 assert!(pos > 0.0 && pos < 1.0);
480 }
481
482 #[test]
483 fn f043_viewer_render_header() {
484 let viewer = create_test_viewer();
485 let header = viewer.render_header_line();
486 assert!(!header.is_empty());
487 assert!(header.contains("id"));
488 }
489
490 #[test]
491 fn f044_viewer_render_row() {
492 let viewer = create_test_viewer();
493 let row = viewer.render_row_line(0);
494 assert!(row.is_some());
495 assert!(row.unwrap().contains("id_0"));
496 }
497
498 #[test]
499 fn f045_viewer_render_row_out_of_bounds() {
500 let viewer = create_test_viewer();
501 let row = viewer.render_row_line(1000);
502 assert!(row.is_none());
503 }
504
505 #[test]
506 fn f046_viewer_render_lines() {
507 let viewer = create_test_viewer();
508 let lines = viewer.render_lines();
509 assert!(!lines.is_empty());
510 assert!(lines[0].contains("id"));
512 }
513
514 #[test]
515 fn f047_viewer_column_widths() {
516 let viewer = create_test_viewer();
517 let widths = viewer.column_widths();
518 assert_eq!(widths.len(), 3);
519 for w in widths {
520 assert!(*w >= 3);
521 }
522 }
523
524 #[test]
525 fn f048_viewer_is_row_selected() {
526 let mut viewer = create_test_viewer();
527 viewer.select_row(5);
528 assert!(viewer.is_row_selected(5));
529 assert!(!viewer.is_row_selected(4));
530 }
531
532 #[test]
533 fn f049_viewer_set_dimensions() {
534 let mut viewer = create_test_viewer();
535 viewer.set_dimensions(40, 10);
536 assert_eq!(viewer.visible_row_count(), 9);
537 }
538
539 #[test]
540 fn f050_viewer_empty() {
541 let adapter = DatasetAdapter::empty();
542 let viewer = DatasetViewer::new(adapter);
543 assert!(viewer.is_empty());
544 assert_eq!(viewer.row_count(), 0);
545 }
546
547 #[test]
548 fn f_viewer_viewport_to_data_row() {
549 let mut viewer = create_test_viewer();
550 viewer.set_scroll_offset(10);
551 assert_eq!(viewer.viewport_to_data_row(5), 15);
552 }
553
554 #[test]
555 fn f_viewer_is_clone() {
556 let viewer = create_test_viewer();
557 let cloned = viewer.clone();
558 assert_eq!(viewer.row_count(), cloned.row_count());
559 }
560
561 #[test]
562 fn f_viewer_adapter_access() {
563 let viewer = create_test_viewer();
564 let adapter = viewer.adapter();
565 assert_eq!(adapter.column_count(), 3);
566 }
567
568 #[test]
569 fn f_viewer_scrollbar_size() {
570 let viewer = create_test_viewer();
571 let size = viewer.scrollbar_size();
572 assert!(size > 0.0 && size <= 1.0);
574 }
575
576 #[test]
577 fn f_viewer_scrollbar_size_small_dataset() {
578 let adapter = create_test_adapter(5);
579 let viewer = DatasetViewer::with_dimensions(adapter, 80, 24);
580 let size = viewer.scrollbar_size();
581 assert!((size - 1.0).abs() < 0.01);
583 }
584
585 #[test]
586 fn f_viewer_format_row_with_null() {
587 use arrow::array::NullArray;
589
590 let schema = Arc::new(Schema::new(vec![
591 Field::new("id", DataType::Utf8, false),
592 Field::new("nullable_col", DataType::Null, true),
593 ]));
594
595 let batch = RecordBatch::try_new(
596 schema.clone(),
597 vec![
598 Arc::new(StringArray::from(vec!["a", "b"])),
599 Arc::new(NullArray::new(2)),
600 ],
601 )
602 .unwrap();
603
604 let adapter = DatasetAdapter::from_batches(vec![batch], schema).unwrap();
605 let viewer = DatasetViewer::new(adapter);
606 let rows = viewer.visible_rows_data();
607
608 assert_eq!(rows.len(), 2, "FALSIFIED: Should have 2 rows");
610 assert_eq!(rows[0].len(), 2, "FALSIFIED: Should have 2 columns");
611 assert_eq!(rows[0][0], "a", "FALSIFIED: First cell should be 'a'");
612 assert!(
614 rows[0][1].is_empty() || rows[0][1] == "null" || rows[0][1] == "NULL",
615 "FALSIFIED: Null should render as empty or 'null'/'NULL', got: '{}'",
616 rows[0][1]
617 );
618 }
619
620 #[test]
623 fn f_viewer_search_finds_match() {
624 let mut viewer = create_test_viewer();
625 let result = viewer.search("id_5");
626 assert_eq!(
627 result,
628 Some(5),
629 "FALSIFIED: Search should find 'id_5' at row 5"
630 );
631 assert_eq!(
632 viewer.selected_row(),
633 Some(5),
634 "FALSIFIED: Search should select found row"
635 );
636 }
637
638 #[test]
639 fn f_viewer_search_no_match() {
640 let mut viewer = create_test_viewer();
641 let result = viewer.search("nonexistent_xyz");
642 assert_eq!(result, None, "FALSIFIED: Search should return None");
643 assert_eq!(
644 viewer.selected_row(),
645 None,
646 "FALSIFIED: No selection should change"
647 );
648 }
649
650 #[test]
651 fn f_viewer_search_case_insensitive() {
652 let mut viewer = create_test_viewer();
653 let result = viewer.search("ID_3");
654 assert_eq!(
655 result,
656 Some(3),
657 "FALSIFIED: Search should be case insensitive"
658 );
659 }
660
661 #[test]
662 fn f_viewer_search_next_continues() {
663 let mut viewer = create_test_viewer();
664 viewer.search("id_");
666 let first = viewer.selected_row();
667 viewer.search_next("id_");
669 let second = viewer.selected_row();
670 assert_ne!(first, second, "FALSIFIED: search_next should continue");
671 }
672
673 #[test]
674 fn f_viewer_search_next_wraps() {
675 let mut viewer = create_test_viewer();
676 viewer.select_row(9);
678 let result = viewer.search_next("id_0");
680 assert_eq!(result, Some(0), "FALSIFIED: search_next should wrap");
681 }
682
683 #[test]
684 fn f_viewer_search_empty_query() {
685 let mut viewer = create_test_viewer();
686 let result = viewer.search("");
687 assert_eq!(result, None, "FALSIFIED: Empty query should return None");
688 }
689}