writing combinations by using 'key + key + ...'

This commit is contained in:
sezanzeb 2021-02-12 21:43:40 +01:00 committed by sezanzeb
parent 566823bed4
commit f06d33ad34
15 changed files with 137 additions and 26 deletions

View File

@ -55,6 +55,10 @@ def is_this_a_macro(output):
if not isinstance(output, str):
return False
if '+' in output.strip():
# for example "a + b"
return True
return '(' in output and ')' in output and len(output) >= 4
@ -441,6 +445,34 @@ def _parse_recurse(macro, mapping, macro_instance=None, depth=0):
return macro
def handle_plus_syntax(macro):
"""transform a + b + c to m(a, m(b, m(c, h())))"""
if '+' not in macro:
return macro
if '(' in macro or ')' in macro:
logger.error('Mixing "+" and macros is unsupported: "%s"', macro)
return macro
chunks = [chunk.strip() for chunk in macro.split('+')]
output = ''
depth = 0
for chunk in chunks:
if chunk == '':
# invalid syntax
logger.error('Invalid syntax for "%s"', macro)
return macro
depth += 1
output += f'm({chunk},'
output += 'h()'
output += depth * ')'
logger.debug('Transformed "%s" to "%s"', macro, output)
return output
def parse(macro, mapping, return_errors=False):
"""parse and generate a _Macro that can be run as often as you want.
@ -460,6 +492,8 @@ def parse(macro, mapping, return_errors=False):
If True, returns errors as a string or None if parsing worked.
If False, returns the parsed macro.
"""
macro = handle_plus_syntax(macro)
# whitespaces, tabs, newlines and such don't serve a purpose. make
# the log output clearer and the parsing easier.
macro = re.sub(r'\s', '', macro)

View File

@ -30,7 +30,7 @@ requests.
- [x] mapping joystick directions as buttons, making it act like a D-Pad
- [x] mapping mouse wheel events to buttons
- [x] automatically load presets when devices get plugged in after login (udev)
- [ ] map keys using a `modifier + modifier + ... + key` syntax
- [x] map keys using a `modifier + modifier + ... + key` syntax
- [ ] injecting keys that aren't available in the systems keyboard layout
## Tests

BIN
readme/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -60,6 +60,16 @@ your focused application.
<img src="combination.png"/>
</p>
## Writing Combinations
You can write `control_l + a` as mapping, which will inject those two
keycodes into your system on a single key press. An arbitrary number of
names can be chained using ` + `.
<p align="center">
<img src="plus.png"/>
</p>
## Macros
It is possible to write timed macros into the center column:

View File

@ -411,9 +411,11 @@ _fixture_copy = copy.deepcopy(fixtures)
environ_copy = copy.deepcopy(os.environ)
def cleanup():
def quick_cleanup(log=True):
"""Reset the applications state."""
print('cleanup')
if log:
print('quick cleanup')
keycode_reader.stop_reading()
keycode_reader.__init__()
@ -421,10 +423,6 @@ def cleanup():
for task in asyncio.all_tasks():
task.cancel()
os.system('pkill -f key-mapper-service')
time.sleep(0.05)
if os.path.exists(tmp):
shutil.rmtree(tmp)
@ -459,6 +457,20 @@ def cleanup():
if key not in environ_copy:
del os.environ[key]
def cleanup():
"""Reset the applications state.
Using this is very slow, usually quick_cleanup() is sufficient.
"""
print('cleanup')
os.system('pkill -f key-mapper-service')
time.sleep(0.05)
quick_cleanup(log=False)
refresh_devices()

View File

@ -25,12 +25,12 @@ import unittest
from keymapper.config import config, GlobalConfig
from keymapper.paths import touch, CONFIG_PATH
from tests.test import cleanup, tmp
from tests.test import quick_cleanup, tmp
class TestConfig(unittest.TestCase):
def tearDown(self):
cleanup()
quick_cleanup()
self.assertEqual(len(config.iterate_autoload_presets()), 0)
def test_migrate(self):

View File

