mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-18 03:25:52 +00:00
some fixes and improvements
This commit is contained in:
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)
|
||||
|
||||
if len(self._code_to_code) > 0 or len(macros) > 0:
|
||||
if capabilities.get(EV_KEY) is None:
|
||||
capabilities[EV_KEY] = []
|
||||
|
||||
# Furthermore, support all injected keycodes
|
||||
if len(self.mapping) > 0 and capabilities.get(ecodes.EV_KEY) is None:
|
||||
capabilities[ecodes.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:
|
||||
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,8 +285,16 @@ 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
|
||||
@ -263,22 +303,26 @@ class KeycodeInjector:
|
||||
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 = {}
|
||||
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
|
||||
# only the first column is relevant. The others can be achieved
|
||||
# by using a modifier button and the mapped key
|
||||
# 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]
|
||||
mapping[name] = int(keycode) - XKB_KEYCODE_OFFSET
|
||||
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():
|
||||
mapping[name] = ecode
|
||||
self._set(name, ecode)
|
||||
|
||||
return mapping
|
||||
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_system_mapping():
|
||||
def clear(self):
|
||||
"""Remove all mapped keys. Only needed for tests."""
|
||||
keys = list(system_mapping.keys())
|
||||
keys = list(self._mapping.keys())
|
||||
for key in keys:
|
||||
del system_mapping[key]
|
||||
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)
|
||||
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(mapping['Alt_L'], 56)
|
||||
self.assertEqual(mapping['KEY_LEFTALT'], 56)
|
||||
self.assertEqual(system_mapping.get('AlT_L'), 56)
|
||||
self.assertEqual(system_mapping.get('KEy_LEFtALT'), 56)
|
||||
|
||||
self.assertEqual(mapping['KEY_LEFTSHIFT'], 42)
|
||||
self.assertEqual(mapping['Shift_L'], 42)
|
||||
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…
Reference in New Issue
Block a user