Skip to content

Commit

Permalink
Add parallel macro
Browse files Browse the repository at this point in the history
  • Loading branch information
sezanzeb committed Jan 2, 2025
1 parent 4016f89 commit dba2bd6
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 2 deletions.
12 changes: 10 additions & 2 deletions inputremapper/injection/macros/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from inputremapper.injection.macros.tasks.mod_tap import ModTapTask
from inputremapper.injection.macros.tasks.modify import ModifyTask
from inputremapper.injection.macros.tasks.mouse import MouseTask
from inputremapper.injection.macros.tasks.parallel import ParallelTask
from inputremapper.injection.macros.tasks.repeat import RepeatTask
from inputremapper.injection.macros.tasks.set import SetTask
from inputremapper.injection.macros.tasks.wait import WaitTask
Expand Down Expand Up @@ -76,6 +77,7 @@ class Parser:
"if_single": IfSingleTask,
"add": AddTask,
"mod_tap": ModTapTask,
"parallel": ParallelTask,
# Those are only kept for backwards compatibility with old macros. The space for
# writing macro was very constrained in the past, so shorthands were introduced:
"m": ModifyTask,
Expand Down Expand Up @@ -210,8 +212,14 @@ def _parse_recurse(
code
Just like parse. A single parameter or the complete macro as string.
Comments and redundant whitespace characters are expected to be removed already.
TODO add some examples.
Are all of "foo(1);bar(2)" "foo(1)" and "1" valid inputs?
Example:
- "parallel(key(a),key(b).key($foo))"
- "key(a)"
- "a"
- "key(b).key($foo)"
- "b"
- "key($foo)"
- "$foo"
context : Context
macro_instance
A macro instance to add tasks to. This is the output of the parser, and is
Expand Down
45 changes: 45 additions & 0 deletions inputremapper/injection/macros/tasks/parallel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2024 sezanzeb <[email protected]>
#
# This file is part of input-remapper.
#
# input-remapper 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, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations

import asyncio
from typing import List

from inputremapper.injection.macros.argument import ArgumentConfig, ArgumentFlags
from inputremapper.injection.macros.macro import Macro, InjectEventCallback
from inputremapper.injection.macros.task import Task


class ParallelTask(Task):
"""Run all provided macros in parallel."""

argument_configs = [
ArgumentConfig(
name="*macros",
position=ArgumentFlags.spread,
types=[Macro],
),
]

async def run(self, callback: InjectEventCallback) -> None:
macros: List[Macro] = self.get_argument("*macros").get_values()
coroutines = [macro.run(callback) for macro in macros]
await asyncio.gather(*coroutines)
18 changes: 18 additions & 0 deletions readme/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,24 @@ Bear in mind that anti-cheat software might detect macros in games.
> if_single(key(KEY_A), key(KEY_B), timeout=1000)
> ```
### parallel
> Run all provided macros in parallel.
>
> ```ts
> parallel(*macros: Macro)
> ```
>
> Examples:
>
> ```ts
> parallel(
> mouse(up, 10),
> hold_keys(a),
> wheel(down, 10)
> )
> ```
## Syntax
Multiple functions are chained using `.`.
Expand Down
114 changes: 114 additions & 0 deletions tests/unit/test_macros/test_parallel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2024 sezanzeb <[email protected]>
#
# This file is part of input-remapper.
#
# input-remapper 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, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.

import asyncio
import unittest

from evdev.ecodes import (
EV_KEY,
KEY_A,
KEY_B,
KEY_C,
KEY_D,
)

from inputremapper.injection.macros.parse import Parser
from tests.lib.test_setup import test_setup
from tests.unit.test_macros.macro_test_base import DummyMapping, MacroTestBase


@test_setup
class TestParallel(MacroTestBase):
async def test_1_child_macro(self):
macro = Parser.parse(
"parallel(key(a))",
self.context,
DummyMapping(),
True,
)
self.assertEqual(len(macro.tasks[0].child_macros), 1)
await macro.run(self.handler)
self.assertEqual(self.result, [(EV_KEY, KEY_A, 1), (EV_KEY, KEY_A, 0)])

async def test_4_child_macros(self):
macro = Parser.parse(
"parallel(key(a), key(b), key(c), key(d))",
self.context,
DummyMapping(),
True,
)
self.assertEqual(len(macro.tasks[0].child_macros), 4)
await macro.run(self.handler)
self.assertIn((EV_KEY, KEY_A, 0), self.result)
self.assertIn((EV_KEY, KEY_B, 0), self.result)
self.assertIn((EV_KEY, KEY_C, 0), self.result)
self.assertIn((EV_KEY, KEY_D, 0), self.result)

async def test_one_wait_takes_longer(self):
mapping = DummyMapping()
mapping.macro_key_sleep_ms = 0
macro = Parser.parse(
"parallel(wait(100), wait(10).key(b)).key(c)",
self.context,
mapping,
True,
)

asyncio.ensure_future(macro.run(self.handler))
await asyncio.sleep(0.06)
# The wait(10).key(b) macro is already done, but KEY_C is not yet injected
self.assertEqual(len(self.result), 2)
self.assertIn((EV_KEY, KEY_B, 1), self.result)
self.assertIn((EV_KEY, KEY_B, 0), self.result)

# Both need to complete for it to continue to key(c)
await asyncio.sleep(0.06)
self.assertEqual(len(self.result), 4)
self.assertIn((EV_KEY, KEY_C, 1), self.result)
self.assertIn((EV_KEY, KEY_C, 0), self.result)

async def test_parallel_hold(self):
mapping = DummyMapping()
mapping.macro_key_sleep_ms = 0
macro = Parser.parse(
"parallel(hold_keys(a), hold_keys(b)).key(c)",
self.context,
mapping,
True,
)

macro.press_trigger()
asyncio.ensure_future(macro.run(self.handler))
await asyncio.sleep(0.05)
self.assertIn((EV_KEY, KEY_A, 1), self.result)
self.assertIn((EV_KEY, KEY_B, 1), self.result)
self.assertEqual(len(self.result), 2)

macro.release_trigger()
await asyncio.sleep(0.05)
self.assertIn((EV_KEY, KEY_A, 0), self.result)
self.assertIn((EV_KEY, KEY_B, 0), self.result)
self.assertIn((EV_KEY, KEY_C, 1), self.result)
self.assertIn((EV_KEY, KEY_C, 0), self.result)
self.assertEqual(len(self.result), 6)


if __name__ == "__main__":
unittest.main()

0 comments on commit dba2bd6

Please sign in to comment.