use crate::help;
use crate::internal::fuzzy;
use crate::key::{self, Binding};
use crate::paginator;
use crate::spinner;
use crate::textinput;
use rusty_bubbletea::commands;
use rusty_bubbletea::key::KeyPressMsg;
use rusty_bubbletea::model::{Cmd, Msg};
use rusty_lipgloss::{self, Color, Style};
use rusty_x_ansi;
use std::time::Duration;
type ItemUpdateFunc = Box<dyn Fn(&dyn Msg, &Model) -> Cmd + Send + Sync>;
const BULLET: &str = "•";
const ELLIPSIS: &str = "…";
fn clamp(v: usize, low: usize, high: usize) -> usize {
if low > high {
return v.min(low);
}
v.max(low).min(high)
}
pub trait Item: Send + Sync + std::fmt::Debug {
fn filter_value(&self) -> String;
fn box_clone(&self) -> Box<dyn Item + Send + Sync>;
fn as_any(&self) -> &dyn std::any::Any;
fn as_default_item(&self) -> Option<&dyn DefaultItem> {
None
}
}
pub trait ItemDelegate {
fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String;
fn height(&self) -> usize;
fn spacing(&self) -> usize;
fn update(&self, msg: &dyn Msg, m: &Model) -> Cmd;
fn short_help(&self) -> Vec<Binding> {
vec![]
}
fn full_help(&self) -> Vec<Vec<Binding>> {
vec![]
}
}
#[derive(Debug)]
pub struct FilterMatchesMsg(pub Vec<FilteredItem>);
impl FilteredItem {
fn clone_item(&self) -> FilteredItem {
FilteredItem {
index: self.index,
item: self.item.box_clone(),
matches: self.matches.clone(),
}
}
}
impl FilterMatchesMsg {
pub fn clone_items(&self) -> Vec<FilteredItem> {
self.0
.iter()
.map(|f| FilteredItem {
index: f.index,
item: f.item.box_clone(),
matches: f.matches.clone(),
})
.collect()
}
}
#[derive(Debug)]
pub struct FilteredItem {
pub index: usize,
pub item: Box<dyn Item + Send + Sync>,
pub matches: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct Rank {
pub index: usize,
pub matched_indexes: Vec<usize>,
}
pub type FilterFunc = Box<dyn Fn(&str, &[String]) -> Vec<Rank> + Send + Sync>;
pub fn default_filter(term: &str, targets: &[String]) -> Vec<Rank> {
let matches = fuzzy::find(term, targets);
matches
.iter()
.map(|r| Rank {
index: r.index,
matched_indexes: r.matched_indexes.clone(),
})
.collect()
}
pub fn unsorted_filter(term: &str, targets: &[String]) -> Vec<Rank> {
let matches = fuzzy::find_no_sort(term, targets);
matches
.iter()
.map(|r| Rank {
index: r.index,
matched_indexes: r.matched_indexes.clone(),
})
.collect()
}
#[derive(Debug)]
struct StatusMessageTimeoutMsg;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterState {
#[default]
Unfiltered,
Filtering,
FilterApplied,
}
impl FilterState {
pub fn to_string(&self) -> &'static str {
match self {
FilterState::Unfiltered => "unfiltered",
FilterState::Filtering => "filtering",
FilterState::FilterApplied => "filter applied",
}
}
}
pub struct Model {
pub show_title: bool,
pub show_filter: bool,
pub show_status_bar: bool,
pub show_pagination: bool,
pub show_help: bool,
pub filtering_enabled: bool,
item_name_singular: String,
item_name_plural: String,
pub title: String,
pub styles: Styles,
pub infinite_scrolling: bool,
pub key_map: KeyMap,
pub filter: FilterFunc,
pub disable_quit_keybindings: bool,
pub additional_short_help_keys: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
pub additional_full_help_keys: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
pub spinner: spinner::Model,
pub show_spinner: bool,
pub width: usize,
pub height: usize,
pub paginator: paginator::Model,
cursor: usize,
pub help: help::Model,
pub filter_input: textinput::Model,
pub filter_state: FilterState,
pub status_message_lifetime: Duration,
status_message: String,
status_message_timer: Option<std::thread::JoinHandle<()>>,
pub items: Vec<Box<dyn Item + Send + Sync>>,
filtered_items: Vec<FilteredItem>,
delegate: Box<dyn ItemDelegate + Send + Sync>,
}
impl help::KeyMap for Model {
fn short_help(&self) -> Vec<Binding> {
Model::short_help(self)
}
fn full_help(&self) -> Vec<Vec<Binding>> {
Model::full_help(self)
}
}
impl std::fmt::Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("list::Model")
.field("title", &self.title)
.field("width", &self.width)
.field("height", &self.height)
.field("items", &self.items.len())
.finish()
}
}
pub fn new(
items: Vec<Box<dyn Item + Send + Sync>>,
delegate: Box<dyn ItemDelegate + Send + Sync>,
width: usize,
height: usize,
) -> Model {
let styles = default_styles(true);
let mut sp = spinner::new(vec![]);
sp.spinner = spinner::line();
sp.style = styles.spinner.clone();
let mut filter_input = textinput::new();
filter_input.prompt = "Filter: ".to_string();
filter_input.char_limit = 64;
filter_input.focus();
let mut p = paginator::new(vec![]);
p.type_ = paginator::Type::Dots;
p.active_dot = styles
.active_pagination_dot
.clone()
.set_string(&[BULLET])
.render("");
p.inactive_dot = styles
.inactive_pagination_dot
.clone()
.set_string(&[BULLET])
.render("");
let mut m = Model {
show_title: true,
show_filter: true,
show_status_bar: true,
show_pagination: true,
show_help: true,
item_name_singular: "item".to_string(),
item_name_plural: "items".to_string(),
filtering_enabled: true,
key_map: default_key_map(),
filter: Box::new(default_filter),
styles,
title: "List".to_string(),
filter_input,
status_message_lifetime: Duration::from_secs(1),
width,
height,
delegate,
items,
paginator: p,
spinner: sp,
help: help::new(),
cursor: 0,
filter_state: FilterState::Unfiltered,
infinite_scrolling: false,
disable_quit_keybindings: false,
additional_short_help_keys: None,
additional_full_help_keys: None,
show_spinner: false,
status_message: String::new(),
status_message_timer: None,
filtered_items: vec![],
};
m.update_pagination();
m.update_keybindings();
m
}
impl Model {
pub fn set_filtering_enabled(&mut self, v: bool) {
self.filtering_enabled = v;
if !v {
self.reset_filtering();
}
self.update_keybindings();
}
pub fn filtering_enabled(&self) -> bool {
self.filtering_enabled
}
pub fn set_show_title(&mut self, v: bool) {
self.show_title = v;
self.update_pagination();
}
pub fn set_filter_text(&mut self, filter: &str) {
self.filter_state = FilterState::Filtering;
self.filter_input.set_value(filter);
let fmm = filter_items(self);
self.filtered_items = fmm;
self.filter_state = FilterState::FilterApplied;
self.go_to_start();
self.filter_input.cursor_end();
self.update_pagination();
self.update_keybindings();
}
pub fn set_filter_state(&mut self, state: FilterState) {
self.go_to_start();
self.filter_state = state;
self.filter_input.cursor_end();
self.filter_input.focus();
self.update_keybindings();
}
pub fn show_title(&self) -> bool {
self.show_title
}
pub fn set_show_filter(&mut self, v: bool) {
self.show_filter = v;
self.update_pagination();
}
pub fn show_filter(&self) -> bool {
self.show_filter
}
pub fn set_show_status_bar(&mut self, v: bool) {
self.show_status_bar = v;
self.update_pagination();
}
pub fn show_status_bar(&self) -> bool {
self.show_status_bar
}
pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
self.item_name_singular = singular.to_string();
self.item_name_plural = plural.to_string();
}
pub fn status_bar_item_name(&self) -> (String, String) {
(
self.item_name_singular.clone(),
self.item_name_plural.clone(),
)
}
pub fn set_show_pagination(&mut self, v: bool) {
self.show_pagination = v;
self.update_pagination();
}
pub fn show_pagination(&mut self) -> bool {
self.show_pagination
}
pub fn set_show_help(&mut self, v: bool) {
self.show_help = v;
self.update_pagination();
}
pub fn show_help(&self) -> bool {
self.show_help
}
pub fn items(&self) -> &[Box<dyn Item + Send + Sync>] {
&self.items
}
pub fn set_items(&mut self, items: Vec<Box<dyn Item + Send + Sync>>) -> Cmd {
let mut cmd: Cmd = None;
self.items = items;
if self.filter_state != FilterState::Unfiltered {
self.filtered_items = vec![];
cmd = filter_items_cmd(self);
}
self.update_pagination();
self.update_keybindings();
cmd
}
pub fn select(&mut self, index: usize) {
self.paginator.page = index / self.paginator.per_page;
self.cursor = index % self.paginator.per_page;
}
pub fn reset_selected(&mut self) {
self.select(0);
}
pub fn reset_filter(&mut self) {
self.reset_filtering();
}
pub fn set_item(&mut self, index: usize, item: Box<dyn Item + Send + Sync>) -> Cmd {
let mut cmd: Cmd = None;
self.items[index] = item;
if self.filter_state != FilterState::Unfiltered {
cmd = filter_items_cmd(self);
}
self.update_pagination();
cmd
}
pub fn insert_item(&mut self, index: usize, item: Box<dyn Item + Send + Sync>) -> Cmd {
let mut cmd: Cmd = None;
self.items = insert_item_into_slice(&self.items, item, index);
if self.filter_state != FilterState::Unfiltered {
cmd = filter_items_cmd(self);
}
self.update_pagination();
self.update_keybindings();
cmd
}
pub fn remove_item(&mut self, index: usize) {
self.items = remove_item_from_slice(&self.items, index);
if self.filter_state != FilterState::Unfiltered {
self.filtered_items = remove_filter_match_from_slice(&self.filtered_items, index);
if self.filtered_items.is_empty() {
self.reset_filtering();
}
}
self.update_pagination();
}
pub fn set_delegate(&mut self, d: Box<dyn ItemDelegate + Send + Sync>) {
self.delegate = d;
self.update_pagination();
}
pub fn visible_items(&self) -> Vec<&dyn Item> {
if self.filter_state != FilterState::Unfiltered {
return self
.filtered_items
.iter()
.map(|f| f.item.as_ref() as &dyn Item)
.collect();
}
self.items.iter().map(|i| i.as_ref() as &dyn Item).collect()
}
pub fn selected_item(&self) -> Option<&dyn Item> {
let i = self.index();
let items = self.visible_items();
if i >= items.len() {
return None;
}
Some(items[i])
}
pub fn matches_for_item(&self, index: usize) -> Vec<usize> {
if self.filtered_items.is_empty() || index >= self.filtered_items.len() {
return vec![];
}
self.filtered_items[index].matches.clone()
}
pub fn index(&self) -> usize {
self.paginator.page * self.paginator.per_page + self.cursor
}
pub fn global_index(&self) -> usize {
let index = self.index();
if self.filtered_items.is_empty() || index >= self.filtered_items.len() {
return index;
}
self.filtered_items[index].index
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn cursor_up(&mut self) {
if self.cursor == 0 && self.paginator.on_first_page() {
if self.infinite_scrolling {
self.go_to_end();
return;
}
return;
}
if self.cursor > 0 {
self.cursor -= 1;
return;
}
self.paginator.prev_page();
self.cursor = self.max_cursor_index();
}
pub fn cursor_down(&mut self) {
let max_cursor_index = self.max_cursor_index();
self.cursor += 1;
if self.cursor <= max_cursor_index {
return;
}
if !self.paginator.on_last_page() {
self.paginator.next_page();
self.cursor = 0;
return;
}
self.cursor = max_cursor_index;
if self.infinite_scrolling {
self.go_to_start();
}
}
pub fn go_to_start(&mut self) {
self.paginator.page = 0;
self.cursor = 0;
}
pub fn go_to_end(&mut self) {
self.paginator.page = self.paginator.total_pages - 1;
self.cursor = self.max_cursor_index();
}
pub fn prev_page(&mut self) {
self.paginator.prev_page();
self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
}
pub fn next_page(&mut self) {
self.paginator.next_page();
self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
}
fn max_cursor_index(&self) -> usize {
self.paginator
.items_on_page(self.visible_items().len())
.saturating_sub(1)
}
pub fn filter_state(&self) -> FilterState {
self.filter_state
}
pub fn filter_value(&self) -> String {
self.filter_input.value()
}
pub fn setting_filter(&self) -> bool {
self.filter_state == FilterState::Filtering
}
pub fn is_filtered(&self) -> bool {
self.filter_state == FilterState::FilterApplied
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn set_spinner(&mut self, spinner: spinner::Spinner) {
self.spinner.spinner = spinner;
}
pub fn toggle_spinner(&mut self) -> Cmd {
if !self.show_spinner {
return self.start_spinner();
}
self.stop_spinner();
None
}
pub fn start_spinner(&mut self) -> Cmd {
self.show_spinner = true;
let tick = self.spinner.tick_msg();
Some(Box::new(move || Some(Box::new(tick))))
}
pub fn stop_spinner(&mut self) {
self.show_spinner = false;
}
pub fn disable_quit_keybindings(&mut self) {
self.disable_quit_keybindings = true;
self.key_map.quit.set_enabled(false);
self.key_map.force_quit.set_enabled(false);
}
pub fn new_status_message(&mut self, s: &str) -> Cmd {
self.status_message = s.to_string();
if let Some(t) = &self.status_message_timer {
let _ = t.thread().id();
}
let lifetime = self.status_message_lifetime;
self.status_message_timer = Some(std::thread::spawn(move || {
std::thread::sleep(lifetime);
}));
Some(Box::new(|| Some(Box::new(StatusMessageTimeoutMsg))))
}
pub fn set_width(&mut self, v: usize) {
self.set_size(v, self.height);
}
pub fn set_height(&mut self, v: usize) {
self.set_size(self.width, v);
}
pub fn set_size(&mut self, width: usize, height: usize) {
let prompt_width = rusty_lipgloss::size::width(
&self.styles.title.clone().render(&self.filter_input.prompt),
);
self.width = width;
self.height = height;
self.help.set_width(width);
let sw = self.spinner_view();
let sw_width = rusty_lipgloss::size::width(&sw);
self.filter_input
.set_width(width.saturating_sub(prompt_width + sw_width));
self.update_pagination();
self.update_keybindings();
}
fn reset_filtering(&mut self) {
if self.filter_state == FilterState::Unfiltered {
return;
}
self.filter_state = FilterState::Unfiltered;
self.filter_input.reset();
self.filtered_items = vec![];
self.update_pagination();
self.update_keybindings();
}
fn items_as_filter_items(&self) -> Vec<FilteredItem> {
self.items
.iter()
.enumerate()
.map(|(i, item)| FilteredItem {
index: i,
item: item.box_clone(),
matches: vec![],
})
.collect()
}
pub fn update_keybindings(&mut self) {
match self.filter_state {
FilterState::Filtering => {
self.key_map.cursor_up.set_enabled(false);
self.key_map.cursor_down.set_enabled(false);
self.key_map.next_page.set_enabled(false);
self.key_map.prev_page.set_enabled(false);
self.key_map.go_to_start.set_enabled(false);
self.key_map.go_to_end.set_enabled(false);
self.key_map.filter.set_enabled(false);
self.key_map.clear_filter.set_enabled(false);
self.key_map.cancel_while_filtering.set_enabled(true);
self.key_map
.accept_while_filtering
.set_enabled(!self.filter_input.value().is_empty());
self.key_map.quit.set_enabled(false);
self.key_map.show_full_help.set_enabled(false);
self.key_map.close_full_help.set_enabled(false);
}
_ => {
let has_items = !self.items.is_empty();
self.key_map.cursor_up.set_enabled(has_items);
self.key_map.cursor_down.set_enabled(has_items);
let has_pages = self.paginator.total_pages > 1;
self.key_map.next_page.set_enabled(has_pages);
self.key_map.prev_page.set_enabled(has_pages);
self.key_map.go_to_start.set_enabled(has_items);
self.key_map.go_to_end.set_enabled(has_items);
self.key_map
.filter
.set_enabled(self.filtering_enabled && has_items);
self.key_map
.clear_filter
.set_enabled(self.filter_state == FilterState::FilterApplied);
self.key_map.cancel_while_filtering.set_enabled(false);
self.key_map.accept_while_filtering.set_enabled(false);
self.key_map
.quit
.set_enabled(!self.disable_quit_keybindings);
if self.help.show_all {
self.key_map.show_full_help.set_enabled(true);
self.key_map.close_full_help.set_enabled(true);
} else {
let min_help = count_enabled_bindings(&self.full_help()) > 1;
self.key_map.show_full_help.set_enabled(min_help);
self.key_map.close_full_help.set_enabled(min_help);
}
}
}
}
pub fn update_pagination(&mut self) {
let index = self.index();
let mut avail_height = self.height;
if self.show_title || (self.show_filter && self.filtering_enabled) {
avail_height =
avail_height.saturating_sub(rusty_lipgloss::size::height(&self.title_view()));
}
if self.show_status_bar {
avail_height =
avail_height.saturating_sub(rusty_lipgloss::size::height(&self.status_view()));
}
if self.show_pagination {
avail_height =
avail_height.saturating_sub(rusty_lipgloss::size::height(&self.pagination_view()));
}
if self.show_help {
avail_height =
avail_height.saturating_sub(rusty_lipgloss::size::height(&self.help_view()));
}
let delegate_height = self.delegate.height();
let delegate_spacing = self.delegate.spacing();
self.paginator.per_page =
1usize.max(avail_height / (delegate_height + delegate_spacing).max(1));
let pages = self.visible_items().len();
if pages < 1 {
self.paginator.set_total_pages(1);
} else {
self.paginator.set_total_pages(pages);
}
self.paginator.page = index / self.paginator.per_page.max(1);
self.cursor = index % self.paginator.per_page.max(1);
if self.paginator.page >= self.paginator.total_pages - 1 {
self.paginator.page = self.paginator.total_pages - 1;
}
}
fn hide_status_message(&mut self) {
self.status_message = String::new();
if let Some(t) = &self.status_message_timer {
let _ = t.thread().id();
}
self.status_message_timer = None;
}
pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
let mut cmds: Vec<Cmd> = Vec::new();
if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
if key::matches(&m.0, std::slice::from_ref(&self.key_map.force_quit)) {
return commands::quit();
}
}
if let Some(m) = msg.as_any().downcast_ref::<FilterMatchesMsg>() {
self.filtered_items = m.clone_items();
return None;
}
if let Some(m) = msg.as_any().downcast_ref::<spinner::TickMsg>() {
let cmd = self.spinner.update(msg);
if self.show_spinner {
cmds.push(cmd);
}
let _ = m;
}
if msg
.as_any()
.downcast_ref::<StatusMessageTimeoutMsg>()
.is_some()
{
self.hide_status_message();
}
let cmd = if self.filter_state == FilterState::Filtering {
self.handle_filtering(msg)
} else {
self.handle_browsing(msg)
};
cmds.push(cmd);
commands::batch(cmds)
}
fn handle_browsing(&mut self, msg: &dyn Msg) -> Cmd {
if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
let k = &m.0;
if key::matches(k, std::slice::from_ref(&self.key_map.clear_filter)) {
self.reset_filtering();
} else if key::matches(k, std::slice::from_ref(&self.key_map.quit)) {
return commands::quit();
} else if key::matches(k, std::slice::from_ref(&self.key_map.cursor_up)) {
self.cursor_up();
} else if key::matches(k, std::slice::from_ref(&self.key_map.cursor_down)) {
self.cursor_down();
} else if key::matches(k, std::slice::from_ref(&self.key_map.prev_page)) {
self.paginator.prev_page();
} else if key::matches(k, std::slice::from_ref(&self.key_map.next_page)) {
self.paginator.next_page();
} else if key::matches(k, std::slice::from_ref(&self.key_map.go_to_start)) {
self.go_to_start();
} else if key::matches(k, std::slice::from_ref(&self.key_map.go_to_end)) {
self.go_to_end();
} else if key::matches(k, std::slice::from_ref(&self.key_map.filter)) {
self.hide_status_message();
if self.filter_input.value().is_empty() {
self.filtered_items = self.items_as_filter_items();
}
self.go_to_start();
self.filter_state = FilterState::Filtering;
self.filter_input.cursor_end();
self.filter_input.focus();
self.update_keybindings();
return Some(Box::new(|| Some(textinput::blink())));
} else if key::matches(k, std::slice::from_ref(&self.key_map.show_full_help))
|| key::matches(k, std::slice::from_ref(&self.key_map.close_full_help))
{
self.help.show_all = !self.help.show_all;
self.update_pagination();
}
}
let cmd = self.delegate.update(msg, self);
self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
cmd
}
fn handle_filtering(&mut self, msg: &dyn Msg) -> Cmd {
let mut cmds: Vec<Cmd> = Vec::new();
if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
let k = &m.0;
if key::matches(
k,
std::slice::from_ref(&self.key_map.cancel_while_filtering),
) {
self.reset_filtering();
self.key_map.filter.set_enabled(true);
self.key_map.clear_filter.set_enabled(false);
} else if key::matches(
k,
std::slice::from_ref(&self.key_map.accept_while_filtering),
) {
self.hide_status_message();
if self.items.is_empty() {
} else {
let h = self.visible_items();
if h.is_empty() {
self.reset_filtering();
} else {
self.filter_input.blur();
self.filter_state = FilterState::FilterApplied;
self.update_keybindings();
if self.filter_input.value().is_empty() {
self.reset_filtering();
}
}
}
}
}
let old_value = self.filter_input.value();
let input_cmd = self.filter_input.update(msg);
let filter_changed = old_value != self.filter_input.value();
cmds.push(input_cmd);
if filter_changed {
cmds.push(filter_items_cmd(self));
self.key_map
.accept_while_filtering
.set_enabled(!self.filter_input.value().is_empty());
}
self.update_pagination();
commands::batch(cmds)
}
pub fn short_help(&self) -> Vec<Binding> {
let mut kb = vec![
self.key_map.cursor_up.clone(),
self.key_map.cursor_down.clone(),
];
let filtering = self.filter_state == FilterState::Filtering;
if !filtering {
kb.extend(self.delegate.short_help());
}
kb.extend(vec![
self.key_map.filter.clone(),
self.key_map.clear_filter.clone(),
self.key_map.accept_while_filtering.clone(),
self.key_map.cancel_while_filtering.clone(),
]);
if !filtering {
if let Some(f) = &self.additional_short_help_keys {
kb.extend(f());
}
}
kb.push(self.key_map.quit.clone());
kb.push(self.key_map.show_full_help.clone());
kb
}
pub fn full_help(&self) -> Vec<Vec<Binding>> {
let mut kb: Vec<Vec<Binding>> = vec![vec![
self.key_map.cursor_up.clone(),
self.key_map.cursor_down.clone(),
self.key_map.next_page.clone(),
self.key_map.prev_page.clone(),
self.key_map.go_to_start.clone(),
self.key_map.go_to_end.clone(),
]];
let filtering = self.filter_state == FilterState::Filtering;
if !filtering {
let fh = self.delegate.full_help();
if !fh.is_empty() {
kb.extend(fh);
}
}
let mut list_level_bindings = vec![
self.key_map.filter.clone(),
self.key_map.clear_filter.clone(),
self.key_map.accept_while_filtering.clone(),
self.key_map.cancel_while_filtering.clone(),
];
if !filtering {
if let Some(f) = &self.additional_full_help_keys {
list_level_bindings.extend(f());
}
}
kb.push(list_level_bindings);
kb.push(vec![
self.key_map.quit.clone(),
self.key_map.close_full_help.clone(),
]);
kb
}
pub fn view(&self) -> String {
let mut sections: Vec<String> = Vec::new();
let mut avail_height = self.height;
if self.show_title || (self.show_filter && self.filtering_enabled) {
let v = self.title_view();
sections.push(v.clone());
avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&v));
}
if self.show_status_bar {
let v = self.status_view();
sections.push(v.clone());
avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&v));
}
let mut pagination = String::new();
if self.show_pagination {
pagination = self.pagination_view();
avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&pagination));
}
let mut help_view = String::new();
if self.show_help {
help_view = self.help_view();
avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&help_view));
}
let content = rusty_lipgloss::new_style()
.height(avail_height)
.render(&self.populated_view());
sections.push(content);
if self.show_pagination {
sections.push(pagination);
}
if self.show_help {
sections.push(help_view);
}
let refs: Vec<&str> = sections.iter().map(|s| s.as_str()).collect();
rusty_lipgloss::join::join_vertical(rusty_lipgloss::LEFT, &refs)
}
fn title_view(&self) -> String {
let title_bar_style = self.styles.title_bar.clone();
let spinner_view = self.spinner_view();
let spinner_width = rusty_lipgloss::size::width(&spinner_view);
let spinner_left_gap = " ";
let spinner_on_left = title_bar_style.get_padding_left()
>= spinner_width + rusty_lipgloss::size::width(spinner_left_gap)
&& self.show_spinner;
let mut view = String::new();
if self.show_filter && self.filter_state == FilterState::Filtering {
view += &self.filter_input.view();
} else if self.show_title {
if self.show_spinner && spinner_on_left {
view += &spinner_view;
view += spinner_left_gap;
let title_bar_gap = title_bar_style.get_padding_left();
let _ = title_bar_gap;
}
view += &self.styles.title.clone().render(&self.title);
if self.filter_state != FilterState::Filtering {
view += " ";
view += &self.status_message;
view = rusty_x_ansi::truncate(
&view,
self.width.saturating_sub(spinner_width),
ELLIPSIS,
);
}
}
if self.show_spinner && !spinner_on_left {
let avail_space =
self.width - rusty_lipgloss::size::width(&title_bar_style.render(&view));
if avail_space > spinner_width {
view += &" ".repeat(avail_space - spinner_width);
view += &spinner_view;
}
}
if !view.is_empty() {
return title_bar_style.render(&view);
}
view
}
fn status_view(&self) -> String {
let mut status = String::new();
let total_items = self.items.len();
let visible_items = self.visible_items().len();
let item_name = if visible_items != 1 {
self.item_name_plural.clone()
} else {
self.item_name_singular.clone()
};
let items_display = format!("{} {}", visible_items, item_name);
if self.filter_state == FilterState::Filtering {
if visible_items == 0 {
status = self.styles.status_empty.clone().render("Nothing matched");
} else {
status = items_display;
}
} else if self.items.is_empty() {
status = self
.styles
.status_empty
.clone()
.render(&format!("No {}", self.item_name_plural));
} else {
let filtered = self.filter_state == FilterState::FilterApplied;
if filtered {
let f = self.filter_input.value().trim().to_string();
let f = rusty_x_ansi::truncate(&f, 10, "…");
status += &format!("“{}” ", f);
}
status += &items_display;
}
let num_filtered = total_items - visible_items;
if num_filtered > 0 {
status += &self.styles.divider_dot.clone().render("");
status += &self
.styles
.status_bar_filter_count
.clone()
.render(&format!("{} filtered", num_filtered));
}
self.styles.status_bar.clone().render(&status)
}
fn pagination_view(&self) -> String {
if self.paginator.total_pages < 2 {
return String::new();
}
let mut s = self.paginator.view();
if rusty_x_ansi::string_width(&s) > self.width {
self.paginator_type_change_to_arabic();
s = self
.styles
.arabic_pagination
.clone()
.render(&self.paginator.view());
}
let mut style = self.styles.pagination_style.clone();
if self.delegate.spacing() == 0 && style.get_margin_top() == 0 {
style = style.margin_top(1);
}
style.render(&s)
}
fn paginator_type_change_to_arabic(&self) {
let _ = self;
}
fn populated_view(&self) -> String {
let items = self.visible_items();
if items.is_empty() {
if self.filter_state == FilterState::Filtering {
return String::new();
}
return self
.styles
.no_items
.clone()
.render(&format!("No {}.", self.item_name_plural));
}
let mut b = String::new();
if !items.is_empty() {
let (start, end) = self.paginator.get_slice_bounds(items.len());
let docs = &items[start..end];
for (i, item) in docs.iter().enumerate() {
b += &self.delegate.render(self, i + start, *item);
if i != docs.len() - 1 {
b += &"\n".repeat(self.delegate.spacing() + 1);
}
}
}
let items_on_page = self.paginator.items_on_page(items.len());
if items_on_page < self.paginator.per_page {
let n = (self.paginator.per_page - items_on_page)
* (self.delegate.height() + self.delegate.spacing());
b += &"\n".repeat(n);
}
b
}
fn help_view(&self) -> String {
self.styles.help_style.clone().render(&self.help.view(self))
}
fn spinner_view(&self) -> String {
self.spinner.view()
}
}
fn filter_items(m: &Model) -> Vec<FilteredItem> {
if m.filter_input.value().is_empty() || m.filter_state == FilterState::Unfiltered {
return m
.items
.iter()
.enumerate()
.map(|(i, item)| FilteredItem {
index: i,
item: item.box_clone(),
matches: vec![],
})
.collect();
}
let items = &m.items;
let targets: Vec<String> = items.iter().map(|t| t.filter_value()).collect();
let mut filter_matches: Vec<FilteredItem> = vec![];
for r in (m.filter)(&m.filter_input.value(), &targets) {
filter_matches.push(FilteredItem {
index: r.index,
item: items[r.index].box_clone(),
matches: r.matched_indexes,
});
}
filter_matches
}
fn filter_items_cmd(m: &Model) -> Cmd {
let items = filter_items(m);
Some(Box::new(move || Some(Box::new(FilterMatchesMsg(items)))))
}
fn insert_item_into_slice(
items: &[Box<dyn Item + Send + Sync>],
item: Box<dyn Item + Send + Sync>,
index: usize,
) -> Vec<Box<dyn Item + Send + Sync>> {
if items.is_empty() {
return vec![item];
}
if index >= items.len() {
let mut items: Vec<Box<dyn Item + Send + Sync>> =
items.iter().map(|i| i.box_clone()).collect();
items.push(item);
return items;
}
let mut items: Vec<Box<dyn Item + Send + Sync>> = items.iter().map(|i| i.box_clone()).collect();
items.insert(index, item);
items
}
fn remove_item_from_slice(
items: &[Box<dyn Item + Send + Sync>],
index: usize,
) -> Vec<Box<dyn Item + Send + Sync>> {
if index >= items.len() {
return items.iter().map(|i| i.box_clone()).collect(); }
let mut items: Vec<Box<dyn Item + Send + Sync>> = items.iter().map(|i| i.box_clone()).collect();
items.remove(index);
items
}
fn remove_filter_match_from_slice(items: &[FilteredItem], index: usize) -> Vec<FilteredItem> {
if index >= items.len() {
return items.iter().map(|f| f.clone_item()).collect(); }
let mut items: Vec<FilteredItem> = items.iter().map(|f| f.clone_item()).collect();
items.remove(index);
items
}
fn count_enabled_bindings(groups: &[Vec<Binding>]) -> usize {
let mut agg = 0;
for group in groups {
for kb in group {
if kb.enabled() {
agg += 1;
}
}
}
agg
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub cursor_up: Binding,
pub cursor_down: Binding,
pub next_page: Binding,
pub prev_page: Binding,
pub go_to_start: Binding,
pub go_to_end: Binding,
pub filter: Binding,
pub clear_filter: Binding,
pub cancel_while_filtering: Binding,
pub accept_while_filtering: Binding,
pub show_full_help: Binding,
pub close_full_help: Binding,
pub quit: Binding,
pub force_quit: Binding,
}
pub fn default_key_map() -> KeyMap {
KeyMap {
cursor_up: key::new_binding(vec![
key::with_keys(&["up", "k"]),
key::with_help("↑/k", "up"),
]),
cursor_down: key::new_binding(vec![
key::with_keys(&["down", "j"]),
key::with_help("↓/j", "down"),
]),
prev_page: key::new_binding(vec![
key::with_keys(&["left", "h", "pgup", "b", "u"]),
key::with_help("←/h/pgup", "prev page"),
]),
next_page: key::new_binding(vec![
key::with_keys(&["right", "l", "pgdown", "f", "d"]),
key::with_help("→/l/pgdn", "next page"),
]),
go_to_start: key::new_binding(vec![
key::with_keys(&["home", "g"]),
key::with_help("g/home", "go to start"),
]),
go_to_end: key::new_binding(vec![
key::with_keys(&["end", "G"]),
key::with_help("G/end", "go to end"),
]),
filter: key::new_binding(vec![key::with_keys(&["/"]), key::with_help("/", "filter")]),
clear_filter: key::new_binding(vec![
key::with_keys(&["esc"]),
key::with_help("esc", "clear filter"),
]),
cancel_while_filtering: key::new_binding(vec![
key::with_keys(&["esc"]),
key::with_help("esc", "cancel"),
]),
accept_while_filtering: key::new_binding(vec![
key::with_keys(&[
"enter",
"tab",
"shift+tab",
"ctrl+k",
"up",
"ctrl+j",
"down",
]),
key::with_help("enter", "apply filter"),
]),
show_full_help: key::new_binding(vec![key::with_keys(&["?"]), key::with_help("?", "more")]),
close_full_help: key::new_binding(vec![
key::with_keys(&["?"]),
key::with_help("?", "close help"),
]),
quit: key::new_binding(vec![
key::with_keys(&["q", "esc"]),
key::with_help("q", "quit"),
]),
force_quit: key::new_binding(vec![key::with_keys(&["ctrl+c"])]),
}
}
#[derive(Debug, Clone)]
pub struct DefaultItemStyles {
pub normal_title: Style,
pub normal_desc: Style,
pub selected_title: Style,
pub selected_desc: Style,
pub dimmed_title: Style,
pub dimmed_desc: Style,
pub filter_match: Style,
}
pub fn new_default_item_styles(is_dark: bool) -> DefaultItemStyles {
let light_dark = rusty_lipgloss::color::light_dark(is_dark);
let mut s = DefaultItemStyles {
normal_title: rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#1a1a1a"), Color::parse("#dddddd")))
.padding(&[0, 0, 0, 2]),
normal_desc: rusty_lipgloss::new_style(),
selected_title: rusty_lipgloss::new_style()
.border(
rusty_lipgloss::Border::normal(),
&[false, false, false, true],
)
.border_foreground(&[
&light_dark(Color::parse("#F793FF"), Color::parse("#AD58B4")).to_string(),
])
.foreground_color(light_dark(Color::parse("#EE6FF8"), Color::parse("#EE6FF8")))
.padding(&[0, 0, 0, 1]),
selected_desc: rusty_lipgloss::new_style(),
dimmed_title: rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")))
.padding(&[0, 0, 0, 2]),
dimmed_desc: rusty_lipgloss::new_style(),
filter_match: rusty_lipgloss::new_style().underline(true),
};
s.normal_desc = s
.normal_title
.clone()
.foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")));
s.selected_desc = s
.selected_title
.clone()
.foreground_color(light_dark(Color::parse("#F793FF"), Color::parse("#AD58B4")));
s.dimmed_desc = s
.dimmed_title
.clone()
.foreground_color(light_dark(Color::parse("#C2B8C2"), Color::parse("#4D4D4D")));
s
}
pub trait DefaultItem: Item {
fn title(&self) -> String;
fn description(&self) -> String;
fn as_default_item(&self) -> Option<&dyn DefaultItem>
where
Self: Sized,
{
Some(self)
}
}
pub struct DefaultDelegate {
pub show_description: bool,
pub styles: DefaultItemStyles,
pub update_func: Option<ItemUpdateFunc>,
pub short_help_func: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
pub full_help_func: Option<Box<dyn Fn() -> Vec<Vec<Binding>> + Send + Sync>>,
height: usize,
spacing: usize,
}
pub fn new_default_delegate() -> DefaultDelegate {
const DEFAULT_HEIGHT: usize = 2;
const DEFAULT_SPACING: usize = 1;
DefaultDelegate {
show_description: true,
styles: new_default_item_styles(true),
height: DEFAULT_HEIGHT,
spacing: DEFAULT_SPACING,
update_func: None,
short_help_func: None,
full_help_func: None,
}
}
impl DefaultDelegate {
pub fn set_height(&mut self, i: usize) {
self.height = i;
}
pub fn height(&self) -> usize {
if self.show_description {
self.height
} else {
1
}
}
pub fn set_spacing(&mut self, i: usize) {
self.spacing = i;
}
pub fn spacing(&self) -> usize {
self.spacing
}
pub fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String {
let mut title;
let mut desc;
let di = item.as_default_item();
if let Some(i) = di {
title = i.title();
desc = i.description();
} else {
return String::new();
}
if m.width == 0 {
return String::new();
}
let s = &self.styles;
let textwidth =
m.width - s.normal_title.get_padding_left() - s.normal_title.get_padding_right();
title = rusty_x_ansi::truncate(&title, textwidth, ELLIPSIS);
if self.show_description {
let mut lines: Vec<String> = vec![];
for (i, line) in desc.split('\n').enumerate() {
if i >= self.height - 1 {
break;
}
lines.push(rusty_x_ansi::truncate(line, textwidth, ELLIPSIS));
}
desc = lines.join("\n");
}
let is_selected = index == m.index();
let empty_filter =
m.filter_state() == FilterState::Filtering && m.filter_value().is_empty();
let is_filtered = m.filter_state() == FilterState::Filtering
|| m.filter_state() == FilterState::FilterApplied;
let mut matched_rumes: Vec<usize> = vec![];
if is_filtered && index < m.filtered_items.len() {
matched_rumes = m.matches_for_item(index);
}
if empty_filter {
title = s.dimmed_title.clone().render(&title);
desc = s.dimmed_desc.clone().render(&desc);
} else if is_selected && m.filter_state() != FilterState::Filtering {
if is_filtered {
let unmatched = s.selected_title.clone().inline(true);
let matched = unmatched.clone().inherit(&s.filter_match);
title = rusty_lipgloss::runes::style_runes(
&title,
&matched_rumes,
&matched,
&unmatched,
);
}
title = s.selected_title.clone().render(&title);
desc = s.selected_desc.clone().render(&desc);
} else {
if is_filtered {
let unmatched = s.normal_title.clone().inline(true);
let matched = unmatched.clone().inherit(&s.filter_match);
title = rusty_lipgloss::runes::style_runes(
&title,
&matched_rumes,
&matched,
&unmatched,
);
}
title = s.normal_title.clone().render(&title);
desc = s.normal_desc.clone().render(&desc);
}
if self.show_description {
return format!("{}\n{}", title, desc);
}
title
}
}
impl ItemDelegate for DefaultDelegate {
fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String {
DefaultDelegate::render(self, m, index, item)
}
fn height(&self) -> usize {
DefaultDelegate::height(self)
}
fn spacing(&self) -> usize {
DefaultDelegate::spacing(self)
}
fn update(&self, msg: &dyn Msg, m: &Model) -> Cmd {
if let Some(f) = &self.update_func {
return f(msg, m);
}
None
}
fn short_help(&self) -> Vec<Binding> {
if let Some(f) = &self.short_help_func {
return f();
}
vec![]
}
fn full_help(&self) -> Vec<Vec<Binding>> {
if let Some(f) = &self.full_help_func {
return f();
}
vec![]
}
}
#[derive(Debug, Clone)]
pub struct Styles {
pub title_bar: Style,
pub title: Style,
pub spinner: Style,
pub filter: textinput::Styles,
pub default_filter_character_match: Style,
pub status_bar: Style,
pub status_empty: Style,
pub status_bar_active_filter: Style,
pub status_bar_filter_count: Style,
pub no_items: Style,
pub pagination_style: Style,
pub help_style: Style,
pub active_pagination_dot: Style,
pub inactive_pagination_dot: Style,
pub arabic_pagination: Style,
pub divider_dot: Style,
}
pub fn default_styles(is_dark: bool) -> Styles {
let light_dark = rusty_lipgloss::color::light_dark(is_dark);
let very_subdued_color = light_dark(Color::parse("#DDDADA"), Color::parse("#3C3C3C"));
let subdued_color = light_dark(Color::parse("#9B9B9B"), Color::parse("#5C5C5C"));
let title_bar = rusty_lipgloss::new_style().padding(&[0, 0, 1, 2]);
let title = rusty_lipgloss::new_style()
.background_color(Color::parse("62"))
.foreground_color(Color::parse("230"))
.padding(&[0, 1]);
let spinner_style = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#8E8E8E"), Color::parse("#747373")));
let prompt = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#04B575"), Color::parse("#ECFD65")));
let mut filter = textinput::default_styles(is_dark);
filter.cursor.color = light_dark(Color::parse("#EE6FF8"), Color::parse("#EE6FF8"));
filter.blurred.prompt = prompt.clone();
filter.focused.prompt = prompt;
let default_filter_character_match = rusty_lipgloss::new_style().underline(true);
let status_bar = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")))
.padding(&[0, 0, 1, 2]);
let status_empty = rusty_lipgloss::new_style().foreground_color(subdued_color.clone());
let status_bar_active_filter = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#1a1a1a"), Color::parse("#dddddd")));
let status_bar_filter_count =
rusty_lipgloss::new_style().foreground_color(very_subdued_color.clone());
let no_items = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#909090"), Color::parse("#626262")));
let arabic_pagination = rusty_lipgloss::new_style().foreground_color(subdued_color);
let pagination_style = rusty_lipgloss::new_style().padding_left(2);
let help_style = rusty_lipgloss::new_style().padding(&[1, 0, 0, 2]);
let active_pagination_dot = rusty_lipgloss::new_style()
.foreground_color(light_dark(Color::parse("#847A85"), Color::parse("#979797")))
.set_string(&[BULLET]);
let inactive_pagination_dot = rusty_lipgloss::new_style()
.foreground_color(very_subdued_color.clone())
.set_string(&[BULLET]);
let divider_dot = rusty_lipgloss::new_style()
.foreground_color(very_subdued_color)
.set_string(&[" • "]);
Styles {
title_bar,
title,
spinner: spinner_style,
filter,
default_filter_character_match,
status_bar,
status_empty,
status_bar_active_filter,
status_bar_filter_count,
no_items,
pagination_style,
help_style,
active_pagination_dot,
inactive_pagination_dot,
arabic_pagination,
divider_dot,
}
}