termenu/
keymap.rs

1use std::{cmp, io};
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5use crate::{macros::*, Menu, Mode};
6
7pub(crate) struct KeyResponse(bool, bool);
8
9impl KeyResponse {
10    fn new(exit: bool, redraw: bool) -> Self {
11        KeyResponse(exit, redraw)
12    }
13
14    pub(crate) fn exit(&self) -> bool {
15        self.0
16    }
17
18    pub(crate) fn redraw(&self) -> bool {
19        self.1
20    }
21}
22
23impl<T: Send + Sync> Menu<T> {
24    pub(crate) fn dispatch_key(&mut self, key: KeyEvent) -> io::Result<KeyResponse> {
25        match key.modifiers {
26            KeyModifiers::NONE | KeyModifiers::SHIFT => self.dispatch_code(key.code),
27
28            KeyModifiers::CONTROL => {
29                if key.code == KeyCode::Char('c') {
30                    return Ok(KeyResponse::new(true, false));
31                }
32                match key.code {
33                    KeyCode::Char('n') => return self.key_down(),
34                    KeyCode::Char('p') => return self.key_up(),
35                    _ => {}
36                }
37                Ok(KeyResponse::new(false, false))
38            }
39
40            _ => Ok(KeyResponse::new(false, false)),
41        }
42    }
43
44    fn dispatch_code(&mut self, code: KeyCode) -> io::Result<KeyResponse> {
45        match self.mode {
46            Mode::Normal => self.dispatch_normal(code),
47            Mode::Query => self.dispatch_query(code),
48        }
49    }
50
51    fn dispatch_normal(&mut self, code: KeyCode) -> io::Result<KeyResponse> {
52        match code {
53            KeyCode::Up | KeyCode::Char('k') => return self.key_up(),
54
55            KeyCode::Down | KeyCode::Char('j') => return self.key_down(),
56
57            KeyCode::Esc => return self.key_esc(),
58
59            KeyCode::Enter => return self.key_enter(),
60
61            KeyCode::Char('/') => {
62                self.enter_query_mode()?;
63            }
64            _ => {}
65        };
66        Ok(KeyResponse::new(false, true))
67    }
68
69    fn dispatch_query(&mut self, code: KeyCode) -> io::Result<KeyResponse> {
70        match code {
71            KeyCode::Esc => return self.key_esc(),
72
73            KeyCode::Up => return self.key_up(),
74
75            KeyCode::Down => return self.key_down(),
76
77            KeyCode::Left => {
78                self.insert_idx = self.insert_idx.saturating_sub(1);
79            }
80
81            KeyCode::Right => {
82                self.insert_idx = cmp::min(self.insert_idx + 1, self.query.len());
83            }
84
85            KeyCode::Enter => return self.key_enter(),
86
87            KeyCode::Char(c) => {
88                let insert_pos = get_insert_pos!(&self.query, self.insert_idx);
89                self.query.insert(insert_pos, c);
90                self.insert_idx += 1;
91                self.fuzzy_match();
92            }
93
94            KeyCode::Backspace => {
95                if !self.query.is_empty() {
96                    self.insert_idx = self.insert_idx.saturating_sub(1);
97                    let pos = get_insert_pos!(&self.query, self.insert_idx);
98                    self.query.remove(pos);
99                    self.fuzzy_match();
100                }
101            }
102            _ => {}
103        }
104        Ok(KeyResponse(false, true))
105    }
106}
107
108impl<T: Send + Sync> Menu<T> {
109    fn key_up(&mut self) -> io::Result<KeyResponse> {
110        if self.selection_idx == 0 {
111            if self.scroll_offset == 0 {
112                return Ok(KeyResponse(false, false));
113            }
114            self.scroll_offset -= 1;
115            return Ok(KeyResponse(false, true));
116        }
117        self.selection_idx -= 1;
118        Ok(KeyResponse(false, true))
119    }
120
121    fn key_down(&mut self) -> io::Result<KeyResponse> {
122        let item_cnt = match self.mode {
123            Mode::Normal => self.item_list.len(),
124            Mode::Query => self.matched_item_indices.len(),
125        } as u16;
126        if self.selection_idx + self.scroll_offset == item_cnt - 1 {
127            return Ok(KeyResponse(false, false));
128        }
129        let (row, _) = self.cursor_abs_pos;
130        if self.selection_idx + row == self.max_row - 3 {
131            self.scroll_offset += 1;
132            return Ok(KeyResponse(false, true));
133        }
134        self.selection_idx += 1;
135        Ok(KeyResponse(false, true))
136    }
137
138    fn key_esc(&mut self) -> io::Result<KeyResponse> {
139        match self.mode {
140            Mode::Normal => Ok(KeyResponse(true, false)),
141            _ => {
142                self.enter_normal_mode()?;
143                Ok(KeyResponse(false, true))
144            }
145        }
146    }
147
148    fn key_enter(&mut self) -> io::Result<KeyResponse> {
149        match self.mode {
150            Mode::Normal => {
151                self.selected = true;
152            }
153            Mode::Query => {
154                if !self.matched_item_indices.is_empty() {
155                    self.selected = true;
156                }
157            }
158        }
159        Ok(KeyResponse(true, false))
160    }
161}
162
163impl<T: Send + Sync> Menu<T> {
164    pub(crate) fn get_query_cursor_col(&self) -> u16 {
165        let mut col = 0;
166
167        // calculate the prefix cells
168        let prefix = format!("{} /", self.get_title());
169        col += prefix
170            .chars()
171            // a Chinese character takes 3 bytes, however, it only takes 2 cells in terminals
172            .fold(0, |acc, c| acc + cmp::min(2, c.len_utf8() as u16));
173
174        // calculate the query cells from 0 to self.insert_idx
175        col += self
176            .query
177            .chars()
178            .take(self.insert_idx)
179            .fold(0, |acc, c| acc + cmp::min(2, c.len_utf8() as u16));
180
181        col
182    }
183}