overwriting global configs within the preset.json files

xkb
sezanzeb 4 years ago committed by sezanzeb
parent 4f20c4a37b
commit 57162ab251

@ -25,7 +25,6 @@
import os import os
import json import json
import shutil import shutil
import copy
from keymapper.paths import CONFIG, USER, touch from keymapper.paths import CONFIG, USER, touch
from keymapper.logger import logger from keymapper.logger import logger
@ -34,7 +33,13 @@ from keymapper.logger import logger
MOUSE = 'mouse' MOUSE = 'mouse'
WHEEL = 'wheel' WHEEL = 'wheel'
CONFIG_PATH = os.path.join(CONFIG, 'config') CONFIG_PATH = os.path.join(CONFIG, 'config.json')
# to support early versions in which the .json ending was missing:
deprecated_path = os.path.join(CONFIG, 'config')
if os.path.exists(deprecated_path) and not os.path.exists(CONFIG_PATH):
logger.info('Moving "%s" to "%s"', deprecated_path, CONFIG_PATH)
os.rename(os.path.join(CONFIG, 'config'), CONFIG_PATH)
INITIAL_CONFIG = { INITIAL_CONFIG = {
'autoload': {}, 'autoload': {},
@ -58,13 +63,32 @@ INITIAL_CONFIG = {
} }
class _Config: class ConfigBase:
def __init__(self): """Base class for config objects.
Loading and saving is optional and handled by classes that derive from
this base.
"""
def __init__(self, fallback=None):
"""Set up the needed members to turn your object into a config.
Parameters
----------
fallback : ConfigBase
a configuration that contains fallback default configs, if your
object doesn't configure a certain key.
"""
self._config = {} self._config = {}
self.load_config() self.fallback = fallback
def _resolve(self, path, func, config=None): def _resolve(self, path, func, config=None):
"""Call func for the given config value.""" """Call func for the given config value.
Parameters
----------
config : dict
The dictionary to search. Defaults to self._config.
"""
chunks = path.split('.') chunks = path.split('.')
if config is None: if config is None:
@ -108,7 +132,10 @@ class _Config:
For example 'macros.keystroke_sleep_ms' For example 'macros.keystroke_sleep_ms'
value : any value : any
""" """
logger.debug('Changing "%s" to "%s"', path, value) logger.debug(
'Changing "%s" to "%s" in %s',
path, value, self.__class__.__name__
)
def callback(parent, child, chunk): def callback(parent, child, chunk):
parent[chunk] = value parent[chunk] = value
@ -129,6 +156,8 @@ class _Config:
return child return child
resolved = self._resolve(path, callback) resolved = self._resolve(path, callback)
if resolved is None and self.fallback is not None:
resolved = self.fallback._resolve(path, callback)
if resolved is None: if resolved is None:
resolved = self._resolve(path, callback, INITIAL_CONFIG) resolved = self._resolve(path, callback, INITIAL_CONFIG)
@ -137,6 +166,23 @@ class _Config:
return resolved return resolved
def clear_config(self):
"""Remove all configurations in memory."""
self._config = {}
class GlobalConfig(ConfigBase):
"""Global default configuration.
It can also contain some extra stuff not relevant for presets, like the
autoload stuff. If presets have a config key set, it will ignore
the default global configuration for that one. If none of the configs
have the key set, a hardcoded default value will be used.
"""
def __init__(self):
super().__init__()
self.load_config()
def set_autoload_preset(self, device, preset): def set_autoload_preset(self, device, preset):
"""Set a preset to be automatically applied on start. """Set a preset to be automatically applied on start.
@ -167,6 +213,7 @@ class _Config:
# treated like an empty config # treated like an empty config
logger.debug('Config "%s" doesn\'t exist yet', CONFIG_PATH) logger.debug('Config "%s" doesn\'t exist yet', CONFIG_PATH)
self.clear_config() self.clear_config()
self._config = INITIAL_CONFIG
self.save_config() self.save_config()
return return
@ -174,10 +221,6 @@ class _Config:
self._config.update(json.load(file)) self._config.update(json.load(file))
logger.info('Loaded config from "%s"', CONFIG_PATH) logger.info('Loaded config from "%s"', CONFIG_PATH)
def clear_config(self):
"""Reset the configuration to the initial values."""
self._config = copy.deepcopy(INITIAL_CONFIG)
def save_config(self): def save_config(self):
"""Save the config to the file system.""" """Save the config to the file system."""
touch(CONFIG_PATH) touch(CONFIG_PATH)
@ -189,4 +232,4 @@ class _Config:
file.write('\n') file.write('\n')
config = _Config() config = GlobalConfig()

@ -29,7 +29,7 @@ import evdev
from evdev.ecodes import EV_ABS, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL from evdev.ecodes import EV_ABS, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.config import config, MOUSE, WHEEL from keymapper.config import MOUSE, WHEEL
# other events for ABS include buttons # other events for ABS include buttons
@ -100,7 +100,7 @@ def get_values(abs_state, left_purpose, right_purpose):
return mouse_x, mouse_y, wheel_x, wheel_y return mouse_x, mouse_y, wheel_x, wheel_y
async def ev_abs_mapper(abs_state, input_device, keymapper_device): async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
"""Keep writing mouse movements based on the gamepad stick position. """Keep writing mouse movements based on the gamepad stick position.
Parameters Parameters
@ -112,6 +112,8 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device):
the outside. the outside.
input_device : evdev.InputDevice input_device : evdev.InputDevice
keymapper_device : evdev.UInput keymapper_device : evdev.UInput
mapping : Mapping
the mapping object that configures the current injection
""" """
max_value = input_device.absinfo(EV_ABS).max max_value = input_device.absinfo(EV_ABS).max
@ -127,11 +129,11 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device):
pending_rx_rel = 0 pending_rx_rel = 0
pending_ry_rel = 0 pending_ry_rel = 0
# TODO move this stuff into the preset configuration # TODO overwrite mapping stuff in tests
pointer_speed = config.get('gamepad.joystick.pointer_speed') pointer_speed = mapping.get('gamepad.joystick.pointer_speed')
non_linearity = config.get('gamepad.joystick.non_linearity') non_linearity = mapping.get('gamepad.joystick.non_linearity')
left_purpose = config.get('gamepad.joystick.left_purpose') left_purpose = mapping.get('gamepad.joystick.left_purpose')
right_purpose = config.get('gamepad.joystick.right_purpose') right_purpose = mapping.get('gamepad.joystick.right_purpose')
logger.info( logger.info(
'Left joystick as %s, right joystick as %s', 'Left joystick as %s, right joystick as %s',

@ -305,7 +305,7 @@ class KeycodeInjector:
macros = {} macros = {}
for key, output in self.mapping: for key, output in self.mapping:
if is_this_a_macro(output): if is_this_a_macro(output):
macro = parse(output) macro = parse(output, self.mapping)
if macro is None: if macro is None:
continue continue
@ -341,7 +341,12 @@ class KeycodeInjector:
if abs_to_rel: if abs_to_rel:
self.abs_state[0] = 0 self.abs_state[0] = 0
self.abs_state[1] = 0 self.abs_state[1] = 0
coroutine = ev_abs_mapper(self.abs_state, source, uinput) coroutine = ev_abs_mapper(
self.abs_state,
source,
uinput,
self.mapping
)
coroutines.append(coroutine) coroutines.append(coroutine)
if len(coroutines) == 0: if len(coroutines) == 0:

@ -65,17 +65,20 @@ class _Macro:
Calling functions on _Macro does not inject anything yet, it means that Calling functions on _Macro does not inject anything yet, it means that
once .run is used it will be executed along with all other queued tasks. once .run is used it will be executed along with all other queued tasks.
""" """
def __init__(self, code): def __init__(self, code, mapping):
"""Create a macro instance that can be populated with tasks. """Create a macro instance that can be populated with tasks.
Parameters Parameters
---------- ----------
code : string code : string
The original parsed code, for logging purposes. The original parsed code, for logging purposes.
mapping : Mapping
The preset object, needed for some config stuff
""" """
self.tasks = [] self.tasks = []
self.handler = lambda *args: logger.error('No handler set') self.handler = lambda *args: logger.error('No handler set')
self.code = code self.code = code
self.mapping = mapping
# supposed to be True between key event values 1 (down) and 0 (up) # supposed to be True between key event values 1 (down) and 0 (up)
self.holding = False self.holding = False
@ -212,7 +215,7 @@ class _Macro:
def add_keycode_pause(self): def add_keycode_pause(self):
"""To add a pause between keystrokes.""" """To add a pause between keystrokes."""
sleeptime = config.get('macros.keystroke_sleep_ms') / 1000 sleeptime = self.mapping.get('macros.keystroke_sleep_ms') / 1000
async def sleep(): async def sleep():
await asyncio.sleep(sleeptime) await asyncio.sleep(sleeptime)
@ -311,13 +314,15 @@ def _count_brackets(macro):
return position return position
def _parse_recurse(macro, macro_instance=None, depth=0): def _parse_recurse(macro, mapping, macro_instance=None, depth=0):
"""Handle a subset of the macro, e.g. one parameter or function call. """Handle a subset of the macro, e.g. one parameter or function call.
Parameters Parameters
---------- ----------
macro : string macro : string
Just like parse Just like parse
mapping : Mapping
The preset configuration
macro_instance : _Macro or None macro_instance : _Macro or None
A macro instance to add tasks to A macro instance to add tasks to
depth : int depth : int
@ -332,7 +337,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
assert isinstance(depth, int) assert isinstance(depth, int)
if macro_instance is None: if macro_instance is None:
macro_instance = _Macro(macro) macro_instance = _Macro(macro, mapping)
else: else:
assert isinstance(macro_instance, _Macro) assert isinstance(macro_instance, _Macro)
@ -367,7 +372,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
logger.spam('%scalls %s with %s', space, call, string_params) logger.spam('%scalls %s with %s', space, call, string_params)
# evaluate the params # evaluate the params
params = [ params = [
_parse_recurse(param.strip(), None, depth + 1) _parse_recurse(param.strip(), mapping, None, depth + 1)
for param in string_params for param in string_params
] ]
@ -393,7 +398,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
if len(macro) > position and macro[position] == '.': if len(macro) > position and macro[position] == '.':
chain = macro[position + 1:] chain = macro[position + 1:]
logger.spam('%sfollowed by %s', space, chain) logger.spam('%sfollowed by %s', space, chain)
_parse_recurse(chain, macro_instance, depth) _parse_recurse(chain, mapping, macro_instance, depth)
return macro_instance return macro_instance
@ -409,7 +414,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
return macro return macro
def parse(macro, return_errors=False): def parse(macro, mapping, return_errors=False):
"""parse and generate a _Macro that can be run as often as you want. """parse and generate a _Macro that can be run as often as you want.
You need to use set_handler on it before running. If it could not You need to use set_handler on it before running. If it could not
@ -422,6 +427,8 @@ def parse(macro, return_errors=False):
"r(3, k(a).w(10))" "r(3, k(a).w(10))"
"r(2, k(a).k(-)).k(b)" "r(2, k(a).k(-)).k(b)"
"w(1000).m(Shift_L, r(2, k(a))).w(10, 20).k(b)" "w(1000).m(Shift_L, r(2, k(a))).w(10, 20).k(b)"
mapping : Mapping
The preset object, needed for some config stuff
return_errors : bool return_errors : bool
if True, returns errors as a string or None if parsing worked if True, returns errors as a string or None if parsing worked
""" """
@ -439,7 +446,7 @@ def parse(macro, return_errors=False):
logger.spam('preparing macro %s for later execution', macro) logger.spam('preparing macro %s for later execution', macro)
try: try:
macro_object = _parse_recurse(macro) macro_object = _parse_recurse(macro, mapping)
return macro_object if not return_errors else None return macro_object if not return_errors else None
except Exception as error: except Exception as error:
logger.error('Failed to parse macro "%s": %s', macro, error) logger.error('Failed to parse macro "%s": %s', macro, error)

@ -298,7 +298,7 @@ class Window:
if not is_this_a_macro(output): if not is_this_a_macro(output):
continue continue
error = parse(output, return_errors=True) error = parse(output, custom_mapping, return_errors=True)
if error is None: if error is None:
continue continue

@ -28,6 +28,7 @@ import copy
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.paths import get_config_path, touch from keymapper.paths import get_config_path, touch
from keymapper.config import ConfigBase, config
def verify_key(key): def verify_key(key):
@ -39,18 +40,12 @@ def verify_key(key):
raise ValueError(f'Can only use numbers in the tuples, but got {key}') raise ValueError(f'Can only use numbers in the tuples, but got {key}')
class Mapping: class Mapping(ConfigBase):
"""Contains and manages mappings. """Contains and manages mappings and config of a single preset."""
The keycode is always unique, multiple keycodes may map to the same
character.
"""
def __init__(self): def __init__(self):
self._mapping = {} self._mapping = {}
self.changed = False self.changed = False
super().__init__(fallback=config)
self.config = {}
def __iter__(self): def __iter__(self):
"""Iterate over tuples of unique keycodes and their character.""" """Iterate over tuples of unique keycodes and their character."""
@ -81,8 +76,8 @@ class Mapping:
the previous key, same format as new_key the previous key, same format as new_key
If not set, will not remove any previous mapping. If you recently If not set, will not remove any previous mapping. If you recently
used 10 for new_keycode and want to overwrite that with 11, used (1, 10, 1) for new_key and want to overwrite that with
provide 5 here. (1, 11, 1), provide (1, 5, 1) here.
""" """
if character is None: if character is None:
raise ValueError('Expected `character` not to be None') raise ValueError('Expected `character` not to be None')
@ -143,6 +138,8 @@ class Mapping:
logger.error('Tried to load non-existing preset "%s"', path) logger.error('Tried to load non-existing preset "%s"', path)
return return
self.clear_config()
with open(path, 'r') as file: with open(path, 'r') as file:
preset_dict = json.load(file) preset_dict = json.load(file)
if not isinstance(preset_dict.get('mapping'), dict): if not isinstance(preset_dict.get('mapping'), dict):
@ -176,7 +173,7 @@ class Mapping:
for key in preset_dict: for key in preset_dict:
if key == 'mapping': if key == 'mapping':
continue continue
self.config[key] = preset_dict[key] self._config[key] = preset_dict[key]
self.changed = False self.changed = False
@ -195,6 +192,12 @@ class Mapping:
touch(path) touch(path)
with open(path, 'w') as file: with open(path, 'w') as file:
if self._config.get('mapping') is not None:
logger.error(
'"mapping" is reserved and cannot be used as config key'
)
preset_dict = self._config
# make sure to keep the option to add metadata if ever needed, # make sure to keep the option to add metadata if ever needed,
# so put the mapping into a special key # so put the mapping into a special key
json_ready_mapping = {} json_ready_mapping = {}
@ -203,8 +206,7 @@ class Mapping:
new_key = ','.join([str(value) for value in key]) new_key = ','.join([str(value) for value in key])
json_ready_mapping[new_key] = value json_ready_mapping[new_key] = value
preset_dict = {'mapping': json_ready_mapping} preset_dict['mapping'] = json_ready_mapping
preset_dict.update(self.config)
json.dump(preset_dict, file, indent=4) json.dump(preset_dict, file, indent=4)
file.write('\n') file.write('\n')

@ -87,8 +87,7 @@ class SystemMapping:
del self._mapping[key] del self._mapping[key]
# one mapping object for the whole application that holds all # one mapping object for the GUI application
# customizations, as shown in the UI
custom_mapping = Mapping() custom_mapping = Mapping()
# this mapping represents the xmodmap output, which stays constant # this mapping represents the xmodmap output, which stays constant

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text> <text x="22.0" y="14">pylint</text>
</g> </g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.76</text> <text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.74</text>
<text x="62.0" y="14">9.76</text> <text x="62.0" y="14">9.74</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -53,11 +53,58 @@ Examples:
Joystick movements will be translated to mouse movements, while the second Joystick movements will be translated to mouse movements, while the second
joystick acts as a mouse wheel. All buttons, triggers and D-Pads can be joystick acts as a mouse wheel. All buttons, triggers and D-Pads can be
mapped to keycodes and macros. Configuring the purpose of your joysticks mapped to keycodes and macros. The purpose of your joysticks can be
is currently done in the global configuration at `~/.config/key-mapper/config`. configured in the json files with the `gamepad.joystick.left_purpose` and
`right_purpose` keys. See below for more info.
The D-Pad can be mapped to W, A, S, D for example, to run around in games, The D-Pad can be mapped to W, A, S, D for example, to run around in games,
while the joystick turns the view. while the joystick turns the view.
Tested with the XBOX 360 Gamepad. On Ubuntu, gamepads worked better in Tested with the XBOX 360 Gamepad. On Ubuntu, gamepads worked better in
Wayland than with X11 for me. Wayland than with X11 for me.
## Configuration Files
The default configuration is stored at `~/.config/key-mapper/config.json`.
The current default configuration as of commit `42cb7fe` looks like:
```json
{
"autoload": {},
"macros": {
"keystroke_sleep_ms": 10
},
"gamepad": {
"joystick": {
"non_linearity": 4,
"pointer_speed": 80,
"left_purpose": "mouse",
"right_purpose": "wheel"
}
}
}
```
Anything that is relevant to presets can be overwritten in them as well.
Here is an example configuration for preset "a" for the "gamepad" device:
`~/.config/key-mapper/gamepad/a.json`
```json
{
"macros": {
"keystroke_sleep_ms": 100
},
"mapping": {
"1,315,1": "1",
"1,307,1": "k(2).k(3)"
}
}
```
Both need to be valid json files, otherwise the parser refuses to work. This
preset maps the EV_KEY down event with code 315 to '1', code 307 to a macro
and sets the time between injected events of macros to 100 ms. Note that
a complete keystroke consists of two events: down and up. Other than that,
it inherits all configurations from `~/.config/key-mapper/config.json`.
If config.json is missing some stuff, it will query the hardcoded default
values.

@ -332,10 +332,15 @@ patch_select()
from keymapper.logger import update_verbosity from keymapper.logger import update_verbosity
from keymapper.dev.injector import KeycodeInjector from keymapper.dev.injector import KeycodeInjector
from keymapper.config import config
# no need for a high number in tests # no need for a high number in tests
KeycodeInjector.regrab_timeout = 0.15 KeycodeInjector.regrab_timeout = 0.15
# create an empty config beforehand
config.clear_config()
config.save_config()
def main(): def main():
update_verbosity(True) update_verbosity(True)

@ -19,9 +19,10 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>. # along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
import os
import unittest import unittest
from keymapper.config import config from keymapper.config import config, CONFIG_PATH
class TestConfig(unittest.TestCase): class TestConfig(unittest.TestCase):
@ -53,7 +54,6 @@ class TestConfig(unittest.TestCase):
self.assertEqual(config._config['a']['b']['c'], 3) self.assertEqual(config._config['a']['b']['c'], 3)
def test_autoload(self): def test_autoload(self):
del config._config['autoload']
self.assertEqual(len(config.iterate_autoload_presets()), 0) self.assertEqual(len(config.iterate_autoload_presets()), 0)
self.assertFalse(config.is_autoloaded('d1', 'a')) self.assertFalse(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2', 'b')) self.assertFalse(config.is_autoloaded('d2', 'b'))
@ -87,6 +87,18 @@ class TestConfig(unittest.TestCase):
[('d1', 'a')] [('d1', 'a')]
) )
def test_initial(self):
# when loading for the first time, create a config file with
# the default values
os.remove(CONFIG_PATH)
self.assertFalse(os.path.exists(CONFIG_PATH))
config.load_config()
self.assertTrue(os.path.exists(CONFIG_PATH))
with open(CONFIG_PATH, 'r') as file:
contents = file.read()
self.assertIn('"keystroke_sleep_ms": 10', contents)
def test_save_load(self): def test_save_load(self):
self.assertEqual(len(config.iterate_autoload_presets()), 0) self.assertEqual(len(config.iterate_autoload_presets()), 0)

@ -26,6 +26,7 @@ from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.dev.ev_abs_mapper import ev_abs_mapper from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.config import config from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \ from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
@ -35,24 +36,26 @@ from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
abs_state = [0, 0, 0, 0] abs_state = [0, 0, 0, 0]
SPEED = 20
class TestEvAbsMapper(unittest.TestCase): class TestEvAbsMapper(unittest.TestCase):
# there is also `test_abs_to_rel` in test_injector.py # there is also `test_abs_to_rel` in test_injector.py
def setUp(self): def setUp(self):
config.set('gamepad.joystick.non_linearity', 1)
config.set('gamepad.joystick.pointer_speed', SPEED)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
self.mapping = Mapping()
device = InputDevice('/dev/input/event30') device = InputDevice('/dev/input/event30')
uinput = UInput() uinput = UInput()
asyncio.ensure_future(ev_abs_mapper(abs_state, device, uinput)) asyncio.ensure_future(ev_abs_mapper(
abs_state,
device,
uinput,
self.mapping
))
def tearDown(self): def tearDown(self):
config.clear_config() config.clear_config()
self.mapping.clear_config()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
for task in asyncio.Task.all_tasks(): for task in asyncio.Task.all_tasks():
@ -79,13 +82,16 @@ class TestEvAbsMapper(unittest.TestCase):
self.assertEqual(history.count(expectation), len(history)) self.assertEqual(history.count(expectation), len(history))
def test_joystick_purpose_1(self): def test_joystick_purpose_1(self):
config.set('gamepad.joystick.left_purpose', MOUSE) speed = 20
config.set('gamepad.joystick.right_purpose', WHEEL) self.mapping.set('gamepad.joystick.non_linearity', 1)
self.mapping.set('gamepad.joystick.pointer_speed', speed)
self.mapping.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, SPEED)) self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -SPEED)) self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, SPEED)) self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -SPEED)) self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -speed))
# wheel event values are negative # wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1)) self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1))
@ -94,6 +100,9 @@ class TestEvAbsMapper(unittest.TestCase):
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 1)) self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 1))
def test_joystick_purpose_2(self): def test_joystick_purpose_2(self):
speed = 30
config.set('gamepad.joystick.non_linearity', 1)
config.set('gamepad.joystick.pointer_speed', speed)
config.set('gamepad.joystick.left_purpose', WHEEL) config.set('gamepad.joystick.left_purpose', WHEEL)
config.set('gamepad.joystick.right_purpose', MOUSE) config.set('gamepad.joystick.right_purpose', MOUSE)
@ -103,25 +112,28 @@ class TestEvAbsMapper(unittest.TestCase):
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 1)) self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 1))
# wheel event values are negative # wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, SPEED)) self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -SPEED)) self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, SPEED)) self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -SPEED)) self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -speed))
def test_joystick_purpose_3(self): def test_joystick_purpose_3(self):
config.set('gamepad.joystick.left_purpose', MOUSE) speed = 40
self.mapping.set('gamepad.joystick.non_linearity', 1)
config.set('gamepad.joystick.pointer_speed', speed)
self.mapping.set('gamepad.joystick.left_purpose', MOUSE)
config.set('gamepad.joystick.right_purpose', MOUSE) config.set('gamepad.joystick.right_purpose', MOUSE)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, SPEED)) self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -SPEED)) self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, SPEED)) self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -SPEED)) self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -speed))
# wheel event values are negative # wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, SPEED)) self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -SPEED)) self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, SPEED)) self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -SPEED)) self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -speed))
def test_joystick_purpose_4(self): def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL) config.set('gamepad.joystick.left_purpose', WHEEL)

