renamed injector class, fixed config path for injection

This commit is contained in:
sezanzeb 2021-01-04 20:50:05 +01:00
parent df8649cbad
commit fdbc139c59
12 changed files with 92 additions and 56 deletions

View File

@ -1,5 +1,5 @@
[run]
branch = True
source = /usr/lib/python3.8/site-packages/keymapper
source = /usr/lib/python3.9/site-packages/keymapper
concurrency = multiprocessing
debug = multiproc

View File

@ -59,7 +59,7 @@ def group_exists(name):
return False
def main(options, daemon, xmodmap_path):
def main(options, daemon, config_path):
"""Do the stuff that the executable is supposed to do."""
# Is a function so that I can import it and test it
if options.list_devices:
@ -85,7 +85,7 @@ def main(options, daemon, xmodmap_path):
mapping = Mapping()
preset_path = get_preset_path(device, preset)
mapping.load(preset_path)
daemon.start_injecting(device, preset_path, xmodmap_path)
daemon.start_injecting(device, preset_path, config_path)
if options.command == START:
if options.device is None:
@ -97,7 +97,7 @@ def main(options, daemon, xmodmap_path):
sys.exit(1)
preset_path = os.path.abspath(os.path.expanduser(options.preset))
daemon.start_injecting(options.device, preset_path, xmodmap_path)
daemon.start_injecting(options.device, preset_path, config_path)
if options.command == STOP:
if options.device is None:
@ -149,6 +149,6 @@ if __name__ == '__main__':
if daemon is None:
sys.exit(0)
xmodmap_path = get_config_path(XMODMAP_FILENAME)
config_path = get_config_path()
main(options, daemon, xmodmap_path)
main(options, daemon, config_path)

View File

@ -214,8 +214,22 @@ class GlobalConfig(ConfigBase):
"""Should this preset be loaded automatically?"""
return self.get(['autoload', device], log_unknown=False) == preset
def load_config(self):
"""Load the config from the file system."""
def load_config(self, path=None):
"""Load the config from the file system.
Parameters
----------
path : string or None
If set, will change the path to load from and save to.
"""
if path is not None:
# TODO Test
if not os.path.exists(path):
logger.error('Config at "%s" not found', path)
return
self.path = path
self.clear_config()
if not os.path.exists(self.path):

View File

@ -25,6 +25,7 @@ https://github.com/LEW21/pydbus/tree/cc407c8b1d25b7e28a6d661a29f9e661b1c9b964/ex
"""
import os
import subprocess
import json
@ -32,7 +33,7 @@ from pydbus import SystemBus
from gi.repository import GLib
from keymapper.logger import logger
from keymapper.dev.injector import KeycodeInjector
from keymapper.dev.injector import Injector
from keymapper.mapping import Mapping
from keymapper.config import config
from keymapper.state import system_mapping
@ -148,7 +149,7 @@ class Daemon:
"""Is this device being mapped?"""
return device in self.injectors
def start_injecting(self, device, path, xmodmap_path=None):
def start_injecting(self, device, preset_path, config_dir=None):
"""Start injecting the preset for the device.
Returns True on success.
@ -157,33 +158,38 @@ class Daemon:
----------
device : string
The name of the device
path : string
preset_path : string
Path to the preset. The daemon, if started via systemctl, has no
knowledge of the user and their home path, so the complete
absolute path needs to be provided here.
xmodmap_path : string, None
Path to a dump of the xkb mappings, to provide more human
readable keys in the correct keyboard layout to the service.
The service cannot use `xmodmap -pke` because it's running via
systemd.
config_dir : string
Contains xmodmap.json and config.json of the current users session
"""
# reload the config, since it may have been changed
if config_dir is not None:
config_path = os.path.join(config_dir, 'config.json')
config.load_config(config_path)
if device not in get_devices():
logger.debug('Devices possibly outdated, refreshing')
refresh_devices()
# reload the config, since it may have been changed
config.load_config()
if self.injectors.get(device) is not None:
self.injectors[device].stop_injecting()
mapping = Mapping()
try:
mapping.load(path)
mapping.load(preset_path)
except FileNotFoundError as error:
logger.error(str(error))
return False
if xmodmap_path is not None:
# Path to a dump of the xkb mappings, to provide more human
# readable keys in the correct keyboard layout to the service.
# The service cannot use `xmodmap -pke` because it's running via
# systemd.
if config_dir is not None:
xmodmap_path = os.path.join(config_dir, 'xmodmap.json')
try:
with open(xmodmap_path, 'r') as file:
xmodmap = json.load(file)
@ -195,7 +201,7 @@ class Daemon:
logger.error('Could not find "%s"', xmodmap_path)
try:
injector = KeycodeInjector(device, mapping)
injector = Injector(device, mapping)
injector.start_injecting()
self.injectors[device] = injector
except OSError:

View File

@ -112,11 +112,11 @@ def is_in_capabilities(key, capabilities):
return False
class KeycodeInjector:
"""Keeps injecting keycodes in the background based on the mapping.
class Injector:
"""Keeps injecting events in the background based on mapping and config.
Is a process to make it non-blocking for the rest of the code and to
make running multiple injector easier. There is one procss per
make running multiple injector easier. There is one process per
hardware-device that is being mapped.
"""
regrab_timeout = 0.5

