From 3cc70ed7e71afb3989f50a0defcbb745fec7d81f Mon Sep 17 00:00:00 2001 From: xs5871 <60395129+xs5871@users.noreply.github.com> Date: Sat, 1 Jun 2024 18:13:18 +0000 Subject: [PATCH] Re-implement sequences as macro in a module (#967) * Re-implement sequences as macro in a module * Fix lint * Add keys for uc mode selection to macros + missing docs --- docs/en/Getting_Started.md | 2 +- docs/en/README.md | 1 + docs/en/macros.md | 226 +++++++++++++++++++++++++++++++++++++ docs/en/modules.md | 1 + docs/en/porting_to_kmk.md | 16 +-- kmk/modules/macros.py | 213 ++++++++++++++++++++++++++++++++++ tests/test_macros.py | 224 ++++++++++++++++++++++++++++++++++++ util/aspell.en.pws | 3 +- 8 files changed, 676 insertions(+), 10 deletions(-) create mode 100644 docs/en/macros.md create mode 100644 kmk/modules/macros.py create mode 100644 tests/test_macros.py diff --git a/docs/en/Getting_Started.md b/docs/en/Getting_Started.md index 59bdb1200..38e7478dc 100644 --- a/docs/en/Getting_Started.md +++ b/docs/en/Getting_Started.md @@ -62,7 +62,7 @@ Once you've got the gist of it: - [International](international.md) extension adds keys for non US layouts and [Media Keys](media_keys.md) adds keys for ... media And to go even further: -- [Sequences](sequences.md) are used for sending multiple keystrokes in a single action +- [Macros](macros.md) are used for sending multiple keystrokes in a single action - [Layers](layers.md) can transform the whole way your keyboard is behaving with a single touch - [HoldTap](holdtap.md) allow you to customize the way a key behaves whether it is tapped or hold, and [TapDance](tapdance.md) depending on the number of times it is pressed diff --git a/docs/en/README.md b/docs/en/README.md index 7da22c36e..d422a290a 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -26,6 +26,7 @@ Before you look further, you probably want to start with our [getting started gu - [Combos](combos.md): Adds chords and sequences - [Layers](layers.md): Adds layer support (Fn key) to allow many more keys to be put on your keyboard - [HoldTap](holdtap.md): Adds support for augmented modifier keys to act as one key when tapped, and modifier when held. +- [Macros](macros.md): Adds macros. - [Mouse keys](mouse_keys.md): Adds mouse keycodes - [OneShot](oneshot.md): Adds support for oneshot/sticky keys. - [Power](power.md): Power saving features. This is mostly useful when on battery power. diff --git a/docs/en/macros.md b/docs/en/macros.md new file mode 100644 index 000000000..f087b23c1 --- /dev/null +++ b/docs/en/macros.md @@ -0,0 +1,226 @@ +# Macros + +Macros are used for sending multiple keystrokes in a single action, and can +be used for things like Unicode characters (even emojis! 🇨🇦), _Lorem ipsum_ +generators, triggering side effects (think lighting, speakers, +microcontroller-optimized cryptocurrency miners, whatever). + +## Setup + +```python +from kmk.modules.macros import Macros + +macros = Macros() +keyboard.modules.append(macros) +``` + +This will enable a new type of keycode: `KC.MACRO()` + +## Keycodes + +|Key |Description | +|-------------------|------------------------------------------| +|`KC.MACRO(macro)` |Create a key that will play back a macro. | +|`KC.UC_MODE_IBUS` |Switch Unicode mode to IBus. | +|`KC.UC_MODE_MACOS` |Switch Unicode mode to macOS. | +|`KC.UC_MODE_WINC` |Switch Unicode mode to Windows Compose. | + +## Sending strings + +The most basic sequence is an ASCII string. It can be used to send any standard +English alphabet character, and an assortment of other "standard" keyboard keys +(return, space, exclamation points, etc.). +Keep in mind that some characters from shifted keys are i18n dependent. + +```python +WOW = KC.MACRO("Wow, KMK is awesome!") + +keyboard.keymap = [, WOW, ] +``` + +## Key sequences + +If you need to add modifier keys to your sequence or you need more granular control. +You can use it to add things like copying/pasting, tabbing between fields, etc. + +```python +from kmk.modules.macros import Press, Release, Tap + +PASTE_WITH_COMMENTARY = KC.MACRO( + "look at this: ", + Press(KC.LCTL), + Tap(KC.V), + Release(KC.LCTL) +) + +keyboard.keymap = [, PASTE_WITH_COMMENTARY, ] +``` + +The above example will type out "look at this: " and then paste the contents of your +clipboard. + + +### Sleeping within a sequence + +If you need to wait during a sequence, you can use `Delay(ms)` to wait a +length of time, in milliseconds. + +```python +from kmk.modules.macros import Tap, Delay + +COUNTDOWN_TO_PASTE = KC.MACRO( + Tap(KC.N3), + Tap(KC.ENTER), + Delay(1000), + Tap(KC.N2), + Tap(KC.ENTER), + Delay(1000), + Tap(KC.N1), + Tap(KC.ENTER), + Delay(1000), + Tap(KC.LCTL(KC.V)), +) + +keyboard.keymap = [, COUNTDOWN_TO_PASTE, ] +``` + +This example will type out the following, waiting one second (1000 ms) between numbers: + + 3 + 2 + 1 + +and then paste the contents of your clipboard. + +### Alt Tab with delay + +If alt tab isn't working because it requires a delay, adding a delay and triggering +down and up on ALT manually may fix the issue. + +``` python +from kmk.modules.macros import Delay, Press, Release, Tap + +NEXT = KC.MACRO( + Press(KC.LALT), + Delay(30), + Tap(KC.TAB), + Delay(30), + Release(KC.LALT), +) +``` + +## Unicode + +### Unicode Modes + +On Linux, Unicode uses `Ctrl-Shift-U`, which is supported by `ibus` and GTK+3. +`ibus` users will need to add `IBUS_ENABLE_CTRL_SHIFT_U=1` to their environment +(`~/profile`, `~/.bashrc`, `~/.zshrc`, or through your desktop environment's +configurator). + +On Windows, [WinCompose](https://github.com/samhocevar/wincompose) is required. + +- Linux : `UnicodeModeIBus`, the default +- MacOS: `UnicodeModeMacOS` +- Windows: `UnicodeModeWinC` + +### Unicode Examples + +Initialize `Macros` to use `UnicodeModeMac` and make a key to cycle between modes +at runtime. + +```python +from kmk.keys import Key +from kmk.modules.macros import Macros, UnicodeModeIBus, UnicodeModeMacOS, UnicodeModeWinC + +macros = Macros(unicode_mode=UnicodeModeMacOS) +keyboard.modules.append(macros) + +def switch_um(keyboard): + if macros.unicode_mode == UnicodeModeIBus: + macros.unicode_mode = UnicodeModeMacOS + elif macros.unicode_mode == UnicodeModeMacOS: + macros.unicode_mode = UnicodeModeWinC + else: + macros.Unicode_mode = UnicodeModeIBus + +UCCYCLE = Key(code=None, on_press=switch_um) + +FLIP = KC.MACRO('(ノಠ痊ಠ)ノ彡┻━┻') + +keyboard.keymap = [, UCCYCLE, FLIP, ] +``` + +## Arbitrary Actions + +As it happens, macros accept any callable object (even generators) as arguments. +The `KMKKeyboard` object is passed as argument to that callable. + +### Example 1 + +Change the RGB animation mode to "SWIRL" for five seconds and print an ASCII +spinner + +```python +# ... boilerplate omitted for brevity. + +prev_animation = None + +def start_spinning(keyboard): + global prev_animation + prev_animation = rgb.animation_mode + rgb.animation_mode = AnimationModes.SWIRL + rgb.effect_init = True + +def stop_spinning(keyboard): + rgb.animation_mode = prev_animation + rgb.effect_init = True + +DISCO = KC.MACRO( + "disco time!", + start_color_wheel, + "-", + DELAY(1000), + KC.BSPC, + "\\", + DELAY(1000), + KC.BSPC, + "|", + DELAY(1000), + KC.BSPC, + "/", + DELAY(1000), + KC.BSPC, + "-", + DELAY(1000), + KC.BSPC, + stop_color_wheel, + " disco time over.", + ) +``` + +### Example 2 + +Here's a programmatic version of the earlier countdown-to-paste example, using a +generator. +Any return value that is not `None` is interpreted as a delay instruction in +milliseconds. + +```python +def countdown(count, delay_ms): + def generator(keyboard): + for n in range(count, 0, -1): + KC[n].on_press(keyboard) + yield + KC[n].on_release(keyboard) + yield + KC.ENTER.on_press(keyboard) + yield + KC.ENTER.on_release(keyboard) + yield delay_ms + return generator + +COUNTDOWN_TO_PASTE = KC.MACRO( + countdown(3, 1000), + Tap(KC.LCTL(KC.V)), +) diff --git a/docs/en/modules.md b/docs/en/modules.md index 760012499..fc6631677 100644 --- a/docs/en/modules.md +++ b/docs/en/modules.md @@ -12,6 +12,7 @@ modules are put on your keyboard. - [HoldTap](holdtap.md): Adds support for augmented modifier keys to act as one key when tapped, and modifier when held. +- [Macros](macros.md): Adds macros. - [Mouse keys](mouse_keys.md): Adds mouse keycodes. - [OneShot](oneshot.md): Adds support for oneshot/sticky keys. - [Power](power.md): Power saving features. This is mostly useful when on battery power. diff --git a/docs/en/porting_to_kmk.md b/docs/en/porting_to_kmk.md index eaa30ec37..6bbe87227 100644 --- a/docs/en/porting_to_kmk.md +++ b/docs/en/porting_to_kmk.md @@ -58,9 +58,11 @@ send its corresponding number. Use it after your pins and module definition to define both `keyboard.coord_mapping` and `keyboard.keymap`. ```python -from kmk.handlers.sequences import simple_key_sequence +from kmk.modules.macros import Macros from kmk.keys import KC +keyboard.modules.append(Macros()) + # *2 for split keyboards, which will typically manage twice the number of keys # of one side. Having this N too large will have no impact (maybe slower boot..) N = len(keyboard.col_pins) * len(keyboard.row_pins) * 2 @@ -73,13 +75,11 @@ for i in range(N): c, r = divmod(i, 100) d, u = divmod(r, 10) layer.append( - simple_key_sequence( - ( - getattr(KC, 'N' + str(c)), - getattr(KC, 'N' + str(d)), - getattr(KC, 'N' + str(u)), - KC.SPC, - ) + KC.MACRO( + getattr(KC, 'N' + str(c)), + getattr(KC, 'N' + str(d)), + getattr(KC, 'N' + str(u)), + KC.SPC, ) ) keyboard.keymap = [layer] diff --git a/kmk/modules/macros.py b/kmk/modules/macros.py new file mode 100644 index 000000000..47f5f96f0 --- /dev/null +++ b/kmk/modules/macros.py @@ -0,0 +1,213 @@ +from kmk.keys import KC, make_argumented_key, make_key +from kmk.modules import Module +from kmk.scheduler import create_task +from kmk.utils import Debug + +debug = Debug(__name__) + + +class MacroMeta: + def __init__(self, *macro, **kwargs): + self.macro = macro + + +def Delay(delay): + return lambda keyboard: delay + + +def Press(key): + return lambda keyboard: key.on_press(keyboard) + + +def Release(key): + return lambda keyboard: key.on_release(keyboard) + + +def Tap(key): + def _(keyboard): + key.on_press(keyboard) + yield + key.on_release(keyboard) + + return _ + + +class UnicodeModeIBus: + @staticmethod + def pre(keyboard): + macro = (KC.LCTL, KC.LSFT, KC.U) + for k in macro: + k.on_press(keyboard) + yield + for k in macro: + k.on_release(keyboard) + + @staticmethod + def post(keyboard): + KC.ENTER.on_press(keyboard) + yield + KC.ENTER.on_release(keyboard) + + +class UnicodeModeMacOS: + @staticmethod + def pre(keyboard): + KC.LALT.on_press(keyboard) + yield + + @staticmethod + def post(keyboard): + KC.LALT.on_release(keyboard) + yield + + +class UnicodeModeWinC: + @staticmethod + def pre(keyboard): + macro = (KC.RALT, KC.U) + for k in macro: + k.on_press(keyboard) + yield + for k in macro: + k.on_release(keyboard) + + @staticmethod + def post(keyboard): + KC.ENTER.on_press(keyboard) + yield + KC.ENTER.on_release(keyboard) + + +def MacroIter(keyboard, macro, unicode_mode): + for item in macro: + if callable(item): + ret = item(keyboard) + if ret.__class__.__name__ == 'generator': + for _ in ret: + yield _ + yield + else: + yield ret + + elif isinstance(item, str): + for char in item: + if ord(char) <= 127: + # ANSII key codes + key = KC[char] + if char.isupper(): + KC.LSHIFT.on_press(keyboard) + key.on_press(keyboard) + yield + + if char.isupper(): + KC.LSHIFT.on_release(keyboard) + key.on_release(keyboard) + yield + + else: + # unicode code points + for _ in unicode_mode.pre(keyboard): + yield _ + yield + + for digit in hex(ord(char))[2:]: + key = KC[digit] + key.on_press(keyboard) + yield + key.on_release(keyboard) + yield + + for _ in unicode_mode.post(keyboard): + yield _ + yield + + elif debug.enabled: + debug('unsupported macro type', item.__class__.__name__) + + +class Macros(Module): + def __init__(self, unicode_mode=UnicodeModeIBus, delay=10): + self._active = False + self.key_buffer = [] + self.unicode_mode = unicode_mode + self.delay = delay + + make_argumented_key( + validator=MacroMeta, + names=('MACRO',), + on_press=self.on_press_macro, + ) + make_key( + names=('UC_MODE_IBUS',), + meta=UnicodeModeIBus, + on_press=self.on_press_unicode_mode, + ) + make_key( + names=('UC_MODE_MACOS',), + meta=UnicodeModeMacOS, + on_press=self.on_press_unicode_mode, + ) + make_key( + names=('UC_MODE_WINC',), + meta=UnicodeModeWinC, + on_press=self.on_press_unicode_mode, + ) + + def during_bootup(self, keyboard): + return + + def before_matrix_scan(self, keyboard): + return + + def after_matrix_scan(self, keyboard): + return + + def process_key(self, keyboard, key, is_pressed, int_coord): + if not self._active: + return key + + self.key_buffer.append((int_coord, key, is_pressed)) + + def before_hid_send(self, keyboard): + return + + def after_hid_send(self, keyboard): + return + + def on_powersave_enable(self, keyboard): + return + + def on_powersave_disable(self, keyboard): + return + + def on_press_unicode_mode(self, key, keyboard, *args, **kwargs): + self.unicode_mode = key.meta + + def on_press_macro(self, key, keyboard, *args, **kwargs): + self._active = True + + _iter = MacroIter(keyboard, key.meta.macro, self.unicode_mode) + + def process_macro_async(): + delay = self.delay + try: + # any not None value the iterator yields is a delay value in ms. + ret = next(_iter) + if ret is not None: + delay = ret + keyboard._send_hid() + create_task(process_macro_async, after_ms=delay) + except StopIteration: + self.send_key_buffer(keyboard) + + process_macro_async() + + def send_key_buffer(self, keyboard): + self._active = False + if not self.key_buffer: + return + + for int_coord, key, is_pressed in self.key_buffer: + keyboard.resume_process_key(self, key, is_pressed, int_coord, False) + + self.key_buffer.clear() diff --git a/tests/test_macros.py b/tests/test_macros.py new file mode 100644 index 000000000..3632d76e3 --- /dev/null +++ b/tests/test_macros.py @@ -0,0 +1,224 @@ +import unittest + +from kmk.keys import KC +from kmk.modules.macros import ( + Delay, + Macros, + Press, + Release, + Tap, + UnicodeModeIBus, + UnicodeModeMacOS, + UnicodeModeWinC, +) +from tests.keyboard_test import KeyboardTest + + +class TestMacro(unittest.TestCase): + def setUp(self): + self.macros = Macros() + self.kb = KeyboardTest( + [self.macros], + [ + [ + KC.MACRO(Press(KC.A), Release(KC.A)), + KC.MACRO(Press(KC.A), Press(KC.B), Release(KC.A), Release(KC.B)), + KC.MACRO(Tap(KC.A), Tap(KC.A)), + KC.MACRO(Tap(KC.A), Delay(10), Tap(KC.B)), + KC.Y, + KC.MACRO('Foo1'), + KC.MACRO(Press(KC.LCTL), 'Foo1', Release(KC.LCTL)), + KC.MACRO('🍺!'), + ] + ], + debug_enabled=False, + ) + + def test_0(self): + self.kb.test( + '', + [(0, True), (0, False)], + [{KC.A}, {}], + ) + + def test_1(self): + self.kb.test( + '', + [(1, True), (1, False)], + [{KC.A}, {KC.A, KC.B}, {KC.B}, {}], + ) + + def test_2(self): + self.kb.test( + '', + [(2, True), (2, False)], + [{KC.A}, {}, {KC.A}, {}], + ) + + def test_3(self): + self.kb.test( + '', + [(3, True), (3, False)], + [{KC.A}, {}, {KC.B}, {}], + ) + + def test_4(self): + self.kb.test( + '', + [(3, True), (3, False), (4, True), (4, False)], + [{KC.A}, {}, {KC.B}, {}, {KC.Y}, {}], + ) + + def test_5(self): + self.kb.test( + '', + [(5, True), (5, False)], + [{KC.LSFT, KC.F}, {}, {KC.O}, {}, {KC.O}, {}, {KC.N1}, {}], + ) + + def test_6(self): + self.kb.test( + '', + [(6, True), (6, False)], + [ + {KC.LCTL}, + {KC.LCTL, KC.LSFT, KC.F}, + {KC.LCTL}, + {KC.LCTL, KC.O}, + {KC.LCTL}, + {KC.LCTL, KC.O}, + {KC.LCTL}, + {KC.LCTL, KC.N1}, + {KC.LCTL}, + {}, + ], + ) + + def test_7_ibus(self): + self.kb.test( + '', + [(7, True), (7, False)], + [ + {KC.LCTL, KC.LSFT, KC.U}, + {}, + {KC.N1}, + {}, + {KC.F}, + {}, + {KC.N3}, + {}, + {KC.N7}, + {}, + {KC.A}, + {}, + {KC.ENTER}, + {}, + {KC.LSFT, KC.N1}, + {}, + ], + ) + + def test_7_ibus_explicit(self): + self.macros.unicode_mode = UnicodeModeIBus + self.kb.test( + '', + [(7, True), (7, False)], + [ + {KC.LCTL, KC.LSFT, KC.U}, + {}, + {KC.N1}, + {}, + {KC.F}, + {}, + {KC.N3}, + {}, + {KC.N7}, + {}, + {KC.A}, + {}, + {KC.ENTER}, + {}, + {KC.LSFT, KC.N1}, + {}, + ], + ) + + def test_7_ralt(self): + self.macros.unicode_mode = UnicodeModeMacOS + self.kb.test( + '', + [(7, True), (7, False)], + [ + {KC.LALT}, + {KC.LALT, KC.N1}, + {KC.LALT}, + {KC.LALT, KC.F}, + {KC.LALT}, + {KC.LALT, KC.N3}, + {KC.LALT}, + {KC.LALT, KC.N7}, + {KC.LALT}, + {KC.LALT, KC.A}, + {KC.LALT}, + {}, + {KC.LSFT, KC.N1}, + {}, + ], + ) + + def test_8_winc(self): + self.macros.unicode_mode = UnicodeModeWinC + self.kb.test( + '', + [(7, True), (7, False)], + [ + {KC.RALT, KC.U}, + {}, + {KC.N1}, + {}, + {KC.F}, + {}, + {KC.N3}, + {}, + {KC.N7}, + {}, + {KC.A}, + {}, + {KC.ENTER}, + {}, + {KC.LSFT, KC.N1}, + {}, + ], + ) + + +class TestUnicodeModeKeys(unittest.TestCase): + def setUp(self): + self.macros = Macros() + self.kb = KeyboardTest( + [self.macros], + [ + [ + KC.UC_MODE_IBUS, + KC.UC_MODE_MACOS, + KC.UC_MODE_WINC, + ] + ], + debug_enabled=False, + ) + + def test_ibus(self): + self.kb.test('', [(0, True), (0, False)], [{}]) + self.assertEqual(self.macros.unicode_mode, UnicodeModeIBus) + + def test_mac(self): + self.kb.test('', [(1, True), (1, False)], [{}]) + self.assertEqual(self.macros.unicode_mode, UnicodeModeMacOS) + + def test_winc(self): + self.kb.test('', [(2, True), (2, False)], [{}]) + self.assertEqual(self.macros.unicode_mode, UnicodeModeWinC) + + +if __name__ == '__main__': + unittest.main() diff --git a/util/aspell.en.pws b/util/aspell.en.pws index 9a4bfb1b0..0c2a3a568 100644 --- a/util/aspell.en.pws +++ b/util/aspell.en.pws @@ -1,4 +1,4 @@ -personal_ws-1.1 en 354 +personal_ws-1.1 en 355 ADNS AMS ANAVI @@ -69,6 +69,7 @@ Hanja Hankaku Henkan HoldTap +IBus InternalState ItsyBitsy JIS