1use std::cell::{Cell, RefCell};
41use std::rc::Rc;
42use std::time::Duration;
43
44use rich::cells::cell_len;
45use rich::{Segment, Style};
46
47use crate::chrome::{Breadcrumbs, StatusBar, key_hint};
48use crate::component::{Component, Context, Flow, View};
49use crate::compose::{ComponentExt, Layer, LayerHandle, Layers, Rect};
50use crate::event::{Event, Key, KeyCode, MouseKind};
51use crate::item::{Action, ActionTarget, Actions, TargetKind};
52use crate::keymap::{Binding, Keymap, keys};
53use crate::kit::{self, ActionMenu, FilterState, ListState, MenuReply, ScrollState, Theme};
54use crate::policy::{LineIo, NotInteractive};
55
56pub const PALETTE: &str = "palette";
58pub const HELP: &str = "help";
60pub const SHORTCUTS: &str = "shortcuts";
62pub const ACTIONS: &str = "actions";
64
65fn style(definition: &str) -> Style {
66 Style::parse(definition).expect("a built-in style")
67}
68
69fn typed(key: Key) -> Option<char> {
71 match key.code {
72 KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => Some(c),
73 _ => None,
74 }
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
83#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
84pub struct Command {
85 pub id: String,
87 pub label: String,
89 pub category: String,
91 pub keys: Vec<Key>,
94 pub context: Option<String>,
97 pub sequence: bool,
99 pub unavailable: Option<String>,
102}
103
104impl Command {
105 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Command {
106 Command {
107 id: id.into(),
108 label: label.into(),
109 category: String::new(),
110 keys: Vec::new(),
111 context: None,
112 sequence: false,
113 unavailable: None,
114 }
115 }
116
117 pub fn category(mut self, category: impl Into<String>) -> Self {
118 self.category = category.into();
119 self
120 }
121
122 pub fn keys(mut self, keys: impl IntoIterator<Item = Key>) -> Self {
125 self.keys = keys.into_iter().collect();
126 self
127 }
128
129 pub fn context(mut self, context: impl Into<String>) -> Self {
131 self.context = Some(context.into());
132 self
133 }
134
135 pub fn unavailable(mut self, reason: impl Into<String>) -> Self {
137 self.unavailable = Some(reason.into());
138 self
139 }
140
141 pub fn from_binding(binding: &Binding) -> Command {
144 Command {
145 id: binding.id(),
146 label: binding.description.clone(),
147 category: binding.context.clone(),
148 keys: binding.keys.clone(),
149 context: Some(binding.context.clone()),
150 sequence: binding.sequence,
151 unavailable: binding.unavailable.clone(),
152 }
153 }
154
155 pub fn keys_label(&self) -> String {
158 crate::chrome::keys_hint_of(&self.keys, self.sequence)
159 }
160
161 fn search_text(&self) -> String {
163 format!("{} {}", self.category, self.label)
164 }
165}
166
167pub fn palette_keymap() -> Keymap {
169 Keymap::new("palette")
170 .bind("up", keys("up ctrl+p"), "move up")
171 .bind("down", keys("down ctrl+n"), "move down")
172 .bind("page-up", keys("pageup"), "page up")
173 .bind("page-down", keys("pagedown"), "page down")
174 .bind("run", keys("enter"), "run the command")
175 .bind("close", keys("escape"), "close")
176 .bind("delete", keys("backspace"), "delete a character")
177 .bind("clear", keys("ctrl+u"), "clear the search")
178}
179
180#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
188pub struct Palette {
189 commands: Vec<Command>,
190 icons: Vec<(String, Vec<Segment>)>,
192 listed: Vec<usize>,
194 active: Option<Vec<String>>,
195 filter: FilterState,
196 list: ListState,
197 prompt: String,
198 theme: Theme,
199 keymap: Keymap,
200 page: Cell<usize>,
201}
202
203impl Palette {
204 pub fn new(commands: impl IntoIterator<Item = Command>) -> Palette {
205 let mut palette = Palette {
206 commands: commands.into_iter().collect(),
207 icons: Vec::new(),
208 listed: Vec::new(),
209 active: None,
210 filter: FilterState::default(),
211 list: ListState::new(),
212 prompt: "Command".to_string(),
213 theme: Theme::default(),
214 keymap: palette_keymap(),
215 page: Cell::new(10),
216 };
217 palette.relist();
218 palette
219 }
220
221 pub fn from_keymap(keymap: &Keymap) -> Palette {
223 Palette::new(
224 keymap
225 .bindings()
226 .iter()
227 .filter(|binding| !binding.keys.is_empty())
228 .map(Command::from_binding),
229 )
230 }
231
232 pub fn category_icon(mut self, category: impl Into<String>, icon: &rich::Text) -> Self {
236 let category = category.into();
237 self.icons.retain(|(known, _)| *known != category);
238 self.icons.push((category, kit::icon(icon)));
239 self
240 }
241
242 fn icon(&self, category: &str) -> Option<&[Segment]> {
243 self.icons
244 .iter()
245 .find(|(known, _)| known == category)
246 .map(|(_, icon)| icon.as_slice())
247 }
248
249 fn category_cells(&self, category: &str) -> usize {
251 cell_len(category) + self.icon(category).map_or(0, |icon| kit::width(icon) + 1)
252 }
253
254 pub fn command(mut self, command: Command) -> Self {
255 self.commands.push(command);
256 self.relist();
257 self
258 }
259
260 pub fn commands(mut self, commands: impl IntoIterator<Item = Command>) -> Self {
262 self.commands.extend(commands);
263 self.relist();
264 self
265 }
266
267 pub fn contexts<I, S>(mut self, contexts: I) -> Self
269 where
270 I: IntoIterator<Item = S>,
271 S: Into<String>,
272 {
273 self.active = Some(contexts.into_iter().map(Into::into).collect());
274 self.relist();
275 self
276 }
277
278 pub fn hints(mut self, keymap: &Keymap) -> Self {
282 let bindings = keymap.bindings();
283 for command in &mut self.commands {
284 if let Some(binding) = bindings.iter().find(|b| b.id() == command.id) {
285 command.keys = binding.keys.clone();
286 }
287 }
288 self
289 }
290
291 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
293 self.prompt = prompt.into();
294 self
295 }
296
297 pub fn theme(mut self, theme: Theme) -> Self {
298 self.theme = theme;
299 self
300 }
301
302 pub fn query(mut self, query: impl Into<String>) -> Self {
304 self.filter.set_query(query);
305 self.reset();
306 self
307 }
308
309 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
311 self.keymap.rebind(action, keys);
312 self
313 }
314
315 pub fn set_commands(&mut self, commands: impl IntoIterator<Item = Command>) {
329 let focused = self.focused().map(|command| command.id.clone());
330 let at = self.list.cursor();
331 let offset = self.list.offset();
332 self.commands = commands.into_iter().collect();
333 self.relist();
335 let found = focused.and_then(|id| self.position_of(&id));
336 let len = self.filter.len();
337 if len > 0 {
338 let to = found.unwrap_or(at.min(len - 1));
339 self.list.set_offset(offset.min(to));
340 self.list.select_row(to, self.page.get());
341 }
342 }
343
344 pub fn focus_id(&mut self, id: &str) -> bool {
346 match self.position_of(id) {
347 Some(at) => {
348 self.list.select_row(at, self.page.get());
349 true
350 }
351 None => false,
352 }
353 }
354
355 fn position_of(&self, id: &str) -> Option<usize> {
357 self.filter
358 .matches()
359 .iter()
360 .position(|(index, _)| self.commands[self.listed[*index]].id == id)
361 }
362
363 pub fn matches(&self) -> Vec<&Command> {
365 self.filter
366 .matches()
367 .iter()
368 .map(|(index, _)| &self.commands[self.listed[*index]])
369 .collect()
370 }
371
372 pub fn focused(&self) -> Option<&Command> {
374 self.filter
375 .index(self.list.cursor())
376 .map(|index| &self.commands[self.listed[index]])
377 }
378
379 pub fn filter(&self) -> &FilterState {
381 &self.filter
382 }
383
384 fn relist(&mut self) {
385 let active = self.active.as_ref();
386 self.listed = (0..self.commands.len())
387 .filter(|&index| match (&self.commands[index].context, active) {
388 (Some(context), Some(active)) => active.iter().any(|a| a == context),
389 _ => true,
390 })
391 .collect();
392 let candidates: Vec<String> = self
393 .listed
394 .iter()
395 .map(|&index| self.commands[index].search_text())
396 .collect();
397 self.filter.set_candidates(candidates);
398 self.reset();
399 }
400
401 fn reset(&mut self) {
402 self.list.set_len(self.filter.len());
403 self.list.reset(0, self.page.get());
404 }
405
406 fn row(&self, position: usize, category_width: usize, width: usize) -> Vec<Segment> {
407 let theme = &self.theme;
408 let (index, positions) = &self.filter.matches()[position];
409 let command = &self.commands[self.listed[*index]];
410 let focused = position == self.list.cursor();
411 let mut line = if focused {
412 vec![kit::text(
413 format!("{} ", theme.pointer),
414 &theme.pointer_style,
415 )]
416 } else {
417 vec![kit::plain(" ")]
418 };
419 let split = command.category.chars().count() + 1;
421 let (in_category, in_label): (Vec<usize>, Vec<usize>) =
422 positions.iter().partition(|&&p| p < split);
423 let in_label: Vec<usize> = in_label.iter().map(|p| p - split).collect();
424 let mut category = match self.icon(&command.category) {
425 Some(icon) => {
426 let mut cells = icon.to_vec();
427 cells.push(kit::plain(" "));
428 cells
429 }
430 None => Vec::new(),
431 };
432 category.extend(kit::highlight(
433 &command.category,
434 &in_category,
435 Some(&theme.hint),
436 &theme.matched,
437 ));
438 category.push(kit::plain(" "));
439 line.extend(kit::pad(category, category_width + 1));
440 let base = match (&command.unavailable, focused) {
441 (Some(_), true) => Some(theme.focused.combine(&theme.hint)),
442 (Some(_), false) => Some(theme.hint.clone()),
443 (None, true) => Some(theme.focused.clone()),
444 (None, false) => None,
445 };
446 line.extend(kit::highlight(
447 &command.label,
448 &in_label,
449 base.as_ref(),
450 &theme.matched,
451 ));
452 if let Some(reason) = command.unavailable.as_ref().filter(|r| !r.is_empty()) {
453 line.push(kit::text(format!(" ({reason})"), &theme.hint));
454 }
455 let shortcut = command.keys_label();
456 if !shortcut.is_empty() {
457 let used = kit::width(&line);
458 let wanted = cell_len(&shortcut) + 1;
459 if used + wanted < width {
460 line = kit::pad(line, width - wanted);
461 line.push(kit::text(format!("{shortcut} "), &theme.hint));
462 } else if width > wanted + 8 {
463 let room = width - wanted - 1;
465 line = kit::pad(line, room - 1);
466 line.push(kit::plain("… "));
467 line.push(kit::text(format!("{shortcut} "), &theme.hint));
468 }
469 }
470 kit::fit(line, width)
471 }
472
473 fn move_by(&mut self, delta: isize) {
474 self.list.step(delta, self.page.get());
475 }
476}
477
478impl Component for Palette {
479 type Output = Command;
480
481 fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<Command> {
482 let page = self.page.get();
483 match event {
484 Event::Paste(text) => {
485 self.filter.push_str(&kit::pasted(text, " "));
486 self.reset();
487 return Flow::Continue;
488 }
489 Event::Mouse(mouse) => {
490 match mouse.kind {
491 MouseKind::ScrollUp => self.move_by(-3),
492 MouseKind::ScrollDown => self.move_by(3),
493 _ if mouse.is_click() => {
494 let row = mouse.row as usize;
495 let position = self.list.offset() + row.wrapping_sub(1);
496 if row >= 1 && row <= page && position < self.filter.len() {
497 self.list.select_row(position, page);
498 if let Some(command) =
499 self.focused().filter(|c| c.unavailable.is_none())
500 {
501 return Flow::Done(command.clone());
502 }
503 }
504 }
505 _ => {}
506 }
507 return Flow::Continue;
508 }
509 _ => {}
510 }
511 let Some(key) = event.key() else {
512 return Flow::Ignored;
513 };
514 match self.keymap.action(key) {
515 Some("up") => self.move_by(-1),
516 Some("down") => self.move_by(1),
517 Some("page-up") => self.move_by(-(page as isize)),
518 Some("page-down") => self.move_by(page as isize),
519 Some("run") => {
520 return match self.focused().filter(|c| c.unavailable.is_none()) {
522 Some(command) => Flow::Done(command.clone()),
523 None => Flow::Continue,
524 };
525 }
526 Some("close") => return Flow::Cancel,
527 Some("delete") => {
528 if self.filter.pop() {
529 self.reset();
530 }
531 }
532 Some("clear") => {
533 self.filter.clear();
534 self.reset();
535 }
536 _ => match typed(key) {
537 Some(c) => {
538 self.filter.push(c);
539 self.reset();
540 }
541 None => return Flow::Ignored,
542 },
543 }
544 Flow::Continue
545 }
546
547 fn render(&self, context: &Context<'_>) -> View {
548 let width = context.width;
549 let theme = &self.theme;
550 let page = context.height.saturating_sub(2).max(1);
552 self.page.set(page);
553 let mut header = kit::question(theme, &self.prompt);
554 let column = kit::width(&header) + cell_len(self.filter.query());
555 header.push(kit::plain(self.filter.query().to_string()));
556 let mut lines = vec![kit::fit(header, width)];
557 let category_width = self
558 .listed
559 .iter()
560 .map(|&index| self.category_cells(&self.commands[index].category))
561 .max()
562 .unwrap_or(0)
563 .min(width / 3);
564 for position in self.list.visible(page) {
565 lines.push(self.row(position, category_width, width));
566 }
567 if self.filter.is_empty() {
568 lines.push(vec![kit::text(" no matching commands", &theme.hint)]);
569 }
570 lines.resize_with(page + 1, Vec::new);
571 let hint = format!(
572 " {}/{} · ↑↓ move · enter run · esc close",
573 self.filter.len(),
574 self.listed.len()
575 );
576 lines.push(kit::fit(vec![kit::text(hint, &theme.hint)], width));
577 View::new(lines).with_cursor(0, column.min(width.saturating_sub(1)))
578 }
579
580 fn mouse(&self) -> bool {
581 true
582 }
583
584 fn keymap(&self) -> Keymap {
585 self.keymap.clone()
586 }
587}
588
589#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
605pub struct Help {
606 bindings: Vec<Binding>,
607 filter: FilterState,
608 scroll: ScrollState,
609 prompt: String,
610 theme: Theme,
611 page: Cell<usize>,
612 wrap: bool,
613 flat: bool,
614 format: Option<KeyFormat>,
615 filter_key: Option<Key>,
617 searching: bool,
619 width: Cell<usize>,
621}
622
623type KeyFormat = Rc<dyn Fn(&Binding) -> String>;
625
626impl Help {
627 pub fn new(keymap: &Keymap) -> Help {
629 Help::from_bindings(keymap.bindings())
630 }
631
632 pub fn from_bindings(bindings: impl IntoIterator<Item = Binding>) -> Help {
633 let bindings: Vec<Binding> = bindings
634 .into_iter()
635 .filter(|binding| !binding.keys.is_empty())
636 .collect();
637 let mut help = Help {
638 bindings,
639 filter: FilterState::new(Vec::<String>::new()),
640 scroll: ScrollState::new(0),
641 prompt: "Search keys".to_string(),
642 theme: Theme::default(),
643 page: Cell::new(10),
644 wrap: false,
645 flat: false,
646 format: None,
647 filter_key: None,
648 searching: true,
649 width: Cell::new(0),
650 };
651 help.search_keys();
652 help
653 }
654
655 pub fn filter_key(mut self, key: Option<Key>) -> Self {
660 self.filter_key = key;
661 self.searching = key.is_none();
662 self
663 }
664
665 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
666 self.prompt = prompt.into();
667 self
668 }
669
670 pub fn theme(mut self, theme: Theme) -> Self {
671 self.theme = theme;
672 self
673 }
674
675 pub fn wrap(mut self, on: bool) -> Self {
678 self.wrap = on;
679 self.refilter();
680 self
681 }
682
683 pub fn flat(mut self, on: bool) -> Self {
686 self.flat = on;
687 self.refilter();
688 self
689 }
690
691 pub fn key_format(mut self, format: impl Fn(&Binding) -> String + 'static) -> Self {
694 self.format = Some(Rc::new(format));
695 self.search_keys();
696 self
697 }
698
699 pub fn query(mut self, query: impl Into<String>) -> Self {
701 self.filter.set_query(query);
702 self.refilter();
703 self
704 }
705
706 pub fn set_bindings(&mut self, bindings: impl IntoIterator<Item = Binding>) {
710 let offset = self.scroll.offset();
711 self.bindings = bindings
712 .into_iter()
713 .filter(|binding| !binding.keys.is_empty())
714 .collect();
715 self.search_keys();
716 self.scroll.scroll_to(offset, self.page.get());
717 }
718
719 pub fn matches(&self) -> Vec<&Binding> {
721 let mut indices: Vec<usize> = self.filter.matches().iter().map(|(i, _)| *i).collect();
722 indices.sort_unstable();
723 indices.iter().map(|&index| &self.bindings[index]).collect()
724 }
725
726 fn hint_of(&self, binding: &Binding) -> String {
728 match &self.format {
729 Some(format) => format(binding),
730 None => binding.hint(),
731 }
732 }
733
734 fn search_keys(&mut self) {
737 let candidates: Vec<String> = self
738 .bindings
739 .iter()
740 .map(|b| {
741 let hint = self.hint_of(b);
742 format!("{hint} {} {} {}", b.keys_label(), b.description, b.context)
743 })
744 .collect();
745 self.filter.set_candidates(candidates);
746 self.refilter();
747 }
748
749 fn refilter(&mut self) {
750 self.scroll = ScrollState::new(self.rows(false, self.width.get()).len());
751 }
752
753 fn rows(&self, styled: bool, width: usize) -> Vec<Vec<Segment>> {
756 let theme = &self.theme;
757 let matches = self.matches();
758 let hints: Vec<String> = matches.iter().map(|b| self.hint_of(b)).collect();
759 let mut key_width = hints
760 .iter()
761 .map(|hint| cell_len(hint))
762 .max()
763 .unwrap_or(0)
764 .min(24);
765 if self.wrap && width > 0 {
766 key_width = key_width.min((width.saturating_sub(4) / 2).max(4));
768 }
769 let indent = key_width + 4;
771 let room = width.saturating_sub(indent);
772 let wrapping = self.wrap && width > 0 && room >= 4;
773 let mut rows = Vec::new();
774 let mut contexts: Vec<&str> = Vec::new();
775 for binding in &matches {
776 if !contexts.contains(&binding.context.as_str()) {
777 contexts.push(&binding.context);
778 }
779 }
780 let groups: Vec<Option<&str>> = if self.flat {
781 vec![None]
782 } else {
783 contexts.into_iter().map(Some).collect()
784 };
785 for group in groups {
786 if let Some(context) = group {
787 rows.push(vec![kit::text(context.to_string(), &style("bold"))]);
788 }
789 let shown = matches
790 .iter()
791 .zip(&hints)
792 .filter(|(b, _)| group.is_none_or(|context| b.context == context));
793 for (binding, hint) in shown {
794 let description = match &binding.unavailable {
796 Some(reason) if !reason.is_empty() => {
797 format!("{} ({reason})", binding.description)
798 }
799 _ => binding.description.clone(),
800 };
801 let (keys, lines) = if wrapping {
802 (wrap_keys(hint, key_width), wrap_words(&description, room))
803 } else {
804 (vec![hint.clone()], vec![description])
805 };
806 let (key_style, text_style) = match binding.unavailable {
807 Some(_) => (theme.hint.clone(), Some(theme.hint.clone())),
808 None => (theme.prompt.clone(), None),
809 };
810 let count = keys.len().max(lines.len());
811 if !styled {
812 rows.extend((0..count).map(|_| Vec::new()));
813 continue;
814 }
815 for n in 0..count {
816 let mut line = vec![kit::plain(" ")];
817 let text = lines.get(n);
818 match keys.get(n) {
819 Some(keys) if text.is_none() => {
821 line.push(kit::text(keys.clone(), &key_style));
822 }
823 Some(keys) => line.extend(kit::pad(
824 vec![kit::text(keys.clone(), &key_style)],
825 key_width,
826 )),
827 None => line.push(kit::plain(" ".repeat(key_width))),
828 }
829 if let Some(text) = text {
830 line.push(kit::plain(" "));
831 line.push(match &text_style {
832 Some(style) => kit::text(text.clone(), style),
833 None => kit::plain(text.clone()),
834 });
835 }
836 rows.push(line);
837 }
838 }
839 }
840 rows
841 }
842}
843
844fn wrap_keys(keys: &str, width: usize) -> Vec<String> {
848 if cell_len(keys) <= width || width == 0 {
849 return vec![keys.to_string()];
850 }
851 let mut pieces: Vec<String> = Vec::new();
853 let mut piece = String::new();
854 let mut chars = keys.chars().peekable();
855 while let Some(c) = chars.next() {
856 piece.push(c);
857 let ends = match c {
858 '/' | ' ' => chars.peek().is_some_and(|next| *next != ' '),
859 ',' => {
860 chars.peek() == Some(&' ') && {
861 piece.push(chars.next().unwrap_or(' '));
862 true
863 }
864 }
865 _ => false,
866 };
867 if ends {
868 pieces.push(std::mem::take(&mut piece));
869 }
870 }
871 if !piece.is_empty() {
872 pieces.push(piece);
873 }
874 let mut lines: Vec<String> = Vec::new();
875 let mut line = String::new();
876 for piece in pieces {
877 if !line.is_empty() && cell_len(&line) + cell_len(piece.trim_end()) > width {
878 lines.push(line.trim_end().to_string());
879 line.clear();
880 }
881 if line.is_empty() && cell_len(piece.trim_end()) > width {
882 let mut broken = wrap_words(piece.trim_end(), width);
884 line = broken.pop().unwrap_or_default();
885 lines.extend(broken);
886 if piece.ends_with(' ') {
887 line.push(' ');
888 }
889 continue;
890 }
891 line.push_str(&piece);
892 }
893 if !line.trim_end().is_empty() {
894 lines.push(line.trim_end().to_string());
895 }
896 lines
897}
898
899fn wrap_words(text: &str, width: usize) -> Vec<String> {
902 let mut lines = Vec::new();
903 let mut line = String::new();
904 for word in text.split(' ') {
905 let mut word = word.to_string();
906 loop {
907 let needed = if line.is_empty() {
908 cell_len(&word)
909 } else {
910 cell_len(&line) + 1 + cell_len(&word)
911 };
912 if needed <= width {
913 if !line.is_empty() {
914 line.push(' ');
915 }
916 line.push_str(&word);
917 break;
918 }
919 if !line.is_empty() {
920 lines.push(std::mem::take(&mut line));
921 continue;
922 }
923 let mut head = String::new();
925 let mut rest = word.chars();
926 for c in rest.by_ref() {
927 if cell_len(&head) + cell_len(&c.to_string()) > width {
928 lines.push(std::mem::take(&mut head));
929 head.push(c);
930 break;
931 }
932 head.push(c);
933 }
934 word = head + rest.as_str();
935 if cell_len(&word) <= width {
936 line = word;
937 break;
938 }
939 }
940 }
941 if !line.is_empty() || lines.is_empty() {
942 lines.push(line);
943 }
944 lines
945}
946
947impl Component for Help {
948 type Output = ();
949
950 fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<()> {
951 let page = self.page.get();
952 let rows = self.rows(false, self.width.get()).len();
954 self.scroll.set_len(rows);
955 match event {
956 Event::Paste(text) => {
957 self.filter.push_str(&kit::pasted(text, " "));
958 self.refilter();
959 return Flow::Continue;
960 }
961 Event::Mouse(_) => {
962 self.scroll.handle(event, page);
963 return Flow::Continue;
964 }
965 _ => {}
966 }
967 let Some(key) = event.key() else {
968 return Flow::Ignored;
969 };
970 if !self.searching {
971 if Some(key) == self.filter_key {
973 self.searching = true;
974 return Flow::Continue;
975 }
976 let plain = !key.modifiers.ctrl && !key.modifiers.alt;
977 let moved = match key.code {
978 KeyCode::Char('j') if plain => self.scroll.scroll(1, page),
979 KeyCode::Char('k') if plain => self.scroll.scroll(-1, page),
980 KeyCode::Char('g') if plain => self.scroll.scroll_to(0, page),
981 KeyCode::Char('G') if plain => self.scroll.scroll_to(usize::MAX, page),
982 _ => typed(key).is_some(),
984 };
985 if moved || typed(key).is_some() {
986 return Flow::Continue;
987 }
988 }
989 match key.code {
990 KeyCode::Escape if self.filter_key.is_some() && self.searching => {
991 self.searching = false;
993 self.filter.clear();
994 self.refilter();
995 }
996 KeyCode::Escape if self.filter.is_filtering() => {
997 self.filter.clear();
998 self.refilter();
999 }
1000 KeyCode::Escape => return Flow::Cancel,
1001 KeyCode::Enter => return Flow::Done(()),
1002 KeyCode::Backspace => {
1003 if self.filter.pop() {
1004 self.refilter();
1005 }
1006 }
1007 KeyCode::Char('u') if key.modifiers.ctrl => {
1008 self.filter.clear();
1009 self.refilter();
1010 }
1011 KeyCode::Up => {
1012 self.scroll.scroll(-1, page);
1013 }
1014 KeyCode::Down => {
1015 self.scroll.scroll(1, page);
1016 }
1017 KeyCode::PageUp => {
1018 self.scroll.scroll(-(page as isize), page);
1019 }
1020 KeyCode::PageDown => {
1021 self.scroll.scroll(page as isize, page);
1022 }
1023 KeyCode::Home => {
1024 self.scroll.scroll_to(0, page);
1025 }
1026 KeyCode::End => {
1027 self.scroll.scroll_to(usize::MAX, page);
1028 }
1029 _ => match typed(key) {
1030 Some(c) => {
1031 self.filter.push(c);
1032 self.refilter();
1033 }
1034 None => return Flow::Ignored,
1035 },
1036 }
1037 Flow::Continue
1038 }
1039
1040 fn render(&self, context: &Context<'_>) -> View {
1041 let width = context.width;
1042 let theme = &self.theme;
1043 let top = if self.flat { 2 } else { 1 };
1045 let page = context.height.saturating_sub(top + 1).max(1);
1046 self.page.set(page);
1047 self.width.set(width);
1048 let mut header = kit::question(theme, &self.prompt);
1049 let column = kit::width(&header) + cell_len(self.filter.query());
1050 header.push(kit::plain(self.filter.query().to_string()));
1051 let mut lines = vec![kit::fit(header, width)];
1052 if self.flat {
1053 lines.push(vec![kit::text("─".repeat(width), &theme.hint)]);
1054 }
1055 let rows = self.rows(true, width);
1056 let mut scroll = ScrollState::new(rows.len());
1058 scroll.scroll_to(self.scroll.offset(), page);
1059 let shown = scroll.visible(page);
1060 let more = shown.end < rows.len();
1061 lines.extend(rows[shown].iter().map(|row| kit::fit(row.clone(), width)));
1062 if rows.is_empty() {
1063 lines.push(vec![kit::text(" no matching keys", &theme.hint)]);
1064 }
1065 lines.resize_with(page + top, Vec::new);
1066 let count = self.matches().len();
1067 let search = match (self.filter_key, self.searching) {
1068 (Some(key), false) => format!("{} search · j/k move", key_hint(key)),
1069 _ => "type to search".to_string(),
1070 };
1071 let hint = format!(
1072 " {count} {}{} · {search} · ↑↓ scroll · esc close",
1073 if count == 1 { "key" } else { "keys" },
1074 if more { " · more below" } else { "" }
1075 );
1076 lines.push(kit::fit(vec![kit::text(hint, &theme.hint)], width));
1077 let view = View::new(lines);
1078 if self.searching {
1079 view.with_cursor(0, column.min(width.saturating_sub(1)))
1080 } else {
1081 view
1082 }
1083 }
1084
1085 fn mouse(&self) -> bool {
1086 true
1087 }
1088
1089 fn keymap(&self) -> Keymap {
1090 Keymap::new("help")
1091 .bind("scroll-up", keys("up"), "scroll up")
1092 .bind("scroll-down", keys("down"), "scroll down")
1093 .bind("page-up", keys("pageup"), "page up")
1094 .bind("page-down", keys("pagedown"), "page down")
1095 .bind("close", keys("escape enter"), "close")
1096 .bind("clear", keys("ctrl+u"), "clear the search")
1097 }
1098}
1099
1100fn shortcut_hint(binding: &Binding) -> String {
1106 if binding.sequence {
1107 binding.hint()
1108 } else {
1109 key_hint(binding.keys[0])
1110 }
1111}
1112pub struct Shortcuts {
1116 bindings: Vec<Binding>,
1117 theme: Theme,
1118}
1119
1120impl Shortcuts {
1121 pub fn new(keymap: &Keymap) -> Shortcuts {
1122 Shortcuts::from_bindings(keymap.bindings())
1123 }
1124
1125 pub fn from_bindings(bindings: impl IntoIterator<Item = Binding>) -> Shortcuts {
1126 Shortcuts {
1127 bindings: bindings
1128 .into_iter()
1129 .filter(|binding| !binding.keys.is_empty())
1130 .collect(),
1131 theme: Theme::default(),
1132 }
1133 }
1134
1135 #[must_use]
1136 pub fn theme(mut self, theme: Theme) -> Self {
1137 self.theme = theme;
1138 self
1139 }
1140
1141 fn widths(&self) -> (usize, usize) {
1142 let key = self
1143 .bindings
1144 .iter()
1145 .map(|b| cell_len(&shortcut_hint(b)))
1146 .max()
1147 .unwrap_or(0);
1148 let entry = self
1149 .bindings
1150 .iter()
1151 .map(|b| cell_len(&b.description))
1152 .max()
1153 .unwrap_or(0);
1154 (key, (key + 2 + entry).min(36))
1155 }
1156
1157 fn grid(&self, width: usize) -> (usize, usize) {
1159 let (_, entry) = self.widths();
1160 let columns = ((width + 3) / (entry + 3)).min(self.bindings.len()).max(1);
1161 (columns, self.bindings.len().div_ceil(columns))
1162 }
1163
1164 pub fn size(&self, width: usize, height: usize) -> (usize, usize) {
1167 let inner = width.saturating_sub(4);
1168 let (columns, rows) = self.grid(inner);
1169 let (_, entry) = self.widths();
1170 let used = columns * (entry + 3) - 3;
1171 ((used + 4).min(width), (rows + 4).min(height))
1172 }
1173}
1174
1175impl Component for Shortcuts {
1176 type Output = ();
1177
1178 fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<()> {
1179 match event {
1180 Event::Key(_) => Flow::Done(()),
1181 Event::Mouse(mouse) if mouse.is_click() => Flow::Done(()),
1182 _ => Flow::Ignored,
1183 }
1184 }
1185
1186 fn render(&self, context: &Context<'_>) -> View {
1187 let theme = &self.theme;
1188 let width = context.width.saturating_sub(2);
1189 let (key_width, entry) = self.widths();
1190 let (columns, rows) = self.grid(width);
1191 let mut lines = Vec::with_capacity(rows + 2);
1192 for row in 0..rows {
1193 let mut line = vec![kit::plain(" ")];
1194 for column in 0..columns {
1195 let Some(binding) = self.bindings.get(column * rows + row) else {
1196 break;
1197 };
1198 if column > 0 {
1199 line.push(kit::plain(" "));
1200 }
1201 let mut cell = kit::pad(
1202 vec![kit::text(shortcut_hint(binding), &theme.prompt)],
1203 key_width,
1204 );
1205 cell.push(kit::plain(" "));
1206 cell.push(kit::text(binding.description.clone(), &Style::default()));
1207 line.extend(kit::pad(cell, entry));
1208 }
1209 lines.push(kit::fit(line, context.width));
1210 }
1211 lines.push(Vec::new());
1212 lines.push(kit::fit(
1213 vec![kit::text(" any key closes", &theme.hint)],
1214 context.width,
1215 ));
1216 View::new(lines)
1217 }
1218
1219 fn mouse(&self) -> bool {
1220 true
1221 }
1222
1223 fn keymap(&self) -> Keymap {
1224 Keymap::new("shortcuts").bind("close", keys("escape"), "close (any key)")
1225 }
1226}
1227
1228#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
1237pub struct Menu {
1238 menu: ActionMenu,
1239 target: ActionTarget,
1240 theme: Theme,
1241}
1242
1243impl Menu {
1244 pub fn new(actions: Vec<Action>, target: ActionTarget) -> Menu {
1245 Menu {
1246 menu: ActionMenu::new(actions, Key::ctrl('k')),
1247 target,
1248 theme: Theme::default(),
1249 }
1250 }
1251
1252 pub fn key(mut self, key: Key) -> Self {
1254 self.menu.key = key;
1255 self
1256 }
1257
1258 pub fn theme(mut self, theme: Theme) -> Self {
1259 self.theme = theme;
1260 self
1261 }
1262
1263 pub fn target(&self) -> &ActionTarget {
1264 &self.target
1265 }
1266
1267 pub fn menu(&self) -> &ActionMenu {
1268 &self.menu
1269 }
1270
1271 pub fn size(&self) -> (usize, usize) {
1273 let (width, rows) = self.menu.size();
1274 (width + 2, rows + 2)
1275 }
1276}
1277
1278impl Component for Menu {
1279 type Output = Action;
1280
1281 fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<Action> {
1282 if !matches!(event, Event::Key(_) | Event::Mouse(_)) {
1283 return Flow::Ignored;
1284 }
1285 match self.menu.handle(event, 0) {
1286 MenuReply::Stay => Flow::Continue,
1287 MenuReply::Close => Flow::Cancel,
1288 MenuReply::Run(action) => Flow::Done(action),
1289 }
1290 }
1291
1292 fn render(&self, context: &Context<'_>) -> View {
1293 View::new(self.menu.render(&self.theme, context.width))
1294 }
1295
1296 fn mouse(&self) -> bool {
1297 true
1298 }
1299
1300 fn keymap(&self) -> Keymap {
1301 Keymap::new("menu")
1302 .bind("up", keys("up"), "move up")
1303 .bind("down", keys("down tab"), "move down")
1304 .bind("run", keys("enter"), "run")
1305 .bind("close", keys("escape"), "close")
1306 }
1307}
1308
1309pub fn overlays_keymap() -> Keymap {
1314 Keymap::new("overlays")
1315 .bind(PALETTE, keys("ctrl+o"), "commands")
1316 .bind(HELP, keys("f1"), "help")
1317 .bind(SHORTCUTS, keys("? f2"), "shortcuts")
1318}
1319
1320enum Picked {
1322 Command(Command),
1323 Action(Action),
1324}
1325
1326type Handler<'a, M> = Box<dyn FnMut() -> Flow<M> + 'a>;
1327type ActionHandler<'a, M> = Box<dyn FnMut(&Action, &ActionTarget) -> Flow<M> + 'a>;
1328
1329#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
1346pub struct Overlays<'a, M> {
1347 layers: Layers<'a, M>,
1348 keymap: Keymap,
1349 commands: Vec<Command>,
1350 category_icons: Vec<(String, rich::Text)>,
1351 handlers: Vec<(String, Handler<'a, M>)>,
1352 picked: Rc<RefCell<Option<Picked>>>,
1353 region: Option<ActionTarget>,
1354 actions: Actions,
1355 on_action: Option<ActionHandler<'a, M>>,
1356 ran: Option<(String, ActionTarget)>,
1357 status: Option<StatusBar<M>>,
1358 crumbs: Option<Breadcrumbs<'a, M>>,
1359 theme: Theme,
1360}
1361
1362impl<'a, M: 'a> Overlays<'a, M> {
1363 pub fn new(component: impl Component<Output = M> + 'a) -> Overlays<'a, M> {
1364 Overlays {
1365 layers: Layers::new(component),
1366 keymap: overlays_keymap(),
1367 commands: Vec::new(),
1368 category_icons: Vec::new(),
1369 handlers: Vec::new(),
1370 picked: Rc::new(RefCell::new(None)),
1371 region: None,
1372 actions: Actions::new(),
1373 on_action: None,
1374 ran: None,
1375 status: None,
1376 crumbs: None,
1377 theme: Theme::default(),
1378 }
1379 }
1380
1381 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
1384 self.keymap.rebind(action, keys);
1385 self
1386 }
1387
1388 pub fn category_icon(mut self, category: impl Into<String>, icon: rich::Text) -> Self {
1391 let category = category.into();
1392 self.category_icons.retain(|(known, _)| *known != category);
1393 self.category_icons.push((category, icon));
1394 self
1395 }
1396
1397 pub fn command(mut self, command: Command, handler: impl FnMut() -> Flow<M> + 'a) -> Self {
1399 self.handlers.retain(|(id, _)| *id != command.id);
1400 self.handlers.push((command.id.clone(), Box::new(handler)));
1401 self.commands.retain(|known| known.id != command.id);
1402 self.commands.push(command);
1403 self
1404 }
1405
1406 pub fn region(mut self, label: impl Into<String>, value: impl Into<String>) -> Self {
1409 self.region = Some(ActionTarget {
1410 kind: TargetKind::Region,
1411 index: 0,
1412 label: label.into(),
1413 value: value.into(),
1414 });
1415 self
1416 }
1417
1418 pub fn actions(mut self, actions: Actions) -> Self {
1423 self.actions = actions;
1424 if !self.actions.is_empty() {
1425 self.keymap = std::mem::take(&mut self.keymap).bind(
1426 ACTIONS,
1427 keys("ctrl+k"),
1428 "actions for this region",
1429 );
1430 }
1431 self
1432 }
1433
1434 pub fn on_action(
1437 mut self,
1438 handler: impl FnMut(&Action, &ActionTarget) -> Flow<M> + 'a,
1439 ) -> Self {
1440 self.on_action = Some(Box::new(handler));
1441 self
1442 }
1443
1444 pub fn status_bar(mut self, bar: StatusBar<M>) -> Self {
1448 self.status = Some(bar);
1449 self
1450 }
1451
1452 pub fn breadcrumbs(mut self, crumbs: Breadcrumbs<'a, M>) -> Self {
1454 self.crumbs = Some(crumbs);
1455 self
1456 }
1457
1458 pub fn theme(mut self, theme: Theme) -> Self {
1459 self.theme = theme;
1460 self
1461 }
1462
1463 pub fn own_keymap(&self) -> &Keymap {
1465 &self.keymap
1466 }
1467
1468 pub fn layers(&self) -> &Layers<'a, M> {
1470 &self.layers
1471 }
1472
1473 pub fn layers_mut(&mut self) -> &mut Layers<'a, M> {
1474 &mut self.layers
1475 }
1476
1477 pub fn layer_handle(&self) -> LayerHandle<'a, M> {
1479 self.layers.handle()
1480 }
1481
1482 pub fn component(&self) -> &(dyn Component<Output = M> + 'a) {
1484 self.layers.base()
1485 }
1486
1487 pub fn status(&self) -> Option<&StatusBar<M>> {
1488 self.status.as_ref()
1489 }
1490
1491 pub fn action(&self) -> Option<&str> {
1493 self.ran.as_ref().map(|(id, _)| id.as_str())
1494 }
1495
1496 pub fn target(&self) -> ActionTarget {
1498 self.region.clone().unwrap_or(ActionTarget {
1499 kind: TargetKind::Region,
1500 index: 0,
1501 label: "region".to_string(),
1502 value: "region".to_string(),
1503 })
1504 }
1505
1506 pub fn host_keymap(&self) -> Keymap {
1509 let mut keymap = self.layers.base().keymap();
1510 keymap.extend(self.keymap.clone());
1511 keymap
1512 }
1513
1514 pub fn palette(&self) -> Palette {
1518 let keymap = self.host_keymap();
1519 let mut active: Vec<String> = Vec::new();
1520 for binding in keymap.bindings() {
1521 if !active.contains(&binding.context) {
1522 active.push(binding.context);
1523 }
1524 }
1525 let bindings = keymap
1526 .bindings()
1527 .into_iter()
1528 .filter(|b| !b.keys.is_empty() && b.id() != format!("overlays.{PALETTE}"));
1529 let mut palette = Palette::new(self.commands.iter().cloned())
1531 .commands(bindings.map(|b| Command::from_binding(&b)))
1532 .contexts(active)
1533 .hints(&keymap)
1534 .theme(self.theme.clone());
1535 for (category, icon) in &self.category_icons {
1536 palette = palette.category_icon(category.clone(), icon);
1537 }
1538 palette
1539 }
1540
1541 fn body(&self, context: &Context<'_>) -> Rect {
1543 let top = usize::from(self.crumbs.is_some());
1544 let chrome = top + usize::from(self.status.is_some());
1545 Rect::new(
1546 0,
1547 top,
1548 context.width,
1549 context.height.saturating_sub(chrome).max(1),
1550 )
1551 }
1552
1553 fn open(&mut self, action: &str, body: &Context<'_>) -> bool {
1554 let (width, height) = (body.width, body.height);
1555 let slot = Rc::clone(&self.picked);
1556 let layer = match action {
1557 PALETTE => {
1558 let palette = self.palette().map(move |command| {
1559 *slot.borrow_mut() = Some(Picked::Command(command));
1560 Flow::Continue
1561 });
1562 Layer::modal(palette)
1563 .title("Commands")
1564 .size(width.min(72), height.min(18))
1565 }
1566 HELP => {
1567 let help = Help::new(&self.host_keymap())
1568 .theme(self.theme.clone())
1569 .map(|()| Flow::Continue);
1570 Layer::modal(help)
1571 .title("Keys")
1572 .size(width.min(72), height.min(20))
1573 }
1574 SHORTCUTS => {
1575 let sheet = Shortcuts::new(&self.host_keymap()).theme(self.theme.clone());
1576 let (w, h) = sheet.size(width, height);
1577 Layer::modal(sheet.map(|()| Flow::Continue))
1578 .title("Shortcuts")
1579 .size(w, h)
1580 }
1581 ACTIONS => {
1582 let target = self.target();
1583 let actions = self.actions.for_target(&target, &[]);
1584 if actions.is_empty() {
1585 return false;
1586 }
1587 let menu = Menu::new(actions, target.clone())
1588 .key(self.keymap.key(ACTIONS).unwrap_or(Key::ctrl('k')))
1589 .theme(self.theme.clone());
1590 let (w, h) = menu.size();
1591 let menu = menu.map(move |action| {
1592 *slot.borrow_mut() = Some(Picked::Action(action));
1593 Flow::Continue
1594 });
1595 Layer::modal(menu)
1596 .title(format!("Actions · {}", target.label))
1597 .size(w.min(width), h.min(height))
1598 }
1599 _ => return false,
1600 };
1601 self.layers.open(layer);
1602 true
1603 }
1604
1605 fn run(&mut self, picked: Picked, context: &Context<'_>) -> Flow<M> {
1607 match picked {
1608 Picked::Command(command) => {
1609 if let Some((_, handler)) =
1610 self.handlers.iter_mut().find(|(id, _)| *id == command.id)
1611 {
1612 return handler();
1613 }
1614 match command.keys.first() {
1615 Some(key) => match Component::handle(self, &Event::Key(*key), context) {
1616 Flow::Ignored => Flow::Continue,
1617 flow => flow,
1618 },
1619 None => Flow::Continue,
1620 }
1621 }
1622 Picked::Action(action) => {
1623 let target = self.target();
1624 self.ran = Some((action.id.clone(), target.clone()));
1625 match &mut self.on_action {
1626 Some(handler) => handler(&action, &target),
1627 None => Flow::Continue,
1628 }
1629 }
1630 }
1631 }
1632
1633 fn overlay_for(&self, key: Key) -> Option<String> {
1636 let action = self.keymap.action(key).or_else(|| match key.code {
1637 KeyCode::Char(c) if key.modifiers.shift && !c.is_alphabetic() => {
1638 let mut plain = key;
1639 plain.modifiers.shift = false;
1640 self.keymap.action(plain)
1641 }
1642 _ => None,
1643 })?;
1644 Some(action.to_string())
1645 }
1646
1647 fn status_keymap(&self) -> Keymap {
1648 self.keymap()
1649 }
1650}
1651
1652impl<'a, M: 'a> Component for Overlays<'a, M> {
1653 type Output = M;
1654
1655 fn start(&mut self, context: &Context<'_>) -> Flow<M> {
1656 let body = self.body(context);
1657 self.layers.start(&body.context(context))
1658 }
1659
1660 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<M> {
1661 let body = self.body(context);
1662 let inner = body.context(context);
1663 let flow = match event {
1664 Event::Mouse(mouse) => {
1665 let row = mouse.row as usize;
1666 if let Some(crumbs) = self.crumbs.as_mut().filter(|_| row == 0) {
1667 if !crumbs.mouse() {
1668 return Flow::Ignored;
1669 }
1670 let line = Rect::new(0, 0, context.width, 1);
1671 return crumbs.handle(event, &line.context(context));
1672 }
1673 if row < body.y || row >= body.y + body.height {
1674 return Flow::Ignored;
1675 }
1676 Component::handle(&mut self.layers, &Event::Mouse(body.local(*mouse)), &inner)
1677 }
1678 Event::Tick => {
1679 let flow = if self.layers.tick().is_some() {
1680 Component::handle(&mut self.layers, event, &inner)
1681 } else {
1682 Flow::Ignored
1683 };
1684 if self.status.as_ref().is_some_and(|s| s.tick().is_some()) {
1685 return match flow {
1687 Flow::Ignored => Flow::Continue,
1688 flow => flow,
1689 };
1690 }
1691 flow
1692 }
1693 _ => Component::handle(&mut self.layers, event, &inner),
1694 };
1695 let picked = self.picked.borrow_mut().take();
1696 if let Some(picked) = picked {
1697 return match flow {
1698 Flow::Continue | Flow::Ignored => self.run(picked, context),
1699 flow => flow,
1700 };
1701 }
1702 if !matches!(flow, Flow::Ignored) || self.layers.depth() > 0 {
1703 return flow;
1704 }
1705 let Some(action) = event.key().and_then(|key| self.overlay_for(key)) else {
1706 return flow;
1707 };
1708 if self.open(&action, &inner) {
1709 Flow::Continue
1710 } else {
1711 flow
1712 }
1713 }
1714
1715 fn render(&self, context: &Context<'_>) -> View {
1716 let body = self.body(context);
1717 let mut lines = Vec::new();
1718 if let Some(crumbs) = &self.crumbs {
1719 let line = Rect::new(0, 0, context.width, 1);
1720 lines.extend(
1721 crumbs
1722 .render(&line.context(context))
1723 .lines
1724 .into_iter()
1725 .take(1),
1726 );
1727 lines.resize_with(1, Vec::new);
1728 }
1729 let view = self.layers.render(&body.context(context));
1730 let cursor = view.cursor.map(|(row, column)| (row + body.y, column));
1731 lines.extend(view.lines.into_iter().take(body.height));
1732 if let Some(status) = &self.status {
1733 let line = Rect::new(0, 0, context.width, 1);
1734 lines.push(status.line(&line.context(context), Some(&self.status_keymap())));
1735 }
1736 View { lines, cursor }
1737 }
1738
1739 fn tick(&self) -> Option<Duration> {
1740 let status = self.status.as_ref().and_then(|status| status.tick());
1741 self.layers.tick().into_iter().chain(status).min()
1742 }
1743
1744 fn mouse(&self) -> bool {
1745 self.layers.mouse() || self.crumbs.as_ref().is_some_and(|crumbs| crumbs.mouse())
1746 }
1747
1748 fn keymap(&self) -> Keymap {
1749 let mut keymap = self.layers.keymap();
1750 if self.layers.depth() == 0 {
1751 keymap.extend(self.keymap.clone());
1752 }
1753 keymap
1754 }
1755
1756 fn focusable(&self) -> bool {
1757 self.layers.focusable()
1758 }
1759
1760 fn focus_step(&mut self, forward: bool) -> bool {
1761 self.layers.focus_step(forward)
1762 }
1763
1764 fn focus_enter(&mut self, forward: bool) -> bool {
1765 self.layers.focus_enter(forward)
1766 }
1767
1768 fn default_value(&self) -> Option<M> {
1769 self.layers.default_value()
1770 }
1771
1772 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<M>, NotInteractive> {
1773 self.layers.prompt(io)
1774 }
1775}
1776
1777#[cfg(test)]
1778mod tests {
1779 use super::*;
1780
1781 fn keymap() -> Keymap {
1782 Keymap::new("demo")
1783 .bind("save", keys("ctrl+s"), "save the file")
1784 .bind("quit", keys("q"), "quit")
1785 }
1786
1787 #[test]
1788 fn a_palette_filters_by_context_and_reads_hints_from_the_keymap() {
1789 let keymap = keymap().rebound("save", keys("ctrl+w"));
1790 let palette = Palette::from_keymap(&keymap)
1791 .command(Command::new("demo.save", "save").context("demo"))
1792 .command(Command::new("other", "elsewhere").context("elsewhere"))
1793 .command(Command::new("always", "always there"))
1794 .contexts(["demo"])
1795 .hints(&keymap);
1796 let labels: Vec<&str> = palette.matches().iter().map(|c| c.label.as_str()).collect();
1797 assert_eq!(labels, ["save the file", "quit", "save", "always there"]);
1798 assert_eq!(palette.matches()[2].keys_label(), "ctrl+w");
1799 let palette = palette.query("qt");
1800 assert_eq!(palette.focused().unwrap().id, "demo.quit");
1801 }
1802
1803 #[test]
1804 fn help_groups_and_searches_bindings() {
1805 let mut keymap = keymap();
1806 keymap.extend(Keymap::new("tabs").bind("next", keys("alt+right"), "next tab"));
1807 let help = Help::new(&keymap);
1808 assert_eq!(help.matches().len(), 3);
1809 let help = help.query("tab");
1810 let found: Vec<String> = help.matches().iter().map(|b| b.id()).collect();
1811 assert_eq!(found, ["tabs.next"]);
1812 }
1813
1814 fn help_rows(help: &Help, width: usize) -> Vec<String> {
1816 help.rows(true, width)
1817 .iter()
1818 .map(|row| row.iter().map(|s| s.text.as_str()).collect::<String>())
1819 .collect()
1820 }
1821
1822 #[test]
1823 fn help_wraps_a_long_row_under_its_description() {
1824 let keymap = Keymap::new("scope").bind(
1825 "up",
1826 keys("pageup"),
1827 "threshold up (shift ×10, ctrl ×5, alt ×0.2)",
1828 );
1829 let cut = help_rows(&Help::new(&keymap), 30);
1830 assert_eq!(cut.len(), 2, "{cut:?}");
1831 let rows = help_rows(&Help::new(&keymap).wrap(true), 30);
1832 assert_eq!(
1833 rows,
1834 [
1835 "scope",
1836 " pgup threshold up (shift",
1837 " ×10, ctrl ×5, alt",
1838 " ×0.2)",
1839 ]
1840 );
1841 assert_eq!(wrap_words("abcdefghij", 4), ["abcd", "efgh", "ij"]);
1842 assert_eq!(wrap_words("", 4), [""]);
1843 }
1844
1845 #[test]
1846 fn help_wraps_a_long_key_list_between_keys() {
1847 let keymap = Keymap::new("scope").bind(
1848 "more",
1849 keys("pageup shift+pageup ctrl+pageup alt+pageup"),
1850 "average more",
1851 );
1852 let rows = help_rows(&Help::new(&keymap).wrap(true), 40);
1853 assert_eq!(
1854 rows,
1855 [
1856 "scope",
1857 " pgup/shift+pgup/ average more",
1858 " ctrl+pgup/alt+pgup",
1859 ]
1860 );
1861 assert_eq!(wrap_keys("<Up>, k, <C-p>", 8), ["<Up>, k,", "<C-p>"]);
1862 assert_eq!(wrap_keys("abcdefghij", 4), ["abcd", "efgh", "ij"]);
1863 assert_eq!(wrap_keys("a/b", 8), ["a/b"]);
1864 }
1865
1866 #[test]
1867 fn a_filter_key_makes_letters_move_until_it_starts_the_search() {
1868 let console = rich::Console::new();
1869 let context = Context {
1870 console: &console,
1871 width: 40,
1872 height: 4,
1873 };
1874 let keymap = Keymap::new("demo")
1875 .bind("a", keys("a"), "first")
1876 .bind("b", keys("b"), "second")
1877 .bind("c", keys("c"), "third")
1878 .bind("d", keys("d"), "fourth");
1879 let mut help = Help::new(&keymap)
1880 .flat(true)
1881 .filter_key(Key::parse("slash"));
1882 help.render(&context);
1883 let press = |help: &mut Help, name: &str| {
1884 let key = Key::parse(name).unwrap();
1885 help.handle(&Event::Key(key), &context)
1886 };
1887 assert!(matches!(press(&mut help, "u"), Flow::Continue));
1889 assert_eq!(help.matches().len(), 4);
1890 press(&mut help, "j");
1891 assert_eq!(help.scroll.offset(), 1);
1892 press(&mut help, "slash");
1893 press(&mut help, "u");
1894 assert_eq!(help.matches().len(), 1);
1895 press(&mut help, "escape");
1897 assert_eq!(help.matches().len(), 4);
1898 assert!(matches!(press(&mut help, "escape"), Flow::Cancel));
1899 }
1900
1901 #[test]
1902 fn help_says_keys_its_own_way_and_lists_them_flat() {
1903 let mut keymap = Keymap::new("a").bind("x", keys("ctrl+u"), "clear");
1904 keymap.extend(Keymap::new("b").bind("y", keys("g"), "go"));
1905 let help = Help::new(&keymap)
1906 .key_format(|b| format!("<{}>", b.keys_label()))
1907 .flat(true);
1908 assert_eq!(
1909 help_rows(&help, 30),
1910 [" <ctrl+u> clear", " <g> go"]
1911 );
1912 assert_eq!(help.query("<g>").matches().len(), 1);
1914 }
1915
1916 #[test]
1917 fn palette_categories_show_their_icons() {
1918 let palette = Palette::new([
1919 Command::new("a", "deploy").category("ship"),
1920 Command::new("b", "open").category("file"),
1921 ])
1922 .category_icon("ship", &rich::Text::new("^^"));
1923 let console = rich::Console::new();
1924 let context = Context {
1925 console: &console,
1926 width: 40,
1927 height: 6,
1928 };
1929 let view = palette.render(&context);
1930 let rows: Vec<String> = view
1931 .lines
1932 .iter()
1933 .map(|line| line.iter().map(|s| s.text.as_str()).collect::<String>())
1934 .collect();
1935 assert!(
1936 rows.iter().any(|r| r.contains("^^ ship deploy")),
1937 "{rows:?}"
1938 );
1939 assert!(rows.iter().any(|r| r.contains("file open")), "{rows:?}");
1941 }
1942
1943 #[test]
1944 fn a_shortcut_sheet_packs_columns() {
1945 let sheet = Shortcuts::new(&keymap());
1946 assert_eq!(sheet.grid(80), (2, 1));
1947 assert_eq!(sheet.grid(10), (1, 2));
1948 }
1949}