started work on injecting keycodes instead of configs-only

This commit is contained in:
sezanzeb 2020-11-17 14:35:36 +01:00
parent 0796cea1bd
commit d91694c1df
8 changed files with 79 additions and 49 deletions

View File

@ -264,7 +264,8 @@ def generate_symbols(name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping)
keycodes = re.findall(r'<.+?>', f.read()) keycodes = re.findall(r'<.+?>', f.read())
xkb_symbols = [] xkb_symbols = []
for keycode, character in mapping: for keycode, output in mapping:
target_keycode, character = output
if f'<{keycode}>' not in keycodes: if f'<{keycode}>' not in keycodes:
logger.error(f'Unknown keycode <{keycode}> for "{character}"') logger.error(f'Unknown keycode <{keycode}> for "{character}"')
# don't append that one, otherwise X would crash when loading # don't append that one, otherwise X would crash when loading

View File

@ -86,7 +86,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(new_keycode) is not None: if custom_mapping.get_character(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)

View File

@ -301,12 +301,12 @@ class Window:
parse_symbols_file(self.selected_device, self.selected_preset) parse_symbols_file(self.selected_device, self.selected_preset)
key_list = self.get('key_list') key_list = self.get('key_list')
for keycode, character 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,
keycode=keycode, keycode=keycode,
character=character character=output[1]
) )
key_list.insert(single_key_mapping, -1) key_list.insert(single_key_mapping, -1)

View File

@ -24,6 +24,7 @@
import subprocess import subprocess
import multiprocessing import multiprocessing
import threading
import evdev import evdev
@ -46,9 +47,10 @@ class KeycodeReader:
"""Keeps reading keycodes in the background for the UI to use. """Keeps reading keycodes in the background for the UI to use.
When a button was pressed, the newest keycode can be obtained from this When a button was pressed, the newest keycode can be obtained from this
object. This was written before I figured out there is get_keycode in Gdk. object.
""" """
def __init__(self): def __init__(self, device):
self.device = device
self.virtual_devices = [] self.virtual_devices = []
def clear(self): def clear(self):
@ -58,22 +60,35 @@ class KeycodeReader:
while virtual_device.read_one(): while virtual_device.read_one():
pass pass
def start_reading(self, device): def start_injecting_worker(self, path):
"""Tell the evdev lib to start looking for keycodes. """Inject keycodes for one of the virtual devices."""
device = evdev.InputDevice(path)
uinput = evdev.UInput
for event in device.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
# value: 1 for down, 0 for up, 2 for hold.
# this happens to report key codes that are 8 lower
# than the ones reported by xev
# TODO the mapping writes something starting from 256 to not
# clash with existing mappings of other devices
input_keycode = event.code
output_keycode = custom_mapping.get_keycode(event.code)
output_char = custom_mapping.get_character(event.code)
print('output', output_keycode, output_char)
uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A)
If read is called without prior start_reading, no keycodes def start_injecting(self):
will be available. """Read keycodes and inject the mapped character forever."""
""" paths = _devices[self.device]['paths']
paths = _devices[device]['paths']
logger.debug( logger.debug(
'Starting reading keycodes for %s on %s', 'Starting injecting the mapping for %s on %s',
device, self.device,
', '.join(paths) ', '.join(paths)
) )
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per hardware
self.virtual_devices = [ virtual_devices = [
evdev.InputDevice(path) evdev.InputDevice(path)
for path in paths for path in paths
] ]
@ -94,7 +109,6 @@ class KeycodeReader:
return newest_keycode return newest_keycode
# not used anymore since the overlooked get_keycode function is now being used
# keycode_reader = KeycodeReader() # keycode_reader = KeycodeReader()

View File

