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 json
import shutil
import copy
from keymapper.paths import CONFIG, USER, touch
from keymapper.logger import logger
@ -34,7 +33,13 @@ from keymapper.logger import logger
MOUSE = 'mouse'
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 = {
'autoload': {},
@ -58,13 +63,32 @@ INITIAL_CONFIG = {
}
class _Config:
def __init__(self):
class ConfigBase:
"""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.load_config()
self.fallback = fallback
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('.')
if config is None:
@ -108,7 +132,10 @@ class _Config:
For example 'macros.keystroke_sleep_ms'
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):
parent[chunk] = value
@ -129,6 +156,8 @@ class _Config:
return child
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:
resolved = self._resolve(path, callback, INITIAL_CONFIG)
@ -137,6 +166,23 @@ class _Config:
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):
"""Set a preset to be automatically applied on start.
@ -167,6 +213,7 @@ class _Config:
# treated like an empty config
logger.debug('Config "%s" doesn\'t exist yet', CONFIG_PATH)
self.clear_config()
self._config = INITIAL_CONFIG
self.save_config()
return
@ -174,10 +221,6 @@ class _Config:
self._config.update(json.load(file))
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):
"""Save the config to the file system."""
touch(CONFIG_PATH)
@ -189,4 +232,4 @@ class _Config:
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 keymapper.logger import logger
from keymapper.config import config, MOUSE, WHEEL
from keymapper.config import MOUSE, WHEEL
# 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
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.
Parameters
@ -112,6 +112,8 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device):
the outside.
input_device : evdev.InputDevice
keymapper_device : evdev.UInput
mapping : Mapping
the mapping object that configures the current injection
"""
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_ry_rel = 0
# TODO move this stuff into the preset configuration
pointer_speed = config.get('gamepad.joystick.pointer_speed')
non_linearity = config.get('gamepad.joystick.non_linearity')
left_purpose = config.get('gamepad.joystick.left_purpose')
right_purpose = config.get('gamepad.joystick.right_purpose')
# TODO overwrite mapping stuff in tests
pointer_speed = mapping.get('gamepad.joystick.pointer_speed')
non_linearity = mapping.get('gamepad.joystick.non_linearity')
left_purpose = mapping.get('gamepad.joystick.left_purpose')
right_purpose = mapping.get('gamepad.joystick.right_purpose')
logger.info(
'Left joystick as %s, right joystick as %s',

@ -305,7 +305,7 @@ class KeycodeInjector:
macros = {}
for key, output in self.mapping:
if is_this_a_macro(output):
macro = parse(output)
macro = parse(output, self.mapping)
if macro is None:
continue
@ -341,7 +341,12 @@ class KeycodeInjector:
if abs_to_rel:
self.abs_state[0] = 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)
if len(coroutines) == 0:

@ -65,17 +65,20 @@ class _Macro:
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.
"""
def __init__(self, code):
def __init__(self, code, mapping):
"""Create a macro instance that can be populated with tasks.
Parameters
----------
code : string
The original parsed code, for logging purposes.
mapping : Mapping
The preset object, needed for some config stuff
"""
self.tasks = []
self.handler = lambda *args: logger.error('No handler set')
self.code = code
self.mapping = mapping
# supposed to be True between key event values 1 (down) and 0 (up)
self.holding = False
@ -212,7 +215,7 @@ class _Macro:
def add_keycode_pause(self):
"""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():
await asyncio.sleep(sleeptime)
@ -311,13 +314,15 @@ def _count_brackets(macro):
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.
Parameters
----------
macro : string
Just like parse
mapping : Mapping
The preset configuration
macro_instance : _Macro or None
A macro instance to add tasks to
depth : int
@ -332,7 +337,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
assert isinstance(depth, int)
if macro_instance is None:
macro_instance = _Macro(macro)
macro_instance = _Macro(macro, mapping)
else:
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)
# evaluate the params
params = [
_parse_recurse(param.strip(), None, depth + 1)
_parse_recurse(param.strip(), mapping, None, depth + 1)
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] == '.':
chain = macro[position + 1:]
logger.spam('%sfollowed by %s', space, chain)
_parse_recurse(chain, macro_instance, depth)
_parse_recurse(chain, mapping, macro_instance, depth)
return macro_instance
@ -409,7 +414,7 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
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.
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(2, k(a).k(-)).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
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)
try:
macro_object = _parse_recurse(macro)
macro_object = _parse_recurse(macro, mapping)
return macro_object if not return_errors else None
except Exception as error:
logger.error('Failed to parse macro "%s": %s', macro, error)

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

@ -28,6 +28,7 @@ import copy
from keymapper.logger import logger
from keymapper.paths import get_config_path, touch
from keymapper.config import ConfigBase, config
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}')
class Mapping:
"""Contains and manages mappings.
The keycode is always unique, multiple keycodes may map to the same
character.
"""
class Mapping(ConfigBase):
"""Contains and manages mappings and config of a single preset."""
def __init__(self):
self._mapping = {}
self.changed = False
self.config = {}
super().__init__(fallback=config)
def __iter__(self):
"""Iterate over tuples of unique keycodes and their character."""
@ -81,8 +76,8 @@ class Mapping:
the previous key, same format as new_key
If not set, will not remove any previous mapping. If you recently
used 10 for new_keycode and want to overwrite that with 11,
provide 5 here.
used (1, 10, 1) for new_key and want to overwrite that with
(1, 11, 1), provide (1, 5, 1) here.
"""
if character is 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)
return
self.clear_config()
with open(path, 'r') as file:
preset_dict = json.load(file)
if not isinstance(preset_dict.get('mapping'), dict):
@ -176,7 +173,7 @@ class Mapping:
for key in preset_dict:
if key == 'mapping':
continue
self.config[key] = preset_dict[key]
self._config[key] = preset_dict[key]
self.changed = False
@ -195,6 +192,12 @@ class Mapping:
touch(path)
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,
# so put the mapping into a special key
json_ready_mapping = {}
@ -203,8 +206,7 @@ class Mapping:
new_key = ','.join([str(value) for value in key])
json_ready_mapping[new_key] = value
preset_dict = {'mapping': json_ready_mapping}
preset_dict.update(self.config)
preset_dict['mapping'] = json_ready_mapping
json.dump(preset_dict, file, indent=4)
file.write('\n')

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

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text>
</g>
<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="62.0" y="14">9.76</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.74</text>
<text x="62.0" y="14">9.74</text>
</g>
</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 acts as a mouse wheel. All buttons, triggers and D-Pads can be
mapped to keycodes and macros. Configuring the purpose of your joysticks
is currently done in the global configuration at `~/.config/key-mapper/config`.
mapped to keycodes and macros. The purpose of your joysticks can be
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,
while the joystick turns the view.
Tested with the XBOX 360 Gamepad. On Ubuntu, gamepads worked better in
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.dev.injector import KeycodeInjector
from keymapper.config import config
# no need for a high number in tests
KeycodeInjector.regrab_timeout = 0.15
# create an empty config beforehand
config.clear_config()
config.save_config()
def main():
update_verbosity(True)

