diff --git a/doc/plugin/Macros.md b/doc/plugin/Macros.md new file mode 100644 index 00000000..0fdd07b3 --- /dev/null +++ b/doc/plugin/Macros.md @@ -0,0 +1,183 @@ +# Kaleidoscope-Macros + +Macros are a standard feature on many keyboards and Kaleidoscope-powered ones +are no exceptions. Macros are a way to have a single key-press do a whole lot of +things under the hood: conventionally, macros play back a key sequence, but with +Kaleidoscope, there is much more we can do. Nevertheless, playing back a +sequence of events is still the primary use of macros. + +Playing back a sequence means that when we press a macro key, we can have it +play pretty much any sequence. It can type some text for us, or invoke a +complicated shortcut - the possibilities are endless! + +In Kaleidoscope, macros are implemented via this plugin. You can define upto 256 macros. + +## Using the plugin + +To use the plugin, we need to include the header, tell the firmware to `use` the +plugin, place macros on the keymap, and create a special handler function +(`macroAction`) that will tell the plugin what shall happen when macro keys are +pressed. It is best illustrated with an example: + +```c++ +#include +#include + +// Give a name to the macros! +enum { + MACRO_MODEL01, + MACRO_HELLO, + MACRO_SPECIAL, +}; + +// Somewhere in the keymap: +M(MACRO_MODEL01), M(MACRO_HELLO), M(MACRO_SPECIAL) + +// later in the Sketch: +const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) { + switch (macroIndex) { + case MACRO_MODEL01: + return MACRODOWN(I(25), + D(LeftShift), T(M), U(LeftShift), T(O), T(D), T(E), T(L), + T(Spacebar), + W(100), + T(0), T(1) ); + case MACRO_HELLO: + if (keyToggledOn(keyState)) { + return Macros.type(PSTR("Hello "), PSTR("world!")); + } + break; + case MACRO_SPECIAL: + if (keyToggledOn(keyState)) { + // Do something special + } + break; + } + return MACRO_NONE; +} + +KALEIDOSCOPE_INIT_PLUGINS(Macros); + +void setup() { + Kaleidoscope.setup (); +} +``` + +## Keymap markup + +### `M(id)` + +> Places a macro key on the keymap, with the `id` number (0 to 255) as identifier. Whenever this key +> has to be handled, the `macroAction` overrideable function will be called, +> with the identifier and key state as arguments. +> +> It is recommended to give a *name* to macro ids, by using an `enum`. + +## Plugin methods + +The plugin provides a `Macros` object, with the following methods and properties available: + +### `.play(macro)` + +> Plays back a macro, where a macro is a sequence created with the `MACRO()` +> helper discussed below. This method will be used by the plugin to play back +> the result of the `macroAction()` method, but is used rarely otherwise. +> +> The `macro` argument must be a sequence created with the `MACRO()` helper! + +### `.type(strings...)` + +> In cases where we only want to type some strings, it is far more convenient to +> use this method: we do not have to use the `MACRO()` helper, but just give +> this one a set of strings, and it will type them for us on the keyboard. We +> can use as many strings as we want, and all of them will be typed in order. +> +> Each string is limited to a sequence of printable ASCII characters. No +> international symbols, or unicode, or anything like it: just plain ASCII. +> +> Each of `strings` arguments must also reside in program memory, and the +> easiest way to do that is to wrap the string in a `PSTR()` helper. See the +> program code at the beginning of this documentation for an example! + +### `.row`, `.col` + +> The `row` and `col` properties describe the physical position a macro was +> triggered from if it was triggered by a key. The playback functions +> do not use these properties, but they are available, would one want to create +> a macro that needs to know which key triggered it. +> +> When the macro was not triggered by a key the value of these properties are +> unspecified. + +## Macro helpers + +Macros need to be able to simulate key down and key up events for any key - even +keys that may not be on the keymap otherwise. For this reason and others, we +need to define them in a special way, using the `MACRO` helper (or its +`MACRODOWN()` variant, see below): + +### `MACRO(steps...)` + +> Defines a macro, that is built up from `steps` (explained below). The plugin +> will iterate through the sequence, and re-play the steps in order. +> +> Note: In older versions of the Macros plugin, the sequence of steps had to end +> with a special step called END. This is no longer required. Existing macros +> that end with END will still work correctly, but new code should not use END; +> usage of END is deprecated. + +### `MACRODOWN(steps...)` + +> The same as the `MACRO()` helper above, but it will create a special sequence, +> where the steps are only played back when the triggering key was just pressed. +> That is, the macro will not be performed when the key is released, or held, or +> not pressed at all. +> +> Use this over `MACRO()` when you only want to perform an action when the key +> actuates, and no action should be taken when it is held, released, or when it +> is not pressed at all. For a lot of macros that emit a sequence without any +> other side effects, `MACRODOWN()` is usually the better choice. +> +> Can only be used from the `macroAction()` overrideable method. + +## `MACRO` steps + +Macro steps can be divided into two groups: + +### Delays + +* `I(millis)`: Sets the interval between steps to `millis`. By default, there is + no delay between steps, and they are played back as fast as possible. Useful + when we want to see the macro being typed, or need to slow it down, to allow + the host to process it. +* `W(millis)`: Waits for `millis` milliseconds. For dramatic effects. + +### Key events + +Key event steps have three variants: one that prefixes its argument with `Key_`, +one that does not, and a third that allows for a more compact - but also more +limited - representation. The first are the `D`, `U`, and `T` variants, the +second are `Dr`, `Ur`, and `Tr`, and the last variant are `Dc`, `Uc`, and `Tc`. +In most cases, one is likely use normal keys for the steps, so the `D`, `U`, and +`T` steps apply the `Key_` prefix. This allows us to write `MACRO(T(X))` instead +of `MACRO(Tr(Key_X))` - making the macro definition shorter, and more readable. + +The compact variant (`Dc`, `Uc`, and `Tc`) prefix the argument with `Key_` too, +but unlike `D`, `U`, and `T`, they ignore the `flags` component of the key, and +as such, are limited to ordinary keys. Mouse keys, consumer- or system keys are +not supported by this compact representation. + +* `D(key)`, `Dr(key)`, `Dc(key)`: Simulates a key being pressed (pushed down). +* `U(key)`, `Ur(key)`, `Uc(key)`: Simulates a key being released (going up). +* `T(key)`, `Tr(key)`, `Tc(key)`: Simulates a key being tapped (pressed first, then released). + +## Overrideable methods + +### `macroAction(macroIndex, keyState)` + +> The `macroAction` method is the brain of the macro support in Kaleidoscope: +> this function tells the plugin what sequence to play when given a macro index +> and a key state. +> +> It should return a macro sequence, or `MACRO_NONE` if nothing is to be played +> back. diff --git a/src/Kaleidoscope-Macros.h b/src/Kaleidoscope-Macros.h new file mode 100644 index 00000000..0aa76885 --- /dev/null +++ b/src/Kaleidoscope-Macros.h @@ -0,0 +1,19 @@ +/* Kaleidoscope-Macros - Macro keys for Kaleidoscope. + * Copyright (C) 2017-2018 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "kaleidoscope/plugin/Macros.h" diff --git a/src/kaleidoscope/plugin/Macros.cpp b/src/kaleidoscope/plugin/Macros.cpp new file mode 100644 index 00000000..de67e540 --- /dev/null +++ b/src/kaleidoscope/plugin/Macros.cpp @@ -0,0 +1,244 @@ +/* Kaleidoscope-Macros - Macro keys for Kaleidoscope. + * Copyright (C) 2017-2018 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "Kaleidoscope-Macros.h" +#include "kaleidoscope/hid.h" + +__attribute__((weak)) +const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) { + return MACRO_NONE; +} + +namespace kaleidoscope { +namespace plugin { + +MacroKeyEvent Macros_::active_macros[]; +byte Macros_::active_macro_count; +byte Macros_::row, Macros_::col; + +void playMacroKeyswitchEvent(Key key, uint8_t keyswitch_state) { + handleKeyswitchEvent(key, UNKNOWN_KEYSWITCH_LOCATION, keyswitch_state | INJECTED); + + kaleidoscope::hid::sendKeyboardReport(); + kaleidoscope::hid::sendMouseReport(); +} + +static void readKeyCodeAndPlay(const macro_t *macro_p, uint8_t flags, uint8_t keyStates) { + Key key; + key.flags = flags; + key.keyCode = pgm_read_byte(macro_p++); + + if (keyIsPressed(keyStates)) { + playMacroKeyswitchEvent(key, IS_PRESSED); + } + if (keyWasPressed(keyStates)) { + playMacroKeyswitchEvent(key, WAS_PRESSED); + } +} + +void Macros_::play(const macro_t *macro_p) { + macro_t macro = MACRO_ACTION_END; + uint8_t interval = 0; + uint8_t flags; + + if (!macro_p) + return; + + while (true) { + switch (macro = pgm_read_byte(macro_p++)) { + case MACRO_ACTION_STEP_INTERVAL: + interval = pgm_read_byte(macro_p++); + break; + case MACRO_ACTION_STEP_WAIT: { + uint8_t wait = pgm_read_byte(macro_p++); + delay(wait); + break; + } + case MACRO_ACTION_STEP_KEYDOWN: + flags = pgm_read_byte(macro_p++); + readKeyCodeAndPlay(macro_p++, flags, IS_PRESSED); + break; + case MACRO_ACTION_STEP_KEYUP: + flags = pgm_read_byte(macro_p++); + readKeyCodeAndPlay(macro_p++, flags, WAS_PRESSED); + break; + case MACRO_ACTION_STEP_TAP: + flags = pgm_read_byte(macro_p++); + readKeyCodeAndPlay(macro_p++, flags, IS_PRESSED | WAS_PRESSED); + break; + + case MACRO_ACTION_STEP_KEYCODEDOWN: + readKeyCodeAndPlay(macro_p++, 0, IS_PRESSED); + break; + case MACRO_ACTION_STEP_KEYCODEUP: + readKeyCodeAndPlay(macro_p++, 0, WAS_PRESSED); + break; + case MACRO_ACTION_STEP_TAPCODE: + readKeyCodeAndPlay(macro_p++, 0, IS_PRESSED | WAS_PRESSED); + break; + + case MACRO_ACTION_END: + default: + return; + } + + delay(interval); + } +} + +static const Key ascii_to_key_map[] PROGMEM = { + // 0x21 - 0x30 + LSHIFT(Key_1), + LSHIFT(Key_Quote), + LSHIFT(Key_3), + LSHIFT(Key_4), + LSHIFT(Key_5), + LSHIFT(Key_7), + Key_Quote, + LSHIFT(Key_9), + LSHIFT(Key_0), + LSHIFT(Key_8), + LSHIFT(Key_Equals), + Key_Comma, + Key_Minus, + Key_Period, + Key_Slash, + Key_0, + + // 0x3a ... 0x40 + LSHIFT(Key_Semicolon), + Key_Semicolon, + LSHIFT(Key_Comma), + Key_Equals, + LSHIFT(Key_Period), + LSHIFT(Key_Slash), + LSHIFT(Key_2), + + // 0x5b ... 0x60 + Key_LeftBracket, + Key_Backslash, + Key_RightBracket, + LSHIFT(Key_6), + LSHIFT(Key_Minus), + Key_Backtick, + + // 0x7b ... 0x7e + LSHIFT(Key_LeftBracket), + LSHIFT(Key_Backslash), + LSHIFT(Key_RightBracket), + LSHIFT(Key_Backtick), +}; + + +Key Macros_::lookupAsciiCode(uint8_t ascii_code) { + Key key = Key_NoKey; + + switch (ascii_code) { + case 0x08 ... 0x09: + key.keyCode = Key_Backspace.keyCode + ascii_code - 0x08; + break; + case 0x0A: + key.keyCode = Key_Enter.keyCode; + break; + case 0x1B: + key.keyCode = Key_Escape.keyCode; + break; + case 0x20: + key.keyCode = Key_Spacebar.keyCode; + break; + case 0x21 ... 0x30: + key.raw = pgm_read_word(&ascii_to_key_map[ascii_code - 0x21]); + break; + case 0x31 ... 0x39: + key.keyCode = Key_1.keyCode + ascii_code - 0x31; + break; + case 0x3A ... 0x40: + key.raw = pgm_read_word(&ascii_to_key_map[ascii_code - 0x3A + 16]); + break; + case 0x41 ... 0x5A: + key.flags = SHIFT_HELD; + key.keyCode = Key_A.keyCode + ascii_code - 0x41; + break; + case 0x5B ... 0x60: + key.raw = pgm_read_word(&ascii_to_key_map[ascii_code - 0x5B + 23]); + break; + case 0x61 ... 0x7A: + key.keyCode = Key_A.keyCode + ascii_code - 0x61; + break; + case 0x7B ... 0x7E: + key.raw = pgm_read_word(&ascii_to_key_map[ascii_code - 0x7B + 29]); + break; + } + return key; +} + +const macro_t *Macros_::type(const char *string) { + while (true) { + uint8_t ascii_code = pgm_read_byte(string++); + if (!ascii_code) + break; + + Key key = lookupAsciiCode(ascii_code); + + + if (key.raw == Key_NoKey.raw) + continue; + + playMacroKeyswitchEvent(key, IS_PRESSED); + playMacroKeyswitchEvent(key, WAS_PRESSED); + + } + + return MACRO_NONE; +} + +EventHandlerResult Macros_::onKeyswitchEvent(Key &mappedKey, byte row, byte col, uint8_t keyState) { + if (mappedKey.flags != (SYNTHETIC | IS_MACRO)) + return EventHandlerResult::OK; + + byte key_id = (row * COLS) + col; + addActiveMacroKey(mappedKey.keyCode, key_id, keyState); + + return EventHandlerResult::EVENT_CONSUMED; +} + +EventHandlerResult Macros_::afterEachCycle() { + active_macro_count = 0; + + return EventHandlerResult::OK; +} + +EventHandlerResult Macros_::beforeReportingState() { + for (byte i = 0; i < active_macro_count; ++i) { + if (active_macros[i].key_id == 0xFF) { + // i.e. UNKNOWN_KEYSWITCH_LOCATION + row = 0xFF; + col = 0xFF; + } else { + row = active_macros[i].key_id / COLS; + col = active_macros[i].key_id % COLS; + } + const macro_t *m = macroAction(active_macros[i].key_code, + active_macros[i].key_state); + Macros.play(m); + } + return EventHandlerResult::OK; +} + +} +} + +kaleidoscope::plugin::Macros_ Macros; diff --git a/src/kaleidoscope/plugin/Macros.h b/src/kaleidoscope/plugin/Macros.h new file mode 100644 index 00000000..e0dc9e13 --- /dev/null +++ b/src/kaleidoscope/plugin/Macros.h @@ -0,0 +1,86 @@ +/* Kaleidoscope-Macros - Macro keys for Kaleidoscope. + * Copyright (C) 2017-2018 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include + +#include "kaleidoscope/plugin/Macros/MacroKeyDefs.h" +#include "kaleidoscope/plugin/Macros/MacroSteps.h" + +const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState); + +#if !defined(MAX_CONCURRENT_MACROS) +#define MAX_CONCURRENT_MACROS 8 +#endif + +struct MacroKeyEvent { + byte key_code; + byte key_id; + byte key_state; +}; + +namespace kaleidoscope { +namespace plugin { + +class Macros_ : public kaleidoscope::Plugin { + public: + Macros_(void) {} + + static MacroKeyEvent active_macros[MAX_CONCURRENT_MACROS]; + static byte active_macro_count; + static void addActiveMacroKey(byte key_code, byte key_id, byte key_state) { + // If we've got too many active macros, give up: + if (active_macro_count >= MAX_CONCURRENT_MACROS) { + return; + } + active_macros[active_macro_count].key_code = key_code; + active_macros[active_macro_count].key_id = key_id; + active_macros[active_macro_count].key_state = key_state; + ++active_macro_count; + } + + EventHandlerResult onKeyswitchEvent(Key &mappedKey, byte row, byte col, uint8_t keyState); + EventHandlerResult beforeReportingState(); + EventHandlerResult afterEachCycle(); + + void play(const macro_t *macro_p); + + /* What follows below, is a bit of template magic that allows us to use + Macros.type() with any number of arguments, without having to use a + sentinel. See the comments on Kaleidoscope.use() for more details - this is + the same trick. + */ + inline const macro_t *type() { + return MACRO_NONE; + } + const macro_t *type(const char *string); + template + const macro_t *type(const char *first, Strings&&... strings) { + type(first); + return type(strings...); + } + + static byte row, col; + + private: + Key lookupAsciiCode(uint8_t ascii_code); +}; + +} +} + +extern kaleidoscope::plugin::Macros_ Macros; diff --git a/src/kaleidoscope/plugin/Macros/MacroKeyDefs.h b/src/kaleidoscope/plugin/Macros/MacroKeyDefs.h new file mode 100644 index 00000000..ae4fbb7d --- /dev/null +++ b/src/kaleidoscope/plugin/Macros/MacroKeyDefs.h @@ -0,0 +1,31 @@ +/* Kaleidoscope-Macros - Macro keys for Kaleidoscope. + * Copyright (C) 2017-2018 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#define IS_MACRO B00100000 + +#define M(n) (Key){ n, KEY_FLAGS|SYNTHETIC|IS_MACRO } +#define Key_macroKey1 M(1) +#define Key_macroKey2 M(2) +#define Key_macroKey3 M(3) +#define Key_macroKey4 M(4) +#define Key_macroKey5 M(5) +#define Key_macroKey6 M(6) +#define Key_macroKey7 M(7) +#define Key_macroKey8 M(8) +#define Key_macroKey9 M(9) +#define Key_macroKey10 M(10) diff --git a/src/kaleidoscope/plugin/Macros/MacroSteps.h b/src/kaleidoscope/plugin/Macros/MacroSteps.h new file mode 100644 index 00000000..39bf2a64 --- /dev/null +++ b/src/kaleidoscope/plugin/Macros/MacroSteps.h @@ -0,0 +1,54 @@ +/* Kaleidoscope-Macros - Macro keys for Kaleidoscope. + * Copyright (C) 2017-2018 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +typedef enum { + MACRO_ACTION_END, + + MACRO_ACTION_STEP_INTERVAL, + MACRO_ACTION_STEP_WAIT, + + MACRO_ACTION_STEP_KEYDOWN, + MACRO_ACTION_STEP_KEYUP, + MACRO_ACTION_STEP_TAP, + + MACRO_ACTION_STEP_KEYCODEDOWN, + MACRO_ACTION_STEP_KEYCODEUP, + MACRO_ACTION_STEP_TAPCODE, +} MacroActionStepType; + +typedef uint8_t macro_t; + +#define MACRO_NONE 0 +#define MACRO(...) ({static const macro_t __m[] PROGMEM = { __VA_ARGS__, MACRO_ACTION_END }; &__m[0]; }) +#define MACRODOWN(...) (keyToggledOn(keyState) ? MACRO(__VA_ARGS__) : MACRO_NONE) + +#define I(n) MACRO_ACTION_STEP_INTERVAL, n +#define W(n) MACRO_ACTION_STEP_WAIT, n + +#define Dr(k) MACRO_ACTION_STEP_KEYDOWN, (k).flags, (k).keyCode +#define D(k) Dr(Key_ ## k) +#define Ur(k) MACRO_ACTION_STEP_KEYUP, (k).flags, (k).keyCode +#define U(k) Ur(Key_ ## k) +#define Tr(k) MACRO_ACTION_STEP_TAP, (k).flags, (k).keyCode +#define T(k) Tr(Key_ ## k) + +#define Dc(k) MACRO_ACTION_STEP_KEYCODEDOWN, (Key_ ## k).keyCode +#define Uc(k) MACRO_ACTION_STEP_KEYCODEUP, (Key_ ## k).keyCode +#define Tc(k) MACRO_ACTION_STEP_TAPCODE, (Key_ ## k).keyCode + +__attribute__((deprecated("END is no longer required to end macros"))) const MacroActionStepType END = MACRO_ACTION_END;