add event type to mapping key

This commit is contained in:
sezanzeb 2020-12-02 19:33:31 +01:00 committed by sezanzeb
parent 5fe404dd2d
commit edf71e82e0
14 changed files with 180 additions and 116 deletions

View File

@ -41,8 +41,13 @@ INITIAL_CONFIG = {
'keystroke_sleep_ms': 10 'keystroke_sleep_ms': 10
}, },
'gamepad': { 'gamepad': {
'non_linearity': 4, 'joystick': {
'pointer_speed': 80 'non_linearity': 4,
'pointer_speed': 80,
},
'triggers': {
'button_threshold': 0.5
},
} }
} }
@ -52,10 +57,15 @@ class _Config:
self._config = {} self._config = {}
self.load_config() self.load_config()
def _resolve(self, path, func): def _resolve(self, path, func, config=None):
"""Call func for the given config value.""" """Call func for the given config value."""
chunks = path.split('.') chunks = path.split('.')
child = self._config
if config is None:
child = self._config
else:
child = config
while True: while True:
chunk = chunks.pop(0) chunk = chunks.pop(0)
parent = child parent = child
@ -97,19 +107,27 @@ class _Config:
self._resolve(path, do) self._resolve(path, do)
def get(self, path, default=None): def get(self, path, log_unknown=True):
"""Get a config value. """Get a config value. If not set, return the default
Parameters Parameters
---------- ----------
path : string path : string
For example 'macros.keystroke_sleep_ms' For example 'macros.keystroke_sleep_ms'
default : any log_unknown : bool
If the configured value is not available or None, return this If True, write an error.
instead
""" """
resolved = self._resolve(path, lambda parent, child, chunk: child) def do(parent, child, chunk):
return resolved if resolved is not None else default return child
resolved = self._resolve(path, do)
if resolved is None:
resolved = self._resolve(path, do, INITIAL_CONFIG)
if resolved is None and log_unknown:
logger.error('Unknown config key "%s"', path)
return resolved
def set_autoload_preset(self, device, preset): def set_autoload_preset(self, device, preset):
"""Set a preset to be automatically applied on start. """Set a preset to be automatically applied on start.
@ -118,7 +136,7 @@ class _Config:
---------- ----------
device : string device : string
preset : string or None preset : string or None
if None, don't autoload something for this device if None, don't autoload something for this device.
""" """
if preset is not None: if preset is not None:
self.set(f'autoload.{device}', preset) self.set(f'autoload.{device}', preset)
@ -131,7 +149,7 @@ class _Config:
def is_autoloaded(self, device, preset): def is_autoloaded(self, device, preset):
"""Should this preset be loaded automatically?""" """Should this preset be loaded automatically?"""
return self.get(f'autoload.{device}') == preset return self.get(f'autoload.{device}', '') == preset
def load_config(self): def load_config(self):
"""Load the config from the file system.""" """Load the config from the file system."""
@ -139,7 +157,9 @@ class _Config:
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
# treated like an empty config # treated like an empty config
logger.debug('Config file "%s" doesn\'t exist', CONFIG_PATH) logger.debug('Config "%s" doesn\'t exist yet', CONFIG_PATH)
self.clear_config()
self.save_config()
return return
with open(CONFIG_PATH, 'r') as file: with open(CONFIG_PATH, 'r') as file:

View File

@ -57,8 +57,8 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device):
max_value = input_device.absinfo(EV_ABS).max max_value = input_device.absinfo(EV_ABS).max
max_speed = ((max_value ** 2) * 2) ** 0.5 max_speed = ((max_value ** 2) * 2) ** 0.5
pointer_speed = config.get('gamepad.pointer_speed', 80) pointer_speed = config.get('gamepad.joystick.pointer_speed')
non_linearity = config.get('gamepad.non_linearity', 4) non_linearity = config.get('gamepad.joystick.non_linearity')
while True: while True:
start = time.time() start = time.time()

View File

