Skip to content

Commit 3361917

Browse files
authored
Add a procedural macro for defining layouts (#54)
1 parent 45b8810 commit 3361917

7 files changed

Lines changed: 345 additions & 7 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/Cargo.lock
2-
/target
1+
Cargo.lock
2+
target/
33
**/*.rs.bk
44
*~

CHANGELOG.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22

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

67
Breaking changes:
7-
* Update to generic_array 0.14, that is exposed in matrix. The update
8+
* Update to generic_array 0.14, which is exposed in matrix. The update
89
should be transparent.
910
* `Action::HoldTap` now takes a configuration for different behaviors.
1011
* `Action::HoldTap` now takes the `tap_hold_interval` field. Not
1112
implemented yet.
1213
* `Action` is now generic, for the `Action::Custom(T)` variant,
13-
allowing custom action to be handled outside of keyberon. This
14-
functionality can be used to drive non keyboard actions, as reset
15-
the microcontroller, drive leds (for backlight or underglow for
16-
example), manage a mouse emulation, or any other ideas you can
14+
allowing custom actions to be handled outside of keyberon. This
15+
functionality can be used to drive non keyboard actions, such as resetting
16+
the microcontroller, driving leds (for backlight or underglow for
17+
example), managing a mouse emulation, or any other ideas you can
1718
have. As there is a default value for the type parameter, the update
1819
should be transparent.
1920
* Rename MeidaCoffee in MediaCoffee to fix typo.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ license = "MIT"
1212
readme = "README.md"
1313

1414
[dependencies]
15+
keyberon-macros = { version = "0.1.0", path = "./keyberon-macros" }
1516
either = { version = "1.6", default-features = false }
1617
generic-array = "0.14"
1718
embedded-hal = { version = "0.2", features = ["unproven"] }

keyberon-macros/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "keyberon-macros"
3+
version = "0.1.0"
4+
authors = ["Antoni Simka <antonisimka.8@gmail.com>"]
5+
edition = "2018"
6+
7+
[lib]
8+
proc-macro = true
9+
10+
[dependencies]
11+
proc-macro-error = "1.0.4"
12+
proc-macro2 = "1.0"
13+
quote = "1.0"
14+
15+
[dev-dependencies]
16+
keyberon = { path = "../" }

keyberon-macros/src/lib.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
extern crate proc_macro;
2+
use proc_macro2::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree};
3+
use proc_macro_error::proc_macro_error;
4+
use proc_macro_error::{abort, emit_error};
5+
use quote::quote;
6+
7+
#[proc_macro_error]
8+
#[proc_macro]
9+
pub fn layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
10+
let input: TokenStream = input.into();
11+
12+
let mut out = TokenStream::new();
13+
14+
let mut inside = TokenStream::new();
15+
16+
for t in input {
17+
match t {
18+
TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
19+
let layer = parse_layer(g.stream());
20+
inside.extend(quote! {
21+
&[#layer],
22+
});
23+
}
24+
_ => abort!(t, "Invalid token, expected layer: {{ ... }}"),
25+
}
26+
}
27+
28+
let all: TokenStream = quote! { &[#inside] };
29+
out.extend(all);
30+
31+
out.into()
32+
}
33+
34+
fn parse_layer(input: TokenStream) -> TokenStream {
35+
let mut out = TokenStream::new();
36+
for t in input {
37+
match t {
38+
TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => {
39+
let row = parse_row(g.stream());
40+
out.extend(quote! {
41+
&[#row],
42+
});
43+
}
44+
TokenTree::Punct(p) if p.as_char() == ',' => (),
45+
_ => abort!(t, "Invalid token, expected row: [ ... ]"),
46+
}
47+
}
48+
out
49+
}
50+
51+
fn parse_row(input: TokenStream) -> TokenStream {
52+
let mut out = TokenStream::new();
53+
for t in input {
54+
match t {
55+
TokenTree::Ident(i) => match i.to_string().as_str() {
56+
"n" => out.extend(quote! { keyberon::action::Action::NoOp, }),
57+
"t" => out.extend(quote! { keyberon::action::Action::Trans, }),
58+
_ => out.extend(quote! {
59+
keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i),
60+
}),
61+
},
62+
TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut out),
63+
TokenTree::Literal(l) => literal_to_keycode(&l, &mut out),
64+
TokenTree::Group(g) => parse_group(&g, &mut out),
65+
}
66+
}
67+
out
68+
}
69+
70+
fn parse_group(g: &Group, out: &mut TokenStream) {
71+
match g.delimiter() {
72+
// Handle empty groups
73+
Delimiter::Parenthesis if g.stream().is_empty() => {
74+
emit_error!(g, "Expected a layer number in layer switch"; help = "To create a parenthesis keycode, enclose it in apostrophes: '('")
75+
}
76+
Delimiter::Brace if g.stream().is_empty() => {
77+
emit_error!(g, "Expected an action - group cannot be empty"; help = "To create a brace keycode, enclose it in apostrophes: '{'")
78+
}
79+
Delimiter::Bracket if g.stream().is_empty() => {
80+
emit_error!(g, "Expected keycodes - keycode group cannot be empty"; help = "To create a bracket keycode, enclose it in apostrophes: '['")
81+
}
82+
83+
// Momentary layer switch (Action::Layer)
84+
Delimiter::Parenthesis => {
85+
let tokens = g.stream();
86+
out.extend(quote! { keyberon::action::Action::Layer(#tokens), });
87+
}
88+
// Pass the expression unchanged (adding a comma after it)
89+
Delimiter::Brace => out.extend(g.stream().into_iter().chain(TokenStream::from(
90+
TokenTree::Punct(Punct::new(',', Spacing::Alone)),
91+
))),
92+
// Multiple keycodes (Action::MultipleKeyCodes)
93+
Delimiter::Bracket => parse_keycode_group(g.stream(), out),
94+
95+
// Is this reachable?
96+
Delimiter::None => emit_error!(g, "Unexpected group"),
97+
}
98+
}
99+
100+
fn parse_keycode_group(input: TokenStream, out: &mut TokenStream) {
101+
let mut inner = TokenStream::new();
102+
for t in input {
103+
match t {
104+
TokenTree::Ident(i) => inner.extend(quote! {
105+
keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i),
106+
}),
107+
TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut inner),
108+
TokenTree::Literal(l) => literal_to_keycode(&l, &mut inner),
109+
TokenTree::Group(g) => parse_group(&g, &mut inner),
110+
}
111+
}
112+
out.extend(quote! { keyberon::action::Action::MultipleActions(&[#inner]) });
113+
}
114+
115+
fn punctuation_to_keycode(p: &Punct, out: &mut TokenStream) {
116+
match p.as_char() {
117+
// Normal punctuation
118+
'-' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Minus), }),
119+
'=' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Equal), }),
120+
';' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::SColon), }),
121+
',' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Comma), }),
122+
'.' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Dot), }),
123+
'/' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Slash), }),
124+
125+
// Shifted punctuation
126+
'!' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb1]), }),
127+
'@' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb2]), }),
128+
'#' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb3]), }),
129+
'$' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb4]), }),
130+
'%' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb5]), }),
131+
'^' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb6]), }),
132+
'&' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb7]), }),
133+
'*' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb8]), }),
134+
'_' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus]), }),
135+
'+' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Equal]), }),
136+
'|' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Bslash]), }),
137+
'~' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Grave]), }),
138+
'<' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Comma]), }),
139+
'>' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Dot]), }),
140+
'?' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Slash]), }),
141+
// Is this reachable?
142+
_ => emit_error!(p, "Punctuation could not be parsed as a keycode")
143+
}
144+
}
145+
146+
fn literal_to_keycode(l: &Literal, out: &mut TokenStream) {
147+
//let repr = l.to_string();
148+
match l.to_string().as_str() {
149+
"1" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb1), }),
150+
"2" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb2), }),
151+
"3" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb3), }),
152+
"4" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb4), }),
153+
"5" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb5), }),
154+
"6" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb6), }),
155+
"7" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb7), }),
156+
"8" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb8), }),
157+
"9" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb9), }),
158+
"0" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb0), }),
159+
160+
// Char literals; mostly punctuation which can't be properly tokenized alone
161+
r#"'\''"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Quote), }),
162+
r#"'\\'"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Bslash), }),
163+
// Shifted characters
164+
"'['" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::LBracket), }),
165+
"']'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::RBracket), }),
166+
"'`'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Grave), }),
167+
"'\"'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Quote]), }),
168+
"'('" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb9]), }),
169+
"')'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb0]), }),
170+
"'{'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::LBracket]), }),
171+
"'}'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::RBracket]), }),
172+
"'_'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus]), }),
173+
174+
s if s.starts_with('\'') => emit_error!(l, "Literal could not be parsed as a keycode"; help = "Maybe try without quotes?"),
175+
176+
s if s.starts_with('\"') => {
177+
if s.len() == 3 {
178+
emit_error!(l, "Typing strings on key press is not yet supported"; help = "Did you mean to use apostrophes instead of quotes?");
179+
} else {
180+
emit_error!(l, "Typing strings on key press is not yet supported");
181+
}
182+
}
183+
_ => emit_error!(l, "Literal could not be parsed as a keycode")
184+
}
185+
}

keyberon-macros/tests/mod.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
extern crate keyberon_macros;
2+
use keyberon::action::{k, l, m, Action, Action::*, HoldTapConfig};
3+
use keyberon::key_code::KeyCode::*;
4+
use keyberon_macros::layout;
5+
6+
#[test]
7+
fn test_layout_equality() {
8+
macro_rules! s {
9+
($k:expr) => {
10+
m(&[LShift, $k])
11+
};
12+
}
13+
14+
static S_ENTER: Action = Action::HoldTap {
15+
timeout: 280,
16+
hold: &Action::KeyCode(RShift),
17+
tap: &Action::KeyCode(Enter),
18+
config: HoldTapConfig::PermissiveHold,
19+
tap_hold_interval: 0,
20+
};
21+
22+
#[rustfmt::skip]
23+
pub static LAYERS_OLD: keyberon::layout::Layers = &[
24+
&[
25+
&[k(Tab), k(Q), k(W), k(E), k(R), k(T), k(Y), k(U), k(I), k(O), k(P), k(BSpace)],
26+
&[k(LCtrl), k(A), k(S), k(D), k(F), k(G), k(H), k(J), k(K), k(L), k(SColon), k(Quote) ],
27+
&[k(LShift), k(Z), k(X), k(C), k(V), k(B), k(N), k(M), k(Comma), k(Dot), k(Slash), k(Escape)],
28+
&[NoOp, NoOp, k(LGui), l(1), k(Space), k(Escape), k(BSpace), S_ENTER, l(1), k(RAlt), NoOp, NoOp],
29+
],
30+
&[
31+
&[k(Tab), k(Kb1), k(Kb2), k(Kb3), k(Kb4), k(Kb5), k(Kb6), k(Kb7), k(Kb8), k(Kb9), k(Kb0), k(BSpace)],
32+
&[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)])],
33+
&[k(LShift), NoOp, NoOp, NoOp, NoOp, NoOp, k(Left), k(Down), k(Up), k(Right), NoOp, s!(Grave)],
34+
&[NoOp, NoOp, k(LGui), Trans, Trans, Trans, Trans, Trans, Trans, k(RAlt), NoOp, NoOp],
35+
],
36+
];
37+
38+
pub static LAYERS: keyberon::layout::Layers = layout! {
39+
{
40+
[ Tab Q W E R T Y U I O P BSpace ]
41+
[ LCtrl A S D F G H J K L ; Quote ]
42+
[ LShift Z X C V B N M , . / Escape ]
43+
[ n n LGui (1) Space Escape BSpace {S_ENTER} (1) RAlt n n ]
44+
}
45+
{
46+
[ Tab 1 2 3 4 5 6 7 8 9 0 BSpace ]
47+
[ LCtrl ! @ # $ % ^ & * '(' ')' [LCtrl '`'] ]
48+
[ LShift n n n n n Left Down Up Right n ~ ]
49+
[ n n LGui t t t t t t RAlt n n ]
50+
}
51+
};
52+
53+
assert_eq!(LAYERS, LAYERS_OLD);
54+
use std::mem::size_of_val;
55+
assert_eq!(size_of_val(LAYERS), size_of_val(LAYERS_OLD))
56+
}
57+
58+
#[test]
59+
fn test_nesting() {
60+
static A: keyberon::layout::Layers = layout! {
61+
{
62+
[{k(D)} [(5) [C {k(D)}]]]
63+
}
64+
};
65+
static B: keyberon::layout::Layers = &[&[&[
66+
k(D),
67+
Action::MultipleActions(&[Action::Layer(5), Action::MultipleActions(&[k(C), k(D)])]),
68+
]]];
69+
assert_eq!(A, B);
70+
}
71+
72+
#[test]
73+
fn test_layer_switch() {
74+
static A: keyberon::layout::Layers = layout! {
75+
{
76+
[(0xa), (0b0110), (b'a' as usize), (1 + 8 & 32), ([4,5][0])]
77+
}
78+
};
79+
}
80+
81+
#[test]
82+
fn test_escapes() {
83+
static A: keyberon::layout::Layers = layout! {
84+
{
85+
['\\' '\'']
86+
}
87+
};
88+
static B: keyberon::layout::Layers = &[&[&[k(Bslash), k(Quote)]]];
89+
assert_eq!(A, B);
90+
}

src/layout.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
//! Layout management.
22
3+
/// A procedural macro to generate [Layers](type.Layers.html)
4+
/// ## Syntax
5+
/// Items inside the macro are converted to Actions as such:
6+
/// - [`Action::KeyCode`]: Idents are automatically understood as keycodes: `A`, `RCtrl`, `Space`
7+
/// - Punctuation, numbers and other literals that aren't special to the rust parser are converted
8+
/// to KeyCodes as well: `,` becomes `KeyCode::Commma`, `2` becomes `KeyCode::Kb2`, `/` becomes `KeyCode::Slash`
9+
/// - Characters which require shifted keys are converted to `Action::MultipleKeyCodes(&[LShift, <character>])`:
10+
/// `!` becomes `Action::MultipleKeyCodes(&[LShift, Kb1])` etc
11+
/// - Characters special to the rust parser (parentheses, brackets, braces, quotes, apostrophes, underscores, backslashes and backticks)
12+
/// left alone cause parsing errors and as such have to be enclosed by apostrophes: `'['` becomes `KeyCode::LBracket`,
13+
/// `'\''` becomes `KeyCode::Quote`, `'\\'` becomes `KeyCode::BSlash`
14+
/// - [`Action::NoOp`]: Lowercase `n`
15+
/// - [`Action::Trans`]: Lowercase `t`
16+
/// - [`Action::Layer`]: A number in parentheses: `(1)`, `(4 - 2)`, `(0x4u8 as usize)`
17+
/// - [`Action::MultipleActions`]: Actions in brackets: `[LCtrl S]`, `[LAlt LCtrl C]`, `[(2) B {Action::NoOp}]`
18+
/// - Other `Action`s: anything in braces (`{}`) is copied unchanged to the final layout - `{ Action::Custom(42) }`
19+
/// simply becomes `Action::Custom(42)`
20+
///
21+
/// **Important note**: comma (`,`) is a keycode on its own, and can't be used to separate keycodes as one would have
22+
/// to do when not using a macro.
23+
///
24+
/// ## Usage example:
25+
/// Example layout for a 4x12 split keyboard:
26+
/// ```
27+
/// use keyberon::action::Action;
28+
/// static DLAYER: Action = Action::DefaultLayer(5);
29+
///
30+
/// pub static LAYERS: keyberon::layout::Layers = keyberon::layout::layout! {
31+
/// {
32+
/// [ Tab Q W E R T Y U I O P BSpace ]
33+
/// [ LCtrl A S D F G H J K L ; Quote ]
34+
/// [ LShift Z X C V B N M , . / Escape ]
35+
/// [ n n LGui {DLAYER} Space Escape BSpace Enter (1) RAlt n n ]
36+
/// }
37+
/// {
38+
/// [ Tab 1 2 3 4 5 6 7 8 9 0 BSpace ]
39+
/// [ LCtrl ! @ # $ % ^ & * '(' ')' - = ]
40+
/// [ LShift n n n n n n n n n n [LAlt A]]
41+
/// [ n n LGui (2) t t t t t RAlt n n ]
42+
/// }
43+
/// // ...
44+
/// };
45+
/// ```
46+
pub use keyberon_macros::*;
47+
348
use crate::action::{Action, HoldTapConfig};
449
use crate::key_code::KeyCode;
550
use arraydeque::ArrayDeque;

0 commit comments

Comments
 (0)