pub mod defaultitem;
pub mod keys;
pub mod style;
mod api;
mod filtering;
mod model;
mod rendering;
mod types;
pub use model::Model;
pub use keys::ListKeyMap;
pub use style::ListStyles;
pub use types::{FilterState, FilterStateInfo, Item, ItemDelegate};
pub use defaultitem::{DefaultDelegate, DefaultItem, DefaultItemStyles};
use crate::{help, key};
use bubbletea_rs::{Cmd, KeyMsg, Model as BubbleTeaModel, Msg};
use crossterm::event::KeyCode;
impl<I: Item> help::KeyMap for Model<I> {
fn short_help(&self) -> Vec<&key::Binding> {
match self.filter_state {
FilterState::Filtering => vec![&self.keymap.accept_filter, &self.keymap.cancel_filter],
_ => vec![
&self.keymap.cursor_up,
&self.keymap.cursor_down,
&self.keymap.filter,
&self.keymap.quit,
&self.keymap.show_full_help, ],
}
}
fn full_help(&self) -> Vec<Vec<&key::Binding>> {
vec![
vec![
&self.keymap.cursor_up,
&self.keymap.cursor_down,
&self.keymap.next_page,
&self.keymap.prev_page,
&self.keymap.go_to_start,
&self.keymap.go_to_end,
],
vec![
&self.keymap.filter,
&self.keymap.clear_filter,
&self.keymap.accept_filter,
&self.keymap.cancel_filter,
],
vec![
&self.keymap.show_full_help,
&self.keymap.close_full_help,
&self.keymap.quit,
],
]
}
}
impl<I: Item + Send + Sync + 'static> BubbleTeaModel for Model<I> {
fn init() -> (Self, Option<Cmd>) {
let model = Self::new(vec![], defaultitem::DefaultDelegate::new(), 80, 24);
(model, None)
}
fn update(&mut self, msg: Msg) -> Option<Cmd> {
if self.filter_state == FilterState::Filtering {
if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
match key_msg.key {
crossterm::event::KeyCode::Esc => {
self.filter_state = if self.filtered_items.is_empty() {
FilterState::Unfiltered
} else {
FilterState::FilterApplied
};
self.filter_input.blur();
return None;
}
crossterm::event::KeyCode::Enter => {
self.apply_filter();
self.filter_state = FilterState::FilterApplied;
self.filter_input.blur();
return None;
}
crossterm::event::KeyCode::Char(c) => {
let textinput_msg = Box::new(KeyMsg {
key: KeyCode::Char(c),
modifiers: key_msg.modifiers,
}) as Msg;
self.filter_input.update(textinput_msg);
self.apply_filter();
}
crossterm::event::KeyCode::Backspace => {
let textinput_msg = Box::new(KeyMsg {
key: KeyCode::Backspace,
modifiers: key_msg.modifiers,
}) as Msg;
self.filter_input.update(textinput_msg);
self.apply_filter();
}
crossterm::event::KeyCode::Left => {
let pos = self.filter_input.position();
if pos > 0 {
self.filter_input.set_cursor(pos - 1);
}
}
crossterm::event::KeyCode::Right => {
let pos = self.filter_input.position();
self.filter_input.set_cursor(pos + 1);
}
crossterm::event::KeyCode::Home => {
self.filter_input.cursor_start();
}
crossterm::event::KeyCode::End => {
self.filter_input.cursor_end();
}
_ => {}
}
}
return None;
}
if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
if self.keymap.cursor_up.matches(key_msg) {
if self.cursor > 0 {
if self.is_cursor_at_viewport_top() {
let items_per_view = self.calculate_items_per_view();
self.cursor -= 1;
self.viewport_start = self.cursor.saturating_sub(items_per_view - 1);
} else {
self.cursor -= 1;
self.sync_viewport_with_cursor();
}
}
} else if self.keymap.cursor_down.matches(key_msg) {
if self.cursor < self.len().saturating_sub(1) {
if self.is_cursor_at_viewport_bottom() {
self.cursor += 1;
self.viewport_start = self.cursor;
} else {
self.cursor += 1;
self.sync_viewport_with_cursor();
}
}
} else if self.keymap.go_to_start.matches(key_msg) {
self.cursor = 0;
self.sync_viewport_with_cursor();
} else if self.keymap.go_to_end.matches(key_msg) {
self.cursor = self.len().saturating_sub(1);
self.sync_viewport_with_cursor();
} else if self.keymap.next_page.matches(key_msg) {
let items_len = self.len();
if items_len > 0 {
self.cursor = (self.cursor + self.per_page).min(items_len - 1);
self.sync_viewport_with_cursor();
}
} else if self.keymap.prev_page.matches(key_msg) {
self.cursor = self.cursor.saturating_sub(self.per_page);
self.sync_viewport_with_cursor();
} else if self.keymap.filter.matches(key_msg) {
self.filter_state = FilterState::Filtering;
return Some(self.filter_input.focus());
} else if self.keymap.clear_filter.matches(key_msg) {
self.filter_input.set_value("");
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
self.cursor = 0;
self.update_pagination();
} else if self.keymap.show_full_help.matches(key_msg)
|| self.keymap.close_full_help.matches(key_msg)
{
self.help.show_all = !self.help.show_all;
self.update_pagination(); } else if self.keymap.quit.matches(key_msg) {
return Some(bubbletea_rs::quit());
} else if key_msg.key == crossterm::event::KeyCode::Enter {
if let Some(selected_item) = self.selected_item() {
let original_index = if self.filter_state == FilterState::Unfiltered {
self.cursor
} else if let Some(filtered_item) = self.filtered_items.get(self.cursor) {
filtered_item.index
} else {
return None;
};
if let Some(cmd) = self.delegate.on_select(original_index, selected_item) {
return Some(cmd);
}
}
}
if self.per_page > 0 {
self.paginator.page = self.cursor / self.per_page;
}
}
None
}
fn view(&self) -> String {
let mut sections = Vec::new();
let header = self.view_header();
if !header.is_empty() {
sections.push(header);
}
let items = self.view_items();
if !items.is_empty() {
sections.push(items);
}
if self.show_spinner {
let spinner_view = self.spinner.view();
if !spinner_view.is_empty() {
sections.push(spinner_view);
}
}
if self.show_pagination && !self.is_empty() && self.paginator.total_pages > 1 {
let pagination_view = self.paginator.view();
if !pagination_view.is_empty() {
let styled_pagination = self
.styles
.pagination_style
.clone()
.render(&pagination_view);
sections.push(styled_pagination);
}
}
let footer = self.view_footer();
if !footer.is_empty() {
sections.push(footer);
}
sections.join("\n")
}
}