@ -119,6 +119,13 @@ class KeycodeInjector:
self._process = multiprocessing.Process(target=self._start_injecting) self._process = multiprocessing.Process(target=self._start_injecting)
self._process.start() self._process.start()
def map_ev_to_abs(self, capabilities):
"""Check if joystick movements can and should be mapped."""
# mapping buttons only works without ABS events in the capabilities,
# possibly due to some intentional constraints in the os. So always
# do this without the option to configure, if it is possible.
return evdev.ecodes.ABS_X in capabilities.get(EV_ABS, [])
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.
@ -135,12 +142,13 @@ class KeycodeInjector:
needed = False needed = False
if capabilities.get(EV_KEY) is not None: if capabilities.get(EV_KEY) is not None:
for keycode, _ in self.mapping: for (ev_type, keycode), _ in self.mapping:
if keycode - KEYCODE_OFFSET in capabilities[EV_KEY]: # TEST ev_type
if keycode - KEYCODE_OFFSET in capabilities.get(ev_type, []):
needed = True needed = True
break break
map_ev_abs = evdev.ecodes.ABS_X in capabilities.get(EV_ABS, []) map_ev_abs = self.map_ev_to_abs(capabilities)
if map_ev_abs: if map_ev_abs:
needed = True needed = True
@ -194,10 +202,10 @@ class KeycodeInjector:
if len(self.mapping) > 0 and capabilities.get(ecodes.EV_KEY) is None: if len(self.mapping) > 0 and capabilities.get(ecodes.EV_KEY) is None:
capabilities[ecodes.EV_KEY] = [] capabilities[ecodes.EV_KEY] = []
for _, character in self.mapping: for (ev_type, _), character in self.mapping:
keycode = system_mapping.get(character) keycode = system_mapping.get(character)
if keycode is not None: if keycode is not None:
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET) capabilities[ev_type].append(keycode - KEYCODE_OFFSET)
if map_ev_abs: if map_ev_abs:
del capabilities[ecodes.EV_ABS] del capabilities[ecodes.EV_ABS]
@ -320,7 +328,7 @@ class KeycodeInjector:
# Parse all macros beforehand # Parse all macros beforehand
logger.debug('Parsing macros') logger.debug('Parsing macros')
macros = {} macros = {}
for keycode, output in self.mapping: for (ev_type, keycode), output in self.mapping:
keycode -= KEYCODE_OFFSET keycode -= KEYCODE_OFFSET
if '(' in output and ')' in output and len(output) >= 4: if '(' in output and ')' in output and len(output) >= 4:

View File

@ -114,7 +114,7 @@ class _Macro:
def add_keycode_pause(self): def add_keycode_pause(self):
"""To add a pause between keystrokes.""" """To add a pause between keystrokes."""
sleeptime = config.get('macros.keystroke_sleep_ms', 10) / 1000 sleeptime = config.get('macros.keystroke_sleep_ms') / 1000
async def sleep(): async def sleep():
await asyncio.sleep(sleeptime) await asyncio.sleep(sleeptime)

View File

@ -83,6 +83,7 @@ class _GetDevices(threading.Thread):
# https://www.kernel.org/doc/html/latest/input/event-codes.html # https://www.kernel.org/doc/html/latest/input/event-codes.html
capabilities = device.capabilities().keys() capabilities = device.capabilities().keys()
if EV_KEY not in capabilities and EV_ABS not in capabilities: if EV_KEY not in capabilities and EV_ABS not in capabilities:
# or gamepads, because they can be mapped like a keyboard
continue continue
usb = device.phys.split('/')[0] usb = device.phys.split('/')[0]

View File

