bubbletea_widgets/table.rs
1//! Interactive data table component with navigation, selection, and scrolling capabilities.
2//!
3//! This module provides a comprehensive table implementation for terminal user interfaces,
4//! designed for displaying structured data with keyboard navigation and visual selection.
5//! It features efficient viewport-based rendering, customizable styling, and integration
6//! with the Bubble Tea architecture.
7//!
8//! # Core Components
9//!
10//! - **`Model`**: The main table with data, state, and navigation logic
11//! - **`Column`**: Column definitions with titles and width constraints
12//! - **`Row`**: Row data containing cell values
13//! - **`Styles`**: Visual styling for headers, cells, and selection states
14//! - **`TableKeyMap`**: Configurable keyboard bindings for navigation
15//!
16//! # Key Features
17//!
18//! - **Vim-Style Navigation**: Familiar keyboard shortcuts for power users
19//! - **Viewport Scrolling**: Efficient rendering of large datasets
20//! - **Selection Highlighting**: Visual feedback for the current row
21//! - **Responsive Layout**: Automatic column width and table sizing
22//! - **Help Integration**: Built-in documentation of key bindings
23//! - **Customizable Styling**: Full control over appearance and colors
24//!
25//! # Navigation Controls
26//!
27//! | Keys | Action | Description |
28//! |------|--------| ----------- |
29//! | `↑`, `k` | Row Up | Move selection up one row |
30//! | `↓`, `j` | Row Down | Move selection down one row |
31//! | `PgUp`, `b` | Page Up | Move up one page of rows |
32//! | `PgDn`, `f` | Page Down | Move down one page of rows |
33//! | `u` | Half Page Up | Move up half a page |
34//! | `d` | Half Page Down | Move down half a page |
35//! | `Home`, `g` | Go to Start | Jump to first row |
36//! | `End`, `G` | Go to End | Jump to last row |
37//!
38//! # Quick Start
39//!
40//! ```rust
41//! use bubbletea_widgets::table::{Model, Column, Row};
42//!
43//! // Define table structure
44//! let columns = vec![
45//! Column::new("Product", 25),
46//! Column::new("Price", 10),
47//! Column::new("Stock", 8),
48//! ];
49//!
50//! // Add data rows
51//! let rows = vec![
52//! Row::new(vec!["MacBook Pro".into(), "$2399".into(), "5".into()]),
53//! Row::new(vec!["iPad Air".into(), "$599".into(), "12".into()]),
54//! Row::new(vec!["AirPods Pro".into(), "$249".into(), "23".into()]),
55//! ];
56//!
57//! // Create and configure table
58//! let mut table = Model::new(columns)
59//! .with_rows(rows);
60//! table.set_width(50);
61//! table.set_height(10);
62//! ```
63//!
64//! # Integration with Bubble Tea
65//!
66//! ```rust
67//! use bubbletea_widgets::table::{Model as TableModel, Column, Row};
68//! use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg, KeyMsg};
69//!
70//! struct App {
71//! table: TableModel,
72//! }
73//!
74//! impl BubbleTeaModel for App {
75//! fn init() -> (Self, Option<Cmd>) {
76//! let table = TableModel::new(vec![
77//! Column::new("ID", 8),
78//! Column::new("Name", 20),
79//! Column::new("Status", 12),
80//! ]);
81//! (App { table }, None)
82//! }
83//!
84//! fn update(&mut self, msg: Msg) -> Option<Cmd> {
85//! // Forward navigation messages to table
86//! self.table.update(msg)
87//! }
88//!
89//! fn view(&self) -> String {
90//! format!("My Data Table:\n\n{}", self.table.view())
91//! }
92//! }
93//! ```
94//!
95//! # Styling and Customization
96//!
97//! ```rust
98//! use bubbletea_widgets::table::{Model, Column, Row, Styles};
99//! use lipgloss_extras::prelude::*;
100//!
101//! let mut table = Model::new(vec![Column::new("Data", 20)]);
102//!
103//! // Customize appearance
104//! table.styles = Styles {
105//! header: Style::new()
106//! .bold(true)
107//! .background(Color::from("#1e40af"))
108//! .foreground(Color::from("#ffffff"))
109//! .padding(0, 1, 0, 1),
110//! cell: Style::new()
111//! .padding(0, 1, 0, 1),
112//! selected: Style::new()
113//! .bold(true)
114//! .background(Color::from("#10b981"))
115//! .foreground(Color::from("#ffffff")),
116//! };
117//! ```
118//!
119//! # Performance Considerations
120//!
121//! - Uses viewport rendering for efficient display of large datasets
122//! - Only visible rows are rendered, enabling smooth performance with thousands of rows
123//! - Column widths should be set appropriately to avoid layout recalculation
124//! - Selection changes trigger content rebuilding, but viewport limits render cost
125
126use crate::{
127 help,
128 key::{self, KeyMap as KeyMapTrait},
129 viewport,
130};
131use bubbletea_rs::{Cmd, KeyMsg, Model as BubbleTeaModel, Msg};
132use crossterm::event::KeyCode;
133use lipgloss_extras::prelude::*;
134use lipgloss_extras::table::Table as LGTable;
135
136/// Represents a table column with its title and display width.
137///
138/// Columns define the structure of the table by specifying headers and how much
139/// horizontal space each column should occupy. The width is used for text wrapping
140/// and alignment within the column boundaries.
141///
142/// # Examples
143///
144/// ```rust
145/// use bubbletea_widgets::table::Column;
146///
147/// // Create a column for names with 20 character width
148/// let name_col = Column::new("Full Name", 20);
149/// assert_eq!(name_col.title, "Full Name");
150/// assert_eq!(name_col.width, 20);
151/// ```
152///
153/// Multiple columns for a complete table structure:
154/// ```rust
155/// use bubbletea_widgets::table::Column;
156///
157/// let columns = vec![
158/// Column::new("ID", 8), // Short numeric ID
159/// Column::new("Description", 35), // Longer text field
160/// Column::new("Status", 12), // Medium status field
161/// ];
162/// ```
163///
164/// # Width Guidelines
165///
166/// - **Numeric columns**: 6-10 characters usually sufficient
167/// - **Short text**: 10-15 characters for codes, statuses
168/// - **Names/titles**: 20-30 characters for readable display
169/// - **Descriptions**: 30+ characters for detailed content
170#[derive(Debug, Clone)]
171pub struct Column {
172 /// The display title for this column header.
173 ///
174 /// This text will be shown in the table header row and should be
175 /// descriptive enough for users to understand the column content.
176 pub title: String,
177 /// The display width for this column in characters.
178 ///
179 /// This determines how much horizontal space the column occupies.
180 /// Content longer than this width will be wrapped or truncated
181 /// depending on the styling configuration.
182 pub width: i32,
183}
184
185impl Column {
186 /// Creates a new column with the specified title and width.
187 ///
188 /// # Arguments
189 ///
190 /// * `title` - The column header text (accepts any type convertible to String)
191 /// * `width` - The display width in characters (should be positive)
192 ///
193 /// # Returns
194 ///
195 /// A new `Column` instance ready for use in table construction
196 ///
197 /// # Examples
198 ///
199 /// ```rust
200 /// use bubbletea_widgets::table::Column;
201 ///
202 /// // Using string literals
203 /// let col1 = Column::new("User ID", 10);
204 ///
205 /// // Using String values
206 /// let title = String::from("Email Address");
207 /// let col2 = Column::new(title, 30);
208 ///
209 /// // Using &str references
210 /// let header = "Created Date";
211 /// let col3 = Column::new(header, 15);
212 /// ```
213 ///
214 /// Creating columns for different data types:
215 /// ```rust
216 /// use bubbletea_widgets::table::Column;
217 ///
218 /// let columns = vec![
219 /// Column::new("#", 5), // Row numbers
220 /// Column::new("Name", 25), // Person names
221 /// Column::new("Email", 30), // Email addresses
222 /// Column::new("Joined", 12), // Dates
223 /// Column::new("Active", 8), // Boolean status
224 /// ];
225 /// ```
226 pub fn new(title: impl Into<String>, width: i32) -> Self {
227 Self {
228 title: title.into(),
229 width,
230 }
231 }
232}
233
234/// Represents a single row of data in the table.
235///
236/// Each row contains a vector of cell values (as strings) that correspond to the
237/// columns defined in the table. The order of cells should match the order of
238/// columns for proper display alignment.
239///
240/// # Examples
241///
242/// ```rust
243/// use bubbletea_widgets::table::Row;
244///
245/// // Create a row with user data
246/// let user_row = Row::new(vec![
247/// "12345".to_string(),
248/// "Alice Johnson".to_string(),
249/// "alice@example.com".to_string(),
250/// "Active".to_string(),
251/// ]);
252/// assert_eq!(user_row.cells.len(), 4);
253/// ```
254///
255/// Using the `into()` conversion for convenient syntax:
256/// ```rust
257/// use bubbletea_widgets::table::Row;
258///
259/// let product_row = Row::new(vec![
260/// "SKU-001".into(),
261/// "Wireless Mouse".into(),
262/// "$29.99".into(),
263/// "In Stock".into(),
264/// ]);
265/// ```
266///
267/// # Data Alignment
268///
269/// Cell data should align with column definitions:
270/// ```rust
271/// use bubbletea_widgets::table::{Column, Row};
272///
273/// let columns = vec![
274/// Column::new("ID", 8),
275/// Column::new("Product", 20),
276/// Column::new("Price", 10),
277/// ];
278///
279/// // Row data should match column order
280/// let row = Row::new(vec![
281/// "PROD-123".into(), // Goes in ID column
282/// "Laptop Stand".into(), // Goes in Product column
283/// "$49.99".into(), // Goes in Price column
284/// ]);
285/// ```
286#[derive(Debug, Clone)]
287pub struct Row {
288 /// The cell values for this row.
289 ///
290 /// Each string represents the content of one cell, and the vector
291 /// order should correspond to the table's column order. All values
292 /// are stored as strings regardless of their logical data type.
293 pub cells: Vec<String>,
294}
295
296impl Row {
297 /// Creates a new table row with the specified cell values.
298 ///
299 /// # Arguments
300 ///
301 /// * `cells` - Vector of strings representing the data for each column
302 ///
303 /// # Returns
304 ///
305 /// A new `Row` instance containing the provided cell data
306 ///
307 /// # Examples
308 ///
309 /// ```rust
310 /// use bubbletea_widgets::table::Row;
311 ///
312 /// // Create a simple data row
313 /// let row = Row::new(vec![
314 /// "001".to_string(),
315 /// "John Doe".to_string(),
316 /// "Engineer".to_string(),
317 /// ]);
318 /// assert_eq!(row.cells[0], "001");
319 /// assert_eq!(row.cells[1], "John Doe");
320 /// ```
321 ///
322 /// Using `.into()` for more concise syntax:
323 /// ```rust
324 /// use bubbletea_widgets::table::Row;
325 ///
326 /// let employees = vec![
327 /// Row::new(vec!["E001".into(), "Alice".into(), "Manager".into()]),
328 /// Row::new(vec!["E002".into(), "Bob".into(), "Developer".into()]),
329 /// Row::new(vec!["E003".into(), "Carol".into(), "Designer".into()]),
330 /// ];
331 /// ```
332 ///
333 /// # Cell Count Considerations
334 ///
335 /// While not enforced at construction time, rows should typically have the same
336 /// number of cells as there are columns in the table:
337 ///
338 /// ```rust
339 /// use bubbletea_widgets::table::{Column, Row};
340 ///
341 /// let columns = vec![
342 /// Column::new("Name", 15),
343 /// Column::new("Age", 5),
344 /// Column::new("City", 15),
345 /// ];
346 ///
347 /// // Good: 3 cells for 3 columns
348 /// let good_row = Row::new(vec!["John".into(), "30".into(), "NYC".into()]);
349 ///
350 /// // Will work but may display oddly: 2 cells for 3 columns
351 /// let short_row = Row::new(vec!["Jane".into(), "25".into()]);
352 /// ```
353 pub fn new(cells: Vec<String>) -> Self {
354 Self { cells }
355 }
356}
357
358/// Visual styling configuration for table rendering.
359///
360/// This struct defines the appearance of different table elements including headers,
361/// regular cells, and selected rows. Each style can control colors, padding, borders,
362/// and text formatting using the lipgloss styling system.
363///
364/// # Examples
365///
366/// ```rust
367/// use bubbletea_widgets::table::Styles;
368/// use lipgloss_extras::prelude::*;
369///
370/// // Create custom styling
371/// let styles = Styles {
372/// header: Style::new()
373/// .bold(true)
374/// .background(Color::from("#2563eb"))
375/// .foreground(Color::from("#ffffff"))
376/// .padding(0, 1, 0, 1),
377/// cell: Style::new()
378/// .padding(0, 1, 0, 1)
379/// .foreground(Color::from("#374151")),
380/// selected: Style::new()
381/// .bold(true)
382/// .background(Color::from("#10b981"))
383/// .foreground(Color::from("#ffffff")),
384/// };
385/// ```
386///
387/// Using styles with a table:
388/// ```rust
389/// use bubbletea_widgets::table::{Model, Column, Styles};
390/// use lipgloss_extras::prelude::*;
391///
392/// let mut table = Model::new(vec![Column::new("Name", 20)]);
393/// table.styles = Styles {
394/// header: Style::new().bold(true).background(Color::from("blue")),
395/// cell: Style::new().padding(0, 1, 0, 1),
396/// selected: Style::new().background(Color::from("green")),
397/// };
398/// ```
399///
400/// # Styling Guidelines
401///
402/// - **Headers**: Usually bold with distinct background colors
403/// - **Cells**: Minimal styling with consistent padding for readability
404/// - **Selected**: High contrast colors to clearly indicate selection
405/// - **Padding**: `padding(top, right, bottom, left)` for consistent spacing
406#[derive(Debug, Clone)]
407pub struct Styles {
408 /// Style for table header cells.
409 ///
410 /// Applied to the first row containing column titles. Typically uses
411 /// bold text and distinct background colors to differentiate from data rows.
412 pub header: Style,
413 /// Style for regular data cells.
414 ///
415 /// Applied to all non-selected data rows. Should provide good readability
416 /// with appropriate padding and neutral colors.
417 pub cell: Style,
418 /// Style for the currently selected row.
419 ///
420 /// Applied to highlight the active selection. Should use high contrast
421 /// colors to clearly indicate which row is selected.
422 pub selected: Style,
423}
424
425impl Default for Styles {
426 /// Creates default table styling with reasonable visual defaults.
427 ///
428 /// The default styles provide:
429 /// - **Header**: Bold text with padding for clear column identification
430 /// - **Cell**: Simple padding for consistent data alignment
431 /// - **Selected**: Bold text with a distinct foreground color (#212 - light purple)
432 ///
433 /// # Examples
434 ///
435 /// ```rust
436 /// use bubbletea_widgets::table::{Styles, Model, Column};
437 ///
438 /// // Using default styles
439 /// let table = Model::new(vec![Column::new("Data", 20)]);
440 /// // table.styles is automatically set to Styles::default()
441 ///
442 /// // Explicitly using defaults
443 /// let styles = Styles::default();
444 /// ```
445 ///
446 /// # Style Details
447 ///
448 /// - Header padding: `(0, 1, 0, 1)` adds horizontal spacing
449 /// - Cell padding: `(0, 1, 0, 1)` maintains consistent alignment
450 /// - Selected color: `"212"` is a light purple that works on most terminals
451 fn default() -> Self {
452 Self {
453 header: Style::new().bold(true).padding(0, 1, 0, 1),
454 cell: Style::new().padding(0, 1, 0, 1),
455 selected: Style::new().bold(true).foreground(Color::from("212")),
456 }
457 }
458}
459
460/// Keyboard binding configuration for table navigation.
461///
462/// This struct defines all the key combinations that control table navigation,
463/// including row-by-row movement, page scrolling, and jumping to start/end positions.
464/// Each binding can accept multiple key combinations and includes help text for documentation.
465///
466/// # Key Binding Types
467///
468/// - **Row Navigation**: Single row up/down movement
469/// - **Page Navigation**: Full page up/down scrolling
470/// - **Half Page Navigation**: Half page up/down scrolling
471/// - **Jump Navigation**: Instant movement to start/end
472///
473/// # Examples
474///
475/// ```rust
476/// use bubbletea_widgets::table::{TableKeyMap, Model, Column};
477/// use bubbletea_widgets::key;
478/// use crossterm::event::KeyCode;
479///
480/// // Create table with custom key bindings
481/// let mut table = Model::new(vec![Column::new("Data", 20)]);
482/// table.keymap.row_up = key::Binding::new(vec![KeyCode::Char('w')])
483/// .with_help("w", "move up");
484/// table.keymap.row_down = key::Binding::new(vec![KeyCode::Char('s')])
485/// .with_help("s", "move down");
486/// ```
487///
488/// Using with help system:
489/// ```rust
490/// use bubbletea_widgets::table::Model;
491/// use bubbletea_widgets::key::KeyMap as KeyMapTrait;
492///
493/// let table = Model::new(vec![]);
494/// let help_bindings = table.keymap.short_help();
495/// // Returns the most common navigation keys for display
496/// ```
497///
498/// # Default Bindings
499///
500/// - **Row Up**: `↑` (Up Arrow), `k` (Vim style)
501/// - **Row Down**: `↓` (Down Arrow), `j` (Vim style)
502/// - **Page Up**: `PgUp`, `b` (Vim style)
503/// - **Page Down**: `PgDn`, `f` (Vim style)
504/// - **Half Page Up**: `u` (Vim style)
505/// - **Half Page Down**: `d` (Vim style)
506/// - **Go to Start**: `Home`, `g` (Vim style)
507/// - **Go to End**: `End`, `G` (Vim style)
508#[derive(Debug, Clone)]
509pub struct TableKeyMap {
510 /// Key binding for moving selection up one row.
511 ///
512 /// Default: Up arrow key (`↑`) and `k` key (Vim-style)
513 pub row_up: key::Binding,
514 /// Key binding for moving selection down one row.
515 ///
516 /// Default: Down arrow key (`↓`) and `j` key (Vim-style)
517 pub row_down: key::Binding,
518 /// Key binding for moving up one full page of rows.
519 ///
520 /// Default: Page Up key and `b` key (Vim-style)
521 pub page_up: key::Binding,
522 /// Key binding for moving down one full page of rows.
523 ///
524 /// Default: Page Down key and `f` key (Vim-style)
525 pub page_down: key::Binding,
526 /// Key binding for moving up half a page of rows.
527 ///
528 /// Default: `u` key (Vim-style)
529 pub half_page_up: key::Binding,
530 /// Key binding for moving down half a page of rows.
531 ///
532 /// Default: `d` key (Vim-style)
533 pub half_page_down: key::Binding,
534 /// Key binding for jumping to the first row.
535 ///
536 /// Default: Home key and `g` key (Vim-style)
537 pub go_to_start: key::Binding,
538 /// Key binding for jumping to the last row.
539 ///
540 /// Default: End key and `G` key (Vim-style)
541 pub go_to_end: key::Binding,
542}
543
544impl Default for TableKeyMap {
545 /// Creates default table key bindings with Vim-style navigation.
546 ///
547 /// The default bindings provide both traditional arrow keys and Vim-style letter keys
548 /// for maximum compatibility and user preference accommodation.
549 ///
550 /// # Default Key Mappings
551 ///
552 /// | Binding | Keys | Description |
553 /// |---------|------|-------------|
554 /// | `row_up` | `↑`, `k` | Move selection up one row |
555 /// | `row_down` | `↓`, `j` | Move selection down one row |
556 /// | `page_up` | `PgUp`, `b` | Move up one page of rows |
557 /// | `page_down` | `PgDn`, `f` | Move down one page of rows |
558 /// | `half_page_up` | `u` | Move up half a page |
559 /// | `half_page_down` | `d` | Move down half a page |
560 /// | `go_to_start` | `Home`, `g` | Jump to first row |
561 /// | `go_to_end` | `End`, `G` | Jump to last row |
562 ///
563 /// # Examples
564 ///
565 /// ```rust
566 /// use bubbletea_widgets::table::TableKeyMap;
567 ///
568 /// // Using default key bindings
569 /// let keymap = TableKeyMap::default();
570 ///
571 /// // Check if a binding includes specific help text
572 /// assert_eq!(keymap.row_up.help().key, "↑/k");
573 /// assert_eq!(keymap.row_up.help().desc, "up");
574 /// ```
575 ///
576 /// # Design Philosophy
577 ///
578 /// - **Vim Compatibility**: Letter keys follow Vim navigation patterns
579 /// - **Arrow Key Support**: Traditional navigation for all users
580 /// - **Page Navigation**: Efficient movement through large datasets
581 /// - **Jump Commands**: Quick access to start/end positions
582 fn default() -> Self {
583 Self {
584 row_up: key::Binding::new(vec![KeyCode::Up, KeyCode::Char('k')]).with_help("↑/k", "up"),
585 row_down: key::Binding::new(vec![KeyCode::Down, KeyCode::Char('j')])
586 .with_help("↓/j", "down"),
587 page_up: key::Binding::new(vec![KeyCode::PageUp, KeyCode::Char('b')])
588 .with_help("pgup/b", "page up"),
589 page_down: key::Binding::new(vec![KeyCode::PageDown, KeyCode::Char('f')])
590 .with_help("pgdn/f", "page down"),
591 half_page_up: key::Binding::new(vec![KeyCode::Char('u')]).with_help("u", "½ page up"),
592 half_page_down: key::Binding::new(vec![KeyCode::Char('d')])
593 .with_help("d", "½ page down"),
594 go_to_start: key::Binding::new(vec![KeyCode::Home, KeyCode::Char('g')])
595 .with_help("g/home", "go to start"),
596 go_to_end: key::Binding::new(vec![KeyCode::End, KeyCode::Char('G')])
597 .with_help("G/end", "go to end"),
598 }
599 }
600}
601
602impl KeyMapTrait for TableKeyMap {
603 fn short_help(&self) -> Vec<&key::Binding> {
604 vec![&self.row_up, &self.row_down, &self.page_up, &self.page_down]
605 }
606 fn full_help(&self) -> Vec<Vec<&key::Binding>> {
607 vec![
608 vec![&self.row_up, &self.row_down],
609 vec![&self.page_up, &self.page_down],
610 vec![&self.half_page_up, &self.half_page_down],
611 vec![&self.go_to_start, &self.go_to_end],
612 ]
613 }
614}
615
616/// Interactive table model containing data, styling, navigation state,
617/// and a viewport for efficient rendering and scrolling.
618#[derive(Debug, Clone)]
619pub struct Model {
620 /// Column definitions controlling headers and widths.
621 pub columns: Vec<Column>,
622 /// Row data; each row contains one string per column.
623 pub rows: Vec<Row>,
624 /// Index of the currently selected row (0-based).
625 pub selected: usize,
626 /// Rendered width of the table in characters.
627 pub width: i32,
628 /// Rendered height of the table in lines.
629 pub height: i32,
630 /// Key bindings for navigation and movement.
631 pub keymap: TableKeyMap,
632 /// Styles used when rendering the table.
633 pub styles: Styles,
634 /// Whether this table currently has keyboard focus.
635 pub focus: bool,
636 /// Help model used to render key binding help.
637 pub help: help::Model,
638 /// Internal viewport that manages scrolling of rendered lines.
639 viewport: viewport::Model,
640}
641
642impl Model {
643 /// Creates a new table with the given `columns` and sensible defaults.
644 ///
645 /// Defaults: height 20, focused, empty rows, and default styles/keymap.
646 pub fn new(columns: Vec<Column>) -> Self {
647 let mut s = Self {
648 columns,
649 rows: Vec::new(),
650 selected: 0,
651 width: 0,
652 height: 20,
653 keymap: TableKeyMap::default(),
654 styles: Styles::default(),
655 focus: true,
656 help: help::Model::new(),
657 viewport: viewport::Model::new(0, 0),
658 };
659 // Initialize viewport dimensions
660 s.sync_viewport_dimensions();
661 s.rebuild_viewport_content();
662 s
663 }
664
665 /// Sets the table rows on construction and returns `self` for chaining.
666 pub fn with_rows(mut self, rows: Vec<Row>) -> Self {
667 self.rows = rows;
668 self
669 }
670 /// Sets the table width in characters and rebuilds the viewport content.
671 pub fn set_width(&mut self, w: i32) {
672 self.width = w;
673 self.sync_viewport_dimensions();
674 self.rebuild_viewport_content();
675 }
676 /// Sets the table height in lines and rebuilds the viewport content.
677 pub fn set_height(&mut self, h: i32) {
678 self.height = h;
679 self.sync_viewport_dimensions();
680 self.rebuild_viewport_content();
681 }
682 /// Appends a row to the table and refreshes the rendered content.
683 pub fn add_row(&mut self, row: Row) {
684 self.rows.push(row);
685 self.rebuild_viewport_content();
686 }
687 /// Returns a reference to the currently selected row, if any.
688 pub fn selected_row(&self) -> Option<&Row> {
689 self.rows.get(self.selected)
690 }
691 /// Moves the selection down by one row.
692 pub fn select_next(&mut self) {
693 if !self.rows.is_empty() {
694 self.selected = (self.selected + 1).min(self.rows.len() - 1);
695 }
696 }
697 /// Moves the selection up by one row.
698 pub fn select_prev(&mut self) {
699 if !self.rows.is_empty() {
700 self.selected = self.selected.saturating_sub(1);
701 }
702 }
703
704 /// Renders the table as a string.
705 pub fn view(&self) -> String {
706 // Render table directly to string
707 let mut tbl = LGTable::new();
708 if self.width > 0 {
709 tbl = tbl.width(self.width);
710 }
711
712 let headers: Vec<String> = self.columns.iter().map(|c| c.title.clone()).collect();
713 tbl = tbl.headers(headers);
714
715 let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
716 let cell_style = self.styles.cell.clone();
717 let header_style = self.styles.header.clone();
718 let selected_row = self.selected as i32;
719 let selected_style = self.styles.selected.clone();
720 tbl = tbl.style_func_boxed(Box::new(move |row: i32, col: usize| {
721 let mut s = if row == lipgloss_extras::table::HEADER_ROW {
722 header_style.clone()
723 } else {
724 cell_style.clone()
725 };
726 if let Some(w) = widths.get(col) {
727 s = s.width(*w);
728 }
729 if row >= 0 && row == selected_row {
730 s = selected_style.clone().inherit(s);
731 }
732 s
733 }));
734
735 let row_vecs: Vec<Vec<String>> = self.rows.iter().map(|r| r.cells.clone()).collect();
736 tbl = tbl.rows(row_vecs);
737 tbl.to_string()
738 }
739
740 fn rebuild_viewport_content(&mut self) {
741 let mut tbl = LGTable::new();
742 if self.width > 0 {
743 tbl = tbl.width(self.width);
744 }
745 // Don't set table height; viewport will handle vertical scrolling
746
747 // Headers
748 let headers: Vec<String> = self.columns.iter().map(|c| c.title.clone()).collect();
749 tbl = tbl.headers(headers);
750
751 // Column widths via style_func
752 let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
753 let cell_style = self.styles.cell.clone();
754 let header_style = self.styles.header.clone();
755 let selected_row = self.selected as i32; // data rows are 0-based in lipgloss-table
756 let selected_style = self.styles.selected.clone();
757 tbl = tbl.style_func_boxed(Box::new(move |row: i32, col: usize| {
758 let mut s = if row == lipgloss_extras::table::HEADER_ROW {
759 header_style.clone()
760 } else {
761 cell_style.clone()
762 };
763 if let Some(w) = widths.get(col) {
764 s = s.width(*w);
765 }
766 if row >= 0 && row == selected_row {
767 s = selected_style.clone().inherit(s);
768 }
769 s
770 }));
771
772 // Rows
773 let row_vecs: Vec<Vec<String>> = self.rows.iter().map(|r| r.cells.clone()).collect();
774 tbl = tbl.rows(row_vecs);
775
776 let rendered = tbl.to_string();
777 let lines: Vec<String> = rendered.split('\n').map(|s| s.to_string()).collect();
778 self.viewport.set_content_lines(lines);
779
780 // Ensure selection is visible (header is line 0; rows begin at line 1)
781 self.ensure_selected_visible();
782 }
783
784 fn ensure_selected_visible(&mut self) {
785 let target_line = self.selected.saturating_add(1); // account for header
786 let h = (self.height.max(1)) as usize;
787 let top = self.viewport.y_offset;
788 let bottom = top.saturating_add(h.saturating_sub(1));
789 if target_line < top {
790 self.viewport.set_y_offset(target_line);
791 } else if target_line > bottom {
792 let new_top = target_line.saturating_sub(h.saturating_sub(1));
793 self.viewport.set_y_offset(new_top);
794 }
795 }
796
797 fn sync_viewport_dimensions(&mut self) {
798 self.viewport.width = self.width.max(0) as usize;
799 self.viewport.height = self.height.max(0) as usize;
800 }
801
802 /// Gives keyboard focus to the table.
803 pub fn focus(&mut self) {
804 self.focus = true;
805 }
806 /// Removes keyboard focus from the table.
807 pub fn blur(&mut self) {
808 self.focus = false;
809 }
810
811 /// Renders the help view for the table's key bindings.
812 pub fn help_view(&self) -> String {
813 self.help.view(self)
814 }
815}
816
817impl BubbleTeaModel for Model {
818 /// Creates a new empty table model for Bubble Tea applications.
819 ///
820 /// This initialization method creates a table with no columns or data,
821 /// suitable for applications that will configure the table structure
822 /// after initialization. The table starts focused and ready to receive
823 /// keyboard input.
824 ///
825 /// # Returns
826 ///
827 /// A tuple containing the new table model and no initial command
828 ///
829 /// # Examples
830 ///
831 /// ```rust
832 /// use bubbletea_widgets::table::Model;
833 /// use bubbletea_rs::Model as BubbleTeaModel;
834 ///
835 /// // This is typically called by the Bubble Tea framework
836 /// let (mut table, cmd) = Model::init();
837 /// assert_eq!(table.columns.len(), 0);
838 /// assert_eq!(table.rows.len(), 0);
839 /// assert!(cmd.is_none());
840 /// ```
841 ///
842 /// # Note
843 ///
844 /// Most applications will want to use `Model::new(columns)` directly
845 /// instead of this init method, as it allows specifying the table
846 /// structure immediately.
847 fn init() -> (Self, Option<Cmd>) {
848 (Self::new(Vec::new()), None)
849 }
850
851 /// Processes messages and updates table state with keyboard navigation.
852 ///
853 /// This method handles all keyboard navigation for the table, including
854 /// row selection, page scrolling, and jumping to start/end positions.
855 /// It only processes messages when the table is focused, ensuring proper
856 /// behavior in multi-component applications.
857 ///
858 /// # Arguments
859 ///
860 /// * `msg` - The message to process, typically a `KeyMsg` for keyboard input
861 ///
862 /// # Returns
863 ///
864 /// An optional `Cmd` that may need to be executed (currently always `None`)
865 ///
866 /// # Key Handling
867 ///
868 /// The following keys are processed based on the table's key map configuration:
869 ///
870 /// - **Row Navigation**: Up/Down arrows, `k`/`j` keys
871 /// - **Page Navigation**: Page Up/Down, `b`/`f` keys
872 /// - **Half Page**: `u`/`d` keys for half-page scrolling
873 /// - **Jump Navigation**: Home/End, `g`/`G` keys for start/end
874 ///
875 /// # Examples
876 ///
877 /// ```rust
878 /// use bubbletea_widgets::table::{Model, Column};
879 /// use bubbletea_rs::{KeyMsg, Model as BubbleTeaModel};
880 /// use crossterm::event::{KeyCode, KeyModifiers};
881 ///
882 /// let mut table = Model::new(vec![Column::new("Data", 20)]);
883 ///
884 /// // Simulate down arrow key press
885 /// let key_msg = Box::new(KeyMsg {
886 /// key: KeyCode::Down,
887 /// modifiers: KeyModifiers::NONE,
888 /// });
889 /// let cmd = table.update(key_msg);
890 /// // Table selection moves down (if there are rows)
891 /// ```
892 ///
893 /// # Focus Handling
894 ///
895 /// If the table is not focused (`self.focus == false`), this method
896 /// returns immediately without processing the message. This allows
897 /// multiple components to coexist without interference.
898 ///
899 /// # Performance Optimization
900 ///
901 /// After any navigation that changes the selection, the viewport content
902 /// is automatically rebuilt to ensure the selected row remains visible
903 /// and the display is updated correctly.
904 fn update(&mut self, msg: Msg) -> Option<Cmd> {
905 if let Some(k) = msg.downcast_ref::<KeyMsg>() {
906 if !self.focus {
907 return None;
908 }
909 if self.keymap.row_up.matches(k) {
910 self.select_prev();
911 } else if self.keymap.row_down.matches(k) {
912 self.select_next();
913 } else if self.keymap.go_to_start.matches(k) {
914 self.selected = 0;
915 } else if self.keymap.go_to_end.matches(k) {
916 if !self.rows.is_empty() {
917 self.selected = self.rows.len() - 1;
918 }
919 }
920 // Page and half-page moves adjust selection relative to height
921 else if self.keymap.page_up.matches(k) {
922 self.selected = self.selected.saturating_sub(self.height as usize);
923 } else if self.keymap.page_down.matches(k) {
924 self.selected =
925 (self.selected + self.height as usize).min(self.rows.len().saturating_sub(1));
926 } else if self.keymap.half_page_up.matches(k) {
927 self.selected = self
928 .selected
929 .saturating_sub((self.height as usize).max(1) / 2);
930 } else if self.keymap.half_page_down.matches(k) {
931 self.selected = (self.selected + (self.height as usize).max(1) / 2)
932 .min(self.rows.len().saturating_sub(1));
933 }
934 // After any movement, refresh content and ensure visibility
935 self.rebuild_viewport_content();
936 }
937 None
938 }
939
940 /// Renders the table for display in a Bubble Tea application.
941 ///
942 /// This method delegates to the table's own `view()` method to generate
943 /// the formatted string representation. It's called by the Bubble Tea
944 /// framework during the render cycle.
945 ///
946 /// # Returns
947 ///
948 /// A multi-line string containing the formatted table with headers,
949 /// data rows, selection highlighting, and applied styling
950 ///
951 /// # Examples
952 ///
953 /// ```rust
954 /// use bubbletea_widgets::table::{Model, Column, Row};
955 /// use bubbletea_rs::Model as BubbleTeaModel;
956 ///
957 /// let mut table = Model::new(vec![Column::new("Name", 15)]);
958 /// table.add_row(Row::new(vec!["Alice".into()]));
959 ///
960 /// let output = table.view();
961 /// // Contains formatted table ready for terminal display
962 /// ```
963 ///
964 /// # Integration Pattern
965 ///
966 /// This method is typically called from your application's main `view()` method:
967 ///
968 /// ```rust
969 /// use bubbletea_widgets::table::Model as TableModel;
970 /// use bubbletea_rs::Model as BubbleTeaModel;
971 ///
972 /// struct App {
973 /// table: TableModel,
974 /// }
975 ///
976 /// impl BubbleTeaModel for App {
977 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) {
978 /// # (Self { table: TableModel::new(vec![]) }, None)
979 /// # }
980 /// #
981 /// # fn update(&mut self, _msg: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> {
982 /// # None
983 /// # }
984 ///
985 /// fn view(&self) -> String {
986 /// format!("My Application\n\n{}", self.table.view())
987 /// }
988 /// }
989 /// ```
990 fn view(&self) -> String {
991 self.view()
992 }
993}
994
995/// Help system integration for displaying table navigation keys.
996///
997/// This implementation provides the help system with information about
998/// the table's key bindings, enabling automatic generation of help text
999/// that documents the available navigation commands.
1000impl help::KeyMap for Model {
1001 /// Returns the most commonly used key bindings for short help display.
1002 ///
1003 /// This method provides a concise list of the most essential navigation
1004 /// keys that users need to know for basic table operation. It's used
1005 /// when displaying compact help information.
1006 ///
1007 /// # Returns
1008 ///
1009 /// A vector of key binding references for row and page navigation
1010 ///
1011 /// # Examples
1012 ///
1013 /// ```rust
1014 /// use bubbletea_widgets::table::{Model, Column};
1015 /// use bubbletea_widgets::help::KeyMap;
1016 ///
1017 /// let table = Model::new(vec![Column::new("Data", 20)]);
1018 /// let short_bindings = table.short_help();
1019 ///
1020 /// // Returns bindings for: up, down, page up, page down
1021 /// assert_eq!(short_bindings.len(), 4);
1022 /// ```
1023 ///
1024 /// # Help Content
1025 ///
1026 /// The short help includes:
1027 /// - **Row Up**: Move selection up one row
1028 /// - **Row Down**: Move selection down one row
1029 /// - **Page Up**: Move up one page of rows
1030 /// - **Page Down**: Move down one page of rows
1031 fn short_help(&self) -> Vec<&key::Binding> {
1032 vec![
1033 &self.keymap.row_up,
1034 &self.keymap.row_down,
1035 &self.keymap.page_up,
1036 &self.keymap.page_down,
1037 ]
1038 }
1039 /// Returns all key bindings organized by category for full help display.
1040 ///
1041 /// This method provides a comprehensive list of all available navigation
1042 /// keys, organized into logical groups for clear presentation in detailed
1043 /// help displays. Each group contains related navigation commands.
1044 ///
1045 /// # Returns
1046 ///
1047 /// A vector of groups, where each group is a vector of related key bindings
1048 ///
1049 /// # Examples
1050 ///
1051 /// ```rust
1052 /// use bubbletea_widgets::table::{Model, Column};
1053 /// use bubbletea_widgets::help::KeyMap;
1054 ///
1055 /// let table = Model::new(vec![Column::new("Data", 20)]);
1056 /// let full_bindings = table.full_help();
1057 ///
1058 /// // Returns 4 groups of key bindings
1059 /// assert_eq!(full_bindings.len(), 4);
1060 ///
1061 /// // First group: row navigation (up/down)
1062 /// assert_eq!(full_bindings[0].len(), 2);
1063 /// ```
1064 ///
1065 /// # Help Organization
1066 ///
1067 /// The full help is organized into these groups:
1068 /// 1. **Row Navigation**: Single row up/down movement
1069 /// 2. **Page Navigation**: Full page up/down scrolling
1070 /// 3. **Half Page Navigation**: Half page up/down movement
1071 /// 4. **Jump Navigation**: Go to start/end positions
1072 ///
1073 /// # Display Integration
1074 ///
1075 /// This grouped format allows help displays to show related commands
1076 /// together with appropriate spacing and categorization for better
1077 /// user comprehension.
1078 fn full_help(&self) -> Vec<Vec<&key::Binding>> {
1079 vec![
1080 vec![&self.keymap.row_up, &self.keymap.row_down],
1081 vec![&self.keymap.page_up, &self.keymap.page_down],
1082 vec![&self.keymap.half_page_up, &self.keymap.half_page_down],
1083 vec![&self.keymap.go_to_start, &self.keymap.go_to_end],
1084 ]
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091
1092 fn cols() -> Vec<Column> {
1093 vec![
1094 Column::new("col1", 10),
1095 Column::new("col2", 10),
1096 Column::new("col3", 10),
1097 ]
1098 }
1099
1100 #[test]
1101 fn test_new_defaults() {
1102 let m = Model::new(cols());
1103 assert_eq!(m.selected, 0);
1104 assert_eq!(m.height, 20);
1105 }
1106
1107 #[test]
1108 fn test_with_rows() {
1109 let m = Model::new(cols()).with_rows(vec![Row::new(vec!["1".into(), "Foo".into()])]);
1110 assert_eq!(m.rows.len(), 1);
1111 }
1112
1113 #[test]
1114 fn test_view_basic() {
1115 let mut m = Model::new(cols());
1116 m.set_height(5);
1117 m.rows = vec![Row::new(vec![
1118 "Foooooo".into(),
1119 "Baaaaar".into(),
1120 "Baaaaaz".into(),
1121 ])];
1122 let out = m.view();
1123 assert!(out.contains("Foooooo"));
1124 }
1125}