@ -52,6 +52,8 @@ class Mapping:
previous_keycode : int or None previous_keycode : int or None
If None, will not remove any previous mapping. If None, will not remove any previous mapping.
new_keycode : int new_keycode : int
The source keycode, what the mouse would report without any
modification.
character : string or string[] character : string or string[]
If an array of strings, will put something like { [ a, A ] }; If an array of strings, will put something like { [ a, A ] };
into the symbols file. into the symbols file.
@ -73,7 +75,16 @@ class Mapping:
character = ', '.join([str(c) for c in character]) character = ', '.join([str(c) for c in character])
if new_keycode and character: if new_keycode and character:
self._mapping[new_keycode] = str(character) target_keycode = new_keycode + 256
if target_keycode >= 512:
# because key-mappers keycodes file has a maximum of 511,
# while all system keycodes have a maximum of 255.
# To avoid clashes, keycodes <= 255 should not be injected.
raise ValueError(
f'Expected target_keycode {target_keycode} to not '
f'be >= 512. '
)
self._mapping[new_keycode] = (target_keycode, str(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.
@ -99,14 +110,18 @@ class Mapping:
self._mapping = {} self._mapping = {}
self.changed = True self.changed = True
def get(self, keycode): def get_keycode(self, keycode):
"""Read the output keycode that is mapped to this input keycode."""
return self._mapping.get(keycode, (None, None))[0]
def get_character(self, 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
""" """
return self._mapping.get(keycode) return self._mapping.get(keycode, (None, None))[1]
# one mapping object for the whole application that holds all # one mapping object for the whole application that holds all

View File

@ -38,8 +38,8 @@ class TestConfig(unittest.TestCase):
custom_mapping.change(None, 11, 'KP_1') custom_mapping.change(None, 11, 'KP_1')
custom_mapping.change(None, 12, 3) custom_mapping.change(None, 12, 3)
custom_mapping.change(None, 13, ['a', 'A', 'NoSymbol', 3]) custom_mapping.change(None, 13, ['a', 'A', 'NoSymbol', 3])
self.assertEqual(custom_mapping.get(12), '3') self.assertEqual(custom_mapping.get_character(12), '3')
self.assertEqual(custom_mapping.get(13), 'a, A, NoSymbol, 3') self.assertEqual(custom_mapping.get_character(13), 'a, A, NoSymbol, 3')
if os.path.exists(tmp): if os.path.exists(tmp):
shutil.rmtree(tmp) shutil.rmtree(tmp)
@ -66,12 +66,12 @@ class TestConfig(unittest.TestCase):
# it should be loaded correctly after saving # it should be loaded correctly after saving
parse_symbols_file('device_a', 'preset_b') parse_symbols_file('device_a', 'preset_b')
self.assertIsNone(custom_mapping.get(9)) self.assertIsNone(custom_mapping.get_character(9))
self.assertEqual(custom_mapping.get(10), 'a') self.assertEqual(custom_mapping.get_character(10), 'a')
self.assertEqual(custom_mapping.get(11), 'KP_1') self.assertEqual(custom_mapping.get_character(11), 'KP_1')
self.assertEqual(custom_mapping.get(12), '3') self.assertEqual(custom_mapping.get_character(12), '3')
self.assertEqual(custom_mapping.get(13), 'a, A, NoSymbol, 3') self.assertEqual(custom_mapping.get_character(13), 'a, A, NoSymbol, 3')
self.assertIsNone(custom_mapping.get(14)) self.assertIsNone(custom_mapping.get_character(14))
def test_default_symbols(self): def test_default_symbols(self):
# keycodes are missing # keycodes are missing

View File

@ -151,8 +151,8 @@ class Integration(unittest.TestCase):
time.sleep(0.2) time.sleep(0.2)
self.assertEqual(len(self.get_rows()), 3) self.assertEqual(len(self.get_rows()), 3)
self.assertEqual(custom_mapping.get(10), 'a') self.assertEqual(custom_mapping.get_character(10), 'a')
self.assertEqual(custom_mapping.get(11), 'b') self.assertEqual(custom_mapping.get_character(11), 'b')
self.assertTrue(custom_mapping.changed) self.assertTrue(custom_mapping.changed)
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
@ -174,22 +174,22 @@ class Integration(unittest.TestCase):
row.get_style_context().list_classes() row.get_style_context().list_classes()
) )
self.assertEqual(custom_mapping.get(10), 'c') self.assertEqual(custom_mapping.get_character(10), 'c')
self.assertEqual(custom_mapping.get(11), 'b') self.assertEqual(custom_mapping.get_character(11), 'b')
self.assertTrue(custom_mapping.changed) self.assertTrue(custom_mapping.changed)
def test_rename_and_save(self): def test_rename_and_save(self):
custom_mapping.change(None, 14, 'a') custom_mapping.change(None, 14, 'a')
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(14), 'a') self.assertEqual(custom_mapping.get_character(14), 'a')
custom_mapping.change(None, 14, 'b') custom_mapping.change(None, 14, 'b')
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'{USERS_SYMBOLS}/device_1/asdf')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/asdf'))
self.assertEqual(custom_mapping.get(14), 'b') self.assertEqual(custom_mapping.get_character(14), 'b')
def test_select_device_and_preset(self): def test_select_device_and_preset(self):
class FakeDropdown(Gtk.ComboBoxText): class FakeDropdown(Gtk.ComboBoxText):