@ -27,6 +27,8 @@ gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0') gi.require_version('GLib', '2.0')
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
from evdev.ecodes import EV_KEY
from keymapper.state import custom_mapping from keymapper.state import custom_mapping
from keymapper.logger import logger from keymapper.logger import logger
@ -76,7 +78,7 @@ class Row(Gtk.ListBoxRow):
return return
# keycode is already set by some other row # keycode is already set by some other row
if custom_mapping.get_character(new_keycode) is not None: if custom_mapping.get_character(EV_KEY, new_keycode) is not None:
msg = f'Keycode {new_keycode} is already mapped' msg = f'Keycode {new_keycode} is already mapped'
logger.info(msg) logger.info(msg)
self.window.get('status_bar').push(CTX_KEYCODE, msg) self.window.get('status_bar').push(CTX_KEYCODE, msg)
@ -98,7 +100,7 @@ class Row(Gtk.ListBoxRow):
return return
# else, the keycode has changed, the character is set, all good # else, the keycode has changed, the character is set, all good
custom_mapping.change(new_keycode, character, previous_keycode) custom_mapping.change(EV_KEY, new_keycode, character, previous_keycode)
def highlight(self): def highlight(self):
"""Mark this row as changed.""" """Mark this row as changed."""
@ -116,7 +118,7 @@ class Row(Gtk.ListBoxRow):
self.highlight() self.highlight()
if keycode is not None: if keycode is not None:
custom_mapping.change( custom_mapping.change(EV_KEY,
previous_keycode=None, previous_keycode=None,
new_keycode=keycode, new_keycode=keycode,
character=character character=character
@ -178,7 +180,7 @@ class Row(Gtk.ListBoxRow):
"""Destroy the row and remove it from the config.""" """Destroy the row and remove it from the config."""
keycode = self.get_keycode() keycode = self.get_keycode()
if keycode is not None: if keycode is not None:
custom_mapping.clear(keycode) custom_mapping.clear(EV_KEY, keycode)
self.character_input.set_text('') self.character_input.set_text('')
self.keycode_input.set_label('') self.keycode_input.set_label('')
self.delete_callback(self) self.delete_callback(self)

View File

@ -374,7 +374,7 @@ class Window:
custom_mapping.load(self.selected_device, self.selected_preset) custom_mapping.load(self.selected_device, self.selected_preset)
key_list = self.get('key_list') key_list = self.get('key_list')
for keycode, output in custom_mapping: for (_, keycode), output in custom_mapping:
single_key_mapping = Row( single_key_mapping = Row(
window=self, window=self,
delete_callback=self.on_row_removed, delete_callback=self.on_row_removed,

View File

@ -25,6 +25,7 @@
import os import os
import json import json
import copy import copy
import evdev
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.paths import get_config_path, touch from keymapper.paths import get_config_path, touch
@ -50,16 +51,18 @@ class Mapping:
def __len__(self): def __len__(self):
return len(self._mapping) return len(self._mapping)
def change(self, new_keycode, character, previous_keycode=None): def change(self, ev_type, new_keycode, character, previous_keycode=None):
"""Replace the mapping of a keycode with a different one. """Replace the mapping of a keycode with a different one.
Return True on success. Return True on success.
Parameters Parameters
---------- ----------
ev_type : int
one of evdev.events. The original event
new_keycode : int new_keycode : int
The source keycode, what the mouse would report without any The source keycode, what the mouse would report without any
modification. modification. xkb keycode.
character : string or string[] character : string or string[]
A single character known to xkb, Examples: KP_1, Shift_L, a, B. A single character known to xkb, Examples: KP_1, Shift_L, a, B.
Can also be an array, which is used for reading the xkbmap output Can also be an array, which is used for reading the xkbmap output
@ -67,7 +70,7 @@ class Mapping:
previous_keycode : int or None previous_keycode : int or None
If None, will not remove any previous mapping. If you recently If None, will not remove any previous mapping. If you recently
used 10 for new_keycode and want to overwrite that with 11, used 10 for new_keycode and want to overwrite that with 11,
provide 5 here. provide 5 here. xkb keycode.
""" """
try: try:
new_keycode = int(new_keycode) new_keycode = int(new_keycode)
@ -78,25 +81,28 @@ class Mapping:
return False return False
if new_keycode and character: if new_keycode and character:
self._mapping[new_keycode] = character self._mapping[(ev_type, new_keycode)] = character
if new_keycode != previous_keycode: if new_keycode != previous_keycode:
# clear previous mapping of that code, because the line # clear previous mapping of that code, because the line
# representing that one will now represent a different one. # representing that one will now represent a different one.
self.clear(previous_keycode) self.clear(ev_type, previous_keycode)
self.changed = True self.changed = True
return True return True
return False return False
def clear(self, keycode): def clear(self, ev_type, keycode):
"""Remove a keycode from the mapping. """Remove a keycode from the mapping.
Parameters Parameters
---------- ----------
keycode : int keycode : int
the xkb keycode
ev_type : int
one of evdev.events
""" """
if self._mapping.get(keycode) is not None: if self._mapping.get((ev_type, keycode)) is not None:
del self._mapping[keycode] del self._mapping[(ev_type, keycode)]
self.changed = True self.changed = True
def empty(self): def empty(self):
@ -119,13 +125,23 @@ class Mapping:
logger.error('Invalid preset config at "%s"', path) logger.error('Invalid preset config at "%s"', path)
return return
for keycode, character in preset_dict['mapping'].items(): for key, character in preset_dict['mapping'].items():
if ',' not in key:
logger.error('Found invalid key: "%s"', key)
continue
ev_type, keycode = key.split(',')
try: try:
keycode = int(keycode) keycode = int(keycode)
except ValueError: except ValueError:
logger.error('Found non-int keycode: %s', keycode) logger.error('Found non-int keycode: "%s"', keycode)
continue continue
self._mapping[keycode] = character try:
ev_type = int(ev_type)
except ValueError:
logger.error('Found non-int ev_type: "%s"', ev_type)
continue
self._mapping[(ev_type, keycode)] = character
# add any metadata of the mapping # add any metadata of the mapping
for key in preset_dict: for key in preset_dict:
@ -152,18 +168,27 @@ class Mapping:
with open(path, 'w') as file: with open(path, 'w') as file:
# make sure to keep the option to add metadata if ever needed, # make sure to keep the option to add metadata if ever needed,
# so put the mapping into a special key # so put the mapping into a special key
preset_dict = {'mapping': self._mapping} json_ready_mapping = {}
# tuple keys are not possible in json
for key, value in self._mapping.items():
new_key = f'{key[0]},{key[1]}'
json_ready_mapping[new_key] = value
preset_dict = {'mapping': json_ready_mapping}
preset_dict.update(self.config) preset_dict.update(self.config)
json.dump(preset_dict, file, indent=4) json.dump(preset_dict, file, indent=4)
file.write('\n') file.write('\n')
self.changed = False self.changed = False
def get_character(self, keycode): def get_character(self, ev_type, keycode):
"""Read the character that is mapped to this keycode. """Read the character that is mapped to this keycode.
Parameters Parameters
---------- ----------
keycode : int keycode : int
ev_type : int
one of evdev.events. codes may be the same for various
event types.
""" """
return self._mapping.get(keycode) return self._mapping.get((ev_type, keycode))

View File

@ -30,9 +30,15 @@ class TestConfig(unittest.TestCase):
self.assertEqual(len(config.iterate_autoload_presets()), 0) self.assertEqual(len(config.iterate_autoload_presets()), 0)
config.save_config() config.save_config()
def test_get_default(self):
config._config = {}
self.assertEqual(config.get('gamepad.joystick.non_linearity'), 4)
config.set('gamepad.joystick.non_linearity', 3)
self.assertEqual(config.get('gamepad.joystick.non_linearity'), 3)
def test_basic(self): def test_basic(self):
self.assertEqual(config.get('a'), None) self.assertEqual(config.get('a'), None)
self.assertEqual(config.get('a', 'foo'), 'foo')
config.set('a', 1) config.set('a', 1)
self.assertEqual(config.get('a'), 1) self.assertEqual(config.get('a'), 1)

View File

@ -25,6 +25,7 @@ import unittest
import time import time
import evdev import evdev
from evdev.ecodes import EV_KEY
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
@ -84,8 +85,8 @@ class TestDaemon(unittest.TestCase):
keycode_from_2 = 12 keycode_from_2 = 12
keycode_to_2 = 100 keycode_to_2 = 100
custom_mapping.change(keycode_from_1, 'a') custom_mapping.change(EV_KEY, keycode_from_1, 'a')
custom_mapping.change(keycode_from_2, 'b') custom_mapping.change(EV_KEY, keycode_from_2, 'b')
clear_system_mapping() clear_system_mapping()
system_mapping['a'] = keycode_to_1 system_mapping['a'] = keycode_to_1
system_mapping['b'] = keycode_to_2 system_mapping['b'] = keycode_to_2

View File

@ -76,7 +76,7 @@ class TestInjector(unittest.TestCase):
} }
mapping = Mapping() mapping = Mapping()
mapping.change( mapping.change(EV_KEY,
new_keycode=80, new_keycode=80,
character='a' character='a'
) )
@ -99,7 +99,7 @@ class TestInjector(unittest.TestCase):
def test_grab(self): def test_grab(self):
# path is from the fixtures # path is from the fixtures
custom_mapping.change(10, 'a') custom_mapping.change(EV_KEY, 10, 'a')
self.injector = KeycodeInjector('device 1', custom_mapping) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event10' path = '/dev/input/event10'
@ -124,7 +124,7 @@ class TestInjector(unittest.TestCase):
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(EV_KEY, 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, map_ev_abs = self.injector._prepare_device(path) device, map_ev_abs = self.injector._prepare_device(path)
@ -166,11 +166,9 @@ 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_ev_abs returns true config.set('gamepad.joystick.non_linearity', 1)
# in the .json file of the mapping.
config.set('gamepad.non_linearity', 1)
pointer_speed = 80 pointer_speed = 80
config.set('gamepad.pointer_speed', pointer_speed) config.set('gamepad.joystick.pointer_speed', pointer_speed)
# same for ABS, 0 for x, 1 for y # same for ABS, 0 for x, 1 for y
rel_x = evdev.ecodes.REL_X rel_x = evdev.ecodes.REL_X
@ -286,11 +284,11 @@ class TestInjector(unittest.TestCase):
self.assertIn(('b', 0), history) self.assertIn(('b', 0), history)
def test_injector(self): def test_injector(self):
custom_mapping.change(8, 'k(KEY_Q).k(w)') custom_mapping.change(EV_KEY, 8, 'k(KEY_Q).k(w)')
custom_mapping.change(9, 'a') custom_mapping.change(EV_KEY, 9, 'a')
# one mapping that is unknown in the system_mapping on purpose # one mapping that is unknown in the system_mapping on purpose
input_b = 10 input_b = 10
custom_mapping.change(input_b, 'b') custom_mapping.change(EV_KEY, input_b, 'b')
clear_system_mapping() clear_system_mapping()
code_a = 100 code_a = 100

View File

@ -24,6 +24,7 @@ import time
import os import os
import unittest import unittest
import evdev import evdev
from evdev.events import EV_KEY
import json import json
from unittest.mock import patch from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec from importlib.util import spec_from_loader, module_from_spec
@ -156,9 +157,9 @@ class TestIntegration(unittest.TestCase):
def test_select_device(self): def test_select_device(self):
# creates a new empty preset when no preset exists for the device # creates a new empty preset when no preset exists for the device
self.window.on_select_device(FakeDropdown('device 1')) self.window.on_select_device(FakeDropdown('device 1'))
custom_mapping.change(50, 'q') custom_mapping.change(EV_KEY, 50, 'q')
custom_mapping.change(51, 'u') custom_mapping.change(EV_KEY, 51, 'u')
custom_mapping.change(52, 'x') custom_mapping.change(EV_KEY, 52, 'x')
self.assertEqual(len(custom_mapping), 3) self.assertEqual(len(custom_mapping), 3)
self.window.on_select_device(FakeDropdown('device 2')) self.window.on_select_device(FakeDropdown('device 2'))
self.assertEqual(len(custom_mapping), 0) self.assertEqual(len(custom_mapping), 0)
@ -202,7 +203,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(len(self.window.get('key_list').get_children()), 2) self.assertEqual(len(self.window.get('key_list').get_children()), 2)
self.assertEqual(custom_mapping.get_character(30), 'Shift_L') self.assertEqual(custom_mapping.get_character(EV_KEY, 30), 'Shift_L')
self.assertEqual(row.get_character(), 'Shift_L') self.assertEqual(row.get_character(), 'Shift_L')
self.assertEqual(row.get_keycode(), 30) self.assertEqual(row.get_keycode(), 30)
@ -270,8 +271,8 @@ class TestIntegration(unittest.TestCase):
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(len(self.get_rows()), num_rows_target) self.assertEqual(len(self.get_rows()), num_rows_target)
self.assertEqual(custom_mapping.get_character(10), 'a') self.assertEqual(custom_mapping.get_character(EV_KEY, 10), 'a')
self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
self.assertTrue(custom_mapping.changed) self.assertTrue(custom_mapping.changed)
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
@ -293,13 +294,13 @@ class TestIntegration(unittest.TestCase):
row.get_style_context().list_classes() row.get_style_context().list_classes()
) )
self.assertEqual(custom_mapping.get_character(10), 'c') self.assertEqual(custom_mapping.get_character(EV_KEY, 10), 'c')
self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
self.assertTrue(custom_mapping.changed) self.assertTrue(custom_mapping.changed)
# try to add a duplicate keycode, it should be ignored # try to add a duplicate keycode, it should be ignored
self.change_empty_row(11, 'd', success=False) self.change_empty_row(11, 'd', success=False)
self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
# and the number of rows shouldn't change # and the number of rows shouldn't change
self.assertEqual(len(self.get_rows()), num_rows_target) self.assertEqual(len(self.get_rows()), num_rows_target)
@ -316,11 +317,11 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(len(self.get_rows()), 3) self.assertEqual(len(self.get_rows()), 3)
self.assertEqual(custom_mapping.get_character(11), 'b') self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'b')
def remove(row, code, char, num_rows_after): def remove(row, code, char, num_rows_after):
if code is not None and char is not None: if code is not None and char is not None:
self.assertEqual(custom_mapping.get_character(code), char) self.assertEqual(custom_mapping.get_character(EV_KEY, code), char)
self.assertEqual(row.get_character(), char) self.assertEqual(row.get_character(), char)
self.assertEqual(row.get_keycode(), code) self.assertEqual(row.get_keycode(), code)
row.on_delete_button_clicked() row.on_delete_button_clicked()
@ -328,7 +329,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_keycode())
self.assertIsNone(row.get_character()) self.assertIsNone(row.get_character())
self.assertIsNone(custom_mapping.get_character(code)) self.assertIsNone(custom_mapping.get_character(EV_KEY, code))
self.assertEqual(len(self.get_rows()), num_rows_after) self.assertEqual(len(self.get_rows()), num_rows_after)
remove(row_1, 10, 'a', 2) remove(row_1, 10, 'a', 2)
@ -339,17 +340,17 @@ class TestIntegration(unittest.TestCase):
remove(row_3, None, 'c', 1) remove(row_3, None, 'c', 1)
def test_rename_and_save(self): def test_rename_and_save(self):
custom_mapping.change(14, 'a', None) custom_mapping.change(EV_KEY, 14, 'a', None)
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertEqual(custom_mapping.get_character(14), 'a') self.assertEqual(custom_mapping.get_character(EV_KEY, 14), 'a')
custom_mapping.change(14, 'b', None) custom_mapping.change(EV_KEY, 14, 'b', None)
self.window.get('preset_name_input').set_text('asdf') self.window.get('preset_name_input').set_text('asdf')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertEqual(self.window.selected_preset, 'asdf') self.assertEqual(self.window.selected_preset, 'asdf')
self.assertTrue(os.path.exists(f'{CONFIG}/device 1/asdf.json')) self.assertTrue(os.path.exists(f'{CONFIG}/device 1/asdf.json'))
self.assertEqual(custom_mapping.get_character(14), 'b') self.assertEqual(custom_mapping.get_character(EV_KEY, 14), 'b')
def test_select_device_and_preset(self): def test_select_device_and_preset(self):
# created on start because the first device is selected and some empty # created on start because the first device is selected and some empty
@ -379,7 +380,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.assertFalse(os.path.exists(f'{CONFIG}/device 1/abc 123.json')) self.assertFalse(os.path.exists(f'{CONFIG}/device 1/abc 123.json'))
custom_mapping.change(10, '1', None) custom_mapping.change(EV_KEY, 10, '1', None)
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'abc 123') self.assertEqual(self.window.selected_preset, 'abc 123')