@ -35,7 +35,7 @@ from keymapper.daemon import Daemon
from keymapper.mapping import Mapping
from keymapper.paths import get_preset_path
from tests.test import cleanup, tmp
from tests.test import quick_cleanup, tmp
def import_control():
@ -63,7 +63,7 @@ options = collections.namedtuple(
class TestControl(unittest.TestCase):
def tearDown(self):
cleanup()
quick_cleanup()
def test_autoload(self):
devices = ['device 1', 'device 2']

View File

@ -30,7 +30,7 @@ from keymapper.mapping import Mapping
from keymapper.dev.event_producer import EventProducer, MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
uinput_write_history, cleanup, new_event
uinput_write_history, quick_cleanup, new_event
abs_state = [0, 0, 0, 0]
@ -55,7 +55,7 @@ class TestEventProducer(unittest.TestCase):
config.set('gamepad.joystick.y_scroll_speed', 1)
def tearDown(self):
cleanup()
quick_cleanup()
def test_debounce_1(self):
loop = asyncio.get_event_loop()

View File

@ -41,7 +41,7 @@ from keymapper.getdevices import get_devices, is_gamepad
from tests.test import new_event, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, cleanup, read_write_history_pipe, InputDevice
MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice
original_smeab = utils.should_map_event_as_btn
@ -75,7 +75,7 @@ class TestInjector(unittest.TestCase):
self.injector = None
evdev.InputDevice.grab = self.grab
cleanup()
quick_cleanup()
def test_grab(self):
# path is from the fixtures
@ -827,7 +827,7 @@ class TestModifyCapabilities(unittest.TestCase):
self.assertNotIn(DISABLE_CODE, keys)
def tearDown(self):
cleanup()
quick_cleanup()
def test_modify_capabilities(self):
self.injector = Injector('foo', self.mapping)

View File

@ -34,7 +34,7 @@ from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import new_event, UInput, uinput_write_history, \
cleanup, InputDevice, MAX_ABS
quick_cleanup, InputDevice, MAX_ABS
def wait(func, timeout=1.0):
@ -84,7 +84,7 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertFalse(macro.is_holding())
self.assertFalse(macro.running)
cleanup()
quick_cleanup()
def test_subsets(self):
a = subsets(((1,), (2,), (3,)))

View File

@ -24,11 +24,13 @@ import unittest
import asyncio
from keymapper.dev.macros import parse, _Macro, _extract_params, \
is_this_a_macro, _parse_recurse
is_this_a_macro, _parse_recurse, handle_plus_syntax
from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.state import system_mapping
from tests.test import quick_cleanup
class TestMacros(unittest.TestCase):
def setUp(self):
@ -39,6 +41,7 @@ class TestMacros(unittest.TestCase):
def tearDown(self):
self.result = []
self.mapping.clear_config()
quick_cleanup()
def handler(self, code, value):
"""Where macros should write codes to."""
@ -55,6 +58,58 @@ class TestMacros(unittest.TestCase):
self.assertFalse(is_this_a_macro('minus'))
self.assertFalse(is_this_a_macro('k'))
self.assertTrue(is_this_a_macro('a+b'))
self.assertTrue(is_this_a_macro('a+b+c'))
self.assertTrue(is_this_a_macro('a + b'))
self.assertTrue(is_this_a_macro('a + b + c'))
def test_handle_plus_syntax(self):
self.assertEqual(handle_plus_syntax('a + b'), 'm(a,m(b,h()))')
self.assertEqual(handle_plus_syntax('a + b + c'), 'm(a,m(b,m(c,h())))')
self.assertEqual(handle_plus_syntax(' a+b+c '), 'm(a,m(b,m(c,h())))')
# invalid
self.assertEqual(handle_plus_syntax('+'), '+')
self.assertEqual(handle_plus_syntax('a+'), 'a+')
self.assertEqual(handle_plus_syntax('+b'), '+b')
self.assertEqual(handle_plus_syntax('k(a + b)'), 'k(a + b)')
self.assertEqual(handle_plus_syntax('a'), 'a')
self.assertEqual(handle_plus_syntax('k(a)'), 'k(a)')
self.assertEqual(handle_plus_syntax(''), '')
def test_run_plus_syntax(self):
macro = parse('a + b + c + d', self.mapping)
macro.set_handler(self.handler)
self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('a'),
system_mapping.get('b'),
system_mapping.get('c'),
system_mapping.get('d')
})
macro.press_key()
asyncio.ensure_future(macro.run())
self.loop.run_until_complete(asyncio.sleep(0.2))
self.assertTrue(macro.is_holding())
print(self.mapping.get('macros.keystroke_sleep_ms'))
print(self.result)
# starting from the left, presses each one down
self.assertEqual(self.result[0], (system_mapping.get('a'), 1))
self.assertEqual(self.result[1], (system_mapping.get('b'), 1))
self.assertEqual(self.result[2], (system_mapping.get('c'), 1))
self.assertEqual(self.result[3], (system_mapping.get('d'), 1))
# and then releases starting with the previously pressed key
macro.release_key()
self.loop.run_until_complete(asyncio.sleep(0.2))
self.assertFalse(macro.is_holding())
print(self.result)
self.assertEqual(self.result[4], (system_mapping.get('d'), 0))
self.assertEqual(self.result[5], (system_mapping.get('c'), 0))
self.assertEqual(self.result[6], (system_mapping.get('b'), 0))
self.assertEqual(self.result[7], (system_mapping.get('a'), 0))
def test_extract_params(self):
def expect(raw, expectation):
self.assertListEqual(_extract_params(raw), expectation)

View File

@ -31,12 +31,12 @@ from keymapper.config import config
from keymapper.paths import get_preset_path
from keymapper.key import Key
from tests.test import tmp, cleanup
from tests.test import tmp, quick_cleanup
class TestSystemMapping(unittest.TestCase):
def tearDown(self):
cleanup()
quick_cleanup()
def test_update(self):
system_mapping = SystemMapping()
@ -113,7 +113,7 @@ class TestMapping(unittest.TestCase):
self.assertFalse(self.mapping.changed)
def tearDown(self):
cleanup()
quick_cleanup()
def test_config(self):
self.mapping.save(get_preset_path('foo', 'bar2'))

View File

@ -25,7 +25,7 @@ import unittest
from keymapper.paths import get_user, touch, mkdir, \
get_preset_path, get_config_path
from tests.test import cleanup, tmp
from tests.test import quick_cleanup, tmp
original_getlogin = os.getlogin()
@ -37,7 +37,7 @@ def _raise(error):
class TestPaths(unittest.TestCase):
def tearDown(self):
cleanup()
quick_cleanup()
os.getlogin = original_getlogin
def test_get_user(self):

View File

@ -34,7 +34,7 @@ from keymapper.config import BUTTONS, MOUSE
from keymapper.key import Key
from tests.test import new_event, pending_events, EVENT_READ_TIMEOUT, \
cleanup, MAX_ABS
quick_cleanup, MAX_ABS
CODE_1 = 100
@ -59,7 +59,7 @@ class TestReader(unittest.TestCase):
self.assertEqual(keycode_reader.read(), None)
def tearDown(self):
cleanup()
quick_cleanup()
def test_will_report_up(self):
self.assertFalse(will_report_up(EV_REL))