kll_macros/lib.rs
1// Copyright 2021 Jacob Alexander
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8use proc_macro::{TokenStream, TokenTree};
9use std::iter::FromIterator;
10
11/// Takes a list of sequences of combos and turns it into a u8 array
12/// that can be stored in memory as a contiguous piece of data.
13/// This is necessary to store the trigger guide independently of rust compilation.
14///
15/// ```
16/// use kll_core::{Capability, CapabilityState, TriggerCondition, trigger};
17///
18/// const TRIGGER_GUIDES: &'static [u8] = kll_macros::trigger_guide!(
19/// [[
20/// TriggerCondition::Switch {
21/// state: trigger::Phro::Hold,
22/// index: 6,
23/// loop_condition_index: 0,
24/// },
25/// TriggerCondition::Switch {
26/// state: trigger::Phro::Hold,
27/// index: 7,
28/// loop_condition_index: 0,
29/// },
30/// ]],
31/// [[
32/// TriggerCondition::Switch {
33/// state: trigger::Phro::Hold,
34/// index: 6,
35/// loop_condition_index: 0,
36/// },
37/// ]],
38/// [[
39/// TriggerCondition::Layer {
40/// state: trigger::LayerState::ShiftActivate,
41/// layer: 3,
42/// loop_condition_index: 0,
43/// },
44/// ]],
45/// [[
46/// TriggerCondition::AnalogDistance {
47/// reserved: 0,
48/// index: 8,
49/// val: 1500,
50/// },
51/// ]],
52/// );
53/// ```
54#[proc_macro]
55pub fn trigger_guide(input: TokenStream) -> TokenStream {
56 let mut output: Vec<String> = vec!["unsafe { &[".to_string()];
57
58 for sequence in input {
59 match sequence {
60 TokenTree::Group(sequence) => {
61 for combo in sequence.stream() {
62 match combo {
63 TokenTree::Group(combo) => {
64 let mut combo_output: Vec<String> = Vec::new();
65 let mut prefix_output: Vec<String> = Vec::new();
66 let mut elem_count = 0;
67 // Largest enum is 6 bytes
68 // Adjusts the size depending on the enum
69 let mut byte_count = 0;
70 let byte_max_count = 6;
71 for elem in combo.stream() {
72 // Detect TriggerCondition
73 match elem.clone() {
74 TokenTree::Ident(ident) => {
75 // Check for hardcoded byte count
76 // NOTE: It may be possible to use std::mem::size_of
77 // if we make a new struct crate that this macro
78 // crate can depend on. Determining byte count is the
79 // most important feature of this macro. This is
80 // needed to prevent undefined struct memory accesses
81 // when building the const u8 array.
82 match ident.to_string().as_str() {
83 "TriggerCondition" => {
84 // New element
85 elem_count += 1;
86 prefix_output = Vec::new();
87 }
88 "Switch" | "Animation" => {
89 byte_count = 6;
90 }
91 "AnalogDistance" | "AnalogVelocity"
92 | "AnalogAcceleration" | "AnalogJerk" => {
93 byte_count = 6;
94 }
95 "HidLed" | "Layer" | "Rotation" => {
96 byte_count = 5;
97 }
98 "Sleep" | "Resume" | "Inactive" | "Active" => {
99 byte_count = 4;
100 }
101 _ => {
102 panic!("Unknown elem ident: {:?}", ident);
103 }
104 }
105 }
106 TokenTree::Punct(_) => {}
107 TokenTree::Group(group) => {
108 // Finished element, prepare additions
109 for n in 0..byte_count {
110 combo_output.append(&mut prefix_output.clone());
111 combo_output.push(group.to_string());
112 combo_output.push(".bytes()[".to_string());
113 combo_output.push(n.to_string());
114 combo_output.push("],".to_string());
115 }
116 // Fill empty bytes (to prevent undefined struct access)
117 for _ in byte_count..byte_max_count {
118 combo_output.push("0,".to_string());
119 }
120 }
121 _ => {}
122 }
123 prefix_output.push(elem.to_string());
124 }
125
126 // Add combo element count
127 output.push(elem_count.to_string());
128 output.push(",".to_string());
129 // Add combo
130 output.append(&mut combo_output);
131 }
132 TokenTree::Punct(_) => {}
133 _ => {
134 panic!("Invalid combo element: {:?}", combo);
135 }
136 }
137 }
138 }
139 TokenTree::Punct(_) => {}
140 _ => {
141 panic!("Invalid sequence element: {:?}", sequence);
142 }
143 }
144 }
145
146 // Final 0 length sequence to indicate finished
147 output.push("0 ] }".to_string());
148 String::from_iter(output).parse().unwrap()
149}
150
151/// Takes a list of sequences of combos of Capabilities and turns it into a u8 array
152/// that can be stored in memory as a contiguous piece of data.
153/// This is necessary to store the result guide independently of rust compilation.
154///
155/// ```
156/// use kll_core::{Capability, CapabilityState};
157///
158/// const RESULT_GUIDES: &'static [u8] = kll_macros::result_guide!(
159/// [
160/// // Press Shift + A; Release Shift; Release A
161/// [
162/// Capability::HidKeyboard {
163/// state: CapabilityState::Initial,
164/// loop_condition_index: 0,
165/// id: kll_hid::Keyboard::A,
166/// },
167/// Capability::HidKeyboard {
168/// state: CapabilityState::Initial,
169/// loop_condition_index: 0,
170/// id: kll_hid::Keyboard::LeftShift,
171/// },
172/// ],
173/// [Capability::HidKeyboard {
174/// state: CapabilityState::Last,
175/// loop_condition_index: 0,
176/// id: kll_hid::Keyboard::LeftShift,
177/// },],
178/// [Capability::HidKeyboard {
179/// state: CapabilityState::Last,
180/// loop_condition_index: 0,
181/// id: kll_hid::Keyboard::A,
182/// },],
183/// ],
184/// // Press B
185/// [[Capability::HidKeyboard {
186/// state: CapabilityState::Initial,
187/// loop_condition_index: 0,
188/// id: kll_hid::Keyboard::B,
189/// },]],
190/// // Release B
191/// [[Capability::HidKeyboard {
192/// state: CapabilityState::Last,
193/// loop_condition_index: 0,
194/// id: kll_hid::Keyboard::B,
195/// },]],
196/// );
197/// ```
198#[proc_macro]
199pub fn result_guide(input: TokenStream) -> TokenStream {
200 let mut output: Vec<String> = vec!["unsafe { &[".to_string()];
201
202 for sequence in input {
203 match sequence {
204 TokenTree::Group(sequence) => {
205 for combo in sequence.stream() {
206 match combo {
207 TokenTree::Group(combo) => {
208 let mut combo_output: Vec<String> = Vec::new();
209 let mut prefix_output: Vec<String> = Vec::new();
210 let mut elem_count = 0;
211 // Largest enum is 8 bytes
212 // Adjusts the size depending on the enum
213 let mut byte_count = 0;
214 let byte_max_count = 8;
215 for elem in combo.stream() {
216 // Detect TriggerCondition
217 match elem.clone() {
218 TokenTree::Ident(ident) => {
219 // Check for hardcoded byte count
220 // NOTE: It may be possible to use std::mem::size_of
221 // if we make a new struct crate that this macro
222 // crate can depend on. Determining byte count is the
223 // most important feature of this macro. This is
224 // needed to prevent undefined struct memory accesses
225 // when building the const u8 array.
226 match ident.to_string().as_str() {
227 "Capability" => {
228 // New element
229 elem_count += 1;
230 prefix_output = Vec::new();
231 }
232 "LayerClear" | "McuFlashMode" | "NoOp" => {
233 byte_count = 4;
234 }
235 "HidKeyboard"
236 | "HidProtocol"
237 | "HidLed"
238 | "HidSystemControl"
239 | "LayerRotate"
240 | "PixelAnimationControl"
241 | "PixelFadeLayer"
242 | "PixelGammaControl" => {
243 byte_count = 5;
244 }
245 "HidioOpenUrl"
246 | "HidioUnicodeString"
247 | "HidConsumerControl"
248 | "HidKeyboardState"
249 | "LayerState"
250 | "PixelAnimationIndex"
251 | "PixelLedControl"
252 | "Rotate" => {
253 byte_count = 6;
254 }
255 "PixelFadeIndex" | "PixelFadeSet" | "PixelTest" => {
256 byte_count = 7;
257 }
258 "HidioUnicodeState" => {
259 byte_count = 8;
260 }
261 _ => {
262 panic!("Unknown elem ident: {:?}", ident);
263 }
264 }
265 }
266 TokenTree::Punct(_) => {}
267 TokenTree::Group(group) => {
268 // Finished element, prepare additions
269 for n in 0..byte_count {
270 combo_output.append(&mut prefix_output.clone());
271 combo_output.push(group.to_string());
272 combo_output.push(".bytes()[".to_string());
273 combo_output.push(n.to_string());
274 combo_output.push("],".to_string());
275 }
276 // Fill empty bytes (to prevent undefined struct access)
277 for _ in byte_count..byte_max_count {
278 combo_output.push("0,".to_string());
279 }
280 }
281 _ => {}
282 }
283 prefix_output.push(elem.to_string());
284 }
285
286 // Add combo element count
287 output.push(elem_count.to_string());
288 output.push(",".to_string());
289 // Add combo
290 output.append(&mut combo_output);
291 }
292 TokenTree::Punct(_) => {}
293 _ => {
294 panic!("Invalid combo element: {:?}", combo);
295 }
296 }
297 }
298 }
299 TokenTree::Punct(_) => {}
300 _ => {
301 panic!("Invalid sequence element: {:?}", sequence);
302 }
303 }
304 }
305
306 // Final 0 length sequence to indicate finished
307 output.push("0 ] }".to_string());
308 String::from_iter(output).parse().unwrap()
309}
310
311enum LayerLookupState {
312 Layer,
313 LayerComma,
314 Type,
315 TypeComma,
316 Index,
317 IndexComma,
318 Triggers,
319 TriggersComma,
320}
321
322/// Takes data in the following format and turns it into a byte array.
323/// The trigger count is automatically calculated.
324/// Triggers are u16, all the other fields are u8.
325///
326/// ```
327/// const LAYER_LOOKUP: &'static [u8] = kll_macros::layer_lookup!(
328/// // Layer 0, Switch Type (1), Index 5, No Triggers
329/// 0, 1, 5, [],
330/// // Layer 0, Switch Type (1), Index 6, 2 Triggers: 0 14
331/// 0, 1, 6, [0, 14],
332/// // Layer 0, Switch Type (1), Index 7, 1 Trigger: 0
333/// 0, 1, 7, [0],
334/// // Layer 1, None Type (0), Index 2, No Triggers
335/// 1, 0, 2, [],
336/// // Layer 1, Layer Type (7), Layer(index) 3, 1 Trigger: A
337/// 1, 7, 3, [0xA],
338/// // Layer 2, AnalogDistance Type (3), Index 8, 1 Trigger: A
339/// 2, 3, 8, [0xA],
340/// // Layer 2, Switch Type (1), Index 6, 1 Trigger: 14
341/// 2, 1, 6, [14],
342/// );
343/// ```
344///
345#[proc_macro]
346pub fn layer_lookup(input: TokenStream) -> TokenStream {
347 let mut state = LayerLookupState::Layer;
348
349 let mut triggers: Vec<u16> = Vec::new();
350
351 let mut output: Vec<String> = vec!["&".to_string(), "[".to_string()];
352
353 // TODO Add error checking for syntax
354 for token in input {
355 match state {
356 LayerLookupState::Layer => {
357 output.push(token.to_string());
358 state = LayerLookupState::LayerComma;
359 }
360 LayerLookupState::LayerComma => {
361 output.push(token.to_string());
362 state = LayerLookupState::Type;
363 }
364 LayerLookupState::Type => {
365 output.push(token.to_string());
366 state = LayerLookupState::TypeComma;
367 }
368 LayerLookupState::TypeComma => {
369 output.push(token.to_string());
370 state = LayerLookupState::Index;
371 }
372 LayerLookupState::Index => {
373 match token {
374 TokenTree::Literal(literal) => {
375 // Check if this is a hex value
376 let val = if literal.to_string().contains("0x") {
377 u16::from_str_radix(literal.to_string().trim_start_matches("0x"), 16)
378 .unwrap()
379 } else {
380 literal.to_string().parse::<u16>().unwrap()
381 };
382
383 // Push index as two bytes
384 for num in val.to_le_bytes() {
385 output.push(format!("{}, ", num));
386 }
387 }
388 _ => {
389 panic!("Invalid token, expected index token: {:?}", token);
390 }
391 }
392 state = LayerLookupState::IndexComma;
393 }
394 LayerLookupState::IndexComma => {
395 // Comma has already been added due to the index
396 state = LayerLookupState::Triggers;
397 }
398 LayerLookupState::Triggers => {
399 match token {
400 TokenTree::Group(group) => {
401 for subtoken in group.stream() {
402 match subtoken.clone() {
403 TokenTree::Punct(_) => {}
404 TokenTree::Literal(literal) => {
405 // Check if this is a hex value
406 if literal.to_string().contains("0x") {
407 triggers.push(
408 u16::from_str_radix(
409 literal.to_string().trim_start_matches("0x"),
410 16,
411 )
412 .unwrap(),
413 );
414 } else {
415 triggers.push(literal.to_string().parse::<u16>().unwrap());
416 }
417 }
418 _ => {
419 panic!("Invalid trigger list token: {:?}", subtoken);
420 }
421 }
422 }
423
424 // Finished gathering triggers
425 // 1. Add the count
426 output.push(format!("{},", triggers.len()));
427
428 // 2. Add each of the triggers as a little endian u16
429 if !triggers.is_empty() {
430 for trigger in triggers {
431 for num in trigger.to_le_bytes() {
432 output.push(format!("{},", num));
433 }
434 }
435 triggers = Vec::new();
436 }
437 state = LayerLookupState::TriggersComma;
438 }
439 _ => {
440 panic!("Invalid trigger token group: {:?}", token);
441 }
442 }
443 }
444 LayerLookupState::TriggersComma => {
445 state = LayerLookupState::Layer;
446 }
447 }
448 }
449
450 output.push("]".to_string());
451 String::from_iter(output).parse().unwrap()
452}