Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a procedural macro for defining layouts #54

Merged
merged 4 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/Cargo.lock
/target
Cargo.lock
target/
**/*.rs.bk
*~
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

* New Keyboard::leds_mut function for getting underlying leds object.
* Made Layout::current_layer public for getting current active layer.
* Added a procedural macro for defining layouts (`keyberon::layout::layout`)

Breaking changes:
* Update to generic_array 0.14, that is exposed in matrix. The update
* Update to generic_array 0.14, which is exposed in matrix. The update
should be transparent.
* `Action::HoldTap` now takes a configuration for different behaviors.
* `Action::HoldTap` now takes the `tap_hold_interval` field. Not
implemented yet.
* `Action` is now generic, for the `Action::Custom(T)` variant,
allowing custom action to be handled outside of keyberon. This
functionality can be used to drive non keyboard actions, as reset
the microcontroller, drive leds (for backlight or underglow for
example), manage a mouse emulation, or any other ideas you can
allowing custom actions to be handled outside of keyberon. This
functionality can be used to drive non keyboard actions, such as resetting
the microcontroller, driving leds (for backlight or underglow for
example), managing a mouse emulation, or any other ideas you can
have. As there is a default value for the type parameter, the update
should be transparent.
* Rename MeidaCoffee in MediaCoffee to fix typo.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ license = "MIT"
readme = "README.md"

[dependencies]
keyberon-macros = { version = "0.1.0", path = "./keyberon-macros" }
either = { version = "1.6", default-features = false }
generic-array = "0.14"
embedded-hal = { version = "0.2", features = ["unproven"] }
Expand Down
16 changes: 16 additions & 0 deletions keyberon-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "keyberon-macros"
version = "0.1.0"
authors = ["Antoni Simka <[email protected]>"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]
proc-macro-error = "1.0.4"
proc-macro2 = "1.0"
quote = "1.0"

[dev-dependencies]
keyberon = { path = "../" }
185 changes: 185 additions & 0 deletions keyberon-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
extern crate proc_macro;
use proc_macro2::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree};
use proc_macro_error::proc_macro_error;
use proc_macro_error::{abort, emit_error};
use quote::quote;

#[proc_macro_error]
#[proc_macro]
pub fn layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: TokenStream = input.into();

let mut out = TokenStream::new();

let mut inside = TokenStream::new();

for t in input {
match t {
TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
let layer = parse_layer(g.stream());
inside.extend(quote! {
&[#layer],
});
}
_ => abort!(t, "Invalid token, expected layer: {{ ... }}"),
}
}

let all: TokenStream = quote! { &[#inside] };
out.extend(all);

out.into()
}

fn parse_layer(input: TokenStream) -> TokenStream {
let mut out = TokenStream::new();
for t in input {
match t {
TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => {
let row = parse_row(g.stream());
out.extend(quote! {
&[#row],
});
}
TokenTree::Punct(p) if p.as_char() == ',' => (),
_ => abort!(t, "Invalid token, expected row: [ ... ]"),
}
}
out
}

fn parse_row(input: TokenStream) -> TokenStream {
let mut out = TokenStream::new();
for t in input {
match t {
TokenTree::Ident(i) => match i.to_string().as_str() {
"n" => out.extend(quote! { keyberon::action::Action::NoOp, }),
"t" => out.extend(quote! { keyberon::action::Action::Trans, }),
_ => out.extend(quote! {
keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i),
}),
},
TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut out),
TokenTree::Literal(l) => literal_to_keycode(&l, &mut out),
TokenTree::Group(g) => parse_group(&g, &mut out),
}
}
out
}

fn parse_group(g: &Group, out: &mut TokenStream) {
match g.delimiter() {
// Handle empty groups
Delimiter::Parenthesis if g.stream().is_empty() => {
emit_error!(g, "Expected a layer number in layer switch"; help = "To create a parenthesis keycode, enclose it in apostrophes: '('")
}
Delimiter::Brace if g.stream().is_empty() => {
emit_error!(g, "Expected an action - group cannot be empty"; help = "To create a brace keycode, enclose it in apostrophes: '{'")
}
Delimiter::Bracket if g.stream().is_empty() => {
emit_error!(g, "Expected keycodes - keycode group cannot be empty"; help = "To create a bracket keycode, enclose it in apostrophes: '['")
}

// Momentary layer switch (Action::Layer)
Delimiter::Parenthesis => {
let tokens = g.stream();
out.extend(quote! { keyberon::action::Action::Layer(#tokens), });
}
// Pass the expression unchanged (adding a comma after it)
Delimiter::Brace => out.extend(g.stream().into_iter().chain(TokenStream::from(
TokenTree::Punct(Punct::new(',', Spacing::Alone)),
))),
// Multiple keycodes (Action::MultipleKeyCodes)
Delimiter::Bracket => parse_keycode_group(g.stream(), out),

// Is this reachable?
Delimiter::None => emit_error!(g, "Unexpected group"),
}
}

fn parse_keycode_group(input: TokenStream, out: &mut TokenStream) {
let mut inner = TokenStream::new();
for t in input {
match t {
TokenTree::Ident(i) => inner.extend(quote! {
keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i),
}),
TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut inner),
TokenTree::Literal(l) => literal_to_keycode(&l, &mut inner),
TokenTree::Group(g) => parse_group(&g, &mut inner),
}
}
out.extend(quote! { keyberon::action::Action::MultipleActions(&[#inner]) });
}

fn punctuation_to_keycode(p: &Punct, out: &mut TokenStream) {
match p.as_char() {
// Normal punctuation
'-' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Minus), }),
'=' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Equal), }),
';' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::SColon), }),
',' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Comma), }),
'.' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Dot), }),
'/' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Slash), }),

// Shifted punctuation
'!' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb1]), }),
'@' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb2]), }),
'#' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb3]), }),
'$' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb4]), }),
'%' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb5]), }),
'^' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb6]), }),
'&' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb7]), }),
'*' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb8]), }),
'_' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus]), }),
'+' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Equal]), }),
'|' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Bslash]), }),
'~' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Grave]), }),
'<' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Comma]), }),
'>' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Dot]), }),
'?' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Slash]), }),
// Is this reachable?
_ => emit_error!(p, "Punctuation could not be parsed as a keycode")
}
}

fn literal_to_keycode(l: &Literal, out: &mut TokenStream) {
//let repr = l.to_string();
match l.to_string().as_str() {
"1" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb1), }),
"2" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb2), }),
"3" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb3), }),
"4" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb4), }),
"5" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb5), }),
"6" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb6), }),
"7" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb7), }),
"8" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb8), }),
"9" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb9), }),
"0" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb0), }),

// Char literals; mostly punctuation which can't be properly tokenized alone
r#"'\''"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Quote), }),
r#"'\\'"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Bslash), }),
// Shifted characters
"'['" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::LBracket), }),
"']'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::RBracket), }),
"'`'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Grave), }),
"'\"'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Quote]), }),
"'('" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb9]), }),
"')'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb0]), }),
"'{'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::LBracket]), }),
"'}'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::RBracket]), }),
"'_'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus]), }),

s if s.starts_with('\'') => emit_error!(l, "Literal could not be parsed as a keycode"; help = "Maybe try without quotes?"),

s if s.starts_with('\"') => {
if s.len() == 3 {
emit_error!(l, "Typing strings on key press is not yet supported"; help = "Did you mean to use apostrophes instead of quotes?");
} else {
emit_error!(l, "Typing strings on key press is not yet supported");
}
}
_ => emit_error!(l, "Literal could not be parsed as a keycode")
}
}
90 changes: 90 additions & 0 deletions keyberon-macros/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
extern crate keyberon_macros;
use keyberon::action::{k, l, m, Action, Action::*, HoldTapConfig};
use keyberon::key_code::KeyCode::*;
use keyberon_macros::layout;

#[test]
fn test_layout_equality() {
macro_rules! s {
($k:expr) => {
m(&[LShift, $k])
};
}

static S_ENTER: Action = Action::HoldTap {
timeout: 280,
hold: &Action::KeyCode(RShift),
tap: &Action::KeyCode(Enter),
config: HoldTapConfig::PermissiveHold,
tap_hold_interval: 0,
};

#[rustfmt::skip]
pub static LAYERS_OLD: keyberon::layout::Layers = &[
&[
&[k(Tab), k(Q), k(W), k(E), k(R), k(T), k(Y), k(U), k(I), k(O), k(P), k(BSpace)],
&[k(LCtrl), k(A), k(S), k(D), k(F), k(G), k(H), k(J), k(K), k(L), k(SColon), k(Quote) ],
&[k(LShift), k(Z), k(X), k(C), k(V), k(B), k(N), k(M), k(Comma), k(Dot), k(Slash), k(Escape)],
&[NoOp, NoOp, k(LGui), l(1), k(Space), k(Escape), k(BSpace), S_ENTER, l(1), k(RAlt), NoOp, NoOp],
],
&[
&[k(Tab), k(Kb1), k(Kb2), k(Kb3), k(Kb4), k(Kb5), k(Kb6), k(Kb7), k(Kb8), k(Kb9), k(Kb0), k(BSpace)],
&[k(LCtrl), s!(Kb1), s!(Kb2), s!(Kb3), s!(Kb4), s!(Kb5), s!(Kb6), s!(Kb7), s!(Kb8), s!(Kb9), s!(Kb0), MultipleActions(&[k(LCtrl), k(Grave)])],
&[k(LShift), NoOp, NoOp, NoOp, NoOp, NoOp, k(Left), k(Down), k(Up), k(Right), NoOp, s!(Grave)],
&[NoOp, NoOp, k(LGui), Trans, Trans, Trans, Trans, Trans, Trans, k(RAlt), NoOp, NoOp],
],
];

pub static LAYERS: keyberon::layout::Layers = layout! {
{
[ Tab Q W E R T Y U I O P BSpace ]
[ LCtrl A S D F G H J K L ; Quote ]
[ LShift Z X C V B N M , . / Escape ]
[ n n LGui (1) Space Escape BSpace {S_ENTER} (1) RAlt n n ]
}
{
[ Tab 1 2 3 4 5 6 7 8 9 0 BSpace ]
[ LCtrl ! @ # $ % ^ & * '(' ')' [LCtrl '`'] ]
[ LShift n n n n n Left Down Up Right n ~ ]
[ n n LGui t t t t t t RAlt n n ]
}
};

assert_eq!(LAYERS, LAYERS_OLD);
use std::mem::size_of_val;
assert_eq!(size_of_val(LAYERS), size_of_val(LAYERS_OLD))
}

#[test]
fn test_nesting() {
static A: keyberon::layout::Layers = layout! {
{
[{k(D)} [(5) [C {k(D)}]]]
}
};
static B: keyberon::layout::Layers = &[&[&[
k(D),
Action::MultipleActions(&[Action::Layer(5), Action::MultipleActions(&[k(C), k(D)])]),
]]];
assert_eq!(A, B);
}

#[test]
fn test_layer_switch() {
static A: keyberon::layout::Layers = layout! {
{
[(0xa), (0b0110), (b'a' as usize), (1 + 8 & 32), ([4,5][0])]
}
};
}

#[test]
fn test_escapes() {
static A: keyberon::layout::Layers = layout! {
{
['\\' '\'']
}
};
static B: keyberon::layout::Layers = &[&[&[k(Bslash), k(Quote)]]];
assert_eq!(A, B);
}
45 changes: 45 additions & 0 deletions src/layout.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
//! Layout management.

/// A procedural macro to generate [Layers](type.Layers.html)
/// ## Syntax
/// Items inside the macro are converted to Actions as such:
/// - [`Action::KeyCode`]: Idents are automatically understood as keycodes: `A`, `RCtrl`, `Space`
/// - Punctuation, numbers and other literals that aren't special to the rust parser are converted
/// to KeyCodes as well: `,` becomes `KeyCode::Commma`, `2` becomes `KeyCode::Kb2`, `/` becomes `KeyCode::Slash`
/// - Characters which require shifted keys are converted to `Action::MultipleKeyCodes(&[LShift, <character>])`:
/// `!` becomes `Action::MultipleKeyCodes(&[LShift, Kb1])` etc
/// - Characters special to the rust parser (parentheses, brackets, braces, quotes, apostrophes, underscores, backslashes and backticks)
/// left alone cause parsing errors and as such have to be enclosed by apostrophes: `'['` becomes `KeyCode::LBracket`,
/// `'\''` becomes `KeyCode::Quote`, `'\\'` becomes `KeyCode::BSlash`
/// - [`Action::NoOp`]: Lowercase `n`
/// - [`Action::Trans`]: Lowercase `t`
/// - [`Action::Layer`]: A number in parentheses: `(1)`, `(4 - 2)`, `(0x4u8 as usize)`
/// - [`Action::MultipleActions`]: Actions in brackets: `[LCtrl S]`, `[LAlt LCtrl C]`, `[(2) B {Action::NoOp}]`
/// - Other `Action`s: anything in braces (`{}`) is copied unchanged to the final layout - `{ Action::Custom(42) }`
/// simply becomes `Action::Custom(42)`
///
/// **Important note**: comma (`,`) is a keycode on its own, and can't be used to separate keycodes as one would have
/// to do when not using a macro.
///
/// ## Usage example:
/// Example layout for a 4x12 split keyboard:
/// ```
/// use keyberon::action::Action;
/// static DLAYER: Action = Action::DefaultLayer(5);
///
/// pub static LAYERS: keyberon::layout::Layers = keyberon::layout::layout! {
/// {
/// [ Tab Q W E R T Y U I O P BSpace ]
/// [ LCtrl A S D F G H J K L ; Quote ]
/// [ LShift Z X C V B N M , . / Escape ]
/// [ n n LGui {DLAYER} Space Escape BSpace Enter (1) RAlt n n ]
/// }
/// {
/// [ Tab 1 2 3 4 5 6 7 8 9 0 BSpace ]
/// [ LCtrl ! @ # $ % ^ & * '(' ')' - = ]
/// [ LShift n n n n n n n n n n [LAlt A]]
/// [ n n LGui (2) t t t t t RAlt n n ]
/// }
/// // ...
/// };
/// ```
pub use keyberon_macros::*;

use crate::action::{Action, HoldTapConfig};
use crate::key_code::KeyCode;
use arraydeque::ArrayDeque;
Expand Down