diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 5f26c539c..5ef3571ca 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -328,6 +328,27 @@ async def task(handler): self.tasks.append(task) + def add_toggle(self, symbol): + """Press when calling toggle the first time, release it next time.""" + _type_check_symbol(symbol) + + async def task(handler): + key = f"_toggle_{symbol}" + + resolved_symbol = _resolve(symbol, [str]) + code = _type_check_symbol(resolved_symbol) + + if macro_variables[key]: + macro_variables[key] = False + handler(EV_KEY, code, 0) + await self._keycode_pause() + else: + macro_variables[key] = True + handler(EV_KEY, code, 1) + await self._keycode_pause() + + self.tasks.append(task) + def add_key_down(self, symbol): """Press the symbol.""" _type_check_symbol(symbol) @@ -387,6 +408,10 @@ async def task(handler): # not-releasing any key await macro.run(handler) + # to avoid extremely fast loops that can freeze input-remapper + # while holding down, sleep for 1 ms + await asyncio.sleep(1 / 1000) + self.tasks.append(task) self.child_macros.append(macro) diff --git a/inputremapper/injection/macros/parse.py b/inputremapper/injection/macros/parse.py index 86fe5d82c..a31353430 100644 --- a/inputremapper/injection/macros/parse.py +++ b/inputremapper/injection/macros/parse.py @@ -48,6 +48,7 @@ def is_this_a_macro(output): "key": Macro.add_key, "key_down": Macro.add_key_down, "key_up": Macro.add_key_up, + "toggle": Macro.add_toggle, "event": Macro.add_event, "wait": Macro.add_wait, "hold": Macro.add_hold, diff --git a/readme/macros.md b/readme/macros.md index 60d3c0744..6e99068c4 100644 --- a/readme/macros.md +++ b/readme/macros.md @@ -244,6 +244,14 @@ Bear in mind that anti-cheat software might detect macros in games. > if_single(key(KEY_A), key(KEY_B), timeout=1000) > ``` +### toggle + +> Press it once to inject a key-down event, press it a second time to inject a key-up event. +> +> ```c# +> toggle(KEY_A) +> ``` + ## Syntax Multiple functions are chained using `.`. diff --git a/tests/unit/test_keycode_mapper.py b/tests/unit/test_keycode_mapper.py index ec0c6220d..e015e06d7 100644 --- a/tests/unit/test_keycode_mapper.py +++ b/tests/unit/test_keycode_mapper.py @@ -76,7 +76,8 @@ def wait(func, timeout=1.0): def calculate_event_number(holdtime, before, after): - """ + """Calculate how many events a k(a).h(k(b)).k(c) macro would inject + Parameters ---------- holdtime : int @@ -91,6 +92,10 @@ def calculate_event_number(holdtime, before, after): # one initial k(a): events = before * 2 holdtime -= keystroke_sleep * 2 + + # because it sleeps for a millisecond to prevent freezes if keystroke_sleep_ms is 0 + holdtime -= 1 + # hold events events += (holdtime / (keystroke_sleep * 2)) * 2 # one trailing k(c) diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 1513df764..359d4b764 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -381,6 +381,41 @@ async def test_fails(self): # it might look like it without the string quotes. self.assertIsNone(parse('"modify(a, b)"', self.context)) + async def test_toggle(self): + macro = parse("toggle(KEY_B).key(KEY_A).toggle(KEY_B)", self.context) + code_a = system_mapping.get("a") + code_b = system_mapping.get("b") + + await macro.run(self.handler) + self.assertListEqual( + self.result, + [ + (EV_KEY, code_b, 1), + (EV_KEY, code_a, 1), + (EV_KEY, code_a, 0), + (EV_KEY, code_b, 0), + ], + ) + self.assertEqual(len(macro.child_macros), 0) + + async def test_toggle_multiple_macro_runs(self): + macro = parse("toggle(KEY_A)", self.context) + code_a = system_mapping.get("a") + + await macro.run(self.handler) + self.assertListEqual(self.result, [(EV_KEY, code_a, 1)]) + self.assertEqual(len(macro.child_macros), 0) + + await macro.run(self.handler) + self.assertListEqual( + self.result, + [ + (EV_KEY, code_a, 1), + (EV_KEY, code_a, 0), + ], + ) + self.assertEqual(len(macro.child_macros), 0) + async def test_0(self): macro = parse("key(1)", self.context) one_code = system_mapping.get("1")