View File

@ -33,35 +33,35 @@ class TestMapping(unittest.TestCase):
# 1 is not assigned yet, ignore it # 1 is not assigned yet, ignore it
self.mapping.change(1, 2, 'a') self.mapping.change(1, 2, 'a')
self.assertTrue(self.mapping.changed) self.assertTrue(self.mapping.changed)
self.assertIsNone(self.mapping.get(1)) self.assertIsNone(self.mapping.get_character(1))
self.assertEqual(self.mapping.get(2), 'a') self.assertEqual(self.mapping.get_character(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(2, 3, 'b') self.mapping.change(2, 3, 'b')
self.assertIsNone(self.mapping.get(2)) self.assertIsNone(self.mapping.get_character(2))
self.assertEqual(self.mapping.get(3), 'b') self.assertEqual(self.mapping.get_character(3), 'b')
self.assertEqual(len(self.mapping), 1) self.assertEqual(len(self.mapping), 1)
# add 4 # add 4
self.mapping.change(None, 4, 'c') self.mapping.change(None, 4, 'c')
self.assertEqual(self.mapping.get(3), 'b') self.assertEqual(self.mapping.get_character(3), 'b')
self.assertEqual(self.mapping.get(4), 'c') self.assertEqual(self.mapping.get_character(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(None, 4, 'd') self.mapping.change(None, 4, 'd')
self.assertEqual(self.mapping.get(4), 'd') self.assertEqual(self.mapping.get_character(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, 4, 'e') self.mapping.change(4, 4, 'e')
self.assertEqual(self.mapping.get(4), 'e') self.assertEqual(self.mapping.get_character(4), 'e')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# and this # and this
self.mapping.change('4', '4', 'f') self.mapping.change('4', '4', 'f')
self.assertEqual(self.mapping.get(4), 'f') self.assertEqual(self.mapping.get_character(4), 'f')
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
# non-int keycodes are ignored # non-int keycodes are ignored
@ -70,11 +70,11 @@ class TestMapping(unittest.TestCase):
def test_change_multiple(self): def test_change_multiple(self):
self.mapping.change(None, 1, ['a', 'A']) self.mapping.change(None, 1, ['a', 'A'])
self.assertEqual(self.mapping.get(1), 'a, A') self.assertEqual(self.mapping.get_character(1), 'a, A')
self.mapping.change(1, 2, ['b', 'B']) self.mapping.change(1, 2, ['b', 'B'])
self.assertEqual(self.mapping.get(2), 'b, B') self.assertEqual(self.mapping.get_character(2), 'b, B')
self.assertIsNone(self.mapping.get(1)) self.assertIsNone(self.mapping.get_character(1))
def test_clear(self): def test_clear(self):
# does nothing # does nothing
@ -90,9 +90,9 @@ class TestMapping(unittest.TestCase):
self.mapping.change(None, 20, 'KP_2') self.mapping.change(None, 20, 'KP_2')
self.mapping.change(None, 30, 'KP_3') self.mapping.change(None, 30, 'KP_3')
self.mapping.clear(20) self.mapping.clear(20)
self.assertEqual(self.mapping.get(10), 'KP_1') self.assertEqual(self.mapping.get_character(10), 'KP_1')
self.assertIsNone(self.mapping.get(20)) self.assertIsNone(self.mapping.get_character(20))
self.assertEqual(self.mapping.get(30), 'KP_3') self.assertEqual(self.mapping.get_character(30), 'KP_3')
def test_iterate_and_convert(self): def test_iterate_and_convert(self):
self.mapping.change(None, 10, 1) self.mapping.change(None, 10, 1)
@ -100,7 +100,7 @@ class TestMapping(unittest.TestCase):
self.mapping.change(None, 30, 3) self.mapping.change(None, 30, 3)
self.assertListEqual( self.assertListEqual(
list(self.mapping), list(self.mapping),
[(10, '1'), (20, '2'), (30, '3')] [(10, (266, '1')), (20, (276, '2')), (30, (286, '3'))]
) )