use std::fmt;
use std::str::FromStr;
use bevy_app::App;
use bevy_ecs::entity_disabling::Disabled;
use bevy_ecs::prelude::*;
use bevy_ecs::query::Allow;
use bevy_ecs::system::IntoObserverSystem;
use bevy_log::warn;
use bevy_reflect::prelude::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use rig_harness::commands::RunCommand;
use super::view::TuiView;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct KeyPress {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyPress {
pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
let modifiers = match code {
KeyCode::Char(_) | KeyCode::BackTab => modifiers.difference(KeyModifiers::SHIFT),
_ => modifiers,
};
Self { code, modifiers }
}
pub fn key(code: KeyCode) -> Self {
Self::new(code, KeyModifiers::NONE)
}
pub fn ctrl(character: char) -> Self {
Self::new(KeyCode::Char(character), KeyModifiers::CONTROL)
}
pub fn alt(character: char) -> Self {
Self::new(KeyCode::Char(character), KeyModifiers::ALT)
}
pub fn shift(code: KeyCode) -> Self {
Self::new(code, KeyModifiers::SHIFT)
}
pub fn of(event: &KeyEvent) -> Self {
Self::new(event.code, event.modifiers)
}
pub fn typed(&self) -> Option<char> {
match self.code {
KeyCode::Char(character)
if !self
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
Some(character)
}
_ => None,
}
}
fn bare(&self) -> Option<Self> {
let character = matches!(self.code, KeyCode::Char(_));
(!character && !self.modifiers.is_empty()).then(|| Self::key(self.code))
}
}
impl fmt::Display for KeyPress {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
for (modifier, name) in [
(KeyModifiers::CONTROL, "ctrl+"),
(KeyModifiers::ALT, "alt+"),
(KeyModifiers::SUPER, "super+"),
(KeyModifiers::SHIFT, "shift+"),
] {
if self.modifiers.contains(modifier) {
out.write_str(name)?;
}
}
match self.code {
KeyCode::Char(' ') => out.write_str("space"),
KeyCode::Char(character) => write!(out, "{character}"),
KeyCode::F(number) => write!(out, "f{number}"),
KeyCode::BackTab => out.write_str("shift+tab"),
code => {
let name = NAMES
.iter()
.find(|(_, named)| *named == code)
.map_or("?", |(name, _)| name);
out.write_str(name)
}
}
}
}
const NAMES: &[(&str, KeyCode)] = &[
("enter", KeyCode::Enter),
("return", KeyCode::Enter),
("esc", KeyCode::Esc),
("escape", KeyCode::Esc),
("tab", KeyCode::Tab),
("backtab", KeyCode::BackTab),
("backspace", KeyCode::Backspace),
("delete", KeyCode::Delete),
("del", KeyCode::Delete),
("insert", KeyCode::Insert),
("home", KeyCode::Home),
("end", KeyCode::End),
("pageup", KeyCode::PageUp),
("pgup", KeyCode::PageUp),
("pagedown", KeyCode::PageDown),
("pgdn", KeyCode::PageDown),
("up", KeyCode::Up),
("down", KeyCode::Down),
("left", KeyCode::Left),
("right", KeyCode::Right),
];
impl FromStr for KeyPress {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let mut modifiers = KeyModifiers::NONE;
let mut rest = text.trim();
while let Some((modifier, after)) =
rest.split_once('+').filter(|(_, after)| !after.is_empty())
{
modifiers |= match modifier.to_lowercase().as_str() {
"ctrl" | "control" => KeyModifiers::CONTROL,
"alt" | "meta" | "option" => KeyModifiers::ALT,
"shift" => KeyModifiers::SHIFT,
"super" | "cmd" => KeyModifiers::SUPER,
_ => return Err(format!("`{modifier}` in `{text}` is not a modifier")),
};
rest = after;
}
let mut characters = rest.chars();
let code = match (characters.next(), characters.next()) {
(Some(character), None) if modifiers.contains(KeyModifiers::SHIFT) => {
KeyCode::Char(character.to_ascii_uppercase())
}
(Some(character), None) => KeyCode::Char(character),
_ => {
let name = rest.to_lowercase();
let numbered = name.strip_prefix('f').and_then(|n| n.parse::<u8>().ok());
match (name.as_str(), numbered) {
("space", _) => KeyCode::Char(' '),
(_, Some(number @ 1..=24)) => KeyCode::F(number),
_ => NAMES
.iter()
.find(|(named, _)| *named == name)
.map(|(_, code)| *code)
.ok_or_else(|| format!("`{rest}` in `{text}` is not a key"))?,
}
}
};
let code = match code {
KeyCode::Tab if modifiers.contains(KeyModifiers::SHIFT) => KeyCode::BackTab,
code => code,
};
Ok(Self::new(code, modifiers))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Keys(Vec<KeyPress>);
impl Keys {
pub fn presses(&self) -> &[KeyPress] {
&self.0
}
fn starts_with(&self, presses: &[KeyPress]) -> bool {
self.0.starts_with(presses)
}
}
impl From<KeyPress> for Keys {
fn from(press: KeyPress) -> Self {
Self(vec![press])
}
}
impl<const N: usize> From<[KeyPress; N]> for Keys {
fn from(presses: [KeyPress; N]) -> Self {
Self(presses.to_vec())
}
}
impl FromStr for Keys {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let presses = text
.split_whitespace()
.map(KeyPress::from_str)
.collect::<Result<Vec<_>, _>>()?;
if presses.is_empty() {
return Err("no key given".to_owned());
}
Ok(Self(presses))
}
}
impl fmt::Display for Keys {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, press) in self.0.iter().enumerate() {
if index > 0 {
out.write_str(" ")?;
}
write!(out, "{press}")?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum KeyScope {
#[default]
Global,
Input,
Transcript,
Panel(Entity),
}
impl KeyScope {
fn overlaps(self, other: Self) -> bool {
match (self, other) {
(Self::Global | Self::Transcript, Self::Input)
| (Self::Input, Self::Global | Self::Transcript) => false,
(Self::Global, _) | (_, Self::Global) => true,
_ => self == other,
}
}
pub(crate) fn applying(self) -> impl Iterator<Item = Self> {
let reading = (self == Self::Transcript).then_some(Self::Input);
[Self::Global, self].into_iter().chain(reading)
}
}
#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq)]
#[reflect(opaque, Component, Clone, Debug, PartialEq)]
pub struct KeyBinding {
pub keys: Keys,
pub label: String,
pub scope: KeyScope,
pub releases: bool,
}
impl KeyBinding {
pub fn new(keys: impl Into<Keys>, label: impl Into<String>) -> Self {
Self {
keys: keys.into(),
label: label.into(),
scope: KeyScope::Global,
releases: false,
}
}
pub fn in_scope(mut self, scope: KeyScope) -> Self {
self.scope = scope;
self
}
pub fn with_releases(mut self) -> Self {
self.releases = true;
self
}
pub fn conflicts(&self, other: &Self) -> bool {
self.scope.overlaps(other.scope)
&& (self.keys.starts_with(other.keys.presses())
|| other.keys.starts_with(self.keys.presses()))
}
}
#[derive(EntityEvent, Reflect, Clone, Copy, Debug)]
#[reflect(Event, Clone, Debug)]
pub struct KeyPressed {
pub entity: Entity,
pub agent: Option<Entity>,
pub released: bool,
}
#[derive(Resource, Reflect, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[reflect(Resource, Clone, Debug, Default, PartialEq)]
pub struct KeyFocus(pub Option<Entity>);
pub(crate) fn release_focus(removed: On<Remove<crate::TuiPanel>>, mut focus: ResMut<KeyFocus>) {
if focus.0 == Some(removed.entity) {
focus.0 = None;
}
}
pub trait AppKeyBindingsExt {
fn add_key_binding<M>(
&mut self,
binding: KeyBinding,
on_press: impl IntoObserverSystem<KeyPressed, M>,
) -> Option<Entity>;
}
impl AppKeyBindingsExt for App {
fn add_key_binding<M>(
&mut self,
binding: KeyBinding,
on_press: impl IntoObserverSystem<KeyPressed, M>,
) -> Option<Entity> {
let mut bound = bind(self.world_mut(), binding)?;
bound.observe(on_press);
Some(bound.id())
}
}
fn bind(world: &mut World, binding: KeyBinding) -> Option<EntityWorldMut<'_>> {
let mut bindings = world.query::<&KeyBinding>();
if let Some(taken) = bindings.iter(world).find(|bound| bound.conflicts(&binding)) {
warn!(
"key binding `{}` ({}) not added: `{}` ({}) is bound; insert `Disabled` on it to \
replace it",
binding.keys, binding.label, taken.keys, taken.label
);
return None;
}
let name = Name::new(format!("key:{}", binding.keys));
Some(world.spawn((name, binding)))
}
pub fn run_command(
line: impl Into<String>,
) -> impl FnMut(On<KeyPressed>, Commands) + Send + Sync + 'static {
let line = line.into();
move |pressed: On<KeyPressed>, mut commands: Commands| {
if let (Some(entity), false) = (pressed.agent, pressed.released) {
commands.trigger(RunCommand::new(entity, line.clone()));
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputAction {
Clear,
NewLine,
LineStart,
LineEnd,
Left,
Right,
WordLeft,
WordRight,
Backspace,
Delete,
DeleteWordBefore,
DeleteWordAfter,
DeleteToLineStart,
DeleteToLineEnd,
LineUp,
LineDown,
Up,
Down,
ScrollUp,
ScrollDown,
PageUp,
PageDown,
Tab,
Send,
Stop,
PasteImage,
SelectUp,
SelectDown,
Deselect,
AnswerInline(u8),
DismissInline,
}
#[derive(Component)]
pub(crate) struct InlineKey;
fn built_in() -> Vec<(Keys, &'static str, InputAction)> {
use InputAction as Do;
use KeyCode as K;
let key = |code| Keys::from(KeyPress::key(code));
let ctrl = |character| Keys::from(KeyPress::ctrl(character));
let alt = |character| Keys::from(KeyPress::alt(character));
let with = |modifiers, code| Keys::from(KeyPress::new(code, modifiers));
let (control, meta) = (KeyModifiers::CONTROL, KeyModifiers::ALT);
vec![
(ctrl('c'), "Clear the input", Do::Clear),
(ctrl('j'), "New line", Do::NewLine),
(with(KeyModifiers::SHIFT, K::Enter), "New line", Do::NewLine),
(with(meta, K::Enter), "New line", Do::NewLine),
(ctrl('a'), "Start of the line", Do::LineStart),
(key(K::Home), "Start of the line", Do::LineStart),
(ctrl('e'), "End of the line", Do::LineEnd),
(key(K::End), "End of the line", Do::LineEnd),
(ctrl('b'), "Back a character", Do::Left),
(key(K::Left), "Back a character", Do::Left),
(ctrl('f'), "Forward a character", Do::Right),
(key(K::Right), "Forward a character", Do::Right),
(alt('b'), "Back a word", Do::WordLeft),
(with(control, K::Left), "Back a word", Do::WordLeft),
(with(meta, K::Left), "Back a word", Do::WordLeft),
(alt('f'), "Forward a word", Do::WordRight),
(with(control, K::Right), "Forward a word", Do::WordRight),
(with(meta, K::Right), "Forward a word", Do::WordRight),
(key(K::Backspace), "Delete back", Do::Backspace),
(key(K::Delete), "Delete forward", Do::Delete),
(ctrl('w'), "Delete the word back", Do::DeleteWordBefore),
(
with(control, K::Backspace),
"Delete the word back",
Do::DeleteWordBefore,
),
(
with(meta, K::Backspace),
"Delete the word back",
Do::DeleteWordBefore,
),
(alt('d'), "Delete the word forward", Do::DeleteWordAfter),
(
with(control, K::Delete),
"Delete the word forward",
Do::DeleteWordAfter,
),
(
with(meta, K::Delete),
"Delete the word forward",
Do::DeleteWordAfter,
),
(
ctrl('u'),
"Delete to the line's start",
Do::DeleteToLineStart,
),
(ctrl('k'), "Delete to the line's end", Do::DeleteToLineEnd),
(ctrl('p'), "Line up, or an earlier prompt", Do::LineUp),
(ctrl('n'), "Line down, or a later prompt", Do::LineDown),
(key(K::Up), "Previous completion, line or prompt", Do::Up),
(key(K::Down), "Next completion, line or prompt", Do::Down),
(with(KeyModifiers::SHIFT, K::Up), "Scroll up", Do::ScrollUp),
(
with(KeyModifiers::SHIFT, K::Down),
"Scroll down",
Do::ScrollDown,
),
(key(K::PageUp), "Scroll a page up", Do::PageUp),
(key(K::PageDown), "Scroll a page down", Do::PageDown),
(key(K::Tab), "Complete, or send after this turn", Do::Tab),
(key(K::Enter), "Send, or steer the running turn", Do::Send),
(
key(K::Esc),
"Close the completion, or stop the agent",
Do::Stop,
),
(ctrl('v'), "Paste an image", Do::PasteImage),
(with(meta, K::Up), "Select the item above", Do::SelectUp),
(with(meta, K::Down), "Select the item below", Do::SelectDown),
]
}
pub(crate) fn add_built_in(app: &mut App) {
app.init_resource::<KeyFocus>();
for (keys, label, action) in built_in() {
let binding = KeyBinding::new(keys, label).in_scope(KeyScope::Input);
if let Some(mut bound) = bind(app.world_mut(), binding) {
bound.insert(action);
}
}
let back = KeyBinding::new(KeyPress::key(KeyCode::Esc), "Back to the input");
if let Some(mut bound) = bind(app.world_mut(), back.in_scope(KeyScope::Transcript)) {
bound.insert(InputAction::Deselect);
}
let inline = (1..=9u8).map(|number| (number, "Answer the question beside the work"));
let inline = inline.chain([(0, "Dismiss the question beside the work")]);
for (number, label) in inline {
let digit = char::from(b'0' + number);
let binding = KeyBinding::new(KeyPress::alt(digit), label).in_scope(KeyScope::Input);
let action = match number {
0 => InputAction::DismissInline,
number => InputAction::AnswerInline(number),
};
if let Some(mut bound) = bind(app.world_mut(), binding) {
bound.insert((action, InlineKey, Disabled));
}
}
}
pub(crate) fn arm_inline_keys(
shown: Res<TuiView>,
keys: Query<(Entity, Has<Disabled>), (With<InlineKey>, Allow<Disabled>)>,
mut commands: Commands,
) {
let showing = !shown.inline.is_empty();
for (key, disabled) in &keys {
match (showing, disabled) {
(true, true) => {
commands.entity(key).remove::<Disabled>();
}
(false, false) => {
commands.entity(key).insert(Disabled);
}
_ => {}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Found {
Binding(Entity),
Partial,
Nothing,
}
pub fn find(bindings: &[(Entity, &KeyBinding)], typed: &[KeyPress], focus: KeyScope) -> Found {
let scoped = |scope: KeyScope| {
bindings
.iter()
.filter(move |(_, binding)| binding.scope == scope)
};
let exact = |typed: &[KeyPress], scope: KeyScope| {
scoped(scope)
.filter(|(_, binding)| binding.keys.presses() == typed)
.map(|(entity, _)| *entity)
.min()
};
for scope in focus.applying() {
if let Some(entity) = exact(typed, scope) {
return Found::Binding(entity);
}
if scoped(scope).any(|(_, binding)| binding.keys.starts_with(typed)) {
return Found::Partial;
}
}
let bare = match typed {
[press] => press.bare(),
_ => None,
};
bare.and_then(|bare| focus.applying().find_map(|scope| exact(&[bare], scope)))
.map_or(Found::Nothing, Found::Binding)
}
#[cfg(test)]
mod tests;