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}