mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-04 12:00:16 +00:00
started work on injecting keycodes instead of configs-only
This commit is contained in:
parent
0796cea1bd
commit
d91694c1df
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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'))]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user