use crate::model::{Match, Verdict};
pub const DEFAULT_PAGE: usize = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Filter,
Help,
Detail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortKey {
Similarity,
Popularity,
Name,
}
impl SortKey {
pub fn label(self) -> &'static str {
match self {
SortKey::Similarity => "similarity",
SortKey::Popularity => "popularity",
SortKey::Name => "name",
}
}
fn next(self) -> Self {
match self {
SortKey::Similarity => SortKey::Popularity,
SortKey::Popularity => SortKey::Name,
SortKey::Name => SortKey::Similarity,
}
}
}
pub struct App<'a> {
idea: &'a str,
verdict: &'a Verdict,
matches: &'a [Match],
cursor: usize,
filter: String,
visible: Vec<usize>,
mode: Mode,
expanded: bool,
quit: bool,
sort: SortKey,
}
impl<'a> App<'a> {
pub fn new(idea: &'a str, verdict: &'a Verdict, matches: &'a [Match]) -> Self {
let visible = (0..matches.len()).collect();
let mut app = Self {
idea,
verdict,
matches,
cursor: 0,
filter: String::new(),
visible,
mode: Mode::Normal,
expanded: false,
quit: false,
sort: SortKey::Similarity,
};
app.apply_sort();
app
}
pub fn idea(&self) -> &str {
self.idea
}
pub fn verdict(&self) -> &Verdict {
self.verdict
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn filter_text(&self) -> &str {
&self.filter
}
pub fn should_quit(&self) -> bool {
self.quit
}
pub fn is_expanded(&self) -> bool {
self.expanded
}
pub fn quit(&mut self) {
self.quit = true;
}
pub fn visible_matches(&self) -> Vec<&Match> {
self.visible.iter().map(|&i| &self.matches[i]).collect()
}
pub fn displayed_matches(&self) -> Vec<&Match> {
let limit = self.display_limit();
self.visible
.iter()
.take(limit)
.map(|&i| &self.matches[i])
.collect()
}
pub fn has_more(&self) -> bool {
!self.expanded && self.visible.len() > DEFAULT_PAGE
}
pub fn toggle_expand(&mut self) {
self.expanded = !self.expanded;
self.clamp_cursor();
}
pub fn scroll_down(&mut self) {
let max = self.display_limit().saturating_sub(1);
if self.cursor < max {
self.cursor += 1;
} else {
self.cursor = 0;
}
}
pub fn scroll_up(&mut self) {
if self.cursor == 0 {
self.cursor = self.display_limit().saturating_sub(1);
} else {
self.cursor -= 1;
}
}
pub fn total_matches(&self) -> usize {
self.matches.len()
}
pub fn scroll_to_top(&mut self) {
self.cursor = 0;
}
pub fn scroll_to_bottom(&mut self) {
self.cursor = self.display_limit().saturating_sub(1);
}
pub fn toggle_help(&mut self) {
self.mode = if self.mode == Mode::Help {
Mode::Normal
} else {
Mode::Help
};
}
pub fn enter_filter(&mut self) {
self.mode = Mode::Filter;
self.cursor = 0;
}
pub fn confirm_filter(&mut self) {
self.mode = Mode::Normal;
}
pub fn exit_filter(&mut self) {
self.mode = Mode::Normal;
self.filter.clear();
self.recompute_visible();
self.cursor = 0;
}
pub fn filter_push(&mut self, c: char) {
self.filter.push(c);
self.recompute_visible();
self.clamp_cursor();
}
pub fn filter_pop(&mut self) {
self.filter.pop();
self.recompute_visible();
self.clamp_cursor();
}
pub fn selected_match(&self) -> Option<&Match> {
let limit = self.display_limit();
self.visible
.iter()
.take(limit)
.nth(self.cursor)
.map(|&i| &self.matches[i])
}
pub fn selected_url(&self) -> Option<&str> {
self.selected_match().map(|m| m.url.as_str())
}
pub fn select_row(&mut self, row: usize) {
let limit = self.display_limit();
if limit == 0 {
return;
}
self.cursor = row.min(limit - 1);
}
pub fn enter_detail(&mut self) {
if self.selected_match().is_some() {
self.mode = Mode::Detail;
}
}
pub fn exit_detail(&mut self) {
self.mode = Mode::Normal;
}
pub fn sort(&self) -> SortKey {
self.sort
}
pub fn cycle_sort(&mut self) {
self.sort = self.sort.next();
self.apply_sort();
self.cursor = 0;
}
fn display_limit(&self) -> usize {
if self.expanded {
self.visible.len()
} else {
self.visible.len().min(DEFAULT_PAGE)
}
}
fn recompute_visible(&mut self) {
if self.filter.is_empty() {
self.visible = (0..self.matches.len()).collect();
} else {
let lower = self.filter.to_lowercase();
self.visible = self
.matches
.iter()
.enumerate()
.filter(|(_, m)| {
m.name.to_lowercase().contains(&lower)
|| m.description.to_lowercase().contains(&lower)
})
.map(|(i, _)| i)
.collect();
}
self.apply_sort();
}
fn apply_sort(&mut self) {
let matches = self.matches;
match self.sort {
SortKey::Similarity => self.visible.sort_by(|&a, &b| {
matches[b]
.similarity
.partial_cmp(&matches[a].similarity)
.unwrap_or(std::cmp::Ordering::Equal)
}),
SortKey::Popularity => self
.visible
.sort_by(|&a, &b| matches[b].popularity.cmp(&matches[a].popularity)),
SortKey::Name => self.visible.sort_by(|&a, &b| {
matches[a]
.name
.to_lowercase()
.cmp(&matches[b].name.to_lowercase())
}),
}
}
fn clamp_cursor(&mut self) {
let limit = self.display_limit();
if limit == 0 {
self.cursor = 0;
} else if self.cursor >= limit {
self.cursor = limit - 1;
}
}
}