1use zellij_tile::prelude::*;
66
67use std::fmt::Write as _;
68
69use owo_colors::OwoColorize as _;
70use unicode_width::UnicodeWidthChar as _;
71
72const PICKER_EVENTS: &[EventType] = &[EventType::Key];
73
74#[derive(Debug)]
79pub struct Entry<T> {
80 pub string: String,
83 pub data: T,
86}
87
88impl<T> AsRef<str> for Entry<T> {
89 fn as_ref(&self) -> &str {
90 &self.string
91 }
92}
93
94#[derive(Debug)]
96pub enum Response {
97 Select(usize),
99 Cancel,
101}
102
103#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
104enum InputMode {
105 #[default]
106 Normal,
107 Search,
108}
109
110#[derive(Default)]
112pub struct Picker<T> {
113 query: String,
114 all_entries: Vec<Entry<T>>,
115 search_results: Vec<SearchResult>,
116 selected: usize,
117 input_mode: InputMode,
118 needs_redraw: bool,
119
120 pattern: nucleo_matcher::pattern::Pattern,
121 matcher: nucleo_matcher::Matcher,
122 case_matching: nucleo_matcher::pattern::CaseMatching,
123}
124
125impl<T> Picker<T> {
126 pub fn load(
129 &mut self,
130 configuration: &std::collections::BTreeMap<String, String>,
131 ) {
132 subscribe(PICKER_EVENTS);
133
134 match configuration
135 .get("nucleo_case_matching")
136 .map(|s| s.as_ref())
137 {
138 Some("respect") => {
139 self.case_matching =
140 nucleo_matcher::pattern::CaseMatching::Respect
141 }
142 Some("ignore") => {
143 self.case_matching =
144 nucleo_matcher::pattern::CaseMatching::Ignore
145 }
146 Some("smart") => {
147 self.case_matching =
148 nucleo_matcher::pattern::CaseMatching::Smart
149 }
150 Some(s) => {
151 panic!("unrecognized value {s} for option 'nucleo_case_matching': expected 'respect', 'ignore', 'smart'");
152 }
153 None => {}
154 }
155
156 match configuration.get("nucleo_match_paths").map(|s| s.as_ref()) {
157 Some("true") => {
158 self.set_match_paths();
159 }
160 Some("false") => {
161 self.clear_match_paths();
162 }
163 Some(s) => {
164 panic!("unrecognized value {s} for option 'nucleo_match_paths': expected 'true', 'false'");
165 }
166 None => {}
167 }
168
169 match configuration
170 .get("nucleo_start_in_search_mode")
171 .map(|s| s.as_ref())
172 {
173 Some("true") => {
174 self.enter_search_mode();
175 }
176 Some("false") => {
177 self.enter_normal_mode();
178 }
179 Some(s) => {
180 panic!("unrecognized value {s} for option 'nucleo_start_in_search_mode': expected 'true', 'false'");
181 }
182 None => {}
183 }
184 }
185
186 pub fn update(&mut self, event: &Event) -> Option<Response> {
194 match event {
195 Event::Key(key) => self.handle_key(key),
196 _ => None,
197 }
198 }
199
200 pub fn render(&mut self, rows: usize, cols: usize) {
203 if rows == 0 {
204 return;
205 }
206
207 let visible_entry_count = rows - 1;
208 let visible_entries_start =
209 (self.selected / visible_entry_count) * visible_entry_count;
210 let visible_selected = self.selected % visible_entry_count;
211
212 print!(" ");
213 if self.input_mode == InputMode::Normal && self.query.is_empty() {
214 print!(
215 "{}",
216 "(press / to search)".fg::<owo_colors::colors::BrightBlack>()
217 );
218 } else {
219 print!("{}", self.query);
220 if self.input_mode == InputMode::Search {
221 print!("{}", " ".bg::<owo_colors::colors::Green>());
222 }
223 }
224 println!();
225
226 let lines: Vec<_> = self
227 .search_results
228 .iter()
229 .skip(visible_entries_start)
230 .take(visible_entry_count)
231 .enumerate()
232 .map(|(i, search_result)| {
233 let mut line = String::new();
234
235 if i == visible_selected {
236 write!(
237 &mut line,
238 "{} ",
239 ">".fg::<owo_colors::colors::Yellow>()
240 )
241 .unwrap();
242 } else {
243 write!(&mut line, " ").unwrap();
244 }
245
246 let mut current_col = 2;
247 for (char_idx, c) in self.all_entries[search_result.entry]
248 .string
249 .chars()
250 .enumerate()
251 {
252 let width = c.width().unwrap_or(0);
253 if current_col + width > cols - 6 {
254 write!(
255 &mut line,
256 "{}",
257 " [...]".fg::<owo_colors::colors::BrightBlack>()
258 )
259 .unwrap();
260 break;
261 }
262
263 if search_result
264 .indices
265 .contains(&u32::try_from(char_idx).unwrap())
266 {
267 write!(
268 &mut line,
269 "{}",
270 c.fg::<owo_colors::colors::Cyan>()
271 )
272 .unwrap();
273 } else if i == visible_selected {
274 write!(
275 &mut line,
276 "{}",
277 c.fg::<owo_colors::colors::Yellow>()
278 )
279 .unwrap();
280 } else {
281 write!(&mut line, "{}", c).unwrap();
282 }
283
284 current_col += width;
285 }
286 line
287 })
288 .collect();
289
290 print!("{}", lines.join("\n"));
291
292 self.needs_redraw = false;
293 }
294
295 pub fn needs_redraw(&self) -> bool {
299 self.needs_redraw
300 }
301
302 pub fn entries(&self) -> &[Entry<T>] {
304 &self.all_entries
305 }
306
307 pub fn select(&mut self, idx: usize) {
309 self.selected = idx;
310 self.needs_redraw = true;
311 }
312
313 pub fn clear(&mut self) {
315 self.all_entries.clear();
316 self.search();
317 }
318
319 pub fn extend(&mut self, iter: impl IntoIterator<Item = Entry<T>>) {
321 let prev_selected =
322 self.search_results.get(self.selected).map(|search_result| {
323 self.all_entries[search_result.entry].string.clone()
324 });
325
326 self.all_entries.extend(iter);
327 self.search();
328
329 if let Some(prev_selected) = prev_selected {
330 self.selected = self
331 .search_results
332 .iter()
333 .enumerate()
334 .find_map(|(idx, search_result)| {
335 (self.all_entries[search_result.entry].string
336 == prev_selected)
337 .then_some(idx)
338 })
339 .unwrap_or(0);
340 } else {
341 self.selected = 0;
342 }
343 }
344
345 pub fn use_case_matching_respect(&mut self) {
347 self.case_matching = nucleo_matcher::pattern::CaseMatching::Respect;
348 }
349
350 pub fn use_case_matching_ignore(&mut self) {
352 self.case_matching = nucleo_matcher::pattern::CaseMatching::Ignore;
353 }
354
355 pub fn use_case_matching_smart(&mut self) {
359 self.case_matching = nucleo_matcher::pattern::CaseMatching::Smart;
360 }
361
362 pub fn enter_search_mode(&mut self) {
365 self.input_mode = InputMode::Search;
366 }
367
368 pub fn enter_normal_mode(&mut self) {
371 self.input_mode = InputMode::Normal;
372 }
373
374 pub fn set_match_paths(&mut self) {
377 self.matcher.config.set_match_paths();
378 }
379
380 pub fn clear_match_paths(&mut self) {
383 self.matcher.config = nucleo_matcher::Config::DEFAULT;
384 }
385
386 fn search(&mut self) {
387 self.pattern.reparse(
388 &self.query,
389 self.case_matching,
390 nucleo_matcher::pattern::Normalization::Smart,
391 );
392 let mut haystack = vec![];
393 self.search_results = self
394 .all_entries
395 .iter()
396 .enumerate()
397 .filter_map(|(i, entry)| {
398 let haystack = nucleo_matcher::Utf32Str::new(
399 &entry.string,
400 &mut haystack,
401 );
402 let mut indices = vec![];
403 self.pattern
404 .indices(haystack, &mut self.matcher, &mut indices)
405 .map(|score| SearchResult {
406 entry: i,
407 score,
408 indices,
409 })
410 })
411 .collect();
412 self.search_results.sort_by_key(|search_result| {
413 SearchResultWithString {
414 score: search_result.score,
415 first_index: search_result.indices.first().copied(),
416 string: &self.all_entries[search_result.entry].string,
417 }
418 });
419
420 self.needs_redraw = true;
421 }
422
423 fn handle_key(&mut self, key: &KeyWithModifier) -> Option<Response> {
424 self.handle_global_key(key)
425 .or_else(|| match self.input_mode {
426 InputMode::Normal => self.handle_normal_key(key),
427 InputMode::Search => self.handle_search_key(key),
428 })
429 }
430
431 fn handle_normal_key(
432 &mut self,
433 key: &KeyWithModifier,
434 ) -> Option<Response> {
435 match key.bare_key {
436 BareKey::Char('j') if key.has_no_modifiers() => {
437 self.down();
438 }
439 BareKey::Char('k') if key.has_no_modifiers() => {
440 self.up();
441 }
442 BareKey::Char(c @ '1'..='8') if key.has_no_modifiers() => {
443 let position =
444 usize::try_from(c.to_digit(10).unwrap() - 1).unwrap();
445 return self.search_results.get(position).map(
446 |search_result| Response::Select(search_result.entry),
447 );
448 }
449 BareKey::Char('9') if key.has_no_modifiers() => {
450 return self.search_results.last().map(|search_result| {
451 Response::Select(search_result.entry)
452 })
453 }
454 BareKey::Char('/') if key.has_no_modifiers() => {
455 self.input_mode = InputMode::Search;
456 self.needs_redraw = true;
457 }
458 _ => {}
459 }
460
461 None
462 }
463
464 fn handle_search_key(
465 &mut self,
466 key: &KeyWithModifier,
467 ) -> Option<Response> {
468 match key.bare_key {
469 BareKey::Char(c) if key.has_no_modifiers() => {
470 self.query.push(c);
471 self.search();
472 self.selected = 0;
473 }
474 BareKey::Char('u') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
475 self.query.clear();
476 self.search();
477 self.selected = 0;
478 }
479 BareKey::Backspace if key.has_no_modifiers() => {
480 self.query.pop();
481 self.search();
482 self.selected = 0;
483 }
484 _ => {}
485 }
486
487 None
488 }
489
490 fn handle_global_key(
491 &mut self,
492 key: &KeyWithModifier,
493 ) -> Option<Response> {
494 match key.bare_key {
495 BareKey::Tab if key.has_no_modifiers() => {
496 self.down();
497 }
498 BareKey::Down if key.has_no_modifiers() => {
499 self.down();
500 }
501 BareKey::Tab if key.has_modifiers(&[KeyModifier::Shift]) => {
502 self.up();
503 }
504 BareKey::Up if key.has_no_modifiers() => {
505 self.up();
506 }
507 BareKey::Esc if key.has_no_modifiers() => {
508 self.input_mode = InputMode::Normal;
509 self.needs_redraw = true;
510 }
511 BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
512 return Some(Response::Cancel);
513 }
514 BareKey::Enter if key.has_no_modifiers() => {
515 return Some(Response::Select(
516 self.search_results[self.selected].entry,
517 ));
518 }
519 _ => {}
520 }
521
522 None
523 }
524
525 fn down(&mut self) {
526 if self.search_results.is_empty() {
527 return;
528 }
529 self.selected = (self.search_results.len() + self.selected + 1)
530 % self.search_results.len();
531 self.needs_redraw = true;
532 }
533
534 fn up(&mut self) {
535 if self.search_results.is_empty() {
536 return;
537 }
538 self.selected = (self.search_results.len() + self.selected - 1)
539 % self.search_results.len();
540 self.needs_redraw = true;
541 }
542}
543
544#[derive(Debug)]
545struct SearchResult {
546 entry: usize,
547 score: u32,
548 indices: Vec<u32>,
549}
550
551#[derive(Debug)]
552struct SearchResultWithString<'a> {
553 score: u32,
554 first_index: Option<u32>,
555 string: &'a str,
556}
557
558impl Ord for SearchResultWithString<'_> {
559 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
560 self.score
561 .cmp(&other.score)
562 .reverse()
563 .then_with(|| self.first_index.cmp(&other.first_index))
564 .then_with(|| self.string.cmp(other.string))
565 }
566}
567
568impl PartialOrd for SearchResultWithString<'_> {
569 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
570 Some(self.cmp(other))
571 }
572}
573
574impl Eq for SearchResultWithString<'_> {}
575
576impl PartialEq for SearchResultWithString<'_> {
577 fn eq(&self, other: &Self) -> bool {
578 self.cmp(other) == std::cmp::Ordering::Equal
579 }
580}