Skip to main content

pito_tui/
filter.rs

1use std::borrow::Cow;
2
3use crossterm::event::KeyEvent;
4use pito_footer::{Edit, Input, InputBar, Key, Tone};
5use pito_list::{Column, Keys, List, ListView, Row, Step};
6
7use crate::palette::Palette;
8
9const START: &[Key] = &[Key::Char('/')];
10const CLEAR: &[Key] = &[Key::Esc];
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum Turn {
15    Pass,
16    Taken,
17    Filtered,
18    Open(usize),
19}
20
21#[derive(Debug, Clone)]
22pub struct Filter {
23    list: List,
24    input: Input,
25    typing: bool,
26    shown: Vec<usize>,
27    label: Cow<'static, str>,
28    placeholder: Cow<'static, str>,
29    hint: Cow<'static, str>,
30    start: &'static [Key],
31    clear: &'static [Key],
32}
33
34impl Filter {
35    pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
36        Filter {
37            list: List::new(),
38            input: Input::new(),
39            typing: false,
40            shown: Vec::new(),
41            label: label.into(),
42            placeholder: Cow::Borrowed(""),
43            hint: Cow::Borrowed(""),
44            start: START,
45            clear: CLEAR,
46        }
47    }
48
49    pub fn placeholder(mut self, text: impl Into<Cow<'static, str>>) -> Self {
50        self.placeholder = text.into();
51        self
52    }
53
54    pub fn hint(mut self, text: impl Into<Cow<'static, str>>) -> Self {
55        self.hint = text.into();
56        self
57    }
58
59    pub fn keys(mut self, keys: Keys) -> Self {
60        self.list = self.list.keys(keys);
61        self
62    }
63
64    pub fn start_keys(mut self, keys: &'static [Key]) -> Self {
65        self.start = keys;
66        self
67    }
68
69    pub fn clear_keys(mut self, keys: &'static [Key]) -> Self {
70        self.clear = keys;
71        self
72    }
73
74    pub fn input(mut self, input: Input) -> Self {
75        self.input = input;
76        self
77    }
78
79    pub fn typing(&self) -> bool {
80        self.typing
81    }
82
83    pub fn query(&self) -> &str {
84        self.input.value()
85    }
86
87    pub fn active(&self) -> bool {
88        !self.input.value().trim().is_empty()
89    }
90
91    pub fn shown(&self) -> &[usize] {
92        &self.shown
93    }
94
95    pub fn current(&self) -> Option<usize> {
96        self.list
97            .selected()
98            .and_then(|at| self.shown.get(at).copied())
99    }
100
101    pub fn list(&self) -> &List {
102        &self.list
103    }
104
105    pub fn list_mut(&mut self) -> &mut List {
106        &mut self.list
107    }
108
109    pub fn sift<R>(
110        &mut self,
111        rows: &[R],
112        keeps: impl Fn(&R, &str) -> bool,
113        line: impl Fn(&R) -> Row,
114    ) {
115        let kept = self.current();
116        let query = self.input.value();
117        self.shown = rows
118            .iter()
119            .enumerate()
120            .filter(|(_, row)| keeps(row, query))
121            .map(|(index, _)| index)
122            .collect();
123        let lines: Vec<Row> = self.shown.iter().map(|&index| line(&rows[index])).collect();
124        self.list.set_rows(lines);
125        let at = kept
126            .and_then(|kept| self.shown.iter().position(|&index| index == kept))
127            .unwrap_or(0);
128        self.list.select(at);
129    }
130
131    pub fn key(&mut self, key: KeyEvent) -> Turn {
132        if self.typing {
133            return match self.input.key(key.into()) {
134                Edit::Changed => Turn::Filtered,
135                Edit::Submit(_) => {
136                    self.typing = false;
137                    Turn::Taken
138                }
139                Edit::Cancel => {
140                    self.typing = false;
141                    self.input.clear();
142                    Turn::Filtered
143                }
144                Edit::Pass => match self.list.key(key.into()) {
145                    Step::Pass | Step::Open(_) => Turn::Pass,
146                    _ => Turn::Taken,
147                },
148                _ => Turn::Taken,
149            };
150        }
151        let pressed: Key = key.into();
152        if self.start.contains(&pressed) {
153            self.typing = true;
154            return Turn::Taken;
155        }
156        if self.active() && self.clear.contains(&pressed) {
157            self.input.clear();
158            return Turn::Filtered;
159        }
160        match self.list.key(key.into()) {
161            Step::Pass => Turn::Pass,
162            Step::Open(at) => self
163                .shown
164                .get(at)
165                .map_or(Turn::Taken, |&index| Turn::Open(index)),
166            _ => Turn::Taken,
167        }
168    }
169
170    pub fn paste(&mut self, text: &str) -> Turn {
171        if !self.typing {
172            return Turn::Pass;
173        }
174        match self.input.paste(text) {
175            Edit::Changed => Turn::Filtered,
176            _ => Turn::Taken,
177        }
178    }
179
180    pub fn bar(&self) -> Option<InputBar<'_>> {
181        self.typing.then(|| {
182            let bar = InputBar::new(&self.label, &self.input)
183                .placeholder(&self.placeholder)
184                .tone(Tone::Accent);
185            if self.hint.is_empty() {
186                bar
187            } else {
188                bar.hint(&self.hint)
189            }
190        })
191    }
192
193    pub fn view<'a>(&'a mut self, columns: &'a [Column<'a>], palette: &Palette) -> ListView<'a> {
194        palette.view(&mut self.list, columns)
195    }
196}
197
198pub fn matches(text: &str, query: &str) -> bool {
199    let text = text.to_lowercase();
200    query
201        .split_whitespace()
202        .all(|word| text.contains(&word.to_lowercase()))
203}