@ -19,9 +19,10 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
import os
import unittest
from keymapper.config import config
from keymapper.config import config, CONFIG_PATH
class TestConfig(unittest.TestCase):
@ -53,7 +54,6 @@ class TestConfig(unittest.TestCase):
self.assertEqual(config._config['a']['b']['c'], 3)
def test_autoload(self):
del config._config['autoload']
self.assertEqual(len(config.iterate_autoload_presets()), 0)
self.assertFalse(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2', 'b'))
@ -87,6 +87,18 @@ class TestConfig(unittest.TestCase):
[('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):
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.config import config
from keymapper.mapping import Mapping
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
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]
SPEED = 20
class TestEvAbsMapper(unittest.TestCase):
# there is also `test_abs_to_rel` in test_injector.py
def setUp(self):
config.set('gamepad.joystick.non_linearity', 1)
config.set('gamepad.joystick.pointer_speed', SPEED)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.mapping = Mapping()
device = InputDevice('/dev/input/event30')
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):
config.clear_config()
self.mapping.clear_config()
loop = asyncio.get_event_loop()
for task in asyncio.Task.all_tasks():
@ -79,13 +82,16 @@ class TestEvAbsMapper(unittest.TestCase):
self.assertEqual(history.count(expectation), len(history))
def test_joystick_purpose_1(self):
config.set('gamepad.joystick.left_purpose', MOUSE)
config.set('gamepad.joystick.right_purpose', WHEEL)
speed = 20
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(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, SPEED))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -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))
# wheel event values are negative
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))
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.right_purpose', MOUSE)
@ -103,25 +112,28 @@ class TestEvAbsMapper(unittest.TestCase):
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 1))
# 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, 0, MAX_ABS, (EV_REL, REL_Y, SPEED))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -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))
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)
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(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))
# 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, 0, MAX_ABS, (EV_REL, REL_Y, SPEED))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -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))
def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL)

@ -85,7 +85,7 @@ class TestInjector(unittest.TestCase):
mapping.change((EV_KEY, 80, 1), 'a')
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)

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

