diff --git a/README.md b/README.md index 7c9bcf15..0418e3a1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Documentation: - `m` holds a modifier while executing the second parameter - `.` executes two actions behind each other -For a list of supported keystrokes and their names, check the output of `xmodmap -pke` +For a list of supported keystrokes and their names, check the output of +`xmodmap -pke` Maybe you shouldn't use this feature in online PVP though. Might even get detected by the game. diff --git a/keymapper/dev/macros.py b/keymapper/dev/macros.py index c8d4fabf..77887bca 100644 --- a/keymapper/dev/macros.py +++ b/keymapper/dev/macros.py @@ -42,9 +42,18 @@ from keymapper.logger import logger from keymapper.config import config +# for debugging purposes +MODIFIER = 1 +CHILD_MACRO = 2 +SLEEP = 3 +REPEAT = 4 +KEYSTROKE = 5 +DEBUG = 6 + + class _Macro: """Supports chaining and preparing actions.""" - def __init__(self, handler): + def __init__(self, handler, depth): """Create a macro instance that can be populated with tasks. Parameters @@ -53,13 +62,17 @@ class _Macro: A function that accepts keycodes as the first parameter and the key-press state as the second. 1 for down and 0 for up. The macro will write to this function once executed with `.run()`. + depth : int + 0 for the outermost parent macro, 1 or greater for child macros, + like the second argument of repeat. """ self.tasks = [] self.handler = handler + self.depth = depth async def run(self): """Run the macro.""" - for task in self.tasks: + for i, (_, task) in enumerate(self.tasks): coroutine = task() if asyncio.iscoroutine(coroutine): await coroutine @@ -76,11 +89,11 @@ class _Macro: modifier : str macro : _Macro """ - self.tasks.append(lambda: self.handler(modifier, 1)) + self.tasks.append((MODIFIER, lambda: self.handler(modifier, 1))) self.add_keycode_pause() - self.tasks.append(macro.run) + self.tasks.append((CHILD_MACRO, macro.run)) self.add_keycode_pause() - self.tasks.append(lambda: self.handler(modifier, 0)) + self.tasks.append((MODIFIER, lambda: self.handler(modifier, 0))) self.add_keycode_pause() return self @@ -93,7 +106,7 @@ class _Macro: macro : _Macro """ for _ in range(repeats): - self.tasks.append(macro.run) + self.tasks.append((CHILD_MACRO, macro.run)) return self def add_keycode_pause(self): @@ -103,17 +116,17 @@ class _Macro: async def sleep(): await asyncio.sleep(sleeptime) - self.tasks.append(sleep) + self.tasks.append((SLEEP, sleep)) def keycode(self, character): """Write the character.""" - self.tasks.append(lambda: self.handler(character, 1)) - self.tasks.append(lambda: logger.spam( + self.tasks.append((KEYSTROKE, lambda: self.handler(character, 1))) + self.tasks.append((DEBUG, lambda: logger.spam( 'macro writes character %s', character - )) + ))) self.add_keycode_pause() - self.tasks.append(lambda: self.handler(character, 0)) + self.tasks.append((KEYSTROKE, lambda: self.handler(character, 0))) self.add_keycode_pause() return self @@ -124,7 +137,7 @@ class _Macro: async def sleep(): await asyncio.sleep(sleeptime) - self.tasks.append(sleep) + self.tasks.append((SLEEP, sleep)) return self @@ -171,7 +184,6 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0): macro_instance : _Macro or None A macro instance to add tasks to depth : int - For logging and debugging purposes """ # to anyone who knows better about compilers and thinks this is horrible: # please make a pull request. Because it probably is. @@ -184,7 +196,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0): assert isinstance(depth, int) if macro_instance is None: - macro_instance = _Macro(handler) + macro_instance = _Macro(handler, depth) else: assert isinstance(macro_instance, _Macro) @@ -192,7 +204,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0): space = ' ' * depth # is it another macro? - call_match = re.match(r'^(\w+)\(.+?', macro) + call_match = re.match(r'^(\w+)\(', macro) call = call_match[1] if call_match else None if call is not None: # available functions in the macro @@ -255,6 +267,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0): macro = int(macro) except ValueError: pass + logger.spam('%s%s %s', space, type(macro), macro) return macro @@ -272,8 +285,11 @@ def parse(macro, handler): key-press state as the second. 1 for down and 0 for up. The macro will write to this function once executed with `.run()`. """ + # whitespaces, tabs, newlines and such don't serve a purpose. make + # the log output clearer. + macro = re.sub(r'\s', '', macro) + logger.spam('preparing macro %s for later execution', macro) try: - logger.spam('input %s', macro) return _parse_recurse(macro, handler) except Exception as e: logger.error('Failed to parse macro "%s": %s', macro, e) diff --git a/tests/testcases/macros.py b/tests/testcases/macros.py index e320e63c..4ade8ecd 100644 --- a/tests/testcases/macros.py +++ b/tests/testcases/macros.py @@ -41,10 +41,10 @@ class TestMacros(unittest.TestCase): self.assertListEqual(self.result, [(1, 1), (1, 0)]) def test_1(self): - macro = 'k(1 2).k(a).k(3)' + macro = 'k(1).k(a).k(3)' self.loop.run_until_complete(parse(macro, self.handler).run()) self.assertListEqual(self.result, [ - ('1 2', 1), ('1 2', 0), + (1, 1), (1, 0), ('a', 1), ('a', 0), (3, 1), (3, 0), ]) @@ -77,7 +77,7 @@ class TestMacros(unittest.TestCase): ]) def test_4(self): - macro = ' r(2,\nk(\rr ).k(-\n )).k(m) ' + macro = ' r(2,\nk(\nr ).k(-\n )).k(m) ' self.loop.run_until_complete(parse(macro, self.handler).run()) self.assertListEqual(self.result, [ ('r', 1), ('r', 0), @@ -89,7 +89,7 @@ class TestMacros(unittest.TestCase): def test_5(self): start = time.time() - macro = 'w(200).r(2,m(w,\rr(2,\tk(r))).w(10).k(k))' + macro = 'w(200).r(2,m(w,\nr(2,\tk(r))).w(10).k(k))' self.loop.run_until_complete(parse(macro, self.handler).run()) num_pauses = 8 + 6 + 4