@ -85,7 +85,7 @@ class TestInjector(unittest.TestCase):
mapping.change((EV_KEY, 80, 1), 'a') mapping.change((EV_KEY, 80, 1), 'a')
macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))' macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code) macro = parse(macro_code, mapping)
mapping.change((EV_KEY, 60, 111), macro_code) mapping.change((EV_KEY, 60, 111), macro_code)

@ -31,6 +31,7 @@ from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
from keymapper.state import system_mapping from keymapper.state import system_mapping
from keymapper.dev.macros import parse from keymapper.dev.macros import parse
from keymapper.config import config from keymapper.config import config
from keymapper.mapping import Mapping
from tests.test import InputEvent, UInput, uinput_write_history, \ from tests.test import InputEvent, UInput, uinput_write_history, \
clear_write_history clear_write_history
@ -71,6 +72,9 @@ def calculate_event_number(holdtime, before, after):
class TestKeycodeMapper(unittest.TestCase): class TestKeycodeMapper(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
def tearDown(self): def tearDown(self):
system_mapping.populate() system_mapping.populate()
@ -166,8 +170,8 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('b', code_b) system_mapping._set('b', code_b)
macro_mapping = { macro_mapping = {
(EV_KEY, 1, 1): parse('k(a)'), (EV_KEY, 1, 1): parse('k(a)', self.mapping),
(EV_KEY, 2, 1): parse('r(5, k(b))') (EV_KEY, 2, 1): parse('r(5, k(b))', self.mapping)
} }
macro_mapping[(EV_KEY, 1, 1)].set_handler(lambda *args: history.append(args)) macro_mapping[(EV_KEY, 1, 1)].set_handler(lambda *args: history.append(args))
@ -202,7 +206,7 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('c', code_c) system_mapping._set('c', code_c)
macro_mapping = { macro_mapping = {
(EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)') (EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)', self.mapping)
} }
def handler(*args): def handler(*args):
@ -266,9 +270,9 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('d', code_d) system_mapping._set('d', code_d)
macro_mapping = { macro_mapping = {
(EV_KEY, 1, 1): parse('h(k(b))'), (EV_KEY, 1, 1): parse('h(k(b))', self.mapping),
(EV_KEY, 2, 1): parse('k(c).r(1, r(1, r(1, h(k(a))))).k(d)'), (EV_KEY, 2, 1): parse('k(c).r(1, r(1, r(1, h(k(a))))).k(d)', self.mapping),
(EV_KEY, 3, 1): parse('h(k(b))') (EV_KEY, 3, 1): parse('h(k(b))', self.mapping)
} }
history = [] history = []
@ -389,7 +393,7 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('c', code_c) system_mapping._set('c', code_c)
macro_mapping = { macro_mapping = {
(EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)'), (EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)', self.mapping),
} }
history = [] history = []
@ -457,8 +461,8 @@ class TestKeycodeMapper(unittest.TestCase):
up_2 = (*key_2, 0) up_2 = (*key_2, 0)
macro_mapping = { macro_mapping = {
down_1: parse('k(1).h(k(2)).k(3)'), down_1: parse('k(1).h(k(2)).k(3)', self.mapping),
down_2: parse('k(a).h(k(b)).k(c)') down_2: parse('k(a).h(k(b)).k(c)', self.mapping)
} }
def handler(*args): def handler(*args):
@ -550,8 +554,8 @@ class TestKeycodeMapper(unittest.TestCase):
repeats = 10 repeats = 10
macro_mapping = { macro_mapping = {
down_1: parse(f'r({repeats}, k(1))'), down_1: parse(f'r({repeats}, k(1))', self.mapping),
down_2: parse(f'r({repeats}, k(2))') down_2: parse(f'r({repeats}, k(2))', self.mapping)
} }
history = [] history = []

