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/// Configuration option for table construction.
617///
618/// This type enables the flexible option-based constructor pattern used by
619/// the Go version. Each option is a function that modifies a table model
620/// during construction, allowing for clean, composable configuration.
621///
622/// # Examples
623///
624/// ```rust
625/// use bubbletea_widgets::table::{Model, TableOption, with_columns, with_rows, with_height, Column, Row};
626///
627/// let table = Model::with_options(vec![
628///     with_columns(vec![Column::new("Name", 20)]),
629///     with_rows(vec![Row::new(vec!["Alice".into()])]),
630///     with_height(15),
631/// ]);
632/// ```
633pub type TableOption = Box<dyn FnOnce(&mut Model) + Send>;
634
635/// Creates an option to set table columns during construction.
636///
637/// This option sets the column structure for the table, defining headers
638/// and column widths. This is typically the first option used when
639/// creating a new table.
640///
641/// # Arguments
642///
643/// * `cols` - Vector of column definitions
644///
645/// # Examples
646///
647/// ```rust
648/// use bubbletea_widgets::table::{Model, with_columns, Column};
649///
650/// let table = Model::with_options(vec![
651///     with_columns(vec![
652///         Column::new("ID", 8),
653///         Column::new("Name", 25),
654///         Column::new("Status", 12),
655///     ]),
656/// ]);
657/// assert_eq!(table.columns.len(), 3);
658/// ```
659pub fn with_columns(cols: Vec<Column>) -> TableOption {
660    Box::new(move |m: &mut Model| {
661        m.columns = cols;
662    })
663}
664
665/// Creates an option to set table rows during construction.
666///
667/// This option populates the table with initial data rows. Each row
668/// should have the same number of cells as there are columns.
669///
670/// # Arguments
671///
672/// * `rows` - Vector of row data
673///
674/// # Examples
675///
676/// ```rust
677/// use bubbletea_widgets::table::{Model, with_rows, Row};
678///
679/// let table = Model::with_options(vec![
680///     with_rows(vec![
681///         Row::new(vec!["001".into(), "Alice".into()]),
682///         Row::new(vec!["002".into(), "Bob".into()]),
683///     ]),
684/// ]);
685/// assert_eq!(table.rows.len(), 2);
686/// ```
687pub fn with_rows(rows: Vec<Row>) -> TableOption {
688    Box::new(move |m: &mut Model| {
689        m.rows = rows;
690    })
691}
692
693/// Creates an option to set table height during construction.
694///
695/// This option configures the vertical display space for the table,
696/// affecting how many rows are visible and viewport scrolling behavior.
697///
698/// # Arguments
699///
700/// * `h` - Height in terminal lines
701///
702/// # Examples
703///
704/// ```rust
705/// use bubbletea_widgets::table::{Model, with_height};
706///
707/// let table = Model::with_options(vec![
708///     with_height(25),
709/// ]);
710/// assert_eq!(table.height, 25);
711/// ```
712pub fn with_height(h: i32) -> TableOption {
713    Box::new(move |m: &mut Model| {
714        m.height = h;
715        m.sync_viewport_dimensions();
716    })
717}
718
719/// Creates an option to set table width during construction.
720///
721/// This option configures the horizontal display space for the table,
722/// affecting column layout and content wrapping behavior.
723///
724/// # Arguments
725///
726/// * `w` - Width in terminal columns
727///
728/// # Examples
729///
730/// ```rust
731/// use bubbletea_widgets::table::{Model, with_columns, with_width, Column};
732///
733/// let table = Model::with_options(vec![
734///     with_columns(vec![Column::new("Data", 20)]),
735///     with_width(80),
736/// ]);
737/// assert_eq!(table.width, 80);
738/// ```
739pub fn with_width(w: i32) -> TableOption {
740    Box::new(move |m: &mut Model| {
741        m.width = w;
742        m.sync_viewport_dimensions();
743    })
744}
745
746/// Creates an option to set table focus state during construction.
747///
748/// This option configures whether the table should be focused (and thus
749/// respond to keyboard input) when initially created.
750///
751/// # Arguments
752///
753/// * `f` - `true` for focused, `false` for unfocused
754///
755/// # Examples
756///
757/// ```rust
758/// use bubbletea_widgets::table::{Model, with_focused};
759///
760/// let table = Model::with_options(vec![
761///     with_focused(false),
762/// ]);
763/// assert!(!table.focus);
764/// ```
765pub fn with_focused(f: bool) -> TableOption {
766    Box::new(move |m: &mut Model| {
767        m.focus = f;
768    })
769}
770
771/// Creates an option to set table styles during construction.
772///
773/// This option applies custom styling configuration to the table,
774/// controlling the appearance of headers, cells, and selection.
775///
776/// # Arguments
777///
778/// * `s` - Styling configuration
779///
780/// # Examples
781///
782/// ```rust
783/// use bubbletea_widgets::table::{Model, with_styles, Styles};
784/// use lipgloss_extras::prelude::*;
785///
786/// let custom_styles = Styles {
787///     header: Style::new().bold(true),
788///     cell: Style::new().padding(0, 1, 0, 1),
789///     selected: Style::new().background(Color::from("green")),
790/// };
791///
792/// let table = Model::with_options(vec![
793///     with_styles(custom_styles),
794/// ]);
795/// ```
796pub fn with_styles(s: Styles) -> TableOption {
797    Box::new(move |m: &mut Model| {
798        m.styles = s;
799    })
800}
801
802/// Creates an option to set table key map during construction.
803///
804/// This option applies custom key bindings to the table, allowing
805/// applications to override the default navigation keys.
806///
807/// # Arguments
808///
809/// * `km` - Key mapping configuration
810///
811/// # Examples
812///
813/// ```rust
814/// use bubbletea_widgets::table::{Model, with_key_map, TableKeyMap};
815/// use bubbletea_widgets::key;
816/// use crossterm::event::KeyCode;
817///
818/// let mut custom_keymap = TableKeyMap::default();
819/// custom_keymap.row_up = key::Binding::new(vec![KeyCode::Char('w')])
820///     .with_help("w", "up");
821///
822/// let table = Model::with_options(vec![
823///     with_key_map(custom_keymap),
824/// ]);
825/// ```
826pub fn with_key_map(km: TableKeyMap) -> TableOption {
827    Box::new(move |m: &mut Model| {
828        m.keymap = km;
829    })
830}
831
832/// Interactive table model containing data, styling, navigation state,
833/// and a viewport for efficient rendering and scrolling.
834#[derive(Debug, Clone)]
835pub struct Model {
836    /// Column definitions controlling headers and widths.
837    pub columns: Vec<Column>,
838    /// Row data; each row contains one string per column.
839    pub rows: Vec<Row>,
840    /// Index of the currently selected row (0-based).
841    pub selected: usize,
842    /// Rendered width of the table in characters.
843    pub width: i32,
844    /// Rendered height of the table in lines.
845    pub height: i32,
846    /// Key bindings for navigation and movement.
847    pub keymap: TableKeyMap,
848    /// Styles used when rendering the table.
849    pub styles: Styles,
850    /// Whether this table currently has keyboard focus.
851    pub focus: bool,
852    /// Help model used to render key binding help.
853    pub help: help::Model,
854    /// Internal viewport that manages scrolling of rendered lines.
855    viewport: viewport::Model,
856}
857
858impl Model {
859    /// Creates a new table with the given `columns` and sensible defaults.
860    ///
861    /// Defaults: height 20, focused, empty rows, and default styles/keymap.
862    pub fn new(columns: Vec<Column>) -> Self {
863        let mut s = Self {
864            columns,
865            rows: Vec::new(),
866            selected: 0,
867            width: 0,
868            height: 20,
869            keymap: TableKeyMap::default(),
870            styles: Styles::default(),
871            focus: true,
872            help: help::Model::new(),
873            viewport: viewport::Model::new(0, 0),
874        };
875        // Initialize viewport dimensions
876        s.sync_viewport_dimensions();
877        s.rebuild_viewport_content();
878        s
879    }
880
881    /// Creates a new table with configuration options (Go-compatible constructor).
882    ///
883    /// This constructor provides a flexible, option-based approach to table creation
884    /// that matches the Go version's `New(opts...)` pattern. Each option is a
885    /// function that configures a specific aspect of the table.
886    ///
887    /// # Arguments
888    ///
889    /// * `opts` - Vector of configuration options to apply
890    ///
891    /// # Returns
892    ///
893    /// A configured table model with all options applied
894    ///
895    /// # Examples
896    ///
897    /// ```rust
898    /// use bubbletea_widgets::table::{Model, with_columns, with_rows, with_height, Column, Row};
899    ///
900    /// // Create a fully configured table
901    /// let table = Model::with_options(vec![
902    ///     with_columns(vec![
903    ///         Column::new("ID", 8),
904    ///         Column::new("Name", 20),
905    ///         Column::new("Status", 12),
906    ///     ]),
907    ///     with_rows(vec![
908    ///         Row::new(vec!["001".into(), "Alice".into(), "Active".into()]),
909    ///         Row::new(vec!["002".into(), "Bob".into(), "Inactive".into()]),
910    ///     ]),
911    ///     with_height(15),
912    /// ]);
913    /// ```
914    ///
915    /// Creating an empty table (equivalent to Go's `New()`):
916    /// ```rust
917    /// use bubbletea_widgets::table::Model;
918    ///
919    /// let table = Model::with_options(vec![]);
920    /// assert_eq!(table.columns.len(), 0);
921    /// assert_eq!(table.rows.len(), 0);
922    /// ```
923    ///
924    /// # Constructor Philosophy
925    ///
926    /// This pattern provides the same flexibility as the Go version while
927    /// maintaining Rust's type safety and ownership semantics. Options are
928    /// applied in the order provided, allowing later options to override
929    /// earlier ones if they configure the same property.
930    pub fn with_options(opts: Vec<TableOption>) -> Self {
931        let mut m = Self {
932            columns: Vec::new(),
933            rows: Vec::new(),
934            selected: 0,
935            width: 0,
936            height: 20,
937            keymap: TableKeyMap::default(),
938            styles: Styles::default(),
939            focus: true,
940            help: help::Model::new(),
941            viewport: viewport::Model::new(0, 0),
942        };
943
944        // Apply all options in order
945        for opt in opts {
946            opt(&mut m);
947        }
948
949        // Initialize viewport after all options are applied
950        m.sync_viewport_dimensions();
951        m.rebuild_viewport_content();
952        m
953    }
954
955    /// Sets the table rows on construction and returns `self` for chaining.
956    pub fn with_rows(mut self, rows: Vec<Row>) -> Self {
957        self.rows = rows;
958        self
959    }
960    /// Sets the table width in characters and rebuilds the viewport content.
961    pub fn set_width(&mut self, w: i32) {
962        self.width = w;
963        self.sync_viewport_dimensions();
964        self.rebuild_viewport_content();
965    }
966    /// Sets the table height in lines and rebuilds the viewport content.
967    pub fn set_height(&mut self, h: i32) {
968        self.height = h;
969        self.sync_viewport_dimensions();
970        self.rebuild_viewport_content();
971    }
972    /// Appends a row to the table and refreshes the rendered content.
973    pub fn add_row(&mut self, row: Row) {
974        self.rows.push(row);
975        self.rebuild_viewport_content();
976    }
977    /// Returns a reference to the currently selected row, if any.
978    pub fn selected_row(&self) -> Option<&Row> {
979        self.rows.get(self.selected)
980    }
981    /// Moves the selection down by one row.
982    pub fn select_next(&mut self) {
983        if !self.rows.is_empty() {
984            self.selected = (self.selected + 1).min(self.rows.len() - 1);
985        }
986    }
987    /// Moves the selection up by one row.
988    pub fn select_prev(&mut self) {
989        if !self.rows.is_empty() {
990            self.selected = self.selected.saturating_sub(1);
991        }
992    }
993
994    /// Moves the selection up by the specified number of rows (Go-compatible alias).
995    ///
996    /// This method provides Go API compatibility by matching the `MoveUp` method
997    /// signature and behavior from the original table implementation.
998    ///
999    /// # Arguments
1000    ///
1001    /// * `n` - Number of rows to move up
1002    ///
1003    /// # Examples
1004    ///
1005    /// ```rust
1006    /// use bubbletea_widgets::table::{Model, Column, Row};
1007    ///
1008    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1009    /// table.rows = vec![
1010    ///     Row::new(vec!["Row 1".into()]),
1011    ///     Row::new(vec!["Row 2".into()]),
1012    ///     Row::new(vec!["Row 3".into()]),
1013    /// ];
1014    /// table.selected = 2;
1015    ///
1016    /// table.move_up(1);
1017    /// assert_eq!(table.selected, 1);
1018    /// ```
1019    pub fn move_up(&mut self, n: usize) {
1020        if !self.rows.is_empty() {
1021            self.selected = self.selected.saturating_sub(n);
1022        }
1023    }
1024
1025    /// Moves the selection down by the specified number of rows (Go-compatible alias).
1026    ///
1027    /// This method provides Go API compatibility by matching the `MoveDown` method
1028    /// signature and behavior from the original table implementation.
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `n` - Number of rows to move down
1033    ///
1034    /// # Examples
1035    ///
1036    /// ```rust
1037    /// use bubbletea_widgets::table::{Model, Column, Row};
1038    ///
1039    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1040    /// table.rows = vec![
1041    ///     Row::new(vec!["Row 1".into()]),
1042    ///     Row::new(vec!["Row 2".into()]),
1043    ///     Row::new(vec!["Row 3".into()]),
1044    /// ];
1045    /// table.selected = 0;
1046    ///
1047    /// table.move_down(2);
1048    /// assert_eq!(table.selected, 2);
1049    /// ```
1050    pub fn move_down(&mut self, n: usize) {
1051        if !self.rows.is_empty() {
1052            self.selected = (self.selected + n).min(self.rows.len() - 1);
1053        }
1054    }
1055
1056    /// Moves the selection to the first row (Go-compatible alias).
1057    ///
1058    /// This method provides Go API compatibility by matching the `GotoTop` method
1059    /// from the original table implementation.
1060    ///
1061    /// # Examples
1062    ///
1063    /// ```rust
1064    /// use bubbletea_widgets::table::{Model, Column, Row};
1065    ///
1066    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1067    /// table.rows = vec![
1068    ///     Row::new(vec!["Row 1".into()]),
1069    ///     Row::new(vec!["Row 2".into()]),
1070    ///     Row::new(vec!["Row 3".into()]),
1071    /// ];
1072    /// table.selected = 2;
1073    ///
1074    /// table.goto_top();
1075    /// assert_eq!(table.selected, 0);
1076    /// ```
1077    pub fn goto_top(&mut self) {
1078        self.selected = 0;
1079    }
1080
1081    /// Moves the selection to the last row (Go-compatible alias).
1082    ///
1083    /// This method provides Go API compatibility by matching the `GotoBottom` method
1084    /// from the original table implementation.
1085    ///
1086    /// # Examples
1087    ///
1088    /// ```rust
1089    /// use bubbletea_widgets::table::{Model, Column, Row};
1090    ///
1091    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1092    /// table.rows = vec![
1093    ///     Row::new(vec!["Row 1".into()]),
1094    ///     Row::new(vec!["Row 2".into()]),
1095    ///     Row::new(vec!["Row 3".into()]),
1096    /// ];
1097    /// table.selected = 0;
1098    ///
1099    /// table.goto_bottom();
1100    /// assert_eq!(table.selected, 2);
1101    /// ```
1102    pub fn goto_bottom(&mut self) {
1103        if !self.rows.is_empty() {
1104            self.selected = self.rows.len() - 1;
1105        }
1106    }
1107
1108    /// Sets table styles and rebuilds the viewport content.
1109    ///
1110    /// This method matches the Go version's `SetStyles` functionality by updating
1111    /// the table's visual styling and ensuring the viewport content is rebuilt
1112    /// to reflect the new styles.
1113    ///
1114    /// # Arguments
1115    ///
1116    /// * `s` - The new styling configuration to apply
1117    ///
1118    /// # Examples
1119    ///
1120    /// ```rust
1121    /// use bubbletea_widgets::table::{Model, Column, Styles};
1122    /// use lipgloss_extras::prelude::*;
1123    ///
1124    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1125    ///
1126    /// let custom_styles = Styles {
1127    ///     header: Style::new().bold(true).background(Color::from("blue")),
1128    ///     cell: Style::new().padding(0, 1, 0, 1),
1129    ///     selected: Style::new().background(Color::from("green")),
1130    /// };
1131    ///
1132    /// table.set_styles(custom_styles);
1133    /// // Table now uses the new styles and viewport is updated
1134    /// ```
1135    pub fn set_styles(&mut self, s: Styles) {
1136        self.styles = s;
1137        self.update_viewport();
1138    }
1139
1140    /// Updates the viewport content based on current columns, rows, and styling.
1141    ///
1142    /// This method matches the Go version's `UpdateViewport` functionality by
1143    /// rebuilding the rendered table content and ensuring the selected row
1144    /// remains visible. It should be called after any changes to table
1145    /// structure, data, or styling.
1146    ///
1147    /// # Examples
1148    ///
1149    /// ```rust
1150    /// use bubbletea_widgets::table::{Model, Column, Row};
1151    ///
1152    /// let mut table = Model::new(vec![Column::new("Name", 20)]);
1153    /// table.rows.push(Row::new(vec!["Alice".into()]));
1154    ///
1155    /// // After manual changes, update the viewport
1156    /// table.update_viewport();
1157    /// ```
1158    ///
1159    /// # When to Call
1160    ///
1161    /// This method is automatically called by most table methods, but you may
1162    /// need to call it manually when:
1163    /// - Directly modifying the `rows` or `columns` fields
1164    /// - Changing dimensions or styling outside of provided methods
1165    /// - Ensuring content is current after external modifications
1166    pub fn update_viewport(&mut self) {
1167        self.rebuild_viewport_content();
1168    }
1169
1170    /// Renders help information for table navigation keys.
1171    ///
1172    /// This method matches the Go version's `HelpView` functionality by
1173    /// generating formatted help text that documents all available key
1174    /// bindings for table navigation.
1175    ///
1176    /// # Returns
1177    ///
1178    /// A formatted string containing help information for table navigation
1179    ///
1180    /// # Examples
1181    ///
1182    /// ```rust
1183    /// use bubbletea_widgets::table::{Model, Column};
1184    ///
1185    /// let table = Model::new(vec![Column::new("Data", 20)]);
1186    /// let help_text = table.help_view();
1187    ///
1188    /// // Contains formatted help showing navigation keys
1189    /// println!("Table Help:\n{}", help_text);
1190    /// ```
1191    ///
1192    /// # Integration
1193    ///
1194    /// This method is typically used to display help information separately
1195    /// from the main table view:
1196    ///
1197    /// ```rust
1198    /// use bubbletea_widgets::table::{Model, Column};
1199    ///
1200    /// struct App {
1201    ///     table: Model,
1202    ///     show_help: bool,
1203    /// }
1204    ///
1205    /// impl App {
1206    ///     fn view(&self) -> String {
1207    ///         let mut output = self.table.view();
1208    ///         if self.show_help {
1209    ///             output.push_str("\n\n");
1210    ///             output.push_str(&self.table.help_view());
1211    ///         }
1212    ///         output
1213    ///     }
1214    /// }
1215    /// ```
1216    pub fn help_view(&self) -> String {
1217        self.help.view(self)
1218    }
1219
1220    /// Renders the table as a string.
1221    pub fn view(&self) -> String {
1222        // Render table directly to string
1223        let mut tbl = LGTable::new();
1224        if self.width > 0 {
1225            tbl = tbl.width(self.width);
1226        }
1227
1228        let headers: Vec<String> = self.columns.iter().map(|c| c.title.clone()).collect();
1229        tbl = tbl.headers(headers);
1230
1231        let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
1232        let cell_style = self.styles.cell.clone();
1233        let header_style = self.styles.header.clone();
1234        let selected_row = self.selected as i32;
1235        let selected_style = self.styles.selected.clone();
1236        tbl = tbl.style_func_boxed(Box::new(move |row: i32, col: usize| {
1237            let mut s = if row == lipgloss_extras::table::HEADER_ROW {
1238                header_style.clone()
1239            } else {
1240                cell_style.clone()
1241            };
1242            if let Some(w) = widths.get(col) {
1243                s = s.width(*w);
1244            }
1245            if row >= 0 && row == selected_row {
1246                s = selected_style.clone().inherit(s);
1247            }
1248            s
1249        }));
1250
1251        let row_vecs: Vec<Vec<String>> = self.rows.iter().map(|r| r.cells.clone()).collect();
1252        tbl = tbl.rows(row_vecs);
1253        tbl.to_string()
1254    }
1255
1256    fn rebuild_viewport_content(&mut self) {
1257        let mut tbl = LGTable::new();
1258        if self.width > 0 {
1259            tbl = tbl.width(self.width);
1260        }
1261        // Don't set table height; viewport will handle vertical scrolling
1262
1263        // Headers
1264        let headers: Vec<String> = self.columns.iter().map(|c| c.title.clone()).collect();
1265        tbl = tbl.headers(headers);
1266
1267        // Column widths via style_func
1268        let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
1269        let cell_style = self.styles.cell.clone();
1270        let header_style = self.styles.header.clone();
1271        let selected_row = self.selected as i32; // data rows are 0-based in lipgloss-table
1272        let selected_style = self.styles.selected.clone();
1273        tbl = tbl.style_func_boxed(Box::new(move |row: i32, col: usize| {
1274            let mut s = if row == lipgloss_extras::table::HEADER_ROW {
1275                header_style.clone()
1276            } else {
1277                cell_style.clone()
1278            };
1279            if let Some(w) = widths.get(col) {
1280                s = s.width(*w);
1281            }
1282            if row >= 0 && row == selected_row {
1283                s = selected_style.clone().inherit(s);
1284            }
1285            s
1286        }));
1287
1288        // Rows
1289        let row_vecs: Vec<Vec<String>> = self.rows.iter().map(|r| r.cells.clone()).collect();
1290        tbl = tbl.rows(row_vecs);
1291
1292        let rendered = tbl.to_string();
1293        let lines: Vec<String> = rendered.split('\n').map(|s| s.to_string()).collect();
1294        self.viewport.set_content_lines(lines);
1295
1296        // Ensure selection is visible (header is line 0; rows begin at line 1)
1297        self.ensure_selected_visible();
1298    }
1299
1300    fn ensure_selected_visible(&mut self) {
1301        let target_line = self.selected.saturating_add(1); // account for header
1302        let h = (self.height.max(1)) as usize;
1303        let top = self.viewport.y_offset;
1304        let bottom = top.saturating_add(h.saturating_sub(1));
1305        if target_line < top {
1306            self.viewport.set_y_offset(target_line);
1307        } else if target_line > bottom {
1308            let new_top = target_line.saturating_sub(h.saturating_sub(1));
1309            self.viewport.set_y_offset(new_top);
1310        }
1311    }
1312
1313    fn sync_viewport_dimensions(&mut self) {
1314        self.viewport.width = self.width.max(0) as usize;
1315        self.viewport.height = self.height.max(0) as usize;
1316    }
1317
1318    /// Gives keyboard focus to the table.
1319    pub fn focus(&mut self) {
1320        self.focus = true;
1321    }
1322    /// Removes keyboard focus from the table.
1323    pub fn blur(&mut self) {
1324        self.focus = false;
1325    }
1326}
1327
1328impl BubbleTeaModel for Model {
1329    /// Creates a new empty table model for Bubble Tea applications.
1330    ///
1331    /// This initialization method creates a table with no columns or data,
1332    /// suitable for applications that will configure the table structure
1333    /// after initialization. The table starts focused and ready to receive
1334    /// keyboard input.
1335    ///
1336    /// # Returns
1337    ///
1338    /// A tuple containing the new table model and no initial command
1339    ///
1340    /// # Examples
1341    ///
1342    /// ```rust
1343    /// use bubbletea_widgets::table::Model;
1344    /// use bubbletea_rs::Model as BubbleTeaModel;
1345    ///
1346    /// // This is typically called by the Bubble Tea framework
1347    /// let (mut table, cmd) = Model::init();
1348    /// assert_eq!(table.columns.len(), 0);
1349    /// assert_eq!(table.rows.len(), 0);
1350    /// assert!(cmd.is_none());
1351    /// ```
1352    ///
1353    /// # Note
1354    ///
1355    /// Most applications will want to use `Model::new(columns)` directly
1356    /// instead of this init method, as it allows specifying the table
1357    /// structure immediately.
1358    fn init() -> (Self, Option<Cmd>) {
1359        (Self::new(Vec::new()), None)
1360    }
1361
1362    /// Processes messages and updates table state with keyboard navigation.
1363    ///
1364    /// This method handles all keyboard navigation for the table, including
1365    /// row selection, page scrolling, and jumping to start/end positions.
1366    /// It only processes messages when the table is focused, ensuring proper
1367    /// behavior in multi-component applications.
1368    ///
1369    /// # Arguments
1370    ///
1371    /// * `msg` - The message to process, typically a `KeyMsg` for keyboard input
1372    ///
1373    /// # Returns
1374    ///
1375    /// An optional `Cmd` that may need to be executed (currently always `None`)
1376    ///
1377    /// # Key Handling
1378    ///
1379    /// The following keys are processed based on the table's key map configuration:
1380    ///
1381    /// - **Row Navigation**: Up/Down arrows, `k`/`j` keys
1382    /// - **Page Navigation**: Page Up/Down, `b`/`f` keys  
1383    /// - **Half Page**: `u`/`d` keys for half-page scrolling
1384    /// - **Jump Navigation**: Home/End, `g`/`G` keys for start/end
1385    ///
1386    /// # Examples
1387    ///
1388    /// ```rust
1389    /// use bubbletea_widgets::table::{Model, Column};
1390    /// use bubbletea_rs::{KeyMsg, Model as BubbleTeaModel};
1391    /// use crossterm::event::{KeyCode, KeyModifiers};
1392    ///
1393    /// let mut table = Model::new(vec![Column::new("Data", 20)]);
1394    ///
1395    /// // Simulate down arrow key press
1396    /// let key_msg = Box::new(KeyMsg {
1397    ///     key: KeyCode::Down,
1398    ///     modifiers: KeyModifiers::NONE,
1399    /// });
1400    /// let cmd = table.update(key_msg);
1401    /// // Table selection moves down (if there are rows)
1402    /// ```
1403    ///
1404    /// # Focus Handling
1405    ///
1406    /// If the table is not focused (`self.focus == false`), this method
1407    /// returns immediately without processing the message. This allows
1408    /// multiple components to coexist without interference.
1409    ///
1410    /// # Performance Optimization
1411    ///
1412    /// After any navigation that changes the selection, the viewport content
1413    /// is automatically rebuilt to ensure the selected row remains visible
1414    /// and the display is updated correctly.
1415    fn update(&mut self, msg: Msg) -> Option<Cmd> {
1416        if let Some(k) = msg.downcast_ref::<KeyMsg>() {
1417            if !self.focus {
1418                return None;
1419            }
1420            if self.keymap.row_up.matches(k) {
1421                self.select_prev();
1422            } else if self.keymap.row_down.matches(k) {
1423                self.select_next();
1424            } else if self.keymap.go_to_start.matches(k) {
1425                self.selected = 0;
1426            } else if self.keymap.go_to_end.matches(k) {
1427                if !self.rows.is_empty() {
1428                    self.selected = self.rows.len() - 1;
1429                }
1430            }
1431            // Page and half-page moves adjust selection relative to height
1432            else if self.keymap.page_up.matches(k) {
1433                self.selected = self.selected.saturating_sub(self.height as usize);
1434            } else if self.keymap.page_down.matches(k) {
1435                self.selected =
1436                    (self.selected + self.height as usize).min(self.rows.len().saturating_sub(1));
1437            } else if self.keymap.half_page_up.matches(k) {
1438                self.selected = self
1439                    .selected
1440                    .saturating_sub((self.height as usize).max(1) / 2);
1441            } else if self.keymap.half_page_down.matches(k) {
1442                self.selected = (self.selected + (self.height as usize).max(1) / 2)
1443                    .min(self.rows.len().saturating_sub(1));
1444            }
1445            // After any movement, ensure visibility without rebuilding content
1446            self.ensure_selected_visible();
1447        }
1448        None
1449    }
1450
1451    /// Renders the table for display in a Bubble Tea application.
1452    ///
1453    /// This method delegates to the table's own `view()` method to generate
1454    /// the formatted string representation. It's called by the Bubble Tea
1455    /// framework during the render cycle.
1456    ///
1457    /// # Returns
1458    ///
1459    /// A multi-line string containing the formatted table with headers,
1460    /// data rows, selection highlighting, and applied styling
1461    ///
1462    /// # Examples
1463    ///
1464    /// ```rust
1465    /// use bubbletea_widgets::table::{Model, Column, Row};
1466    /// use bubbletea_rs::Model as BubbleTeaModel;
1467    ///
1468    /// let mut table = Model::new(vec![Column::new("Name", 15)]);
1469    /// table.add_row(Row::new(vec!["Alice".into()]));
1470    ///
1471    /// let output = table.view();
1472    /// // Contains formatted table ready for terminal display
1473    /// ```
1474    ///
1475    /// # Integration Pattern
1476    ///
1477    /// This method is typically called from your application's main `view()` method:
1478    ///
1479    /// ```rust
1480    /// use bubbletea_widgets::table::Model as TableModel;
1481    /// use bubbletea_rs::Model as BubbleTeaModel;
1482    ///
1483    /// struct App {
1484    ///     table: TableModel,
1485    /// }
1486    ///
1487    /// impl BubbleTeaModel for App {
1488    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) {
1489    /// #       (Self { table: TableModel::new(vec![]) }, None)
1490    /// #   }
1491    /// #   
1492    /// #   fn update(&mut self, _msg: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> {
1493    /// #       None
1494    /// #   }
1495    ///     
1496    ///     fn view(&self) -> String {
1497    ///         format!("My Application\n\n{}", self.table.view())
1498    ///     }
1499    /// }
1500    /// ```
1501    fn view(&self) -> String {
1502        self.viewport.view()
1503    }
1504}
1505
1506/// Help system integration for displaying table navigation keys.
1507///
1508/// This implementation provides the help system with information about
1509/// the table's key bindings, enabling automatic generation of help text
1510/// that documents the available navigation commands.
1511impl help::KeyMap for Model {
1512    /// Returns the most commonly used key bindings for short help display.
1513    ///
1514    /// This method provides a concise list of the most essential navigation
1515    /// keys that users need to know for basic table operation. It's used
1516    /// when displaying compact help information.
1517    ///
1518    /// # Returns
1519    ///
1520    /// A vector of key binding references for row and page navigation
1521    ///
1522    /// # Examples
1523    ///
1524    /// ```rust
1525    /// use bubbletea_widgets::table::{Model, Column};
1526    /// use bubbletea_widgets::help::KeyMap;
1527    ///
1528    /// let table = Model::new(vec![Column::new("Data", 20)]);
1529    /// let short_bindings = table.short_help();
1530    ///
1531    /// // Returns bindings for: up, down, page up, page down
1532    /// assert_eq!(short_bindings.len(), 4);
1533    /// ```
1534    ///
1535    /// # Help Content
1536    ///
1537    /// The short help includes:
1538    /// - **Row Up**: Move selection up one row
1539    /// - **Row Down**: Move selection down one row  
1540    /// - **Page Up**: Move up one page of rows
1541    /// - **Page Down**: Move down one page of rows
1542    fn short_help(&self) -> Vec<&key::Binding> {
1543        vec![
1544            &self.keymap.row_up,
1545            &self.keymap.row_down,
1546            &self.keymap.page_up,
1547            &self.keymap.page_down,
1548        ]
1549    }
1550    /// Returns all key bindings organized by category for full help display.
1551    ///
1552    /// This method provides a comprehensive list of all available navigation
1553    /// keys, organized into logical groups for clear presentation in detailed
1554    /// help displays. Each group contains related navigation commands.
1555    ///
1556    /// # Returns
1557    ///
1558    /// A vector of groups, where each group is a vector of related key bindings
1559    ///
1560    /// # Examples
1561    ///
1562    /// ```rust
1563    /// use bubbletea_widgets::table::{Model, Column};
1564    /// use bubbletea_widgets::help::KeyMap;
1565    ///
1566    /// let table = Model::new(vec![Column::new("Data", 20)]);
1567    /// let full_bindings = table.full_help();
1568    ///
1569    /// // Returns 4 groups of key bindings
1570    /// assert_eq!(full_bindings.len(), 4);
1571    ///
1572    /// // First group: row navigation (up/down)
1573    /// assert_eq!(full_bindings[0].len(), 2);
1574    /// ```
1575    ///
1576    /// # Help Organization
1577    ///
1578    /// The full help is organized into these groups:
1579    /// 1. **Row Navigation**: Single row up/down movement
1580    /// 2. **Page Navigation**: Full page up/down scrolling
1581    /// 3. **Half Page Navigation**: Half page up/down movement
1582    /// 4. **Jump Navigation**: Go to start/end positions
1583    ///
1584    /// # Display Integration
1585    ///
1586    /// This grouped format allows help displays to show related commands
1587    /// together with appropriate spacing and categorization for better
1588    /// user comprehension.
1589    fn full_help(&self) -> Vec<Vec<&key::Binding>> {
1590        vec![
1591            vec![&self.keymap.row_up, &self.keymap.row_down],
1592            vec![&self.keymap.page_up, &self.keymap.page_down],
1593            vec![&self.keymap.half_page_up, &self.keymap.half_page_down],
1594            vec![&self.keymap.go_to_start, &self.keymap.go_to_end],
1595        ]
1596    }
1597}
1598
1599#[cfg(test)]
1600mod tests {
1601    use super::*;
1602
1603    fn cols() -> Vec<Column> {
1604        vec![
1605            Column::new("col1", 10),
1606            Column::new("col2", 10),
1607            Column::new("col3", 10),
1608        ]
1609    }
1610
1611    #[test]
1612    fn test_new_defaults() {
1613        let m = Model::new(cols());
1614        assert_eq!(m.selected, 0);
1615        assert_eq!(m.height, 20);
1616    }
1617
1618    #[test]
1619    fn test_with_rows() {
1620        let m = Model::new(cols()).with_rows(vec![Row::new(vec!["1".into(), "Foo".into()])]);
1621        assert_eq!(m.rows.len(), 1);
1622    }
1623
1624    #[test]
1625    fn test_view_basic() {
1626        let mut m = Model::new(cols());
1627        m.set_height(5);
1628        m.rows = vec![Row::new(vec![
1629            "Foooooo".into(),
1630            "Baaaaar".into(),
1631            "Baaaaaz".into(),
1632        ])];
1633        let out = m.view();
1634        assert!(out.contains("Foooooo"));
1635    }
1636}