Skip to main content

rich_interact/
overlay.rs

1//! Overlays: a command palette, a help overlay, a shortcut sheet and an
2//! action menu, all read from the keymap, and a wrapper that puts them over
3//! any component (0.0.14 workstream 2: #453, #473, #474, #475).
4//!
5//! Each overlay is a [`Component`] of its own, so it runs alone, sits in a
6//! [`Layer`] of your own [`Layers`], or goes anywhere else a component
7//! goes:
8//!
9//! - [`Palette`]: [`Command`]s by category, with their shortcuts as the
10//!   keymap has them now, the ones for contexts that are not active left
11//!   out, found by fuzzy search. It answers the command picked.
12//! - [`Help`]: every binding, grouped by context and searchable as you
13//!   type.
14//! - [`Shortcuts`]: every binding at a glance, in columns; any key closes
15//!   it.
16//! - [`Menu`]: the actions for one [`ActionTarget`], an item's or a
17//!   region's ([`TargetKind::Region`]), as Ctrl+K opens them.
18//!
19//! [`Overlays`] wires them into any host: it wraps a component, opens the
20//! palette on Ctrl+O, the help on F1 and the shortcuts on `?` or F2 when
21//! the component does not use the key, reads what they list from the
22//! component's [`keymap`](Component::keymap) at that moment, and runs what
23//! is picked: a binding's command by sending the component its key, a
24//! command of yours by calling its handler. It also carries a region's
25//! actions on Ctrl+K, and a [`StatusBar`] and [`Breadcrumbs`] round the
26//! component.
27//!
28//! ```
29//! use rich_interact::overlay::Overlays;
30//! use rich_interact::{headless, Outcome, Select};
31//!
32//! // Ctrl+O, then "down" finds "move down"; Enter runs it; Enter picks.
33//! let app = Overlays::new(Select::new("Fruit", ["apple", "banana"]));
34//! let script = headless::Script::new().keys("ctrl+o").text("move down").keys("enter enter");
35//! let (outcome, record) = headless::run(app, script, 60, 16);
36//! assert_eq!(outcome.unwrap(), Outcome::Done("banana"));
37//! assert!(record.frames.iter().any(|frame| frame.contains("Commands")));
38//! ```
39
40use 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
56/// The action that opens the [`Palette`] in [`Overlays`] (Ctrl+O).
57pub const PALETTE: &str = "palette";
58/// The action that opens the [`Help`] overlay (F1).
59pub const HELP: &str = "help";
60/// The action that opens the [`Shortcuts`] sheet (`?`, F2).
61pub const SHORTCUTS: &str = "shortcuts";
62/// The action that opens a region's [`Menu`] (Ctrl+K).
63pub const ACTIONS: &str = "actions";
64
65fn style(definition: &str) -> Style {
66    Style::parse(definition).expect("a built-in style")
67}
68
69/// A typed key: a character without Ctrl or Alt.
70fn 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// ---------------------------------------------------------------------------
78// Commands and the palette.
79
80/// Something the [`Palette`] offers: a binding of the keymap
81/// ([`from_binding`](Self::from_binding)) or a command of yours.
82#[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    /// What identifies it: `context.action` for a binding.
86    pub id: String,
87    /// What it does, for people.
88    pub label: String,
89    /// The group it is listed under: a binding's context.
90    pub category: String,
91    /// The keys that run it, shown as its shortcut; the first runs it
92    /// under [`Overlays`] when it has no handler.
93    pub keys: Vec<Key>,
94    /// The context it belongs to: listed only while that context is
95    /// active ([`Palette::contexts`]). `None`: always listed.
96    pub context: Option<String>,
97    /// Its keys are pressed one after another (`g g`), not alternatives.
98    pub sequence: bool,
99    /// Why it cannot run now: listed greyed with the reason, and Enter or
100    /// a click on it does nothing.
101    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    /// Its shortcut, when the keymap does not say (see
123    /// [`Palette::hints`]).
124    pub fn keys(mut self, keys: impl IntoIterator<Item = Key>) -> Self {
125        self.keys = keys.into_iter().collect();
126        self
127    }
128
129    /// List it only while `context` is active.
130    pub fn context(mut self, context: impl Into<String>) -> Self {
131        self.context = Some(context.into());
132        self
133    }
134
135    /// List it greyed, saying `reason`, and do not run it.
136    pub fn unavailable(mut self, reason: impl Into<String>) -> Self {
137        self.unavailable = Some(reason.into());
138        self
139    }
140
141    /// The command a binding makes: its description, under its context,
142    /// with its keys.
143    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    /// The keys as a hint shows them: `↑/ctrl+p`, or `g g` for a
156    /// sequence.
157    pub fn keys_label(&self) -> String {
158        crate::chrome::keys_hint_of(&self.keys, self.sequence)
159    }
160
161    /// What fuzzy search looks in: the category, then the label.
162    fn search_text(&self) -> String {
163        format!("{} {}", self.category, self.label)
164    }
165}
166
167/// The palette's keys, in context `palette`. Every other character types.
168pub 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/// A command palette (#453): [`Command`]s in categories, each with its
181/// shortcut, found by fuzzy search over category and label. Commands of a
182/// context that is not active are left out ([`contexts`](Self::contexts)).
183/// Enter answers the command focused; Escape cancels.
184///
185/// [`Overlays`] builds one from the host's keymap each time it opens, so
186/// what it lists is what the host can do then.
187#[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    /// An icon before a category's name, by category.
191    icons: Vec<(String, Vec<Segment>)>,
192    /// Indices into `commands` of those listed: in an active context.
193    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    /// A command for every binding of `keymap` that has keys.
222    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    /// Add a command.
233    /// Show `icon` (one line of text: a micro asset's placeholder, say)
234    /// before `category`'s name on each of its commands.
235    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    /// A category's width: its icon and a space, then its name.
250    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    /// Add commands.
261    pub fn commands(mut self, commands: impl IntoIterator<Item = Command>) -> Self {
262        self.commands.extend(commands);
263        self.relist();
264        self
265    }
266
267    /// Only these contexts are active: a command in another is left out.
268    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    /// Read each command's shortcut from `keymap`: a command whose id is a
279    /// binding's `context.action` shows the keys that do it now, rebinding
280    /// included.
281    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    /// The prompt before the search (default `Command`).
292    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    /// Search for `query` from the start.
303    pub fn query(mut self, query: impl Into<String>) -> Self {
304        self.filter.set_query(query);
305        self.reset();
306        self
307    }
308
309    /// Make `keys` do `action` (see [`palette_keymap`]) on this palette.
310    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
311        self.keymap.rebind(action, keys);
312        self
313    }
314
315    /// Put `commands` in place of those it has, as they change while it is
316    /// open: the search stays, and so does the focus, on the command of
317    /// the same id while it is still listed (else on the one at the same
318    /// place, or the last).
319    ///
320    /// ```
321    /// use rich_interact::overlay::{Command, Palette};
322    ///
323    /// let mut palette = Palette::new([Command::new("a", "Alpha"), Command::new("b", "Beta")]);
324    /// palette.focus_id("b");
325    /// palette.set_commands([Command::new("b", "Beta"), Command::new("c", "Gamma"), Command::new("a", "Alpha")]);
326    /// assert_eq!(palette.focused().unwrap().id, "b");
327    /// ```
328    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        // The search is kept: the candidates are matched against it again.
334        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    /// Focus the command `id`, if it is listed; whether it is.
345    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    /// Where the command `id` is among those matching.
356    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    /// The commands listed now, best match first.
364    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    /// The command focused, if any matches.
373    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    /// The search.
380    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        // Positions count characters of `category label`.
420        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                // A long label is cut, so its keys still show.
464                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                // One that is unavailable stays listed, and does not run.
521                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        // The search, the commands, and a line of hints.
551        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// ---------------------------------------------------------------------------
590// Help.
591
592/// A searchable help overlay (#473): every binding of a keymap, grouped by
593/// context, with its keys and what it does. Typing searches keys,
594/// descriptions and contexts; the arrows and PageUp/PageDown scroll;
595/// Escape clears the search, then closes; Enter closes.
596///
597/// Options: [`wrap`](Help::wrap) wraps a row too long for the width (its
598/// keys and its description) rather than cutting it,
599/// [`key_format`](Help::key_format) says the keys another way (`<C-u>`),
600/// [`flat`](Help::flat) lists every binding in one list under the search
601/// line and a rule, with no context headings, and
602/// [`filter_key`](Help::filter_key) makes letters move until a key starts
603/// the search (Yazi's `/`).
604#[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    /// The key that starts the search; until it, letters move.
616    filter_key: Option<Key>,
617    /// The search has the keys (always, with no `filter_key`).
618    searching: bool,
619    /// The width last drawn at, which wrapped rows are counted for.
620    width: Cell<usize>,
621}
622
623/// How a [`Help`] says a binding's keys.
624type KeyFormat = Rc<dyn Fn(&Binding) -> String>;
625
626impl Help {
627    /// Help for every binding of `keymap` that has keys.
628    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    /// Move with the letters until `key` is pressed, and only then search:
656    /// `j`/`k` move a row, `g` and `G` go to the top and the bottom, and
657    /// `key` (Yazi's `/`) starts the search, which Escape ends (clearing
658    /// it). `None`, the default, searches as soon as a letter is typed.
659    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    /// Wrap a row too long for the width onto the next rows, under its
676    /// description, rather than cut it. Off by default.
677    pub fn wrap(mut self, on: bool) -> Self {
678        self.wrap = on;
679        self.refilter();
680        self
681    }
682
683    /// Every binding in one list, under the search line and a rule, with no
684    /// heading for each context (Yazi's help). Off by default.
685    pub fn flat(mut self, on: bool) -> Self {
686        self.flat = on;
687        self.refilter();
688        self
689    }
690
691    /// Say each binding's keys with `format` (vim's `<C-u>`, say) rather
692    /// than as [`Binding::hint`] does; searching finds them as said.
693    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    /// Search for `query` from the start.
700    pub fn query(mut self, query: impl Into<String>) -> Self {
701        self.filter.set_query(query);
702        self.refilter();
703        self
704    }
705
706    /// Put `bindings` in place of those it has, as they change while it is
707    /// open: the search stays, and so does the scroll, as far as the new
708    /// rows reach.
709    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    /// The bindings that match the search, in keymap order.
720    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    /// The keys of `binding`, as this help says them.
727    fn hint_of(&self, binding: &Binding) -> String {
728        match &self.format {
729            Some(format) => format(binding),
730            None => binding.hint(),
731        }
732    }
733
734    /// What the search looks in: each binding's keys, as said and as
735    /// named, its description and its context.
736    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    /// The body's rows at `width` (0: not known yet, nothing wraps): a
754    /// heading per context, then its bindings; or, flat, the bindings.
755    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            // Keys wrap too: they take at most half the width.
767            key_width = key_width.min((width.saturating_sub(4) / 2).max(4));
768        }
769        // Where a description starts, and how wide it may be.
770        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                // One unavailable now says why, greyed.
795                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                        // Keys alone on their row need no padding.
820                        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
844/// A help row's keys in lines of `width` cells at most, broken after a
845/// key's separator (`/`, `, `, a space), and within a key only where one
846/// is wider than a line.
847fn wrap_keys(keys: &str, width: usize) -> Vec<String> {
848    if cell_len(keys) <= width || width == 0 {
849        return vec![keys.to_string()];
850    }
851    // The keys, each with the separator after it.
852    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            // One key wider than a line: broken where it must be.
883            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
899/// `text` in lines of `width` cells at most, broken between words (a word
900/// longer than a line is broken where it must).
901fn 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            // A word wider than a line: as much as fits, then the rest.
924            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        // As many rows as the last width wrapped them into.
953        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            // Letters move until the filter key starts the search.
972            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                // Other letters do nothing until the search starts.
983                _ => 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                // Back to moving, the search cleared.
992                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        // Flat, a rule under the search line takes a row.
1044        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        // Wrapped rows are as many as this width makes them.
1057        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
1100// ---------------------------------------------------------------------------
1101// Shortcuts.
1102
1103/// The key a shortcut list shows for `binding`: its first, or a whole
1104/// sequence (`g g`).
1105fn shortcut_hint(binding: &Binding) -> String {
1106    if binding.sequence {
1107        binding.hint()
1108    } else {
1109        key_hint(binding.keys[0])
1110    }
1111}
1112/// A shortcut sheet (#475): every binding of a keymap at a glance, its
1113/// first key and what it does, in as many columns as fit. Any key or a
1114/// click closes it.
1115pub 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    /// How many columns fit in `width`, and the rows they take.
1158    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    /// The size of a box that shows it all, border included, in at most
1165    /// `width` × `height` cells.
1166    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// ---------------------------------------------------------------------------
1229// Menu.
1230
1231/// The actions for one [`ActionTarget`], as a component (#474): an
1232/// [`ActionMenu`] to show in a modal. The arrows move, Enter or an
1233/// action's own key runs one (the menu answers it), Escape or the key that
1234/// opened it cancels. [`Select`](crate::Select) opens one over the focused
1235/// item; [`Overlays`] opens one for a region.
1236#[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    /// The key that opened it, which closes it again (default Ctrl+K).
1253    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    /// The size of a box that shows it all, border included.
1272    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
1309// ---------------------------------------------------------------------------
1310// Overlays.
1311
1312/// The keys [`Overlays`] opens its overlays with, in context `overlays`.
1313pub 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
1320/// What an overlay picked, for the host to run.
1321enum 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/// Overlays and chrome for any component: wrap it, and it gets a command
1330/// palette (Ctrl+O), a help overlay (F1) and a shortcut sheet (`?` or F2),
1331/// each read from its keymap when opened, a region's action menu (Ctrl+K)
1332/// when given [`actions`](Self::actions), and a [`StatusBar`] under it and
1333/// [`Breadcrumbs`] over it when given them.
1334///
1335/// The keys reach the component first, and open an overlay only when it
1336/// does not use them ([`Flow::Ignored`]), so `?` still types in a search.
1337/// They are rebound with [`rebind`](Self::rebind) or, for the whole
1338/// process, under context `overlays` ([`keymap::install`](crate::keymap::install)).
1339///
1340/// A command picked in the palette runs: one of yours through its handler,
1341/// a binding by sending the component the binding's first key, exactly as
1342/// if it had been pressed. The overlays are modal [`Layer`]s of a
1343/// [`Layers`] host round the component ([`layers`](Self::layers)), so
1344/// layers of your own open there too.
1345#[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    /// Make `keys` open overlay `action` ([`PALETTE`], [`HELP`],
1382    /// [`SHORTCUTS`], [`ACTIONS`]); no keys turns it off.
1383    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
1384        self.keymap.rebind(action, keys);
1385        self
1386    }
1387
1388    /// Show `icon` before `category` in the palette: see
1389    /// [`Palette::category_icon`].
1390    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    /// Offer `command` in the palette; picking it runs `handler`.
1398    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    /// Name the component as a region, `label` shown and `value` given to
1407    /// actions ([`TargetKind::Region`]), for [`actions`](Self::actions).
1408    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    /// Actions on the region (#474), opened in a modal [`Menu`] on Ctrl+K
1419    /// when the component does not use the key: when the item focused
1420    /// has no actions of its own, or there is no item. A region with no
1421    /// name ([`region`](Self::region)) is called `region`.
1422    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    /// What running a region's action does (default: record it, see
1435    /// [`action`](Self::action), and carry on).
1436    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    /// A status bar under the component; its [`Hints`](crate::chrome::StatusItem::Hints)
1445    /// read the keymap of what has the keys: the component, or the
1446    /// overlay open over it.
1447    pub fn status_bar(mut self, bar: StatusBar<M>) -> Self {
1448        self.status = Some(bar);
1449        self
1450    }
1451
1452    /// Breadcrumbs over the component.
1453    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    /// The overlays' own bindings.
1464    pub fn own_keymap(&self) -> &Keymap {
1465        &self.keymap
1466    }
1467
1468    /// The layer host round the component.
1469    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    /// A handle that opens layers of your own over the component.
1478    pub fn layer_handle(&self) -> LayerHandle<'a, M> {
1479        self.layers.handle()
1480    }
1481
1482    /// The component wrapped.
1483    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    /// The id of the region action run last, if one was.
1492    pub fn action(&self) -> Option<&str> {
1493        self.ran.as_ref().map(|(id, _)| id.as_str())
1494    }
1495
1496    /// The region's target, as actions see it.
1497    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    /// What the overlays list: the component's bindings, then the
1507    /// overlays' own.
1508    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    /// The palette as it would open now: the host's bindings and your
1515    /// commands, in the contexts active now, with shortcuts from the
1516    /// keymap.
1517    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        // Your commands first: they are what the host is for.
1530        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    /// The area the component and its layers take: all but the chrome.
1542    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    /// Run what an overlay picked.
1606    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    /// The overlay `key` opens, if any: `?` arrives with Shift from some
1634    /// terminals, and counts without it.
1635    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                    // Repainted with the spinner's next frame.
1686                    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    /// The rows of `help` at `width`, as plain text.
1815    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        // `u` searches nothing; `j` moves down a row.
1888        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        // Escape ends the search, then closes.
1896        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        // Found as said.
1913        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        // Categories line up: the other one is padded past the icon.
1940        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}