Skip to main content

rig_tui/
keys.rs

1//! Key bindings: every key the view acts on, its own editing keys
2//! included, is a [`KeyBinding`] entity named `key:<keys>`, so a plugin
3//! binds a key or a sequence (`ctrl+x ctrl+e`) as the view does, and
4//! listings read them as data.
5//!
6//! The first binding of a key stays ([`AppKeyBindingsExt::add_key_binding`]
7//! warns on a conflict); `Disabled` on a binding, in `Plugin::finish`,
8//! frees its keys. [`KeyScope`] says where a binding applies and which
9//! shadows which. A pressed binding triggers [`KeyPressed`] on its entity,
10//! unless it has an [`InputAction`], which the view does instead. A
11//! [`TuiPanel`](crate::TuiPanel) takes the keys while it is the
12//! [`KeyFocus`]. For keys before any binding, filter
13//! [`PendingInput`](crate::PendingInput) in
14//! [`InputSystems::Filter`](crate::InputSystems::Filter).
15//!
16//! ```no_run
17//! use rig_harness::prelude::*;
18//! use rig_tui::{AppKeyBindingsExt, KeyBinding, KeyPress, run_command};
19//!
20//! fn build(app: &mut App) {
21//!     // Alt+K compacts the conversation of the shown agent.
22//!     app.add_key_binding(KeyBinding::new(KeyPress::alt('k'), "Compact"), run_command("compact"));
23//! }
24//! ```
25
26use std::fmt;
27use std::str::FromStr;
28
29use bevy_app::App;
30use bevy_ecs::entity_disabling::Disabled;
31use bevy_ecs::prelude::*;
32use bevy_ecs::query::Allow;
33use bevy_ecs::system::IntoObserverSystem;
34use bevy_log::warn;
35use bevy_reflect::prelude::*;
36use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
37use rig_harness::commands::RunCommand;
38
39use super::view::TuiView;
40
41/// One key with its modifiers, as the terminal reports it. A character
42/// carries its case instead of Shift (`A` is Shift+a); Shift+Tab is
43/// [`KeyCode::BackTab`].
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub struct KeyPress {
46    /// The key.
47    pub code: KeyCode,
48    /// Ctrl, Alt, Shift and Super held with it.
49    pub modifiers: KeyModifiers,
50}
51
52impl KeyPress {
53    /// `code` with `modifiers`, in the form the terminal reports it.
54    pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
55        let modifiers = match code {
56            KeyCode::Char(_) | KeyCode::BackTab => modifiers.difference(KeyModifiers::SHIFT),
57            _ => modifiers,
58        };
59        Self { code, modifiers }
60    }
61
62    /// `code` alone.
63    pub fn key(code: KeyCode) -> Self {
64        Self::new(code, KeyModifiers::NONE)
65    }
66
67    /// Ctrl+`character`.
68    pub fn ctrl(character: char) -> Self {
69        Self::new(KeyCode::Char(character), KeyModifiers::CONTROL)
70    }
71
72    /// Alt+`character`.
73    pub fn alt(character: char) -> Self {
74        Self::new(KeyCode::Char(character), KeyModifiers::ALT)
75    }
76
77    /// `code` with Shift.
78    pub fn shift(code: KeyCode) -> Self {
79        Self::new(code, KeyModifiers::SHIFT)
80    }
81
82    /// The press `event` reports.
83    pub fn of(event: &KeyEvent) -> Self {
84        Self::new(event.code, event.modifiers)
85    }
86
87    /// Whether it types its character into the input: a character with
88    /// neither Ctrl nor Alt.
89    pub fn typed(&self) -> Option<char> {
90        match self.code {
91            KeyCode::Char(character)
92                if !self
93                    .modifiers
94                    .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
95            {
96                Some(character)
97            }
98            _ => None,
99        }
100    }
101
102    /// The same key without modifiers, for a key that is no character
103    /// pressed with some: Ctrl+Home is Home where nothing binds it.
104    fn bare(&self) -> Option<Self> {
105        let character = matches!(self.code, KeyCode::Char(_));
106        (!character && !self.modifiers.is_empty()).then(|| Self::key(self.code))
107    }
108}
109
110impl fmt::Display for KeyPress {
111    fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
112        for (modifier, name) in [
113            (KeyModifiers::CONTROL, "ctrl+"),
114            (KeyModifiers::ALT, "alt+"),
115            (KeyModifiers::SUPER, "super+"),
116            (KeyModifiers::SHIFT, "shift+"),
117        ] {
118            if self.modifiers.contains(modifier) {
119                out.write_str(name)?;
120            }
121        }
122        match self.code {
123            KeyCode::Char(' ') => out.write_str("space"),
124            KeyCode::Char(character) => write!(out, "{character}"),
125            KeyCode::F(number) => write!(out, "f{number}"),
126            KeyCode::BackTab => out.write_str("shift+tab"),
127            code => {
128                let name = NAMES
129                    .iter()
130                    .find(|(_, named)| *named == code)
131                    .map_or("?", |(name, _)| name);
132                out.write_str(name)
133            }
134        }
135    }
136}
137
138/// The keys known by name, the first of each code being how it is shown.
139const NAMES: &[(&str, KeyCode)] = &[
140    ("enter", KeyCode::Enter),
141    ("return", KeyCode::Enter),
142    ("esc", KeyCode::Esc),
143    ("escape", KeyCode::Esc),
144    ("tab", KeyCode::Tab),
145    ("backtab", KeyCode::BackTab),
146    ("backspace", KeyCode::Backspace),
147    ("delete", KeyCode::Delete),
148    ("del", KeyCode::Delete),
149    ("insert", KeyCode::Insert),
150    ("home", KeyCode::Home),
151    ("end", KeyCode::End),
152    ("pageup", KeyCode::PageUp),
153    ("pgup", KeyCode::PageUp),
154    ("pagedown", KeyCode::PageDown),
155    ("pgdn", KeyCode::PageDown),
156    ("up", KeyCode::Up),
157    ("down", KeyCode::Down),
158    ("left", KeyCode::Left),
159    ("right", KeyCode::Right),
160];
161
162impl FromStr for KeyPress {
163    type Err = String;
164
165    /// Reads a key such as `ctrl+e`, `alt+shift+left`, `f5`, `space` or
166    /// `?`: modifiers (`ctrl`, `alt`, `shift`, `super`), then the key,
167    /// joined by `+`, ignoring case except in a lone character.
168    fn from_str(text: &str) -> Result<Self, Self::Err> {
169        let mut modifiers = KeyModifiers::NONE;
170        let mut rest = text.trim();
171        while let Some((modifier, after)) =
172            rest.split_once('+').filter(|(_, after)| !after.is_empty())
173        {
174            modifiers |= match modifier.to_lowercase().as_str() {
175                "ctrl" | "control" => KeyModifiers::CONTROL,
176                "alt" | "meta" | "option" => KeyModifiers::ALT,
177                "shift" => KeyModifiers::SHIFT,
178                "super" | "cmd" => KeyModifiers::SUPER,
179                _ => return Err(format!("`{modifier}` in `{text}` is not a modifier")),
180            };
181            rest = after;
182        }
183        let mut characters = rest.chars();
184        let code = match (characters.next(), characters.next()) {
185            (Some(character), None) if modifiers.contains(KeyModifiers::SHIFT) => {
186                KeyCode::Char(character.to_ascii_uppercase())
187            }
188            (Some(character), None) => KeyCode::Char(character),
189            _ => {
190                let name = rest.to_lowercase();
191                let numbered = name.strip_prefix('f').and_then(|n| n.parse::<u8>().ok());
192                match (name.as_str(), numbered) {
193                    ("space", _) => KeyCode::Char(' '),
194                    (_, Some(number @ 1..=24)) => KeyCode::F(number),
195                    _ => NAMES
196                        .iter()
197                        .find(|(named, _)| *named == name)
198                        .map(|(_, code)| *code)
199                        .ok_or_else(|| format!("`{rest}` in `{text}` is not a key"))?,
200                }
201            }
202        };
203        let code = match code {
204            KeyCode::Tab if modifiers.contains(KeyModifiers::SHIFT) => KeyCode::BackTab,
205            code => code,
206        };
207        Ok(Self::new(code, modifiers))
208    }
209}
210
211/// A key or a sequence of keys pressed one after another, such as
212/// `ctrl+x ctrl+e`.
213#[derive(Clone, Debug, PartialEq, Eq, Hash)]
214pub struct Keys(Vec<KeyPress>);
215
216impl Keys {
217    /// The presses, in order.
218    pub fn presses(&self) -> &[KeyPress] {
219        &self.0
220    }
221
222    /// Whether these keys start with `presses`, or are them.
223    fn starts_with(&self, presses: &[KeyPress]) -> bool {
224        self.0.starts_with(presses)
225    }
226}
227
228impl From<KeyPress> for Keys {
229    fn from(press: KeyPress) -> Self {
230        Self(vec![press])
231    }
232}
233
234impl<const N: usize> From<[KeyPress; N]> for Keys {
235    fn from(presses: [KeyPress; N]) -> Self {
236        Self(presses.to_vec())
237    }
238}
239
240impl FromStr for Keys {
241    type Err = String;
242
243    /// Reads keys separated by spaces, each as [`KeyPress`] reads it.
244    fn from_str(text: &str) -> Result<Self, Self::Err> {
245        let presses = text
246            .split_whitespace()
247            .map(KeyPress::from_str)
248            .collect::<Result<Vec<_>, _>>()?;
249        if presses.is_empty() {
250            return Err("no key given".to_owned());
251        }
252        Ok(Self(presses))
253    }
254}
255
256impl fmt::Display for Keys {
257    fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
258        for (index, press) in self.0.iter().enumerate() {
259            if index > 0 {
260                out.write_str(" ")?;
261            }
262            write!(out, "{press}")?;
263        }
264        Ok(())
265    }
266}
267
268/// Where a [`KeyBinding`] applies.
269#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
270pub enum KeyScope {
271    /// Wherever the keys go, as long as no picker or question is open.
272    #[default]
273    Global,
274    /// In the input, while no panel has the [`KeyFocus`]: the view's own
275    /// editing keys. A global or transcript binding of the same keys
276    /// shadows one.
277    Input,
278    /// On the transcript, while the person reads it: an item of it is
279    /// selected ([`TranscriptView::selected`](crate::TranscriptView::selected))
280    /// and no panel has the [`KeyFocus`]. Its bindings are found after the
281    /// global ones and before the input's, whose keys work otherwise, so
282    /// moving the selection and scrolling go on reading. The
283    /// reading ends when the input's text changes or is sent, and on Esc,
284    /// which the view binds here to give the keys back to the input.
285    Transcript,
286    /// In this panel, while it has the [`KeyFocus`].
287    Panel(Entity),
288}
289
290impl KeyScope {
291    /// Whether a key can be bound in both scopes at once: not the same
292    /// keys in one scope, nor globally and in a panel or the transcript.
293    /// In the input and globally or on the transcript it can: the other
294    /// binding shadows the input's.
295    fn overlaps(self, other: Self) -> bool {
296        match (self, other) {
297            (Self::Global | Self::Transcript, Self::Input)
298            | (Self::Input, Self::Global | Self::Transcript) => false,
299            (Self::Global, _) | (_, Self::Global) => true,
300            _ => self == other,
301        }
302    }
303
304    /// The scopes whose bindings apply while the keys go to this one, in
305    /// the order they are found: the global ones, then this one's, then,
306    /// while reading the transcript, the input's.
307    pub(crate) fn applying(self) -> impl Iterator<Item = Self> {
308        let reading = (self == Self::Transcript).then_some(Self::Input);
309        [Self::Global, self].into_iter().chain(reading)
310    }
311}
312
313/// Keys bound to something, on an entity of its own named `key:<keys>`.
314/// Disable it to free its keys. See [`keys`](crate::keys).
315#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq)]
316#[reflect(opaque, Component, Clone, Debug, PartialEq)]
317pub struct KeyBinding {
318    /// The keys.
319    pub keys: Keys,
320    /// What it does, in a few words, for listings.
321    pub label: String,
322    /// Where it applies.
323    pub scope: KeyScope,
324    /// Whether letting go of the key triggers [`KeyPressed`] too, with
325    /// `released`, for holding a key down (push to talk). A key held down
326    /// then triggers once, not again for each repeat; a binding of a
327    /// sequence never hears its release. While one asks for releases the
328    /// terminal is asked to report them, which not every terminal does.
329    pub releases: bool,
330}
331
332impl KeyBinding {
333    /// `keys` bound everywhere, doing what `label` says.
334    pub fn new(keys: impl Into<Keys>, label: impl Into<String>) -> Self {
335        Self {
336            keys: keys.into(),
337            label: label.into(),
338            scope: KeyScope::Global,
339            releases: false,
340        }
341    }
342
343    /// The same binding, applying only in `scope`.
344    pub fn in_scope(mut self, scope: KeyScope) -> Self {
345        self.scope = scope;
346        self
347    }
348
349    /// The same binding, also triggered when its key is let go, where the
350    /// terminal reports that.
351    pub fn with_releases(mut self) -> Self {
352        self.releases = true;
353        self
354    }
355
356    /// Whether this and `other` cannot both be bound: their scopes
357    /// overlap, and one's keys are the other's or start them.
358    pub fn conflicts(&self, other: &Self) -> bool {
359        self.scope.overlaps(other.scope)
360            && (self.keys.starts_with(other.keys.presses())
361                || other.keys.starts_with(self.keys.presses()))
362    }
363}
364
365/// Triggered on a [`KeyBinding`]'s entity when its keys are pressed, or,
366/// for one with [`KeyBinding::releases`], let go.
367#[derive(EntityEvent, Reflect, Clone, Copy, Debug)]
368#[reflect(Event, Clone, Debug)]
369pub struct KeyPressed {
370    /// The binding.
371    pub entity: Entity,
372    /// The shown agent, if any.
373    pub agent: Option<Entity>,
374    /// Whether the key was let go rather than pressed.
375    pub released: bool,
376}
377
378/// The panel that takes the keys, or `None` for the input. Set it to a
379/// [`TuiPanel`](crate::TuiPanel)'s entity to give the panel the keys; the view sets it back
380/// to `None` when that entity is no panel, or gone, and when Esc is
381/// pressed that the panel does not bind.
382#[derive(Resource, Reflect, Clone, Copy, Debug, Default, PartialEq, Eq)]
383#[reflect(Resource, Clone, Debug, Default, PartialEq)]
384pub struct KeyFocus(pub Option<Entity>);
385
386/// Gives the keys back to the input as soon as the panel that has them
387/// stops being one, or goes.
388pub(crate) fn release_focus(removed: On<Remove<crate::TuiPanel>>, mut focus: ResMut<KeyFocus>) {
389    if focus.0 == Some(removed.entity) {
390        focus.0 = None;
391    }
392}
393
394/// Binds keys on an [`App`].
395pub trait AppKeyBindingsExt {
396    /// Spawns `binding` with `on_press`, an observer of the [`KeyPressed`]
397    /// it triggers, and returns its entity, unless another binding
398    /// conflicts with it ([`KeyBinding::conflicts`]): that one stays, with
399    /// a warning.
400    fn add_key_binding<M>(
401        &mut self,
402        binding: KeyBinding,
403        on_press: impl IntoObserverSystem<KeyPressed, M>,
404    ) -> Option<Entity>;
405}
406
407impl AppKeyBindingsExt for App {
408    fn add_key_binding<M>(
409        &mut self,
410        binding: KeyBinding,
411        on_press: impl IntoObserverSystem<KeyPressed, M>,
412    ) -> Option<Entity> {
413        let mut bound = bind(self.world_mut(), binding)?;
414        bound.observe(on_press);
415        Some(bound.id())
416    }
417}
418
419/// Spawns `binding`, unless one conflicts with it: that one stays, with a
420/// warning. A query skips a disabled binding.
421fn bind(world: &mut World, binding: KeyBinding) -> Option<EntityWorldMut<'_>> {
422    let mut bindings = world.query::<&KeyBinding>();
423    if let Some(taken) = bindings.iter(world).find(|bound| bound.conflicts(&binding)) {
424        warn!(
425            "key binding `{}` ({}) not added: `{}` ({}) is bound; insert `Disabled` on it to \
426             replace it",
427            binding.keys, binding.label, taken.keys, taken.label
428        );
429        return None;
430    }
431    let name = Name::new(format!("key:{}", binding.keys));
432    Some(world.spawn((name, binding)))
433}
434
435/// An observer for [`AppKeyBindingsExt::add_key_binding`] that runs the
436/// slash command `line` (without its `/`) for the shown agent.
437pub fn run_command(
438    line: impl Into<String>,
439) -> impl FnMut(On<KeyPressed>, Commands) + Send + Sync + 'static {
440    let line = line.into();
441    move |pressed: On<KeyPressed>, mut commands: Commands| {
442        if let (Some(entity), false) = (pressed.agent, pressed.released) {
443            commands.trigger(RunCommand::new(entity, line.clone()));
444        }
445    }
446}
447
448/// A built-in action of the input. On a [`KeyBinding`]'s entity it is what
449/// the binding does, in order with the other keys of the frame, instead of
450/// triggering [`KeyPressed`]: the view's own keys are such bindings, and a
451/// plugin rebinds one by spawning its own. Pushed onto the
452/// [`PendingInput`](crate::PendingInput) as
453/// [`KeyInput::Action`](crate::KeyInput::Action), it is done where it
454/// stands among the keys.
455///
456/// ```no_run
457/// use rig_harness::prelude::*;
458/// use rig_tui::{InputAction, KeyBinding, KeyPress};
459///
460/// fn build(app: &mut App) {
461///     // Ctrl+K browses back as Up does: the completion, the line, the history.
462///     app.world_mut().spawn((KeyBinding::new(KeyPress::ctrl('k'), "Up"), InputAction::Up));
463/// }
464/// ```
465#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
466pub enum InputAction {
467    /// Empties the input.
468    Clear,
469    /// Types a newline.
470    NewLine,
471    /// The start of the cursor's line.
472    LineStart,
473    /// The end of the cursor's line.
474    LineEnd,
475    /// A character back.
476    Left,
477    /// A character forward.
478    Right,
479    /// The start of the word before the cursor.
480    WordLeft,
481    /// The end of the word after the cursor.
482    WordRight,
483    /// Deletes the character before the cursor.
484    Backspace,
485    /// Deletes the character after the cursor.
486    Delete,
487    /// Deletes the word before the cursor.
488    DeleteWordBefore,
489    /// Deletes the word after the cursor.
490    DeleteWordAfter,
491    /// Deletes to the start of the line, or the newline before it.
492    DeleteToLineStart,
493    /// Deletes to the end of the line, or the newline after it.
494    DeleteToLineEnd,
495    /// The line above, or an earlier prompt of the history: Ctrl+P.
496    LineUp,
497    /// The line below, or a later prompt of the history: Ctrl+N.
498    LineDown,
499    /// The completion's previous item, else the line above, else an earlier
500    /// prompt.
501    Up,
502    /// The completion's next item, else the line below, else a later
503    /// prompt.
504    Down,
505    /// Scrolls the transcript a row up.
506    ScrollUp,
507    /// Scrolls the transcript a row down.
508    ScrollDown,
509    /// Scrolls the transcript a page up.
510    PageUp,
511    /// Scrolls the transcript a page down.
512    PageDown,
513    /// Completes, or with a turn running and nothing to complete, sends
514    /// for after the turn.
515    Tab,
516    /// Completes a partly typed command or path, continues a line ending
517    /// in `\`, else sends.
518    Send,
519    /// Closes the completion, else interrupts the shown agent.
520    Stop,
521    /// Saves the clipboard's image in the session's
522    /// [`Scratch::images`](rig_harness::prelude::Scratch::images) and types
523    /// `@path` for it, which attaches it to the message, or says there is
524    /// none. A pasted path to an image file (a file dropped on the
525    /// terminal) becomes `@path` too.
526    PasteImage,
527    /// Selects the item of the transcript above the one selected, or the
528    /// newest.
529    SelectUp,
530    /// Selects the item below the one selected; past the newest, none.
531    SelectDown,
532    /// Selects none, which gives the keys back to the input: Esc while
533    /// the person reads the transcript ([`KeyScope::Transcript`]).
534    Deselect,
535    /// Answers the question shown beside the work with its choice of this
536    /// number, counted from 1, or the number after its choices: types its
537    /// answer in the input.
538    AnswerInline(u8),
539    /// Closes the question shown beside the work, refused.
540    DismissInline,
541}
542
543/// On the bindings of the keys that answer a question shown beside the
544/// work: bound only while one shows.
545#[derive(Component)]
546pub(crate) struct InlineKey;
547
548/// The view's own keys, as `(keys, label, action)`.
549fn built_in() -> Vec<(Keys, &'static str, InputAction)> {
550    use InputAction as Do;
551    use KeyCode as K;
552    let key = |code| Keys::from(KeyPress::key(code));
553    let ctrl = |character| Keys::from(KeyPress::ctrl(character));
554    let alt = |character| Keys::from(KeyPress::alt(character));
555    let with = |modifiers, code| Keys::from(KeyPress::new(code, modifiers));
556    let (control, meta) = (KeyModifiers::CONTROL, KeyModifiers::ALT);
557    vec![
558        (ctrl('c'), "Clear the input", Do::Clear),
559        (ctrl('j'), "New line", Do::NewLine),
560        (with(KeyModifiers::SHIFT, K::Enter), "New line", Do::NewLine),
561        (with(meta, K::Enter), "New line", Do::NewLine),
562        (ctrl('a'), "Start of the line", Do::LineStart),
563        (key(K::Home), "Start of the line", Do::LineStart),
564        (ctrl('e'), "End of the line", Do::LineEnd),
565        (key(K::End), "End of the line", Do::LineEnd),
566        (ctrl('b'), "Back a character", Do::Left),
567        (key(K::Left), "Back a character", Do::Left),
568        (ctrl('f'), "Forward a character", Do::Right),
569        (key(K::Right), "Forward a character", Do::Right),
570        (alt('b'), "Back a word", Do::WordLeft),
571        (with(control, K::Left), "Back a word", Do::WordLeft),
572        (with(meta, K::Left), "Back a word", Do::WordLeft),
573        (alt('f'), "Forward a word", Do::WordRight),
574        (with(control, K::Right), "Forward a word", Do::WordRight),
575        (with(meta, K::Right), "Forward a word", Do::WordRight),
576        (key(K::Backspace), "Delete back", Do::Backspace),
577        (key(K::Delete), "Delete forward", Do::Delete),
578        (ctrl('w'), "Delete the word back", Do::DeleteWordBefore),
579        (
580            with(control, K::Backspace),
581            "Delete the word back",
582            Do::DeleteWordBefore,
583        ),
584        (
585            with(meta, K::Backspace),
586            "Delete the word back",
587            Do::DeleteWordBefore,
588        ),
589        (alt('d'), "Delete the word forward", Do::DeleteWordAfter),
590        (
591            with(control, K::Delete),
592            "Delete the word forward",
593            Do::DeleteWordAfter,
594        ),
595        (
596            with(meta, K::Delete),
597            "Delete the word forward",
598            Do::DeleteWordAfter,
599        ),
600        (
601            ctrl('u'),
602            "Delete to the line's start",
603            Do::DeleteToLineStart,
604        ),
605        (ctrl('k'), "Delete to the line's end", Do::DeleteToLineEnd),
606        (ctrl('p'), "Line up, or an earlier prompt", Do::LineUp),
607        (ctrl('n'), "Line down, or a later prompt", Do::LineDown),
608        (key(K::Up), "Previous completion, line or prompt", Do::Up),
609        (key(K::Down), "Next completion, line or prompt", Do::Down),
610        (with(KeyModifiers::SHIFT, K::Up), "Scroll up", Do::ScrollUp),
611        (
612            with(KeyModifiers::SHIFT, K::Down),
613            "Scroll down",
614            Do::ScrollDown,
615        ),
616        (key(K::PageUp), "Scroll a page up", Do::PageUp),
617        (key(K::PageDown), "Scroll a page down", Do::PageDown),
618        (key(K::Tab), "Complete, or send after this turn", Do::Tab),
619        (key(K::Enter), "Send, or steer the running turn", Do::Send),
620        (
621            key(K::Esc),
622            "Close the completion, or stop the agent",
623            Do::Stop,
624        ),
625        (ctrl('v'), "Paste an image", Do::PasteImage),
626        (with(meta, K::Up), "Select the item above", Do::SelectUp),
627        (with(meta, K::Down), "Select the item below", Do::SelectDown),
628    ]
629}
630
631/// Spawns the view's own key bindings, in the input's scope, and Esc on
632/// the transcript, which gives the keys back to the input; the keys that
633/// answer a question shown beside the work, Alt+1 to Alt+9 and Alt+0,
634/// stay disabled until one shows. A plugin's global binding of the same
635/// keys wins over them.
636pub(crate) fn add_built_in(app: &mut App) {
637    app.init_resource::<KeyFocus>();
638    for (keys, label, action) in built_in() {
639        let binding = KeyBinding::new(keys, label).in_scope(KeyScope::Input);
640        if let Some(mut bound) = bind(app.world_mut(), binding) {
641            bound.insert(action);
642        }
643    }
644    let back = KeyBinding::new(KeyPress::key(KeyCode::Esc), "Back to the input");
645    if let Some(mut bound) = bind(app.world_mut(), back.in_scope(KeyScope::Transcript)) {
646        bound.insert(InputAction::Deselect);
647    }
648    let inline = (1..=9u8).map(|number| (number, "Answer the question beside the work"));
649    let inline = inline.chain([(0, "Dismiss the question beside the work")]);
650    for (number, label) in inline {
651        let digit = char::from(b'0' + number);
652        let binding = KeyBinding::new(KeyPress::alt(digit), label).in_scope(KeyScope::Input);
653        let action = match number {
654            0 => InputAction::DismissInline,
655            number => InputAction::AnswerInline(number),
656        };
657        if let Some(mut bound) = bind(app.world_mut(), binding) {
658            bound.insert((action, InlineKey, Disabled));
659        }
660    }
661}
662
663/// Binds the keys that answer a question shown beside the work while one
664/// shows, and frees them otherwise.
665pub(crate) fn arm_inline_keys(
666    shown: Res<TuiView>,
667    keys: Query<(Entity, Has<Disabled>), (With<InlineKey>, Allow<Disabled>)>,
668    mut commands: Commands,
669) {
670    let showing = !shown.inline.is_empty();
671    for (key, disabled) in &keys {
672        match (showing, disabled) {
673            (true, true) => {
674                commands.entity(key).remove::<Disabled>();
675            }
676            (false, false) => {
677                commands.entity(key).insert(Disabled);
678            }
679            _ => {}
680        }
681    }
682}
683
684/// What the keys pressed so far match, as [`find`] says.
685#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686pub enum Found {
687    /// This binding's keys.
688    Binding(Entity),
689    /// The start of a binding's keys: wait for the next.
690    Partial,
691    /// No binding.
692    Nothing,
693}
694
695/// The binding that `typed` matches in the scopes that apply while the
696/// keys go to `focus` (`KeyScope::applying`), so a global binding
697/// shadows the input's; the oldest entity of two bound around the conflict
698/// check. A single key that is no character, pressed with modifiers that
699/// nothing binds, is that key alone.
700pub fn find(bindings: &[(Entity, &KeyBinding)], typed: &[KeyPress], focus: KeyScope) -> Found {
701    let scoped = |scope: KeyScope| {
702        bindings
703            .iter()
704            .filter(move |(_, binding)| binding.scope == scope)
705    };
706    let exact = |typed: &[KeyPress], scope: KeyScope| {
707        scoped(scope)
708            .filter(|(_, binding)| binding.keys.presses() == typed)
709            .map(|(entity, _)| *entity)
710            .min()
711    };
712    for scope in focus.applying() {
713        if let Some(entity) = exact(typed, scope) {
714            return Found::Binding(entity);
715        }
716        if scoped(scope).any(|(_, binding)| binding.keys.starts_with(typed)) {
717            return Found::Partial;
718        }
719    }
720    let bare = match typed {
721        [press] => press.bare(),
722        _ => None,
723    };
724    bare.and_then(|bare| focus.applying().find_map(|scope| exact(&[bare], scope)))
725        .map_or(Found::Nothing, Found::Binding)
726}
727
728#[cfg(test)]
729mod tests;