1use crate::key::{Binding, matches};
28use crate::viewport::Viewport;
29use bubbletea::{Cmd, KeyMsg, Message, Model, MouseAction, MouseButton, MouseMsg};
30use lipgloss::{Color, Style};
31
32#[derive(Debug, Clone)]
34pub struct Column {
35 pub title: String,
37 pub width: usize,
39}
40
41impl Column {
42 #[must_use]
44 pub fn new(title: impl Into<String>, width: usize) -> Self {
45 Self {
46 title: title.into(),
47 width,
48 }
49 }
50}
51
52pub type Row = Vec<String>;
54
55#[derive(Debug, Clone)]
57pub struct KeyMap {
58 pub line_up: Binding,
60 pub line_down: Binding,
62 pub page_up: Binding,
64 pub page_down: Binding,
66 pub half_page_up: Binding,
68 pub half_page_down: Binding,
70 pub goto_top: Binding,
72 pub goto_bottom: Binding,
74}
75
76impl Default for KeyMap {
77 fn default() -> Self {
78 Self {
79 line_up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
80 line_down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
81 page_up: Binding::new()
82 .keys(&["b", "pgup"])
83 .help("b/pgup", "page up"),
84 page_down: Binding::new()
85 .keys(&["f", "pgdown", " "])
86 .help("f/pgdn", "page down"),
87 half_page_up: Binding::new().keys(&["u", "ctrl+u"]).help("u", "½ page up"),
88 half_page_down: Binding::new()
89 .keys(&["d", "ctrl+d"])
90 .help("d", "½ page down"),
91 goto_top: Binding::new()
92 .keys(&["home", "g"])
93 .help("g/home", "go to start"),
94 goto_bottom: Binding::new()
95 .keys(&["end", "G"])
96 .help("G/end", "go to end"),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct Styles {
104 pub header: Style,
106 pub cell: Style,
108 pub selected: Style,
110}
111
112impl Default for Styles {
113 fn default() -> Self {
114 Self {
115 header: Style::new().bold().padding_left(1).padding_right(1),
116 cell: Style::new().padding_left(1).padding_right(1),
117 selected: Style::new().bold().foreground_color(Color::from("212")),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Table {
125 pub key_map: KeyMap,
127 pub styles: Styles,
129 pub mouse_wheel_enabled: bool,
131 pub mouse_wheel_delta: usize,
133 pub mouse_click_enabled: bool,
135 columns: Vec<Column>,
137 rows: Vec<Row>,
139 cursor: usize,
141 focus: bool,
143 viewport: Viewport,
145 start: usize,
147 end: usize,
149}
150
151impl Default for Table {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157impl Table {
158 #[must_use]
160 pub fn new() -> Self {
161 Self {
162 key_map: KeyMap::default(),
163 styles: Styles::default(),
164 mouse_wheel_enabled: true,
165 mouse_wheel_delta: 3,
166 mouse_click_enabled: true,
167 columns: Vec::new(),
168 rows: Vec::new(),
169 cursor: 0,
170 focus: false,
171 viewport: Viewport::new(0, 20),
172 start: 0,
173 end: 0,
174 }
175 }
176
177 #[must_use]
179 pub fn columns(mut self, columns: Vec<Column>) -> Self {
180 self.columns = columns;
181 self.update_viewport();
182 self
183 }
184
185 #[must_use]
187 pub fn rows(mut self, rows: Vec<Row>) -> Self {
188 self.rows = rows;
189 self.update_viewport();
190 self
191 }
192
193 #[must_use]
195 pub fn height(mut self, h: usize) -> Self {
196 let header_height = 1; self.viewport.height = h.saturating_sub(header_height);
198 self.update_viewport();
199 self
200 }
201
202 #[must_use]
204 pub fn width(mut self, w: usize) -> Self {
205 self.viewport.width = w;
206 self.update_viewport();
207 self
208 }
209
210 #[must_use]
212 pub fn focused(mut self, f: bool) -> Self {
213 self.focus = f;
214 self.update_viewport();
215 self
216 }
217
218 #[must_use]
220 pub fn with_styles(mut self, styles: Styles) -> Self {
221 self.styles = styles;
222 self.update_viewport();
223 self
224 }
225
226 #[must_use]
228 pub fn with_key_map(mut self, key_map: KeyMap) -> Self {
229 self.key_map = key_map;
230 self
231 }
232
233 #[must_use]
235 pub fn mouse_wheel(mut self, enabled: bool) -> Self {
236 self.mouse_wheel_enabled = enabled;
237 self
238 }
239
240 #[must_use]
242 pub fn mouse_wheel_delta(mut self, delta: usize) -> Self {
243 self.mouse_wheel_delta = delta;
244 self
245 }
246
247 #[must_use]
249 pub fn mouse_click(mut self, enabled: bool) -> Self {
250 self.mouse_click_enabled = enabled;
251 self
252 }
253
254 #[must_use]
256 pub fn is_focused(&self) -> bool {
257 self.focus
258 }
259
260 pub fn focus(&mut self) {
262 self.focus = true;
263 self.update_viewport();
264 }
265
266 pub fn blur(&mut self) {
268 self.focus = false;
269 self.update_viewport();
270 }
271
272 #[must_use]
274 pub fn get_columns(&self) -> &[Column] {
275 &self.columns
276 }
277
278 #[must_use]
280 pub fn get_rows(&self) -> &[Row] {
281 &self.rows
282 }
283
284 pub fn set_columns(&mut self, columns: Vec<Column>) {
286 self.columns = columns;
287 self.update_viewport();
288 }
289
290 pub fn set_rows(&mut self, rows: Vec<Row>) {
292 self.rows = rows;
293 if self.cursor > self.rows.len().saturating_sub(1) {
294 self.cursor = self.rows.len().saturating_sub(1);
295 }
296 self.update_viewport();
297 }
298
299 pub fn set_width(&mut self, w: usize) {
301 self.viewport.width = w;
302 self.update_viewport();
303 }
304
305 pub fn set_height(&mut self, h: usize) {
307 let header_height = 1;
308 self.viewport.height = h.saturating_sub(header_height);
309 self.update_viewport();
310 }
311
312 #[must_use]
314 pub fn get_height(&self) -> usize {
315 self.viewport.height
316 }
317
318 #[must_use]
320 pub fn get_width(&self) -> usize {
321 self.viewport.width
322 }
323
324 #[must_use]
326 pub fn selected_row(&self) -> Option<&Row> {
327 self.rows.get(self.cursor)
328 }
329
330 #[must_use]
332 pub fn cursor(&self) -> usize {
333 self.cursor
334 }
335
336 pub fn set_cursor(&mut self, n: usize) {
338 self.cursor = n.min(self.rows.len().saturating_sub(1));
339 self.update_viewport();
340 }
341
342 pub fn move_up(&mut self, n: usize) {
344 if self.rows.is_empty() {
345 return;
346 }
347 self.cursor = self.cursor.saturating_sub(n);
348 self.update_viewport();
349 }
350
351 pub fn move_down(&mut self, n: usize) {
353 if self.rows.is_empty() {
354 return;
355 }
356 self.cursor = (self.cursor + n).min(self.rows.len().saturating_sub(1));
357 self.update_viewport();
358 }
359
360 pub fn goto_top(&mut self) {
362 self.cursor = 0;
363 self.update_viewport();
364 }
365
366 pub fn goto_bottom(&mut self) {
368 if !self.rows.is_empty() {
369 self.cursor = self.rows.len() - 1;
370 }
371 self.update_viewport();
372 }
373
374 pub fn from_values(&mut self, value: &str, separator: &str) {
376 let rows: Vec<Row> = value
377 .lines()
378 .map(|line| line.split(separator).map(String::from).collect())
379 .collect();
380 self.set_rows(rows);
381 }
382
383 fn update_viewport(&mut self) {
385 if self.rows.is_empty() {
386 self.start = 0;
387 self.end = 0;
388 self.viewport.set_content("");
389 return;
390 }
391
392 let height = self.viewport.height;
393 if height == 0 {
394 self.start = 0;
395 self.end = 0;
396 self.viewport.set_content("");
397 return;
398 }
399
400 if self.cursor < self.start {
402 self.start = self.cursor;
404 } else if self.cursor >= self.start + height {
405 self.start = self.cursor - height + 1;
407 }
408
409 self.end = (self.start + height).min(self.rows.len());
411
412 if self.end - self.start < height && self.start > 0 {
414 self.start = self.end.saturating_sub(height);
415 }
416
417 let rendered: Vec<String> = (self.start..self.end).map(|i| self.render_row(i)).collect();
419
420 self.viewport.set_content(&rendered.join("\n"));
421 }
422
423 fn headers_view(&self) -> String {
425 let cells: Vec<String> = self
426 .columns
427 .iter()
428 .filter(|col| col.width > 0)
429 .map(|col| {
430 let truncated = truncate_string(&col.title, col.width);
431 let padded = pad_string(&truncated, col.width);
432 self.styles.header.render(&padded)
433 })
434 .collect();
435
436 cells.join("")
437 }
438
439 fn render_row(&self, row_idx: usize) -> String {
441 let row = &self.rows[row_idx];
442
443 let cells: Vec<String> = self
444 .columns
445 .iter()
446 .enumerate()
447 .filter(|(_, col)| col.width > 0)
448 .map(|(i, col)| {
449 let value = row.get(i).map(String::as_str).unwrap_or("");
450 let truncated = truncate_string(value, col.width);
451 let padded = pad_string(&truncated, col.width);
452 self.styles.cell.render(&padded)
453 })
454 .collect();
455
456 let row_str = cells.join("");
457
458 if row_idx == self.cursor {
459 self.styles.selected.render(&row_str)
460 } else {
461 row_str
462 }
463 }
464
465 pub fn update(&mut self, msg: &Message) {
467 if !self.focus {
468 return;
469 }
470
471 if let Some(key) = msg.downcast_ref::<KeyMsg>() {
472 let key_str = key.to_string();
473
474 if matches(&key_str, &[&self.key_map.line_up]) {
475 self.move_up(1);
476 } else if matches(&key_str, &[&self.key_map.line_down]) {
477 self.move_down(1);
478 } else if matches(&key_str, &[&self.key_map.page_up]) {
479 self.move_up(self.viewport.height);
480 } else if matches(&key_str, &[&self.key_map.page_down]) {
481 self.move_down(self.viewport.height);
482 } else if matches(&key_str, &[&self.key_map.half_page_up]) {
483 self.move_up(self.viewport.height / 2);
484 } else if matches(&key_str, &[&self.key_map.half_page_down]) {
485 self.move_down(self.viewport.height / 2);
486 } else if matches(&key_str, &[&self.key_map.goto_top]) {
487 self.goto_top();
488 } else if matches(&key_str, &[&self.key_map.goto_bottom]) {
489 self.goto_bottom();
490 }
491 }
492
493 if let Some(mouse) = msg.downcast_ref::<MouseMsg>() {
495 if mouse.action != MouseAction::Press {
497 return;
498 }
499
500 match mouse.button {
501 MouseButton::WheelUp if self.mouse_wheel_enabled => {
503 self.move_up(self.mouse_wheel_delta);
504 }
505 MouseButton::WheelDown if self.mouse_wheel_enabled => {
506 self.move_down(self.mouse_wheel_delta);
507 }
508 MouseButton::Left if self.mouse_click_enabled => {
510 let header_height = 1usize;
513 let click_y = mouse.y as usize;
514
515 if click_y >= header_height {
516 let visible_row = click_y - header_height;
518 let row_index = self.start + visible_row;
520
521 if row_index < self.rows.len() {
523 self.cursor = row_index;
524 self.update_viewport();
525 }
526 }
527 }
528 _ => {}
529 }
530 }
531 }
532
533 #[must_use]
535 pub fn view(&self) -> String {
536 format!("{}\n{}", self.headers_view(), self.viewport.view())
537 }
538}
539
540fn pad_string(s: &str, width: usize) -> String {
542 use unicode_width::UnicodeWidthStr;
543 let current_width = UnicodeWidthStr::width(s);
544 if current_width >= width {
545 s.to_string()
546 } else {
547 let padding = width - current_width;
548 format!("{}{}", s, " ".repeat(padding))
549 }
550}
551
552fn truncate_string(s: &str, width: usize) -> String {
554 use unicode_width::UnicodeWidthStr;
555
556 if UnicodeWidthStr::width(s) <= width {
557 return s.to_string();
558 }
559
560 if width == 0 {
561 return String::new();
562 }
563
564 let target_width = width.saturating_sub(1);
566 let mut current_width = 0;
567 let mut result = String::new();
568
569 for c in s.chars() {
570 let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
571 if current_width + w > target_width {
572 break;
573 }
574 result.push(c);
575 current_width += w;
576 }
577
578 format!("{}…", result)
579}
580
581impl Model for Table {
582 fn init(&self) -> Option<Cmd> {
586 None
587 }
588
589 fn update(&mut self, msg: Message) -> Option<Cmd> {
591 self.update(&msg);
592 None
593 }
594
595 fn view(&self) -> String {
597 Table::view(self)
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_column_new() {
607 let col = Column::new("Name", 20);
608 assert_eq!(col.title, "Name");
609 assert_eq!(col.width, 20);
610 }
611
612 #[test]
613 fn test_table_new() {
614 let table = Table::new();
615 assert!(table.get_columns().is_empty());
616 assert!(table.get_rows().is_empty());
617 assert!(!table.is_focused());
618 }
619
620 #[test]
621 fn test_table_builder() {
622 let columns = vec![Column::new("ID", 10), Column::new("Name", 20)];
623 let rows = vec![
624 vec!["1".into(), "Alice".into()],
625 vec!["2".into(), "Bob".into()],
626 ];
627
628 let table = Table::new()
629 .columns(columns)
630 .rows(rows)
631 .height(10)
632 .focused(true);
633
634 assert_eq!(table.get_columns().len(), 2);
635 assert_eq!(table.get_rows().len(), 2);
636 assert!(table.is_focused());
637 }
638
639 #[test]
640 fn test_table_navigation() {
641 let rows = vec![
642 vec!["1".into()],
643 vec!["2".into()],
644 vec!["3".into()],
645 vec!["4".into()],
646 vec!["5".into()],
647 ];
648
649 let mut table = Table::new().rows(rows).height(10);
650
651 assert_eq!(table.cursor(), 0);
652
653 table.move_down(1);
654 assert_eq!(table.cursor(), 1);
655
656 table.move_down(2);
657 assert_eq!(table.cursor(), 3);
658
659 table.move_up(1);
660 assert_eq!(table.cursor(), 2);
661
662 table.goto_bottom();
663 assert_eq!(table.cursor(), 4);
664
665 table.goto_top();
666 assert_eq!(table.cursor(), 0);
667 }
668
669 #[test]
670 fn test_table_selected_row() {
671 let rows = vec![
672 vec!["1".into(), "Alice".into()],
673 vec!["2".into(), "Bob".into()],
674 ];
675
676 let mut table = Table::new().rows(rows);
677
678 assert_eq!(
679 table.selected_row(),
680 Some(&vec!["1".into(), "Alice".into()])
681 );
682
683 table.move_down(1);
684 assert_eq!(table.selected_row(), Some(&vec!["2".into(), "Bob".into()]));
685 }
686
687 #[test]
688 fn test_table_focus_blur() {
689 let mut table = Table::new();
690 assert!(!table.is_focused());
691
692 table.focus();
693 assert!(table.is_focused());
694
695 table.blur();
696 assert!(!table.is_focused());
697 }
698
699 #[test]
700 fn test_table_set_cursor() {
701 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
702
703 let mut table = Table::new().rows(rows);
704
705 table.set_cursor(2);
706 assert_eq!(table.cursor(), 2);
707
708 table.set_cursor(100);
710 assert_eq!(table.cursor(), 2);
711 }
712
713 #[test]
714 fn test_table_from_values() {
715 let mut table = Table::new();
716 table.from_values("a,b,c\n1,2,3\nx,y,z", ",");
717
718 assert_eq!(table.get_rows().len(), 3);
719 assert_eq!(table.get_rows()[0], vec!["a", "b", "c"]);
720 assert_eq!(table.get_rows()[1], vec!["1", "2", "3"]);
721 }
722
723 #[test]
724 fn test_table_view() {
725 let columns = vec![Column::new("ID", 5), Column::new("Name", 10)];
726 let rows = vec![
727 vec!["1".into(), "Alice".into()],
728 vec!["2".into(), "Bob".into()],
729 ];
730
731 let table = Table::new().columns(columns).rows(rows).height(5);
732 let view = table.view();
733
734 assert!(view.contains("ID"));
735 assert!(view.contains("Name"));
736 }
737
738 #[test]
739 fn test_truncate_string() {
740 assert_eq!(truncate_string("Hello", 10), "Hello");
741 assert_eq!(truncate_string("Hello World", 5), "Hell…");
742 assert_eq!(truncate_string("Hi", 2), "Hi");
743 assert_eq!(truncate_string("", 5), "");
744 }
745
746 #[test]
747 fn test_table_empty() {
748 let table = Table::new();
749 assert!(table.selected_row().is_none());
750 assert_eq!(table.cursor(), 0);
751 }
752
753 #[test]
754 fn test_keymap_default() {
755 let km = KeyMap::default();
756 assert!(!km.line_up.get_keys().is_empty());
757 assert!(!km.goto_bottom.get_keys().is_empty());
758 }
759
760 #[test]
762 fn test_model_init() {
763 let table = Table::new();
764 let cmd = Model::init(&table);
766 assert!(cmd.is_none());
767 }
768
769 #[test]
770 fn test_model_view() {
771 let columns = vec![Column::new("ID", 5)];
772 let rows = vec![vec!["1".into()]];
773 let table = Table::new().columns(columns).rows(rows);
774 let model_view = Model::view(&table);
776 let table_view = Table::view(&table);
777 assert_eq!(model_view, table_view);
778 }
779
780 #[test]
781 fn test_model_update_handles_navigation() {
782 use bubbletea::{KeyMsg, KeyType, Message};
783
784 let columns = vec![Column::new("ID", 5), Column::new("Name", 20)];
785 let rows = vec![
786 vec!["1".into(), "First".into()],
787 vec!["2".into(), "Second".into()],
788 vec!["3".into(), "Third".into()],
789 ];
790 let mut table = Table::new().columns(columns).rows(rows).focused(true);
791 assert_eq!(table.cursor(), 0);
792
793 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
795 let _ = Model::update(&mut table, down_msg);
796
797 assert_eq!(table.cursor(), 1, "Table should navigate down on Down key");
798 }
799
800 #[test]
801 fn test_model_update_unfocused_ignores_input() {
802 use bubbletea::{KeyMsg, KeyType, Message};
803
804 let columns = vec![Column::new("ID", 5)];
805 let rows = vec![vec!["1".into()], vec!["2".into()]];
806 let mut table = Table::new().columns(columns).rows(rows);
807 assert!(!table.focus);
809 assert_eq!(table.cursor(), 0);
810
811 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
813 let _ = Model::update(&mut table, down_msg);
814
815 assert_eq!(table.cursor(), 0, "Unfocused table should ignore key input");
816 }
817
818 #[test]
819 fn test_model_update_goto_bottom() {
820 use bubbletea::{KeyMsg, KeyType, Message};
821
822 let columns = vec![Column::new("ID", 5)];
823 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
824 let mut table = Table::new().columns(columns).rows(rows).focused(true);
825 assert_eq!(table.cursor(), 0);
826
827 let end_msg = Message::new(KeyMsg::from_type(KeyType::End));
829 let _ = Model::update(&mut table, end_msg);
830
831 assert_eq!(table.cursor(), 2, "Table should go to bottom on End key");
832 }
833
834 #[test]
835 fn test_table_satisfies_model_bounds() {
836 fn requires_model<T: Model + Send + 'static>() {}
837 requires_model::<Table>();
838 }
839
840 #[test]
841 fn test_model_update_page_down() {
842 use bubbletea::{KeyMsg, KeyType, Message};
843
844 let columns = vec![Column::new("ID", 5)];
845 let rows: Vec<Row> = (1..=20).map(|i| vec![i.to_string()]).collect();
847 let mut table = Table::new()
848 .columns(columns)
849 .rows(rows)
850 .focused(true)
851 .height(5); assert_eq!(table.cursor(), 0);
854
855 let msg = Message::new(KeyMsg::from_type(KeyType::PgDown));
857 let _ = Model::update(&mut table, msg);
858
859 assert!(
861 table.cursor() > 0,
862 "Table should navigate down on PageDown key"
863 );
864 }
865
866 #[test]
867 fn test_model_update_page_up() {
868 use bubbletea::{KeyMsg, KeyType, Message};
869
870 let columns = vec![Column::new("ID", 5)];
871 let rows: Vec<Row> = (1..=20).map(|i| vec![i.to_string()]).collect();
872 let mut table = Table::new()
873 .columns(columns)
874 .rows(rows)
875 .focused(true)
876 .height(5);
877
878 table.set_cursor(10);
880 assert_eq!(table.cursor(), 10);
881
882 let msg = Message::new(KeyMsg::from_type(KeyType::PgUp));
884 let _ = Model::update(&mut table, msg);
885
886 assert!(
888 table.cursor() < 10,
889 "Table should navigate up on PageUp key"
890 );
891 }
892
893 #[test]
894 fn test_model_update_goto_top() {
895 use bubbletea::{KeyMsg, KeyType, Message};
896
897 let columns = vec![Column::new("ID", 5)];
898 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
899 let mut table = Table::new().columns(columns).rows(rows).focused(true);
900
901 table.set_cursor(2);
903 assert_eq!(table.cursor(), 2);
904
905 let msg = Message::new(KeyMsg::from_type(KeyType::Home));
907 let _ = Model::update(&mut table, msg);
908
909 assert_eq!(table.cursor(), 0, "Table should go to top on Home key");
910 }
911
912 #[test]
913 fn test_table_set_rows_replaces_data() {
914 let columns = vec![Column::new("Name", 10)];
915 let initial_rows = vec![vec!["Alice".into()], vec!["Bob".into()]];
916 let mut table = Table::new().columns(columns).rows(initial_rows);
917
918 assert_eq!(table.rows.len(), 2);
919 assert_eq!(table.rows[0][0], "Alice");
920
921 let new_rows = vec![
923 vec!["Charlie".into()],
924 vec!["Diana".into()],
925 vec!["Eve".into()],
926 ];
927 table.set_rows(new_rows);
928
929 assert_eq!(table.rows.len(), 3);
930 assert_eq!(table.rows[0][0], "Charlie");
931 assert_eq!(table.rows[1][0], "Diana");
932 assert_eq!(table.rows[2][0], "Eve");
933 }
934
935 #[test]
936 fn test_table_set_columns_updates_headers() {
937 let initial_cols = vec![Column::new("A", 5), Column::new("B", 5)];
938 let rows = vec![vec!["1".into(), "2".into()]];
939 let mut table = Table::new().columns(initial_cols).rows(rows);
940
941 assert_eq!(table.columns.len(), 2);
942 assert_eq!(table.columns[0].title, "A");
943
944 let new_cols = vec![
946 Column::new("X", 10),
947 Column::new("Y", 10),
948 Column::new("Z", 10),
949 ];
950 table.set_columns(new_cols);
951
952 assert_eq!(table.columns.len(), 3);
953 assert_eq!(table.columns[0].title, "X");
954 assert_eq!(table.columns[1].title, "Y");
955 assert_eq!(table.columns[2].title, "Z");
956 }
957
958 #[test]
959 fn test_table_single_row() {
960 use bubbletea::{KeyMsg, KeyType, Message};
961
962 let columns = vec![Column::new("Item", 10)];
963 let rows = vec![vec!["Only One".into()]];
964 let mut table = Table::new().columns(columns).rows(rows).focused(true);
965
966 assert_eq!(table.cursor(), 0);
967 assert_eq!(table.rows.len(), 1);
968
969 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
971 let _ = Model::update(&mut table, down_msg);
972 assert_eq!(table.cursor(), 0, "Single row table should not move down");
973
974 let up_msg = Message::new(KeyMsg::from_type(KeyType::Up));
976 let _ = Model::update(&mut table, up_msg);
977 assert_eq!(table.cursor(), 0, "Single row table should not move up");
978
979 assert!(table.selected_row().is_some());
981 assert_eq!(table.selected_row().unwrap()[0], "Only One");
982 }
983
984 #[test]
985 fn test_table_single_column() {
986 let columns = vec![Column::new("Solo", 15)];
987 let rows = vec![
988 vec!["Row 1".into()],
989 vec!["Row 2".into()],
990 vec!["Row 3".into()],
991 ];
992 let table = Table::new().columns(columns).rows(rows);
993
994 assert_eq!(table.columns.len(), 1);
995 assert_eq!(table.columns[0].title, "Solo");
996 assert_eq!(table.columns[0].width, 15);
997
998 let view = table.view();
1000 assert!(!view.is_empty());
1001 assert!(
1002 view.contains("Solo") || view.contains("Row"),
1003 "Single column table should render"
1004 );
1005 }
1006
1007 #[test]
1012 fn test_table_empty_navigation() {
1013 use bubbletea::{KeyMsg, KeyType, Message};
1014
1015 let mut table = Table::new().focused(true);
1016 assert!(table.rows.is_empty());
1017 assert_eq!(table.cursor(), 0);
1018
1019 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
1021 let _ = Model::update(&mut table, down_msg);
1022 assert_eq!(table.cursor(), 0, "Empty table cursor should stay at 0");
1023
1024 let up_msg = Message::new(KeyMsg::from_type(KeyType::Up));
1025 let _ = Model::update(&mut table, up_msg);
1026 assert_eq!(table.cursor(), 0, "Empty table cursor should stay at 0");
1027
1028 let end_msg = Message::new(KeyMsg::from_type(KeyType::End));
1029 let _ = Model::update(&mut table, end_msg);
1030 assert_eq!(
1031 table.cursor(),
1032 0,
1033 "Empty table goto_bottom should stay at 0"
1034 );
1035
1036 let home_msg = Message::new(KeyMsg::from_type(KeyType::Home));
1037 let _ = Model::update(&mut table, home_msg);
1038 assert_eq!(table.cursor(), 0, "Empty table goto_top should stay at 0");
1039 }
1040
1041 #[test]
1042 fn test_table_view_empty() {
1043 let table = Table::new();
1044 let view = table.view();
1045 let _ = view;
1048 }
1049
1050 #[test]
1051 fn test_table_view_renders_column_widths() {
1052 let columns = vec![Column::new("Short", 5), Column::new("LongerColumn", 15)];
1053 let rows = vec![vec!["A".into(), "B".into()]];
1054 let table = Table::new().columns(columns).rows(rows);
1055 let view = table.view();
1056
1057 assert!(!view.is_empty());
1059 assert!(view.contains("Short") || view.contains("Longer"));
1061 }
1062
1063 #[test]
1064 fn test_model_update_navigate_up() {
1065 use bubbletea::{KeyMsg, KeyType, Message};
1066
1067 let columns = vec![Column::new("ID", 5)];
1068 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
1069 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1070 table.set_cursor(2);
1071 assert_eq!(table.cursor(), 2);
1072
1073 let up_msg = Message::new(KeyMsg::from_type(KeyType::Up));
1075 let _ = Model::update(&mut table, up_msg);
1076
1077 assert_eq!(table.cursor(), 1, "Table should navigate up on Up key");
1078 }
1079
1080 #[test]
1081 fn test_table_view_with_long_content() {
1082 let columns = vec![Column::new("Name", 5)];
1083 let rows = vec![vec!["VeryLongNameThatExceedsColumnWidth".into()]];
1084 let table = Table::new().columns(columns).rows(rows);
1085 let view = table.view();
1086
1087 assert!(!view.is_empty());
1089 }
1090
1091 #[test]
1092 fn test_table_cursor_boundary_top() {
1093 use bubbletea::{KeyMsg, KeyType, Message};
1094
1095 let columns = vec![Column::new("ID", 5)];
1096 let rows = vec![vec!["1".into()], vec!["2".into()]];
1097 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1098 assert_eq!(table.cursor(), 0);
1099
1100 let up_msg = Message::new(KeyMsg::from_type(KeyType::Up));
1102 let _ = Model::update(&mut table, up_msg);
1103 assert_eq!(table.cursor(), 0, "Cursor should not go below 0");
1104 }
1105
1106 #[test]
1107 fn test_table_cursor_boundary_bottom() {
1108 use bubbletea::{KeyMsg, KeyType, Message};
1109
1110 let columns = vec![Column::new("ID", 5)];
1111 let rows = vec![vec!["1".into()], vec!["2".into()]];
1112 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1113 table.set_cursor(1);
1114 assert_eq!(table.cursor(), 1);
1115
1116 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
1118 let _ = Model::update(&mut table, down_msg);
1119 assert_eq!(table.cursor(), 1, "Cursor should not exceed row count");
1120 }
1121
1122 #[test]
1123 fn test_table_update_with_j_k_keys() {
1124 use bubbletea::{KeyMsg, Message};
1125
1126 let columns = vec![Column::new("ID", 5)];
1127 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
1128 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1129 assert_eq!(table.cursor(), 0);
1130
1131 let j_msg = Message::new(KeyMsg::from_char('j'));
1133 let _ = Model::update(&mut table, j_msg);
1134 assert_eq!(table.cursor(), 1, "'j' should move cursor down");
1135
1136 let k_msg = Message::new(KeyMsg::from_char('k'));
1138 let _ = Model::update(&mut table, k_msg);
1139 assert_eq!(table.cursor(), 0, "'k' should move cursor up");
1140 }
1141
1142 #[test]
1143 fn test_table_update_with_g_and_shift_g_keys() {
1144 use bubbletea::{KeyMsg, Message};
1145
1146 let columns = vec![Column::new("ID", 5)];
1147 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
1148 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1149 assert_eq!(table.cursor(), 0);
1150
1151 let g_upper_msg = Message::new(KeyMsg::from_char('G'));
1153 let _ = Model::update(&mut table, g_upper_msg);
1154 assert_eq!(table.cursor(), 2, "'G' should go to bottom");
1155
1156 let g_msg = Message::new(KeyMsg::from_char('g'));
1158 let _ = Model::update(&mut table, g_msg);
1159 assert_eq!(table.cursor(), 0, "'g' should go to top");
1160 }
1161
1162 #[test]
1163 fn test_table_height_affects_pagination() {
1164 use bubbletea::{KeyMsg, KeyType, Message};
1165
1166 let columns = vec![Column::new("ID", 5)];
1167 let rows: Vec<Row> = (1..=20).map(|i| vec![i.to_string()]).collect();
1169 let mut table = Table::new()
1170 .columns(columns)
1171 .rows(rows)
1172 .focused(true)
1173 .height(3); assert_eq!(table.cursor(), 0);
1176
1177 let pgdown_msg = Message::new(KeyMsg::from_type(KeyType::PgDown));
1179 let _ = Model::update(&mut table, pgdown_msg);
1180
1181 assert!(table.cursor() > 0, "PageDown should move cursor down");
1183 }
1184
1185 #[test]
1186 fn test_table_selected_row_after_navigation() {
1187 use bubbletea::{KeyMsg, KeyType, Message};
1188
1189 let columns = vec![Column::new("Name", 10)];
1190 let rows = vec![
1191 vec!["Alice".into()],
1192 vec!["Bob".into()],
1193 vec!["Carol".into()],
1194 ];
1195 let mut table = Table::new().columns(columns).rows(rows).focused(true);
1196
1197 assert_eq!(table.selected_row().unwrap()[0], "Alice");
1198
1199 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
1200 let _ = Model::update(&mut table, down_msg);
1201 assert_eq!(table.selected_row().unwrap()[0], "Bob");
1202
1203 let _ = Model::update(&mut table, Message::new(KeyMsg::from_type(KeyType::Down)));
1204 assert_eq!(table.selected_row().unwrap()[0], "Carol");
1205 }
1206
1207 mod mouse_tests {
1212 use super::*;
1213 use bubbletea::Message;
1214
1215 fn wheel_up_msg() -> Message {
1216 Message::new(MouseMsg {
1217 x: 0,
1218 y: 0,
1219 shift: false,
1220 alt: false,
1221 ctrl: false,
1222 action: MouseAction::Press,
1223 button: MouseButton::WheelUp,
1224 })
1225 }
1226
1227 fn wheel_down_msg() -> Message {
1228 Message::new(MouseMsg {
1229 x: 0,
1230 y: 0,
1231 shift: false,
1232 alt: false,
1233 ctrl: false,
1234 action: MouseAction::Press,
1235 button: MouseButton::WheelDown,
1236 })
1237 }
1238
1239 fn click_msg(x: u16, y: u16) -> Message {
1240 Message::new(MouseMsg {
1241 x,
1242 y,
1243 shift: false,
1244 alt: false,
1245 ctrl: false,
1246 action: MouseAction::Press,
1247 button: MouseButton::Left,
1248 })
1249 }
1250
1251 #[test]
1252 fn test_table_mouse_wheel_scroll_down() {
1253 let rows = vec![
1254 vec!["1".into()],
1255 vec!["2".into()],
1256 vec!["3".into()],
1257 vec!["4".into()],
1258 vec!["5".into()],
1259 ];
1260 let mut table = Table::new().rows(rows).focused(true);
1261 assert_eq!(table.cursor(), 0);
1262
1263 table.update(&wheel_down_msg());
1264 assert_eq!(table.cursor(), 3);
1266 }
1267
1268 #[test]
1269 fn test_table_mouse_wheel_scroll_up() {
1270 let rows = vec![
1271 vec!["1".into()],
1272 vec!["2".into()],
1273 vec!["3".into()],
1274 vec!["4".into()],
1275 vec!["5".into()],
1276 ];
1277 let mut table = Table::new().rows(rows).focused(true);
1278
1279 table.goto_bottom();
1281 assert_eq!(table.cursor(), 4);
1282
1283 table.update(&wheel_up_msg());
1284 assert_eq!(table.cursor(), 1);
1286 }
1287
1288 #[test]
1289 fn test_table_mouse_click_select_row() {
1290 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
1291 let mut table = Table::new().rows(rows).focused(true);
1292 assert_eq!(table.cursor(), 0);
1293
1294 table.update(&click_msg(5, 2));
1296 assert_eq!(table.cursor(), 1);
1297
1298 table.update(&click_msg(5, 3));
1300 assert_eq!(table.cursor(), 2);
1301 }
1302
1303 #[test]
1304 fn test_table_mouse_click_header_does_nothing() {
1305 let rows = vec![vec!["1".into()], vec!["2".into()]];
1306 let mut table = Table::new().rows(rows).focused(true);
1307 assert_eq!(table.cursor(), 0);
1308
1309 table.update(&click_msg(5, 0));
1311 assert_eq!(table.cursor(), 0);
1313 }
1314
1315 #[test]
1316 fn test_table_mouse_click_out_of_bounds() {
1317 let rows = vec![vec!["1".into()], vec!["2".into()]];
1318 let mut table = Table::new().rows(rows).focused(true);
1319 assert_eq!(table.cursor(), 0);
1320
1321 table.update(&click_msg(5, 100));
1323 assert_eq!(table.cursor(), 0);
1325 }
1326
1327 #[test]
1328 fn test_table_mouse_disabled() {
1329 let rows = vec![vec!["1".into()], vec!["2".into()], vec!["3".into()]];
1330 let mut table = Table::new()
1331 .rows(rows)
1332 .focused(true)
1333 .mouse_wheel(false)
1334 .mouse_click(false);
1335
1336 table.update(&wheel_down_msg());
1338 assert_eq!(table.cursor(), 0);
1339
1340 table.update(&click_msg(5, 2));
1342 assert_eq!(table.cursor(), 0);
1343 }
1344
1345 #[test]
1346 fn test_table_mouse_not_focused() {
1347 let rows = vec![vec!["1".into()], vec!["2".into()]];
1348 let mut table = Table::new().rows(rows).focused(false);
1349
1350 table.update(&wheel_down_msg());
1352 assert_eq!(table.cursor(), 0);
1353
1354 table.update(&click_msg(5, 2));
1355 assert_eq!(table.cursor(), 0);
1356 }
1357
1358 #[test]
1359 fn test_table_mouse_wheel_delta_builder() {
1360 let rows = vec![
1361 vec!["1".into()],
1362 vec!["2".into()],
1363 vec!["3".into()],
1364 vec!["4".into()],
1365 vec!["5".into()],
1366 ];
1367 let mut table = Table::new().rows(rows).focused(true).mouse_wheel_delta(1); table.update(&wheel_down_msg());
1370 assert_eq!(table.cursor(), 1); }
1372
1373 #[test]
1374 fn test_table_mouse_release_ignored() {
1375 let rows = vec![vec!["1".into()], vec!["2".into()]];
1376 let mut table = Table::new().rows(rows).focused(true);
1377
1378 let release_msg = Message::new(MouseMsg {
1380 x: 5,
1381 y: 2,
1382 shift: false,
1383 alt: false,
1384 ctrl: false,
1385 action: MouseAction::Release,
1386 button: MouseButton::Left,
1387 });
1388 table.update(&release_msg);
1389 assert_eq!(table.cursor(), 0);
1390 }
1391 }
1392}