def test_stop_injecting(self): keycode_from = 16 keycode_to = 90 self.change_empty_row(Key(EV_KEY, keycode_from, 1), 't') system_mapping.clear() system_mapping._set('t', keycode_to) # not all of those events should be processed, since that takes some # time due to time.sleep in the fakes and the injection is stopped. pending_events['device 2'] = [InputEvent(1, keycode_from, 1)] * 100 custom_mapping.save(get_preset_path('device 2', 'foo preset')) self.window.selected_device = 'device 2' self.window.selected_preset = 'foo preset' self.window.on_apply_preset_clicked(None) pipe = uinput_write_history_pipe[0] # block until the first event is available, indicating that # the injector is ready write_history = [pipe.recv()] # stop self.window.on_apply_system_layout_clicked(None) # try to receive a few of the events time.sleep(0.2) while pipe.poll(): write_history.append(pipe.recv()) len_before = len(write_history) self.assertLess(len(write_history), 50) # since the injector should not be running anymore, no more events # should be received after waiting even more time time.sleep(0.2) while pipe.poll(): write_history.append(pipe.recv()) self.assertEqual(len(write_history), len_before)
def test_refresh_devices_on_start(self): ev = (EV_KEY, 9) keycode_to = 100 device = '9876 name' # this test only makes sense if this device is unknown yet self.assertIsNone(get_devices().get(device)) custom_mapping.change(Key(*ev, 1), 'a') system_mapping.clear() system_mapping._set('a', keycode_to) preset = 'foo' custom_mapping.save(get_preset_path(device, preset)) config.set_autoload_preset(device, preset) pending_events[device] = [InputEvent(*ev, 1)] self.daemon = Daemon() preset_path = get_preset_path(device, preset) # make sure the devices are populated get_devices() fixtures[self.new_fixture] = { 'capabilities': { evdev.ecodes.EV_KEY: [ev[1]] }, 'phys': '9876 phys', 'info': 'abcd', 'name': device } self.daemon.start_injecting(device, preset_path) # test if the injector called refresh_devices successfully self.assertIsNotNone(get_devices().get(device)) event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, EV_KEY) self.assertEqual(event.code, keycode_to) self.assertEqual(event.value, 1) self.daemon.stop_injecting(device) self.assertFalse(self.daemon.is_injecting(device))
def test_start_injecting(self): keycode_from = 9 keycode_to = 200 self.change_empty_row(Key(EV_KEY, keycode_from, 1), 'a') system_mapping.clear() system_mapping._set('a', keycode_to) pending_events['device 2'] = [ InputEvent(evdev.events.EV_KEY, keycode_from, 1), InputEvent(evdev.events.EV_KEY, keycode_from, 0) ] custom_mapping.save(get_preset_path('device 2', 'foo preset')) # use only the manipulated system_mapping os.remove(os.path.join(tmp, XMODMAP_FILENAME)) self.window.selected_device = 'device 2' self.window.selected_preset = 'foo preset' self.window.on_apply_preset_clicked(None) # the integration tests will cause the injection to be started as # processes, as intended. Luckily, recv will block until the events # are handled and pushed. # Note, that pushing events to pending_events won't work anymore # from here on because the injector processes memory cannot be # modified from here. event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, evdev.events.EV_KEY) self.assertEqual(event.code, keycode_to) self.assertEqual(event.value, 1) event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, evdev.events.EV_KEY) self.assertEqual(event.code, keycode_to) self.assertEqual(event.value, 0)
def test_autoload_3(self): # based on a bug preset = 'preset7' group = groups.find(key='Foo Device 2') mapping = Mapping() mapping.change(Key(3, 2, 1), 'a') mapping.save(group.get_preset_path(preset)) config.set_autoload_preset(group.key, preset) config.save_config() self.daemon = Daemon() self.daemon.set_config_dir(get_config_path()) groups.set_groups([]) # caused the bug self.assertIsNone(groups.find(key='Foo Device 2')) self.daemon.autoload() # it should try to refresh the groups because all the # group_keys are unknown at the moment history = self.daemon.autoload_history._autoload_history self.assertEqual(history[group.key][1], preset) self.assertEqual(self.daemon.get_state(group.key), STARTING) self.assertIsNotNone(groups.find(key='Foo Device 2'))
def test_autoload_2(self): self.daemon = Daemon() history = self.daemon.autoload_history._autoload_history # existing device preset = 'preset7' group = groups.find(key='Foo Device 2') mapping = Mapping() mapping.change(Key(3, 2, 1), 'a') mapping.save(group.get_preset_path(preset)) config.set_autoload_preset(group.key, preset) # ignored, won't cause problems: config.set_autoload_preset('non-existant-key', 'foo') # daemon is missing the config directory yet self.daemon.autoload() self.assertEqual(len(history), 0) config.save_config() self.daemon.set_config_dir(get_config_path()) self.daemon.autoload() self.assertEqual(len(history), 1) self.assertEqual(history[group.key][1], preset)
def test_injector(self): # the tests in test_keycode_mapper.py test this stuff in detail numlock_before = is_numlock_on() combination = Key((EV_KEY, 8, 1), (EV_KEY, 9, 1)) custom_mapping.change(combination, 'k(KEY_Q).k(w)') custom_mapping.change(Key(EV_ABS, ABS_HAT0X, -1), 'a') # one mapping that is unknown in the system_mapping on purpose input_b = 10 custom_mapping.change(Key(EV_KEY, input_b, 1), 'b') # stuff the custom_mapping outputs (except for the unknown b) system_mapping.clear() code_a = 100 code_q = 101 code_w = 102 system_mapping._set('a', code_a) system_mapping._set('key_q', code_q) system_mapping._set('w', code_w) push_events( 'Bar Device', [ # should execute a macro... new_event(EV_KEY, 8, 1), new_event(EV_KEY, 9, 1), # ...now new_event(EV_KEY, 8, 0), new_event(EV_KEY, 9, 0), # gamepad stuff. trigger a combination new_event(EV_ABS, ABS_HAT0X, -1), new_event(EV_ABS, ABS_HAT0X, 0), # just pass those over without modifying new_event(EV_KEY, 10, 1), new_event(EV_KEY, 10, 0), new_event(3124, 3564, 6542), ]) self.injector = Injector(groups.find(name='Bar Device'), custom_mapping) self.assertEqual(self.injector.get_state(), UNKNOWN) self.injector.start() self.assertEqual(self.injector.get_state(), STARTING) uinput_write_history_pipe[0].poll(timeout=1) self.assertEqual(self.injector.get_state(), RUNNING) time.sleep(EVENT_READ_TIMEOUT * 10) # sending anything arbitrary does not stop the process # (is_alive checked later after some time) self.injector._msg_pipe[1].send(1234) # convert the write history to some easier to manage list history = read_write_history_pipe() # 1 event before the combination was triggered (+1 for release) # 4 events for the macro # 2 for mapped keys # 3 for forwarded events self.assertEqual(len(history), 11) # since the macro takes a little bit of time to execute, its # keystrokes are all over the place. # just check if they are there and if so, remove them from the list. self.assertIn((EV_KEY, 8, 1), history) self.assertIn((EV_KEY, code_q, 1), history) self.assertIn((EV_KEY, code_q, 1), history) self.assertIn((EV_KEY, code_q, 0), history) self.assertIn((EV_KEY, code_w, 1), history) self.assertIn((EV_KEY, code_w, 0), history) index_q_1 = history.index((EV_KEY, code_q, 1)) index_q_0 = history.index((EV_KEY, code_q, 0)) index_w_1 = history.index((EV_KEY, code_w, 1)) index_w_0 = history.index((EV_KEY, code_w, 0)) self.assertGreater(index_q_0, index_q_1) self.assertGreater(index_w_1, index_q_0) self.assertGreater(index_w_0, index_w_1) del history[index_q_1] index_q_0 = history.index((EV_KEY, code_q, 0)) del history[index_q_0] index_w_1 = history.index((EV_KEY, code_w, 1)) del history[index_w_1] index_w_0 = history.index((EV_KEY, code_w, 0)) del history[index_w_0] # the rest should be in order. # first the incomplete combination key that wasn't mapped to anything # and just forwarded. The input event that triggered the macro # won't appear here. self.assertEqual(history[0], (EV_KEY, 8, 1)) self.assertEqual(history[1], (EV_KEY, 8, 0)) # value should be 1, even if the input event was -1. # Injected keycodes should always be either 0 or 1 self.assertEqual(history[2], (EV_KEY, code_a, 1)) self.assertEqual(history[3], (EV_KEY, code_a, 0)) self.assertEqual(history[4], (EV_KEY, input_b, 1)) self.assertEqual(history[5], (EV_KEY, input_b, 0)) self.assertEqual(history[6], (3124, 3564, 6542)) time.sleep(0.1) self.assertTrue(self.injector.is_alive()) numlock_after = is_numlock_on() self.assertEqual(numlock_before, numlock_after) self.assertEqual(self.injector.get_state(), RUNNING)
def test_start_stop(self): group = groups.find(key='Foo Device 2') preset = 'preset8' daemon = Daemon() self.daemon = daemon mapping = Mapping() mapping.change(Key(3, 2, 1), 'a') mapping.save(group.get_preset_path(preset)) # the daemon needs set_config_dir first before doing anything daemon.start_injecting(group.key, preset) self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertNotIn(group.key, daemon.injectors) self.assertTrue(daemon.autoload_history.may_autoload( group.key, preset)) # start config.save_config() daemon.set_config_dir(get_config_path()) daemon.start_injecting(group.key, preset) # explicit start, not autoload, so the history stays empty self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload( group.key, preset)) # path got translated to the device name self.assertIn(group.key, daemon.injectors) # start again previous_injector = daemon.injectors[group.key] self.assertNotEqual(previous_injector.get_state(), STOPPED) daemon.start_injecting(group.key, preset) self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload( group.key, preset)) self.assertIn(group.key, daemon.injectors) self.assertEqual(previous_injector.get_state(), STOPPED) # a different injetor is now running self.assertNotEqual(previous_injector, daemon.injectors[group.key]) self.assertNotEqual(daemon.injectors[group.key].get_state(), STOPPED) # trying to inject a non existing preset keeps the previous inejction # alive injector = daemon.injectors[group.key] daemon.start_injecting(group.key, 'qux') self.assertEqual(injector, daemon.injectors[group.key]) self.assertNotEqual(daemon.injectors[group.key].get_state(), STOPPED) # trying to start injecting for an unknown device also just does # nothing daemon.start_injecting('quux', 'qux') self.assertNotEqual(daemon.injectors[group.key].get_state(), STOPPED) # after all that stuff autoload_history is still unharmed self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload( group.key, preset)) # stop daemon.stop_injecting(group.key) self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertEqual(daemon.injectors[group.key].get_state(), STOPPED) self.assertTrue(daemon.autoload_history.may_autoload( group.key, preset))
def test_daemon(self): # remove the existing system mapping to force our own into it if os.path.exists(get_config_path('xmodmap.json')): os.remove(get_config_path('xmodmap.json')) ev_1 = (EV_KEY, 9) ev_2 = (EV_ABS, 12) keycode_to_1 = 100 keycode_to_2 = 101 group = groups.find(name='Bar Device') # unrelated group that shouldn't be affected at all group2 = groups.find(name='gamepad') custom_mapping.change(Key(*ev_1, 1), 'a') custom_mapping.change(Key(*ev_2, -1), 'b') system_mapping.clear() # since this is in the same memory as the daemon, there is no need # to save it to disk system_mapping._set('a', keycode_to_1) system_mapping._set('b', keycode_to_2) preset = 'foo' custom_mapping.save(group.get_preset_path(preset)) config.set_autoload_preset(group.key, preset) """injection 1""" # should forward the event unchanged push_events(group.key, [new_event(EV_KEY, 13, 1)]) self.daemon = Daemon() self.daemon.set_config_dir(get_config_path()) self.assertFalse(uinput_write_history_pipe[0].poll()) self.daemon.start_injecting(group.key, preset) self.assertEqual(self.daemon.get_state(group.key), STARTING) self.assertEqual(self.daemon.get_state(group2.key), UNKNOWN) event = uinput_write_history_pipe[0].recv() self.assertEqual(self.daemon.get_state(group.key), RUNNING) self.assertEqual(event.type, EV_KEY) self.assertEqual(event.code, 13) self.assertEqual(event.value, 1) self.daemon.stop_injecting(group.key) self.assertEqual(self.daemon.get_state(group.key), STOPPED) time.sleep(0.1) try: self.assertFalse(uinput_write_history_pipe[0].poll()) except AssertionError: print('Unexpected', uinput_write_history_pipe[0].recv()) # possibly a duplicate write! raise """injection 2""" # -1234 will be normalized to -1 by the injector push_events(group.key, [new_event(*ev_2, -1234)]) self.daemon.start_injecting(group.key, preset) time.sleep(0.1) self.assertTrue(uinput_write_history_pipe[0].poll()) # the written key is a key-down event, not the original # event value of -1234 event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, EV_KEY) self.assertEqual(event.code, keycode_to_2) self.assertEqual(event.value, 1)
def test_raises(self): self.assertRaises(ValueError, lambda: Key(1)) self.assertRaises(ValueError, lambda: Key(None)) self.assertRaises(ValueError, lambda: Key([1])) self.assertRaises(ValueError, lambda: Key((1, ))) self.assertRaises(ValueError, lambda: Key((1, 2))) self.assertRaises(ValueError, lambda: Key(('1', '2', '3'))) self.assertRaises(ValueError, lambda: Key('1')) self.assertRaises(ValueError, lambda: Key('(1,2,3)')) self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, '3'))) self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, 3), None)) # those don't raise errors Key((1, 2, 3), (1, 2, 3)) Key((1, 2, 3))
def test_prepare_device_non_existing(self): custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a') self.injector = KeycodeInjector('foobar', custom_mapping) _prepare_device = self.injector._prepare_device self.assertIsNone(_prepare_device('/dev/input/event1234')[0])
def test_get_permutations(self): key_1 = Key((1, 3, 1)) self.assertEqual(len(key_1.get_permutations()), 1) self.assertEqual(key_1.get_permutations()[0], key_1) key_2 = Key((1, 3, 1), (1, 5, 1)) self.assertEqual(len(key_2.get_permutations()), 1) self.assertEqual(key_2.get_permutations()[0], key_2) key_3 = Key((1, 3, 1), (1, 5, 1), (1, 7, 1)) self.assertEqual(len(key_3.get_permutations()), 2) self.assertEqual(key_3.get_permutations()[0], Key((1, 3, 1), (1, 5, 1), (1, 7, 1))) self.assertEqual(key_3.get_permutations()[1], ((1, 5, 1), (1, 3, 1), (1, 7, 1)))
def test_combination(self): # it should be possible to write a key combination ev_1 = Key(EV_KEY, evdev.ecodes.KEY_A, 1) ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1) ev_3 = Key(EV_KEY, evdev.ecodes.KEY_C, 1) ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) combination_1 = Key(ev_1, ev_2, ev_3) combination_2 = Key(ev_2, ev_1, ev_3) # same as 1, but different D-Pad direction combination_3 = Key(ev_1, ev_4, ev_3) combination_4 = Key(ev_4, ev_1, ev_3) # same as 1, but the last key is different combination_5 = Key(ev_1, ev_3, ev_2) combination_6 = Key(ev_3, ev_1, ev_2) self.change_empty_row(combination_1, 'a') self.assertEqual(custom_mapping.get_character(combination_1), 'a') self.assertEqual(custom_mapping.get_character(combination_2), 'a') self.assertIsNone(custom_mapping.get_character(combination_3)) self.assertIsNone(custom_mapping.get_character(combination_4)) self.assertIsNone(custom_mapping.get_character(combination_5)) self.assertIsNone(custom_mapping.get_character(combination_6)) # it won't write the same combination again, even if the # first two events are in a different order self.change_empty_row(combination_2, 'b', expect_success=False) self.assertEqual(custom_mapping.get_character(combination_1), 'a') self.assertEqual(custom_mapping.get_character(combination_2), 'a') self.assertIsNone(custom_mapping.get_character(combination_3)) self.assertIsNone(custom_mapping.get_character(combination_4)) self.assertIsNone(custom_mapping.get_character(combination_5)) self.assertIsNone(custom_mapping.get_character(combination_6)) self.change_empty_row(combination_3, 'c') self.assertEqual(custom_mapping.get_character(combination_1), 'a') self.assertEqual(custom_mapping.get_character(combination_2), 'a') self.assertEqual(custom_mapping.get_character(combination_3), 'c') self.assertEqual(custom_mapping.get_character(combination_4), 'c') self.assertIsNone(custom_mapping.get_character(combination_5)) self.assertIsNone(custom_mapping.get_character(combination_6)) # same as with combination_2, the existing combination_3 blocks # combination_4 because they have the same keys and end in the # same key. self.change_empty_row(combination_4, 'd', expect_success=False) self.assertEqual(custom_mapping.get_character(combination_1), 'a') self.assertEqual(custom_mapping.get_character(combination_2), 'a') self.assertEqual(custom_mapping.get_character(combination_3), 'c') self.assertEqual(custom_mapping.get_character(combination_4), 'c') self.assertIsNone(custom_mapping.get_character(combination_5)) self.assertIsNone(custom_mapping.get_character(combination_6)) self.change_empty_row(combination_5, 'e') self.assertEqual(custom_mapping.get_character(combination_1), 'a') self.assertEqual(custom_mapping.get_character(combination_2), 'a') self.assertEqual(custom_mapping.get_character(combination_3), 'c') self.assertEqual(custom_mapping.get_character(combination_4), 'c') self.assertEqual(custom_mapping.get_character(combination_5), 'e') self.assertEqual(custom_mapping.get_character(combination_6), 'e') error_icon = self.window.get('error_status_icon') warning_icon = self.window.get('warning_status_icon') self.assertFalse(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible())
def test_daemon(self): ev_1 = (EV_KEY, 9) ev_2 = (EV_ABS, 12) keycode_to_1 = 100 keycode_to_2 = 101 device = 'device 2' custom_mapping.change(Key(*ev_1, 1), 'a') custom_mapping.change(Key(*ev_2, -1), 'b') system_mapping.clear() system_mapping._set('a', keycode_to_1) system_mapping._set('b', keycode_to_2) preset = 'foo' custom_mapping.save(get_preset_path(device, preset)) config.set_autoload_preset(device, preset) """injection 1""" # should forward the event unchanged pending_events[device] = [InputEvent(EV_KEY, 13, 1)] self.daemon = Daemon() preset_path = get_preset_path(device, preset) self.assertFalse(uinput_write_history_pipe[0].poll()) self.daemon.start_injecting(device, preset_path) self.assertTrue(self.daemon.is_injecting(device)) self.assertFalse(self.daemon.is_injecting('device 1')) event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, EV_KEY) self.assertEqual(event.code, 13) self.assertEqual(event.value, 1) self.daemon.stop_injecting(device) self.assertFalse(self.daemon.is_injecting(device)) time.sleep(0.2) try: self.assertFalse(uinput_write_history_pipe[0].poll()) except AssertionError: print(uinput_write_history_pipe[0].recv()) raise """injection 2""" # -1234 will be normalized to -1 by the injector pending_events[device] = [InputEvent(*ev_2, -1234)] path = get_preset_path(device, preset) self.daemon.start_injecting(device, path) # the written key is a key-down event, not the original # event value of -5678 event = uinput_write_history_pipe[0].recv() self.assertEqual(event.type, EV_KEY) self.assertEqual(event.code, keycode_to_2) self.assertEqual(event.value, 1)
def read(self): """Get the newest key as Key object If the timing of two recent events is very close, prioritize key events over abs events. """ if self._pipe is None: self.fail_counter += 1 if self.fail_counter % 10 == 0: # spam less logger.debug('No pipe available to read from') return None newest_event = self.newest_event newest_time = (0 if newest_event is None else newest_event.sec + newest_event.usec / 1000000) while self._pipe[0].poll(): event = self._pipe[0].recv() without_value = (event.type, event.code) if event.value == 0: if without_value in self._unreleased: del self._unreleased[without_value] continue if without_value in self._unreleased: # no duplicate down events (gamepad triggers) continue time = event.sec + event.usec / 1000000 delta = time - newest_time if delta < FILTER_THRESHOLD: if prioritize([newest_event, event]) != event: # two events happened very close, probably some weird # spam from the device. The wacom intuos 5 adds an # ABS_MISC event to every button press, filter that out logger.spam('Ignoring event (%s, %s, %s)', event.type, event.code, event.value) continue # the previous event is ignored previous_without_value = (newest_event.type, newest_event.code) if previous_without_value in self._unreleased: del self._unreleased[previous_without_value] self._unreleased[without_value] = (event.type, event.code, event.value) newest_event = event newest_time = time if newest_event == self.newest_event: # don't return the same event twice return None self.newest_event = newest_event if len(self._unreleased) > 0: return Key(*self._unreleased.values()) # nothing return None
def test_any_funky_event_as_button(self): # as long as should_map_as_btn says it should be a button, # it will be. EV_TYPE = 4531 CODE_1 = 754 CODE_2 = 4139 w_down = (EV_TYPE, CODE_1, -1) w_up = (EV_TYPE, CODE_1, 0) d_down = (EV_TYPE, CODE_2, 1) d_up = (EV_TYPE, CODE_2, 0) custom_mapping.change(Key(*w_down[:2], -1), 'w') custom_mapping.change(Key(*d_down[:2], 1), 'k(d)') system_mapping.clear() code_w = 71 code_d = 74 system_mapping._set('w', code_w) system_mapping._set('d', code_d) def do_stuff(): if self.injector is not None: # discard the previous injector self.injector.stop_injecting() time.sleep(0.1) while uinput_write_history_pipe[0].poll(): uinput_write_history_pipe[0].recv() push_events('gamepad', [ new_event(*w_down), new_event(*d_down), new_event(*w_up), new_event(*d_up), ]) self.injector = Injector(groups.find(name='gamepad'), custom_mapping) # the injector will otherwise skip the device because # the capabilities don't contain EV_TYPE input = InputDevice('/dev/input/event30') self.injector._grab_device = lambda *args: input self.injector.start() uinput_write_history_pipe[0].poll(timeout=1) time.sleep(EVENT_READ_TIMEOUT * 10) return read_write_history_pipe() """no""" history = do_stuff() self.assertEqual(history.count((EV_KEY, code_w, 1)), 0) self.assertEqual(history.count((EV_KEY, code_d, 1)), 0) self.assertEqual(history.count((EV_KEY, code_w, 0)), 0) self.assertEqual(history.count((EV_KEY, code_d, 0)), 0) """yes""" with mock.patch('keymapper.utils.should_map_as_btn', lambda *_: True): history = do_stuff() self.assertEqual(history.count((EV_KEY, code_w, 1)), 1) self.assertEqual(history.count((EV_KEY, code_d, 1)), 1) self.assertEqual(history.count((EV_KEY, code_w, 0)), 1) self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)
def test_is_problematic(self): key_1 = Key((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) self.assertTrue(key_1.is_problematic()) key_2 = Key((1, KEY_RIGHTALT, 1), (1, 5, 1)) self.assertTrue(key_2.is_problematic()) key_3 = Key((1, 3, 1), (1, KEY_LEFTCTRL, 1)) self.assertTrue(key_3.is_problematic()) key_4 = Key(1, 3, 1) self.assertFalse(key_4.is_problematic()) key_5 = Key((1, 3, 1), (1, 5, 1)) self.assertFalse(key_5.is_problematic())
def test_wheel(self): # this tests both keycode_mapper and event_producer, and it seems # to test stuff not covered in test_keycode_mapper, so it's a quite # important one. # wheel release events are made up with a debouncer # map those two to stuff w_up = (EV_REL, REL_WHEEL, -1) hw_right = (EV_REL, REL_HWHEEL, 1) # should be forwarded and present in the capabilities hw_left = (EV_REL, REL_HWHEEL, -1) custom_mapping.change(Key(*hw_right), 'k(b)') custom_mapping.change(Key(*w_up), 'c') system_mapping.clear() code_b = 91 code_c = 92 system_mapping._set('b', code_b) system_mapping._set('c', code_c) group_key = 'Foo Device 2' push_events(group_key, [ new_event(*w_up), ] * 10 + [ new_event(*hw_right), new_event(*w_up), ] * 5 + [new_event(*hw_left)]) group = groups.find(key=group_key) self.injector = Injector(group, custom_mapping) device = InputDevice('/dev/input/event11') # make sure this test uses a device that has the needed capabilities # for the injector to grab it self.assertIn(EV_REL, device.capabilities()) self.assertIn(REL_WHEEL, device.capabilities()[EV_REL]) self.assertIn(REL_HWHEEL, device.capabilities()[EV_REL]) self.assertIn(device.path, group.paths) self.injector.start() # wait for the first injected key down event uinput_write_history_pipe[0].poll(timeout=1) self.assertTrue(uinput_write_history_pipe[0].poll()) event = uinput_write_history_pipe[0].recv() self.assertEqual(event.t, (EV_KEY, code_c, 1)) time.sleep(EVENT_READ_TIMEOUT * 5) # in 5 more read-loop ticks, nothing new should have happened self.assertFalse(uinput_write_history_pipe[0].poll()) time.sleep(EVENT_READ_TIMEOUT * 6) # 5 more and it should be within the second phase in which # the horizontal wheel is used. add some tolerance self.assertTrue(uinput_write_history_pipe[0].poll()) event = uinput_write_history_pipe[0].recv() self.assertEqual(event.t, (EV_KEY, code_b, 1)) time.sleep(EVENT_READ_TIMEOUT * 10 + 5 / 60) # after 21 read-loop ticks all events should be consumed, wait for # at least 3 (=5) producer-ticks so that the debouncers are triggered. # Key-up events for both wheel events should be written now that no # new key-down event arrived. events = read_write_history_pipe() self.assertEqual(events.count((EV_KEY, code_b, 0)), 1) self.assertEqual(events.count((EV_KEY, code_c, 0)), 1) self.assertEqual(events.count(hw_left), 1) # the unmapped wheel # the unmapped wheel won't get a debounced release command, it's # forwarded as is self.assertNotIn((EV_REL, REL_HWHEEL, 0), events) self.assertEqual(len(events), 3)
def read(self): """Get the newest key/combination as Key object. Only reports keys from down-events. On key-down events the pipe returns changed combinations. Release events won't cause that and the reader will return None as in "nothing new to report". So In order to change a combination, one of its keys has to be released and then a different one pressed. Otherwise making combinations wouldn't be possible. Because at some point the keys have to be released, and that shouldn't cause the combination to get trimmed. """ # this is in some ways similar to the keycode_mapper and # event_producer, but its much simpler because it doesn't # have to trigger anything, manage any macros and only # reports key-down events. This function is called periodically # by the window. # remember the previous down-event from the pipe in order to # be able to tell if the reader should return the updated combination previous_event = self.previous_event key_down_received = False self._debounce_tick() while self._results.poll(): message = self._results.recv() event = self._get_event(message) if event is None: continue gamepad = GAMEPAD in self.group.types if not utils.should_map_as_btn(event, custom_mapping, gamepad): continue event_tuple = (event.type, event.code, event.value) type_code = (event.type, event.code) if event.value == 0: logger.key_spam(event_tuple, 'release') self._release(type_code) continue if self._unreleased.get(type_code) == event_tuple: logger.key_spam(event_tuple, 'duplicate key down') self._debounce_start(event_tuple) continue # to keep track of combinations. # "I have got this release event, what was this for?" A release # event for a D-Pad axis might be any direction, hence this maps # from release to input in order to remember it. Since all release # events have value 0, the value is not used in the key. key_down_received = True logger.key_spam(event_tuple, 'down') self._unreleased[type_code] = event_tuple self._debounce_start(event_tuple) previous_event = event if not key_down_received: # This prevents writing a subset of the combination into # result after keys were released. In order to control the gui, # they have to be released. return None self.previous_event = previous_event if len(self._unreleased) > 0: result = Key(*self._unreleased.values()) if result == self.previous_result: # don't return the same stuff twice return None self.previous_result = result logger.key_spam(result.keys, 'read result') return result return None