some fixes and improvements

xkb
sezanzeb 4 years ago committed by sezanzeb
parent e3a56d069a
commit 58f4788368

@ -40,6 +40,7 @@ check the output of `xmodmap -pke`
If you can't find what you need, consult
[linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h)
for KEY and BTN names
- Mouse buttons `BTN_LEFT` `BTN_RIGHT` `BTN_MIDDLE` `BTN_SIDE`
- Multimedia keys `KEY_NEXTSONG` `KEY_PLAYPAUSE` ...

@ -37,7 +37,7 @@ from keymapper.state import system_mapping
from keymapper.dev.keycode_mapper import handle_keycode, \
should_map_event_as_btn
from keymapper.dev.ev_abs_mapper import ev_abs_mapper, JOYSTICK
from keymapper.dev.macros import parse
from keymapper.dev.macros import parse, is_this_a_macro
DEV_NAME = 'key-mapper'
@ -101,12 +101,38 @@ class KeycodeInjector:
self.mapping = mapping
self._process = None
self._msg_pipe = multiprocessing.Pipe()
self._code_to_code = self._map_codes_to_codes()
self.stopped = False
# some EV_ABS mapping stuff
# when moving the joystick and then staying at a position, no
# events will be written anymore. Remember the last value the
# joystick reported, because it is still remaining at that
# position.
self.abs_state = [0, 0, 0, 0]
def _map_codes_to_codes(self):
"""To quickly get target keycodes during operation."""
_code_to_code = {}
for (ev_type, keycode), output in self.mapping:
if is_this_a_macro(output):
continue
target_keycode = system_mapping.get(output)
if target_keycode is None:
logger.error('Don\'t know what %s is', output)
continue
_code_to_code[keycode] = target_keycode
return _code_to_code
def start_injecting(self):
"""Start injecting keycodes."""
if self.stopped or self._process is not None:
# So that there is less concern about integrity when putting
# stuff into self. Each injector object can only be
# started once.
raise Exception('Please construct a new injector instead')
self._process = multiprocessing.Process(target=self._start_injecting)
self._process.start()
@ -172,11 +198,16 @@ class KeycodeInjector:
return device, abs_to_rel
def _modify_capabilities(self, input_device, abs_to_rel):
"""Adds all keycode into a copy of a devices capabilities.
def _modify_capabilities(self, macros, input_device, abs_to_rel):
"""Adds all used keycodes into a copy of a devices capabilities.
A device with those capabilities can do exactly the stuff it needs
to perform all mappings and macros.
Prameters
---------
macros : dict
maping of int to _Macro
input_device : evdev.InputDevice
abs_to_rel : bool
if ABS capabilities should be removed in favor of REL
@ -187,20 +218,21 @@ class KeycodeInjector:
# to act like the device.
capabilities = input_device.capabilities(absinfo=False)
# Furthermore, support all injected keycodes
if len(self.mapping) > 0 and capabilities.get(ecodes.EV_KEY) is None:
capabilities[ecodes.EV_KEY] = []
if len(self._code_to_code) > 0 or len(macros) > 0:
if capabilities.get(EV_KEY) is None:
capabilities[EV_KEY] = []
for (ev_type, keycode), character in self.mapping:
if not should_map_event_as_btn(ev_type, keycode):
continue
keycode = system_mapping.get(character)
if keycode is not None:
# Furthermore, support all injected keycodes
for keycode in self._code_to_code.values():
if keycode not in capabilities[EV_KEY]:
capabilities[EV_KEY].append(keycode)
# and all keycodes that are injected by macros
for macro in macros.values():
capabilities[EV_KEY] += list(macro.get_capabilities())
if abs_to_rel:
del capabilities[ecodes.EV_ABS]
del capabilities[EV_ABS]
# those are the requirements to recognize it as mouse
# on my system. REL_X and REL_Y are of course required to
# accept the events that the mouse-movement-mapper writes.
@ -209,11 +241,11 @@ class KeycodeInjector:
evdev.ecodes.REL_Y,
evdev.ecodes.REL_WHEEL,
]
if capabilities.get(ecodes.EV_KEY) is None:
capabilities[ecodes.EV_KEY] = []
if capabilities.get(EV_KEY) is None:
capabilities[EV_KEY] = []
# for reasons I don't know, it is also required to have
# any keyboard button in capabilities.
capabilities[ecodes.EV_KEY].append(ecodes.KEY_0)
capabilities[EV_KEY].append(ecodes.KEY_0)
# just like what python-evdev does in from_device
if ecodes.EV_SYN in capabilities:
@ -253,32 +285,44 @@ class KeycodeInjector:
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
input_device, abs_to_rel = self._prepare_device(path)
if input_device is None:
source, abs_to_rel = self._prepare_device(path)
if source is None:
continue
# each device parses the macros with a different handler
logger.debug('Parsing macros for %s', path)
macros = {}
for (ev_type, keycode), output in self.mapping:
if is_this_a_macro(output):
macros[keycode] = parse(output)
continue
# certain capabilities can have side effects apparently. with an
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device.
uinput = evdev.UInput(
name=f'{DEV_NAME} {self.device}',
phys=DEV_NAME,
events=self._modify_capabilities(input_device, abs_to_rel)
events=self._modify_capabilities(macros, source, abs_to_rel)
)
def handler(*args, uinput=uinput):
# this ensures that the right uinput is used for macro_write,
# because this is within a loop
self._macro_write(*args, uinput)
for macro in macros.values():
macro.set_handler(handler)
# keycode injection
coroutine = self._keycode_loop(input_device, uinput, abs_to_rel)
coroutine = self._keycode_loop(macros, source, uinput, abs_to_rel)
coroutines.append(coroutine)
# mouse movement injection
if abs_to_rel:
self.abs_state[0] = 0
self.abs_state[1] = 0
coroutine = ev_abs_mapper(
self.abs_state,
input_device,
uinput
)
coroutine = ev_abs_mapper(self.abs_state, source, uinput)
coroutines.append(coroutine)
if len(coroutines) == 0:
@ -296,59 +340,34 @@ class KeycodeInjector:
if len(coroutines) > 0:
logger.debug('asyncio coroutines ended')
def _macro_write(self, character, value, uinput):
def _macro_write(self, code, value, uinput):
"""Handler for macros."""
keycode = system_mapping[character]
logger.spam(
'macro writes code:%s value:%d char:%s',
keycode, value, character
)
uinput.write(EV_KEY, keycode, value)
logger.spam('macro writes code:%s value:%d', code, value)
uinput.write(EV_KEY, code, value)
uinput.syn()
async def _keycode_loop(self, device, uinput, abs_to_rel):
async def _keycode_loop(self, macros, source, uinput, abs_to_rel):
"""Inject keycodes for one of the virtual devices.
Can be stopped by stopping the asyncio loop.
Parameters
----------
device : evdev.InputDevice
macros : int -> _Macro
macro with a handler that writes to the provided uinput
source : evdev.InputDevice
where to read keycodes from
uinput : evdev.UInput
where to write keycodes to
abs_to_rel : bool
if joystick events should be mapped to mouse movements
"""
# efficiently figure out the target keycode without taking
# extra steps.
code_code_mapping = {}
# Parse all macros beforehand
logger.debug('Parsing macros')
macros = {}
for (ev_type, keycode), output in self.mapping:
if '(' in output and ')' in output and len(output) >= 4:
# probably a macro
macros[keycode] = parse(
output,
lambda *args: self._macro_write(*args, uinput)
)
continue
target_keycode = system_mapping.get(output)
if target_keycode is None:
logger.error('Don\'t know what %s is', output)
continue
code_code_mapping[keycode] = target_keycode
logger.debug(
'Started injecting into %s, fd %s',
uinput.device.path, uinput.fd
)
async for event in device.async_read_loop():
async for event in source.async_read_loop():
if abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
if event.code == evdev.ecodes.ABS_X:
self.abs_state[0] = event.value
@ -361,7 +380,7 @@ class KeycodeInjector:
continue
if should_map_event_as_btn(event.type, event.code):
handle_keycode(code_code_mapping, macros, event, uinput)
handle_keycode(self._code_to_code, macros, event, uinput)
continue
# forward the rest
@ -369,9 +388,6 @@ class KeycodeInjector:
# this already includes SYN events, so need to syn here again
continue
# this should only ever happen in tests to avoid blocking them
# forever, as soon as all events are consumed. In normal operation
# there is no end to the events.
logger.error(
'The injector for "%s" stopped early',
uinput.device.path
@ -382,3 +398,4 @@ class KeycodeInjector:
"""Stop injecting keycodes."""
logger.info('Stopping injecting keycodes for device "%s"', self.device)
self._msg_pipe[1].send(CLOSE)
self.stopped = True

@ -52,12 +52,12 @@ def should_map_event_as_btn(type, code):
return False
def handle_keycode(code_code_mapping, macros, event, uinput):
def handle_keycode(_code_to_code, macros, event, uinput):
"""Write the mapped keycode or forward unmapped ones.
Parameters
----------
code_code_mapping : dict
_code_to_code : dict
mapping of linux-keycode to linux-keycode.
macros : dict
mapping of linux-keycode to _Macro objects
@ -84,8 +84,8 @@ def handle_keycode(code_code_mapping, macros, event, uinput):
asyncio.ensure_future(macro.run())
return
if input_keycode in code_code_mapping:
target_keycode = code_code_mapping[input_keycode]
if input_keycode in _code_to_code:
target_keycode = _code_to_code[input_keycode]
target_type = evdev.events.EV_KEY
logger.spam(
'got code:%s value:%s event:%s, maps to EV_KEY:%s',

@ -40,9 +40,9 @@ import re
from keymapper.logger import logger
from keymapper.config import config
from keymapper.state import system_mapping
# for debugging purposes
MODIFIER = 1
CHILD_MACRO = 2
SLEEP = 3
@ -51,17 +51,19 @@ KEYSTROKE = 5
DEBUG = 6
def is_this_a_macro(output):
"""Figure out if this is a macro."""
if '(' in output and ')' in output and len(output) >= 4:
return True
class _Macro:
"""Supports chaining and preparing actions."""
def __init__(self, handler, depth, code):
def __init__(self, depth, code):
"""Create a macro instance that can be populated with tasks.
Parameters
----------
handler : func
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.
@ -69,13 +71,42 @@ class _Macro:
The original parsed code, for logging purposes.
"""
self.tasks = []
self.handler = handler
self.handler = lambda *args: logger.error('No handler set')
self.depth = depth
self.code = code
# all required capabilities, without those of child macros
self.capabilities = set()
def get_capabilities(self):
"""Resolve all capabilities of the macro and those of its children."""
capabilities = self.capabilities.copy()
for task_type, task in self.tasks:
if task_type == CHILD_MACRO:
capabilities.update(task.get_capabilities())
return capabilities
def set_handler(self, handler):
"""Set the handler function.
Parameters
----------
handler : func
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()`.
"""
self.handler = handler
for task_type, task in self.tasks:
if task_type == CHILD_MACRO:
task.set_handler(handler)
async def run(self):
"""Run the macro."""
for _, task in self.tasks:
for task_type, task in self.tasks:
if task_type == CHILD_MACRO:
task = task.run
coroutine = task()
if asyncio.iscoroutine(coroutine):
await coroutine
@ -88,11 +119,19 @@ class _Macro:
modifier : str
macro : _Macro
"""
self.tasks.append((MODIFIER, lambda: self.handler(modifier, 1)))
modifier = str(modifier)
code = system_mapping.get(modifier)
if code is None:
raise KeyError(f'Unknown modifier "{modifier}"')
self.capabilities.add(code)
self.tasks.append((MODIFIER, lambda: self.handler(code, 1)))
self.add_keycode_pause()
self.tasks.append((CHILD_MACRO, macro.run))
self.tasks.append((CHILD_MACRO, macro))
self.add_keycode_pause()
self.tasks.append((MODIFIER, lambda: self.handler(modifier, 0)))
self.tasks.append((MODIFIER, lambda: self.handler(code, 0)))
self.add_keycode_pause()
return self
@ -104,8 +143,9 @@ class _Macro:
repeats : int
macro : _Macro
"""
repeats = int(repeats)
for _ in range(repeats):
self.tasks.append((CHILD_MACRO, macro.run))
self.tasks.append((CHILD_MACRO, macro))
return self
def add_keycode_pause(self):
@ -119,14 +159,23 @@ class _Macro:
def keycode(self, character):
"""Write the character."""
self.tasks.append((KEYSTROKE, lambda: self.handler(character, 1)))
character = str(character)
code = system_mapping.get(character)
if code is None:
raise KeyError(f'Unknown key "{character}"')
self.capabilities.add(code)
self.tasks.append((KEYSTROKE, lambda: self.handler(code, 1)))
self.add_keycode_pause()
self.tasks.append((KEYSTROKE, lambda: self.handler(character, 0)))
self.tasks.append((KEYSTROKE, lambda: self.handler(code, 0)))
self.add_keycode_pause()
return self
def wait(self, sleeptime):
"""Wait time in milliseconds."""
sleeptime = int(sleeptime)
sleeptime /= 1000
async def sleep():
@ -191,15 +240,13 @@ def _count_brackets(macro):
return position
def _parse_recurse(macro, handler, macro_instance=None, depth=0):
def _parse_recurse(macro, macro_instance=None, depth=0):
"""Handle a subset of the macro, e.g. one parameter or function call.
Parameters
----------
macro : string
Just like parse
handler : function
passed to _Macro constructors
macro_instance : _Macro or None
A macro instance to add tasks to
depth : int
@ -211,11 +258,10 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0):
# If this gets more complicated than that I'd rather make a macro
# editor GUI and store them as json.
assert isinstance(macro, str)
assert callable(handler)
assert isinstance(depth, int)
if macro_instance is None:
macro_instance = _Macro(handler, depth, macro)
macro_instance = _Macro(depth, macro)
else:
assert isinstance(macro_instance, _Macro)
@ -247,7 +293,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0):
logger.spam('%scalls %s with %s', space, call, string_params)
# evaluate the params
params = [
_parse_recurse(param.strip(), handler, None, depth + 1)
_parse_recurse(param.strip(), None, depth + 1)
for param in string_params
]
@ -258,7 +304,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0):
if len(macro) > position and macro[position] == '.':
chain = macro[position + 1:]
logger.spam('%sfollowed by %s', space, chain)
_parse_recurse(chain, handler, macro_instance, depth)
_parse_recurse(chain, macro_instance, depth)
return macro_instance
@ -271,7 +317,7 @@ def _parse_recurse(macro, handler, macro_instance=None, depth=0):
return macro
def parse(macro, handler):
def parse(macro):
"""parse and generate a _Macro that can be run as often as you want.
Parameters
@ -280,17 +326,13 @@ def parse(macro, handler):
"r(3, k(a).w(10))"
"r(2, k(a).k(-)).k(b)"
"w(1000).m(Shift_L, r(2, k(a))).w(10, 20).k(b)"
handler : func
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()`.
"""
# 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)
logger.spam('preparing macro %s for later execution', macro)
try:
return _parse_recurse(macro, handler)
return _parse_recurse(macro)
except Exception as error:
logger.error('Failed to parse macro "%s": %s', macro, error)
return None

@ -34,32 +34,52 @@ from keymapper.mapping import Mapping
XKB_KEYCODE_OFFSET = 8
def populate_system_mapping():
"""Get a mapping of all available names to their keycodes."""
mapping = {}
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
for keycode, names in mappings:
# there might be multiple, like:
# keycode 64 = Alt_L Meta_L Alt_L Meta_L
# keycode 204 = NoSymbol Alt_L NoSymbol Alt_L
# only the first column is relevant. The others can be achieved
# by using a modifier button and the mapped key
name = names.split()[0]
mapping[name] = int(keycode) - XKB_KEYCODE_OFFSET
for name, ecode in evdev.ecodes.ecodes.items():
mapping[name] = ecode
return mapping
def clear_system_mapping():
"""Remove all mapped keys. Only needed for tests."""
keys = list(system_mapping.keys())
for key in keys:
del system_mapping[key]
class SystemMapping:
"""Stores information about all available keycodes."""
def __init__(self):
"""Construct the system_mapping."""
self._mapping = {}
self.populate()
def populate(self):
"""Get a mapping of all available names to their keycodes."""
self.clear()
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
for keycode, names in mappings:
# there might be multiple, like:
# keycode 64 = Alt_L Meta_L Alt_L Meta_L
# keycode 204 = NoSymbol Alt_L NoSymbol Alt_L
# Alt_L should map to code 64. Writing code 204 only works
# if a modifier is applied at the same time. So take the first
# one.
name = names.split()[0]
self._set(name, int(keycode) - XKB_KEYCODE_OFFSET)
for keycode, names in mappings:
# but since KP may be mapped like KP_Home KP_7 KP_Home KP_7,
# make another pass and add all of them if they don't already
# exist. don't overwrite any keycodes.
for name in names.split():
if self.get(name) is None:
self._set(name, int(keycode) - XKB_KEYCODE_OFFSET)
for name, ecode in evdev.ecodes.ecodes.items():
self._set(name, ecode)
def _set(self, name, code):
"""Map name to code."""
self._mapping[str(name).lower()] = code
def get(self, name):
"""Return the code mapped to the key."""
return self._mapping.get(str(name).lower())
def clear(self):
"""Remove all mapped keys. Only needed for tests."""
keys = list(self._mapping.keys())
for key in keys:
del self._mapping[key]
# one mapping object for the whole application that holds all
@ -67,7 +87,7 @@ def clear_system_mapping():
custom_mapping = Mapping()
# this mapping represents the xmodmap output, which stays constant
system_mapping = populate_system_mapping()
system_mapping = SystemMapping()
# permissions for files created in /usr
_PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH

@ -30,8 +30,7 @@ import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping
from keymapper.state import custom_mapping, system_mapping
from keymapper.config import config
from keymapper.daemon import Daemon, get_dbus_interface, BUS_NAME
@ -78,6 +77,7 @@ class TestDaemon(unittest.TestCase):
self.daemon = None
evdev.InputDevice.grab = self.grab
config.clear_config()
system_mapping.populate()
def test_daemon(self):
keycode_from_1 = 9
@ -87,9 +87,9 @@ class TestDaemon(unittest.TestCase):
custom_mapping.change((EV_KEY, keycode_from_1), 'a')
custom_mapping.change((EV_KEY, keycode_from_2), 'b')
clear_system_mapping()
system_mapping['a'] = keycode_to_1
system_mapping['b'] = keycode_to_2
system_mapping.clear()
system_mapping._set('a', keycode_to_1)
system_mapping._set('b', keycode_to_2)
preset = 'foo'

@ -29,8 +29,7 @@ from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X
from keymapper.dev.injector import is_numlock_on, toggle_numlock, \
ensure_numlock, KeycodeInjector
from keymapper.dev.keycode_mapper import handle_keycode
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping
from keymapper.config import config
from keymapper.dev.macros import parse
@ -67,6 +66,7 @@ class TestInjector(unittest.TestCase):
del pending_events[key]
clear_write_history()
custom_mapping.empty()
system_mapping.populate()
def test_modify_capabilities(self):
class FakeDevice:
@ -80,21 +80,34 @@ class TestInjector(unittest.TestCase):
mapping = Mapping()
mapping.change((EV_KEY, 80), 'a')
# going to be ignored
macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code)
mapping.change((EV_KEY, 60), macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change((EV_REL, 1234), 'b')
maps_to = system_mapping['a']
a = system_mapping.get('a')
shift_l = system_mapping.get('ShIfT_L')
one = system_mapping.get(1)
two = system_mapping.get('2')
self.injector = KeycodeInjector('foo', mapping)
fake_device = FakeDevice()
capabilities = self.injector._modify_capabilities(
{60: macro},
fake_device,
abs_to_rel=False
)
self.assertIn(EV_KEY, capabilities)
keys = capabilities[EV_KEY]
self.assertEqual(keys[0], maps_to)
self.assertIn(a, keys)
self.assertIn(one, keys)
self.assertIn(two, keys)
self.assertIn(shift_l, keys)
self.assertNotIn(evdev.ecodes.EV_SYN, capabilities)
self.assertNotIn(evdev.ecodes.EV_FF, capabilities)
@ -154,7 +167,11 @@ class TestInjector(unittest.TestCase):
device, abs_to_rel = self.injector._prepare_device(path)
self.assertTrue(abs_to_rel)
capabilities = self.injector._modify_capabilities(device, abs_to_rel)
capabilities = self.injector._modify_capabilities(
{},
device,
abs_to_rel
)
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
@ -269,7 +286,7 @@ class TestInjector(unittest.TestCase):
self.assertAlmostEqual(history[-2][2], -1)
def test_handle_keycode(self):
code_code_mapping = {
_code_to_code = {
1: 101,
2: 102
}
@ -287,9 +304,9 @@ class TestInjector(unittest.TestCase):
EV_KEY = evdev.ecodes.EV_KEY
handle_keycode(code_code_mapping, {}, Event(EV_KEY, 1, 1), uinput)
handle_keycode(code_code_mapping, {}, Event(EV_KEY, 3, 1), uinput)
handle_keycode(code_code_mapping, {}, Event(EV_KEY, 2, 1), uinput)
handle_keycode(_code_to_code, {}, Event(EV_KEY, 1, 1), uinput)
handle_keycode(_code_to_code, {}, Event(EV_KEY, 3, 1), uinput)
handle_keycode(_code_to_code, {}, Event(EV_KEY, 2, 1), uinput)
self.assertEqual(len(history), 3)
self.assertEqual(history[0], (EV_KEY, 101, 1))
@ -299,18 +316,19 @@ class TestInjector(unittest.TestCase):
def test_handle_keycode_macro(self):
history = []
macro_mapping = {
1: parse('k(a)', lambda *args: history.append(args)),
2: parse('r(5, k(b))', lambda *args: history.append(args))
}
code_a = 100
code_b = 101
system_mapping['a'] = code_a
system_mapping['b'] = code_b
clear_system_mapping()
system_mapping.clear()
system_mapping._set('a', code_a)
system_mapping._set('b', code_b)
EV_KEY = evdev.ecodes.EV_KEY
macro_mapping = {
1: parse('k(a)'),
2: parse('r(5, k(b))')
}
macro_mapping[1].set_handler(lambda *args: history.append(args))
macro_mapping[2].set_handler(lambda *args: history.append(args))
handle_keycode({}, macro_mapping, Event(EV_KEY, 1, 1), None)
handle_keycode({}, macro_mapping, Event(EV_KEY, 2, 1), None)
@ -326,10 +344,10 @@ class TestInjector(unittest.TestCase):
# 6 keycodes written, with down and up events
self.assertEqual(len(history), 12)
self.assertIn(('a', 1), history)
self.assertIn(('a', 0), history)
self.assertIn(('b', 1), history)
self.assertIn(('b', 0), history)
self.assertIn((code_a, 1), history)
self.assertIn((code_a, 0), history)
self.assertIn((code_b, 1), history)
self.assertIn((code_b, 0), history)
def test_injector(self):
custom_mapping.change((EV_KEY, 8), 'k(KEY_Q).k(w)')
@ -338,13 +356,13 @@ class TestInjector(unittest.TestCase):
input_b = 10
custom_mapping.change((EV_KEY, input_b), 'b')
clear_system_mapping()
system_mapping.clear()
code_a = 100
code_q = 101
code_w = 102
system_mapping['a'] = code_a
system_mapping['KEY_Q'] = code_q
system_mapping['w'] = code_w
system_mapping._set('a', code_a)
system_mapping._set('key_q', code_q)
system_mapping._set('w', code_w)
# the second arg of those event objects is 8 lower than the
# keycode used in X and in the mappings

@ -35,8 +35,7 @@ import shutil
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping
from keymapper.state import custom_mapping, system_mapping
from keymapper.paths import CONFIG, get_config_path
from keymapper.config import config
from keymapper.dev.reader import keycode_reader
@ -110,6 +109,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration()
shutil.rmtree('/tmp/key-mapper-test')
clear_write_history()
system_mapping.populate()
def get_rows(self):
return self.window.get('key_list').get_children()
@ -428,8 +428,8 @@ class TestIntegration(unittest.TestCase):
keycode_to = 200
self.change_empty_row(keycode_from, 'a')
clear_system_mapping()
system_mapping['a'] = keycode_to
system_mapping.clear()
system_mapping._set('a', keycode_to)
pending_events['device 2'] = [
Event(evdev.events.EV_KEY, keycode_from, 1),
@ -465,8 +465,8 @@ class TestIntegration(unittest.TestCase):
keycode_to = 90
self.change_empty_row(keycode_from, 't')
clear_system_mapping()
system_mapping['t'] = keycode_to
system_mapping.clear()
system_mapping._set('t', keycode_to)
# not all of those events should be processed, since that takes some
# time due to time.sleep in the fakes and the injection is stopped.

@ -25,6 +25,7 @@ import asyncio
from keymapper.dev.macros import parse, _Macro
from keymapper.config import config
from keymapper.state import system_mapping
class TestMacros(unittest.TestCase):
@ -35,38 +36,70 @@ class TestMacros(unittest.TestCase):
def tearDown(self):
self.result = []
def handler(self, char, value):
"""Where macros should write characters to."""
self.result.append((char, value))
def handler(self, code, value):
"""Where macros should write codes to."""
self.result.append((code, value))
def test_set_handler(self):
macro = parse('r(1, r(1, k(1)))')
one_code = system_mapping.get('1')
self.assertSetEqual(macro.get_capabilities(), {one_code})
self.loop.run_until_complete(macro.run())
self.assertListEqual(self.result, [])
macro.set_handler(self.handler)
self.loop.run_until_complete(macro.run())
self.assertListEqual(self.result, [(one_code, 1), (one_code, 0)])
def test_0(self):
self.loop.run_until_complete(parse('k(1)', self.handler).run())
self.assertListEqual(self.result, [(1, 1), (1, 0)])
macro = parse('k(1)')
macro.set_handler(self.handler)
one_code = system_mapping.get('1')
self.assertSetEqual(macro.get_capabilities(), {one_code})
self.loop.run_until_complete(macro.run())
self.assertListEqual(self.result, [(one_code, 1), (one_code, 0)])
def test_1(self):
macro = 'k(1).k(a).k(3)'
self.loop.run_until_complete(parse(macro, self.handler).run())
macro = parse('k(1).k(a).k(3)')
macro.set_handler(self.handler)
self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('1'),
system_mapping.get('a'),
system_mapping.get('3')
})
self.loop.run_until_complete(macro.run())
self.assertListEqual(self.result, [
(1, 1), (1, 0),
('a', 1), ('a', 0),
(3, 1), (3, 0),
(system_mapping.get('1'), 1), (system_mapping.get('1'), 0),
(system_mapping.get('a'), 1), (system_mapping.get('a'), 0),
(system_mapping.get('3'), 1), (system_mapping.get('3'), 0),
])
def test_2(self):
start = time.time()
repeats = 20
macro = f'r({repeats}, k(k))'
self.loop.run_until_complete(parse(macro, self.handler).run())
macro = parse(f'r({repeats}, k(k))')
macro.set_handler(self.handler)
k_code = system_mapping.get('k')
self.assertSetEqual(macro.get_capabilities(), {k_code})
self.loop.run_until_complete(macro.run())
keystroke_sleep = config.get('macros.keystroke_sleep_ms')
sleep_time = 2 * repeats * keystroke_sleep / 1000
self.assertGreater(time.time() - start, sleep_time * 0.9)
self.assertLess(time.time() - start, sleep_time * 1.1)
self.assertListEqual(self.result, [('k', 1), ('k', 0)] * repeats)
self.assertListEqual(self.result, [(k_code, 1), (k_code, 0)] * repeats)
def test_3(self):
start = time.time()
macro = 'r(3, k(m).w(100))'
self.loop.run_until_complete(parse(macro, self.handler).run())
macro = parse('r(3, k(m).w(100))')
macro.set_handler(self.handler)
m_code = system_mapping.get('m')
self.assertSetEqual(macro.get_capabilities(), {m_code})
self.loop.run_until_complete(macro.run())
keystroke_time = 6 * config.get('macros.keystroke_sleep_ms')
total_time = keystroke_time + 300
@ -75,26 +108,42 @@ class TestMacros(unittest.TestCase):
self.assertGreater(time.time() - start, total_time * 0.9)
self.assertLess(time.time() - start, total_time * 1.1)
self.assertListEqual(self.result, [
('m', 1), ('m', 0),
('m', 1), ('m', 0),
('m', 1), ('m', 0),
(m_code, 1), (m_code, 0),
(m_code, 1), (m_code, 0),
(m_code, 1), (m_code, 0),
])
def test_4(self):
macro = ' r(2,\nk(\nr ).k(-\n )).k(m) '
self.loop.run_until_complete(parse(macro, self.handler).run())
macro = parse(' r(2,\nk(\nr ).k(minus\n )).k(m) ')
macro.set_handler(self.handler)
r = system_mapping.get('r')
minus = system_mapping.get('minus')
m = system_mapping.get('m')
self.assertSetEqual(macro.get_capabilities(), {r, minus, m})
self.loop.run_until_complete(macro.run())
self.assertListEqual(self.result, [
('r', 1), ('r', 0),
('-', 1), ('-', 0),
('r', 1), ('r', 0),
('-', 1), ('-', 0),
('m', 1), ('m', 0),
(r, 1), (r, 0),
(minus, 1), (minus, 0),
(r, 1), (r, 0),
(minus, 1), (minus, 0),
(m, 1), (m, 0),
])
def test_5(self):
start = time.time()
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())
macro = parse('w(200).r(2,m(w,\nr(2,\tk(BtN_LeFt))).w(10).k(k))')
macro.set_handler(self.handler)
w = system_mapping.get('w')
left = system_mapping.get('bTn_lEfT')
k = system_mapping.get('k')
self.assertSetEqual(macro.get_capabilities(), {w, left, k})
self.loop.run_until_complete(macro.run())
num_pauses = 8 + 6 + 4
keystroke_time = num_pauses * config.get('macros.keystroke_sleep_ms')
@ -103,17 +152,18 @@ class TestMacros(unittest.TestCase):
self.assertLess(time.time() - start, total_time * 1.1)
self.assertGreater(time.time() - start, total_time * 0.9)
expected = [('w', 1)]
expected += [('r', 1), ('r', 0)] * 2
expected += [('w', 0)]
expected += [('k', 1), ('k', 0)]
expected = [(w, 1)]
expected += [(left, 1), (left, 0)] * 2
expected += [(w, 0)]
expected += [(k, 1), (k, 0)]
expected *= 2
self.assertListEqual(self.result, expected)
def test_6(self):
# does nothing without .run
ret = parse('k(a).r(3, k(b))', self.handler)
self.assertIsInstance(ret, _Macro)
macro = parse('k(a).r(3, k(b))')
macro.set_handler(self.handler)
self.assertIsInstance(macro, _Macro)
self.assertListEqual(self.result, [])

@ -23,7 +23,7 @@ import unittest
from evdev.events import EV_KEY, EV_ABS
from keymapper.mapping import Mapping
from keymapper.state import populate_system_mapping
from keymapper.state import SystemMapping
class TestMapping(unittest.TestCase):
@ -31,18 +31,28 @@ class TestMapping(unittest.TestCase):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def test_populate_system_mapping(self):
# not actually a mapping object, just a dict
mapping = populate_system_mapping()
self.assertGreater(len(mapping), 100)
self.assertEqual(mapping['1'], 2)
self.assertEqual(mapping['KEY_1'], 2)
self.assertEqual(mapping['Alt_L'], 56)
self.assertEqual(mapping['KEY_LEFTALT'], 56)
self.assertEqual(mapping['KEY_LEFTSHIFT'], 42)
self.assertEqual(mapping['Shift_L'], 42)
def test_system_mapping(self):
system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100)
self.assertEqual(system_mapping.get('1'), 2)
self.assertEqual(system_mapping.get('KeY_1'), 2)
self.assertEqual(system_mapping.get('AlT_L'), 56)
self.assertEqual(system_mapping.get('KEy_LEFtALT'), 56)
self.assertEqual(system_mapping.get('kEY_LeFTSHIFT'), 42)
self.assertEqual(system_mapping.get('ShiFt_L'), 42)
self.assertIsNotNone(system_mapping.get('kp_1'))
self.assertIsNotNone(system_mapping.get('KP_1'))
self.assertEqual(
system_mapping.get('KP_Left'),
system_mapping.get('KP_4')
)
self.assertEqual(
system_mapping.get('KP_Left'),
system_mapping.get('KEY_KP4')
)
def test_clone(self):
mapping1 = Mapping()

Loading…
Cancel
Save