diff --git a/keymapper/X.py b/keymapper/X.py index 442ce8fb..f86e3eec 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -264,7 +264,8 @@ def generate_symbols(name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping) keycodes = re.findall(r'<.+?>', f.read()) xkb_symbols = [] - for keycode, character in mapping: + for keycode, output in mapping: + target_keycode, character = output if f'<{keycode}>' not in keycodes: logger.error(f'Unknown keycode <{keycode}> for "{character}"') # don't append that one, otherwise X would crash when loading diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index c1c2d93d..c3b8ac83 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -86,7 +86,7 @@ class Row(Gtk.ListBoxRow): return # 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' logger.info(msg) self.window.get('status_bar').push(CTX_KEYCODE, msg) diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 64f2c216..76f252c6 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -301,12 +301,12 @@ class Window: parse_symbols_file(self.selected_device, self.selected_preset) key_list = self.get('key_list') - for keycode, character in custom_mapping: + for keycode, output in custom_mapping: single_key_mapping = Row( window=self, delete_callback=self.on_row_removed, keycode=keycode, - character=character + character=output[1] ) key_list.insert(single_key_mapping, -1) diff --git a/keymapper/linux.py b/keymapper/linux.py index 69b4fb28..3e3706fe 100644 --- a/keymapper/linux.py +++ b/keymapper/linux.py @@ -24,6 +24,7 @@ import subprocess import multiprocessing +import threading import evdev @@ -46,9 +47,10 @@ class KeycodeReader: """Keeps reading keycodes in the background for the UI to use. 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 = [] def clear(self): @@ -58,22 +60,35 @@ class KeycodeReader: while virtual_device.read_one(): pass - def start_reading(self, device): - """Tell the evdev lib to start looking for keycodes. - - If read is called without prior start_reading, no keycodes - will be available. - """ - paths = _devices[device]['paths'] + def start_injecting_worker(self, path): + """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) + + def start_injecting(self): + """Read keycodes and inject the mapped character forever.""" + paths = _devices[self.device]['paths'] logger.debug( - 'Starting reading keycodes for %s on %s', - device, + 'Starting injecting the mapping for %s on %s', + self.device, ', '.join(paths) ) # Watch over each one of the potentially multiple devices per hardware - self.virtual_devices = [ + virtual_devices = [ evdev.InputDevice(path) for path in paths ] @@ -94,7 +109,6 @@ class KeycodeReader: return newest_keycode -# not used anymore since the overlooked get_keycode function is now being used # keycode_reader = KeycodeReader() diff --git a/keymapper/mapping.py b/keymapper/mapping.py index f5fc26d1..84c42659 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -52,6 +52,8 @@ class Mapping: previous_keycode : int or None If None, will not remove any previous mapping. new_keycode : int + The source keycode, what the mouse would report without any + modification. character : string or string[] If an array of strings, will put something like { [ a, A ] }; into the symbols file. @@ -73,7 +75,16 @@ class Mapping: character = ', '.join([str(c) for c in 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: # clear previous mapping of that code, because the line # representing that one will now represent a different one. @@ -99,14 +110,18 @@ class Mapping: self._mapping = {} 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. Parameters ---------- 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 diff --git a/tests/testcases/config.py b/tests/testcases/config.py index 677408dd..0e10f0c1 100644 --- a/tests/testcases/config.py +++ b/tests/testcases/config.py @@ -38,8 +38,8 @@ class TestConfig(unittest.TestCase): custom_mapping.change(None, 11, 'KP_1') custom_mapping.change(None, 12, 3) custom_mapping.change(None, 13, ['a', 'A', 'NoSymbol', 3]) - self.assertEqual(custom_mapping.get(12), '3') - self.assertEqual(custom_mapping.get(13), 'a, A, NoSymbol, 3') + self.assertEqual(custom_mapping.get_character(12), '3') + self.assertEqual(custom_mapping.get_character(13), 'a, A, NoSymbol, 3') if os.path.exists(tmp): shutil.rmtree(tmp) @@ -66,12 +66,12 @@ class TestConfig(unittest.TestCase): # it should be loaded correctly after saving parse_symbols_file('device_a', 'preset_b') - self.assertIsNone(custom_mapping.get(9)) - self.assertEqual(custom_mapping.get(10), 'a') - self.assertEqual(custom_mapping.get(11), 'KP_1') - self.assertEqual(custom_mapping.get(12), '3') - self.assertEqual(custom_mapping.get(13), 'a, A, NoSymbol, 3') - self.assertIsNone(custom_mapping.get(14)) + self.assertIsNone(custom_mapping.get_character(9)) + self.assertEqual(custom_mapping.get_character(10), 'a') + self.assertEqual(custom_mapping.get_character(11), 'KP_1') + self.assertEqual(custom_mapping.get_character(12), '3') + self.assertEqual(custom_mapping.get_character(13), 'a, A, NoSymbol, 3') + self.assertIsNone(custom_mapping.get_character(14)) def test_default_symbols(self): # keycodes are missing diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index 31acb563..f840763a 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -151,8 +151,8 @@ class Integration(unittest.TestCase): time.sleep(0.2) self.assertEqual(len(self.get_rows()), 3) - self.assertEqual(custom_mapping.get(10), 'a') - self.assertEqual(custom_mapping.get(11), 'b') + self.assertEqual(custom_mapping.get_character(10), 'a') + self.assertEqual(custom_mapping.get_character(11), 'b') self.assertTrue(custom_mapping.changed) self.window.on_save_preset_clicked(None) @@ -174,22 +174,22 @@ class Integration(unittest.TestCase): row.get_style_context().list_classes() ) - self.assertEqual(custom_mapping.get(10), 'c') - self.assertEqual(custom_mapping.get(11), 'b') + self.assertEqual(custom_mapping.get_character(10), 'c') + self.assertEqual(custom_mapping.get_character(11), 'b') self.assertTrue(custom_mapping.changed) def test_rename_and_save(self): custom_mapping.change(None, 14, 'a') self.assertEqual(self.window.selected_preset, 'new preset') 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') self.window.get('preset_name_input').set_text('asdf') self.window.on_save_preset_clicked(None) self.assertEqual(self.window.selected_preset, '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): class FakeDropdown(Gtk.ComboBoxText): diff --git a/tests/testcases/mapping.py b/tests/testcases/mapping.py index 980693fe..e065e09e 100644 --- a/tests/testcases/mapping.py +++ b/tests/testcases/mapping.py @@ -33,35 +33,35 @@ class TestMapping(unittest.TestCase): # 1 is not assigned yet, ignore it self.mapping.change(1, 2, 'a') self.assertTrue(self.mapping.changed) - self.assertIsNone(self.mapping.get(1)) - self.assertEqual(self.mapping.get(2), 'a') + self.assertIsNone(self.mapping.get_character(1)) + self.assertEqual(self.mapping.get_character(2), 'a') self.assertEqual(len(self.mapping), 1) # change 2 to 3 and change a to b self.mapping.change(2, 3, 'b') - self.assertIsNone(self.mapping.get(2)) - self.assertEqual(self.mapping.get(3), 'b') + self.assertIsNone(self.mapping.get_character(2)) + self.assertEqual(self.mapping.get_character(3), 'b') self.assertEqual(len(self.mapping), 1) # add 4 self.mapping.change(None, 4, 'c') - self.assertEqual(self.mapping.get(3), 'b') - self.assertEqual(self.mapping.get(4), 'c') + self.assertEqual(self.mapping.get_character(3), 'b') + self.assertEqual(self.mapping.get_character(4), 'c') self.assertEqual(len(self.mapping), 2) # change the mapping of 4 to 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) # this also works in the same way 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) # and this 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) # non-int keycodes are ignored @@ -70,11 +70,11 @@ class TestMapping(unittest.TestCase): def test_change_multiple(self): 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.assertEqual(self.mapping.get(2), 'b, B') - self.assertIsNone(self.mapping.get(1)) + self.assertEqual(self.mapping.get_character(2), 'b, B') + self.assertIsNone(self.mapping.get_character(1)) def test_clear(self): # does nothing @@ -90,9 +90,9 @@ class TestMapping(unittest.TestCase): self.mapping.change(None, 20, 'KP_2') self.mapping.change(None, 30, 'KP_3') self.mapping.clear(20) - self.assertEqual(self.mapping.get(10), 'KP_1') - self.assertIsNone(self.mapping.get(20)) - self.assertEqual(self.mapping.get(30), 'KP_3') + self.assertEqual(self.mapping.get_character(10), 'KP_1') + self.assertIsNone(self.mapping.get_character(20)) + self.assertEqual(self.mapping.get_character(30), 'KP_3') def test_iterate_and_convert(self): self.mapping.change(None, 10, 1) @@ -100,7 +100,7 @@ class TestMapping(unittest.TestCase): self.mapping.change(None, 30, 3) self.assertListEqual( list(self.mapping), - [(10, '1'), (20, '2'), (30, '3')] + [(10, (266, '1')), (20, (276, '2')), (30, (286, '3'))] )