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}