Skip to content
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" }
Comment thread
Skelebot marked this conversation as resolved.
Outdated
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 <antonisimka.8@gmail.com>"]
edition = "2018"

[lib]
proc-macro = true

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

[dev-dependencies]
keyberon = { path = "../" }
Comment thread
Skelebot marked this conversation as resolved.
Outdated
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 => {
Comment thread
Skelebot marked this conversation as resolved.
Outdated
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