mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-04 12:00:16 +00:00
no .terminate anymore, proper multiprocessing coverage, some minor cleanup in injector
This commit is contained in:
parent
f4ca07490d
commit
17445558fb
@ -1,4 +1,5 @@
|
|||||||
[run]
|
[run]
|
||||||
concurrency = multiprocessing
|
|
||||||
branch = True
|
branch = True
|
||||||
source = /usr/lib/python3.8/site-packages/keymapper
|
source = keymapper
|
||||||
|
concurrency = multiprocessing
|
||||||
|
debug = multiproc
|
||||||
|
@ -59,7 +59,8 @@ groups
|
|||||||
##### Git/pip
|
##### Git/pip
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip install git+https://github.com/sezanzeb/key-mapper.git
|
git clone https://github.com/sezanzeb/key-mapper.git
|
||||||
|
cd key-mapper && sudo python3 setup.py install
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Manjaro/Arch
|
##### Manjaro/Arch
|
||||||
@ -93,12 +94,12 @@ sudo dpkg -i python3-key-mapper_0.1.0-1_all.deb
|
|||||||
- [x] support timed macros, maybe using some sort of syntax
|
- [x] support timed macros, maybe using some sort of syntax
|
||||||
- [x] add to the AUR, provide .deb file
|
- [x] add to the AUR, provide .deb file
|
||||||
- [ ] automatically load presets when devices get plugged in after login
|
- [ ] automatically load presets when devices get plugged in after login
|
||||||
- [ ] support gamepads as keyboard and mouse combi
|
- [ ] support gamepads as keyboard and mouse combi (partially done)
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pylint keymapper --extension-pkg-whitelist=evdev
|
pylint keymapper --extension-pkg-whitelist=evdev
|
||||||
sudo pip install . && coverage run tests/test.py
|
sudo pip install -e . && coverage run tests/test.py
|
||||||
coverage combine && coverage report -m
|
coverage combine && coverage report -m
|
||||||
```
|
```
|
||||||
|
@ -27,7 +27,7 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from keymapper.paths import CONFIG, touch
|
from keymapper.paths import CONFIG, USER, touch
|
||||||
from keymapper.logger import logger
|
from keymapper.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,8 +138,14 @@ class Daemon(service.Object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@dbus.service.method('keymapper.Interface')
|
@dbus.service.method('keymapper.Interface', in_signature='b')
|
||||||
def stop(self):
|
def stop(self, terminate=False):
|
||||||
"""Stop all mapping injections."""
|
"""Stop all injections and end the service.
|
||||||
|
|
||||||
|
Raises dbus.exceptions.DBusException in your main process.
|
||||||
|
"""
|
||||||
for injector in self.injectors.values():
|
for injector in self.injectors.values():
|
||||||
injector.stop_injecting()
|
injector.stop_injecting()
|
||||||
|
|
||||||
|
if terminate:
|
||||||
|
exit(0)
|
||||||
|
@ -39,9 +39,7 @@ from keymapper.dev.macros import parse
|
|||||||
|
|
||||||
|
|
||||||
DEV_NAME = 'key-mapper'
|
DEV_NAME = 'key-mapper'
|
||||||
DEVICE_CREATED = 1
|
CLOSE = 0
|
||||||
FAILED = 2
|
|
||||||
DEVICE_SKIPPED = 3
|
|
||||||
|
|
||||||
|
|
||||||
def is_numlock_on():
|
def is_numlock_on():
|
||||||
@ -67,8 +65,8 @@ def toggle_numlock():
|
|||||||
logger.debug('numlockx not found, trying to inject a keycode')
|
logger.debug('numlockx not found, trying to inject a keycode')
|
||||||
# and this doesn't always work.
|
# and this doesn't always work.
|
||||||
device = evdev.UInput(
|
device = evdev.UInput(
|
||||||
name=f'key-mapper numlock-control',
|
name=f'{DEV_NAME} numlock-control',
|
||||||
phys='key-mapper',
|
phys=DEV_NAME,
|
||||||
)
|
)
|
||||||
device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1)
|
device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1)
|
||||||
device.syn()
|
device.syn()
|
||||||
@ -110,10 +108,7 @@ class KeycodeInjector:
|
|||||||
self.device = device
|
self.device = device
|
||||||
self.mapping = mapping
|
self.mapping = mapping
|
||||||
self._process = None
|
self._process = None
|
||||||
|
self._msg_pipe = multiprocessing.Pipe()
|
||||||
def __del__(self):
|
|
||||||
if self._process is not None:
|
|
||||||
self._process.terminate()
|
|
||||||
|
|
||||||
def start_injecting(self):
|
def start_injecting(self):
|
||||||
"""Start injecting keycodes."""
|
"""Start injecting keycodes."""
|
||||||
@ -121,11 +116,16 @@ class KeycodeInjector:
|
|||||||
self._process.start()
|
self._process.start()
|
||||||
|
|
||||||
def _prepare_device(self, path):
|
def _prepare_device(self, path):
|
||||||
"""Try to grab the device, return if not needed/possible."""
|
"""Try to grab the device, return if not needed/possible.
|
||||||
|
|
||||||
|
Also return if ABS events are changed to REL mouse movements,
|
||||||
|
because the capabilities of the returned device are changed
|
||||||
|
so this cannot be checked later anymore.
|
||||||
|
"""
|
||||||
device = evdev.InputDevice(path)
|
device = evdev.InputDevice(path)
|
||||||
|
|
||||||
if device is None:
|
if device is None:
|
||||||
return None
|
return None, False
|
||||||
|
|
||||||
capabilities = device.capabilities(absinfo=False)
|
capabilities = device.capabilities(absinfo=False)
|
||||||
|
|
||||||
@ -136,15 +136,15 @@ class KeycodeInjector:
|
|||||||
needed = True
|
needed = True
|
||||||
break
|
break
|
||||||
|
|
||||||
can_do_abs = evdev.ecodes.ABS_X in capabilities.get(EV_ABS, [])
|
map_ABS = self.map_ABS(device)
|
||||||
if self.map_abs_to_rel() and can_do_abs:
|
if map_ABS:
|
||||||
needed = True
|
needed = True
|
||||||
|
|
||||||
if not needed:
|
if not needed:
|
||||||
# skipping reading and checking on events from those devices
|
# skipping reading and checking on events from those devices
|
||||||
# may be beneficial for performance.
|
# may be beneficial for performance.
|
||||||
logger.debug('No need to grab %s', path)
|
logger.debug('No need to grab %s', path)
|
||||||
return None
|
return None, False
|
||||||
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while True:
|
while True:
|
||||||
@ -164,22 +164,25 @@ class KeycodeInjector:
|
|||||||
|
|
||||||
if attempts >= 4:
|
if attempts >= 4:
|
||||||
logger.error('Cannot grab %s, it is possibly in use', path)
|
logger.error('Cannot grab %s, it is possibly in use', path)
|
||||||
return None
|
return None, False
|
||||||
|
|
||||||
time.sleep(0.15)
|
time.sleep(0.15)
|
||||||
|
|
||||||
return device
|
return device, map_ABS
|
||||||
|
|
||||||
def map_abs_to_rel(self):
|
def map_ABS(self, device):
|
||||||
# TODO offer configuration via the UI if a gamepad is elected
|
# TODO offer configuration via the UI if a gamepad is elected
|
||||||
return True
|
capabilities = device.capabilities(absinfo=False)
|
||||||
|
return evdev.ecodes.ABS_X in capabilities.get(EV_ABS, [])
|
||||||
|
|
||||||
def _modify_capabilities(self, input_device):
|
def _modify_capabilities(self, input_device, map_ABS):
|
||||||
"""Adds all keycode into a copy of a devices capabilities.
|
"""Adds all keycode into a copy of a devices capabilities.
|
||||||
|
|
||||||
Prameters
|
Prameters
|
||||||
---------
|
---------
|
||||||
input_device : evdev.InputDevice
|
input_device : evdev.InputDevice
|
||||||
|
map_ABS : bool
|
||||||
|
if ABS capabilities should be removed in favor of REL
|
||||||
"""
|
"""
|
||||||
ecodes = evdev.ecodes
|
ecodes = evdev.ecodes
|
||||||
|
|
||||||
@ -196,9 +199,8 @@ class KeycodeInjector:
|
|||||||
if keycode is not None:
|
if keycode is not None:
|
||||||
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET)
|
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET)
|
||||||
|
|
||||||
if self.map_abs_to_rel():
|
if map_ABS:
|
||||||
if capabilities.get(ecodes.EV_ABS):
|
del capabilities[ecodes.EV_ABS]
|
||||||
del capabilities[ecodes.EV_ABS]
|
|
||||||
capabilities[ecodes.EV_REL] = [
|
capabilities[ecodes.EV_REL] = [
|
||||||
evdev.ecodes.REL_X,
|
evdev.ecodes.REL_X,
|
||||||
evdev.ecodes.REL_Y,
|
evdev.ecodes.REL_Y,
|
||||||
@ -214,23 +216,37 @@ class KeycodeInjector:
|
|||||||
|
|
||||||
return capabilities
|
return capabilities
|
||||||
|
|
||||||
|
async def _msg_listener(self, loop):
|
||||||
|
"""Wait for messages from the main process to do special stuff."""
|
||||||
|
while True:
|
||||||
|
frame_available = asyncio.Event()
|
||||||
|
loop.add_reader(self._msg_pipe[0].fileno(), frame_available.set)
|
||||||
|
await frame_available.wait()
|
||||||
|
frame_available.clear()
|
||||||
|
msg = self._msg_pipe[0].recv()
|
||||||
|
if msg == CLOSE:
|
||||||
|
logger.debug('Received close signal')
|
||||||
|
# stop the event loop and cause the process to reach its end
|
||||||
|
# cleanly. Using .terminate prevents coverage from working.
|
||||||
|
loop.stop()
|
||||||
|
return
|
||||||
|
|
||||||
def _start_injecting(self):
|
def _start_injecting(self):
|
||||||
"""The injection worker that keeps injecting until terminated.
|
"""The injection worker that keeps injecting until terminated.
|
||||||
|
|
||||||
Stuff is non-blocking by using asyncio in order to do multiple things
|
Stuff is non-blocking by using asyncio in order to do multiple things
|
||||||
somewhat concurrently.
|
somewhat concurrently.
|
||||||
"""
|
"""
|
||||||
# TODO do select.select insted of async_read_loop
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
coroutines = []
|
coroutines = []
|
||||||
|
|
||||||
logger.info('Starting injecting the mapping for %s', self.device)
|
logger.info('Starting injecting the mapping for %s', self.device)
|
||||||
|
|
||||||
paths = get_devices()[self.device]['paths']
|
paths = get_devices()[self.device]['paths']
|
||||||
devices = [self._prepare_device(path) for path in paths]
|
|
||||||
|
|
||||||
# Watch over each one of the potentially multiple devices per hardware
|
# Watch over each one of the potentially multiple devices per hardware
|
||||||
for input_device in devices:
|
for path in paths:
|
||||||
|
input_device, map_ABS = self._prepare_device(path)
|
||||||
if input_device is None:
|
if input_device is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -238,17 +254,19 @@ class KeycodeInjector:
|
|||||||
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
|
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
|
||||||
# so don't merge all InputDevices into one UInput device.
|
# so don't merge all InputDevices into one UInput device.
|
||||||
uinput = evdev.UInput(
|
uinput = evdev.UInput(
|
||||||
name=f'key-mapper {self.device}',
|
name=f'{DEV_NAME} {self.device}',
|
||||||
phys='key-mapper',
|
phys=DEV_NAME,
|
||||||
events=self._modify_capabilities(input_device)
|
events=self._modify_capabilities(input_device, map_ABS)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO separate file
|
||||||
# keycode injection
|
# keycode injection
|
||||||
coroutine = self._keycode_loop(input_device, uinput)
|
coroutine = self._keycode_loop(input_device, uinput, map_ABS)
|
||||||
coroutines.append(coroutine)
|
coroutines.append(coroutine)
|
||||||
|
|
||||||
|
# TODO separate file
|
||||||
# mouse movement injection
|
# mouse movement injection
|
||||||
if self.map_abs_to_rel():
|
if map_ABS:
|
||||||
self.abs_x = 0
|
self.abs_x = 0
|
||||||
self.abs_y = 0
|
self.abs_y = 0
|
||||||
# events only take ints, so a movement of 0.3 needs to add
|
# events only take ints, so a movement of 0.3 needs to add
|
||||||
@ -262,7 +280,13 @@ class KeycodeInjector:
|
|||||||
logger.error('Did not grab any device')
|
logger.error('Did not grab any device')
|
||||||
return
|
return
|
||||||
|
|
||||||
loop.run_until_complete(asyncio.gather(*coroutines))
|
coroutines.append(self._msg_listener(loop))
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(asyncio.gather(*coroutines))
|
||||||
|
except RuntimeError:
|
||||||
|
# stopped event loop most likely
|
||||||
|
pass
|
||||||
|
|
||||||
if len(coroutines) > 0:
|
if len(coroutines) > 0:
|
||||||
logger.debug('asyncio coroutines ended')
|
logger.debug('asyncio coroutines ended')
|
||||||
@ -336,7 +360,7 @@ class KeycodeInjector:
|
|||||||
rel_x
|
rel_x
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _keycode_loop(self, device, keymapper_device):
|
async def _keycode_loop(self, device, keymapper_device, map_ABS):
|
||||||
"""Inject keycodes for one of the virtual devices.
|
"""Inject keycodes for one of the virtual devices.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -345,6 +369,8 @@ class KeycodeInjector:
|
|||||||
where to read keycodes from
|
where to read keycodes from
|
||||||
keymapper_device : evdev.UInput
|
keymapper_device : evdev.UInput
|
||||||
where to write keycodes to
|
where to write keycodes to
|
||||||
|
map_ABS : bool
|
||||||
|
the value of map_ABS() for the original device
|
||||||
"""
|
"""
|
||||||
# Parse all macros beforehand
|
# Parse all macros beforehand
|
||||||
logger.debug('Parsing macros')
|
logger.debug('Parsing macros')
|
||||||
@ -363,7 +389,7 @@ class KeycodeInjector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async for event in device.async_read_loop():
|
async for event in device.async_read_loop():
|
||||||
if self.map_abs_to_rel() and event.type == EV_ABS:
|
if map_ABS and event.type == EV_ABS:
|
||||||
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
|
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
|
||||||
continue
|
continue
|
||||||
if event.code == evdev.ecodes.ABS_X:
|
if event.code == evdev.ecodes.ABS_X:
|
||||||
@ -438,5 +464,4 @@ class KeycodeInjector:
|
|||||||
def stop_injecting(self):
|
def stop_injecting(self):
|
||||||
"""Stop injecting keycodes."""
|
"""Stop injecting keycodes."""
|
||||||
logger.info('Stopping injecting keycodes for device "%s"', self.device)
|
logger.info('Stopping injecting keycodes for device "%s"', self.device)
|
||||||
if self._process is not None and self._process.is_alive():
|
self._msg_pipe[1].send(CLOSE)
|
||||||
self._process.terminate()
|
|
||||||
|
@ -31,6 +31,9 @@ from keymapper.getdevices import get_devices, refresh_devices
|
|||||||
from keymapper.state import KEYCODE_OFFSET
|
from keymapper.state import KEYCODE_OFFSET
|
||||||
|
|
||||||
|
|
||||||
|
CLOSE = 1
|
||||||
|
|
||||||
|
|
||||||
class _KeycodeReader:
|
class _KeycodeReader:
|
||||||
"""Keeps reading keycodes in the background for the UI to use.
|
"""Keeps reading keycodes in the background for the UI to use.
|
||||||
|
|
||||||
@ -47,15 +50,9 @@ class _KeycodeReader:
|
|||||||
self.stop_reading()
|
self.stop_reading()
|
||||||
|
|
||||||
def stop_reading(self):
|
def stop_reading(self):
|
||||||
# TODO something like this for the injector?
|
|
||||||
if self._process is not None:
|
|
||||||
logger.debug('Terminating reader process')
|
|
||||||
self._process.terminate()
|
|
||||||
self._process = None
|
|
||||||
|
|
||||||
if self._pipe is not None:
|
if self._pipe is not None:
|
||||||
logger.debug('Closing reader pipe')
|
logger.debug('Sending close msg to reader')
|
||||||
self._pipe[0].close()
|
self._pipe[0].send(CLOSE)
|
||||||
self._pipe = None
|
self._pipe = None
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
@ -77,8 +74,7 @@ class _KeycodeReader:
|
|||||||
|
|
||||||
self.virtual_devices = []
|
self.virtual_devices = []
|
||||||
|
|
||||||
for name, group in get_devices(include_keymapper=True).items():
|
for name, group in get_devices().items():
|
||||||
# also find stuff like "key-mapper {device}"
|
|
||||||
if device_name not in name:
|
if device_name not in name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -99,35 +95,46 @@ class _KeycodeReader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
pipe = multiprocessing.Pipe()
|
pipe = multiprocessing.Pipe()
|
||||||
self._process = multiprocessing.Process(
|
|
||||||
target=self._read_worker,
|
|
||||||
args=(pipe[1],)
|
|
||||||
)
|
|
||||||
self._process.start()
|
|
||||||
self._pipe = pipe
|
self._pipe = pipe
|
||||||
|
self._process = multiprocessing.Process(target=self._read_worker)
|
||||||
|
self._process.start()
|
||||||
|
|
||||||
def _consume_event(self, event, pipe):
|
def _consume_event(self, event):
|
||||||
"""Write the event code into the pipe if it is a key-down press."""
|
"""Write the event code into the pipe if it is a key-down press."""
|
||||||
# value: 1 for down, 0 for up, 2 for hold.
|
# value: 1 for down, 0 for up, 2 for hold.
|
||||||
|
if self._pipe[1].closed:
|
||||||
|
logger.debug('Pipe closed, reader stops.')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
|
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
|
||||||
logger.spam(
|
logger.spam(
|
||||||
'got code:%s value:%s',
|
'got code:%s value:%s',
|
||||||
event.code + KEYCODE_OFFSET, event.value
|
event.code + KEYCODE_OFFSET, event.value
|
||||||
)
|
)
|
||||||
pipe.send(event.code + KEYCODE_OFFSET)
|
self._pipe[1].send(event.code + KEYCODE_OFFSET)
|
||||||
|
|
||||||
def _read_worker(self, pipe):
|
def _read_worker(self):
|
||||||
"""Process that reads keycodes and buffers them into a pipe."""
|
"""Process that reads keycodes and buffers them into a pipe."""
|
||||||
# using a process that blocks instead of read_one made it easier
|
# using a process that blocks instead of read_one made it easier
|
||||||
# to debug via the logs, because the UI was not polling properly
|
# to debug via the logs, because the UI was not polling properly
|
||||||
# at some point which caused logs for events not to be written.
|
# at some point which caused logs for events not to be written.
|
||||||
rlist = {device.fd: device for device in self.virtual_devices}
|
rlist = {device.fd: device for device in self.virtual_devices}
|
||||||
|
rlist[self._pipe[1]] = self._pipe[1]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
ready = select.select(rlist, [], [])[0]
|
ready = select.select(rlist, [], [])[0]
|
||||||
for fd in ready:
|
for fd in ready:
|
||||||
|
readable = rlist[fd]
|
||||||
|
if isinstance(readable, multiprocessing.connection.Connection):
|
||||||
|
msg = readable.recv()
|
||||||
|
if msg == CLOSE:
|
||||||
|
logger.debug('Reader stopped')
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in rlist[fd].read():
|
for event in rlist[fd].read():
|
||||||
self._consume_event(event, pipe)
|
self._consume_event(event)
|
||||||
except OSError:
|
except OSError:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Device "%s" disappeared from the reader',
|
'Device "%s" disappeared from the reader',
|
||||||
|
@ -69,6 +69,9 @@ def get_selected_row_bg():
|
|||||||
return color.to_string()
|
return color.to_string()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO show if the preset is being injected
|
||||||
|
|
||||||
|
|
||||||
class Window:
|
class Window:
|
||||||
"""User Interface."""
|
"""User Interface."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -62,6 +62,13 @@ class Formatter(logging.Formatter):
|
|||||||
SPAM: 34,
|
SPAM: 34,
|
||||||
logging.INFO: 32,
|
logging.INFO: 32,
|
||||||
}.get(record.levelno, 0)
|
}.get(record.levelno, 0)
|
||||||
|
|
||||||
|
# if this runs in a separate process, write down the pid
|
||||||
|
# to debug exit codes and such
|
||||||
|
pid = ''
|
||||||
|
if os.getpid() != logger.main_pid:
|
||||||
|
pid = f'pid {os.getpid()}, '
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
self._style._fmt = ( # noqa
|
self._style._fmt = ( # noqa
|
||||||
'\033[1m' # bold
|
'\033[1m' # bold
|
||||||
@ -69,7 +76,7 @@ class Formatter(logging.Formatter):
|
|||||||
f'%(levelname)s'
|
f'%(levelname)s'
|
||||||
'\033[0m' # end style
|
'\033[0m' # end style
|
||||||
f'\033[{color}m' # color
|
f'\033[{color}m' # color
|
||||||
': %(filename)s, line %(lineno)d, %(message)s'
|
f': {pid}%(filename)s, line %(lineno)d, %(message)s'
|
||||||
'\033[0m' # end style
|
'\033[0m' # end style
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -85,6 +92,7 @@ handler.setFormatter(Formatter())
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
||||||
|
logger.main_pid = os.getpid()
|
||||||
|
|
||||||
|
|
||||||
def is_debug():
|
def is_debug():
|
||||||
|
13
scripts/install.sh
Executable file
13
scripts/install.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# in case setup.py does nothing instead of something.
|
||||||
|
# call via `./scripts/build.sh`
|
||||||
|
|
||||||
|
# try both ways of installation
|
||||||
|
sudo pip3 install .
|
||||||
|
sudo python3 setup.py install
|
||||||
|
|
||||||
|
# copy crucial files
|
||||||
|
sudo cp bin/* /usr/bin/ -r
|
||||||
|
sudo mkdir /usr/share/key-mapper
|
||||||
|
sudo cp data/* /usr/share/key-mapper -r
|
@ -158,10 +158,18 @@ def patch_select():
|
|||||||
import select
|
import select
|
||||||
|
|
||||||
def new_select(rlist, *args):
|
def new_select(rlist, *args):
|
||||||
return ([
|
ret = []
|
||||||
device for device in rlist
|
for thing in rlist:
|
||||||
if len(pending_events.get(device, [])) > 0
|
if hasattr(thing, 'poll') and thing.poll():
|
||||||
],)
|
# the reader receives msgs through pipes. If there is one
|
||||||
|
# ready, provide the pipe
|
||||||
|
ret.append(thing)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(pending_events.get(thing, [])) > 0:
|
||||||
|
ret.append(thing)
|
||||||
|
|
||||||
|
return [ret, [], []]
|
||||||
|
|
||||||
select.select = new_select
|
select.select = new_select
|
||||||
|
|
||||||
|
@ -20,13 +20,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import unittest
|
import unittest
|
||||||
import time
|
import time
|
||||||
from unittest.mock import patch
|
|
||||||
from importlib.util import spec_from_loader, module_from_spec
|
|
||||||
from importlib.machinery import SourceFileLoader
|
|
||||||
|
|
||||||
import dbus
|
import dbus
|
||||||
import evdev
|
import evdev
|
||||||
@ -61,12 +57,10 @@ class TestDBusDaemon(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
cls.interface.stop()
|
try:
|
||||||
time.sleep(0.1)
|
cls.interface.stop(True)
|
||||||
cls.process.terminate()
|
except dbus.exceptions.DBusException:
|
||||||
time.sleep(0.1)
|
pass
|
||||||
os.system('pkill -f key-mapper-service')
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
def test_can_connect(self):
|
def test_can_connect(self):
|
||||||
self.assertIsInstance(self.interface, dbus.Interface)
|
self.assertIsInstance(self.interface, dbus.Interface)
|
||||||
|
@ -58,8 +58,9 @@ class TestInjector(unittest.TestCase):
|
|||||||
self.injector.stop_injecting()
|
self.injector.stop_injecting()
|
||||||
self.injector = None
|
self.injector = None
|
||||||
evdev.InputDevice.grab = self.grab
|
evdev.InputDevice.grab = self.grab
|
||||||
if pending_events.get('device 2') is not None:
|
keys = list(pending_events.keys())
|
||||||
del pending_events['device 2']
|
for key in keys:
|
||||||
|
del pending_events[key]
|
||||||
clear_write_history()
|
clear_write_history()
|
||||||
|
|
||||||
def test_modify_capabilities(self):
|
def test_modify_capabilities(self):
|
||||||
@ -80,7 +81,11 @@ class TestInjector(unittest.TestCase):
|
|||||||
maps_to = system_mapping['a'] - KEYCODE_OFFSET
|
maps_to = system_mapping['a'] - KEYCODE_OFFSET
|
||||||
|
|
||||||
self.injector = KeycodeInjector('foo', mapping)
|
self.injector = KeycodeInjector('foo', mapping)
|
||||||
capabilities = self.injector._modify_capabilities(FakeDevice())
|
fake_device = FakeDevice()
|
||||||
|
capabilities = self.injector._modify_capabilities(
|
||||||
|
fake_device,
|
||||||
|
map_ABS=False
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIn(EV_KEY, capabilities)
|
self.assertIn(EV_KEY, capabilities)
|
||||||
keys = capabilities[EV_KEY]
|
keys = capabilities[EV_KEY]
|
||||||
@ -97,17 +102,30 @@ class TestInjector(unittest.TestCase):
|
|||||||
path = '/dev/input/event10'
|
path = '/dev/input/event10'
|
||||||
# this test needs to pass around all other constraints of
|
# this test needs to pass around all other constraints of
|
||||||
# _prepare_device
|
# _prepare_device
|
||||||
device = self.injector._prepare_device(path)
|
device, map_ABS = self.injector._prepare_device(path)
|
||||||
|
self.assertFalse(map_ABS)
|
||||||
self.assertEqual(self.failed, 2)
|
self.assertEqual(self.failed, 2)
|
||||||
# success on the third try
|
# success on the third try
|
||||||
device.name = fixtures[path]['name']
|
device.name = fixtures[path]['name']
|
||||||
|
|
||||||
|
def test_gamepad_capabilities(self):
|
||||||
|
self.injector = KeycodeInjector('gamepad', custom_mapping)
|
||||||
|
|
||||||
|
path = '/dev/input/event30'
|
||||||
|
device, map_ABS = self.injector._prepare_device(path)
|
||||||
|
self.assertTrue(map_ABS)
|
||||||
|
|
||||||
|
capabilities = self.injector._modify_capabilities(device, map_ABS)
|
||||||
|
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities)
|
||||||
|
self.assertIn(evdev.ecodes.EV_REL, capabilities)
|
||||||
|
|
||||||
def test_skip_unused_device(self):
|
def test_skip_unused_device(self):
|
||||||
# skips a device because its capabilities are not used in the mapping
|
# skips a device because its capabilities are not used in the mapping
|
||||||
custom_mapping.change(10, 'a')
|
custom_mapping.change(10, 'a')
|
||||||
self.injector = KeycodeInjector('device 1', custom_mapping)
|
self.injector = KeycodeInjector('device 1', custom_mapping)
|
||||||
path = '/dev/input/event11'
|
path = '/dev/input/event11'
|
||||||
device = self.injector._prepare_device(path)
|
device, map_ABS = self.injector._prepare_device(path)
|
||||||
|
self.assertFalse(map_ABS)
|
||||||
self.assertEqual(self.failed, 0)
|
self.assertEqual(self.failed, 0)
|
||||||
self.assertIsNone(device)
|
self.assertIsNone(device)
|
||||||
|
|
||||||
@ -115,7 +133,7 @@ class TestInjector(unittest.TestCase):
|
|||||||
# skips a device because its capabilities are not used in the mapping
|
# skips a device because its capabilities are not used in the mapping
|
||||||
self.injector = KeycodeInjector('device 1', custom_mapping)
|
self.injector = KeycodeInjector('device 1', custom_mapping)
|
||||||
path = '/dev/input/event11'
|
path = '/dev/input/event11'
|
||||||
device = self.injector._prepare_device(path)
|
device, _ = self.injector._prepare_device(path)
|
||||||
|
|
||||||
# make sure the test uses a fixture without interesting capabilities
|
# make sure the test uses a fixture without interesting capabilities
|
||||||
capabilities = evdev.InputDevice(path).capabilities()
|
capabilities = evdev.InputDevice(path).capabilities()
|
||||||
@ -145,7 +163,7 @@ class TestInjector(unittest.TestCase):
|
|||||||
|
|
||||||
def test_abs_to_rel(self):
|
def test_abs_to_rel(self):
|
||||||
# maps gamepad joystick events to mouse events
|
# maps gamepad joystick events to mouse events
|
||||||
# TODO enable this somewhere so that map_abs_to_rel returns true
|
# TODO enable this somewhere so that map_ABS returns true
|
||||||
# in the .json file of the mapping.
|
# in the .json file of the mapping.
|
||||||
config.set('gamepad.non_linearity', 1)
|
config.set('gamepad.non_linearity', 1)
|
||||||
pointer_speed = 80
|
pointer_speed = 80
|
||||||
@ -182,6 +200,11 @@ class TestInjector(unittest.TestCase):
|
|||||||
event = uinput_write_history_pipe[0].recv()
|
event = uinput_write_history_pipe[0].recv()
|
||||||
history.append((event.type, event.code, event.value))
|
history.append((event.type, event.code, event.value))
|
||||||
|
|
||||||
|
if history[0][0] == EV_ABS:
|
||||||
|
raise AssertionError(
|
||||||
|
'The injector probably just forwarded them unchanged'
|
||||||
|
)
|
||||||
|
|
||||||
# movement is written at 60hz and it takes `divisor` steps to
|
# movement is written at 60hz and it takes `divisor` steps to
|
||||||
# move 1px. take it times 2 for both x and y events.
|
# move 1px. take it times 2 for both x and y events.
|
||||||
self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor)
|
self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor)
|
||||||
|
@ -50,8 +50,10 @@ def gtk_iteration():
|
|||||||
Gtk.main_iteration()
|
Gtk.main_iteration()
|
||||||
|
|
||||||
|
|
||||||
def launch(argv=None, bin_path='/bin/key-mapper-gtk'):
|
def launch(argv=None):
|
||||||
"""Start key-mapper-gtk with the command line argument array argv."""
|
"""Start key-mapper-gtk with the command line argument array argv."""
|
||||||
|
bin_path = os.path.join(os.getcwd(), 'bin', 'key-mapper-gtk')
|
||||||
|
|
||||||
if not argv:
|
if not argv:
|
||||||
argv = ['-d']
|
argv = ['-d']
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ class TestReader(unittest.TestCase):
|
|||||||
Event(evdev.events.EV_KEY, CODE_3, 1)
|
Event(evdev.events.EV_KEY, CODE_3, 1)
|
||||||
]
|
]
|
||||||
keycode_reader.start_reading('device 1')
|
keycode_reader.start_reading('device 1')
|
||||||
|
|
||||||
|
# sending anything arbitrary does not stop the pipe
|
||||||
|
keycode_reader._pipe[0].send(1234)
|
||||||
|
|
||||||
time.sleep(EVENT_READ_TIMEOUT * 5)
|
time.sleep(EVENT_READ_TIMEOUT * 5)
|
||||||
self.assertEqual(keycode_reader.read(), CODE_3 + 8)
|
self.assertEqual(keycode_reader.read(), CODE_3 + 8)
|
||||||
self.assertIsNone(keycode_reader.read())
|
self.assertIsNone(keycode_reader.read())
|
||||||
@ -67,8 +71,10 @@ class TestReader(unittest.TestCase):
|
|||||||
self.assertIsNone(keycode_reader.read())
|
self.assertIsNone(keycode_reader.read())
|
||||||
|
|
||||||
def test_keymapper_devices(self):
|
def test_keymapper_devices(self):
|
||||||
# In order to show pressed keycodes on the ui while the device is
|
# Don't read from keymapper devices, their keycodes are not
|
||||||
# grabbed, read from that as well.
|
# representative for the original key. As long as this is not
|
||||||
|
# intentionally programmed it won't even do that. But it was at some
|
||||||
|
# point.
|
||||||
pending_events['key-mapper device 2'] = [
|
pending_events['key-mapper device 2'] = [
|
||||||
Event(evdev.events.EV_KEY, CODE_1, 1),
|
Event(evdev.events.EV_KEY, CODE_1, 1),
|
||||||
Event(evdev.events.EV_KEY, CODE_2, 1),
|
Event(evdev.events.EV_KEY, CODE_2, 1),
|
||||||
@ -76,7 +82,6 @@ class TestReader(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
keycode_reader.start_reading('device 2')
|
keycode_reader.start_reading('device 2')
|
||||||
time.sleep(EVENT_READ_TIMEOUT * 5)
|
time.sleep(EVENT_READ_TIMEOUT * 5)
|
||||||
self.assertEqual(keycode_reader.read(), CODE_3 + 8)
|
|
||||||
self.assertIsNone(keycode_reader.read())
|
self.assertIsNone(keycode_reader.read())
|
||||||
|
|
||||||
def test_clear(self):
|
def test_clear(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user