View File

@ -57,7 +57,7 @@ class TestMacros(unittest.TestCase):
repeats = 20 repeats = 20
macro = f'r({repeats}, k(k))' macro = f'r({repeats}, k(k))'
self.loop.run_until_complete(parse(macro, self.handler).run()) self.loop.run_until_complete(parse(macro, self.handler).run())
keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10) keystroke_sleep = config.get('macros.keystroke_sleep_ms')
sleep_time = 2 * repeats * keystroke_sleep / 1000 sleep_time = 2 * repeats * keystroke_sleep / 1000
self.assertGreater(time.time() - start, sleep_time * 0.9) self.assertGreater(time.time() - start, sleep_time * 0.9)
self.assertLess(time.time() - start, sleep_time * 1.1) self.assertLess(time.time() - start, sleep_time * 1.1)
@ -68,7 +68,7 @@ class TestMacros(unittest.TestCase):
macro = 'r(3, k(m).w(100))' macro = 'r(3, k(m).w(100))'
self.loop.run_until_complete(parse(macro, self.handler).run()) self.loop.run_until_complete(parse(macro, self.handler).run())
keystroke_time = 6 * config.get('macros.keystroke_sleep_ms', 10) keystroke_time = 6 * config.get('macros.keystroke_sleep_ms')
total_time = keystroke_time + 300 total_time = keystroke_time + 300
total_time /= 1000 total_time /= 1000
@ -97,7 +97,7 @@ class TestMacros(unittest.TestCase):
self.loop.run_until_complete(parse(macro, self.handler).run()) self.loop.run_until_complete(parse(macro, self.handler).run())
num_pauses = 8 + 6 + 4 num_pauses = 8 + 6 + 4
keystroke_time = num_pauses * config.get('macros.keystroke_sleep_ms', 10) keystroke_time = num_pauses * config.get('macros.keystroke_sleep_ms')
wait_time = 220 wait_time = 220
total_time = (keystroke_time + wait_time) / 1000 total_time = (keystroke_time + wait_time) / 1000