@ -26,6 +26,7 @@ import asyncio
from keymapper.dev.macros import parse, _Macro, _extract_params, \
is_this_a_macro
from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.state import system_mapping
@ -33,9 +34,11 @@ class TestMacros(unittest.TestCase):
def setUp(self):
self.result = []
self.loop = asyncio.get_event_loop()
self.mapping = Mapping()
def tearDown(self):
self.result = []
self.mapping.clear_config()
def handler(self, code, value):
"""Where macros should write codes to."""
@ -76,7 +79,7 @@ class TestMacros(unittest.TestCase):
expect(',,', ['', '', ''])
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')
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)])
def test_fails(self):
self.assertIsNone(parse('r(1, a)'))
self.assertIsNone(parse('r(a, k(b))'))
self.assertIsNone(parse('m(a, b)'))
self.assertIsNone(parse('r(1, a)', self.mapping))
self.assertIsNone(parse('r(a, k(b))', self.mapping))
self.assertIsNone(parse('m(a, b)', self.mapping))
def test_0(self):
macro = parse('k(1)')
macro = parse('k(1)', self.mapping)
macro.set_handler(self.handler)
one_code = system_mapping.get('1')
self.assertSetEqual(macro.get_capabilities(), {one_code})
@ -103,7 +106,7 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros), 0)
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)
self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('1'),
@ -120,37 +123,37 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros), 0)
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)
error = parse('k(1))', return_errors=True)
error = parse('k(1))', self.mapping, return_errors=True)
self.assertIn('bracket', error)
error = parse('k((1)', return_errors=True)
error = parse('k((1)', self.mapping, return_errors=True)
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)
error = parse('r(a, k(1))', return_errors=True)
error = parse('r(a, k(1))', self.mapping, return_errors=True)
self.assertIsNotNone(error)
error = parse('k()', return_errors=True)
error = parse('k()', self.mapping, return_errors=True)
self.assertIsNotNone(error)
error = parse('k(1)', return_errors=True)
error = parse('k(1)', self.mapping, return_errors=True)
self.assertIsNone(error)
error = parse('k(1, 1)', return_errors=True)
error = parse('k(1, 1)', self.mapping, return_errors=True)
self.assertIsNotNone(error)
error = parse('h(1, 1)', return_errors=True)
error = parse('h(1, 1)', self.mapping, return_errors=True)
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)
error = parse('r(1)', return_errors=True)
error = parse('r(1)', self.mapping, return_errors=True)
self.assertIsNotNone(error)
error = parse('r(1, 1)', return_errors=True)
error = parse('r(1, 1)', self.mapping, return_errors=True)
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)
error = parse('r(1, k(1))', return_errors=True)
error = parse('r(1, k(1))', self.mapping, return_errors=True)
self.assertIsNone(error)
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)
self.assertSetEqual(macro.get_capabilities(), {
system_mapping.get('1'),
@ -182,13 +185,13 @@ class TestMacros(unittest.TestCase):
start = time.time()
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)
k_code = system_mapping.get('k')
self.assertSetEqual(macro.get_capabilities(), {k_code})
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
self.assertGreater(time.time() - start, sleep_time * 0.9)
self.assertLess(time.time() - start, sleep_time * 1.1)
@ -203,13 +206,13 @@ class TestMacros(unittest.TestCase):
def test_3(self):
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)
m_code = system_mapping.get('m')
self.assertSetEqual(macro.get_capabilities(), {m_code})
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 /= 1000
@ -224,7 +227,7 @@ class TestMacros(unittest.TestCase):
self.assertEqual(len(macro.child_macros[0].child_macros), 0)
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)
r = system_mapping.get('r')
@ -246,7 +249,7 @@ class TestMacros(unittest.TestCase):
def test_5(self):
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)
self.assertEqual(len(macro.child_macros), 1)
@ -261,7 +264,7 @@ class TestMacros(unittest.TestCase):
self.loop.run_until_complete(macro.run())
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
total_time = (keystroke_time + wait_time) / 1000
@ -276,11 +279,31 @@ class TestMacros(unittest.TestCase):
def test_6(self):
# 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)
self.assertIsInstance(macro, _Macro)
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__':
unittest.main()

@ -27,19 +27,12 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X
from keymapper.mapping import Mapping
from keymapper.state import SystemMapping
from keymapper.config import config
from tests.test import tmp
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)
class TestSystemMapping(unittest.TestCase):
def test_system_mapping(self):
system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100)
@ -78,6 +71,54 @@ class TestMapping(unittest.TestCase):
self.assertIn('btn_left', 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):
ev_1 = (EV_KEY, 1, 1)
ev_2 = (EV_KEY, 2, 0)
@ -104,7 +145,7 @@ class TestMapping(unittest.TestCase):
self.mapping.change(one, '1')
self.mapping.change(two, '2')
self.mapping.change(three, '3')
self.mapping.config['foo'] = 'bar'
self.mapping._config['foo'] = 'bar'
self.mapping.save('device 1', 'test')
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(two), '2')
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):
# loads mappings with only (type, code) as the key

Loading…
Cancel
Save