@ -26,6 +26,7 @@ import asyncio
from keymapper.dev.macros import parse, _Macro, _extract_params, \ from keymapper.dev.macros import parse, _Macro, _extract_params, \
is_this_a_macro is_this_a_macro
from keymapper.config import config from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.state import system_mapping from keymapper.state import system_mapping
@ -33,9 +34,11 @@ class TestMacros(unittest.TestCase):
def setUp(self): def setUp(self):
self.result = [] self.result = []
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.mapping = Mapping()
def tearDown(self): def tearDown(self):
self.result = [] self.result = []
self.mapping.clear_config()
def handler(self, code, value): def handler(self, code, value):
"""Where macros should write codes to.""" """Where macros should write codes to."""
@ -76,7 +79,7 @@ class TestMacros(unittest.TestCase):
expect(',,', ['', '', '']) expect(',,', ['', '', ''])
def test_set_handler(self): def test_set_handler(self):
macro = parse('r(1, r(1, k(1)))') macro = parse('r(1, r(1, k(1)))', self.mapping)
one_code = system_mapping.get('1') one_code = system_mapping.get('1')
self.assertSetEqual(macro.get_capabilities(), {one_code}) self.assertSetEqual(macro.get_capabilities(), {one_code})
@ -88,12 +91,12 @@ class TestMacros(unittest.TestCase):
self.assertListEqual(self.result, [(one_code, 1), (one_code, 0)]) self.assertListEqual(self.result, [(one_code, 1), (one_code, 0)])
def test_fails(self): def test_fails(self):
self.assertIsNone(parse('r(1, a)')) self.assertIsNone(parse('r(1, a)', self.mapping))
self.assertIsNone(parse('r(a, k(b))')) self.assertIsNone(parse('r(a, k(b))', self.mapping))
self.assertIsNone(parse('m(a, b)')) self.assertIsNone(parse('m(a, b)', self.mapping))
def test_0(self): def test_0(self):
macro = parse('k(1)') macro = parse('k(1)', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
one_code = system_mapping.get('1') one_code = system_mapping.get('1')
self.assertSetEqual(macro.get_capabilities(), {one_code}) self.assertSetEqual(macro.get_capabilities(), {one_code})
@ -103,7 +106,7 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros), 0) self.assertEqual(len(macro.child_macros), 0)
def test_1(self): def test_1(self):
macro = parse('k(1).k(a).k(3)') macro = parse('k(1).k(a).k(3)', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
self.assertSetEqual(macro.get_capabilities(), { self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('1'), system_mapping.get('1'),
@ -120,37 +123,37 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros), 0) self.assertEqual(len(macro.child_macros), 0)
def test_return_errors(self): def test_return_errors(self):
error = parse('k(1).h(k(a)).k(3)', return_errors=True) error = parse('k(1).h(k(a)).k(3)', self.mapping, return_errors=True)
self.assertIsNone(error) self.assertIsNone(error)
error = parse('k(1))', return_errors=True) error = parse('k(1))', self.mapping, return_errors=True)
self.assertIn('bracket', error) self.assertIn('bracket', error)
error = parse('k((1)', return_errors=True) error = parse('k((1)', self.mapping, return_errors=True)
self.assertIn('bracket', error) self.assertIn('bracket', error)
error = parse('k((1).k)', return_errors=True) error = parse('k((1).k)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('r(a, k(1))', return_errors=True) error = parse('r(a, k(1))', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('k()', return_errors=True) error = parse('k()', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('k(1)', return_errors=True) error = parse('k(1)', self.mapping, return_errors=True)
self.assertIsNone(error) self.assertIsNone(error)
error = parse('k(1, 1)', return_errors=True) error = parse('k(1, 1)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('h(1, 1)', return_errors=True) error = parse('h(1, 1)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('h(h(h(1, 1)))', return_errors=True) error = parse('h(h(h(1, 1)))', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('r(1)', return_errors=True) error = parse('r(1)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('r(1, 1)', return_errors=True) error = parse('r(1, 1)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('r(k(1), 1)', return_errors=True) error = parse('r(k(1), 1)', self.mapping, return_errors=True)
self.assertIsNotNone(error) self.assertIsNotNone(error)
error = parse('r(1, k(1))', return_errors=True) error = parse('r(1, k(1))', self.mapping, return_errors=True)
self.assertIsNone(error) self.assertIsNone(error)
def test_hold(self): def test_hold(self):
macro = parse('k(1).h(k(a)).k(3)') macro = parse('k(1).h(k(a)).k(3)', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
self.assertSetEqual(macro.get_capabilities(), { self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('1'), system_mapping.get('1'),
@ -182,13 +185,13 @@ class TestMacros(unittest.TestCase):
start = time.time() start = time.time()
repeats = 20 repeats = 20
macro = parse(f'r({repeats}, k(k)).r(1, k(k))') macro = parse(f'r({repeats}, k(k)).r(1, k(k))', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
k_code = system_mapping.get('k') k_code = system_mapping.get('k')
self.assertSetEqual(macro.get_capabilities(), {k_code}) self.assertSetEqual(macro.get_capabilities(), {k_code})
self.loop.run_until_complete(macro.run()) self.loop.run_until_complete(macro.run())
keystroke_sleep = config.get('macros.keystroke_sleep_ms') keystroke_sleep = self.mapping.get('macros.keystroke_sleep_ms')
sleep_time = 2 * repeats * keystroke_sleep / 1000 sleep_time = 2 * repeats * keystroke_sleep / 1000
self.assertGreater(time.time() - start, sleep_time * 0.9) self.assertGreater(time.time() - start, sleep_time * 0.9)
self.assertLess(time.time() - start, sleep_time * 1.1) self.assertLess(time.time() - start, sleep_time * 1.1)
@ -203,13 +206,13 @@ class TestMacros(unittest.TestCase):
def test_3(self): def test_3(self):
start = time.time() start = time.time()
macro = parse('r(3, k(m).w(100))') macro = parse('r(3, k(m).w(100))', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
m_code = system_mapping.get('m') m_code = system_mapping.get('m')
self.assertSetEqual(macro.get_capabilities(), {m_code}) self.assertSetEqual(macro.get_capabilities(), {m_code})
self.loop.run_until_complete(macro.run()) self.loop.run_until_complete(macro.run())
keystroke_time = 6 * config.get('macros.keystroke_sleep_ms') keystroke_time = 6 * self.mapping.get('macros.keystroke_sleep_ms')
total_time = keystroke_time + 300 total_time = keystroke_time + 300
total_time /= 1000 total_time /= 1000
@ -224,7 +227,7 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros[0].child_macros), 0) self.assertEqual(len(macro.child_macros[0].child_macros), 0)
def test_4(self): def test_4(self):
macro = parse(' r(2,\nk(\nr ).k(minus\n )).k(m) ') macro = parse(' r(2,\nk(\nr ).k(minus\n )).k(m) ', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
r = system_mapping.get('r') r = system_mapping.get('r')
@ -246,7 +249,7 @@ class TestMacros(unittest.TestCase):
def test_5(self): def test_5(self):
start = time.time() start = time.time()
macro = parse('w(200).r(2,m(w,\nr(2,\tk(BtN_LeFt))).w(10).k(k))') macro = parse('w(200).r(2,m(w,\nr(2,\tk(BtN_LeFt))).w(10).k(k))', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
self.assertEqual(len(macro.child_macros), 1) self.assertEqual(len(macro.child_macros), 1)
@ -261,7 +264,7 @@ class TestMacros(unittest.TestCase):
self.loop.run_until_complete(macro.run()) self.loop.run_until_complete(macro.run())
num_pauses = 8 + 6 + 4 num_pauses = 8 + 6 + 4
keystroke_time = num_pauses * config.get('macros.keystroke_sleep_ms') keystroke_time = num_pauses * self.mapping.get('macros.keystroke_sleep_ms')
wait_time = 220 wait_time = 220
total_time = (keystroke_time + wait_time) / 1000 total_time = (keystroke_time + wait_time) / 1000
@ -276,11 +279,31 @@ class TestMacros(unittest.TestCase):
def test_6(self): def test_6(self):
# does nothing without .run # does nothing without .run
macro = parse('k(a).r(3, k(b))') macro = parse('k(a).r(3, k(b))', self.mapping)
macro.set_handler(self.handler) macro.set_handler(self.handler)
self.assertIsInstance(macro, _Macro) self.assertIsInstance(macro, _Macro)
self.assertListEqual(self.result, []) self.assertListEqual(self.result, [])
def test_keystroke_sleep_config(self):
# global config as fallback
config.set('macros.keystroke_sleep_ms', 100)
start = time.time()
macro = parse('k(a).k(b)', self.mapping)
self.loop.run_until_complete(macro.run())
delta = time.time() - start
# is currently over 400, k(b) adds another sleep afterwards
# that doesn't do anything
self.assertGreater(delta, 0.300)
# now set the value in the mapping, which is prioritized
self.mapping.set('macros.keystroke_sleep_ms', 50)
start = time.time()
macro = parse('k(a).k(b)', self.mapping)
self.loop.run_until_complete(macro.run())
delta = time.time() - start
self.assertGreater(delta, 0.150)
self.assertLess(delta, 0.300)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -27,19 +27,12 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X
from keymapper.mapping import Mapping from keymapper.mapping import Mapping
from keymapper.state import SystemMapping from keymapper.state import SystemMapping
from keymapper.config import config
from tests.test import tmp from tests.test import tmp
class TestMapping(unittest.TestCase): class TestSystemMapping(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def tearDown(self):
if os.path.exists(tmp):
shutil.rmtree(tmp)
def test_system_mapping(self): def test_system_mapping(self):
system_mapping = SystemMapping() system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100) self.assertGreater(len(system_mapping._mapping), 100)
@ -78,6 +71,54 @@ class TestMapping(unittest.TestCase):
self.assertIn('btn_left', names) self.assertIn('btn_left', names)
self.assertIn('btn_right', names) self.assertIn('btn_right', names)
class TestMapping(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def tearDown(self):
if os.path.exists(tmp):
shutil.rmtree(tmp)
def test_config(self):
self.mapping.save('foo', 'bar2')
self.assertEqual(self.mapping.get('a'), None)
self.mapping.set('a', 1)
self.assertEqual(self.mapping.get('a'), 1)
self.mapping.remove('a')
self.mapping.set('a.b', 2)
self.assertEqual(self.mapping.get('a.b'), 2)
self.assertEqual(self.mapping._config['a']['b'], 2)
self.mapping.remove('a.b')
self.mapping.set('a.b.c', 3)
self.assertEqual(self.mapping.get('a.b.c'), 3)
self.assertEqual(self.mapping._config['a']['b']['c'], 3)
# setting mapping.whatever does not overwrite the mapping
# after saving. It should be ignored.
self.mapping.change((EV_KEY, 81, 1), 'a')
self.mapping.set('mapping.a', 2)
self.mapping.save('foo', 'bar')
self.mapping.load('foo', 'bar')
self.assertEqual(self.mapping.get_character((EV_KEY, 81, 1)), 'a')
self.assertIsNone(self.mapping.get('mapping.a'))
# loading a different preset also removes the configs from memory
self.mapping.set('a.b.c', 6)
self.mapping.load('foo', 'bar2')
self.assertIsNone(self.mapping.get('a.b.c'))
def test_fallback(self):
config.set('d.e.f', 5)
self.assertEqual(self.mapping.get('d.e.f'), 5)
self.mapping.set('d.e.f', 3)
self.assertEqual(self.mapping.get('d.e.f'), 3)
def test_clone(self): def test_clone(self):
ev_1 = (EV_KEY, 1, 1) ev_1 = (EV_KEY, 1, 1)
ev_2 = (EV_KEY, 2, 0) ev_2 = (EV_KEY, 2, 0)
@ -104,7 +145,7 @@ class TestMapping(unittest.TestCase):
self.mapping.change(one, '1') self.mapping.change(one, '1')
self.mapping.change(two, '2') self.mapping.change(two, '2')
self.mapping.change(three, '3') self.mapping.change(three, '3')
self.mapping.config['foo'] = 'bar' self.mapping._config['foo'] = 'bar'
self.mapping.save('device 1', 'test') self.mapping.save('device 1', 'test')
path = os.path.join(tmp, 'device 1', 'test.json') path = os.path.join(tmp, 'device 1', 'test.json')
@ -118,7 +159,7 @@ class TestMapping(unittest.TestCase):
self.assertEqual(loaded.get_character(one), '1') self.assertEqual(loaded.get_character(one), '1')
self.assertEqual(loaded.get_character(two), '2') self.assertEqual(loaded.get_character(two), '2')
self.assertEqual(loaded.get_character(three), '3') self.assertEqual(loaded.get_character(three), '3')
self.assertEqual(loaded.config['foo'], 'bar') self.assertEqual(loaded._config['foo'], 'bar')
def test_save_load_2(self): def test_save_load_2(self):
# loads mappings with only (type, code) as the key # loads mappings with only (type, code) as the key

Loading…
Cancel
Save