diff --git a/README.md b/README.md index de86305f..48a073c1 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Documentation: ##### Names -For a list of supported keystrokes and their names, check the output of -`xmodmap -pke` +For a list of supported keystrokes and their names for the middle column, +check the output of `xmodmap -pke` - Alphanumeric `a` to `z` and `0` to `9` - Modifiers `Alt_L` `Control_L` `Control_R` `Shift_L` `Shift_R` @@ -42,6 +42,7 @@ If you can't find what you need, consult [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) - Mouse buttons `BTN_LEFT` `BTN_RIGHT` `BTN_MIDDLE` `BTN_SIDE` +- Multimedia keys `KEY_NEXTSONG`, `KEY_PLAYPAUSE`, ... - Macro special keys `KEY_MACRO1` `KEY_MACRO2` ... ##### Gamepads diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index fd147cc7..c0b93e31 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -143,7 +143,6 @@ class KeycodeInjector: needed = False for (ev_type, keycode), _ in self.mapping: - # TODO test ev_type if keycode in capabilities.get(ev_type, []): needed = True break @@ -161,13 +160,13 @@ class KeycodeInjector: attempts = 0 while True: - device = evdev.InputDevice(path) try: # grab to avoid e.g. the disabled keycode of 10 to confuse # X, especially when one of the buttons of your mouse also # uses 10. This also avoids having to load an empty xkb # symbols file to prevent writing any unwanted keys. device.grab() + logger.debug('Grab %s', path) break except IOError: attempts += 1 @@ -224,7 +223,6 @@ class KeycodeInjector: capabilities[ecodes.EV_KEY] = [] # for reasons I don't know, it is also required to have # any keyboard button in capabilities. - # TODO test that this is always present when abs_to_rel capabilities[ecodes.EV_KEY].append(ecodes.KEY_0) # just like what python-evdev does in from_device diff --git a/keymapper/dev/keycode_mapper.py b/keymapper/dev/keycode_mapper.py index 89364f41..2dd8ad91 100644 --- a/keymapper/dev/keycode_mapper.py +++ b/keymapper/dev/keycode_mapper.py @@ -43,7 +43,6 @@ def should_map_event_as_btn(type, code): code : int linux keycode """ - # TODO test if type == evdev.events.EV_KEY: return True diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py index dd7611cf..b74c8f63 100644 --- a/keymapper/dev/reader.py +++ b/keymapper/dev/reader.py @@ -108,8 +108,6 @@ class _KeycodeReader: logger.debug('Pipe closed, reader stops.') sys.exit(0) - # TODO write a test to map event `type 3 (EV_ABS), code 16 - # (ABS_HAT0X), value 0` to a button if should_map_event_as_btn(event.type, event.code): logger.spam( 'got code:%s value:%s type:%s', diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index d6fa8437..6df08ffe 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -38,7 +38,6 @@ CTX_KEYCODE = 2 def to_string(ev_type, code): """A nice to show description of the pressed key.""" - # TODO test try: name = evdev.ecodes.bytype[ev_type][code] if isinstance(name, list): diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 40050545..16f211b2 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -86,7 +86,7 @@ class Mapping: prev_keycode = int(prev_keycode) if prev_type is not None: prev_type = int(prev_type) - except ValueError: + except (TypeError, ValueError): logger.error('Can only use numbers in the tuples') return False @@ -100,9 +100,6 @@ class Mapping: if code_changed and prev_keycode is not None: # clear previous mapping of that code, because the line # representing that one will now represent a different one. - # TODO test that None won't reach the clear function. - # TODO test that overwriting an entry with a different type - # works self.clear(prev_type, prev_keycode) self.changed = True return True diff --git a/tests/test.py b/tests/test.py index 714c1354..14aa3876 100644 --- a/tests/test.py +++ b/tests/test.py @@ -25,6 +25,7 @@ import os import sys import time +import copy import unittest import subprocess import multiprocessing @@ -99,7 +100,16 @@ fixtures = { }, '/dev/input/event30': { - 'capabilities': {evdev.ecodes.EV_SYN: [], evdev.ecodes.EV_ABS: [0, 1]}, + # this device is expected to not have EV_KEY capabilities in tests + # yet. Only when the injector is running EV_KEY stuff is added + 'capabilities': { + evdev.ecodes.EV_SYN: [], + evdev.ecodes.EV_ABS: [ + evdev.ecodes.ABS_X, + evdev.ecodes.ABS_Y, + evdev.ecodes.ABS_HAT0X + ] + }, 'phys': 'usb-0000:03:00.0-3/input1', 'name': 'gamepad' }, @@ -252,7 +262,7 @@ def patch_evdev(): await asyncio.sleep(0.01) def capabilities(self, absinfo=True): - return fixtures[self.path]['capabilities'] + return copy.deepcopy(fixtures[self.path]['capabilities']) class UInput: def __init__(self, *args, **kwargs): @@ -288,6 +298,7 @@ def clear_write_history(): while uinput_write_history_pipe[0].poll(): uinput_write_history_pipe[0].recv() + # quickly fake some stuff before any other file gets a chance to import # the original versions patch_paths() diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 515cf58d..4ab14b03 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -24,7 +24,7 @@ import unittest import time import evdev -from evdev.ecodes import EV_REL, EV_KEY, EV_ABS +from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X from keymapper.dev.injector import is_numlock_on, toggle_numlock,\ ensure_numlock, KeycodeInjector @@ -65,6 +65,7 @@ class TestInjector(unittest.TestCase): for key in keys: del pending_events[key] clear_write_history() + custom_mapping.empty() def test_modify_capabilities(self): class FakeDevice: @@ -108,6 +109,15 @@ class TestInjector(unittest.TestCase): # success on the third try device.name = fixtures[path]['name'] + def test_prepare_device_1(self): + # according to the fixtures, /dev/input/event30 can do ABS_HAT0X + custom_mapping.change((EV_ABS, ABS_HAT0X), 'a') + self.injector = KeycodeInjector('foobar', custom_mapping) + + _prepare_device = self.injector._prepare_device + self.assertIsNone(_prepare_device('/dev/input/event10')[0]) + self.assertIsNotNone(_prepare_device('/dev/input/event30')[0]) + def test_gamepad_capabilities(self): self.injector = KeycodeInjector('gamepad', custom_mapping) @@ -119,6 +129,11 @@ class TestInjector(unittest.TestCase): self.assertNotIn(evdev.ecodes.EV_ABS, capabilities) self.assertIn(evdev.ecodes.EV_REL, capabilities) + # for some reason, having any EV_KEY capability is needed to + # be able to control the mouse + self.assertIn(evdev.ecodes.EV_KEY, capabilities) + self.assertEqual(len(capabilities[evdev.ecodes.EV_KEY]), 1) + def test_skip_unused_device(self): # skips a device because its capabilities are not used in the mapping custom_mapping.change((EV_KEY, 10), 'a') @@ -282,7 +297,7 @@ class TestInjector(unittest.TestCase): def test_injector(self): custom_mapping.change((EV_KEY, 8), 'k(KEY_Q).k(w)') - custom_mapping.change((EV_KEY, 9), 'a') + custom_mapping.change((EV_ABS, ABS_HAT0X), 'a') # one mapping that is unknown in the system_mapping on purpose input_b = 10 custom_mapping.change((EV_KEY, input_b), 'b') @@ -301,9 +316,9 @@ class TestInjector(unittest.TestCase): # should execute a macro Event(EV_KEY, 8, 1), Event(EV_KEY, 8, 0), - # normal keystroke - Event(EV_KEY, 9, 1), - Event(EV_KEY, 9, 0), + # normal keystrokes + Event(EV_ABS, ABS_HAT0X, 1), + Event(EV_ABS, ABS_HAT0X, 0), # just pass those over without modifying Event(EV_KEY, 10, 1), Event(EV_KEY, 10, 0), diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index 196d880d..5716fda7 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -40,6 +40,7 @@ from keymapper.state import custom_mapping, system_mapping, \ from keymapper.paths import CONFIG, get_config_path from keymapper.config import config from keymapper.dev.reader import keycode_reader +from keymapper.gtk.row import to_string from tests.test import tmp, pending_events, Event, uinput_write_history_pipe, \ clear_write_history @@ -176,6 +177,11 @@ class TestIntegration(unittest.TestCase): self.assertIsNotNone(self.window) self.assertTrue(self.window.window.get_visible()) + def test_row_keycode_to_string(self): + # not an integration test, but I have all the row tests here already + self.assertEqual(to_string(EV_KEY, 10), '9') + self.assertEqual(to_string(EV_KEY, 39), 'SEMICOLON') + def test_row_simple(self): rows = self.window.get('key_list').get_children() self.assertEqual(len(rows), 1) @@ -185,9 +191,15 @@ class TestIntegration(unittest.TestCase): row.set_new_keycode(None, None) self.assertIsNone(row.get_keycode()) self.assertEqual(len(custom_mapping), 0) + self.assertEqual(row.keycode_input.get_label(), None) + row.set_new_keycode(EV_KEY, 30) self.assertEqual(len(custom_mapping), 0) self.assertEqual(row.get_keycode(), (EV_KEY, 30)) + # this is KEY_A in linux/input-event-codes.h, + # but KEY_ is removed from the text + self.assertEqual(row.keycode_input.get_label(), 'A') + row.set_new_keycode(EV_KEY, 30) self.assertEqual(len(custom_mapping), 0) self.assertEqual(row.get_keycode(), (EV_KEY, 30)) @@ -208,8 +220,20 @@ class TestIntegration(unittest.TestCase): self.assertEqual(row.get_keycode(), (EV_KEY, 30)) def change_empty_row(self, code, char, code_first=True, success=True): - """Modify the one empty row that always exists.""" - # this is not a test, it's a utility function for other tests. + """Modify the one empty row that always exists. + + Utility function for other tests. + + Parameters + ---------- + code_first : boolean + If True, the code is entered and then the character. + If False, the character is entered first. + success : boolean + If this change on the empty row is going to result in a change + in the mapping eventually. False if this change is going to + cause a duplicate. + """ # wait for the window to create a new empty row if needed time.sleep(0.1) gtk_iteration() diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py new file mode 100644 index 00000000..5769766d --- /dev/null +++ b/tests/testcases/test_keycode_mapper.py @@ -0,0 +1,38 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import unittest + +from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A, ABS_X, EV_REL, REL_X + +from keymapper.dev.keycode_mapper import should_map_event_as_btn + + +class TestKeycodeMapper(unittest.TestCase): + def test_should_map_event_as_btn(self): + self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X)) + self.assertTrue(should_map_event_as_btn(EV_KEY, KEY_A)) + self.assertFalse(should_map_event_as_btn(EV_ABS, ABS_X)) + self.assertFalse(should_map_event_as_btn(EV_REL, REL_X)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index 954db3ad..b9ad39e1 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -20,7 +20,7 @@ import unittest -from evdev.events import EV_KEY +from evdev.events import EV_KEY, EV_ABS from keymapper.mapping import Mapping from keymapper.state import populate_system_mapping @@ -81,15 +81,15 @@ class TestMapping(unittest.TestCase): self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a') self.assertEqual(len(self.mapping), 1) - # change 2 to 3 and change a to b - self.mapping.change((EV_KEY, 3), 'b', (EV_KEY, 2)) + # change KEY 2 to ABS 16 and change a to b + self.mapping.change((EV_ABS, 16), 'b', (EV_KEY, 2)) self.assertIsNone(self.mapping.get_character(EV_KEY, 2)) - self.assertEqual(self.mapping.get_character(EV_KEY, 3), 'b') + self.assertEqual(self.mapping.get_character(EV_ABS, 16), 'b') self.assertEqual(len(self.mapping), 1) # add 4 self.mapping.change((EV_KEY, 4), 'c', (None, None)) - self.assertEqual(self.mapping.get_character(EV_KEY, 3), 'b') + self.assertEqual(self.mapping.get_character(EV_ABS, 16), 'b') self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'c') self.assertEqual(len(self.mapping), 2) @@ -109,10 +109,23 @@ class TestMapping(unittest.TestCase): self.assertEqual(len(self.mapping), 2) # non-int keycodes are ignored - # TODO what about non-int types? self.mapping.change((EV_KEY, 'b'), 'c', (EV_KEY, 'a')) + self.mapping.change((EV_KEY, 'b'), 'c') + self.mapping.change(('foo', 1), 'c', ('foo', 2)) + self.mapping.change(('foo', 1), 'c') self.assertEqual(len(self.mapping), 2) + def test_change_2(self): + self.mapping.change((EV_KEY, 2), 'a') + + self.mapping.change((None, 2), 'b', (EV_KEY, 2)) + self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a') + + self.mapping.change((EV_KEY, None), 'c', (EV_KEY, 2)) + self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a') + + self.assertEqual(len(self.mapping), 1) + def test_clear(self): # does nothing self.mapping.clear(EV_KEY, 40) diff --git a/tests/testcases/test_reader.py b/tests/testcases/test_reader.py index 6a14e942..1e89585c 100644 --- a/tests/testcases/test_reader.py +++ b/tests/testcases/test_reader.py @@ -21,7 +21,7 @@ import unittest -from evdev.events import EV_KEY +from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X import time from keymapper.dev.reader import keycode_reader @@ -34,6 +34,17 @@ CODE_2 = 101 CODE_3 = 102 +def wait(func, timeout=1.0): + """Wait for func to return True.""" + iterations = 0 + sleepytime = 0.1 + while not func(): + time.sleep(sleepytime) + iterations += 1 + if iterations * sleepytime > timeout: + break + + class TestReader(unittest.TestCase): def setUp(self): # verify that tearDown properly cleared the reader @@ -45,10 +56,10 @@ class TestReader(unittest.TestCase): for key in keys: del pending_events[key] - def test_reading(self): + def test_reading_1(self): pending_events['device 1'] = [ Event(EV_KEY, CODE_1, 1), - Event(EV_KEY, CODE_2, 1), + Event(EV_ABS, ABS_HAT0X, 1), Event(EV_KEY, CODE_3, 1) ] keycode_reader.start_reading('device 1') @@ -56,10 +67,18 @@ class TestReader(unittest.TestCase): # sending anything arbitrary does not stop the pipe keycode_reader._pipe[0].send((EV_KEY, 1234)) - time.sleep(EVENT_READ_TIMEOUT * 5) + wait(keycode_reader._pipe[0].poll, 0.5) + self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_3)) self.assertEqual(keycode_reader.read(), (None, None)) + def test_reading_2(self): + pending_events['device 1'] = [Event(EV_ABS, ABS_HAT0X, 1)] + keycode_reader.start_reading('device 1') + wait(keycode_reader._pipe[0].poll, 0.5) + self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X)) + self.assertEqual(keycode_reader.read(), (None, None)) + def test_wrong_device(self): pending_events['device 1'] = [ Event(EV_KEY, CODE_1, 1),