started work on injecting keycodes instead of configs-only

xkb
sezanzeb 4 years ago committed by sezanzeb
parent a1503c6e16
commit 7dfb65766d

@ -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

@ -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)

@ -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)

@ -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()

@ -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

@ -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

@ -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):

@ -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'))]
)

Loading…
Cancel
Save