1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
use std::io::{self, IsTerminal};
use crossterm::terminal;
use once_cell::sync::OnceCell;
use crate::{color::colorize, macros::*, term, ColorScheme, Item, Menu, Mode};
impl<T: Send + Sync> Item<T> {
pub fn new(display: &str, value: T) -> Item<T> {
Item {
alias: display.to_string(),
value,
score: None,
matched_indices: None,
}
}
}
// constructor
impl<T: Send + Sync> Menu<T> {
/// Create a new menu instance
///
/// # Example
/// ```no_run
/// let mut menu = termenu::Menu::new().unwrap();
/// ```
pub fn new() -> io::Result<Menu<T>> {
let is_pipe = !io::stdin().is_terminal();
let (_, row) = term::get_cursor_position(is_pipe)?;
let (_, rows) = crossterm::terminal::size()?;
Ok(Menu {
is_pipe,
enable_print_result: true,
colorscheme: ColorScheme::default(),
title: None,
item_list: Vec::new(),
mode: Mode::Normal,
cursor_abs_pos: (row, 0),
max_row: rows,
selection_idx: 0,
selected: false,
fuzzy_matcher: OnceCell::new(),
query: String::new(),
insert_idx: 0,
scroll_offset: 0,
max_height_percent: 1.0,
matched_item_indices: Vec::new(),
show_end_tag: true,
rayon_pool: OnceCell::new(),
})
}
/// Set if show the '---end---' tag at the end of the menu
pub fn show_end_tag(&mut self, b: bool) -> &mut Self {
self.show_end_tag = b;
self
}
/// Set the max height of the menu, should be a percentage in range (0, 1], otherwise it will
/// be ignored
pub fn set_max_height(&mut self, percent: f32) -> &mut Self {
if percent <= 0.0 || percent > 1.0 {
return self;
}
let (row, _) = self.cursor_abs_pos;
let display_cnt = (self.max_row as f32 * percent) as u16;
self.max_height_percent = percent;
self.max_row = self.max_row.min(display_cnt + row + 1);
self
}
/// Set the title of the menu, which will be displayed at the top of the menu
pub fn set_title(&mut self, t: &str) -> &mut Self {
self.title = Some(t.to_string());
self
}
/// Controls whether to print the result to stderr after selecting.
///
/// For example, by default the result would be print like this in stderr:
/// select: $selected_item
///
/// You can disable this by calling this function with `false`.
///
/// This is useful when you want to use termenu in a pipe like this:
/// ```bash
/// cat file.txt | termenu | grep 123
/// ```
pub fn enable_print_result(&mut self, b: bool) -> &mut Self {
self.enable_print_result = b;
self
}
pub(crate) fn get_title(&self) -> &str {
self.title.as_deref().unwrap_or("select")
}
pub fn add(&mut self, item: Item<T>) -> &mut Self {
self.item_list.push(item);
self
}
pub fn add_list(&mut self, items: Vec<Item<T>>) -> &mut Self {
self.item_list.extend(items);
self
}
/// reset the menu state and clear all the items that have been added,
/// this will not reset the color scheme and other reusable settings
pub fn reset(&mut self) -> io::Result<()> {
self.item_list.clear();
self.matched_item_indices.clear();
self.selected = false;
self.mode = Mode::Normal;
// let (_, row) = crossterm::cursor::position()?;
let (_, row) = term::get_cursor_position(self.is_pipe)?;
let (_, rows) = crossterm::terminal::size()?;
self.cursor_abs_pos = (row, 0);
self.max_row = rows;
self.query = String::new();
self.insert_idx = 0;
self.scroll_offset = 0;
Ok(())
}
}
// select api
impl<T: Send + Sync> Menu<T> {
/// Start the menu and return the selection
/// if the user presses `esc` or `ctrl-c`, `None` will be returned
/// otherwise, the selected item will be returned
pub fn select(&mut self) -> io::Result<Option<&T>> {
match self.select0()? {
Some(item) => Ok(Some(&item.value)),
None => Ok(None),
}
}
/// Same as [select](Self::select), but return the selected [Item] instead of the value
pub fn select_item(&mut self) -> io::Result<Option<&Item<T>>> {
self.select0()
}
fn select0(&mut self) -> io::Result<Option<&Item<T>>> {
use crossterm::event;
if self.item_list.is_empty() {
return Ok(None);
}
terminal::enable_raw_mode()?;
self.scroll_to_fit()?;
self.enter_normal_mode()?;
self.draw()?;
loop {
if let event::Event::Key(key) = event::read()? {
let resp = self.dispatch_key(key)?;
if resp.exit() {
break;
}
if resp.redraw() {
self.draw()?;
}
}
}
Ok(self.get_selection())
}
// when the cursor is at the bottom of the screen, scroll up to fit the menu
fn scroll_to_fit(&mut self) -> io::Result<()> {
// get the size of the terminal
let (row, _) = self.cursor_abs_pos;
// check how many rows are left
let (_, term_max_row) = crossterm::terminal::size()?;
let left_rows = term_max_row - row;
// check how many items are there
let item_cnt =
self.item_list
.len()
.min((self.max_row as f32 * self.max_height_percent) as usize) as u16;
// if there are more rows than items, no need to scroll
// plus 2 is for the title and the more tag
if item_cnt + 2 < left_rows {
return Ok(());
}
// if there are more items than rows, scroll up
let diff = item_cnt + 2 - left_rows;
term_exec!(crossterm::terminal::ScrollUp(diff.min(self.max_row - 1)));
// we've alreay scrolled up, but the cursor is still at the bottom of the screen
// just move the cursor up
self.cursor_abs_pos = (row.saturating_sub(diff), 0);
Ok(())
}
fn get_selection(&mut self) -> Option<&Item<T>> {
ignore_io_error!(self.clear()?);
if !self.selected {
if self.enable_print_result {
ignore_io_error!({
term_printf!("{}", colorize(self.get_title(), &self.colorscheme.title),);
term_cursor_down!(1);
});
}
return None;
}
let item_idx = match self.mode {
Mode::Normal => (self.selection_idx + self.scroll_offset) as usize,
Mode::Query => {
self.matched_item_indices[(self.selection_idx + self.scroll_offset) as usize]
}
};
let item = self.item_list.get(item_idx).unwrap();
// print the result to the terminal
if self.enable_print_result {
ignore_io_error!({
term_printf!(
"{} {}",
colorize(self.get_title(), &self.colorscheme.title),
item.alias
);
term_cursor_down!(1);
});
}
Some(item)
}
}
// drop trait
impl<T: Send + Sync> Drop for Menu<T> {
fn drop(&mut self) {
ignore_io_error!({
term_cursor_col!(0);
term_exec!(crossterm::cursor::Show);
});
terminal::disable_raw_mode().unwrap();
}
}