View File

@ -467,8 +467,7 @@ class Window:
self.show_status(CTX_APPLY, f'Applied preset "{preset}"')
path = get_preset_path(device, preset)
xmodmap = get_config_path(XMODMAP_FILENAME)
success = self.dbus.start_injecting(device, path, xmodmap)
success = self.dbus.start_injecting(device, path, get_config_path())
if not success:
self.show_status(CTX_ERROR, 'Error: Could not grab devices!')

View File

@ -30,6 +30,9 @@ import pwd
from keymapper.logger import logger
# TODO unittest everything in here
def get_user():
"""Try to find the user who called sudo/pkexec."""
try:

View File

@ -30,12 +30,13 @@ requests.
- [x] mapping joystick directions as buttons, making it act like a D-Pad
- [ ] mapping mouse wheel events to buttons
- [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] configure locale for preset to provide a different set of possible keys
- [ ] user-friendly way to map btn_left
- [ ] using keys that aren't available in the systems keyboard layout
- [ ] user-friendly way to map the left mouse button
## Tests
```bash
sudo pip install coverage
pylint keymapper --extension-pkg-whitelist=evdev
sudo pip install . && coverage run tests/test.py
coverage combine && coverage report -m

View File

@ -380,7 +380,7 @@ patch_unsaved()
patch_select()
from keymapper.logger import update_verbosity
from keymapper.dev.injector import KeycodeInjector
from keymapper.dev.injector import Injector
from keymapper.config import config
from keymapper.dev.reader import keycode_reader
from keymapper.getdevices import refresh_devices
@ -388,7 +388,7 @@ from keymapper.state import system_mapping, custom_mapping
from keymapper.dev.keycode_mapper import active_macros, unreleased
# no need for a high number in tests
KeycodeInjector.regrab_timeout = 0.15
Injector.regrab_timeout = 0.15
_fixture_copy = copy.deepcopy(fixtures)
@ -401,8 +401,9 @@ def cleanup():
keycode_reader.newest_event = None
keycode_reader._unreleased = {}
for task in asyncio.Task.all_tasks():
task.cancel()
if asyncio.get_event_loop().is_running():
for task in asyncio.all_tasks():
task.cancel()
os.system('pkill -f key-mapper-service')

View File

@ -71,7 +71,7 @@ class TestControl(unittest.TestCase):
get_preset_path(devices[0], presets[0]),
get_preset_path(devices[1], presets[1])
]
xmodmap = 'a/xmodmap.json'
config_dir = '/foo/bar'
Mapping().save(paths[0])
Mapping().save(paths[1])
@ -86,17 +86,17 @@ class TestControl(unittest.TestCase):
config.set_autoload_preset(devices[0], presets[0])
config.set_autoload_preset(devices[1], presets[1])
control(options('autoload', None, None, False, False), daemon, xmodmap)
control(options('autoload', None, None, False, False), daemon, config_dir)
self.assertEqual(len(start_history), 2)
self.assertEqual(len(stop_history), 1)
self.assertEqual(start_history[0], (devices[0], os.path.expanduser(paths[0]), xmodmap))
self.assertEqual(start_history[1], (devices[1], os.path.abspath(paths[1]), xmodmap))
self.assertEqual(start_history[0], (devices[0], os.path.expanduser(paths[0]), config_dir))
self.assertEqual(start_history[1], (devices[1], os.path.abspath(paths[1]), config_dir))
def test_start_stop(self):
device = 'device 1234'
path = '~/a/preset.json'
xmodmap = 'a/xmodmap.json'
config_dir = '/foo/bar'
daemon = Daemon()
@ -105,12 +105,12 @@ class TestControl(unittest.TestCase):
daemon.start_injecting = lambda *args: start_history.append(args)
daemon.stop_injecting = lambda *args: stop_history.append(args)
control(options('start', path, device, False, False), daemon, xmodmap)
control(options('start', path, device, False, False), daemon, config_dir)
control(options('stop', None, device, False, False), daemon, None)
self.assertEqual(len(start_history), 1)
self.assertEqual(len(stop_history), 1)
self.assertEqual(start_history[0], (device, os.path.expanduser(path), xmodmap))
self.assertEqual(start_history[0], (device, os.path.expanduser(path), config_dir))
self.assertEqual(stop_history[0], (device,))

View File

@ -248,18 +248,30 @@ class TestDaemon(unittest.TestCase):
InputEvent(*event)
]
xmodmap_path = os.path.join(tmp, 'foobar.json')
config_dir = os.path.join(tmp, 'foo')
os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, 'config.json')
with open(config_path, 'w') as file:
file.write('{"bar":1234}')
xmodmap_path = os.path.join(config_dir, 'xmodmap.json')
with open(xmodmap_path, 'w') as file:
file.write(f'{{"{to_name}":{to_keycode}}}')
self.daemon = Daemon()
self.daemon.start_injecting(device, path, xmodmap_path)
self.daemon.start_injecting(device, path, config_dir)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, EV_KEY)
self.assertEqual(event.code, to_keycode)
self.assertEqual(event.value, 1)
# since the daemon is running in the same process, the config
# that the test knows will be overwritten
self.assertEqual(config.get('bar'), 1234)
if __name__ == "__main__":
unittest.main()

View File

@ -27,7 +27,7 @@ import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_LEFT, KEY_A
from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, KeycodeInjector, is_in_capabilities
ensure_numlock, Injector, is_in_capabilities
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME
from keymapper.config import config
@ -101,7 +101,7 @@ class TestInjector(unittest.TestCase):
two = system_mapping.get('2')
btn_left = system_mapping.get('BtN_lEfT')
self.injector = KeycodeInjector('foo', mapping)
self.injector = Injector('foo', mapping)
fake_device = FakeDevice()
capabilities_1 = self.injector._modify_capabilities(
{60: macro},
@ -141,7 +141,7 @@ class TestInjector(unittest.TestCase):
# path is from the fixtures
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event10'
# this test needs to pass around all other constraints of
# _prepare_device
@ -155,7 +155,7 @@ class TestInjector(unittest.TestCase):
self.make_it_fail = 10
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event10'
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
@ -171,7 +171,7 @@ class TestInjector(unittest.TestCase):
def test_prepare_device_1(self):
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = KeycodeInjector('foobar', custom_mapping)
self.injector = Injector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
self.assertIsNone(_prepare_device('/dev/input/event10')[0])
@ -179,13 +179,13 @@ class TestInjector(unittest.TestCase):
def test_prepare_device_non_existing(self):
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = KeycodeInjector('foobar', custom_mapping)
self.injector = Injector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
self.assertIsNone(_prepare_device('/dev/input/event1234')[0])
def test_gamepad_capabilities(self):
self.injector = KeycodeInjector('gamepad', custom_mapping)
self.injector = Injector('gamepad', custom_mapping)
path = '/dev/input/event30'
device, abs_to_rel = self.injector._prepare_device(path)
@ -210,7 +210,7 @@ class TestInjector(unittest.TestCase):
def test_adds_ev_key(self):
# for some reason, having any EV_KEY capability is needed to
# be able to control the mouse. it probably wants the mouse click.
self.injector = KeycodeInjector('gamepad 2', custom_mapping)
self.injector = Injector('gamepad 2', custom_mapping)
"""gamepad without any existing key capability"""
@ -263,7 +263,7 @@ class TestInjector(unittest.TestCase):
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event11'
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
@ -272,7 +272,7 @@ class TestInjector(unittest.TestCase):
def test_skip_unknown_device(self):
# skips a device because its capabilities are not used in the mapping
self.injector = KeycodeInjector('device 1', custom_mapping)
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event11'
device, _ = self.injector._prepare_device(path)
@ -329,7 +329,7 @@ class TestInjector(unittest.TestCase):
InputEvent(EV_ABS, rel_y, -y),
]
self.injector = KeycodeInjector('gamepad', custom_mapping)
self.injector = Injector('gamepad', custom_mapping)
self.injector.start_injecting()
# wait for the injector to start sending, at most 1s
@ -400,7 +400,7 @@ class TestInjector(unittest.TestCase):
InputEvent(3124, 3564, 6542),
]
self.injector = KeycodeInjector('device 2', custom_mapping)
self.injector = Injector('device 2', custom_mapping)
self.injector.start_injecting()
uinput_write_history_pipe[0].poll(timeout=1)
@ -500,7 +500,7 @@ class TestInjector(unittest.TestCase):
InputEvent(*d_up),
]
self.injector = KeycodeInjector('gamepad', custom_mapping)
self.injector = Injector('gamepad', custom_mapping)
# the injector will otherwise skip the device because
# the capabilities don't contain EV_TYPE
@ -536,7 +536,7 @@ class TestInjector(unittest.TestCase):
ev_3 = (EV_KEY, 43, 1)
# a combination
mapping.change(Key(ev_1, ev_2, ev_3), 'k(a)')
self.injector = KeycodeInjector('device 1', mapping)
self.injector = Injector('device 1', mapping)
history = []
@ -576,7 +576,7 @@ class TestInjector(unittest.TestCase):
system_mapping._set('a', 51)
system_mapping._set('b', 52)
injector = KeycodeInjector('device 1', mapping)
injector = Injector('device 1', mapping)
self.assertEqual(injector._key_to_code.get((ev_1,)), 51)
# permutations to make matching combinations easier
self.assertEqual(injector._key_to_code.get((ev_2, ev_3, ev_4)), 52)