1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub struct KeyPress {
46 pub code: KeyCode,
48 pub modifiers: KeyModifiers,
50}
51
52impl KeyPress {
53 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 pub fn key(code: KeyCode) -> Self {
64 Self::new(code, KeyModifiers::NONE)
65 }
66
67 pub fn ctrl(character: char) -> Self {
69 Self::new(KeyCode::Char(character), KeyModifiers::CONTROL)
70 }
71
72 pub fn alt(character: char) -> Self {
74 Self::new(KeyCode::Char(character), KeyModifiers::ALT)
75 }
76
77 pub fn shift(code: KeyCode) -> Self {
79 Self::new(code, KeyModifiers::SHIFT)
80 }
81
82 pub fn of(event: &KeyEvent) -> Self {
84 Self::new(event.code, event.modifiers)
85 }
86
87 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 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
138const 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 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
214pub struct Keys(Vec<KeyPress>);
215
216impl Keys {
217 pub fn presses(&self) -> &[KeyPress] {
219 &self.0
220 }
221
222 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
270pub enum KeyScope {
271 #[default]
273 Global,
274 Input,
278 Transcript,
286 Panel(Entity),
288}
289
290impl KeyScope {
291 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 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#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq)]
316#[reflect(opaque, Component, Clone, Debug, PartialEq)]
317pub struct KeyBinding {
318 pub keys: Keys,
320 pub label: String,
322 pub scope: KeyScope,
324 pub releases: bool,
330}
331
332impl KeyBinding {
333 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 pub fn in_scope(mut self, scope: KeyScope) -> Self {
345 self.scope = scope;
346 self
347 }
348
349 pub fn with_releases(mut self) -> Self {
352 self.releases = true;
353 self
354 }
355
356 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#[derive(EntityEvent, Reflect, Clone, Copy, Debug)]
368#[reflect(Event, Clone, Debug)]
369pub struct KeyPressed {
370 pub entity: Entity,
372 pub agent: Option<Entity>,
374 pub released: bool,
376}
377
378#[derive(Resource, Reflect, Clone, Copy, Debug, Default, PartialEq, Eq)]
383#[reflect(Resource, Clone, Debug, Default, PartialEq)]
384pub struct KeyFocus(pub Option<Entity>);
385
386pub(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
394pub trait AppKeyBindingsExt {
396 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
419fn 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
435pub 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#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
466pub enum InputAction {
467 Clear,
469 NewLine,
471 LineStart,
473 LineEnd,
475 Left,
477 Right,
479 WordLeft,
481 WordRight,
483 Backspace,
485 Delete,
487 DeleteWordBefore,
489 DeleteWordAfter,
491 DeleteToLineStart,
493 DeleteToLineEnd,
495 LineUp,
497 LineDown,
499 Up,
502 Down,
505 ScrollUp,
507 ScrollDown,
509 PageUp,
511 PageDown,
513 Tab,
516 Send,
519 Stop,
521 PasteImage,
527 SelectUp,
530 SelectDown,
532 Deselect,
535 AnswerInline(u8),
539 DismissInline,
541}
542
543#[derive(Component)]
546pub(crate) struct InlineKey;
547
548fn 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
631pub(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
663pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686pub enum Found {
687 Binding(Entity),
689 Partial,
691 Nothing,
693}
694
695pub 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;