-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(controllers): Unify event flow for
controller
s (#41)
- Loading branch information
Showing
16 changed files
with
537 additions
and
129 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[package] | ||
name = "input-mapping-common" | ||
version.workspace = true | ||
edition.workspace = true | ||
authors.workspace = true | ||
|
||
[dependencies] | ||
ratatui.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
use ratatui::crossterm::event::{Event, KeyCode}; | ||
|
||
pub trait InputMappingT: Sized { | ||
fn get_mapping() -> InputMapping; | ||
|
||
fn map_event(event: Event) -> Option<Self>; | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct InputMapping { | ||
pub mapping: Vec<MappingEntry>, | ||
} | ||
|
||
impl InputMapping { | ||
pub fn merge(mut self, mut other: InputMapping) -> Self { | ||
self.mapping.append(&mut other.mapping); | ||
self | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct MappingEntry { | ||
pub key: KeyCode, | ||
pub description: String, | ||
} | ||
|
||
pub trait KeyCodeConversions { | ||
fn convert(self) -> KeyCode; | ||
} | ||
|
||
impl KeyCodeConversions for char { | ||
fn convert(self) -> KeyCode { | ||
KeyCode::Char(self) | ||
} | ||
} | ||
|
||
impl KeyCodeConversions for KeyCode { | ||
fn convert(self) -> KeyCode { | ||
self | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "input-mapping-derive" | ||
version.workspace = true | ||
edition.workspace = true | ||
authors.workspace = true | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
input-mapping-common.workspace = true | ||
|
||
itertools.workspace = true | ||
proc-macro2.workspace = true | ||
quote.workspace = true | ||
syn.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
use proc_macro2::{Literal, TokenStream}; | ||
use quote::ToTokens; | ||
use syn::{parse_macro_input, Expr, Fields, ItemEnum, Lit, Meta, MetaNameValue, Variant}; | ||
|
||
#[macro_use] | ||
extern crate quote; | ||
extern crate proc_macro; | ||
extern crate syn; | ||
|
||
#[proc_macro_derive(InputMapping, attributes(key, description))] | ||
pub fn derive_mapping(input: proc_macro::TokenStream) -> proc_macro::TokenStream { | ||
let item_enum = parse_macro_input!(input as ItemEnum); | ||
let trait_impl = generate_trait_impl(item_enum); | ||
|
||
proc_macro::TokenStream::from(trait_impl) | ||
} | ||
|
||
// TODO: Add check that mappings don't overlap. | ||
fn generate_trait_impl(item_enum: ItemEnum) -> TokenStream { | ||
let (unit, to_be_flattened): (Vec<_>, Vec<_>) = item_enum | ||
.variants | ||
.into_iter() | ||
.partition(|variant| matches!(variant.fields, Fields::Unit)); | ||
|
||
let mapping_entries = unit.into_iter().map(generate_mapping_entry); | ||
|
||
let mapping_constructors = mapping_entries.clone().map(|entry| { | ||
let key = entry.key; | ||
let description = entry.description; | ||
|
||
quote! { | ||
::input_mapping_common::MappingEntry { | ||
key: #key.convert(), | ||
description: (#description).to_string(), | ||
} | ||
} | ||
}); | ||
|
||
let mapping_matchers = mapping_entries.map(|entry| { | ||
let key = entry.key; | ||
let event = entry.event; | ||
|
||
quote! { | ||
::ratatui::crossterm::event::Event::Key(::ratatui::crossterm::event::KeyEvent { | ||
kind: ::ratatui::crossterm::event::KeyEventKind::Press, | ||
code, | ||
.. | ||
}) if code == #key.convert() => ::std::option::Option::Some(Self:: #event) | ||
} | ||
}); | ||
|
||
let get_mapping_flattening = to_be_flattened.iter().map(|variant| { | ||
let field_ty = match &variant.fields { | ||
Fields::Unnamed(fields) => { | ||
let fields = &fields.unnamed; | ||
if fields.len() != 1 { | ||
panic!("Multiple unnamed fields are not supported"); | ||
} | ||
&fields.first().expect("fields.len() checked to be = 1").ty | ||
} | ||
Fields::Unit => panic!("Unit fields have been filtered above"), | ||
Fields::Named(_) => panic!("Named variant fields are not supported"), | ||
}; | ||
|
||
quote! { | ||
.merge(#field_ty ::get_mapping()) | ||
} | ||
}); | ||
|
||
let map_event_flattening = to_be_flattened.iter().map(|variant| { | ||
let field_ty = match &variant.fields { | ||
Fields::Unnamed(fields) => { | ||
let fields = &fields.unnamed; | ||
if fields.len() != 1 { | ||
panic!("Multiple unnamed fields are not supported"); | ||
} | ||
&fields.first().expect("fields.len() checked to be = 1").ty | ||
} | ||
Fields::Unit => panic!("Unit fields have been filtered above"), | ||
Fields::Named(_) => panic!("Named variant fields are not supported"), | ||
}; | ||
|
||
let ident = &variant.ident; | ||
|
||
quote! { | ||
.or_else(|| { | ||
#field_ty ::map_event(event).map(Self:: #ident) | ||
}) | ||
} | ||
}); | ||
|
||
let ident = item_enum.ident; | ||
|
||
quote! { | ||
impl ::input_mapping_common::InputMappingT for #ident { | ||
fn get_mapping() -> ::input_mapping_common::InputMapping { | ||
use ::input_mapping_common::KeyCodeConversions; | ||
|
||
::input_mapping_common::InputMapping { | ||
mapping: vec![ | ||
#(#mapping_constructors,)* | ||
] | ||
} | ||
|
||
#(#get_mapping_flattening)* | ||
} | ||
|
||
fn map_event(event: ::ratatui::crossterm::event::Event) -> ::std::option::Option<Self> { | ||
use ::input_mapping_common::KeyCodeConversions; | ||
|
||
match event { | ||
#(#mapping_matchers,)* | ||
_ => None, | ||
} | ||
|
||
#(#map_event_flattening)* | ||
} | ||
} | ||
} | ||
} | ||
|
||
struct MappingEntry { | ||
key: TokenStream, | ||
description: TokenStream, | ||
event: TokenStream, | ||
} | ||
|
||
fn generate_mapping_entry(variant: Variant) -> MappingEntry { | ||
let mut key: Option<TokenStream> = None; | ||
let mut description: Option<TokenStream> = None; | ||
|
||
for attr in &variant.attrs { | ||
match &attr.meta { | ||
Meta::NameValue(MetaNameValue { path, value, .. }) => { | ||
if path.is_ident("key") { | ||
if key.is_some() { | ||
panic!("Duplicate definition for attribute: key"); | ||
} | ||
|
||
key = Some(match value { | ||
Expr::Lit(lit) => match &lit.lit { | ||
Lit::Str(str) => str.value().parse().expect("Invalid expression"), | ||
Lit::Char(char) => char.to_token_stream(), | ||
_ => { | ||
unimplemented!("Values other than string or char are not supported") | ||
} | ||
}, | ||
_ => unimplemented!(), | ||
}); | ||
|
||
//key = Some(value.into_token_stream()); | ||
} else if path.is_ident("description") { | ||
if description.is_some() { | ||
panic!("Duplicate definition for attribute: description"); | ||
} | ||
|
||
description = Some(value.into_token_stream()); | ||
} | ||
} | ||
_ => continue, | ||
} | ||
} | ||
|
||
let key = key.unwrap_or_else(|| { | ||
let key = variant | ||
.ident | ||
.to_string() | ||
.chars() | ||
.next() | ||
.expect("Non-empty identifier expected") | ||
.to_ascii_lowercase(); | ||
|
||
Literal::character(key).into_token_stream() | ||
}); | ||
|
||
let description = description.unwrap_or_else(|| Literal::string("").into_token_stream()); | ||
|
||
MappingEntry { | ||
key, | ||
description, | ||
event: variant.ident.to_token_stream(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[package] | ||
name = "input-mapping-tests" | ||
version.workspace = true | ||
edition.workspace = true | ||
authors.workspace = true | ||
|
||
[dependencies] | ||
input-mapping-derive.workspace = true | ||
input-mapping-common.workspace = true | ||
|
||
ratatui.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#![cfg(test)] | ||
|
||
use std::collections::HashMap; | ||
|
||
use input_mapping_common::InputMappingT; | ||
use input_mapping_derive::InputMapping; | ||
use ratatui::crossterm::event::KeyCode; | ||
|
||
#[derive(InputMapping)] | ||
enum TestEnum { | ||
#[key = 'a'] | ||
One, | ||
|
||
#[description = "test"] | ||
Two, | ||
|
||
#[allow(dead_code)] | ||
Nested(Nested), | ||
} | ||
|
||
#[derive(InputMapping)] | ||
enum Nested { | ||
#[description = "four_test"] | ||
Four, | ||
|
||
#[key = "KeyCode::Up"] | ||
#[description = "up"] | ||
Five, | ||
|
||
Six, | ||
} | ||
|
||
#[test] | ||
fn test_input_mapping_generated_as_expected() { | ||
let mapping = TestEnum::get_mapping(); | ||
let mapping: HashMap<_, _> = mapping | ||
.mapping | ||
.into_iter() | ||
.map(|map| (map.key, map.description)) | ||
.collect(); | ||
|
||
assert_eq!(mapping.len(), 5); | ||
assert_eq!( | ||
mapping.get(&KeyCode::Char('f')), | ||
Some(&"four_test".to_string()) | ||
); | ||
assert_eq!(mapping.get(&KeyCode::Char('t')), Some(&"test".to_string())); | ||
assert_eq!(mapping.get(&KeyCode::Char('s')), Some(&"".to_string())); | ||
assert_eq!(mapping.get(&KeyCode::Up), Some(&"up".to_string())); | ||
} |
Oops, something went wrong.