View File

@ -20,6 +20,7 @@
import unittest import unittest
from evdev.events import EV_KEY
from keymapper.mapping import Mapping from keymapper.mapping import Mapping
from keymapper.state import populate_system_mapping from keymapper.state import populate_system_mapping
@ -31,6 +32,7 @@ class TestMapping(unittest.TestCase):
self.assertFalse(self.mapping.changed) self.assertFalse(self.mapping.changed)
def test_populate_system_mapping(self): def test_populate_system_mapping(self):
# not actually a mapping object, just a dict
mapping = populate_system_mapping() mapping = populate_system_mapping()
self.assertGreater(len(mapping), 100) self.assertGreater(len(mapping), 100)
# xkb keycode 10 is typically mapped to '1' # xkb keycode 10 is typically mapped to '1'
@ -41,20 +43,20 @@ class TestMapping(unittest.TestCase):
def test_clone(self): def test_clone(self):
mapping1 = Mapping() mapping1 = Mapping()
mapping1.change(1, 'a') mapping1.change(EV_KEY, 1, 'a')
mapping2 = mapping1.clone() mapping2 = mapping1.clone()
mapping1.change(2, 'b') mapping1.change(EV_KEY, 2, 'b')
self.assertEqual(mapping1.get_character(1), 'a') self.assertEqual(mapping1.get_character(EV_KEY, 1), 'a')
self.assertEqual(mapping1.get_character(2), 'b') self.assertEqual(mapping1.get_character(EV_KEY, 2), 'b')
self.assertEqual(mapping2.get_character(1), 'a') self.assertEqual(mapping2.get_character(EV_KEY, 1), 'a')
self.assertIsNone(mapping2.get_character(2)) self.assertIsNone(mapping2.get_character(EV_KEY, 2))
def test_save_load(self): def test_save_load(self):
self.mapping.change(10, '1') self.mapping.change(EV_KEY, 10, '1')
self.mapping.change(11, '2') self.mapping.change(EV_KEY, 11, '2')
self.mapping.change(12, '3') self.mapping.change(EV_KEY, 12, '3')
self.mapping.config['foo'] = 'bar' self.mapping.config['foo'] = 'bar'
self.mapping.save('device 1', 'test') self.mapping.save('device 1', 'test')
@ -63,77 +65,77 @@ class TestMapping(unittest.TestCase):
loaded.load('device 1', 'test') loaded.load('device 1', 'test')
self.assertEqual(len(loaded), 3) self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.get_character(10), '1') self.assertEqual(loaded.get_character(EV_KEY, 10), '1')
self.assertEqual(loaded.get_character(11), '2') self.assertEqual(loaded.get_character(EV_KEY, 11), '2')
self.assertEqual(loaded.get_character(12), '3') self.assertEqual(loaded.get_character(EV_KEY, 12), '3')
self.assertEqual(loaded.config['foo'], 'bar') self.assertEqual(loaded.config['foo'], 'bar')
def test_change(self): def test_change(self):
# 1 is not assigned yet, ignore it # 1 is not assigned yet, ignore it
self.mapping.change(2, 'a', 1) self.mapping.change(EV_KEY, 2, 'a', 1)
self.assertTrue(self.mapping.changed) self.assertTrue(self.mapping.changed)
self.assertIsNone(self.mapping.get_character(1)) self.assertIsNone(self.mapping.get_character(EV_KEY, 1))
self.assertEqual(self.mapping.get_character(2), 'a') self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a')
self.assertEqual(len(self.mapping), 1) self.assertEqual(len(self.mapping), 1)
# change 2 to 3 and change a to b # change 2 to 3 and change a to b
self.mapping.change(3, 'b', 2) self.mapping.change(EV_KEY, 3, 'b', 2)
self.assertIsNone(self.mapping.get_character(2)) self.assertIsNone(self.mapping.get_character(EV_KEY, 2))
self.assertEqual(self.mapping.get_character(3), 'b') self.assertEqual(self.mapping.get_character(EV_KEY, 3), 'b')
self.assertEqual(len(self.mapping), 1) self.assertEqual(len(self.mapping), 1)
# add 4 # add 4
self.mapping.change(4, 'c', None) self.mapping.change(EV_KEY, 4, 'c', None)
self.assertEqual(self.mapping.get_character(3), 'b') self.assertEqual(self.mapping.get_character(EV_KEY, 3), 'b')
self.assertEqual(self.mapping.get_character(4), 'c') self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'c')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# change the mapping of 4 to d # change the mapping of 4 to d
self.mapping.change(4, 'd', None) self.mapping.change(EV_KEY, 4, 'd', None)
self.assertEqual(self.mapping.get_character(4), 'd') self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'd')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# this also works in the same way # this also works in the same way
self.mapping.change(4, 'e', 4) self.mapping.change(EV_KEY, 4, 'e', 4)
self.assertEqual(self.mapping.get_character(4), 'e') self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'e')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# and this # and this
self.mapping.change('4', 'f', '4') self.mapping.change(EV_KEY, '4', 'f', '4')
self.assertEqual(self.mapping.get_character(4), 'f') self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'f')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# non-int keycodes are ignored # non-int keycodes are ignored
self.mapping.change('b', 'c', 'a') self.mapping.change(EV_KEY, 'b', 'c', 'a')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
def test_clear(self): def test_clear(self):
# does nothing # does nothing
self.mapping.clear(40) self.mapping.clear(EV_KEY, 40)
self.assertFalse(self.mapping.changed) self.assertFalse(self.mapping.changed)
self.assertEqual(len(self.mapping), 0) self.assertEqual(len(self.mapping), 0)
self.mapping._mapping[40] = 'b' self.mapping._mapping[(EV_KEY, 40)] = 'b'
self.assertEqual(len(self.mapping), 1) self.assertEqual(len(self.mapping), 1)
self.mapping.clear(40) self.mapping.clear(EV_KEY, 40)
self.assertEqual(len(self.mapping), 0) self.assertEqual(len(self.mapping), 0)
self.assertTrue(self.mapping.changed) self.assertTrue(self.mapping.changed)
self.mapping.change(10, 'KP_1', None) self.mapping.change(EV_KEY, 10, 'KP_1', None)
self.assertTrue(self.mapping.changed) self.assertTrue(self.mapping.changed)
self.mapping.change(20, 'KP_2', None) self.mapping.change(EV_KEY, 20, 'KP_2', None)
self.mapping.change(30, 'KP_3', None) self.mapping.change(EV_KEY, 30, 'KP_3', None)
self.assertEqual(len(self.mapping), 3) self.assertEqual(len(self.mapping), 3)
self.mapping.clear(20) self.mapping.clear(EV_KEY, 20)
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
self.assertEqual(self.mapping.get_character(10), 'KP_1') self.assertEqual(self.mapping.get_character(EV_KEY, 10), 'KP_1')
self.assertIsNone(self.mapping.get_character(20)) self.assertIsNone(self.mapping.get_character(EV_KEY, 20))
self.assertEqual(self.mapping.get_character(30), 'KP_3') self.assertEqual(self.mapping.get_character(EV_KEY, 30), 'KP_3')
def test_empty(self): def test_empty(self):
self.mapping.change(10, '1') self.mapping.change(EV_KEY, 10, '1')
self.mapping.change(11, '2') self.mapping.change(EV_KEY, 11, '2')
self.mapping.change(12, '3') self.mapping.change(EV_KEY, 12, '3')
self.assertEqual(len(self.mapping), 3) self.assertEqual(len(self.mapping), 3)
self.mapping.empty() self.mapping.empty()
self.assertEqual(len(self.mapping), 0) self.assertEqual(len(self.mapping), 0)