mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-18 03:25:52 +00:00
writing combinations by using 'key + key + ...'
This commit is contained in:
parent
566823bed4
commit
f06d33ad34
@ -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)
|
||||
|
@ -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
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 |
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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']
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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,)))
|
||||
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user