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 3 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
*~
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]
layout-macro = { version = "0.1.0", path = "./layout-macro" }
Skelebot marked this conversation as resolved.
Show resolved Hide resolved
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 layout-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "layout-macro"
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 = "../" }
Skelebot marked this conversation as resolved.
Show resolved Hide resolved
192 changes: 192 additions & 0 deletions layout-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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 repr.chars().next().unwrap() {
'0'..='9' if repr.len() == 1 => {
Skelebot marked this conversation as resolved.
Show resolved Hide resolved
match repr.chars().next().unwrap() {
'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), }),
_ => unreachable!()
}
}
// Char literals; mostly punctuation which can't be properly tokenized alone
'\'' => {
match repr.chars().nth(1).unwrap() {
'\\' => match repr.chars().nth(2).unwrap() {
'\\' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Bslash), }),
'\'' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Quote), }),
_ => emit_error!(l, "Literal could not be parsed as a keycode"; help = "Maybe try without quotes?")
}
'[' => 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]), }),
_ => emit_error!(l, "Literal could not be parsed as a keycode"; help = "Maybe try without quotes?")
}
}
'"' => if repr.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");
}
// Is this reachable?
_ => emit_error!(l, "Literal could not be parsed as a keycode")
}
}
79 changes: 79 additions & 0 deletions layout-macro/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
extern crate layout_macro;
use keyberon::action::{k, l, m, Action, Action::*, HoldTapConfig};
use keyberon::key_code::KeyCode::*;
use layout_macro::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])]
}
};
}
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 layout_macro::layout;

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