Merge remote-tracking branch 'original/main' into led_test

# Conflicts:
#	keymapper/injection/consumers/keycode_mapper.py
#	keymapper/injection/macros.py
led_test_merge
sezanzeb 3 years ago
commit 8a22db48d4

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: sezanzeb
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

@ -1,5 +1,9 @@
[_]
max-line-length=88 # black
extension-pkg-whitelist=evdev
disable=
# that is the standard way to import GTK afaik
wrong-import-position,

@ -1,7 +1,7 @@
Package: key-mapper
Version: 1.0.0
Version: 1.1.0
Architecture: all
Maintainer: Sezanzeb <proxima@sezanzeb.de>
Depends: build-essential, libpython3-dev, libdbus-1-dev, python3, python3-setuptools, python3-evdev, python3-pydbus, python3-gi, gettext
Depends: build-essential, libpython3-dev, libdbus-1-dev, python3, python3-setuptools, python3-evdev, python3-pydbus, python3-gi, gettext, python3-cairo
Description: A tool to change the mapping of your input device buttons
Replaces: python3-key-mapper

@ -25,10 +25,10 @@ Get a .deb file from the [release page](https://github.com/sezanzeb/key-mapper/r
or install the latest changes via:
```bash
sudo apt install git python3-setuptools
sudo apt install git python3-setuptools gettext
git clone https://github.com/sezanzeb/key-mapper.git
cd key-mapper; ./scripts/build.sh
sudo apt install ./dist/key-mapper-1.0.0.deb
cd key-mapper && ./scripts/build.sh
sudo apt install ./dist/key-mapper-1.1.0.deb
```
##### pip

@ -78,7 +78,7 @@ def utils(options):
sys.exit(0)
if options.key_names:
from keymapper.state import system_mapping
from keymapper.system_mapping import system_mapping
print('\n'.join(system_mapping.list_names()))
sys.exit(0)

@ -972,7 +972,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=9 -->
<!-- n-columns=2 n-rows=11 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
@ -1151,6 +1151,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="can-focus">False</property>
<property name="label" translatable="yes">mouse</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1163,6 +1164,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="can-focus">False</property>
<property name="label" translatable="yes">wheel</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1193,6 +1195,58 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if_tap</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if no other key is pressed until the keys release, execute the first param, otherwise the second</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">10</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if a key is tapped quickly, execute the first param, otherwise the second. The third param is the time in milliseconds</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if_single</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">10</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@ -1223,7 +1277,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=7 -->
<!-- n-columns=2 n-rows=10 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
@ -1235,6 +1289,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">k(1).k(2)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1260,6 +1315,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">r(3, k(a).w(500))</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1273,6 +1329,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">m(Control_L, k(a).k(x))</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1298,6 +1355,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">k(1).h(k(2)).k(3)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1335,6 +1393,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">e(EV_REL, REL_X, 10)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1372,6 +1431,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">mouse(right, 4)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1385,6 +1445,7 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="label" translatable="yes">wheel(down, 1)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
@ -1403,6 +1464,86 @@ See the &lt;a href="https://www.gnu.org/licenses/gpl-3.0.html"&gt;GNU General Pu
<property name="top-attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if_single(k(a), k(b))</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if_tap(k(a), k(b), 1000)</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">if_tap(k(a), k(b))</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">writes a if the key is released within a second, otherwise b</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">writes a if the key is tapped, otherwise b</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">writes b if another key is pressed, or a if the key is released and no other key was pressed in the meantime.</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">9</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>

@ -30,32 +30,32 @@ from keymapper.paths import CONFIG_PATH, USER, touch
from keymapper.logger import logger
MOUSE = 'mouse'
WHEEL = 'wheel'
BUTTONS = 'buttons'
NONE = 'none'
MOUSE = "mouse"
WHEEL = "wheel"
BUTTONS = "buttons"
NONE = "none"
INITIAL_CONFIG = {
'autoload': {},
'macros': {
"autoload": {},
"macros": {
# some time between keystrokes might be required for them to be
# detected properly in software.
'keystroke_sleep_ms': 10
"keystroke_sleep_ms": 10
},
'gamepad': {
'joystick': {
"gamepad": {
"joystick": {
# very small movements of the joystick should result in very
# small mouse movements. With a non_linearity of 1 it is
# impossible/hard to even find a resting position that won't
# move the cursor.
'non_linearity': 4,
'pointer_speed': 80,
'left_purpose': NONE,
'right_purpose': NONE,
'x_scroll_speed': 2,
'y_scroll_speed': 0.5
"non_linearity": 4,
"pointer_speed": 80,
"left_purpose": NONE,
"right_purpose": NONE,
"x_scroll_speed": 2,
"y_scroll_speed": 0.5,
},
}
},
}
@ -65,6 +65,7 @@ class ConfigBase:
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.
@ -85,7 +86,7 @@ class ConfigBase:
config : dict
The dictionary to search. Defaults to self._config.
"""
chunks = path.copy() if isinstance(path, list) else path.split('.')
chunks = path.copy() if isinstance(path, list) else path.split(".")
if config is None:
child = self._config
@ -113,6 +114,7 @@ class ConfigBase:
path : string or string[]
For example 'macros.keystroke_sleep_ms'
"""
def callback(parent, child, chunk):
if child is not None:
del parent[chunk]
@ -129,10 +131,7 @@ class ConfigBase:
or ['macros', 'keystroke_sleep_ms']
value : any
"""
logger.info(
'Changing "%s" to "%s" in %s',
path, value, self.__class__.__name__
)
logger.info('Changing "%s" to "%s" in %s', path, value, self.__class__.__name__)
def callback(parent, child, chunk):
parent[chunk] = value
@ -149,6 +148,7 @@ class ConfigBase:
log_unknown : bool
If True, write an error if `path` does not exist in the config
"""
def callback(parent, child, chunk):
return child
@ -179,14 +179,15 @@ class GlobalConfig(ConfigBase):
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):
self.path = os.path.join(CONFIG_PATH, 'config.json')
self.path = os.path.join(CONFIG_PATH, "config.json")
# migrate from < 0.4.0, add the .json ending
deprecated_path = os.path.join(CONFIG_PATH, 'config')
deprecated_path = os.path.join(CONFIG_PATH, "config")
if os.path.exists(deprecated_path) and not os.path.exists(self.path):
logger.info('Moving "%s" to "%s"', deprecated_path, self.path)
os.rename(os.path.join(CONFIG_PATH, 'config'), self.path)
os.rename(os.path.join(CONFIG_PATH, "config"), self.path)
super().__init__()
@ -203,21 +204,18 @@ class GlobalConfig(ConfigBase):
if None, don't autoload something for this device.
"""
if preset is not None:
self.set(['autoload', group_key], preset)
self.set(["autoload", group_key], preset)
else:
logger.info(
'Not injecting for "%s" automatically anmore',
group_key
)
self.remove(['autoload', group_key])
logger.info('Not injecting for "%s" automatically anmore', group_key)
self.remove(["autoload", group_key])
def iterate_autoload_presets(self):
"""Get tuples of (device, preset)."""
return self._config.get('autoload', {}).items()
return self._config.get("autoload", {}).items()
def is_autoloaded(self, group_key, preset):
"""Should this preset be loaded automatically?"""
return self.get(['autoload', group_key], log_unknown=False) == preset
return self.get(["autoload", group_key], log_unknown=False) == preset
def load_config(self, path=None):
"""Load the config from the file system.
@ -244,30 +242,31 @@ class GlobalConfig(ConfigBase):
self.save_config()
return
with open(self.path, 'r') as file:
with open(self.path, "r") as file:
try:
self._config.update(json.load(file))
logger.info('Loaded config from "%s"', self.path)
except json.decoder.JSONDecodeError as error:
logger.error(
'Failed to parse config "%s": %s. Using defaults',
self.path, str(error)
self.path,
str(error),
)
# uses the default configuration when the config object
# is empty automatically
def save_config(self):
"""Save the config to the file system."""
if USER == 'root':
logger.debug('Skipping config file creation for the root user')
if USER == "root":
logger.debug("Skipping config file creation for the root user")
return
touch(self.path)
with open(self.path, 'w') as file:
with open(self.path, "w") as file:
json.dump(self._config, file, indent=4)
logger.info('Saved config to %s', self.path)
file.write('\n')
logger.info("Saved config to %s", self.path)
file.write("\n")
config = GlobalConfig()

@ -33,22 +33,24 @@ import atexit
from pydbus import SystemBus
import gi
gi.require_version('GLib', '2.0')
gi.require_version("GLib", "2.0")
from gi.repository import GLib
from keymapper.logger import logger, is_debug
from keymapper.injection.injector import Injector, UNKNOWN
from keymapper.mapping import Mapping
from keymapper.config import config
from keymapper.state import system_mapping
from keymapper.system_mapping import system_mapping
from keymapper.groups import groups
BUS_NAME = 'keymapper.Control'
BUS_NAME = "keymapper.Control"
class AutoloadHistory:
"""Contains the autoloading history and constraints."""
def __init__(self):
"""Construct this with an empty history."""
# mapping of device -> (timestamp, preset)
@ -142,7 +144,7 @@ class Daemon:
def __init__(self):
"""Constructs the daemon."""
logger.debug('Creating daemon')
logger.debug("Creating daemon")
self.injectors = {}
self.config_dir = None
@ -164,24 +166,24 @@ class Daemon:
try:
bus = SystemBus()
interface = bus.get(BUS_NAME)
logger.info('Connected to the service')
logger.info("Connected to the service")
except GLib.GError as error:
if not fallback:
logger.error('Service not running? %s', error)
logger.error("Service not running? %s", error)
return None
logger.info('Starting the service')
logger.info("Starting the service")
# Blocks until pkexec is done asking for the password.
# Runs via key-mapper-control so that auth_admin_keep works
# for all pkexec calls of the gui
debug = ' -d' if is_debug() else ''
cmd = f'pkexec key-mapper-control --command start-daemon {debug}'
debug = " -d" if is_debug() else ""
cmd = f"pkexec key-mapper-control --command start-daemon {debug}"
# using pkexec will also cause the service to continue running in
# the background after the gui has been closed, which will keep
# the injections ongoing
logger.debug('Running `%s`', cmd)
logger.debug("Running `%s`", cmd)
os.system(cmd)
time.sleep(0.2)
@ -193,11 +195,12 @@ class Daemon:
except GLib.GError as error:
logger.debug(
'Attempt %d to connect to the service failed: "%s"',
attempt + 1, error
attempt + 1,
error,
)
time.sleep(0.2)
else:
logger.error('Failed to connect to the service')
logger.error("Failed to connect to the service")
sys.exit(1)
return interface
@ -208,13 +211,13 @@ class Daemon:
try:
bus.publish(BUS_NAME, self)
except RuntimeError as error:
logger.error('Is the service is already running? %s', str(error))
logger.error("Is the service already running? (%s)", str(error))
sys.exit(1)
def run(self):
"""Start the daemons loop. Blocks until the daemon stops."""
loop = GLib.MainLoop()
logger.debug('Running daemon')
logger.debug("Running daemon")
loop.run()
def refresh(self, group_key=None):
@ -227,7 +230,7 @@ class Daemon:
"""
now = time.time()
if now - 10 > self.refreshed_devices_at:
logger.debug('Refreshing because last info is too old')
logger.debug("Refreshing because last info is too old")
groups.refresh()
self.refreshed_devices_at = now
return
@ -241,8 +244,7 @@ class Daemon:
"""Stop injecting the mapping for a single device."""
if self.injectors.get(group_key) is None:
logger.debug(
'Tried to stop injector, but none is running for group "%s"',
group_key
'Tried to stop injector, but none is running for group "%s"', group_key
)
return
@ -266,7 +268,7 @@ class Daemon:
This path contains config.json, xmodmap.json and the
presets directory
"""
config_path = os.path.join(config_dir, 'config.json')
config_path = os.path.join(config_dir, "config.json")
if not os.path.exists(config_path):
logger.error('"%s" does not exist', config_path)
return
@ -290,7 +292,7 @@ class Daemon:
# either not relevant for key-mapper, or not connected yet
return
preset = config.get(['autoload', group.key], log_unknown=False)
preset = config.get(["autoload", group.key], log_unknown=False)
if preset is None:
# no autoloading is configured for this device
@ -298,7 +300,7 @@ class Daemon:
if not isinstance(preset, str):
# might be broken due to a previous bug
config.remove(['autoload', group.key])
config.remove(["autoload", group.key])
config.save_config()
return
@ -307,7 +309,8 @@ class Daemon:
if not self.autoload_history.may_autoload(group.key, preset):
logger.info(
'Not autoloading the same preset "%s" again for group "%s"',
preset, group.key
preset,
group.key,
)
return
@ -325,7 +328,7 @@ class Daemon:
unique identifier used by the groups object
"""
# avoid some confusing logs and filter obviously invalid requests
if group_key.startswith('key-mapper'):
if group_key.startswith("key-mapper"):
return
logger.info('Request to autoload for "%s"', group_key)
@ -334,8 +337,8 @@ class Daemon:
# spams on boot, when no user is logged in yet
logger.debug(
'Tried to autoload "%s" without configuring the daemon '
'first via set_config_dir.',
group_key
"first via set_config_dir.",
group_key,
)
return
@ -348,17 +351,17 @@ class Daemon:
"""
if self.config_dir is None:
logger.error(
'Tried to autoload without configuring the daemon first '
'via set_config_dir.'
"Tried to autoload without configuring the daemon first "
"via set_config_dir."
)
return
autoload_presets = list(config.iterate_autoload_presets())
logger.info('Autoloading for all devices')
logger.info("Autoloading for all devices")
if len(autoload_presets) == 0:
logger.error('No presets configured to autoload')
logger.error("No presets configured to autoload")
return
for group_key, _ in autoload_presets:
@ -381,8 +384,8 @@ class Daemon:
if self.config_dir is None:
logger.error(
'Tried to start an injection without configuring the daemon '
'first via set_config_dir.'
"Tried to start an injection without configuring the daemon "
"first via set_config_dir."
)
return False
@ -393,10 +396,7 @@ class Daemon:
return False
preset_path = os.path.join(
self.config_dir,
'presets',
group.name,
f'{preset}.json'
self.config_dir, "presets", group.name, f"{preset}.json"
)
mapping = Mapping()
@ -413,9 +413,9 @@ class Daemon:
# readable keys in the correct keyboard layout to the service.
# The service cannot use `xmodmap -pke` because it's running via
# systemd.
xmodmap_path = os.path.join(self.config_dir, 'xmodmap.json')
xmodmap_path = os.path.join(self.config_dir, "xmodmap.json")
try:
with open(xmodmap_path, 'r') as file:
with open(xmodmap_path, "r") as file:
# do this for each injection to make sure it is up to
# date when the system layout changes.
xmodmap = json.load(file)
@ -439,7 +439,7 @@ class Daemon:
def stop_all(self):
"""Stop all injections."""
logger.info('Stopping all injections')
logger.info("Stopping all injections")
for group_key in list(self.injectors.keys()):
self.stop_injecting(group_key)

@ -33,7 +33,7 @@ from keymapper.logger import logger
logged = False
def get_data_path(filename=''):
def get_data_path(filename=""):
"""Depending on the installation prefix, return the data dir.
Since it is a nightmare to get stuff installed with pip across
@ -44,7 +44,7 @@ def get_data_path(filename=''):
source = None
try:
source = pkg_resources.require('key-mapper')[0].location
source = pkg_resources.require("key-mapper")[0].location
# failed in some ubuntu installations
except pkg_resources.DistributionNotFound:
pass
@ -56,18 +56,18 @@ def get_data_path(filename=''):
data = None
# python3.8/dist-packages python3.7/site-packages, /usr/share,
# /usr/local/share, endless options
if source and '-packages' not in source and 'python' not in source:
if source and "-packages" not in source and "python" not in source:
# probably installed with -e, running from the cloned git source
data = os.path.join(source, 'data')
data = os.path.join(source, "data")
if not os.path.exists(data):
if not logged:
logger.debug('-e, but data missing at "%s"', data)
data = None
candidates = [
'/usr/share/key-mapper',
'/usr/local/share/key-mapper',
os.path.join(site.USER_BASE, 'share/key-mapper'),
"/usr/share/key-mapper",
"/usr/local/share/key-mapper",
os.path.join(site.USER_BASE, "share/key-mapper"),
]
if data is None:
@ -78,7 +78,7 @@ def get_data_path(filename=''):
break
if data is None:
logger.error('Could not find the application data')
logger.error("Could not find the application data")
sys.exit(1)
if not logged:

@ -39,8 +39,19 @@ import asyncio
import json
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, \
ABS_MT_POSITION_X, REL_X, KEY_A, BTN_LEFT, REL_Y, REL_WHEEL
from evdev.ecodes import (
EV_KEY,
EV_ABS,
KEY_CAMERA,
EV_REL,
BTN_STYLUS,
ABS_MT_POSITION_X,
REL_X,
KEY_A,
BTN_LEFT,
REL_Y,
REL_WHEEL,
)
from keymapper.logger import logger
from keymapper.paths import get_preset_path
@ -50,19 +61,19 @@ TABLET_KEYS = [
evdev.ecodes.BTN_STYLUS,
evdev.ecodes.BTN_TOOL_BRUSH,
evdev.ecodes.BTN_TOOL_PEN,
evdev.ecodes.BTN_TOOL_RUBBER
evdev.ecodes.BTN_TOOL_RUBBER,
]
GAMEPAD = 'gamepad'
KEYBOARD = 'keyboard'
MOUSE = 'mouse'
TOUCHPAD = 'touchpad'
GRAPHICS_TABLET = 'graphics-tablet'
CAMERA = 'camera'
UNKNOWN = 'unknown'
GAMEPAD = "gamepad"
KEYBOARD = "keyboard"
MOUSE = "mouse"
TOUCHPAD = "touchpad"
GRAPHICS_TABLET = "graphics-tablet"
CAMERA = "camera"
UNKNOWN = "unknown"
if not hasattr(evdev.InputDevice, 'path'):
if not hasattr(evdev.InputDevice, "path"):
# for evdev < 1.0.0 patch the path property
@property
def path(device):
@ -178,10 +189,7 @@ def classify(device):
return UNKNOWN
DENYLIST = [
'.*Yubico.*YubiKey.*',
'Eee PC WMI hotkeys'
]
DENYLIST = [".*Yubico.*YubiKey.*", "Eee PC WMI hotkeys"]
def is_denylisted(device):
@ -216,9 +224,9 @@ def get_unique_key(device):
# device.info bustype, vendor and product are unique for
# a product, but multiple similar device models would be grouped
# in the same group
f'{device.info.bustype}_'
f'{device.info.vendor}_'
f'{device.info.product}_'
f"{device.info.bustype}_"
f"{device.info.vendor}_"
f"{device.info.product}_"
# deivce.phys if "/input..." is removed from it, because the first
# chunk seems to be unique per hardware (if it's not completely empty)
f'{device.phys.split("/")[0] or "-"}'
@ -242,6 +250,7 @@ class _Group:
look the same for a device model. It is used to generate the
presets folder structure
"""
def __init__(self, paths, names, types, key):
"""Specify a group
@ -284,12 +293,9 @@ class _Group:
def dumps(self):
"""Return a string representing this object."""
return json.dumps(dict(
paths=self.paths,
names=self.names,
types=self.types,
key=self.key
))
return json.dumps(
dict(paths=self.paths, names=self.names, types=self.types, key=self.key)
)
@classmethod
def loads(cls, serialized):
@ -298,7 +304,7 @@ class _Group:
return group
def __repr__(self):
return f'Group({self.key})'
return f"Group({self.key})"
class _FindGroups(threading.Thread):
@ -308,6 +314,7 @@ class _FindGroups(threading.Thread):
asynchronously so that they can take as much time as they want without
slowing down the initialization.
"""
def __init__(self, pipe):
"""Construct the process.
@ -325,7 +332,7 @@ class _FindGroups(threading.Thread):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
logger.debug('Discovering device paths')
logger.debug("Discovering device paths")
# group them together by usb device because there could be stuff like
# "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control"
@ -333,7 +340,7 @@ class _FindGroups(threading.Thread):
for path in evdev.list_devices():
device = evdev.InputDevice(path)
if device.name == 'Power Button':
if device.name == "Power Button":
continue
device_type = classify(device)
@ -358,8 +365,7 @@ class _FindGroups(threading.Thread):
grouped[key] = []
logger.spam(
'Found "%s", "%s", "%s", type: %s',
key, path, device.name, device_type
'Found "%s", "%s", "%s", type: %s', key, path, device.name, device_type
)
grouped[key].append((device.name, path, device_type))
@ -376,7 +382,7 @@ class _FindGroups(threading.Thread):
key = shortest_name
i = 2
while key in used_keys:
key = f'{shortest_name} {i}'
key = f"{shortest_name} {i}"
i += 1
used_keys.add(key)
@ -384,10 +390,7 @@ class _FindGroups(threading.Thread):
key=key,
paths=devs,
names=names,
types=sorted(list({
item[2] for item in group
if item[2] != UNKNOWN
}))
types=sorted(list({item[2] for item in group if item[2] != UNKNOWN})),
)
result.append(group.dumps())
@ -400,6 +403,7 @@ class _FindGroups(threading.Thread):
class _Groups:
"""Contains and manages all groups."""
def __init__(self):
self._groups = {}
self._find_groups()
@ -428,17 +432,17 @@ class _Groups:
self.loads(pipe[0].recv())
if len(self._groups) == 0:
logger.debug('Did not find any input device')
logger.debug("Did not find any input device")
else:
keys = [f'"{group.key}"' for group in self._groups]
logger.info('Found %s', ', '.join(keys))
logger.info("Found %s", ", ".join(keys))
def filter(self, include_keymapper=False):
"""Filter groups."""
result = []
for group in self._groups:
name = group.name
if not include_keymapper and name.startswith('key-mapper'):
if not include_keymapper and name.startswith("key-mapper"):
continue
result.append(group)
@ -452,8 +456,9 @@ class _Groups:
def list_group_names(self):
"""Return a list of all 'name' properties of the groups."""
return [
group.name for group in self._groups
if not group.name.startswith('key-mapper')
group.name
for group in self._groups
if not group.name.startswith("key-mapper")
]
def __len__(self):

@ -0,0 +1,28 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""One mapping object for the GUI application."""
from keymapper.mapping import Mapping
custom_mapping = Mapping()

@ -25,6 +25,11 @@ It should be started via key-mapper-control and pkexec.
GUIs should not run as root
https://wiki.archlinux.org/index.php/Running_GUI_applications_as_root
The service shouldn't do that even though it has root rights, because that
would provide a key-logger that can be accessed by any user at all times,
whereas for the helper to start a password is needed and it stops when the ui
closes.
"""
@ -40,16 +45,17 @@ from keymapper.ipc.pipe import Pipe
from keymapper.logger import logger
from keymapper.groups import groups
from keymapper import utils
from keymapper.user import USER
TERMINATE = 'terminate'
REFRESH_GROUPS = 'refresh_groups'
TERMINATE = "terminate"
REFRESH_GROUPS = "refresh_groups"
def is_helper_running():
"""Check if the helper is running."""
try:
subprocess.check_output(['pgrep', '-f', 'key-mapper-helper'])
subprocess.check_output(["pgrep", "-f", "key-mapper-helper"])
except subprocess.CalledProcessError:
return False
return True
@ -63,10 +69,11 @@ class RootHelper:
Commands are either numbers for generic commands,
or strings to start listening on a specific device.
"""
def __init__(self):
"""Construct the helper and initialize its sockets."""
self._results = Pipe('/tmp/key-mapper/results')
self._commands = Pipe('/tmp/key-mapper/commands')
self._results = Pipe(f"/tmp/key-mapper-{USER}/results")
self._commands = Pipe(f"/tmp/key-mapper-{USER}/commands")
self._send_groups()
@ -81,10 +88,7 @@ class RootHelper:
def _send_groups(self):
"""Send the groups to the gui."""
self._results.send({
'type': 'groups',
'message': groups.dumps()
})
self._results.send({"type": "groups", "message": groups.dumps()})
def _handle_commands(self):
"""Handle all unread commands."""
@ -96,7 +100,7 @@ class RootHelper:
logger.debug('Received command "%s"', cmd)
if cmd == TERMINATE:
logger.debug('Helper terminates')
logger.debug("Helper terminates")
sys.exit(0)
if cmd == REFRESH_GROUPS:
@ -126,7 +130,7 @@ class RootHelper:
rlist = {}
if self.group is None:
logger.error('group is None')
logger.error("group is None")
return
virtual_devices = []
@ -150,7 +154,7 @@ class RootHelper:
logger.debug(
'Starting reading keycodes from "%s"',
'", "'.join([device.name for device in virtual_devices])
'", "'.join([device.name for device in virtual_devices]),
)
rlist[self._commands] = self._commands
@ -192,23 +196,20 @@ class RootHelper:
# ignore hold-down events
return
blacklisted_keys = [
evdev.ecodes.BTN_TOOL_DOUBLETAP
]
blacklisted_keys = [evdev.ecodes.BTN_TOOL_DOUBLETAP]
if event.type == EV_KEY and event.code in blacklisted_keys:
return
if event.type == EV_ABS:
abs_range = utils.get_abs_range(device, event.code)
event.value = utils.normalize_value(event, abs_range)
event.value = utils.classify_action(event, abs_range)
else:
event.value = utils.normalize_value(event)
self._results.send({
'type': 'event',
'message': (
event.sec, event.usec,
event.type, event.code, event.value
)
})
event.value = utils.classify_action(event)
self._results.send(
{
"type": "event",
"message": (event.sec, event.usec, event.type, event.code, event.value),
}
)

@ -34,7 +34,8 @@ from keymapper.groups import groups, GAMEPAD
from keymapper.ipc.pipe import Pipe
from keymapper.gui.helper import TERMINATE, REFRESH_GROUPS
from keymapper import utils
from keymapper.state import custom_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.user import USER
DEBOUNCE_TICKS = 3
@ -54,6 +55,7 @@ class Reader:
object. GTK has get_key for keyboard keys, but Reader also
has knowledge of buttons like the middle-mouse button.
"""
def __init__(self):
self.previous_event = None
self.previous_result = None
@ -69,8 +71,8 @@ class Reader:
def connect(self):
"""Connect to the helper."""
self._results = Pipe('/tmp/key-mapper/results')
self._commands = Pipe('/tmp/key-mapper/commands')
self._results = Pipe(f"/tmp/key-mapper-{USER}/results")
self._commands = Pipe(f"/tmp/key-mapper-{USER}/commands")
def are_new_devices_available(self):
"""Check if groups contains new devices.
@ -83,17 +85,17 @@ class Reader:
def _get_event(self, message):
"""Return an InputEvent if the message contains one. None otherwise."""
message_type = message['type']
message_body = message['message']
message_type = message["type"]
message_body = message["message"]
if message_type == 'groups':
if message_type == "groups":
if message_body != groups.dumps():
groups.loads(message_body)
logger.debug('Received %d devices', len(groups))
logger.debug("Received %d devices", len(groups))
self._devices_updated = True
return None
if message_type == 'event':
if message_type == "event":
return evdev.InputEvent(*message_body)
logger.error('Received unknown message "%s"', message)
@ -141,12 +143,12 @@ class Reader:
type_code = (event.type, event.code)
if event.value == 0:
logger.key_spam(event_tuple, 'release')
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')
logger.key_spam(event_tuple, "duplicate key down")
self._debounce_start(event_tuple)
continue
@ -156,7 +158,7 @@ class Reader:
# 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')
logger.key_spam(event_tuple, "down")
self._unreleased[type_code] = event_tuple
self._debounce_start(event_tuple)
previous_event = event
@ -176,7 +178,7 @@ class Reader:
return None
self.previous_result = result
logger.key_spam(result.keys, 'read result')
logger.key_spam(result.keys, "read result")
return result
@ -191,7 +193,7 @@ class Reader:
def terminate(self):
"""Stop reading keycodes for good."""
logger.debug('Sending close msg to helper')
logger.debug("Sending close msg to helper")
self._commands.send(TERMINATE)
def refresh_groups(self):
@ -200,7 +202,7 @@ class Reader:
def clear(self):
"""Next time when reading don't return the previous keycode."""
logger.debug('Clearing reader')
logger.debug("Clearing reader")
while self._results.poll():
# clear the results pipe and handle any non-event messages,
# otherwise a 'groups' message might get lost
@ -240,10 +242,7 @@ class Reader:
# clear wheel events from unreleased after some time
if self._debounce_remove[type_code] == 0:
logger.key_spam(
self._unreleased[type_code],
'Considered as released'
)
logger.key_spam(self._unreleased[type_code], "Considered as released")
self._release(type_code)
else:
self._debounce_remove[type_code] -= 1

@ -25,7 +25,8 @@
import evdev
from gi.repository import Gtk, GLib, Gdk
from keymapper.state import custom_mapping, system_mapping
from keymapper.system_mapping import system_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.logger import logger
from keymapper.key import Key
from keymapper.gui.reader import reader
@ -43,8 +44,14 @@ def populate_store():
store.append([name])
extra = [
'mouse(up, 1)', 'mouse(down, 1)', 'mouse(left, 1)', 'mouse(right, 1)',
'wheel(up, 1)', 'wheel(down, 1)', 'wheel(left, 1)', 'wheel(right, 1)'
"mouse(up, 1)",
"mouse(down, 1)",
"mouse(left, 1)",
"mouse(right, 1)",
"wheel(up, 1)",
"wheel(down, 1)",
"wheel(left, 1)",
"wheel(right, 1)",
]
for key in extra:
@ -58,20 +65,20 @@ populate_store()
def to_string(key):
"""A nice to show description of the pressed key."""
if isinstance(key, Key):
return ' + '.join([to_string(sub_key) for sub_key in key])
return " + ".join([to_string(sub_key) for sub_key in key])
if isinstance(key[0], tuple):
raise Exception('deprecated stuff')
raise Exception("deprecated stuff")
ev_type, code, value = key
if ev_type not in evdev.ecodes.bytype:
logger.error('Unknown key type for %s', key)
return 'unknown'
logger.error("Unknown key type for %s", key)
return "unknown"
if code not in evdev.ecodes.bytype[ev_type]:
logger.error('Unknown key code for %s', key)
return 'unknown'
logger.error("Unknown key code for %s", key)
return "unknown"
key_name = None
@ -91,60 +98,60 @@ def to_string(key):
if ev_type != evdev.ecodes.EV_KEY:
direction = {
# D-Pad
(evdev.ecodes.ABS_HAT0X, -1): 'Left',
(evdev.ecodes.ABS_HAT0X, 1): 'Right',
(evdev.ecodes.ABS_HAT0Y, -1): 'Up',
(evdev.ecodes.ABS_HAT0Y, 1): 'Down',
(evdev.ecodes.ABS_HAT1X, -1): 'Left',
(evdev.ecodes.ABS_HAT1X, 1): 'Right',
(evdev.ecodes.ABS_HAT1Y, -1): 'Up',
(evdev.ecodes.ABS_HAT1Y, 1): 'Down',
(evdev.ecodes.ABS_HAT2X, -1): 'Left',
(evdev.ecodes.ABS_HAT2X, 1): 'Right',
(evdev.ecodes.ABS_HAT2Y, -1): 'Up',
(evdev.ecodes.ABS_HAT2Y, 1): 'Down',
(evdev.ecodes.ABS_HAT0X, -1): "Left",
(evdev.ecodes.ABS_HAT0X, 1): "Right",
(evdev.ecodes.ABS_HAT0Y, -1): "Up",
(evdev.ecodes.ABS_HAT0Y, 1): "Down",
(evdev.ecodes.ABS_HAT1X, -1): "Left",
(evdev.ecodes.ABS_HAT1X, 1): "Right",
(evdev.ecodes.ABS_HAT1Y, -1): "Up",
(evdev.ecodes.ABS_HAT1Y, 1): "Down",
(evdev.ecodes.ABS_HAT2X, -1): "Left",
(evdev.ecodes.ABS_HAT2X, 1): "Right",
(evdev.ecodes.ABS_HAT2Y, -1): "Up",
(evdev.ecodes.ABS_HAT2Y, 1): "Down",
# joystick
(evdev.ecodes.ABS_X, 1): 'Right',
(evdev.ecodes.ABS_X, -1): 'Left',
(evdev.ecodes.ABS_Y, 1): 'Down',
(evdev.ecodes.ABS_Y, -1): 'Up',
(evdev.ecodes.ABS_RX, 1): 'Right',
(evdev.ecodes.ABS_RX, -1): 'Left',
(evdev.ecodes.ABS_RY, 1): 'Down',
(evdev.ecodes.ABS_RY, -1): 'Up',
(evdev.ecodes.ABS_X, 1): "Right",
(evdev.ecodes.ABS_X, -1): "Left",
(evdev.ecodes.ABS_Y, 1): "Down",
(evdev.ecodes.ABS_Y, -1): "Up",
(evdev.ecodes.ABS_RX, 1): "Right",
(evdev.ecodes.ABS_RX, -1): "Left",
(evdev.ecodes.ABS_RY, 1): "Down",
(evdev.ecodes.ABS_RY, -1): "Up",
# wheel
(evdev.ecodes.REL_WHEEL, -1): 'Down',
(evdev.ecodes.REL_WHEEL, 1): 'Up',
(evdev.ecodes.REL_HWHEEL, -1): 'Left',
(evdev.ecodes.REL_HWHEEL, 1): 'Right',
(evdev.ecodes.REL_WHEEL, -1): "Down",
(evdev.ecodes.REL_WHEEL, 1): "Up",
(evdev.ecodes.REL_HWHEEL, -1): "Left",
(evdev.ecodes.REL_HWHEEL, 1): "Right",
}.get((code, value))
if direction is not None:
key_name += f' {direction}'
key_name += f" {direction}"
key_name = key_name.replace('ABS_Z', 'Trigger Left')
key_name = key_name.replace('ABS_RZ', 'Trigger Right')
key_name = key_name.replace("ABS_Z", "Trigger Left")
key_name = key_name.replace("ABS_RZ", "Trigger Right")
key_name = key_name.replace('ABS_HAT0X', 'DPad')
key_name = key_name.replace('ABS_HAT0Y', 'DPad')
key_name = key_name.replace('ABS_HAT1X', 'DPad 2')
key_name = key_name.replace('ABS_HAT1Y', 'DPad 2')
key_name = key_name.replace('ABS_HAT2X', 'DPad 3')
key_name = key_name.replace('ABS_HAT2Y', 'DPad 3')
key_name = key_name.replace("ABS_HAT0X", "DPad")
key_name = key_name.replace("ABS_HAT0Y", "DPad")
key_name = key_name.replace("ABS_HAT1X", "DPad 2")
key_name = key_name.replace("ABS_HAT1Y", "DPad 2")
key_name = key_name.replace("ABS_HAT2X", "DPad 3")
key_name = key_name.replace("ABS_HAT2Y", "DPad 3")
key_name = key_name.replace('ABS_X', 'Joystick')
key_name = key_name.replace('ABS_Y', 'Joystick')
key_name = key_name.replace('ABS_RX', 'Joystick 2')
key_name = key_name.replace('ABS_RY', 'Joystick 2')
key_name = key_name.replace("ABS_X", "Joystick")
key_name = key_name.replace("ABS_Y", "Joystick")
key_name = key_name.replace("ABS_RX", "Joystick 2")
key_name = key_name.replace("ABS_RY", "Joystick 2")
key_name = key_name.replace('BTN_', 'Button ')
key_name = key_name.replace('KEY_', '')
key_name = key_name.replace("BTN_", "Button ")
key_name = key_name.replace("KEY_", "")
key_name = key_name.replace('REL_', '')
key_name = key_name.replace('HWHEEL', 'Wheel')
key_name = key_name.replace('WHEEL', 'Wheel')
key_name = key_name.replace("REL_", "")
key_name = key_name.replace("HWHEEL", "Wheel")
key_name = key_name.replace("WHEEL", "Wheel")
key_name = key_name.replace('_', ' ')
key_name = key_name.replace(' ', ' ')
key_name = key_name.replace("_", " ")
key_name = key_name.replace(" ", " ")
return key_name
@ -155,7 +162,8 @@ HOLDING = 1
class Row(Gtk.ListBoxRow):
"""A single, configurable key mapping."""
__gtype_name__ = 'ListBoxRow'
__gtype_name__ = "ListBoxRow"
def __init__(self, delete_callback, window, key=None, symbol=None):
"""Construct a row widget.
@ -165,7 +173,7 @@ class Row(Gtk.ListBoxRow):
key : Key
"""
if key is not None and not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
raise TypeError("Expected key to be a Key object")
super().__init__()
self.device = window.group
@ -227,7 +235,7 @@ class Row(Gtk.ListBoxRow):
new_key : Key
"""
if new_key is not None and not isinstance(new_key, Key):
raise TypeError('Expected new_key to be a Key object')
raise TypeError("Expected new_key to be a Key object")
# the newest_keycode is populated since the ui regularly polls it
# in order to display it in the status bar.
@ -267,11 +275,7 @@ class Row(Gtk.ListBoxRow):
return
# else, the keycode has changed, the symbol is set, all good
custom_mapping.change(
new_key=new_key,
symbol=symbol,
previous_key=previous_key
)
custom_mapping.change(new_key=new_key, symbol=symbol, previous_key=previous_key)
def on_symbol_input_change(self, _):
"""When the output symbol for that keycode is typed in."""
@ -282,11 +286,7 @@ class Row(Gtk.ListBoxRow):
return
if key is not None:
custom_mapping.change(
new_key=key,
symbol=symbol,
previous_key=None
)
custom_mapping.change(new_key=key, symbol=symbol, previous_key=None)
def match(self, _, key, tree_iter):
"""Search the avilable names."""
@ -298,7 +298,7 @@ class Row(Gtk.ListBoxRow):
if self.get_key() is not None:
return
self.set_keycode_input_label('click here')
self.set_keycode_input_label("click here")
self.keycode_input.set_opacity(0.3)
def show_press_key(self):
@ -306,7 +306,7 @@ class Row(Gtk.ListBoxRow):
if self.get_key() is not None:
return
self.set_keycode_input_label('press key')
self.set_keycode_input_label("press key")
self.keycode_input.set_opacity(1)
def on_keycode_input_focus(self, *_):
@ -345,14 +345,10 @@ class Row(Gtk.ListBoxRow):
def put_together(self, symbol):
"""Create all child GTK widgets and connect their signals."""
delete_button = Gtk.EventBox()
delete_button.add(Gtk.Image.new_from_icon_name(
'window-close',
Gtk.IconSize.BUTTON
))
delete_button.connect(
'button-press-event',
self.on_delete_button_clicked
delete_button.add(
Gtk.Image.new_from_icon_name("window-close", Gtk.IconSize.BUTTON)
)
delete_button.connect("button-press-event", self.on_delete_button_clicked)
delete_button.set_size_request(50, -1)
keycode_input = Gtk.ToggleButton()
@ -366,20 +362,11 @@ class Row(Gtk.ListBoxRow):
# make the togglebutton go back to its normal state when doing
# something else in the UI
keycode_input.connect(
'focus-in-event',
self.on_keycode_input_focus
)
keycode_input.connect(
'focus-out-event',
self.on_keycode_input_unfocus
)
keycode_input.connect("focus-in-event", self.on_keycode_input_focus)
keycode_input.connect("focus-out-event", self.on_keycode_input_unfocus)
# don't leave the input when using arrow keys or tab. wait for the
# window to consume the keycode from the reader
keycode_input.connect(
'key-press-event',
lambda *args: Gdk.EVENT_STOP
)
keycode_input.connect("key-press-event", lambda *args: Gdk.EVENT_STOP)
symbol_input = Gtk.Entry()
self.symbol_input = symbol_input
@ -395,14 +382,8 @@ class Row(Gtk.ListBoxRow):
if symbol is not None:
symbol_input.set_text(symbol)
symbol_input.connect(
'changed',
self.on_symbol_input_change
)
symbol_input.connect(
'focus-out-event',
self.on_symbol_input_unfocus
)
symbol_input.connect("changed", self.on_symbol_input_change)
symbol_input.connect("focus-out-event", self.on_symbol_input_unfocus)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.set_homogeneous(False)
@ -411,7 +392,7 @@ class Row(Gtk.ListBoxRow):
box.pack_start(symbol_input, expand=True, fill=True, padding=0)
box.pack_start(delete_button, expand=False, fill=True, padding=0)
box.show_all()
box.get_style_context().add_class('row-box')
box.get_style_context().add_class("row-box")
self.add(box)
self.show_all()
@ -422,7 +403,7 @@ class Row(Gtk.ListBoxRow):
if key is not None:
custom_mapping.clear(key)
self.symbol_input.set_text('')
self.set_keycode_input_label('')
self.symbol_input.set_text("")
self.set_keycode_input_label("")
self.key = None
self.delete_callback(self)

@ -30,13 +30,25 @@ from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path
from keymapper.paths import get_config_path
from keymapper.state import custom_mapping, system_mapping
from keymapper.presets import find_newest_preset, get_presets, \
delete_preset, rename_preset, get_available_preset_name
from keymapper.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION, \
is_debug
from keymapper.groups import groups, GAMEPAD, KEYBOARD, UNKNOWN, \
GRAPHICS_TABLET, TOUCHPAD, MOUSE
from keymapper.system_mapping import system_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.presets import (
find_newest_preset,
get_presets,
delete_preset,
rename_preset,
get_available_preset_name,
)
from keymapper.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION, is_debug
from keymapper.groups import (
groups,
GAMEPAD,
KEYBOARD,
UNKNOWN,
GRAPHICS_TABLET,
TOUCHPAD,
MOUSE,
)
from keymapper.gui.row import Row, to_string
from keymapper.key import Key
from keymapper.gui.reader import reader
@ -44,7 +56,7 @@ from keymapper.gui.helper import is_helper_running
from keymapper.injection.injector import RUNNING, FAILED, NO_GRAB
from keymapper.daemon import Daemon
from keymapper.config import config
from keymapper.injection.macros import is_this_a_macro, parse
from keymapper.injection.macros.parse import is_this_a_macro, parse
def gtk_iteration():
@ -63,18 +75,16 @@ CONTINUE = True
GO_BACK = False
ICON_NAMES = {
GAMEPAD: 'input-gaming',
MOUSE: 'input-mouse',
KEYBOARD: 'input-keyboard',
GRAPHICS_TABLET: 'input-tablet',
TOUCHPAD: 'input-touchpad',
GAMEPAD: "input-gaming",
MOUSE: "input-mouse",
KEYBOARD: "input-keyboard",
GRAPHICS_TABLET: "input-tablet",
TOUCHPAD: "input-touchpad",
UNKNOWN: None,
}
# sort types that most devices would fall in easily to the right.
ICON_PRIORITIES = [
GRAPHICS_TABLET, TOUCHPAD, GAMEPAD, MOUSE, KEYBOARD, UNKNOWN
]
ICON_PRIORITIES = [GRAPHICS_TABLET, TOUCHPAD, GAMEPAD, MOUSE, KEYBOARD, UNKNOWN]
def with_group(func):
@ -106,6 +116,7 @@ class HandlerDisabled:
Use in a with statement.
"""
def __init__(self, widget, handler):
self.widget = widget
self.handler = handler
@ -125,6 +136,7 @@ def on_close_about(about, _):
class Window:
"""User Interface."""
def __init__(self):
self.dbus = None
@ -134,16 +146,16 @@ class Window:
self.preset_name = None
css_provider = Gtk.CssProvider()
with open(get_data_path('style.css'), 'r') as file:
css_provider.load_from_data(bytes(file.read(), encoding='UTF-8'))
with open(get_data_path("style.css"), "r") as file:
css_provider.load_from_data(bytes(file.read(), encoding="UTF-8"))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
gladefile = get_data_path('key-mapper.glade')
gladefile = get_data_path("key-mapper.glade")
builder = Gtk.Builder()
builder.add_from_file(gladefile)
builder.connect_signals(self)
@ -151,7 +163,7 @@ class Window:
# set up the device selection
# https://python-gtk-3-tutorial.readthedocs.io/en/latest/treeview.html#the-view
combobox = self.get('device_selection')
combobox = self.get("device_selection")
self.device_store = Gtk.ListStore(str, str, str)
combobox.set_model(self.device_store)
renderer_icon = Gtk.CellRendererPixbuf()
@ -159,26 +171,27 @@ class Window:
renderer_text.set_padding(5, 0)
combobox.pack_start(renderer_icon, False)
combobox.pack_start(renderer_text, False)
combobox.add_attribute(renderer_icon, 'icon-name', 1)
combobox.add_attribute(renderer_text, 'text', 2)
combobox.add_attribute(renderer_icon, "icon-name", 1)
combobox.add_attribute(renderer_text, "text", 2)
combobox.set_id_column(0)
self.confirm_delete = builder.get_object('confirm-delete')
self.about = builder.get_object('about-dialog')
self.about.connect('delete-event', on_close_about)
self.confirm_delete = builder.get_object("confirm-delete")
self.about = builder.get_object("about-dialog")
self.about.connect("delete-event", on_close_about)
# set_position needs to be done once initially, otherwise the
# dialog is not centered when it is opened for the first time
self.about.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.get('version-label').set_text(
f'key-mapper {VERSION} {COMMIT_HASH[:7]}'
f'\npython-evdev {EVDEV_VERSION}' if EVDEV_VERSION else ''
self.get("version-label").set_text(
f"key-mapper {VERSION} {COMMIT_HASH[:7]}" f"\npython-evdev {EVDEV_VERSION}"
if EVDEV_VERSION
else ""
)
window = self.get('window')
window = self.get("window")
window.show()
# hide everything until stuff is populated
self.get('vertical-wrapper').set_opacity(0)
self.get("vertical-wrapper").set_opacity(0)
self.window = window
# if any of the next steps take a bit to complete, have the window
@ -187,8 +200,8 @@ class Window:
# this is not set to invisible in glade to give the ui a default
# height that doesn't jump when a gamepad is selected
self.get('gamepad_separator').hide()
self.get('gamepad_config').hide()
self.get("gamepad_separator").hide()
self.get("gamepad_config").hide()
self.populate_devices()
@ -196,20 +209,20 @@ class Window:
self.setup_timeouts()
# now show the proper finished content of the window
self.get('vertical-wrapper').set_opacity(1)
self.get("vertical-wrapper").set_opacity(1)
self.ctrl = False
self.unreleased_warn = False
self.button_left_warn = False
if not is_helper_running():
self.show_status(CTX_ERROR, 'The helper did not start')
self.show_status(CTX_ERROR, "The helper did not start")
def setup_timeouts(self):
"""Setup all GLib timeouts."""
self.timeouts = [
GLib.timeout_add(100, self.check_add_row),
GLib.timeout_add(1000 / 30, self.consume_newest_keycode)
GLib.timeout_add(1000 / 30, self.consume_newest_keycode),
]
def start_processes(self):
@ -217,20 +230,20 @@ class Window:
# this function is overwritten in tests
self.dbus = Daemon.connect()
debug = ' -d' if is_debug() else ''
cmd = f'pkexec key-mapper-control --command helper {debug}'
debug = " -d" if is_debug() else ""
cmd = f"pkexec key-mapper-control --command helper {debug}"
logger.debug('Running `%s`', cmd)
logger.debug("Running `%s`", cmd)
exit_code = os.system(cmd)
if exit_code != 0:
logger.error('Failed to pkexec the helper, code %d', exit_code)
logger.error("Failed to pkexec the helper, code %d", exit_code)
sys.exit()
def show_confirm_delete(self):
"""Blocks until the user decided about an action."""
text = f'Are you sure to delete preset "{self.preset_name}"?'
self.get('confirm-delete-label').set_text(text)
self.get("confirm-delete-label").set_text(text)
self.confirm_delete.show()
response = self.confirm_delete.run()
@ -275,27 +288,27 @@ class Window:
def initialize_gamepad_config(self):
"""Set slider and dropdown values when a gamepad is selected."""
if GAMEPAD in self.group.types:
self.get('gamepad_separator').show()
self.get('gamepad_config').show()
self.get("gamepad_separator").show()
self.get("gamepad_config").show()
else:
self.get('gamepad_separator').hide()
self.get('gamepad_config').hide()
self.get("gamepad_separator").hide()
self.get("gamepad_config").hide()
return
left_purpose = self.get('left_joystick_purpose')
right_purpose = self.get('right_joystick_purpose')
speed = self.get('joystick_mouse_speed')
left_purpose = self.get("left_joystick_purpose")
right_purpose = self.get("right_joystick_purpose")
speed = self.get("joystick_mouse_speed")
with HandlerDisabled(left_purpose, self.on_left_joystick_changed):
value = custom_mapping.get('gamepad.joystick.left_purpose')
value = custom_mapping.get("gamepad.joystick.left_purpose")
left_purpose.set_active_id(value)
with HandlerDisabled(right_purpose, self.on_right_joystick_changed):
value = custom_mapping.get('gamepad.joystick.right_purpose')
value = custom_mapping.get("gamepad.joystick.right_purpose")
right_purpose.set_active_id(value)
with HandlerDisabled(speed, self.on_joystick_mouse_speed_changed):
value = custom_mapping.get('gamepad.joystick.pointer_speed')
value = custom_mapping.get("gamepad.joystick.pointer_speed")
range_value = math.log(value, 2)
speed.set_value(range_value)
@ -305,7 +318,7 @@ class Window:
def on_close(self, *_):
"""Safely close the application."""
logger.debug('Closing window')
logger.debug("Closing window")
self.save_preset()
self.window.hide()
for timeout in self.timeouts:
@ -316,7 +329,7 @@ class Window:
def check_add_row(self):
"""Ensure that one empty row is available at all times."""
rows = self.get('key_list').get_children()
rows = self.get("key_list").get_children()
# verify that all mappings are displayed.
# One of them is possibly the empty row
@ -324,17 +337,13 @@ class Window:
num_maps = len(custom_mapping)
if num_rows < num_maps or num_rows > num_maps + 1:
logger.error(
'custom_mapping contains %d rows, '
'but %d are displayed',
len(custom_mapping), num_rows
"custom_mapping contains %d rows, " "but %d are displayed",
len(custom_mapping),
num_rows,
)
logger.spam("Mapping %s", list(custom_mapping))
logger.spam(
'Mapping %s',
list(custom_mapping)
)
logger.spam(
'Rows %s',
[(row.get_key(), row.get_symbol()) for row in rows]
"Rows %s", [(row.get_key(), row.get_symbol()) for row in rows]
)
# iterating over that 10 times per second is a bit wasteful,
@ -354,13 +363,13 @@ class Window:
device, preset = find_newest_preset()
group = groups.find(name=device)
if device is not None:
self.get('device_selection').set_active_id(group.key)
self.get("device_selection").set_active_id(group.key)
if preset is not None:
self.get('preset_selection').set_active_id(preset)
self.get("preset_selection").set_active_id(preset)
def populate_devices(self):
"""Make the devices selectable."""
device_selection = self.get('device_selection')
device_selection = self.get("device_selection")
with HandlerDisabled(device_selection, self.on_select_device):
self.device_store.clear()
@ -391,12 +400,9 @@ class Window:
custom_mapping.save(path)
presets = [new_preset]
else:
logger.debug(
'"%s" presets: "%s"',
self.group.name, '", "'.join(presets)
)
logger.debug('"%s" presets: "%s"', self.group.name, '", "'.join(presets))
preset_selection = self.get('preset_selection')
preset_selection = self.get("preset_selection")
with HandlerDisabled(preset_selection, self.on_select_preset):
# otherwise the handler is called with None for each preset
@ -409,7 +415,7 @@ class Window:
def clear_mapping_table(self):
"""Remove all rows from the mappings table."""
key_list = self.get('key_list')
key_list = self.get("key_list")
key_list.forall(key_list.remove)
custom_mapping.empty()
@ -420,11 +426,8 @@ class Window:
# because the device is in grab mode by the daemon and
# therefore the original keycode inaccessible
logger.info('Cannot change keycodes while injecting')
self.show_status(
CTX_ERROR,
'Use "Restore Defaults" to stop before editing'
)
logger.info("Cannot change keycodes while injecting")
self.show_status(CTX_ERROR, 'Use "Restore Defaults" to stop before editing')
def get_focused_row(self):
"""Get the Row and its child that is currently in focus."""
@ -467,10 +470,10 @@ class Window:
if key.is_problematic() and isinstance(focused, Gtk.ToggleButton):
self.show_status(
CTX_WARNING,
'ctrl, alt and shift may not combine properly',
'Your system might reinterpret combinations ' +
'with those after they are injected, and by doing so ' +
'break them.'
"ctrl, alt and shift may not combine properly",
"Your system might reinterpret combinations "
+ "with those after they are injected, and by doing so "
+ "break them.",
)
if row is not None:
@ -482,7 +485,7 @@ class Window:
def on_restore_defaults_clicked(self, *_):
"""Stop injecting the mapping."""
self.dbus.stop_injecting(self.group.key)
self.show_status(CTX_APPLY, 'Applied the system default')
self.show_status(CTX_APPLY, "Applied the system default")
GLib.timeout_add(100, self.show_device_mapping_status)
def show_status(self, context_id, message, tooltip=None):
@ -491,33 +494,33 @@ class Window:
If message is None, it will remove the newest message of the
given context_id.
"""
status_bar = self.get('status_bar')
status_bar = self.get("status_bar")
if message is None:
status_bar.remove_all(context_id)
if context_id in (CTX_ERROR, CTX_MAPPING):
self.get('error_status_icon').hide()
self.get("error_status_icon").hide()
if context_id == CTX_WARNING:
self.get('warning_status_icon').hide()
self.get("warning_status_icon").hide()
status_bar.set_tooltip_text('')
status_bar.set_tooltip_text("")
else:
if tooltip is None:
tooltip = message
self.get('error_status_icon').hide()
self.get('warning_status_icon').hide()
self.get("error_status_icon").hide()
self.get("warning_status_icon").hide()
if context_id in (CTX_ERROR, CTX_MAPPING):
self.get('error_status_icon').show()
self.get("error_status_icon").show()
if context_id == CTX_WARNING:
self.get('warning_status_icon').show()
self.get("warning_status_icon").show()
if len(message) > 55:
message = message[:52] + '...'
message = message[:52] + "..."
status_bar.push(context_id, message)
status_bar.set_tooltip_text(tooltip)
@ -534,34 +537,27 @@ class Window:
continue
position = to_string(key)
msg = f'Syntax error at {position}, hover for info'
msg = f"Syntax error at {position}, hover for info"
self.show_status(CTX_MAPPING, msg, error)
def on_rename_button_clicked(self, _):
"""Rename the preset based on the contents of the name input."""
new_name = self.get('preset_name_input').get_text()
new_name = self.get("preset_name_input").get_text()
if new_name in ['', self.preset_name]:
if new_name in ["", self.preset_name]:
return
self.save_preset()
new_name = rename_preset(
self.group.name,
self.preset_name,
new_name
)
new_name = rename_preset(self.group.name, self.preset_name, new_name)
# if the old preset was being autoloaded, change the
# name there as well
is_autoloaded = config.is_autoloaded(
self.group.key,
self.preset_name
)
is_autoloaded = config.is_autoloaded(self.group.key, self.preset_name)
if is_autoloaded:
config.set_autoload_preset(self.group.key, new_name)
self.get('preset_name_input').set_text('')
self.get("preset_name_input").set_text("")
self.populate_presets()
@with_preset_name
@ -581,34 +577,28 @@ class Window:
self.save_preset()
if custom_mapping.num_saved_keys == 0:
logger.error('Cannot apply empty preset file')
logger.error("Cannot apply empty preset file")
# also helpful for first time use
if custom_mapping.changed:
self.show_status(
CTX_ERROR,
'You need to save your changes first',
'No mappings are stored in the preset .json file yet'
"You need to save your changes first",
"No mappings are stored in the preset .json file yet",
)
else:
self.show_status(
CTX_ERROR,
'You need to add keys and save first'
)
self.show_status(CTX_ERROR, "You need to add keys and save first")
return
preset = self.preset_name
logger.info(
'Applying preset "%s" for "%s"',
preset, self.group.key
)
logger.info('Applying preset "%s" for "%s"', preset, self.group.key)
if not self.button_left_warn:
if custom_mapping.dangerously_mapped_btn_left():
self.show_status(
CTX_ERROR,
'This would disable your click button',
'Map a button to BTN_LEFT to avoid this.\n'
'To overwrite this warning, press apply again.'
"This would disable your click button",
"Map a button to BTN_LEFT to avoid this.\n"
"To overwrite this warning, press apply again.",
)
self.button_left_warn = True
return
@ -619,14 +609,13 @@ class Window:
# it's super annoying if that happens and may break the user
# input in such a way to prevent disabling the mapping
logger.error(
'Tried to apply a preset while keys were held down: %s',
unreleased
"Tried to apply a preset while keys were held down: %s", unreleased
)
self.show_status(
CTX_ERROR,
'Please release your pressed keys first',
'X11 will think they are held down forever otherwise.\n'
'To overwrite this warning, press apply again.'
"Please release your pressed keys first",
"X11 will think they are held down forever otherwise.\n"
"To overwrite this warning, press apply again.",
)
self.unreleased_warn = True
return
@ -636,10 +625,7 @@ class Window:
self.dbus.set_config_dir(get_config_path())
self.dbus.start_injecting(self.group.key, preset)
self.show_status(
CTX_APPLY,
'Starting injection...'
)
self.show_status(CTX_APPLY, "Starting injection...")
GLib.timeout_add(100, self.show_injection_result)
@ -687,7 +673,7 @@ class Window:
msg = f'Applied preset "{self.preset_name}"'
if custom_mapping.get_symbol(Key.btn_left()):
msg += ', CTRL + DEL to stop'
msg += ", CTRL + DEL to stop"
self.show_status(CTX_APPLY, msg)
@ -695,19 +681,16 @@ class Window:
return False
if state == FAILED:
self.show_status(
CTX_ERROR,
f'Failed to apply preset "{self.preset_name}"'
)
self.show_status(CTX_ERROR, f'Failed to apply preset "{self.preset_name}"')
return False
if state == NO_GRAB:
self.show_status(
CTX_ERROR,
'The device was not grabbed',
'Either another application is already grabbing it or '
'your preset doesn\'t contain anything that is sent by the '
'device.'
"The device was not grabbed",
"Either another application is already grabbing it or "
"your preset doesn't contain anything that is sent by the "
"device.",
)
return False
@ -720,9 +703,9 @@ class Window:
state = self.dbus.get_state(group_key)
if state == RUNNING:
logger.info('Group "%s" is currently mapped', group_key)
self.get('apply_system_layout').set_opacity(1)
self.get("apply_system_layout").set_opacity(1)
else:
self.get('apply_system_layout').set_opacity(0.4)
self.get("apply_system_layout").set_opacity(0.4)
@with_preset_name
def on_copy_preset_clicked(self, _):
@ -749,11 +732,11 @@ class Window:
path = self.group.get_preset_path(new_preset)
custom_mapping.save(path)
self.get('preset_selection').append(new_preset, new_preset)
self.get('preset_selection').set_active_id(new_preset)
self.get("preset_selection").append(new_preset, new_preset)
self.get("preset_selection").set_active_id(new_preset)
except PermissionError as error:
error = str(error)
self.show_status(CTX_ERROR, 'Permission denied!', error)
self.show_status(CTX_ERROR, "Permission denied!", error)
logger.error(error)
def on_select_preset(self, dropdown):
@ -776,25 +759,21 @@ class Window:
custom_mapping.load(self.group.get_preset_path(preset))
key_list = self.get('key_list')
key_list = self.get("key_list")
for key, output in custom_mapping:
single_key_mapping = Row(
window=self,
delete_callback=self.on_row_removed,
key=key,
symbol=output
window=self, delete_callback=self.on_row_removed, key=key, symbol=output
)
key_list.insert(single_key_mapping, -1)
autoload_switch = self.get('preset_autoload_switch')
autoload_switch = self.get("preset_autoload_switch")
with HandlerDisabled(autoload_switch, self.on_autoload_switch):
autoload_switch.set_active(config.is_autoloaded(
self.group.key,
self.preset_name
))
autoload_switch.set_active(
config.is_autoloaded(self.group.key, self.preset_name)
)
self.get('preset_name_input').set_text('')
self.get("preset_name_input").set_text("")
self.add_empty()
self.initialize_gamepad_config()
@ -804,24 +783,24 @@ class Window:
def on_left_joystick_changed(self, dropdown):
"""Set the purpose of the left joystick."""
purpose = dropdown.get_active_id()
custom_mapping.set('gamepad.joystick.left_purpose', purpose)
custom_mapping.set("gamepad.joystick.left_purpose", purpose)
self.save_preset()
def on_right_joystick_changed(self, dropdown):
"""Set the purpose of the right joystick."""
purpose = dropdown.get_active_id()
custom_mapping.set('gamepad.joystick.right_purpose', purpose)
custom_mapping.set("gamepad.joystick.right_purpose", purpose)
self.save_preset()
def on_joystick_mouse_speed_changed(self, gtk_range):
"""Set how fast the joystick moves the mouse."""
speed = 2 ** gtk_range.get_value()
custom_mapping.set('gamepad.joystick.pointer_speed', speed)
custom_mapping.set("gamepad.joystick.pointer_speed", speed)
def add_empty(self):
"""Add one empty row for a single mapped key."""
empty = Row(window=self, delete_callback=self.on_row_removed)
key_list = self.get('key_list')
key_list = self.get("key_list")
key_list.insert(empty, -1)
def on_row_removed(self, single_key_mapping):
@ -831,7 +810,7 @@ class Window:
----------
single_key_mapping : Row
"""
key_list = self.get('key_list')
key_list = self.get("key_list")
# https://stackoverflow.com/a/30329591/4417769
key_list.remove(single_key_mapping)
@ -852,7 +831,7 @@ class Window:
self.populate_presets()
except PermissionError as error:
error = str(error)
self.show_status(CTX_ERROR, 'Permission denied!', error)
self.show_status(CTX_ERROR, "Permission denied!", error)
logger.error(error)
for _, symbol in custom_mapping:

@ -0,0 +1,27 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""The injection process.
This folder contains all classes that are only relevant for the injection
process. There is one process for each hardware device that is being injected
for, and one context object per process that is being passed around for all
classes to use.
"""

@ -0,0 +1,118 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Because multiple calls to async_read_loop won't work."""
import asyncio
import evdev
from keymapper.injection.consumers.event_producer import EventProducer
from keymapper.injection.consumers.keycode_mapper import KeycodeMapper
from keymapper.logger import logger
consumer_classes = [
KeycodeMapper,
EventProducer,
]
class ConsumerControl:
"""Reads input events from a single device and distributes them.
There is one Events object for each source, which tells multiple consumers
that a new event is ready so that they can inject all sorts of funny
things.
Other devnodes may be present for the hardware device, in which case this
needs to be created multiple times.
"""
def __init__(self, context, source, forward_to):
"""Initialize all consumers
Parameters
----------
source : evdev.InputDevice
where to read keycodes from
forward_to : evdev.UInput
where to write keycodes to that were not mapped to anything.
Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source.
"""
self._source = source
self._forward_to = forward_to
# add all consumers that are enabled for this particular configuration
self._consumers = []
for Consumer in consumer_classes:
consumer = Consumer(context, source, forward_to)
if consumer.is_enabled():
self._consumers.append(consumer)
async def run(self):
"""Start doing things.
Can be stopped by stopping the asyncio loop. This loop
reads events from a single device only.
"""
for consumer in self._consumers:
# run all of them in parallel
asyncio.ensure_future(consumer.run())
logger.debug(
"Starting to listen for events from %s, fd %s",
self._source.path,
self._source.fd,
)
async for event in self._source.async_read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 2:
# button-hold event. Environments (gnome, etc.) create them on
# their own for the injection-fake-device if the release event
# won't appear, no need to forward or map them.
continue
handled = False
for consumer in self._consumers:
# copy so that the consumer doesn't screw this up for
# all other future consumers
event = evdev.InputEvent(
sec=event.sec,
usec=event.usec,
type=event.type,
code=event.code,
value=event.value,
)
if consumer.is_handled(event):
await consumer.notify(event)
handled = True
if not handled:
# forward the rest
self._forward_to.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
# This happens all the time in tests because the async_read_loop stops when
# there is nothing to read anymore. Otherwise tests would block.
logger.error('The async_read_loop for "%s" stopped early', self._source.path)

@ -0,0 +1,24 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Consumers
Each consumer can listen for events and then inject something mapped.
"""

@ -0,0 +1,88 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Consumer base class.
Can be notified of new events so that inheriting classes can map them and
inject new events based on them.
"""
class Consumer:
"""Can be notified of new events to inject them. Base class."""
def __init__(self, context, source, forward_to=None):
"""Initialize event consuming functionality.
Parameters
----------
context : Context
The configuration of the Injector process
source : InputDevice
Where events used in handle_keycode come from
forward_to : evdev.UInput
Where to write keycodes to that were not mapped to anything.
Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source.
"""
self.context = context
self.forward_to = forward_to
self.source = source
self.context.update_purposes()
def is_enabled(self):
"""Check if the consumer will have work to do."""
raise NotImplementedError
def write(self, key):
"""Shorthand to write stuff."""
self.context.uinput.write(*key)
self.context.uinput.syn()
def forward(self, key):
"""Shorthand to forward an event."""
self.forward_to.write(*key)
async def notify(self, event):
"""A new event is ready.
Overwrite this function if the consumer should do something each time
a new event arrives. E.g. mapping a single button once clicked.
"""
raise NotImplementedError
def is_handled(self, event):
"""Check if the consumer will take care of this event.
If this returns true, the event will not be forwarded anymore
automatically. If you want to forward the event after all you can
inject it into `self.forward_to`.
"""
raise NotImplementedError
async def run(self):
"""Start doing things.
Overwrite this function if the consumer should do something
continuously even if no new event arrives. e.g. continuously injecting
mouse movement events.
"""
raise NotImplementedError

@ -25,12 +25,24 @@
import asyncio
import time
from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, \
EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY
from evdev.ecodes import (
EV_REL,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
EV_ABS,
ABS_X,
ABS_Y,
ABS_RX,
ABS_RY,
)
from keymapper.logger import logger
from keymapper.config import MOUSE, WHEEL
from keymapper import utils
from keymapper.injection.consumers.consumer import Consumer
from keymapper.groups import classify, GAMEPAD
# miniscule movements on the joystick should not trigger a mouse wheel event
WHEEL_THRESHOLD = 0.15
@ -43,35 +55,31 @@ def abs_max(value_1, value_2):
return value_2
class EventProducer:
class EventProducer(Consumer):
"""Keeps producing events at 60hz if needed.
Can debounce arbitrary functions. Maps joysticks to mouse movements.
Maps joysticks to mouse movements.
This class does not handle injecting macro stuff over time, that is done
by the keycode_mapper.
"""
def __init__(self, context):
def __init__(self, *args, **kwargs):
"""Construct the event producer without it doing anything yet."""
self.context = context
super().__init__(*args, **kwargs)
self._abs_range = None
self._set_abs_range_from(self.source)
self.abs_range = None
# events only take ints, so a movement of 0.3 needs to add
# up to 1.2 to affect the cursor, with 0.2 remaining
self.pending_rel = {REL_X: 0, REL_Y: 0, REL_WHEEL: 0, REL_HWHEEL: 0}
# the last known position of the joystick
self.abs_state = {ABS_X: 0, ABS_Y: 0, ABS_RX: 0, ABS_RY: 0}
self.debounces = {}
def notify(self, event):
"""Tell the EventProducer about the newest ABS event.
Afterwards, it can continue moving the mouse pointer in the
correct direction.
"""
if event.type == EV_ABS and event.code in self.abs_state:
self.abs_state[event.code] = event.value
def is_enabled(self):
gamepad = classify(self.source) == GAMEPAD
return gamepad and self.context.joystick_as_mouse()
def _write(self, ev_type, keycode, value):
"""Inject."""
@ -82,23 +90,7 @@ class EventProducer:
self.context.uinput.syn()
except OverflowError:
# screwed up the calculation of mouse movements
logger.error('OverflowError (%s, %s, %s)', ev_type, keycode, value)
def debounce(self, debounce_id, func, args, ticks):
"""Debounce a function call.
Parameters
----------
debounce_id : hashable
If this function is called with the same debounce_id again,
the previous debouncing is overwritten, and there fore restarted.
func : function
args : tuple
ticks : int
After ticks * 1 / 60 seconds the function will be executed,
unless debounce is called again with the same debounce_id
"""
self.debounces[debounce_id] = [func, args, ticks]
logger.error("OverflowError (%s, %s, %s)", ev_type, keycode, value)
def accumulate(self, code, input_value):
"""Since devices can't do float values, stuff has to be accumulated.
@ -112,14 +104,14 @@ class EventProducer:
self.pending_rel[code] -= output_value
return output_value
def set_abs_range_from(self, device):
def _set_abs_range_from(self, device):
"""Update the min and max values joysticks will report.
This information is needed for abs -> rel mapping.
"""
if device is None:
# I don't think this ever happened
logger.error('Expected device to not be None')
logger.error("Expected device to not be None")
return
abs_range = utils.get_abs_range(device)
@ -138,16 +130,11 @@ class EventProducer:
This information is needed for abs -> rel mapping.
"""
self.abs_range = (min_abs, max_abs)
self._abs_range = (min_abs, max_abs)
# all joysticks in resting position by default
center = (self.abs_range[1] + self.abs_range[0]) / 2
self.abs_state = {
ABS_X: center,
ABS_Y: center,
ABS_RX: center,
ABS_RY: center
}
center = (self._abs_range[1] + self._abs_range[0]) / 2
self.abs_state = {ABS_X: center, ABS_Y: center, ABS_RX: center, ABS_RY: center}
def get_abs_values(self):
"""Get the raw values for wheel and mouse movement.
@ -158,9 +145,9 @@ class EventProducer:
absolute values takes over the control.
"""
# center is the value of the resting position
center = (self.abs_range[1] + self.abs_range[0]) / 2
center = (self._abs_range[1] + self._abs_range[0]) / 2
# normalizer is the maximum possible value after centering
normalizer = (self.abs_range[1] - self.abs_range[0]) / 2
normalizer = (self._abs_range[1] - self._abs_range[0]) / 2
mouse_x = 0
mouse_y = 0
@ -195,7 +182,7 @@ class EventProducer:
if event.type != EV_ABS or event.code not in utils.JOYSTICK:
return False
if self.abs_range is None:
if self._abs_range is None:
return False
purposes = [MOUSE, WHEEL]
@ -210,48 +197,38 @@ class EventProducer:
return False
async def notify(self, event):
if event.type == EV_ABS and event.code in self.abs_state:
self.abs_state[event.code] = event.value
async def run(self):
"""Keep writing mouse movements based on the gamepad stick position.
Even if no new input event arrived because the joystick remained at
its position, this will keep injecting the mouse movement events.
"""
abs_range = self.abs_range
abs_range = self._abs_range
mapping = self.context.mapping
pointer_speed = mapping.get('gamepad.joystick.pointer_speed')
non_linearity = mapping.get('gamepad.joystick.non_linearity')
x_scroll_speed = mapping.get('gamepad.joystick.x_scroll_speed')
y_scroll_speed = mapping.get('gamepad.joystick.y_scroll_speed')
pointer_speed = mapping.get("gamepad.joystick.pointer_speed")
non_linearity = mapping.get("gamepad.joystick.non_linearity")
x_scroll_speed = mapping.get("gamepad.joystick.x_scroll_speed")
y_scroll_speed = mapping.get("gamepad.joystick.y_scroll_speed")
max_speed = 2 ** 0.5 # for normalized abs event values
if abs_range is not None:
logger.info(
'Left joystick as %s, right joystick as %s',
"Left joystick as %s, right joystick as %s",
self.context.left_purpose,
self.context.right_purpose
self.context.right_purpose,
)
start = time.time()
while True:
# production loop. try to do this as close to 60hz as possible
# try to do this as close to 60hz as possible
time_taken = time.time() - start
await asyncio.sleep(max(0.0, (1 / 60) - time_taken))
start = time.time()
"""handling debounces"""
for debounce in self.debounces.values():
if debounce[2] == -1:
# has already been triggered
continue
if debounce[2] == 0:
debounce[0](*debounce[1])
debounce[2] = -1
else:
debounce[2] -= 1
"""mouse movement production"""
if abs_range is None:
# no ev_abs events will be mapped to ev_rel
continue
@ -259,7 +236,7 @@ class EventProducer:
abs_values = self.get_abs_values()
if len([val for val in abs_values if not -1 <= val <= 1]) > 0:
logger.error('Inconsistent values: %s', abs_values)
logger.error("Inconsistent values: %s", abs_values)
continue
mouse_x, mouse_y, wheel_x, wheel_y = abs_values

@ -24,13 +24,18 @@
import itertools
import asyncio
import time
import evdev
from evdev.ecodes import EV_KEY, EV_ABS
from evdev import ecodes
from keymapper.logger import logger
from keymapper.mapping import DISABLE_CODE
from keymapper import utils
from keymapper.injection.consumers.consumer import Consumer
from keymapper.utils import RELEASE
from keymapper.groups import classify, GAMEPAD
# this state is shared by all KeycodeMappers of this process
@ -63,16 +68,6 @@ active_macros = {}
unreleased = {}
def is_key_down(value):
"""Is this event value a key press."""
return value != 0
def is_key_up(value):
"""Is this event value a key release."""
return value == 0
COMBINATION_INCOMPLETE = 1 # not all keys of the combination are pressed
NOT_COMBINED = 2 # this key is not part of a combination
@ -88,23 +83,25 @@ def subsets(combination):
Parameters
-----------
combination : tuple
tuple of 3-tuples, each being int, int, int (type, code, value)
tuple of 3-tuples, each being int, int, int (type, code, action)
"""
combination = list(combination)
lengths = list(range(2, len(combination) + 1))
lengths.reverse()
return list(itertools.chain.from_iterable(
itertools.combinations(combination, length)
for length in lengths
))
return list(
itertools.chain.from_iterable(
itertools.combinations(combination, length) for length in lengths
)
)
class Unreleased:
"""This represents a key that has been pressed but not released yet."""
__slots__ = (
'target_type_code',
'input_event_tuple',
'triggered_key',
"target_type_code",
"input_event_tuple",
"triggered_key",
)
def __init__(self, target_type_code, input_event_tuple, triggered_key):
@ -114,7 +111,7 @@ class Unreleased:
target_type_code : 2-tuple
int type and int code of what was injected or forwarded
input_event_tuple : 3-tuple
the original event, int, int, int / type, code, value
int, int, int / type, code, action
triggered_key : tuple of 3-tuples
What was used to index key_to_code or macros when stuff
was triggered.
@ -125,13 +122,10 @@ class Unreleased:
self.input_event_tuple = input_event_tuple
self.triggered_key = triggered_key
if (
not isinstance(input_event_tuple[0], int) or
len(input_event_tuple) != 3
):
if not isinstance(input_event_tuple[0], int) or len(input_event_tuple) != 3:
raise ValueError(
'Expected input_event_tuple to be a 3-tuple of ints, but '
f'got {input_event_tuple}'
"Expected input_event_tuple to be a 3-tuple of ints, but "
f"got {input_event_tuple}"
)
unreleased[input_event_tuple[:2]] = self
@ -147,11 +141,11 @@ class Unreleased:
def __str__(self):
return (
'Unreleased('
f'target{self.target_type_code},'
f'input{self.input_event_tuple},'
"Unreleased("
f"target{self.target_type_code},"
f"input{self.input_event_tuple},"
f'key{self.triggered_key or "(None)"}'
')'
")"
)
def __repr__(self):
@ -182,7 +176,8 @@ def find_by_key(key):
Parameters
----------
key : tuple of 3-tuples
key : tuple of int
type, code, action
"""
unreleased_entry = unreleased.get(key[-1][:2])
if unreleased_entry and unreleased_entry.triggered_key == key:
@ -193,63 +188,140 @@ def find_by_key(key):
def print_unreleased():
"""For debugging purposes."""
logger.debug('unreleased:')
logger.debug('\n'.join([
f' {key}: {str(value)}' for key, value in unreleased.items()
]))
logger.debug("unreleased:")
logger.debug(
"\n".join([f" {key}: {str(value)}" for key, value in unreleased.items()])
)
class KeycodeMapper(Consumer):
"""Injects keycodes and starts macros.
class KeycodeMapper:
"""Injects keycodes and starts macros."""
def __init__(self, context, source, forward_to):
This really is somewhat complicated because it needs to be able to handle
combinations (which is actually not that trivial because the order of keys
matters). The nature of some events (D-Pads and Wheels) adds to the
complexity. Since macros are mapped the same way keys are, this class
takes care of both.
"""
def __init__(self, *args, **kwargs):
"""Create a keycode mapper for one virtual device.
There may be multiple KeycodeMappers for one hardware device. They
share some state (unreleased and active_macros) with each other.
Parameters
----------
context : Context
the configuration of the Injector process
source : InputDevice
where events used in handle_keycode come from
forward_to : UInput
where forwarded/unhandled events should be written to
"""
self.source = source
self.abs_range = None
super().__init__(*args, **kwargs)
self._abs_range = None
if context.maps_joystick():
self.abs_range = utils.get_abs_range(source)
if self.context.maps_joystick():
self._abs_range = utils.get_abs_range(self.source)
self.context = context
self.forward_to = forward_to
self._gamepad = classify(self.source) == GAMEPAD
self.debounces = {}
# some type checking, prevents me from forgetting what that stuff
# is supposed to be when writing tests.
for key in context.key_to_code:
for key in self.context.key_to_code:
for sub_key in key:
if abs(sub_key[2]) > 1:
raise ValueError(
f'Expected values to be one of -1, 0 or 1, '
f'but got {key}'
f"Expected values to be one of -1, 0 or 1, " f"but got {key}"
)
def is_enabled(self):
# even if the source does not provide a capability that is used here, it might
# be important for notifying macros of new events that run on other sources.
return len(self.context.key_to_code) > 0 or len(self.context.macros) > 0
def is_handled(self, event):
return utils.should_map_as_btn(event, self.context.mapping, self._gamepad)
async def run(self):
"""Provide a debouncer to inject wheel releases."""
start = time.time()
while True:
# try to do this as close to 60hz as possible
time_taken = time.time() - start
await asyncio.sleep(max(0.0, (1 / 60) - time_taken))
start = time.time()
for debounce in self.debounces.values():
if debounce[2] == -1:
# has already been triggered
continue
if debounce[2] == 0:
debounce[0](*debounce[1])
debounce[2] = -1
else:
debounce[2] -= 1
def debounce(self, debounce_id, func, args, ticks):
"""Debounce a function call.
Parameters
----------
debounce_id : hashable
If this function is called with the same debounce_id again,
the previous debouncing is overwritten, and therefore restarted.
func : function
args : tuple
ticks : int
After ticks * 1 / 60 seconds the function will be executed,
unless debounce is called again with the same debounce_id
"""
self.debounces[debounce_id] = [func, args, ticks]
async def notify(self, event):
"""Receive the newest event that should be mapped."""
action = utils.classify_action(event, self._abs_range)
for macro in self.context.macros.values():
macro.notify(event, action)
will_report_key_up = utils.will_report_key_up(event)
if not will_report_key_up:
# simulate a key-up event if no down event arrives anymore.
# this may release macros, combinations or keycodes.
release = evdev.InputEvent(0, 0, event.type, event.code, 0)
self.debounce(
debounce_id=(event.type, event.code, action),
func=self.handle_keycode,
args=(release, RELEASE, False),
ticks=3,
)
async def delayed_handle_keycode():
# give macros a priority of working on their asyncio iterations
# first before handle_keycode. This is important for if_single.
# If if_single injects a modifier to modify the key that canceled
# its sleep, it needs to inject it before handle_keycode injects
# anything. This is important for the space cadet shift.
# 1. key arrives
# 2. stop if_single
# 3. make if_single inject `then`
# 4. inject key
# But I can't just wait for if_single to do its thing because it might
# be a macro that sleeps for a few seconds.
# This appears to me to be incredibly race-conditiony. For that
# reason wait a few more asyncio ticks before continuing.
# But a single one also worked. I can't wait for the specific
# macro task here because it might block forever. I'll just give
# it a few asyncio iterations advance before continuing here.
for _ in range(10):
# Noticable delays caused by this start at 10000 iterations
# Also see the python docs on asyncio.sleep. Sleeping for 0
# seconds just iterates the loop once.
await asyncio.sleep(0)
self.handle_keycode(event, action)
await delayed_handle_keycode()
def macro_write(self, ev_type, code, value, to_device=False):
"""Handler for macros."""
if to_device:
self.device_write(ev_type, code, value)
return
self.context.uinput.write(ev_type, code, value)
self.context.uinput.syn()
def device_write(self, ev_type, code, value):
"""Handler for source devices."""
for source in self.context.sources:
if code in source.capabilities().get(ev_type, []):
source.write(ev_type, code, value)
return
logger.warning('No device with capability %s', ecodes.bytype[ev_type][code])
self.write((ev_type, code, value), to_device)
def write(self, key):
"""Shorthand to write stuff."""
@ -269,20 +341,20 @@ class KeycodeMapper:
Otherwise, for unmapped events, returns the input.
The return format is always a tuple of 3-tuples, each 3-tuple being
type, code, value (int, int, int)
type, code, action (int, int, int)
Parameters
----------
key : int, int, int
3-tuple of type, code, value
Value should be one of -1, 0 or 1
key : tuple of int
3-tuple of type, code, action
Action should be one of -1, 0 or 1
"""
unreleased_entry = find_by_event(key)
# The key used to index the mappings `key_to_code` and `macros`.
# If the key triggers a combination, the returned key will be that one
# instead
value = key[2]
action = key[2]
key = (key,)
if unreleased_entry and unreleased_entry.triggered_key is not None:
@ -290,7 +362,7 @@ class KeycodeMapper:
# use the combination that was triggered by this as key.
return unreleased_entry.triggered_key
if is_key_down(value):
if utils.is_key_down(action):
# get the key/combination that the key-down would trigger
# the triggering key-down has to be the last element in
@ -301,8 +373,7 @@ class KeycodeMapper:
# releases. Do not check if key in macros and such, if it is an
# up event. It's going to be False.
combination = tuple(
value.input_event_tuple for value
in unreleased.values()
value.input_event_tuple for value in unreleased.values()
)
if key[0] not in combination: # might be a duplicate-down event
combination += key
@ -323,12 +394,12 @@ class KeycodeMapper:
else:
# no subset found, just use the key. all indices are tuples of
# tuples, both for combinations and single keys.
if value == 1 and len(combination) > 1:
logger.key_spam(combination, 'unknown combination')
if len(combination) > 1:
logger.key_spam(combination, "unknown combination")
return key
def handle_keycode(self, event, forward=True):
def handle_keycode(self, event, action, forward=True):
"""Write mapped keycodes, forward unmapped ones and manage macros.
As long as the provided event is mapped it will handle it, it won't
@ -337,74 +408,64 @@ class KeycodeMapper:
Parameters
----------
action : int
One of PRESS, PRESS_NEGATIVE or RELEASE
Just looking at the events value is not enough, because then mapping
trigger-values that are between 1 and 255 is not possible. They might skip
the 1 when pressed fast enough.
event : evdev.InputEvent
forward : bool
if False, will not forward the event if it didn't trigger any
mapping
"""
if event.type == EV_KEY and event.value == 2:
# button-hold event. Linux creates them on its own for the
# injection-fake-device if the release event won't appear,
# no need to forward or map them.
return
assert isinstance(action, int)
# normalize event numbers to one of -1, 0, +1. Otherwise
# mapping trigger values that are between 1 and 255 is not
# possible, because they might skip the 1 when pressed fast
# enough.
type_and_code = (event.type, event.code)
active_macro = active_macros.get(type_and_code)
original_tuple = (event.type, event.code, event.value)
event.value = utils.normalize_value(event, self.abs_range)
# the tuple of the actual input event. Used to forward the event if
# it is not mapped, and to index unreleased and active_macros. stays
# constant
event_tuple = (event.type, event.code, event.value)
type_code = (event.type, event.code)
active_macro = active_macros.get(type_code)
key = self._get_key(event_tuple)
key = self._get_key((*type_and_code, action))
is_mapped = self.context.is_mapped(key)
"""Releasing keys and macros"""
if is_key_up(event.value):
if utils.is_key_up(action):
if active_macro is not None and active_macro.is_holding():
# Tell the macro for that keycode that the key is released and
# let it decide what to do with that information.
active_macro.release_key()
logger.key_spam(key, 'releasing macro')
logger.key_spam(key, "releasing macro")
if type_code in unreleased:
if type_and_code in unreleased:
# figure out what this release event was for
unreleased_entry = unreleased[type_code]
unreleased_entry = unreleased[type_and_code]
target_type, target_code = unreleased_entry.target_type_code
del unreleased[type_code]
del unreleased[type_and_code]
if target_code == DISABLE_CODE:
logger.key_spam(key, 'releasing disabled key')
logger.key_spam(key, "releasing disabled key")
elif target_code is None:
logger.key_spam(key, 'releasing key')
logger.key_spam(key, "releasing key")
elif unreleased_entry.is_mapped():
# release what the input is mapped to
logger.key_spam(key, 'releasing %s', target_code)
logger.key_spam(key, "releasing %s", target_code)
self.write((target_type, target_code, 0))
elif forward:
# forward the release event
logger.key_spam((original_tuple,), 'forwarding release')
logger.key_spam((original_tuple,), "forwarding release")
self.forward(original_tuple)
else:
logger.key_spam(key, 'not forwarding release')
logger.key_spam(key, "not forwarding release")
elif event.type != EV_ABS:
# ABS events might be spammed like crazy every time the
# position slightly changes
logger.key_spam(key, 'unexpected key up')
logger.key_spam(key, "unexpected key up")
# everything that can be released is released now
return
"""Filtering duplicate key downs"""
if is_mapped and is_key_down(event.value):
if is_mapped and utils.is_key_down(action):
# unmapped keys should not be filtered here, they should just
# be forwarded to populate unreleased and then be written.
@ -413,7 +474,7 @@ class KeycodeMapper:
# duplicate key-down. skip this event. Avoid writing millions
# of key-down events when a continuous value is reported, for
# example for gamepad triggers or mouse-wheel-side buttons
logger.key_spam(key, 'duplicate key down')
logger.key_spam(key, "duplicate key down")
return
# it would start a macro usually
@ -424,22 +485,22 @@ class KeycodeMapper:
# This avoids spawning a second macro while the first one is
# not finished, especially since gamepad-triggers report a ton
# of events with a positive value.
logger.key_spam(key, 'macro already running')
logger.key_spam(key, "macro already running")
return
"""starting new macros or injecting new keys"""
if is_key_down(event.value):
if utils.is_key_down(action):
# also enter this for unmapped keys, as they might end up
# triggering a combination, so they should be remembered in
# unreleased
if key in self.context.macros:
macro = self.context.macros[key]
active_macros[type_code] = macro
Unreleased((None, None), event_tuple, key)
active_macros[type_and_code] = macro
Unreleased((None, None), (*type_and_code, action), key)
macro.press_key()
logger.key_spam(key, 'maps to macro %s', macro.code)
logger.key_spam(key, "maps to macro %s", macro.code)
asyncio.ensure_future(macro.run(self.macro_write))
return
@ -447,25 +508,25 @@ class KeycodeMapper:
target_code = self.context.key_to_code[key]
# remember the key that triggered this
# (this combination or this single key)
Unreleased((EV_KEY, target_code), event_tuple, key)
Unreleased((EV_KEY, target_code), (*type_and_code, action), key)
if target_code == DISABLE_CODE:
logger.key_spam(key, 'disabled')
logger.key_spam(key, "disabled")
return
logger.key_spam(key, 'maps to %s', target_code)
logger.key_spam(key, "maps to %s", target_code)
self.write((EV_KEY, target_code, 1))
return
if forward:
logger.key_spam((original_tuple,), 'forwarding')
logger.key_spam((original_tuple,), "forwarding")
self.forward(original_tuple)
else:
logger.key_spam((event_tuple,), 'not forwarding')
logger.key_spam(((*type_and_code, action),), "not forwarding")
# unhandled events may still be important for triggering
# combinations later, so remember them as well.
Unreleased((event_tuple[:2]), event_tuple, None)
Unreleased(type_and_code, (*type_and_code, action), None)
return
logger.error('%s unhandled', key)
logger.error("%s unhandled", key)

@ -23,8 +23,8 @@
from keymapper.logger import logger
from keymapper.injection.macros import parse, is_this_a_macro
from keymapper.state import system_mapping
from keymapper.injection.macros.parse import parse, is_this_a_macro
from keymapper.system_mapping import system_mapping
from keymapper.config import NONE, MOUSE, WHEEL, BUTTONS
@ -34,6 +34,8 @@ class Context:
In some ways this is a wrapper for the mapping that derives some
information that is specifically important to the injection.
The information in the context does not change during the injection.
One Context exists for each injection process, which is shared
with all coroutines and used objects.
@ -57,7 +59,7 @@ class Context:
This is needed to query keycodes more efficiently without having
to search mapping each time.
macros : dict
Mapping of ((type, code, value),) to _Macro objects.
Mapping of ((type, code, value),) to Macro objects.
Combinations work similar as in key_to_code
uinput : evdev.UInput
Where to inject stuff to. This is an extra node in /dev so that
@ -72,6 +74,7 @@ class Context:
So this uinput should not have EV_ABS capabilities. Only EV_REL
and EV_KEY is allowed.
"""
def __init__(self, mapping):
self.mapping = mapping
@ -88,17 +91,21 @@ class Context:
self.sources = None
def update_purposes(self):
"""Read joystick purposes from the configuration."""
self.left_purpose = self.mapping.get('gamepad.joystick.left_purpose')
self.right_purpose = self.mapping.get('gamepad.joystick.right_purpose')
"""Read joystick purposes from the configuration.
For efficiency, so that the config doesn't have to be read during
runtime repeatedly.
"""
self.left_purpose = self.mapping.get("gamepad.joystick.left_purpose")
self.right_purpose = self.mapping.get("gamepad.joystick.right_purpose")
def _parse_macros(self):
"""To quickly get the target macro during operation."""
logger.debug('Parsing macros')
logger.debug("Parsing macros")
macros = {}
for key, output in self.mapping:
if is_this_a_macro(output):
macro = parse(output, self.mapping)
macro = parse(output, self)
if macro is None:
continue
@ -106,7 +113,7 @@ class Context:
macros[permutation.keys] = macro
if len(macros) == 0:
logger.debug('No macros configured')
logger.debug("No macros configured")
return macros
@ -131,8 +138,8 @@ class Context:
for permutation in key.get_permutations():
if permutation.keys[-1][-1] not in [-1, 1]:
logger.error(
'Expected values to be -1 or 1 at this point: %s',
permutation.keys
"Expected values to be -1 or 1 at this point: %s",
permutation.keys,
)
key_to_code[permutation.keys] = target_code
@ -143,8 +150,10 @@ class Context:
Parameters
----------
key : ((int, int, int),)
One or more 3-tuples of type, code, value
key : tuple of tuple of int
One or more 3-tuples of type, code, action,
for example ((EV_KEY, KEY_A, 1), (EV_ABS, ABS_X, -1))
or ((EV_KEY, KEY_B, 1),)
"""
return key in self.macros or key in self.key_to_code

@ -31,16 +31,13 @@ from evdev.ecodes import EV_KEY, EV_REL
from keymapper.logger import logger
from keymapper.groups import classify, GAMEPAD
from keymapper import utils
from keymapper.mapping import DISABLE_CODE
from keymapper.injection.keycode_mapper import KeycodeMapper
from keymapper.injection.context import Context
from keymapper.injection.event_producer import EventProducer
from keymapper.injection.numlock import set_numlock, is_numlock_on, \
ensure_numlock
from keymapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock
from keymapper.injection.consumer_control import ConsumerControl
DEV_NAME = 'key-mapper'
DEV_NAME = "key-mapper"
# messages
CLOSE = 0
@ -72,12 +69,13 @@ def is_in_capabilities(key, capabilities):
class Injector(multiprocessing.Process):
"""Keeps injecting events in the background based on mapping and config.
"""Initializes, starts and stops injections.
Is a process to make it non-blocking for the rest of the code and to
make running multiple injector easier. There is one process per
hardware-device that is being mapped.
"""
regrab_timeout = 0.2
def __init__(self, group, mapping):
@ -90,11 +88,17 @@ class Injector(multiprocessing.Process):
mapping : Mapping
"""
self.group = group
self._event_producer = None
self._state = UNKNOWN
# used to interact with the parts of this class that are running within
# the new process
self._msg_pipe = multiprocessing.Pipe()
self.mapping = mapping
self.context = None # only needed inside the injection process
self._consumer_controls = []
super().__init__()
"""Functions to interact with the running process"""
@ -126,7 +130,7 @@ class Injector(multiprocessing.Process):
if self._state in [STARTING, RUNNING] and not alive:
self._state = FAILED
logger.error('Injector was unexpectedly found stopped')
logger.error("Injector was unexpectedly found stopped")
return self._state
@ -136,10 +140,7 @@ class Injector(multiprocessing.Process):
Can be safely called from the main procss.
"""
logger.info(
'Stopping injecting keycodes for group "%s"',
self.group.key
)
logger.info('Stopping injecting keycodes for group "%s"', self.group.key)
self._msg_pipe[1].send(CLOSE)
self._state = STOPPED
@ -188,24 +189,24 @@ class Injector(multiprocessing.Process):
if not needed:
# skipping reading and checking on events from those devices
# may be beneficial for performance.
logger.debug('No need to grab %s', path)
logger.debug("No need to grab %s", path)
return None
attempts = 0
while True:
try:
device.grab()
logger.debug('Grab %s', path)
logger.debug("Grab %s", path)
break
except IOError as error:
attempts += 1
# it might take a little time until the device is free if
# it was previously grabbed.
logger.debug('Failed attempts to grab %s: %d', path, attempts)
logger.debug("Failed attempts to grab %s: %d", path, attempts)
if attempts >= 10:
logger.error('Cannot grab %s, it is possibly in use', path)
logger.error("Cannot grab %s, it is possibly in use", path)
logger.error(str(error))
return None
@ -253,9 +254,7 @@ class Injector(multiprocessing.Process):
"""
ecodes = evdev.ecodes
capabilities = {
EV_KEY: []
}
capabilities = {EV_KEY: []}
# support all injected keycodes
for code in self.context.key_to_code.values():
@ -305,7 +304,7 @@ class Injector(multiprocessing.Process):
frame_available.clear()
msg = self._msg_pipe[0].recv()
if msg == CLOSE:
logger.debug('Received close signal')
logger.debug("Received close signal")
# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop.stop()
@ -316,7 +315,7 @@ class Injector(multiprocessing.Process):
max_len = 80 # based on error messages
remaining_len = max_len - len(DEV_NAME) - len(suffix) - 2
middle = name[:remaining_len]
name = f'{DEV_NAME} {middle} {suffix}'
name = f"{DEV_NAME} {middle} {suffix}"
return name
def run(self):
@ -328,6 +327,24 @@ class Injector(multiprocessing.Process):
Use this function as starting point in a process. It creates
the loops needed to read and map events and keeps running them.
"""
# TODO run all injections in a single process via asyncio
# - Make sure that closing asyncio fds won't lag the service
# - SharedDict becomes obsolete
# - quick_cleanup needs to be able to reliably stop the injection
# - I think I want an event listener architecture so that macros,
# event_producer, keycode_mapper and possibly other modules can get
# what they filter for whenever they want, without having to wire
# things through multiple other objects all the time
# - _new_event_arrived moves to the place where events are emitted. injector?
# - active macros and unreleased need to be per injection. it probably
# should move into the keycode_mapper class, but that only works if there
# is only one keycode_mapper per injection, and not per source. Problem was
# that I had to excessively pass around to which device to forward to...
# I also need to have information somewhere which source is a gamepad, I
# probably don't want to evaluate that from scratch each time `notify` is
# called.
# - benefit: writing macros that listen for events from other devices
logger.info('Starting injecting the mapping for "%s"', self.group.key)
# create a new event loop, because somehow running an infinite loop
@ -346,7 +363,10 @@ class Injector(multiprocessing.Process):
# forever
self.context.sources = self._grab_devices()
self._event_producer = EventProducer(self.context)
if len(sources) == 0:
logger.error("Did not grab any device")
self._msg_pipe[0].send(NO_GRAB)
return
numlock_state = is_numlock_on()
coroutines = []
@ -354,41 +374,28 @@ class Injector(multiprocessing.Process):
# where mapped events go to.
# See the Context docstring on why this is needed.
self.context.uinput = evdev.UInput(
name=self.get_udev_name(self.group.key, 'mapped'),
name=self.get_udev_name(self.group.key, "mapped"),
phys=DEV_NAME,
events=self._construct_capabilities(GAMEPAD in self.group.types)
events=self._construct_capabilities(GAMEPAD in self.group.types),
)
for source in self.context.sources:
# certain capabilities can have side effects apparently. with an
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device.
gamepad = classify(source) == GAMEPAD
forward_to = evdev.UInput(
name=self.get_udev_name(source.name, 'forwarded'),
name=self.get_udev_name(source.name, "forwarded"),
phys=DEV_NAME,
events=self._copy_capabilities(source)
events=self._copy_capabilities(source),
)
# actual reading of events
coroutines.append(self._event_consumer(source, forward_to))
# The event source of the current iteration will deliver events
# that are needed for this. It is that one that will be mapped
# to a mouse-like devnode.
if gamepad and self.context.joystick_as_mouse():
self._event_producer.set_abs_range_from(source)
if len(coroutines) == 0:
logger.error('Did not grab any device')
self._msg_pipe[0].send(NO_GRAB)
return
# actually doing things
consumer_control = ConsumerControl(self.context, source, forward_to)
coroutines.append(consumer_control.run())
self._consumer_controls.append(consumer_control)
coroutines.append(self._msg_listener())
# run besides this stuff
coroutines.append(self._event_producer.run())
# set the numlock state to what it was before injecting, because
# grabbing devices screws this up
set_numlock(numlock_state)
@ -407,69 +414,9 @@ class Injector(multiprocessing.Process):
# expected when stop_injecting is called,
# during normal operation as well as tests this point is not
# reached otherwise.
logger.debug('asyncio coroutines ended')
logger.debug("asyncio coroutines ended")
for source in self.context.sources:
# ungrab at the end to make the next injection process not fail
# its grabs
source.ungrab()
async def _event_consumer(self, source, forward_to):
"""Reads input events to inject keycodes or talk to the event_producer.
Can be stopped by stopping the asyncio loop. This loop
reads events from a single device only. Other devnodes may be
present for the hardware device, in which case this needs to be
started multiple times.
Parameters
----------
source : evdev.InputDevice
where to read keycodes from
forward_to : evdev.UInput
where to write keycodes to that were not mapped to anything.
Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source.
"""
logger.debug(
'Started consumer to inject for %s, fd %s',
source.path, source.fd
)
gamepad = classify(source) == GAMEPAD
keycode_handler = KeycodeMapper(self.context, source, forward_to)
async for event in source.async_read_loop():
if self._event_producer.is_handled(event):
# the event_producer will take care of it
self._event_producer.notify(event)
continue
# for mapped stuff
if utils.should_map_as_btn(event, self.context.mapping, gamepad):
will_report_key_up = utils.will_report_key_up(event)
keycode_handler.handle_keycode(event)
if not will_report_key_up:
# simulate a key-up event if no down event arrives anymore.
# this may release macros, combinations or keycodes.
release = evdev.InputEvent(0, 0, event.type, event.code, 0)
self._event_producer.debounce(
debounce_id=(event.type, event.code, event.value),
func=keycode_handler.handle_keycode,
args=(release, False),
ticks=3,
)
continue
# forward the rest
forward_to.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
# This happens all the time in tests because the async_read_loop
# stops when there is nothing to read anymore. Otherwise tests
# would block.
logger.error('The consumer for "%s" stopped early', source.path)

@ -44,7 +44,7 @@ import atexit
import select
from evdev.ecodes import ecodes, EV_KEY, EV_REL, REL_X, REL_Y, REL_WHEEL, \
REL_HWHEEL, EV_LED
REL_HWHEEL
from keymapper.logger import logger
from keymapper.state import system_mapping
@ -411,27 +411,6 @@ class _Macro:
self.tasks.append(lambda handler: handler(ev_type, code, value))
self.tasks.append(self._keycode_pause)
def todevice(self, ev_type, code, value):
"""Write any event to source or other device.
Parameters
----------
ev_type: str or int
examples: 17, 'EV_LED', 18, 'EV_SND'
code : int or int
examples: 0, 'LED_NUML', 0, 'SND_CLICK'
value : int
"""
if isinstance(ev_type, str):
ev_type = ecodes[ev_type.upper()]
if isinstance(code, str):
code = ecodes[code.upper()]
self.tasks.append(lambda handler: handler(ev_type, code, value, True))
def led(self, code, value):
"""Shortcut for todevice(EV_LED, code, value)."""
self.todevice(EV_LED, code, value)
def mouse(self, direction, speed):
"""Shortcut for h(e(...))."""
code, value = {
@ -622,8 +601,6 @@ def _parse_recurse(macro, mapping, macro_instance=None, depth=0):
'wheel': (macro_instance.wheel, 2, 2),
'ifeq': (macro_instance.ifeq, 3, 4),
'set': (macro_instance.set, 2, 2),
'todevice': (macro_instance.todevice, 3, 4),
'led': (macro_instance.led, 2, 2),
}
function = functions.get(call)

@ -0,0 +1,499 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Executes more complex patterns of keystrokes.
To keep it short on the UI, basic functions are one letter long.
The outermost macro (in the examples below the one created by 'r',
'r' and 'w') will be started, which triggers a chain reaction to execute
all of the configured stuff.
Examples
--------
r(3, k(a).w(10)): a <10ms> a <10ms> a
r(2, k(a).k(-)).k(b): a - a - b
w(1000).m(Shift_L, r(2, k(a))).w(10).k(b): <1s> A A <10ms> b
"""
import asyncio
import copy
from evdev.ecodes import ecodes, EV_KEY, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger
from keymapper.system_mapping import system_mapping
from keymapper.ipc.shared_dict import SharedDict
from keymapper.utils import PRESS, PRESS_NEGATIVE
macro_variables = SharedDict()
def type_check(display_name, value, allowed_types, position):
"""Validate a parameter used in a macro."""
for allowed_type in allowed_types:
if allowed_type is None:
if value is None:
return value
else:
continue
# try to parse "1" as 1 if possible
try:
return allowed_type(value)
except (TypeError, ValueError):
pass
if isinstance(value, allowed_type):
return value
raise TypeError(
f"Expected parameter {position} for {display_name} to be "
f"one of {allowed_types}, but got {value}"
)
class Macro:
"""Supports chaining and preparing actions.
Calling functions like keycode on Macro doesn't inject any events yet,
it means that once .run is used it will be executed along with all other
queued tasks.
Those functions need to construct an asyncio coroutine and append it to
self.tasks. This makes parameter checking during compile time possible.
Coroutines receive a handler as argument, which is a function that can be
used to inject input events into the system.
"""
def __init__(self, code, context):
"""Create a macro instance that can be populated with tasks.
Parameters
----------
code : string or None
The original parsed code, for logging purposes.
context : Context
"""
self.code = code
self.context = context
# List of coroutines that will be called sequentially.
# This is the compiled code
self.tasks = []
# can be used to wait for the release of the event
self._holding_event = asyncio.Event()
self._holding_event.set() # released by default
self.running = False
# all required capabilities, without those of child macros
self.capabilities = {
EV_KEY: set(),
EV_REL: set(),
}
self.child_macros = []
self.keystroke_sleep_ms = None
self._new_event_arrived = asyncio.Event()
self._newest_event = None
self._newest_action = None
def notify(self, event, action):
"""Tell the macro about the newest event."""
for macro in self.child_macros:
macro.notify(event, action)
self._newest_event = event
self._newest_action = action
self._new_event_arrived.set()
async def wait_for_event(self, filter=None):
"""Wait until a specific event arrives.
The parameters can be used to provide a filter. It will block
until an event arrives that matches them.
Parameters
----------
filter : function
Receives the event. Stop waiting if it returns true.
"""
while True:
await self._new_event_arrived.wait()
self._new_event_arrived.clear()
if filter is not None:
if not filter(self._newest_event, self._newest_action):
continue
break
def is_holding(self):
"""Check if the macro is waiting for a key to be released."""
return not self._holding_event.is_set()
def get_capabilities(self):
"""Resolve all capabilities of the macro and those of its children."""
capabilities = copy.deepcopy(self.capabilities)
for macro in self.child_macros:
macro_capabilities = macro.get_capabilities()
for ev_type in macro_capabilities:
if ev_type not in capabilities:
capabilities[ev_type] = set()
capabilities[ev_type].update(macro_capabilities[ev_type])
return capabilities
async def run(self, handler):
"""Run the macro.
Parameters
----------
handler : function
Will receive int type, code and value for an event to write
"""
if self.running:
logger.error('Tried to run already running macro "%s"', self.code)
return
# newly arriving events are only interesting if they arrive after the
# macro started
self._new_event_arrived.clear()
self.keystroke_sleep_ms = self.context.mapping.get("macros.keystroke_sleep_ms")
self.running = True
for task in self.tasks:
# one could call tasks the compiled macros. it's lambda functions
# that receive the handler as an argument, so that they know
# where to send the event to.
coroutine = task(handler)
if asyncio.iscoroutine(coroutine):
await coroutine
# done
self.running = False
def press_key(self):
"""The user pressed the key down."""
if self.is_holding():
logger.error("Already holding")
return
self._holding_event.clear()
for macro in self.child_macros:
macro.press_key()
def release_key(self):
"""The user released the key."""
self._holding_event.set()
for macro in self.child_macros:
macro.release_key()
def hold(self, macro=None):
"""Loops the execution until key release."""
if macro is None:
self.tasks.append(lambda _: self._holding_event.wait())
return
if not isinstance(macro, Macro):
# if macro is a key name, hold down the key while the
# keyboard key is physically held down
symbol = str(macro)
code = system_mapping.get(symbol)
if code is None:
raise KeyError(f'Unknown key "{symbol}"')
self.capabilities[EV_KEY].add(code)
self.tasks.append(lambda handler: handler(EV_KEY, code, 1))
self.tasks.append(lambda _: self._holding_event.wait())
self.tasks.append(lambda handler: handler(EV_KEY, code, 0))
return
if isinstance(macro, Macro):
# repeat the macro forever while the key is held down
async def task(handler):
while self.is_holding():
# run the child macro completely to avoid
# not-releasing any key
await macro.run(handler)
self.tasks.append(task)
self.child_macros.append(macro)
def modify(self, modifier, macro):
"""Do stuff while a modifier is activated.
Parameters
----------
modifier : str
macro : Macro
"""
type_check("m (modify)", macro, [Macro], 2)
modifier = str(modifier)
code = system_mapping.get(modifier)
if code is None:
raise KeyError(f'Unknown modifier "{modifier}"')
self.capabilities[EV_KEY].add(code)
self.child_macros.append(macro)
self.tasks.append(lambda handler: handler(EV_KEY, code, 1))
self.tasks.append(self._keycode_pause)
self.tasks.append(macro.run)
self.tasks.append(self._keycode_pause)
self.tasks.append(lambda handler: handler(EV_KEY, code, 0))
self.tasks.append(self._keycode_pause)
def repeat(self, repeats, macro):
"""Repeat actions.
Parameters
----------
repeats : int or Macro
macro : Macro
"""
repeats = type_check("r (repeat)", repeats, [int], 1)
type_check("r (repeat)", macro, [Macro], 2)
async def repeat(handler):
for _ in range(repeats):
await macro.run(handler)
self.tasks.append(repeat)
self.child_macros.append(macro)
async def _keycode_pause(self, _=None):
"""To add a pause between keystrokes."""
await asyncio.sleep(self.keystroke_sleep_ms / 1000)
def keycode(self, symbol):
"""Write the symbol."""
symbol = str(symbol)
code = system_mapping.get(symbol)
if code is None:
raise KeyError(f'Unknown key "{symbol}"')
self.capabilities[EV_KEY].add(code)
async def keycode(handler):
handler(EV_KEY, code, 1)
await self._keycode_pause()
handler(EV_KEY, code, 0)
await self._keycode_pause()
self.tasks.append(keycode)
def event(self, ev_type, code, value):
"""Write any event.
Parameters
----------
ev_type: str or int
examples: 2, 'EV_KEY'
code : int or int
examples: 52, 'KEY_A'
value : int
"""
if isinstance(ev_type, str):
ev_type = ecodes[ev_type.upper()]
if isinstance(code, str):
code = ecodes[code.upper()]
if ev_type not in self.capabilities:
self.capabilities[ev_type] = set()
if ev_type == EV_REL:
# add all capabilities that are required for the display server
# to recognize the device as mouse
self.capabilities[EV_REL].add(REL_X)
self.capabilities[EV_REL].add(REL_Y)
self.capabilities[EV_REL].add(REL_WHEEL)
self.capabilities[ev_type].add(code)
self.tasks.append(lambda handler: handler(ev_type, code, value))
self.tasks.append(self._keycode_pause)
def mouse(self, direction, speed):
"""Shortcut for h(e(...))."""
type_check("mouse", direction, [str], 1)
speed = type_check("mouse", speed, [int], 2)
code, value = {
"up": (REL_Y, -1),
"down": (REL_Y, 1),
"left": (REL_X, -1),
"right": (REL_X, 1),
}[direction.lower()]
value *= speed
child_macro = Macro(None, self.context)
child_macro.event(EV_REL, code, value)
self.hold(child_macro)
def wheel(self, direction, speed):
"""Shortcut for h(e(...))."""
type_check("wheel", direction, [str], 1)
speed = type_check("wheel", speed, [int], 2)
code, value = {
"up": (REL_WHEEL, 1),
"down": (REL_WHEEL, -1),
"left": (REL_HWHEEL, 1),
"right": (REL_HWHEEL, -1),
}[direction.lower()]
child_macro = Macro(None, self.context)
child_macro.event(EV_REL, code, value)
child_macro.wait(100 / speed)
self.hold(child_macro)
def wait(self, sleeptime):
"""Wait time in milliseconds."""
sleeptime = type_check("wait", sleeptime, [int, float], 1) / 1000
async def sleep(_):
await asyncio.sleep(sleeptime)
self.tasks.append(sleep)
def set(self, variable, value):
"""Set a variable to a certain value."""
async def set(_):
logger.debug('"%s" set to "%s"', variable, value)
macro_variables[variable] = value
self.tasks.append(set)
def ifeq(self, variable, value, then, otherwise=None):
"""Perform an equality check.
Parameters
----------
variable : string
value : string | number
then : Macro | None
otherwise : Macro | None
"""
type_check("ifeq", then, [Macro, None], 1)
type_check("ifeq", otherwise, [Macro, None], 2)
async def ifeq(handler):
set_value = macro_variables.get(variable)
logger.debug('"%s" is "%s"', variable, set_value)
if set_value == value:
if then is not None:
await then.run(handler)
elif otherwise is not None:
await otherwise.run(handler)
if isinstance(then, Macro):
self.child_macros.append(then)
if isinstance(otherwise, Macro):
self.child_macros.append(otherwise)
self.tasks.append(ifeq)
def if_tap(self, then=None, otherwise=None, timeout=300):
"""If a key was pressed quickly.
Parameters
----------
then : Macro | None
otherwise : Macro | None
timeout : int
"""
type_check("if_tap", then, [Macro, None], 1)
type_check("if_tap", otherwise, [Macro, None], 2)
timeout = type_check("if_tap", timeout, [int], 3)
if isinstance(then, Macro):
self.child_macros.append(then)
if isinstance(otherwise, Macro):
self.child_macros.append(otherwise)
async def if_tap(handler):
try:
coroutine = self._holding_event.wait()
await asyncio.wait_for(coroutine, timeout / 1000)
if then:
await then.run(handler)
except asyncio.TimeoutError:
if otherwise:
await otherwise.run(handler)
self.tasks.append(if_tap)
def if_single(self, then, otherwise):
"""If a key was pressed without combining it.
Parameters
----------
then : Macro | None
otherwise : Macro | None
"""
type_check("if_single", then, [Macro, None], 1)
type_check("if_single", otherwise, [Macro, None], 2)
if isinstance(then, Macro):
self.child_macros.append(then)
if isinstance(otherwise, Macro):
self.child_macros.append(otherwise)
async def if_single(handler):
mappable_event_1 = (self._newest_event.type, self._newest_event.code)
def event_filter(event, action):
"""Which event may wake if_tap up."""
# release event of the actual key
if (event.type, event.code) == mappable_event_1:
return True
# press event of another key
if action in (PRESS, PRESS_NEGATIVE):
return True
await self.wait_for_event(event_filter)
mappable_event_2 = (self._newest_event.type, self._newest_event.code)
combined = mappable_event_1 != mappable_event_2
if then and not combined:
await then.run(handler)
elif otherwise:
await otherwise.run(handler)
self.tasks.append(if_single)

@ -0,0 +1,280 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Parse macro code"""
import re
import traceback
import inspect
from keymapper.logger import logger
from keymapper.injection.macros.macro import Macro
def is_this_a_macro(output):
"""Figure out if this is a macro."""
if not isinstance(output, str):
return False
if "+" in output.strip():
# for example "a + b"
return True
return "(" in output and ")" in output and len(output) >= 4
FUNCTIONS = {
"m": Macro.modify,
"r": Macro.repeat,
"k": Macro.keycode,
"e": Macro.event,
"w": Macro.wait,
"h": Macro.hold,
"mouse": Macro.mouse,
"wheel": Macro.wheel,
"ifeq": Macro.ifeq,
"set": Macro.set,
"if_tap": Macro.if_tap,
"if_single": Macro.if_single,
}
def get_num_parameters(function):
"""Get the number of required parameters and the maximum number of parameters."""
fullargspec = inspect.getfullargspec(function)
num_args = len(fullargspec.args) - 1 # one is `self`
return num_args - len(fullargspec.defaults or ()), num_args
def _extract_params(inner):
"""Extract parameters from the inner contents of a call.
This does not parse them.
Parameters
----------
inner : string
for example '1, r, r(2, k(a))' should result in ['1', 'r', 'r(2, k(a))']
"""
inner = inner.strip()
brackets = 0
params = []
start = 0
for position, char in enumerate(inner):
if char == "(":
brackets += 1
if char == ")":
brackets -= 1
if char == "," and brackets == 0:
# , potentially starts another parameter, but only if
# the current brackets are all closed.
params.append(inner[start:position].strip())
# skip the comma
start = position + 1
# one last parameter
params.append(inner[start:].strip())
return params
def _count_brackets(macro):
"""Find where the first opening bracket closes."""
openings = macro.count("(")
closings = macro.count(")")
if openings != closings:
raise Exception(
f"You entered {openings} opening and {closings} " "closing brackets"
)
brackets = 0
position = 0
for char in macro:
position += 1
if char == "(":
brackets += 1
continue
if char == ")":
brackets -= 1
if brackets == 0:
# the closing bracket of the call
break
return position
def _parse_recurse(macro, context, macro_instance=None, depth=0):
"""Handle a subset of the macro, e.g. one parameter or function call.
Parameters
----------
macro : string
Just like parse
context : Context
macro_instance : Macro or None
A macro instance to add tasks to
depth : int
"""
# not using eval for security reasons
assert isinstance(macro, str)
assert isinstance(depth, int)
if macro == "":
return None
if macro_instance is None:
macro_instance = Macro(macro, context)
else:
assert isinstance(macro_instance, Macro)
macro = macro.strip()
space = " " * depth
# is it another macro?
call_match = re.match(r"^(\w+)\(", macro)
call = call_match[1] if call_match else None
if call is not None:
# available functions in the macro and the minimum and maximum number
# of their parameters
function = FUNCTIONS.get(call)
if function is None:
raise Exception(f"Unknown function {call}")
# get all the stuff inbetween
position = _count_brackets(macro)
inner = macro[macro.index("(") + 1 : position - 1]
# split "3, k(a).w(10)" into parameters
string_params = _extract_params(inner)
logger.spam("%scalls %s with %s", space, call, string_params)
# evaluate the params
params = [
_parse_recurse(param.strip(), context, None, depth + 1)
for param in string_params
]
logger.spam("%sadd call to %s with %s", space, call, params)
min_params, max_params = get_num_parameters(function)
if len(params) < min_params or len(params) > max_params:
if min_params != max_params:
msg = (
f"{call} takes between {min_params} and {max_params}, "
f"not {len(params)} parameters"
)
else:
msg = f"{call} takes {min_params}, " f"not {len(params)} parameters"
raise ValueError(msg)
function(macro_instance, *params)
# is after this another call? Chain it to the macro_instance
if len(macro) > position and macro[position] == ".":
chain = macro[position + 1 :]
logger.spam("%sfollowed by %s", space, chain)
_parse_recurse(chain, context, macro_instance, depth)
return macro_instance
# probably a parameter for an outer function
try:
# if possible, parse as int
macro = int(macro)
except ValueError:
# use as string instead
pass
logger.spam("%s%s %s", space, type(macro), macro)
return macro
def handle_plus_syntax(macro):
"""transform a + b + c to m(a, m(b, m(c, h())))"""
if "+" not in macro:
return macro
if "(" in macro or ")" in macro:
logger.error('Mixing "+" and macros is unsupported: "%s"', macro)
return macro
chunks = [chunk.strip() for chunk in macro.split("+")]
output = ""
depth = 0
for chunk in chunks:
if chunk == "":
# invalid syntax
logger.error('Invalid syntax for "%s"', macro)
return macro
depth += 1
output += f"m({chunk},"
output += "h()"
output += depth * ")"
logger.debug('Transformed "%s" to "%s"', macro, output)
return output
def parse(macro, context, return_errors=False):
"""parse and generate a Macro that can be run as often as you want.
If it could not be parsed, possibly due to syntax errors, will log the
error and return None.
Parameters
----------
macro : string
"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)"
context : Context
return_errors : bool
If True, returns errors as a string or None if parsing worked.
If False, returns the parsed macro.
"""
macro = handle_plus_syntax(macro)
# whitespaces, tabs, newlines and such don't serve a purpose. make
# the log output clearer and the parsing easier.
macro = re.sub(r"\s", "", macro)
if '"' in macro or "'" in macro:
logger.info("Quotation marks in macros are not needed")
macro = macro.replace('"', "").replace("'", "")
if return_errors:
logger.spam("checking the syntax of %s", macro)
else:
logger.spam("preparing macro %s for later execution", macro)
try:
macro_object = _parse_recurse(macro, context)
return macro_object if not return_errors else None
except Exception as error:
logger.error('Failed to parse macro "%s": %s', macro, error.__repr__())
# print the traceback in case this is a bug of key-mapper
logger.debug("".join(traceback.format_tb(error.__traceback__)).strip())
return f"{error.__class__.__name__}: {str(error)}" if return_errors else None

@ -36,16 +36,12 @@ def is_numlock_on():
"""Get the current state of the numlock."""
try:
xset_q = subprocess.check_output(
['xset', 'q'],
stderr=subprocess.STDOUT
["xset", "q"], stderr=subprocess.STDOUT
).decode()
num_lock_status = re.search(
r'Num Lock:\s+(.+?)\s',
xset_q
)
num_lock_status = re.search(r"Num Lock:\s+(.+?)\s", xset_q)
if num_lock_status is not None:
return num_lock_status[1] == 'on'
return num_lock_status[1] == "on"
return False
except (FileNotFoundError, subprocess.CalledProcessError):
@ -58,23 +54,21 @@ def set_numlock(state):
if state is None:
return
value = {
True: 'on',
False: 'off'
}[state]
value = {True: "on", False: "off"}[state]
try:
subprocess.check_output(['numlockx', value])
subprocess.check_output(["numlockx", value])
except subprocess.CalledProcessError:
# might be in a tty
pass
except FileNotFoundError:
# doesn't seem to be installed everywhere
logger.debug('numlockx not found')
logger.debug("numlockx not found")
def ensure_numlock(func):
"""Decorator to reset the numlock to its initial state afterwards."""
def wrapped(*args, **kwargs):
# for some reason, grabbing a device can modify the num lock state.
# remember it and apply back later
@ -85,4 +79,5 @@ def ensure_numlock(func):
set_numlock(numlock_before)
return result
return wrapped

@ -1,6 +0,0 @@
# Injection
This folder contains all classes that are only relevant for the injection
process. There is one process for each hardware device that is being injected
for, and one context object per process that is being passed around for all
classes to use.

@ -0,0 +1,25 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Since I'm not forking, I can't use multiprocessing.Pipe.
Processes that need privileges are spawned with pkexec, which connect to
known pipe paths to communicate with the non-privileged parent process.
"""

@ -47,16 +47,14 @@ from keymapper.paths import mkdir, chown
class Pipe:
"""Pipe object."""
def __init__(self, path):
"""Create a pipe, or open it if it already exists."""
self._path = path
self._unread = []
self._created_at = time.time()
paths = (
f'{path}r',
f'{path}w'
)
paths = (f"{path}r", f"{path}w")
mkdir(os.path.dirname(path))
@ -70,13 +68,13 @@ class Pipe:
os.remove(paths[1])
self._fds = os.pipe()
fds_dir = f'/proc/{os.getpid()}/fd/'
chown(f'{fds_dir}{self._fds[0]}')
chown(f'{fds_dir}{self._fds[1]}')
fds_dir = f"/proc/{os.getpid()}/fd/"
chown(f"{fds_dir}{self._fds[0]}")
chown(f"{fds_dir}{self._fds[1]}")
# to make it accessible by path constants, create symlinks
os.symlink(f'{fds_dir}{self._fds[0]}', paths[0])
os.symlink(f'{fds_dir}{self._fds[1]}', paths[1])
os.symlink(f"{fds_dir}{self._fds[0]}", paths[0])
os.symlink(f"{fds_dir}{self._fds[1]}", paths[1])
else:
logger.spam('Using existing pipe for "%s"', path)
@ -84,13 +82,10 @@ class Pipe:
# is nothing to read
self._fds = (
os.open(paths[0], os.O_RDONLY | os.O_NONBLOCK),
os.open(paths[1], os.O_WRONLY | os.O_NONBLOCK)
os.open(paths[1], os.O_WRONLY | os.O_NONBLOCK),
)
self._handles = (
open(self._fds[0], 'r'),
open(self._fds[1], 'w')
)
self._handles = (open(self._fds[0], "r"), open(self._fds[1], "w"))
def recv(self):
"""Read an object from the pipe or None if nothing available.
@ -107,11 +102,11 @@ class Pipe:
return None
parsed = json.loads(line)
if parsed[0] < self._created_at and os.environ.get('UNITTEST'):
if parsed[0] < self._created_at and os.environ.get("UNITTEST"):
# important to avoid race conditions between multiple unittests,
# for example old terminate messages reaching a new instance of
# the helper.
logger.spam('Ignoring old message %s', parsed)
logger.spam("Ignoring old message %s", parsed)
return None
return parsed[1]
@ -121,8 +116,8 @@ class Pipe:
dump = json.dumps((time.time(), message))
# there aren't any newlines supposed to be,
# but if there are it breaks readline().
self._handles[1].write(dump.replace('\n', ''))
self._handles[1].write('\n')
self._handles[1].write(dump.replace("\n", ""))
self._handles[1].write("\n")
self._handles[1].flush()
def poll(self):

@ -1,7 +0,0 @@
# IPC
Since I'm not forking, I can't use the handy multiprocessing.Pipe
method anymore.
Processes that need privileges are spawned with pkexec, which connect to
known pipe paths to communicate with the non-privileged parent process.

@ -0,0 +1,109 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""Share a dictionary across processes."""
import multiprocessing
import atexit
import select
from keymapper.logger import logger
class SharedDict:
"""Share a dictionary across processes."""
# because unittests terminate all child processes in cleanup I can't use
# multiprocessing.Manager
def __init__(self):
"""Create a shared dictionary."""
super().__init__()
self.pipe = multiprocessing.Pipe()
self.process = None
atexit.register(self._stop)
self._start()
# To avoid blocking forever if something goes wrong. The maximum
# observed time communication takes was 0.001 for me on a slow pc
self._timeout = 0.02
def _start(self):
"""Ensure the process to manage the dictionary is running."""
if self.process is not None and self.process.is_alive():
return
# if the manager has already been running in the past but stopped
# for some reason, the dictionary contents are lost
self.process = multiprocessing.Process(target=self.manage)
self.process.start()
def manage(self):
"""Manage the dictionary, handle read and write requests."""
shared_dict = dict()
while True:
message = self.pipe[0].recv()
logger.spam("SharedDict got %s", message)
if message[0] == "stop":
return
if message[0] == "set":
shared_dict[message[1]] = message[2]
if message[0] == "get":
self.pipe[0].send(shared_dict.get(message[1]))
if message[0] == "ping":
self.pipe[0].send("pong")
def _stop(self):
"""Stop the managing process."""
self.pipe[1].send(("stop",))
def get(self, key):
"""Get a value from the dictionary."""
return self.__getitem__(key)
def is_alive(self, timeout=None):
"""Check if the manager process is running."""
self.pipe[1].send(("ping",))
select.select([self.pipe[1]], [], [], timeout or self._timeout)
if self.pipe[1].poll():
return self.pipe[1].recv() == "pong"
return False
def __setitem__(self, key, value):
self.pipe[1].send(("set", key, value))
def __getitem__(self, key):
self.pipe[1].send(("get", key))
select.select([self.pipe[1]], [], [], self._timeout)
if self.pipe[1].poll():
return self.pipe[1].recv()
logger.error("select.select timed out")
return None
def __del__(self):
self._stop()

@ -63,9 +63,9 @@ from keymapper.paths import mkdir, chown
# something funny that most likely won't appear in messages.
# also add some ones so that 01 in the payload won't offset
# a match by 2 bits
END = b'\x55\x55\xff\x55' # should be 01010101 01010101 11111111 01010101
END = b"\x55\x55\xff\x55" # should be 01010101 01010101 11111111 01010101
ENCODING = 'utf8'
ENCODING = "utf8"
# reusing existing objects makes tests easier, no headaches about closing
@ -77,6 +77,7 @@ existing_clients = {}
class Base:
"""Abstract base class for Socket and Client."""
def __init__(self, path):
self._path = path
self._unread = []
@ -107,10 +108,10 @@ class Base:
def _receive_new_messages(self):
if not self.connect():
logger.spam('Not connected')
logger.spam("Not connected")
return
messages = b''
messages = b""
attempts = 0
while True:
try:
@ -137,7 +138,7 @@ class Base:
# important to avoid race conditions between multiple
# unittests, for example old terminate messages reaching
# a new instance of the helper.
logger.spam('Ignoring old message %s', parsed)
logger.spam("Ignoring old message %s", parsed)
continue
self._unread.append(parsed[1])
@ -170,7 +171,7 @@ class Base:
self.unsent.append(dump)
if not self.connect():
logger.spam('Not connected')
logger.spam("Not connected")
return
def send_all():
@ -187,7 +188,8 @@ class Base:
if not self.reconnect():
logger.error(
'%s: The other side of "%s" disappeared',
type(self).__name__, self._path
type(self).__name__,
self._path,
)
return
@ -196,12 +198,15 @@ class Base:
except BrokenPipeError as error:
logger.error(
'%s: Failed to send via "%s": %s',
type(self).__name__, self._path, error
type(self).__name__,
self._path,
error,
)
class _Client(Base):
"""A socket that can be written to and read from."""
def connect(self):
if self.socket is not None:
return True
@ -246,6 +251,7 @@ class _Server(Base):
It accepts one connection at a time, and drops old connections if
a new one is in sight.
"""
def connect(self):
if self.socket is None:
if os.path.exists(self._path):

@ -30,17 +30,20 @@ from evdev import ecodes
def verify(key):
"""Check if the key is an int 3-tuple of type, code, value"""
if not isinstance(key, tuple) or len(key) != 3:
raise ValueError(f'Expected key to be a 3-tuple, but got {key}')
raise ValueError(f"Expected key to be a 3-tuple, but got {key}")
if sum([not isinstance(value, int) for value in key]) != 0:
raise ValueError(f'Can only use integers, but got {key}')
raise ValueError(f"Can only use integers, but got {key}")
# having shift in combinations modifies the configured output,
# ctrl might not work at all
DIFFICULT_COMBINATIONS = [
ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT,
ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL,
ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT
ecodes.KEY_LEFTSHIFT,
ecodes.KEY_RIGHTSHIFT,
ecodes.KEY_LEFTCTRL,
ecodes.KEY_RIGHTCTRL,
ecodes.KEY_LEFTALT,
ecodes.KEY_RIGHTALT,
]
@ -49,6 +52,7 @@ class Key:
Can be used in hashmaps/dicts as key
"""
def __init__(self, *keys):
"""
Parameters
@ -66,7 +70,7 @@ class Key:
or Key objects, which will flatten all of them into one combination
"""
if len(keys) == 0:
raise ValueError('At least one key is required')
raise ValueError("At least one key is required")
if isinstance(keys[0], int):
# type, code, value was provided instead of a tuple
@ -103,7 +107,7 @@ class Key:
return len(self.keys)
def __str__(self):
return f'Key{str(self.keys)}'
return f"Key{str(self.keys)}"
def __repr__(self):
# used in the AssertionError output of tests

@ -33,7 +33,7 @@ from keymapper.user import HOME
try:
from keymapper.commit_hash import COMMIT_HASH
except ImportError:
COMMIT_HASH = ''
COMMIT_HASH = ""
SPAM = 5
@ -56,7 +56,7 @@ def key_spam(self, key, msg, *args):
Parameters
----------
key : tuple
key : tuple of int
anything that can be string formatted, but usually a tuple of
(type, code, value) tuples
"""
@ -68,11 +68,11 @@ def key_spam(self, key, msg, *args):
msg = msg % args
str_key = str(key)
str_key = str_key.replace(',)', ')')
spacing = ' ' + '-' * max(0, 30 - len(str_key))
str_key = str_key.replace(",)", ")")
spacing = " " + "-" * max(0, 30 - len(str_key))
if len(spacing) == 1:
spacing = ''
msg = f'{str_key}{spacing} {msg}'
spacing = ""
msg = f"{str_key}{spacing} {msg}"
if msg == previous_key_spam:
# avoid some super spam from EV_ABS events
@ -88,8 +88,9 @@ logging.Logger.spam = spam
logging.Logger.key_spam = key_spam
LOG_PATH = (
'/var/log/key-mapper' if os.access('/var/log', os.W_OK)
else f'{HOME}/.log/key-mapper'
"/var/log/key-mapper"
if os.access("/var/log", os.W_OK)
else f"{HOME}/.log/key-mapper"
)
logger = logging.getLogger()
@ -102,13 +103,14 @@ def is_debug():
class Formatter(logging.Formatter):
"""Overwritten Formatter to print nicer logs."""
def format(self, record):
"""Overwritten format function."""
# pylint: disable=protected-access
debug = is_debug()
if record.levelno == logging.INFO and not debug:
# if not launched with --debug, then don't print "INFO:"
self._style._fmt = '%(message)s'
self._style._fmt = "%(message)s"
else:
# see https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
# for those numbers
@ -122,19 +124,19 @@ class Formatter(logging.Formatter):
}.get(record.levelno, 0)
if debug:
delta = f'{str(time.time() - start)[:7]}'
delta = f"{str(time.time() - start)[:7]}"
self._style._fmt = ( # noqa
f'\033[{color}m' # color
f'{os.getpid()} '
f'{delta} '
f'%(levelname)s '
f'%(filename)s:%(lineno)d: '
'%(message)s'
'\033[0m' # end style
f"\033[{color}m" # color
f"{os.getpid()} "
f"{delta} "
f"%(levelname)s "
f"%(filename)s:%(lineno)d: "
"%(message)s"
"\033[0m" # end style
)
else:
self._style._fmt = ( # noqa
f'\033[{color}m%(levelname)s\033[0m: %(message)s'
f"\033[{color}m%(levelname)s\033[0m: %(message)s"
)
return super().format(record)
@ -143,33 +145,33 @@ handler = logging.StreamHandler()
handler.setFormatter(Formatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logging.getLogger('asyncio').setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)
VERSION = ""
EVDEV_VERSION = None
try:
VERSION = pkg_resources.require('key-mapper')[0].version
EVDEV_VERSION = pkg_resources.require('evdev')[0].version
VERSION = pkg_resources.require("key-mapper")[0].version
EVDEV_VERSION = pkg_resources.require("evdev")[0].version
except pkg_resources.DistributionNotFound as error:
VERSION = ''
EVDEV_VERSION = None
logger.info('Could not figure out the version')
logger.info("Could not figure out the version")
logger.debug(error)
def log_info(name='key-mapper'):
def log_info(name="key-mapper"):
"""Log version and name to the console."""
logger.info(
'%s %s %s https://github.com/sezanzeb/key-mapper',
name, VERSION, COMMIT_HASH
"%s %s %s https://github.com/sezanzeb/key-mapper", name, VERSION, COMMIT_HASH
)
if EVDEV_VERSION:
logger.info('python-evdev %s', EVDEV_VERSION)
logger.info("python-evdev %s", EVDEV_VERSION)
if is_debug():
logger.warning(
'Debug level will log all your keystrokes! Do not post this '
'output in the internet if you typed in sensitive or private '
'information with your device!'
"Debug level will log all your keystrokes! Do not post this "
"output in the internet if you typed in sensitive or private "
"information with your device!"
)
@ -185,12 +187,13 @@ def update_verbosity(debug):
try:
from rich.traceback import install
install(show_locals=True)
logger.debug('Using rich.traceback')
logger.debug("Using rich.traceback")
except Exception as error:
# since this is optional, just skip all exceptions
if not isinstance(error, ImportError):
logger.debug('Cannot use rich.traceback: %s', error)
logger.debug("Cannot use rich.traceback: %s", error)
else:
logger.setLevel(logging.INFO)

@ -34,7 +34,7 @@ from keymapper.config import ConfigBase, config
from keymapper.key import Key
DISABLE_NAME = 'disable'
DISABLE_NAME = "disable"
DISABLE_CODE = -1
@ -43,17 +43,17 @@ def split_key(key):
"""Take a key like "1,2,3" and return a 3-tuple of ints."""
key = key.strip()
if ',' not in key:
if "," not in key:
logger.error('Found invalid key: "%s"', key)
return None
if key.count(',') == 1:
if key.count(",") == 1:
# support for legacy mapping objects that didn't include
# the value in the key
ev_type, code = key.split(',')
ev_type, code = key.split(",")
value = 1
elif key.count(',') == 2:
ev_type, code, value = key.split(',')
elif key.count(",") == 2:
ev_type, code, value = key.split(",")
else:
logger.error('Found more than two commas in the key: "%s"', key)
return None
@ -69,6 +69,7 @@ def split_key(key):
class Mapping(ConfigBase):
"""Contains and manages mappings and config of a single preset."""
def __init__(self):
self._mapping = {} # a mapping of Key objects to strings
self.changed = False
@ -112,10 +113,10 @@ class Mapping(ConfigBase):
(1, 11, 1), provide (1, 10, 1) here.
"""
if not isinstance(new_key, Key):
raise TypeError(f'Expected {new_key} to be a Key object')
raise TypeError(f"Expected {new_key} to be a Key object")
if symbol is None:
raise ValueError('Expected `symbol` not to be None')
raise ValueError("Expected `symbol` not to be None")
symbol = symbol.strip()
logger.debug('%s will map to "%s"', new_key, symbol)
@ -139,11 +140,11 @@ class Mapping(ConfigBase):
key : Key
"""
if not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
raise TypeError("Expected key to be a Key object")
for permutation in key.get_permutations():
if permutation in self._mapping:
logger.debug('%s will be cleared', permutation)
logger.debug("%s will be cleared", permutation)
del self._mapping[permutation]
self.changed = True
# there should be only one variation of the permutations
@ -165,29 +166,31 @@ class Mapping(ConfigBase):
logger.info('Loading preset from "%s"', path)
if not os.path.exists(path):
raise FileNotFoundError(
f'Tried to load non-existing preset "{path}"'
)
raise FileNotFoundError(f'Tried to load non-existing preset "{path}"')
self.clear_config()
with open(path, 'r') as file:
with open(path, "r") as file:
preset_dict = json.load(file)
if not isinstance(preset_dict.get('mapping'), dict):
if not isinstance(preset_dict.get("mapping"), dict):
logger.error(
'Expected mapping to be a dict, but was %s. '
"Expected mapping to be a dict, but was %s. "
'Invalid preset config at "%s"',
preset_dict.get('mapping'), path
preset_dict.get("mapping"),
path,
)
return
for key, symbol in preset_dict['mapping'].items():
for key, symbol in preset_dict["mapping"].items():
try:
key = Key(*[
split_key(chunk) for chunk in key.split('+')
if chunk.strip() != ''
])
key = Key(
*[
split_key(chunk)
for chunk in key.split("+")
if chunk.strip() != ""
]
)
except ValueError as error:
logger.error(str(error))
continue
@ -195,12 +198,12 @@ class Mapping(ConfigBase):
if None in key:
continue
logger.spam('%s maps to %s', key, symbol)
logger.spam("%s maps to %s", key, symbol)
self._mapping[key] = symbol
# add any metadata of the mapping
for key in preset_dict:
if key == 'mapping':
if key == "mapping":
continue
self._config[key] = preset_dict[key]
@ -216,16 +219,15 @@ class Mapping(ConfigBase):
def save(self, path):
"""Dump as JSON into home."""
logger.info('Saving preset to %s', path)
logger.info("Saving preset to %s", path)
touch(path)
with open(path, 'w') as file:
if self._config.get('mapping') is not None:
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: %s',
self._config.get('mapping')
'"mapping" is reserved and cannot be used as config ' "key: %s",
self._config.get("mapping"),
)
preset_dict = self._config.copy() # shallow copy
@ -235,18 +237,14 @@ class Mapping(ConfigBase):
json_ready_mapping = {}
# tuple keys are not possible in json, encode them as string
for key, value in self._mapping.items():
new_key = '+'.join([
','.join([
str(value)
for value in sub_key
])
for sub_key in key
])
new_key = "+".join(
[",".join([str(value) for value in sub_key]) for sub_key in key]
)
json_ready_mapping[new_key] = value
preset_dict['mapping'] = json_ready_mapping
preset_dict["mapping"] = json_ready_mapping
json.dump(preset_dict, file, indent=4)
file.write('\n')
file.write("\n")
self.changed = False
self.num_saved_keys = len(self)
@ -259,7 +257,7 @@ class Mapping(ConfigBase):
key : Key
"""
if not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
raise TypeError("Expected key to be a Key object")
for permutation in key.get_permutations():
existing = self._mapping.get(permutation)
@ -272,6 +270,6 @@ class Mapping(ConfigBase):
"""Return True if this mapping disables BTN_Left."""
if self.get_symbol(Key(EV_KEY, BTN_LEFT, 1)) is not None:
values = [value.lower() for value in self._mapping.values()]
return 'btn_left' not in values
return "btn_left" not in values
return False

@ -40,8 +40,8 @@ def chown(path):
def touch(path, log=True):
"""Create an empty file and all its parent dirs, give it to the user."""
if path.endswith('/'):
raise ValueError(f'Expected path to not end with a slash: {path}')
if path.endswith("/"):
raise ValueError(f"Expected path to not end with a slash: {path}")
if os.path.exists(path):
return
@ -57,7 +57,7 @@ def touch(path, log=True):
def mkdir(path, log=True):
"""Create a folder, give it to the user."""
if path == '' or path is None:
if path == "" or path is None:
return
if os.path.exists(path):
@ -88,7 +88,7 @@ def remove(path):
def get_preset_path(group_name=None, preset=None):
"""Get a path to the stored preset, or to store a preset to."""
presets_base = os.path.join(CONFIG_PATH, 'presets')
presets_base = os.path.join(CONFIG_PATH, "presets")
if group_name is None:
return presets_base
@ -97,8 +97,8 @@ def get_preset_path(group_name=None, preset=None):
# the extension of the preset should not be shown in the ui.
# if a .json extension arrives this place, it has not been
# stripped away properly prior to this.
assert not preset.endswith('.json')
preset = f'{preset}.json'
assert not preset.endswith(".json")
preset = f"{preset}.json"
if preset is None:
return os.path.join(presets_base, group_name)

@ -37,9 +37,9 @@ def migrate_path():
Move existing presets into the new subfolder "presets"
"""
new_preset_folder = os.path.join(CONFIG_PATH, 'presets')
new_preset_folder = os.path.join(CONFIG_PATH, "presets")
if not os.path.exists(get_preset_path()) and os.path.exists(CONFIG_PATH):
logger.info('Migrating presets from < 0.4.0...')
logger.info("Migrating presets from < 0.4.0...")
devices = os.listdir(CONFIG_PATH)
mkdir(get_preset_path())
for device in devices:
@ -48,38 +48,38 @@ def migrate_path():
target = path.replace(CONFIG_PATH, new_preset_folder)
logger.info('Moving "%s" to "%s"', path, target)
os.rename(path, target)
logger.info('done')
logger.info("done")
migrate_path()
def get_available_preset_name(group_name, preset='new preset', copy=False):
def get_available_preset_name(group_name, preset="new preset", copy=False):
"""Increment the preset name until it is available."""
if group_name is None:
# endless loop otherwise
raise ValueError('group_name may not be None')
raise ValueError("group_name may not be None")
preset = preset.strip()
if copy and not re.match(r'^.+\scopy( \d+)?$', preset):
preset = f'{preset} copy'
if copy and not re.match(r"^.+\scopy( \d+)?$", preset):
preset = f"{preset} copy"
# find a name that is not already taken
if os.path.exists(get_preset_path(group_name, preset)):
# if there already is a trailing number, increment it instead of
# adding another one
match = re.match(r'^(.+) (\d+)$', preset)
match = re.match(r"^(.+) (\d+)$", preset)
if match:
preset = match[1]
i = int(match[2]) + 1
else:
i = 2
while os.path.exists(get_preset_path(group_name, f'{preset} {i}')):
while os.path.exists(get_preset_path(group_name, f"{preset} {i}")):
i += 1
return f'{preset} {i}'
return f"{preset} {i}"
return preset
@ -94,7 +94,7 @@ def get_presets(group_name):
device_folder = get_preset_path(group_name)
mkdir(device_folder)
paths = glob.glob(os.path.join(device_folder, '*.json'))
paths = glob.glob(os.path.join(device_folder, "*.json"))
presets = [
os.path.splitext(os.path.basename(path))[0]
for path in sorted(paths, key=os.path.getmtime)
@ -128,17 +128,16 @@ def find_newest_preset(group_name=None):
# sort the oldest files to the front in order to use pop to get the newest
if group_name is None:
paths = sorted(
glob.glob(os.path.join(get_preset_path(), '*/*.json')),
key=os.path.getmtime
glob.glob(os.path.join(get_preset_path(), "*/*.json")), key=os.path.getmtime
)
else:
paths = sorted(
glob.glob(os.path.join(get_preset_path(group_name), '*.json')),
key=os.path.getmtime
glob.glob(os.path.join(get_preset_path(group_name), "*.json")),
key=os.path.getmtime,
)
if len(paths) == 0:
logger.debug('No presets found')
logger.debug("No presets found")
return get_any_preset()
group_names = groups.list_group_names()
@ -154,7 +153,7 @@ def find_newest_preset(group_name=None):
break
if newest_path is None:
logger.debug('None of the configured devices is currently online')
logger.debug("None of the configured devices is currently online")
return get_any_preset()
preset = os.path.splitext(preset)[0]
@ -188,7 +187,7 @@ def rename_preset(group_name, old_preset_name, new_preset_name):
logger.info('Moving "%s" to "%s"', old_preset_name, new_preset_name)
os.rename(
get_preset_path(group_name, old_preset_name),
get_preset_path(group_name, new_preset_name)
get_preset_path(group_name, new_preset_name),
)
# set the modification date to now
now = time.time()

@ -19,7 +19,7 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
"""Create some singleton objects that are needed for the app to work."""
"""Make the systems/environments mapping of keys and codes accessible."""
import re
@ -35,11 +35,12 @@ from keymapper.paths import get_config_path, touch, USER
# xkb uses keycodes that are 8 higher than those from evdev
XKB_KEYCODE_OFFSET = 8
XMODMAP_FILENAME = 'xmodmap.json'
XMODMAP_FILENAME = "xmodmap.json"
class SystemMapping:
"""Stores information about all available keycodes."""
def __init__(self):
"""Construct the system_mapping."""
self._mapping = {}
@ -60,29 +61,28 @@ class SystemMapping:
def populate(self):
"""Get a mapping of all available names to their keycodes."""
logger.debug('Gathering available keycodes')
logger.debug("Gathering available keycodes")
self.clear()
xmodmap_dict = {}
try:
xmodmap = subprocess.check_output(
['xmodmap', '-pke'],
stderr=subprocess.STDOUT
["xmodmap", "-pke"], stderr=subprocess.STDOUT
).decode()
xmodmap = xmodmap
self._xmodmap = re.findall(r'(\d+) = (.+)\n', xmodmap + '\n')
self._xmodmap = re.findall(r"(\d+) = (.+)\n", xmodmap + "\n")
xmodmap_dict = self._find_legit_mappings()
if len(xmodmap_dict) == 0:
logger.info('`xmodmap -pke` did not yield any symbol')
logger.info("`xmodmap -pke` did not yield any symbol")
except (subprocess.CalledProcessError, FileNotFoundError):
# might be within a tty
logger.info('`xmodmap` command not found')
logger.info("`xmodmap` command not found")
if USER != 'root':
if USER != "root":
# write this stuff into the key-mapper config directory, because
# the systemd service won't know the user sessions xmodmap
path = get_config_path(XMODMAP_FILENAME)
touch(path)
with open(path, 'w') as file:
with open(path, "w") as file:
logger.debug('Writing "%s"', path)
json.dump(xmodmap_dict, file, indent=4)
@ -90,7 +90,7 @@ class SystemMapping:
self._set(name, code)
for name, ecode in evdev.ecodes.ecodes.items():
if name.startswith('KEY') or name.startswith('BTN'):
if name.startswith("KEY") or name.startswith("BTN"):
self._set(name, ecode)
self._set(DISABLE_NAME, DISABLE_CODE)
@ -150,8 +150,5 @@ class SystemMapping:
return xmodmap_dict
# one mapping object for the GUI application
custom_mapping = Mapping()
# this mapping represents the xmodmap output, which stays constant
system_mapping = SystemMapping()

@ -36,20 +36,20 @@ def get_user():
pass
try:
user = os.environ['USER']
user = os.environ["USER"]
except KeyError:
# possibly the systemd service. no sudo was used
return getpass.getuser()
if user == 'root':
if user == "root":
try:
return os.environ['SUDO_USER']
return os.environ["SUDO_USER"]
except KeyError:
# no sudo was used
pass
try:
pkexec_uid = int(os.environ['PKEXEC_UID'])
pkexec_uid = int(os.environ["PKEXEC_UID"])
return pwd.getpwuid(pkexec_uid).pw_name
except KeyError:
# no pkexec was used or the uid is unknown
@ -58,8 +58,13 @@ def get_user():
return user
def get_home(user):
"""Try to find the user's home directory."""
return pwd.getpwnam(user).pw_dir
USER = get_user()
HOME = '/root' if USER == 'root' else f'/home/{USER}'
HOME = get_home(USER)
CONFIG_PATH = os.path.join(HOME, '.config/key-mapper')
CONFIG_PATH = os.path.join(HOME, ".config/key-mapper")

@ -25,8 +25,17 @@
import math
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY, \
EV_REL, REL_WHEEL, REL_HWHEEL
from evdev.ecodes import (
EV_KEY,
EV_ABS,
ABS_X,
ABS_Y,
ABS_RX,
ABS_RY,
EV_REL,
REL_WHEEL,
REL_HWHEEL,
)
from keymapper.logger import logger
from keymapper.config import BUTTONS
@ -47,7 +56,7 @@ STYLUS = [
(EV_ABS, evdev.ecodes.ABS_TILT_X),
(EV_ABS, evdev.ecodes.ABS_TILT_Y),
(EV_KEY, evdev.ecodes.BTN_DIGI),
(EV_ABS, evdev.ecodes.ABS_PRESSURE)
(EV_ABS, evdev.ecodes.ABS_PRESSURE),
]
@ -57,6 +66,13 @@ STYLUS = [
JOYSTICK_BUTTON_THRESHOLD = math.sin((math.pi / 2) / 3 * 1)
PRESS = 1
# D-Pads and joysticks can have a second press event, which moves the knob to the
# opposite side, reporting a negative value
PRESS_NEGATIVE = -1
RELEASE = 0
def sign(value):
"""Return -1, 0 or 1 depending on the input value."""
if value > 0:
@ -68,13 +84,20 @@ def sign(value):
return 0
def normalize_value(event, abs_range=None):
"""Fit the event value to one of 0, 1 or -1."""
def classify_action(event, abs_range=None):
"""Fit the event value to one of PRESS, PRESS_NEGATIVE or RELEASE
A joystick that is pushed to the very side will probably send a high value, whereas
having it close to the middle might send values close to 0 with some noise. A value
of 1 is usually noise or from touching the joystick very gently and considered in
resting position.
"""
if event.type == EV_ABS and event.code in JOYSTICK:
if abs_range is None:
logger.error(
'Got %s, but abs_range is %s',
(event.type, event.code, event.value), abs_range
"Got %s, but abs_range is %s",
(event.type, event.code, event.value),
abs_range,
)
return event.value
@ -93,6 +116,16 @@ def normalize_value(event, abs_range=None):
return sign(event.value)
def is_key_down(action):
"""Is this action a key press."""
return action in [PRESS, PRESS_NEGATIVE]
def is_key_up(action):
"""Is this action a key release."""
return action == RELEASE
def is_wheel(event):
"""Check if this is a wheel event."""
return event.type == EV_REL and event.code in [REL_WHEEL, REL_HWHEEL]
@ -104,10 +137,7 @@ def will_report_key_up(event):
def should_map_as_btn(event, mapping, gamepad):
"""Does this event describe a button.
If it does, this function will make sure its value is one of [-1, 0, 1],
so that it matches the possible values in a mapping object if needed.
"""Does this event describe a button that is or can be mapped.
If a new kind of event should be mappable to buttons, this is the place
to add it.
@ -139,8 +169,8 @@ def should_map_as_btn(event, mapping, gamepad):
if not gamepad:
return False
l_purpose = mapping.get('gamepad.joystick.left_purpose')
r_purpose = mapping.get('gamepad.joystick.right_purpose')
l_purpose = mapping.get("gamepad.joystick.left_purpose")
r_purpose = mapping.get("gamepad.joystick.right_purpose")
if event.code in [ABS_X, ABS_Y] and l_purpose == BUTTONS:
return True
@ -178,8 +208,8 @@ def get_abs_range(device, code=ABS_X):
return None
absinfo = [
entry[1] for entry in
capabilities[EV_ABS]
entry[1]
for entry in capabilities[EV_ABS]
if (
entry[0] == code
and isinstance(entry, tuple)
@ -189,8 +219,7 @@ def get_abs_range(device, code=ABS_X):
if len(absinfo) == 0:
logger.error(
'Failed to get ABS info of "%s" for key %d: %s',
device, code, capabilities
'Failed to get ABS info of "%s" for key %d: %s', device, code, capabilities
)
return None

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

@ -86,7 +86,7 @@ ssh/login into a debian/ubuntu environment
./scripts/build.sh
```
This will generate `key-mapper/deb/key-mapper-1.0.0.deb`
This will generate `key-mapper/deb/key-mapper-1.1.0.deb`
## Badges

@ -16,6 +16,10 @@ It is possible to write timed macros into the center column:
- `mouse` and `wheel` take a direction like "up" and speed as parameters
- `set` set a variable to a value, visible to all injection processes
- `ifeq` if that variable is a certain value do something
- `if_tap` if a key is tapped quickly, execute the first param, otherwise the
second. The third param is the time in milliseconds
- `if_single` if no other key is pressed until the keys release, execute
the first param, otherwise the second
The names for the most common functions are kept short, to make it easy to
write them into the constrained space.
@ -34,7 +38,11 @@ Examples:
- `ifeq(foo, 1, k(x), k(y))` if "foo" is 1, write x, otherwise y
- `h()` does nothing as long as your key is held down
- `h(a)` holds down "a" as long as the key is pressed, just like a
regular mapping
regular non-macro mapping
- `if_tap(k(a), k(b))` writes a if the key is tapped, otherwise b
- `if_tap(k(a), k(b), 1000)` writes a if the key is released within a second, otherwise b
- `if_single(k(a), k(b))` writes b if another key is pressed, or a if the key is released
and no other key was pressed in the meantime.
Syntax errors are shown in the UI on save. Each `k` function adds a short
delay of 10ms between key-down, key-up and at the end. See

@ -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.84</text>
<text x="62.0" y="14">9.84</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.69</text>
<text x="62.0" y="14">9.69</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -50,15 +50,21 @@ For example a combination of `LEFTSHIFT + a` for `b` would write "B" insetad,
because shift will be activated before you hit the "a". Therefore the
environment will see shift and a "b", which will then be capitalized.
A better option for a key combination would be `KP1 + a` instead of
`LEFTSHIFT + a`, because there won't be any side effect. You can disable
`KP1` by mapping it to `disable`, so you won't trigger writing a "1" into
your focused application.
The first option to work around this issue is to use `KP1 + a` instead of
`LEFTSHIFT + a`. You can disable `KP1` by mapping it to `disable`, so you
won't trigger writing a "1" into your focused application.
<p align="center">
<img src="combination.png"/>
</p>
The second option is to release the modifier in your combination by writing
the modifier one more time. This will write lowercase "a" characters.
<p align="center">
<img src="combination_workaround.png"/>
</p>
## Writing Combinations
You can write `Control_L + a` as mapping, which will inject those two
@ -147,7 +153,7 @@ configuration files.
The default configuration is stored at `~/.config/key-mapper/config.json`,
which doesn't include any mappings, but rather other parameters that
are interesting for injections. The current default configuration as of 1.0.0
are interesting for injections. The current default configuration as of 1.1.0
looks like, with an example autoload entry:
```json

@ -9,8 +9,8 @@ build_deb() {
mv build/deb/usr/local/lib/python3.*/ build/deb/usr/lib/python3/
cp ./DEBIAN build/deb/ -r
mkdir dist -p
rm dist/key-mapper-1.0.0.deb || true
dpkg -b build/deb dist/key-mapper-1.0.0.deb
rm dist/key-mapper-1.1.0.deb || true
dpkg -b build/deb dist/key-mapper-1.1.0.deb
}
build_deb &

@ -34,10 +34,17 @@ PO_FILES = 'po/*.po'
class Install(install):
"""Add the commit hash and build .mo translations."""
def run(self):
commit = os.popen('git rev-parse HEAD').read().strip()
if re.match(r'^([a-z]|[0-9])+$', commit):
with open('keymapper/commit_hash.py', 'w') as f:
f.write(f"COMMIT_HASH = '{commit}'\n")
try:
commit = os.popen('git rev-parse HEAD').read().strip()
if re.match(r'^([a-z]|[0-9])+$', commit):
# for whatever reason different systems have different paths here
build_dir = ''
if os.path.exists('build/lib/keymapper'):
build_dir = 'build/lib/'
with open(f'{build_dir}keymapper/commit_hash.py', 'w+') as f:
f.write(f"COMMIT_HASH = '{commit}'\n")
except Exception as e:
print('Failed to save the commit hash:', e)
# generate .mo files
make_lang()
@ -45,20 +52,25 @@ class Install(install):
install.run(self)
def get_packages():
def get_packages(base='keymapper'):
"""Return all modules used in key-mapper.
For example 'keymapper.gui'.
For example 'keymapper.gui' or 'keymapper.injection.consumers'
"""
result = ['keymapper']
for name in os.listdir('keymapper'):
if not os.path.isdir(f'keymapper/{name}'):
if not os.path.exists(os.path.join(base, '__init__.py')):
# only python modules
return []
result = [base.replace('/', '.')]
for name in os.listdir(base):
if not os.path.isdir(os.path.join(base, name)):
continue
if name == '__pycache__':
continue
result.append(f'keymapper.{name}')
# find more python submodules in that directory
result += get_packages(os.path.join(base, name))
return result
@ -84,7 +96,7 @@ for po_file in glob.glob(PO_FILES):
setup(
name='key-mapper',
version='1.0.0',
version='1.1.0',
description='A tool to change the mapping of your input device buttons',
author='Sezanzeb',
author_email='proxima@sezanzeb.de',

@ -37,15 +37,16 @@ from unittest.mock import patch
import evdev
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
from xmodmap import xmodmap
assert not os.getcwd().endswith('tests')
assert not os.getcwd().endswith("tests")
os.environ['UNITTEST'] = '1'
os.environ["UNITTEST"] = "1"
def grey_log(*msgs):
@ -55,7 +56,7 @@ def grey_log(*msgs):
def is_service_running():
"""Check if the daemon is running."""
try:
subprocess.check_output(['pgrep', '-f', 'key-mapper-service'])
subprocess.check_output(["pgrep", "-f", "key-mapper-service"])
return True
except subprocess.CalledProcessError:
return False
@ -68,11 +69,11 @@ def join_children():
i = 0
time.sleep(EVENT_READ_TIMEOUT)
children = this.children(recursive=True)
while len([c for c in children if c.status() != 'zombie']) > 0:
while len([c for c in children if c.status() != "zombie"]) > 0:
for child in children:
if i > 10:
child.kill()
grey_log(f'Killed pid {child.pid} because it didn\'t finish in time')
grey_log(f"Killed pid {child.pid} because it didn't finish in time")
children = this.children(recursive=True)
time.sleep(EVENT_READ_TIMEOUT)
@ -81,7 +82,7 @@ def join_children():
if is_service_running():
# let tests control daemon existance
raise Exception('Expected the service not to be running already.')
raise Exception("Expected the service not to be running already.")
# make sure the "tests" module visible
@ -97,11 +98,11 @@ EVENT_READ_TIMEOUT = 0.01
START_READING_DELAY = 0.05
# for joysticks
MIN_ABS = -2 ** 15
MIN_ABS = -(2 ** 15)
MAX_ABS = 2 ** 15
tmp = '/tmp/key-mapper-test'
tmp = "/tmp/key-mapper-test"
uinput_write_history = []
# for tests that makes the injector create its processes
uinput_write_history_pipe = multiprocessing.Pipe()
@ -123,76 +124,69 @@ def read_write_history_pipe():
# key-mapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored.
phys_foo = 'usb-0000:03:00.0-1/input2'
phys_foo = "usb-0000:03:00.0-1/input2"
info_foo = evdev.device.DeviceInfo(1, 1, 1, 1)
keyboard_keys = sorted(evdev.ecodes.keys.keys())[:255]
fixtures = {
'/dev/input/event1': {
'capabilities': {
evdev.ecodes.EV_KEY: [
evdev.ecodes.KEY_A
],
"/dev/input/event1": {
"capabilities": {
evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A],
},
'phys': 'usb-0000:03:00.0-0/input1',
'info': info_foo,
'name': 'Foo Device'
"phys": "usb-0000:03:00.0-0/input1",
"info": info_foo,
"name": "Foo Device",
},
# Another "Foo Device", which will get an incremented key.
# If possible write tests using this one, because name != key here and
# that would be important to test as well. Otherwise the tests can't
# see if the groups correct attribute is used in functions and paths.
'/dev/input/event11': {
'capabilities': {
evdev.ecodes.EV_KEY: [
evdev.ecodes.BTN_LEFT
],
"/dev/input/event11": {
"capabilities": {
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_LEFT],
evdev.ecodes.EV_REL: [
evdev.ecodes.REL_X,
evdev.ecodes.REL_Y,
evdev.ecodes.REL_WHEEL,
evdev.ecodes.REL_HWHEEL
]
evdev.ecodes.REL_HWHEEL,
],
},
'phys': f'{phys_foo}/input2',
'info': info_foo,
'name': 'Foo Device foo',
'group_key': 'Foo Device 2' # expected key
"phys": f"{phys_foo}/input2",
"info": info_foo,
"name": "Foo Device foo",
"group_key": "Foo Device 2", # expected key
},
'/dev/input/event10': {
'capabilities': {evdev.ecodes.EV_KEY: keyboard_keys},
'phys': f'{phys_foo}/input3',
'info': info_foo,
'name': 'Foo Device',
'group_key': 'Foo Device 2'
"/dev/input/event10": {
"capabilities": {evdev.ecodes.EV_KEY: keyboard_keys},
"phys": f"{phys_foo}/input3",
"info": info_foo,
"name": "Foo Device",
"group_key": "Foo Device 2",
},
'/dev/input/event13': {
'capabilities': {evdev.ecodes.EV_KEY: [], evdev.ecodes.EV_SYN: []},
'phys': f'{phys_foo}/input1',
'info': info_foo,
'name': 'Foo Device',
'group_key': 'Foo Device 2'
"/dev/input/event13": {
"capabilities": {evdev.ecodes.EV_KEY: [], evdev.ecodes.EV_SYN: []},
"phys": f"{phys_foo}/input1",
"info": info_foo,
"name": "Foo Device",
"group_key": "Foo Device 2",
},
'/dev/input/event14': {
'capabilities': {evdev.ecodes.EV_SYN: []},
'phys': f'{phys_foo}/input0',
'info': info_foo,
'name': 'Foo Device qux',
'group_key': 'Foo Device 2'
"/dev/input/event14": {
"capabilities": {evdev.ecodes.EV_SYN: []},
"phys": f"{phys_foo}/input0",
"info": info_foo,
"name": "Foo Device qux",
"group_key": "Foo Device 2",
},
# Bar Device
'/dev/input/event20': {
'capabilities': {evdev.ecodes.EV_KEY: keyboard_keys},
'phys': 'usb-0000:03:00.0-2/input1',
'info': evdev.device.DeviceInfo(2, 1, 2, 1),
'name': 'Bar Device'
"/dev/input/event20": {
"capabilities": {evdev.ecodes.EV_KEY: keyboard_keys},
"phys": "usb-0000:03:00.0-2/input1",
"info": evdev.device.DeviceInfo(2, 1, 2, 1),
"name": "Bar Device",
},
'/dev/input/event30': {
'capabilities': {
"/dev/input/event30": {
"capabilities": {
evdev.ecodes.EV_SYN: [],
evdev.ecodes.EV_ABS: [
evdev.ecodes.ABS_X,
@ -201,40 +195,35 @@ fixtures = {
evdev.ecodes.ABS_RY,
evdev.ecodes.ABS_Z,
evdev.ecodes.ABS_RZ,
evdev.ecodes.ABS_HAT0X
evdev.ecodes.ABS_HAT0X,
],
evdev.ecodes.EV_KEY: [
evdev.ecodes.BTN_A
]
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_A],
},
'phys': '', # this is empty sometimes
'info': evdev.device.DeviceInfo(3, 1, 3, 1),
'name': 'gamepad'
"phys": "", # this is empty sometimes
"info": evdev.device.DeviceInfo(3, 1, 3, 1),
"name": "gamepad",
},
# device that is completely ignored
'/dev/input/event31': {
'capabilities': {evdev.ecodes.EV_SYN: []},
'phys': 'usb-0000:03:00.0-4/input1',
'info': evdev.device.DeviceInfo(4, 1, 4, 1),
'name': 'Power Button'
"/dev/input/event31": {
"capabilities": {evdev.ecodes.EV_SYN: []},
"phys": "usb-0000:03:00.0-4/input1",
"info": evdev.device.DeviceInfo(4, 1, 4, 1),
"name": "Power Button",
},
# key-mapper devices are not displayed in the ui, some instance
# of key-mapper started injecting apparently.
'/dev/input/event40': {
'capabilities': {evdev.ecodes.EV_KEY: keyboard_keys},
'phys': 'key-mapper/input1',
'info': evdev.device.DeviceInfo(5, 1, 5, 1),
'name': 'key-mapper Bar Device'
"/dev/input/event40": {
"capabilities": {evdev.ecodes.EV_KEY: keyboard_keys},
"phys": "key-mapper/input1",
"info": evdev.device.DeviceInfo(5, 1, 5, 1),
"name": "key-mapper Bar Device",
},
# denylisted
'/dev/input/event51': {
'capabilities': {evdev.ecodes.EV_KEY: keyboard_keys},
'phys': 'usb-0000:03:00.0-5/input1',
'info': evdev.device.DeviceInfo(6, 1, 6, 1),
'name': 'YuBiCofooYuBiKeYbar'
"/dev/input/event51": {
"capabilities": {evdev.ecodes.EV_KEY: keyboard_keys},
"phys": "usb-0000:03:00.0-5/input1",
"info": evdev.device.DeviceInfo(6, 1, 6, 1),
"name": "YuBiCofooYuBiKeYbar",
},
}
@ -250,8 +239,8 @@ def setup_pipe(group_key):
# make sure those pipes exist before any process (the helper) gets forked,
# so that events can be pushed after the fork.
for fixture in fixtures.values():
if 'group_key' in fixture:
setup_pipe(fixture['group_key'])
if "group_key" in fixture:
setup_pipe(fixture["group_key"])
def get_events():
@ -294,7 +283,8 @@ def new_event(type, code, value, timestamp=None, offset=0):
def patch_paths():
from keymapper import paths
paths.CONFIG_PATH = '/tmp/key-mapper-test'
paths.CONFIG_PATH = "/tmp/key-mapper-test"
class InputDevice:
@ -303,18 +293,18 @@ class InputDevice:
path = None
def __init__(self, path):
if path != 'justdoit' and path not in fixtures:
if path != "justdoit" and path not in fixtures:
raise FileNotFoundError()
self.path = path
fixture = fixtures.get(path, {})
self.phys = fixture.get('phys', 'unset')
self.info = fixture.get('info', evdev.device.DeviceInfo(None, None, None, None))
self.name = fixture.get('name', 'unset')
self.phys = fixture.get("phys", "unset")
self.info = fixture.get("info", evdev.device.DeviceInfo(None, None, None, None))
self.name = fixture.get("name", "unset")
# this property exists only for test purposes and is not part of
# the original evdev.InputDevice class
self.group_key = fixture.get('group_key', self.name)
self.group_key = fixture.get("group_key", self.name)
# ensure a pipe exists to make this object act like
# it is reading events from a device
@ -322,6 +312,9 @@ class InputDevice:
self.fd = pending_events[self.group_key][1].fileno()
def push_events(self, events):
push_events(self.group_key, events)
def fileno(self):
"""Compatibility to select.select."""
return self.fd
@ -330,23 +323,23 @@ class InputDevice:
grey_log(f'{msg} "{self.name}" "{self.path}" {key}')
def absinfo(self, *args):
raise Exception('Ubuntus version of evdev doesn\'t support .absinfo')
raise Exception("Ubuntus version of evdev doesn't support .absinfo")
def grab(self):
grey_log('grab', self.name, self.path)
grey_log("grab", self.name, self.path)
def ungrab(self):
grey_log('ungrab', self.name, self.path)
grey_log("ungrab", self.name, self.path)
async def async_read_loop(self):
if pending_events.get(self.group_key) is None:
self.log('no events to read', self.group_key)
self.log("no events to read", self.group_key)
return
# consume all of them
while pending_events[self.group_key][1].poll():
result = pending_events[self.group_key][1].recv()
self.log(result, 'async_read_loop')
self.log(result, "async_read_loop")
yield result
await asyncio.sleep(0.01)
@ -359,13 +352,13 @@ class InputDevice:
# To be realistic it would have to check if the provided
# element is in its capabilities.
if self.group_key not in pending_events:
self.log('no events to read', self.group_key)
self.log("no events to read", self.group_key)
return
# consume all of them
while pending_events[self.group_key][1].poll():
event = pending_events[self.group_key][1].recv()
self.log(event, 'read')
self.log(event, "read")
yield event
time.sleep(EVENT_READ_TIMEOUT)
@ -374,7 +367,7 @@ class InputDevice:
while True:
event = pending_events[self.group_key][1].recv()
if event is not None:
self.log(event, 'read_loop')
self.log(event, "read_loop")
yield event
time.sleep(EVENT_READ_TIMEOUT)
@ -393,16 +386,20 @@ class InputDevice:
# failed in tests sometimes
return None
self.log(event, 'read_one')
self.log(event, "read_one")
return event
def capabilities(self, absinfo=True, verbose=False):
result = copy.deepcopy(fixtures[self.path]['capabilities'])
result = copy.deepcopy(fixtures[self.path]["capabilities"])
if absinfo and evdev.ecodes.EV_ABS in result:
absinfo_obj = evdev.AbsInfo(
value=None, min=MIN_ABS, fuzz=None, flat=None,
resolution=None, max=MAX_ABS
value=None,
min=MIN_ABS,
fuzz=None,
flat=None,
resolution=None,
max=MAX_ABS,
)
result[evdev.ecodes.EV_ABS] = [
(stuff, absinfo_obj) for stuff in result[evdev.ecodes.EV_ABS]
@ -415,10 +412,10 @@ uinputs = {}
class UInput:
def __init__(self, events=None, name='unnamed', *args, **kwargs):
def __init__(self, events=None, name="unnamed", *args, **kwargs):
self.fd = 0
self.write_count = 0
self.device = InputDevice('justdoit')
self.device = InputDevice("justdoit")
self.name = name
self.events = events
self.write_history = []
@ -435,7 +432,7 @@ class UInput:
uinput_write_history.append(event)
uinput_write_history_pipe[1].send(event)
self.write_history.append(event)
grey_log(f'{(type, code, value)} written')
grey_log(f"{(type, code, value)} written")
def syn(self):
pass
@ -447,13 +444,7 @@ class InputEvent(evdev.InputEvent):
super().__init__(sec, usec, type, code, value)
def copy(self):
return InputEvent(
self.sec,
self.usec,
self.type,
self.code,
self.value
)
return InputEvent(self.sec, self.usec, self.type, self.code, self.value)
def patch_evdev():
@ -469,7 +460,7 @@ def patch_evdev():
def patch_events():
# improve logging of stuff
evdev.InputEvent.__str__ = lambda self: (
f'InputEvent{(self.type, self.code, self.value)}'
f"InputEvent{(self.type, self.code, self.value)}"
)
@ -478,11 +469,11 @@ def patch_os_system():
original_system = os.system
def system(command):
if 'pkexec' in command:
if "pkexec" in command:
# because it
# - will open a window for user input
# - has no knowledge of the fixtures and patches
raise Exception('Write patches to avoid running pkexec stuff')
raise Exception("Write patches to avoid running pkexec stuff")
return original_system(command)
os.system = system
@ -497,7 +488,7 @@ def patch_check_output():
original_check_output = subprocess.check_output
def check_output(command, *args, **kwargs):
if 'xmodmap' in command and '-pke' in command:
if "xmodmap" in command and "-pke" in command:
return xmodmap
return original_check_output(command, *args, **kwargs)
@ -528,10 +519,11 @@ from keymapper.injection.injector import Injector
from keymapper.config import config
from keymapper.gui.reader import reader
from keymapper.groups import groups
from keymapper.state import system_mapping, custom_mapping
from keymapper.system_mapping import system_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.paths import get_config_path
from keymapper.injection.macros import macro_variables
from keymapper.injection.keycode_mapper import active_macros, unreleased
from keymapper.injection.macros.macro import macro_variables
from keymapper.injection.consumers.keycode_mapper import active_macros, unreleased
# no need for a high number in tests
Injector.regrab_timeout = 0.05
@ -543,19 +535,18 @@ environ_copy = copy.deepcopy(os.environ)
def send_event_to_reader(event):
"""Act like the helper and send input events to the reader."""
reader._results._unread.append({
'type': 'event',
'message': (
event.sec, event.usec,
event.type, event.code, event.value
)
})
reader._results._unread.append(
{
"type": "event",
"message": (event.sec, event.usec, event.type, event.code, event.value),
}
)
def quick_cleanup(log=True):
"""Reset the applications state."""
if log:
print('quick cleanup')
print("quick cleanup")
for device in list(pending_events.keys()):
try:
@ -573,12 +564,17 @@ def quick_cleanup(log=True):
except (BrokenPipeError, OSError):
pass
if asyncio.get_event_loop().is_running():
for task in asyncio.all_tasks():
task.cancel()
try:
if asyncio.get_event_loop().is_running():
for task in asyncio.all_tasks():
task.cancel()
except RuntimeError:
# happens when the event loop disappears for magical reasons
# create a fresh event loop
asyncio.set_event_loop(asyncio.new_event_loop())
if not macro_variables.process.is_alive():
raise AssertionError('the SharedDict manager is not running anymore')
raise AssertionError("the SharedDict manager is not running anymore")
macro_variables._stop()
@ -589,7 +585,7 @@ def quick_cleanup(log=True):
if os.path.exists(tmp):
shutil.rmtree(tmp)
config.path = os.path.join(get_config_path(), 'config.json')
config.path = os.path.join(get_config_path(), "config.json")
config.clear_config()
config.save_config()
@ -633,10 +629,10 @@ def cleanup():
Using this is slower, usually quick_cleanup() is sufficient.
"""
print('cleanup')
print("cleanup")
os.system('pkill -f key-mapper-service')
os.system('pkill -f key-mapper-control')
os.system("pkill -f key-mapper-service")
os.system("pkill -f key-mapper-control")
time.sleep(0.05)
quick_cleanup(log=False)
@ -660,13 +656,11 @@ def main():
# `tests/test.py test_integration.TestIntegration.test_can_start`
# or `tests/test.py test_integration test_daemon`
testsuite = unittest.defaultTestLoader.loadTestsFromNames(
[f'testcases.{module}' for module in modules]
[f"testcases.{module}" for module in modules]
)
else:
# run all tests by default
testsuite = unittest.defaultTestLoader.discover(
'testcases', pattern='*.py'
)
testsuite = unittest.defaultTestLoader.discover("testcases", pattern="*.py")
# add a newline to each "qux (foo.bar)..." output before each test,
# because the first log will be on the same line otherwise

@ -34,27 +34,27 @@ class TestConfig(unittest.TestCase):
self.assertEqual(len(config.iterate_autoload_presets()), 0)
def test_migrate(self):
old = os.path.join(CONFIG_PATH, 'config')
new = os.path.join(CONFIG_PATH, 'config.json')
old = os.path.join(CONFIG_PATH, "config")
new = os.path.join(CONFIG_PATH, "config.json")
os.remove(new)
touch(old)
with open(old, 'w') as f:
f.write('{}')
with open(old, "w") as f:
f.write("{}")
GlobalConfig()
self.assertTrue(os.path.exists(new))
self.assertFalse(os.path.exists(old))
def test_wont_migrate(self):
old = os.path.join(CONFIG_PATH, 'config')
new = os.path.join(CONFIG_PATH, 'config.json')
old = os.path.join(CONFIG_PATH, "config")
new = os.path.join(CONFIG_PATH, "config.json")
touch(new)
with open(new, 'w') as f:
f.write('{}')
with open(new, "w") as f:
f.write("{}")
touch(old)
with open(old, 'w') as f:
f.write('{}')
with open(old, "w") as f:
f.write("{}")
GlobalConfig()
self.assertTrue(os.path.exists(new))
@ -62,67 +62,63 @@ class TestConfig(unittest.TestCase):
def test_get_default(self):
config._config = {}
self.assertEqual(config.get('gamepad.joystick.non_linearity'), 4)
self.assertEqual(config.get("gamepad.joystick.non_linearity"), 4)
config.set('gamepad.joystick.non_linearity', 3)
self.assertEqual(config.get('gamepad.joystick.non_linearity'), 3)
config.set("gamepad.joystick.non_linearity", 3)
self.assertEqual(config.get("gamepad.joystick.non_linearity"), 3)
def test_basic(self):
self.assertEqual(config.get('a'), None)
self.assertEqual(config.get("a"), None)
config.set('a', 1)
self.assertEqual(config.get('a'), 1)
config.set("a", 1)
self.assertEqual(config.get("a"), 1)
config.remove('a')
config.set('a.b', 2)
self.assertEqual(config.get('a.b'), 2)
self.assertEqual(config._config['a']['b'], 2)
config.remove("a")
config.set("a.b", 2)
self.assertEqual(config.get("a.b"), 2)
self.assertEqual(config._config["a"]["b"], 2)
config.remove('a.b')
config.set('a.b.c', 3)
self.assertEqual(config.get('a.b.c'), 3)
self.assertEqual(config._config['a']['b']['c'], 3)
config.remove("a.b")
config.set("a.b.c", 3)
self.assertEqual(config.get("a.b.c"), 3)
self.assertEqual(config._config["a"]["b"]["c"], 3)
def test_autoload(self):
self.assertEqual(len(config.iterate_autoload_presets()), 0)
self.assertFalse(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2.foo', 'b'))
self.assertEqual(config.get(['autoload', 'd1']), None)
self.assertEqual(config.get(['autoload', 'd2.foo']), None)
self.assertFalse(config.is_autoloaded("d1", "a"))
self.assertFalse(config.is_autoloaded("d2.foo", "b"))
self.assertEqual(config.get(["autoload", "d1"]), None)
self.assertEqual(config.get(["autoload", "d2.foo"]), None)
config.set_autoload_preset('d1', 'a')
config.set_autoload_preset("d1", "a")
self.assertEqual(len(config.iterate_autoload_presets()), 1)
self.assertTrue(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2.foo', 'b'))
self.assertTrue(config.is_autoloaded("d1", "a"))
self.assertFalse(config.is_autoloaded("d2.foo", "b"))
config.set_autoload_preset('d2.foo', 'b')
config.set_autoload_preset("d2.foo", "b")
self.assertEqual(len(config.iterate_autoload_presets()), 2)
self.assertTrue(config.is_autoloaded('d1', 'a'))
self.assertTrue(config.is_autoloaded('d2.foo', 'b'))
self.assertEqual(config.get(['autoload', 'd1']), 'a')
self.assertEqual(config.get('autoload.d1'), 'a')
self.assertEqual(config.get(['autoload', 'd2.foo']), 'b')
self.assertTrue(config.is_autoloaded("d1", "a"))
self.assertTrue(config.is_autoloaded("d2.foo", "b"))
self.assertEqual(config.get(["autoload", "d1"]), "a")
self.assertEqual(config.get("autoload.d1"), "a")
self.assertEqual(config.get(["autoload", "d2.foo"]), "b")
config.set_autoload_preset('d2.foo', 'c')
config.set_autoload_preset("d2.foo", "c")
self.assertEqual(len(config.iterate_autoload_presets()), 2)
self.assertTrue(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2.foo', 'b'))
self.assertTrue(config.is_autoloaded('d2.foo', 'c'))
self.assertEqual(config._config['autoload']['d2.foo'], 'c')
self.assertTrue(config.is_autoloaded("d1", "a"))
self.assertFalse(config.is_autoloaded("d2.foo", "b"))
self.assertTrue(config.is_autoloaded("d2.foo", "c"))
self.assertEqual(config._config["autoload"]["d2.foo"], "c")
self.assertListEqual(
list(config.iterate_autoload_presets()),
[('d1', 'a'), ('d2.foo', 'c')]
list(config.iterate_autoload_presets()), [("d1", "a"), ("d2.foo", "c")]
)
config.set_autoload_preset('d2.foo', None)
self.assertTrue(config.is_autoloaded('d1', 'a'))
self.assertFalse(config.is_autoloaded('d2.foo', 'b'))
self.assertFalse(config.is_autoloaded('d2.foo', 'c'))
self.assertListEqual(
list(config.iterate_autoload_presets()),
[('d1', 'a')]
)
self.assertEqual(config.get(['autoload', 'd1']), 'a')
config.set_autoload_preset("d2.foo", None)
self.assertTrue(config.is_autoloaded("d1", "a"))
self.assertFalse(config.is_autoloaded("d2.foo", "b"))
self.assertFalse(config.is_autoloaded("d2.foo", "c"))
self.assertListEqual(list(config.iterate_autoload_presets()), [("d1", "a")])
self.assertEqual(config.get(["autoload", "d1"]), "a")
def test_initial(self):
# when loading for the first time, create a config file with
@ -132,7 +128,7 @@ class TestConfig(unittest.TestCase):
config.load_config()
self.assertTrue(os.path.exists(config.path))
with open(config.path, 'r') as file:
with open(config.path, "r") as file:
contents = file.read()
self.assertIn('"keystroke_sleep_ms": 10', contents)
@ -142,22 +138,21 @@ class TestConfig(unittest.TestCase):
config.load_config()
self.assertEqual(len(config.iterate_autoload_presets()), 0)
config.set_autoload_preset('d1', 'a')
config.set_autoload_preset('d2.foo', 'b')
config.set_autoload_preset("d1", "a")
config.set_autoload_preset("d2.foo", "b")
config.save_config()
# ignored after load
config.set_autoload_preset('d3', 'c')
config.set_autoload_preset("d3", "c")
config.load_config()
self.assertListEqual(
list(config.iterate_autoload_presets()),
[('d1', 'a'), ('d2.foo', 'b')]
list(config.iterate_autoload_presets()), [("d1", "a"), ("d2.foo", "b")]
)
config_2 = os.path.join(tmp, 'config_2.json')
config_2 = os.path.join(tmp, "config_2.json")
touch(config_2)
with open(config_2, 'w') as f:
with open(config_2, "w") as f:
f.write('{"a":"b"}')
config.load_config(config_2)

@ -0,0 +1,204 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
import unittest
import asyncio
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, ABS_Y, EV_REL
from keymapper.injection.consumers.keycode_mapper import active_macros
from keymapper.config import BUTTONS, MOUSE, WHEEL
from keymapper.injection.context import Context
from keymapper.mapping import Mapping
from keymapper.key import Key
from keymapper.injection.consumer_control import ConsumerControl, consumer_classes
from keymapper.injection.consumers.consumer import Consumer
from keymapper.injection.consumers.keycode_mapper import KeycodeMapper
from keymapper.system_mapping import system_mapping
from tests.test import new_event, quick_cleanup
class ExampleConsumer(Consumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def is_enabled(self):
return True
async def notify(self, event):
pass
def is_handled(self, event):
pass
async def run(self):
pass
class TestConsumerControl(unittest.IsolatedAsyncioTestCase):
def setUp(self):
consumer_classes.append(ExampleConsumer)
self.gamepad_source = evdev.InputDevice("/dev/input/event30")
self.mapping = Mapping()
def tearDown(self):
quick_cleanup()
consumer_classes.remove(ExampleConsumer)
def setup(self, source, mapping):
"""Set a a ConsumerControl up for the test and run it in the background."""
forward_to = evdev.UInput()
context = Context(mapping)
context.uinput = evdev.UInput()
consumer_control = ConsumerControl(context, source, forward_to)
for consumer in consumer_control._consumers:
consumer._abs_range = (-10, 10)
asyncio.ensure_future(consumer_control.run())
return context, consumer_control
async def test_no_keycode_mapper_needed(self):
self.mapping.change(Key(EV_KEY, 1, 1), "b")
_, consumer_control = self.setup(self.gamepad_source, self.mapping)
consumer_types = [type(consumer) for consumer in consumer_control._consumers]
self.assertIn(KeycodeMapper, consumer_types)
self.mapping.empty()
_, consumer_control = self.setup(self.gamepad_source, self.mapping)
consumer_types = [type(consumer) for consumer in consumer_control._consumers]
self.assertNotIn(KeycodeMapper, consumer_types)
self.mapping.change(Key(EV_KEY, 1, 1), "k(a)")
_, consumer_control = self.setup(self.gamepad_source, self.mapping)
consumer_types = [type(consumer) for consumer in consumer_control._consumers]
self.assertIn(KeycodeMapper, consumer_types)
async def test_if_single_joystick_then(self):
# Integration test style for if_single.
# won't care about the event, because the purpose is not set to BUTTON
code_a = system_mapping.get("a")
code_shift = system_mapping.get("KEY_LEFTSHIFT")
trigger = 1
self.mapping.change(
Key(EV_KEY, trigger, 1), "if_single(k(a), k(KEY_LEFTSHIFT))"
)
self.mapping.change(Key(EV_ABS, ABS_Y, 1), "b")
self.mapping.set("gamepad.joystick.left_purpose", MOUSE)
self.mapping.set("gamepad.joystick.right_purpose", WHEEL)
context, _ = self.setup(self.gamepad_source, self.mapping)
self.gamepad_source.push_events(
[
new_event(EV_KEY, trigger, 1), # start the macro
new_event(EV_ABS, ABS_Y, 10), # ignored
new_event(EV_KEY, 2, 2), # ignored
new_event(EV_KEY, 2, 0), # ignored
new_event(EV_REL, 1, 1), # ignored
new_event(
EV_KEY, trigger, 0
), # stop it, the only way to trigger `then`
]
)
await asyncio.sleep(0.1)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
history = [a.t for a in context.uinput.write_history]
self.assertIn((EV_KEY, code_a, 1), history)
self.assertIn((EV_KEY, code_a, 0), history)
self.assertNotIn((EV_KEY, code_shift, 1), history)
self.assertNotIn((EV_KEY, code_shift, 0), history)
async def test_if_single_joystick_else(self):
"""triggers else + delayed_handle_keycode"""
# Integration test style for if_single.
# If a joystick that is mapped to a button is moved, if_single stops
code_b = system_mapping.get("b")
code_shift = system_mapping.get("KEY_LEFTSHIFT")
trigger = 1
self.mapping.change(
Key(EV_KEY, trigger, 1), "if_single(k(a), k(KEY_LEFTSHIFT))"
)
self.mapping.change(Key(EV_ABS, ABS_Y, 1), "b")
self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
context, _ = self.setup(self.gamepad_source, self.mapping)
self.gamepad_source.push_events(
[
new_event(EV_KEY, trigger, 1), # start the macro
new_event(EV_ABS, ABS_Y, 10), # not ignored, stops it
]
)
await asyncio.sleep(0.1)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
history = [a.t for a in context.uinput.write_history]
# the key that triggered if_single should be injected after
# if_single had a chance to inject keys (if the macro is fast enough),
# so that if_single can inject a modifier to e.g. capitalize the
# triggering key. This is important for the space cadet shift
self.assertListEqual(
history,
[
(EV_KEY, code_shift, 1),
(EV_KEY, code_b, 1), # would be capitalized now
(EV_KEY, code_shift, 0),
],
)
async def test_if_single_joystick_under_threshold(self):
"""triggers then because the joystick events value is too low."""
code_a = system_mapping.get("a")
trigger = 1
self.mapping.change(
Key(EV_KEY, trigger, 1), "if_single(k(a), k(KEY_LEFTSHIFT))"
)
self.mapping.change(Key(EV_ABS, ABS_Y, 1), "b")
self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
context, _ = self.setup(self.gamepad_source, self.mapping)
self.gamepad_source.push_events(
[
new_event(EV_KEY, trigger, 1), # start the macro
new_event(EV_ABS, ABS_Y, 1), # ignored because value too low
new_event(EV_KEY, trigger, 0), # stop, only way to trigger `then`
]
)
await asyncio.sleep(0.1)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
history = [a.t for a in context.uinput.write_history]
# the key that triggered if_single should be injected after
# if_single had a chance to inject keys (if the macro is fast enough),
# so that if_single can inject a modifier to e.g. capitalize the
# triggering key. This is important for the space cadet shift
self.assertListEqual(
history,
[
(EV_KEY, code_a, 1),
(EV_KEY, code_a, 0),
],
)

@ -25,94 +25,93 @@ from keymapper.injection.context import Context
from keymapper.mapping import Mapping
from keymapper.key import Key
from keymapper.config import NONE, MOUSE, WHEEL, BUTTONS
from keymapper.state import system_mapping
from keymapper.system_mapping import system_mapping
from tests.test import quick_cleanup
class TestContext(unittest.TestCase):
@classmethod
def setUpClass(cls):
quick_cleanup()
def setUp(self):
self.mapping = Mapping()
self.mapping.set('gamepad.joystick.left_purpose', WHEEL)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.mapping.change(Key(1, 31, 1), 'k(a)')
self.mapping.change(Key(1, 32, 1), 'b')
self.mapping.change(Key((1, 33, 1), (1, 34, 1), (1, 35, 1)), 'c')
self.mapping.set("gamepad.joystick.left_purpose", WHEEL)
self.mapping.set("gamepad.joystick.right_purpose", WHEEL)
self.mapping.change(Key(1, 31, 1), "k(a)")
self.mapping.change(Key(1, 32, 1), "b")
self.mapping.change(Key((1, 33, 1), (1, 34, 1), (1, 35, 1)), "c")
self.context = Context(self.mapping)
def test_update_purposes(self):
self.mapping.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', MOUSE)
self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", MOUSE)
self.context.update_purposes()
self.assertEqual(self.context.left_purpose, BUTTONS)
self.assertEqual(self.context.right_purpose, MOUSE)
def test_parse_macros(self):
self.assertEqual(len(self.context.macros), 1)
self.assertEqual(self.context.macros[((1, 31, 1),)].code, 'k(a)')
self.assertEqual(self.context.macros[((1, 31, 1),)].code, "k(a)")
def test_map_keys_to_codes(self):
b = system_mapping.get('b')
c = system_mapping.get('c')
b = system_mapping.get("b")
c = system_mapping.get("c")
self.assertEqual(len(self.context.key_to_code), 3)
self.assertEqual(self.context.key_to_code[((1, 32, 1),)], b)
self.assertEqual(self.context.key_to_code[(1, 33, 1), (1, 34, 1), (1, 35, 1)], c)
self.assertEqual(self.context.key_to_code[(1, 34, 1), (1, 33, 1), (1, 35, 1)], c)
self.assertEqual(
self.context.key_to_code[(1, 33, 1), (1, 34, 1), (1, 35, 1)], c
)
self.assertEqual(
self.context.key_to_code[(1, 34, 1), (1, 33, 1), (1, 35, 1)], c
)
def test_is_mapped(self):
self.assertTrue(self.context.is_mapped(
((1, 32, 1),)
))
self.assertTrue(self.context.is_mapped(
((1, 33, 1), (1, 34, 1), (1, 35, 1))
))
self.assertTrue(self.context.is_mapped(
((1, 34, 1), (1, 33, 1), (1, 35, 1))
))
self.assertFalse(self.context.is_mapped(
((1, 34, 1), (1, 35, 1), (1, 33, 1))
))
self.assertFalse(self.context.is_mapped(
((1, 36, 1),)
))
self.assertTrue(self.context.is_mapped(((1, 32, 1),)))
self.assertTrue(self.context.is_mapped(((1, 33, 1), (1, 34, 1), (1, 35, 1))))
self.assertTrue(self.context.is_mapped(((1, 34, 1), (1, 33, 1), (1, 35, 1))))
self.assertFalse(self.context.is_mapped(((1, 34, 1), (1, 35, 1), (1, 33, 1))))
self.assertFalse(self.context.is_mapped(((1, 36, 1),)))
def test_maps_joystick(self):
self.assertTrue(self.context.maps_joystick())
self.mapping.set('gamepad.joystick.left_purpose', NONE)
self.mapping.set('gamepad.joystick.right_purpose', NONE)
self.mapping.set("gamepad.joystick.left_purpose", NONE)
self.mapping.set("gamepad.joystick.right_purpose", NONE)
self.context.update_purposes()
self.assertFalse(self.context.maps_joystick())
def test_joystick_as_dpad(self):
self.assertTrue(self.context.maps_joystick())
self.mapping.set('gamepad.joystick.left_purpose', WHEEL)
self.mapping.set('gamepad.joystick.right_purpose', MOUSE)
self.mapping.set("gamepad.joystick.left_purpose", WHEEL)
self.mapping.set("gamepad.joystick.right_purpose", MOUSE)
self.context.update_purposes()
self.assertFalse(self.context.joystick_as_dpad())
self.mapping.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', NONE)
self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", NONE)
self.context.update_purposes()
self.assertTrue(self.context.joystick_as_dpad())
self.mapping.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.mapping.set("gamepad.joystick.left_purpose", MOUSE)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
self.context.update_purposes()
self.assertTrue(self.context.joystick_as_dpad())
def test_joystick_as_mouse(self):
self.assertTrue(self.context.maps_joystick())
self.mapping.set('gamepad.joystick.right_purpose', MOUSE)
self.mapping.set("gamepad.joystick.right_purpose", MOUSE)
self.context.update_purposes()
self.assertTrue(self.context.joystick_as_mouse())
self.mapping.set('gamepad.joystick.left_purpose', NONE)
self.mapping.set('gamepad.joystick.right_purpose', NONE)
self.mapping.set("gamepad.joystick.left_purpose", NONE)
self.mapping.set("gamepad.joystick.right_purpose", NONE)
self.context.update_purposes()
self.assertFalse(self.context.joystick_as_mouse())
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
self.context.update_purposes()
self.assertFalse(self.context.joystick_as_mouse())

@ -30,7 +30,7 @@ import collections
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
from keymapper.state import custom_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.config import config
from keymapper.daemon import Daemon
from keymapper.mapping import Mapping
@ -44,10 +44,10 @@ def import_control():
"""Import the core function of the key-mapper-control command."""
custom_mapping.empty()
bin_path = os.path.join(os.getcwd(), 'bin', 'key-mapper-control')
bin_path = os.path.join(os.getcwd(), "bin", "key-mapper-control")
loader = SourceFileLoader('__not_main_idk__', bin_path)
spec = spec_from_loader('__not_main_idk__', loader)
loader = SourceFileLoader("__not_main_idk__", bin_path)
spec = spec_from_loader("__not_main_idk__", loader)
module = module_from_spec(spec)
spec.loader.exec_module(module)
@ -58,10 +58,8 @@ communicate, utils, internals = import_control()
options = collections.namedtuple(
'options', [
'command', 'config_dir', 'preset', 'device', 'list_devices',
'key_names', 'debug'
]
"options",
["command", "config_dir", "preset", "device", "list_devices", "key_names", "debug"],
)
@ -70,13 +68,13 @@ class TestControl(unittest.TestCase):
quick_cleanup()
def test_autoload(self):
device_keys = ['Foo Device 2', 'Bar Device']
device_keys = ["Foo Device 2", "Bar Device"]
groups_ = [groups.find(key=key) for key in device_keys]
presets = ['bar0', 'bar', 'bar2']
presets = ["bar0", "bar", "bar2"]
paths = [
get_preset_path(groups_[0].name, presets[0]),
get_preset_path(groups_[1].name, presets[1]),
get_preset_path(groups_[1].name, presets[2])
get_preset_path(groups_[1].name, presets[2]),
]
Mapping().save(paths[0])
@ -92,78 +90,117 @@ class TestControl(unittest.TestCase):
def stop_injecting(self, *args, **kwargs):
nonlocal stop_counter
stop_counter += 1
def start_injecting(device, preset):
print(f'\033[90mstart_injecting "{device}" "{preset}"\033[0m')
start_history.append((device, preset))
daemon.injectors[device] = Injector()
daemon.start_injecting = start_injecting
config.set_autoload_preset(groups_[0].key, presets[0])
config.set_autoload_preset(groups_[1].key, presets[1])
config.save_config()
communicate(options('autoload', None, None, None, False, False, False), daemon)
communicate(options("autoload", None, None, None, False, False, False), daemon)
self.assertEqual(len(start_history), 2)
self.assertEqual(start_history[0], (groups_[0].key, presets[0]))
self.assertEqual(start_history[1], (groups_[1].key, presets[1]))
self.assertIn(groups_[0].key, daemon.injectors)
self.assertIn(groups_[1].key, daemon.injectors)
self.assertFalse(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[1]))
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[1])
)
# calling autoload again doesn't load redundantly
communicate(options('autoload', None, None, None, False, False, False), daemon)
communicate(options("autoload", None, None, None, False, False, False), daemon)
self.assertEqual(len(start_history), 2)
self.assertEqual(stop_counter, 0)
self.assertFalse(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[1]))
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[1])
)
# unless the injection in question ist stopped
communicate(options('stop', None, None, groups_[0].key, False, False, False), daemon)
communicate(
options("stop", None, None, groups_[0].key, False, False, False), daemon
)
self.assertEqual(stop_counter, 1)
self.assertTrue(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[1]))
communicate(options('autoload', None, None, None, False, False, False), daemon)
self.assertTrue(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[1])
)
communicate(options("autoload", None, None, None, False, False, False), daemon)
self.assertEqual(len(start_history), 3)
self.assertEqual(start_history[2], (groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[1]))
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[1])
)
# if a device name is passed, will only start injecting for that one
communicate(options('stop-all', None, None, None, False, False, False), daemon)
self.assertTrue(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertTrue(daemon.autoload_history.may_autoload(groups_[1].key, presets[1]))
communicate(options("stop-all", None, None, None, False, False, False), daemon)
self.assertTrue(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertTrue(
daemon.autoload_history.may_autoload(groups_[1].key, presets[1])
)
self.assertEqual(stop_counter, 3)
config.set_autoload_preset(groups_[1].key, presets[2])
config.save_config()
communicate(options('autoload', None, None, groups_[1].key, False, False, False), daemon)
communicate(
options("autoload", None, None, groups_[1].key, False, False, False), daemon
)
self.assertEqual(len(start_history), 4)
self.assertEqual(start_history[3], (groups_[1].key, presets[2]))
self.assertTrue(daemon.autoload_history.may_autoload(groups_[0].key, presets[0]))
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[2]))
self.assertTrue(
daemon.autoload_history.may_autoload(groups_[0].key, presets[0])
)
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[2])
)
# autoloading for the same device again redundantly will not autoload
# again
communicate(options('autoload', None, None, groups_[1].key, False, False, False), daemon)
communicate(
options("autoload", None, None, groups_[1].key, False, False, False), daemon
)
self.assertEqual(len(start_history), 4)
self.assertEqual(stop_counter, 3)
self.assertFalse(daemon.autoload_history.may_autoload(groups_[1].key, presets[2]))
self.assertFalse(
daemon.autoload_history.may_autoload(groups_[1].key, presets[2])
)
# any other arbitrary preset may be autoloaded
self.assertTrue(daemon.autoload_history.may_autoload(groups_[1].key, 'quuuux'))
self.assertTrue(daemon.autoload_history.may_autoload(groups_[1].key, "quuuux"))
# after 15 seconds it may be autoloaded again
daemon.autoload_history._autoload_history[groups_[1].key] = (time.time() - 16, presets[2])
self.assertTrue(daemon.autoload_history.may_autoload(groups_[1].key, presets[2]))
daemon.autoload_history._autoload_history[groups_[1].key] = (
time.time() - 16,
presets[2],
)
self.assertTrue(
daemon.autoload_history.may_autoload(groups_[1].key, presets[2])
)
def test_autoload_other_path(self):
device_names = ['Foo Device', 'Bar Device']
device_names = ["Foo Device", "Bar Device"]
groups_ = [groups.find(name=name) for name in device_names]
presets = ['bar123', 'bar2']
config_dir = os.path.join(tmp, 'qux', 'quux')
presets = ["bar123", "bar2"]
config_dir = os.path.join(tmp, "qux", "quux")
paths = [
os.path.join(config_dir, 'presets', device_names[0], presets[0] + '.json'),
os.path.join(config_dir, 'presets', device_names[1], presets[1] + '.json')
os.path.join(config_dir, "presets", device_names[0], presets[0] + ".json"),
os.path.join(config_dir, "presets", device_names[1], presets[1] + ".json"),
]
Mapping().save(paths[0])
@ -174,21 +211,23 @@ class TestControl(unittest.TestCase):
start_history = []
daemon.start_injecting = lambda *args: start_history.append(args)
config.path = os.path.join(config_dir, 'config.json')
config.path = os.path.join(config_dir, "config.json")
config.load_config()
config.set_autoload_preset(device_names[0], presets[0])
config.set_autoload_preset(device_names[1], presets[1])
config.save_config()
communicate(options('autoload', config_dir, None, None, False, False, False), daemon)
communicate(
options("autoload", config_dir, None, None, False, False, False), daemon
)
self.assertEqual(len(start_history), 2)
self.assertEqual(start_history[0], (groups_[0].key, presets[0]))
self.assertEqual(start_history[1], (groups_[1].key, presets[1]))
def test_start_stop(self):
group = groups.find(key='Foo Device 2')
preset = 'preset9'
group = groups.find(key="Foo Device 2")
preset = "preset9"
daemon = Daemon()
@ -199,24 +238,28 @@ class TestControl(unittest.TestCase):
daemon.stop_injecting = lambda *args: stop_history.append(args)
daemon.stop_all = lambda *args: stop_all_history.append(args)
communicate(options('start', None, preset, group.paths[0], False, False, False), daemon)
communicate(
options("start", None, preset, group.paths[0], False, False, False), daemon
)
self.assertEqual(len(start_history), 1)
self.assertEqual(start_history[0], (group.key, preset))
communicate(options('stop', None, None, group.paths[1], False, False, False), daemon)
communicate(
options("stop", None, None, group.paths[1], False, False, False), daemon
)
self.assertEqual(len(stop_history), 1)
# provided any of the groups paths as --device argument, figures out
# the correct group.key to use here
self.assertEqual(stop_history[0], (group.key,))
communicate(options('stop-all', None, None, None, False, False, False), daemon)
communicate(options("stop-all", None, None, None, False, False, False), daemon)
self.assertEqual(len(stop_all_history), 1)
self.assertEqual(stop_all_history[0], ())
def test_config_not_found(self):
key = 'Foo Device 2'
path = '~/a/preset.json'
config_dir = '/foo/bar'
key = "Foo Device 2"
path = "~/a/preset.json"
config_dir = "/foo/bar"
daemon = Daemon()
@ -225,46 +268,46 @@ class TestControl(unittest.TestCase):
daemon.start_injecting = lambda *args: start_history.append(args)
daemon.stop_injecting = lambda *args: stop_history.append(args)
options_1 = options('start', config_dir, path, key, False, False, False)
options_1 = options("start", config_dir, path, key, False, False, False)
self.assertRaises(SystemExit, lambda: communicate(options_1, daemon))
options_2 = options('stop', config_dir, None, key, False, False, False)
options_2 = options("stop", config_dir, None, key, False, False, False)
self.assertRaises(SystemExit, lambda: communicate(options_2, daemon))
def test_autoload_config_dir(self):
daemon = Daemon()
path = os.path.join(tmp, 'foo')
path = os.path.join(tmp, "foo")
os.makedirs(path)
with open(os.path.join(path, 'config.json'), 'w') as file:
with open(os.path.join(path, "config.json"), "w") as file:
file.write('{"foo":"bar"}')
self.assertIsNone(config.get('foo'))
self.assertIsNone(config.get("foo"))
daemon.set_config_dir(path)
# since daemon and this test share the same memory, the config
# object that this test can access will be modified
self.assertEqual(config.get('foo'), 'bar')
self.assertEqual(config.get("foo"), "bar")
# passing a path that doesn't exist or a path that doesn't contain
# a config.json file won't do anything
os.makedirs(os.path.join(tmp, 'bar'))
daemon.set_config_dir(os.path.join(tmp, 'bar'))
self.assertEqual(config.get('foo'), 'bar')
daemon.set_config_dir(os.path.join(tmp, 'qux'))
self.assertEqual(config.get('foo'), 'bar')
os.makedirs(os.path.join(tmp, "bar"))
daemon.set_config_dir(os.path.join(tmp, "bar"))
self.assertEqual(config.get("foo"), "bar")
daemon.set_config_dir(os.path.join(tmp, "qux"))
self.assertEqual(config.get("foo"), "bar")
def test_internals(self):
with mock.patch('os.system') as os_system_patch:
internals(options('helper', None, None, None, False, False, False))
with mock.patch("os.system") as os_system_patch:
internals(options("helper", None, None, None, False, False, False))
os_system_patch.assert_called_once()
self.assertIn('key-mapper-helper', os_system_patch.call_args.args[0])
self.assertNotIn('-d', os_system_patch.call_args.args[0])
self.assertIn("key-mapper-helper", os_system_patch.call_args.args[0])
self.assertNotIn("-d", os_system_patch.call_args.args[0])
with mock.patch('os.system') as os_system_patch:
internals(options('start-daemon', None, None, None, False, False, True))
with mock.patch("os.system") as os_system_patch:
internals(options("start-daemon", None, None, None, False, False, True))
os_system_patch.assert_called_once()
self.assertIn('key-mapper-service', os_system_patch.call_args.args[0])
self.assertIn('-d', os_system_patch.call_args.args[0])
self.assertIn("key-mapper-service", os_system_patch.call_args.args[0])
self.assertIn("-d", os_system_patch.call_args.args[0])
if __name__ == "__main__":

@ -31,7 +31,8 @@ from evdev.ecodes import EV_KEY, EV_ABS
from gi.repository import Gtk
from pydbus import SystemBus
from keymapper.state import custom_mapping, system_mapping
from keymapper.system_mapping import system_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.config import config
from keymapper.groups import groups
from keymapper.paths import get_config_path, mkdir, get_preset_path
@ -40,8 +41,15 @@ from keymapper.mapping import Mapping
from keymapper.injection.injector import STARTING, RUNNING, STOPPED, UNKNOWN
from keymapper.daemon import Daemon, BUS_NAME
from tests.test import cleanup, uinput_write_history_pipe, new_event, \
push_events, is_service_running, fixtures, tmp
from tests.test import (
cleanup,
uinput_write_history_pipe,
new_event,
push_events,
is_service_running,
fixtures,
tmp,
)
def gtk_iteration():
@ -53,8 +61,7 @@ def gtk_iteration():
class TestDBusDaemon(unittest.TestCase):
def setUp(self):
self.process = multiprocessing.Process(
target=os.system,
args=('key-mapper-service -d',)
target=os.system, args=("key-mapper-service -d",)
)
self.process.start()
time.sleep(0.5)
@ -65,7 +72,7 @@ class TestDBusDaemon(unittest.TestCase):
def tearDown(self):
self.interface.stop_all()
os.system('pkill -f key-mapper-service')
os.system("pkill -f key-mapper-service")
for _ in range(10):
time.sleep(0.1)
@ -78,7 +85,7 @@ class TestDBusDaemon(unittest.TestCase):
# it's a remote dbus object
self.assertEqual(self.interface._bus_name, BUS_NAME)
self.assertFalse(isinstance(self.interface, Daemon))
self.assertEqual(self.interface.hello('foo'), 'foo')
self.assertEqual(self.interface.hello("foo"), "foo")
check_output = subprocess.check_output
@ -87,7 +94,7 @@ dbus_get = type(SystemBus()).get
class TestDaemon(unittest.TestCase):
new_fixture_path = '/dev/input/event9876'
new_fixture_path = "/dev/input/event9876"
def setUp(self):
self.grab = evdev.InputDevice.grab
@ -128,29 +135,29 @@ class TestDaemon(unittest.TestCase):
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'))
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')
group = groups.find(name="Bar Device")
# unrelated group that shouldn't be affected at all
group2 = groups.find(name='gamepad')
group2 = groups.find(name="gamepad")
custom_mapping.change(Key(*ev_1, 1), 'a')
custom_mapping.change(Key(*ev_2, -1), 'b')
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)
system_mapping._set("a", keycode_to_1)
system_mapping._set("b", keycode_to_2)
preset = 'foo'
preset = "foo"
custom_mapping.save(group.get_preset_path(preset))
config.set_autoload_preset(group.key, preset)
@ -158,9 +165,7 @@ class TestDaemon(unittest.TestCase):
"""injection 1"""
# should forward the event unchanged
push_events(group.key, [
new_event(EV_KEY, 13, 1)
])
push_events(group.key, [new_event(EV_KEY, 13, 1)])
self.daemon = Daemon()
self.daemon.set_config_dir(get_config_path())
@ -184,16 +189,14 @@ class TestDaemon(unittest.TestCase):
try:
self.assertFalse(uinput_write_history_pipe[0].poll())
except AssertionError:
print('Unexpected', uinput_write_history_pipe[0].recv())
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)
])
# -1234 will be classified as -1 by the injector
push_events(group.key, [new_event(*ev_2, -1234)])
self.daemon.start_injecting(group.key, preset)
@ -209,12 +212,12 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.value, 1)
def test_refresh_on_start(self):
if os.path.exists(get_config_path('xmodmap.json')):
os.remove(get_config_path('xmodmap.json'))
if os.path.exists(get_config_path("xmodmap.json")):
os.remove(get_config_path("xmodmap.json"))
ev = (EV_KEY, 9)
keycode_to = 100
group_name = '9876 name'
group_name = "9876 name"
# expected key of the group
group_key = group_name
@ -222,21 +225,19 @@ class TestDaemon(unittest.TestCase):
group = groups.find(name=group_name)
# this test only makes sense if this device is unknown yet
self.assertIsNone(group)
custom_mapping.change(Key(*ev, 1), 'a')
custom_mapping.change(Key(*ev, 1), "a")
system_mapping.clear()
system_mapping._set('a', keycode_to)
system_mapping._set("a", keycode_to)
# make the daemon load the file instead
with open(get_config_path('xmodmap.json'), 'w') as file:
with open(get_config_path("xmodmap.json"), "w") as file:
json.dump(system_mapping._mapping, file, indent=4)
system_mapping.clear()
preset = 'foo'
preset = "foo"
custom_mapping.save(get_preset_path(group_name, preset))
config.set_autoload_preset(group_key, preset)
push_events(group_key, [
new_event(*ev, 1)
])
push_events(group_key, [new_event(*ev, 1)])
self.daemon = Daemon()
# make sure the devices are populated
@ -244,10 +245,10 @@ class TestDaemon(unittest.TestCase):
# the daemon is supposed to find this device by calling refresh
fixtures[self.new_fixture_path] = {
'capabilities': {evdev.ecodes.EV_KEY: [ev[1]]},
'phys': '9876 phys',
'info': evdev.device.DeviceInfo(4, 5, 6, 7),
'name': group_name
"capabilities": {evdev.ecodes.EV_KEY: [ev[1]]},
"phys": "9876 phys",
"info": evdev.device.DeviceInfo(4, 5, 6, 7),
"name": group_name,
}
self.daemon.set_config_dir(get_config_path())
@ -268,7 +269,7 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(self.daemon.get_state(group_key), STOPPED)
def test_refresh_for_unknown_key(self):
device = '9876 name'
device = "9876 name"
# this test only makes sense if this device is unknown yet
self.assertIsNone(groups.find(name=device))
@ -280,13 +281,13 @@ class TestDaemon(unittest.TestCase):
self.daemon.refresh()
fixtures[self.new_fixture_path] = {
'capabilities': {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]},
'phys': '9876 phys',
'info': evdev.device.DeviceInfo(4, 5, 6, 7),
'name': device
"capabilities": {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]},
"phys": "9876 phys",
"info": evdev.device.DeviceInfo(4, 5, 6, 7),
"name": device,
}
self.daemon._autoload('25v7j9q4vtj')
self.daemon._autoload("25v7j9q4vtj")
# this is unknown, so the daemon will scan the devices again
# test if the injector called groups.refresh successfully
@ -294,35 +295,33 @@ class TestDaemon(unittest.TestCase):
def test_xmodmap_file(self):
from_keycode = evdev.ecodes.KEY_A
to_name = 'qux'
to_name = "qux"
to_keycode = 100
event = (EV_KEY, from_keycode, 1)
name = 'Bar Device'
preset = 'foo'
name = "Bar Device"
preset = "foo"
group = groups.find(name=name)
config_dir = os.path.join(tmp, 'foo')
config_dir = os.path.join(tmp, "foo")
path = os.path.join(config_dir, 'presets', name, f'{preset}.json')
path = os.path.join(config_dir, "presets", name, f"{preset}.json")
custom_mapping.change(Key(event), to_name)
custom_mapping.save(path)
system_mapping.clear()
push_events(group.key, [
new_event(*event)
])
push_events(group.key, [new_event(*event)])
# an existing config file is needed otherwise set_config_dir refuses
# to use the directory
config_path = os.path.join(config_dir, 'config.json')
config_path = os.path.join(config_dir, "config.json")
config.path = config_path
config.save_config()
xmodmap_path = os.path.join(config_dir, 'xmodmap.json')
with open(xmodmap_path, 'w') as file:
xmodmap_path = os.path.join(config_dir, "xmodmap.json")
with open(xmodmap_path, "w") as file:
file.write(f'{{"{to_name}":{to_keycode}}}')
self.daemon = Daemon()
@ -339,14 +338,14 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.value, 1)
def test_start_stop(self):
group = groups.find(key='Foo Device 2')
preset = 'preset8'
group = groups.find(key="Foo Device 2")
preset = "preset8"
daemon = Daemon()
self.daemon = daemon
mapping = Mapping()
mapping.change(Key(3, 2, 1), 'a')
mapping.change(Key(3, 2, 1), "a")
mapping.save(group.get_preset_path(preset))
# the daemon needs set_config_dir first before doing anything
@ -380,13 +379,13 @@ class TestDaemon(unittest.TestCase):
# trying to inject a non existing preset keeps the previous inejction
# alive
injector = daemon.injectors[group.key]
daemon.start_injecting(group.key, 'qux')
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')
daemon.start_injecting("quux", "qux")
self.assertNotEqual(daemon.injectors[group.key].get_state(), STOPPED)
# after all that stuff autoload_history is still unharmed
@ -400,15 +399,15 @@ class TestDaemon(unittest.TestCase):
self.assertTrue(daemon.autoload_history.may_autoload(group.key, preset))
def test_autoload(self):
preset = 'preset7'
group = groups.find(key='Foo Device 2')
preset = "preset7"
group = groups.find(key="Foo Device 2")
daemon = Daemon()
self.daemon = daemon
self.daemon.set_config_dir(get_config_path())
mapping = Mapping()
mapping.change(Key(3, 2, 1), 'a')
mapping.change(Key(3, 2, 1), "a")
mapping.save(group.get_preset_path(preset))
# no autoloading is configured yet
@ -423,14 +422,18 @@ class TestDaemon(unittest.TestCase):
# now autoloading is configured, so it will autoload
self.daemon._autoload(group.key)
len_after = len(self.daemon.autoload_history._autoload_history)
self.assertEqual(daemon.autoload_history._autoload_history[group.key][1], preset)
self.assertEqual(
daemon.autoload_history._autoload_history[group.key][1], preset
)
self.assertFalse(daemon.autoload_history.may_autoload(group.key, preset))
injector = daemon.injectors[group.key]
self.assertEqual(len_before + 1, len_after)
# calling duplicate _autoload does nothing
self.daemon._autoload(group.key)
self.assertEqual(daemon.autoload_history._autoload_history[group.key][1], preset)
self.assertEqual(
daemon.autoload_history._autoload_history[group.key][1], preset
)
self.assertEqual(injector, daemon.injectors[group.key])
self.assertFalse(daemon.autoload_history.may_autoload(group.key, preset))
@ -440,13 +443,13 @@ class TestDaemon(unittest.TestCase):
# calling autoload for (yet) unknown devices does nothing
len_before = len(self.daemon.autoload_history._autoload_history)
self.daemon._autoload('unknown-key-1234')
self.daemon._autoload("unknown-key-1234")
len_after = len(self.daemon.autoload_history._autoload_history)
self.assertEqual(len_before, len_after)
# autoloading key-mapper devices does nothing
len_before = len(self.daemon.autoload_history._autoload_history)
self.daemon.autoload_single('Bar Device')
self.daemon.autoload_single("Bar Device")
len_after = len(self.daemon.autoload_history._autoload_history)
self.assertEqual(len_before, len_after)
@ -455,15 +458,15 @@ class TestDaemon(unittest.TestCase):
history = self.daemon.autoload_history._autoload_history
# existing device
preset = 'preset7'
group = groups.find(key='Foo Device 2')
preset = "preset7"
group = groups.find(key="Foo Device 2")
mapping = Mapping()
mapping.change(Key(3, 2, 1), 'a')
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')
config.set_autoload_preset("non-existant-key", "foo")
# daemon is missing the config directory yet
self.daemon.autoload()
@ -477,11 +480,11 @@ class TestDaemon(unittest.TestCase):
def test_autoload_3(self):
# based on a bug
preset = 'preset7'
group = groups.find(key='Foo Device 2')
preset = "preset7"
group = groups.find(key="Foo Device 2")
mapping = Mapping()
mapping.change(Key(3, 2, 1), 'a')
mapping.change(Key(3, 2, 1), "a")
mapping.save(group.get_preset_path(preset))
config.set_autoload_preset(group.key, preset)
@ -490,7 +493,7 @@ class TestDaemon(unittest.TestCase):
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.assertIsNone(groups.find(key="Foo Device 2"))
self.daemon.autoload()
# it should try to refresh the groups because all the
@ -498,7 +501,7 @@ class TestDaemon(unittest.TestCase):
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'))
self.assertIsNotNone(groups.find(key="Foo Device 2"))
if __name__ == "__main__":

@ -29,19 +29,19 @@ from keymapper.data import get_data_path
class TestData(unittest.TestCase):
def test_data_editable(self):
path = os.getcwd()
pkg_resources.require('key-mapper')[0].location = path
self.assertEqual(get_data_path(), path + '/data/')
self.assertEqual(get_data_path('a'), path + '/data/a')
pkg_resources.require("key-mapper")[0].location = path
self.assertEqual(get_data_path(), path + "/data/")
self.assertEqual(get_data_path("a"), path + "/data/a")
def test_data_usr(self):
path = '/usr/some/where/python3.8/dist-packages/'
pkg_resources.require('key-mapper')[0].location = path
path = "/usr/some/where/python3.8/dist-packages/"
pkg_resources.require("key-mapper")[0].location = path
self.assertTrue(get_data_path().startswith('/usr/'))
self.assertTrue(get_data_path().endswith('key-mapper/'))
self.assertTrue(get_data_path().startswith("/usr/"))
self.assertTrue(get_data_path().endswith("key-mapper/"))
self.assertTrue(get_data_path('a').startswith('/usr/'))
self.assertTrue(get_data_path('a').endswith('key-mapper/a'))
self.assertTrue(get_data_path("a").startswith("/usr/"))
self.assertTrue(get_data_path("a").endswith("key-mapper/a"))
if __name__ == "__main__":

@ -22,8 +22,16 @@
import unittest
from evdev import ecodes
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A, \
EV_REL, REL_X, REL_WHEEL, REL_HWHEEL
from evdev.ecodes import (
EV_KEY,
EV_ABS,
ABS_HAT0X,
KEY_A,
EV_REL,
REL_X,
REL_WHEEL,
REL_HWHEEL,
)
from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping
@ -34,17 +42,16 @@ from tests.test import new_event, InputDevice, MAX_ABS, MIN_ABS
class TestDevUtils(unittest.TestCase):
def test_max_abs(self):
self.assertEqual(utils.get_abs_range(InputDevice('/dev/input/event30'))[1], MAX_ABS)
self.assertIsNone(utils.get_abs_range(InputDevice('/dev/input/event10')))
self.assertEqual(
utils.get_abs_range(InputDevice("/dev/input/event30"))[1], MAX_ABS
)
self.assertIsNone(utils.get_abs_range(InputDevice("/dev/input/event10")))
def test_will_report_key_up(self):
self.assertFalse(
utils.will_report_key_up(new_event(EV_REL, REL_WHEEL, 1)))
self.assertFalse(
utils.will_report_key_up(new_event(EV_REL, REL_HWHEEL, -1)))
self.assertFalse(utils.will_report_key_up(new_event(EV_REL, REL_WHEEL, 1)))
self.assertFalse(utils.will_report_key_up(new_event(EV_REL, REL_HWHEEL, -1)))
self.assertTrue(utils.will_report_key_up(new_event(EV_KEY, KEY_A, 1)))
self.assertTrue(
utils.will_report_key_up(new_event(EV_ABS, ABS_HAT0X, -1)))
self.assertTrue(utils.will_report_key_up(new_event(EV_ABS, ABS_HAT0X, -1)))
def test_is_wheel(self):
self.assertTrue(utils.is_wheel(new_event(EV_REL, REL_WHEEL, 1)))
@ -113,8 +120,8 @@ class TestDevUtils(unittest.TestCase):
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1)))
mapping.set('gamepad.joystick.right_purpose', BUTTONS)
config.set('gamepad.joystick.left_purpose', BUTTONS)
mapping.set("gamepad.joystick.right_purpose", BUTTONS)
config.set("gamepad.joystick.left_purpose", BUTTONS)
# but only for gamepads
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1)))
@ -126,13 +133,13 @@ class TestDevUtils(unittest.TestCase):
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
def test_normalize_value(self):
def test_classify_action(self):
""""""
"""0 to MAX_ABS"""
def do(event):
return utils.normalize_value(event, (0, MAX_ABS))
return utils.classify_action(event, (0, MAX_ABS))
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do(event), 1)
@ -148,7 +155,7 @@ class TestDevUtils(unittest.TestCase):
"""MIN_ABS to MAX_ABS"""
def do2(event):
return utils.normalize_value(event, (MIN_ABS, MAX_ABS))
return utils.classify_action(event, (MIN_ABS, MAX_ABS))
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do2(event), 1)
@ -167,7 +174,7 @@ class TestDevUtils(unittest.TestCase):
# it just forwards the value
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(utils.normalize_value(event, None), MAX_ABS)
self.assertEqual(utils.classify_action(event, None), MAX_ABS)
"""Not a joystick"""

@ -22,22 +22,40 @@
import unittest
import asyncio
from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, \
EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY
from evdev.ecodes import (
EV_REL,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
EV_ABS,
ABS_X,
ABS_Y,
ABS_RX,
ABS_RY,
)
from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.injection.context import Context
from keymapper.injection.event_producer import EventProducer, MOUSE, WHEEL
from keymapper.injection.consumers.event_producer import EventProducer, MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
uinput_write_history, quick_cleanup, new_event, MIN_ABS
from tests.test import (
InputDevice,
UInput,
MAX_ABS,
clear_write_history,
uinput_write_history,
quick_cleanup,
new_event,
MIN_ABS,
)
abs_state = [0, 0, 0, 0]
class TestEventProducer(unittest.TestCase):
class TestEventProducer(unittest.IsolatedAsyncioTestCase):
def setUp(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@ -48,76 +66,21 @@ class TestEventProducer(unittest.TestCase):
uinput = UInput()
self.context.uinput = uinput
device = InputDevice('/dev/input/event30')
self.event_producer = EventProducer(self.context)
self.event_producer.set_abs_range_from(device)
asyncio.ensure_future(self.event_producer.run())
source = InputDevice("/dev/input/event30")
self.event_producer = EventProducer(self.context, source)
config.set('gamepad.joystick.x_scroll_speed', 1)
config.set('gamepad.joystick.y_scroll_speed', 1)
config.set("gamepad.joystick.x_scroll_speed", 1)
config.set("gamepad.joystick.y_scroll_speed", 1)
def tearDown(self):
quick_cleanup()
def test_debounce_1(self):
loop = asyncio.get_event_loop()
tick_time = 1 / 60
history = []
self.event_producer.debounce(1234, history.append, (1,), 10)
asyncio.ensure_future(self.event_producer.run())
loop.run_until_complete(asyncio.sleep(6 * tick_time))
self.assertEqual(len(history), 0)
loop.run_until_complete(asyncio.sleep(6 * tick_time))
self.assertEqual(len(history), 1)
# won't get called a second time
loop.run_until_complete(asyncio.sleep(11 * tick_time))
self.assertEqual(len(history), 1)
self.assertEqual(history[0], 1)
def test_debounce_2(self):
loop = asyncio.get_event_loop()
tick_time = 1 / 60
history = []
self.event_producer.debounce(1234, history.append, (1,), 10)
asyncio.ensure_future(self.event_producer.run())
loop.run_until_complete(asyncio.sleep(6 * tick_time))
self.assertEqual(len(history), 0)
# replaces
self.event_producer.debounce(1234, history.append, (2,), 20)
loop.run_until_complete(asyncio.sleep(6 * tick_time))
self.assertEqual(len(history), 0)
loop.run_until_complete(asyncio.sleep(11 * tick_time))
self.assertEqual(len(history), 1)
# won't get called a second time
loop.run_until_complete(asyncio.sleep(21 * tick_time))
self.assertEqual(len(history), 1)
self.assertEqual(history[0], 2)
def test_debounce_3(self):
loop = asyncio.get_event_loop()
tick_time = 1 / 60
history = []
self.event_producer.debounce(1234, history.append, (1,), 10)
self.event_producer.debounce(5678, history.append, (2,), 20)
asyncio.ensure_future(self.event_producer.run())
loop.run_until_complete(asyncio.sleep(11 * tick_time))
self.assertEqual(len(history), 1)
loop.run_until_complete(asyncio.sleep(11 * tick_time))
self.assertEqual(len(history), 2)
loop.run_until_complete(asyncio.sleep(21 * tick_time))
self.assertEqual(len(history), 2)
self.assertEqual(history[0], 1)
self.assertEqual(history[1], 2)
def assertClose(self, a, b, within):
"""a has to be within b - b * within, b + b * within."""
self.assertLess(a - abs(a) * within, b)
self.assertGreater(a + abs(a) * within, b)
def test_assertClose(self):
async def test_assertClose(self):
self.assertClose(5, 5, 0.1)
self.assertClose(5, 5, 1)
self.assertClose(6, 5, 0.2)
@ -132,19 +95,22 @@ class TestEventProducer(unittest.TestCase):
self.assertRaises(AssertionError, lambda: self.assertClose(-6, -5, 0.1))
self.assertRaises(AssertionError, lambda: self.assertClose(-4, -5, 0.1))
def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome."""
async def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome.
Depending on the configuration, the cursor or wheel should move.
"""
clear_write_history()
self.event_producer.context.update_purposes()
self.event_producer.notify(new_event(EV_ABS, ABS_X, a))
self.event_producer.notify(new_event(EV_ABS, ABS_Y, b))
self.event_producer.notify(new_event(EV_ABS, ABS_RX, c))
self.event_producer.notify(new_event(EV_ABS, ABS_RY, d))
# 3 frames
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.sleep(3 / 60))
history = [h.t for h in uinput_write_history]
await self.event_producer.notify(new_event(EV_ABS, ABS_X, a))
await self.event_producer.notify(new_event(EV_ABS, ABS_Y, b))
await self.event_producer.notify(new_event(EV_ABS, ABS_RX, c))
await self.event_producer.notify(new_event(EV_ABS, ABS_RY, d))
# sleep long enough to test if multiple events are written
await asyncio.sleep(5 / 60)
history = [h.t for h in uinput_write_history]
self.assertGreater(len(history), 1)
self.assertIn(expectation, history)
@ -153,12 +119,14 @@ class TestEventProducer(unittest.TestCase):
# if the injected cursor movement is 19 or 20 doesn't really matter
self.assertClose(history_entry[2], expectation[2], 0.1)
def test_joystick_purpose_1(self):
async def test_joystick_purpose_1(self):
asyncio.ensure_future(self.event_producer.run())
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.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)
min_abs = 0
# if `rest` is not exactly `max_abs / 2` decimal places might add up
@ -168,70 +136,76 @@ class TestEventProducer(unittest.TestCase):
rest = 128 # resting position of the cursor
self.event_producer.set_abs_range(min_abs, max_abs)
self.do(max_abs, rest, rest, rest, (EV_REL, REL_X, speed))
self.do(min_abs, rest, rest, rest, (EV_REL, REL_X, -speed))
self.do(rest, max_abs, rest, rest, (EV_REL, REL_Y, speed))
self.do(rest, min_abs, rest, rest, (EV_REL, REL_Y, -speed))
await self.do(max_abs, rest, rest, rest, (EV_REL, REL_X, speed))
await self.do(min_abs, rest, rest, rest, (EV_REL, REL_X, -speed))
await self.do(rest, max_abs, rest, rest, (EV_REL, REL_Y, speed))
await self.do(rest, min_abs, rest, rest, (EV_REL, REL_Y, -speed))
# vertical wheel event values are negative
self.do(rest, rest, max_abs, rest, (EV_REL, REL_HWHEEL, 1))
self.do(rest, rest, min_abs, rest, (EV_REL, REL_HWHEEL, -1))
self.do(rest, rest, rest, max_abs, (EV_REL, REL_WHEEL, -1))
self.do(rest, rest, rest, min_abs, (EV_REL, REL_WHEEL, 1))
await self.do(rest, rest, max_abs, rest, (EV_REL, REL_HWHEEL, 1))
await self.do(rest, rest, min_abs, rest, (EV_REL, REL_HWHEEL, -1))
await self.do(rest, rest, rest, max_abs, (EV_REL, REL_WHEEL, -1))
await self.do(rest, rest, rest, min_abs, (EV_REL, REL_WHEEL, 1))
async def test_joystick_purpose_2(self):
asyncio.ensure_future(self.event_producer.run())
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)
config.set('gamepad.joystick.x_scroll_speed', 1)
config.set('gamepad.joystick.y_scroll_speed', 2)
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)
config.set("gamepad.joystick.x_scroll_speed", 1)
config.set("gamepad.joystick.y_scroll_speed", 2)
# vertical wheel event values are negative
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1))
self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -2))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 2))
await self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1))
await self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
await self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -2))
await self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 2))
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
await self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
await self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
await self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
await self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
async def test_joystick_purpose_3(self):
asyncio.ensure_future(self.event_producer.run())
def test_joystick_purpose_3(self):
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(MIN_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_Y, -speed))
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL)
config.set('gamepad.joystick.right_purpose', WHEEL)
self.mapping.set('gamepad.joystick.x_scroll_speed', 2)
self.mapping.set('gamepad.joystick.y_scroll_speed', 3)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 2))
self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -2))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -3))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 3))
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)
await self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed))
await self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
await self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
await self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_Y, -speed))
await self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
await self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
await self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
await self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
async def test_joystick_purpose_4(self):
asyncio.ensure_future(self.event_producer.run())
config.set("gamepad.joystick.left_purpose", WHEEL)
config.set("gamepad.joystick.right_purpose", WHEEL)
self.mapping.set("gamepad.joystick.x_scroll_speed", 2)
self.mapping.set("gamepad.joystick.y_scroll_speed", 3)
await self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 2))
await self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -2))
await self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -3))
await self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 3))
# vertical wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, 2))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_HWHEEL, -2))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -3))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_WHEEL, 3))
await self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, 2))
await self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_HWHEEL, -2))
await self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -3))
await self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_WHEEL, 3))
if __name__ == "__main__":

@ -27,9 +27,18 @@ import evdev
from evdev.ecodes import EV_KEY, KEY_A
from keymapper.paths import CONFIG_PATH
from keymapper.groups import _FindGroups, groups, classify, \
GAMEPAD, MOUSE, UNKNOWN, GRAPHICS_TABLET, TOUCHPAD, \
KEYBOARD, _Group
from keymapper.groups import (
_FindGroups,
groups,
classify,
GAMEPAD,
MOUSE,
UNKNOWN,
GRAPHICS_TABLET,
TOUCHPAD,
KEYBOARD,
_Group,
)
from tests.test import quick_cleanup, fixtures
@ -47,16 +56,16 @@ class TestGroups(unittest.TestCase):
def test_group(self):
group = _Group(
paths=['/dev/a', '/dev/b', '/dev/c'],
names=['name_bar', 'name_a', 'name_foo'],
paths=["/dev/a", "/dev/b", "/dev/c"],
names=["name_bar", "name_a", "name_foo"],
types=[MOUSE, KEYBOARD, UNKNOWN],
key='key'
key="key",
)
self.assertEqual(group.name, 'name_a')
self.assertEqual(group.key, 'key')
self.assertEqual(group.name, "name_a")
self.assertEqual(group.key, "key")
self.assertEqual(
group.get_preset_path('preset1234'),
os.path.join(CONFIG_PATH, 'presets', group.name, 'preset1234.json')
group.get_preset_path("preset1234"),
os.path.join(CONFIG_PATH, "presets", group.name, "preset1234.json"),
)
def test_find_groups(self):
@ -66,129 +75,134 @@ class TestGroups(unittest.TestCase):
groups.loads(pipe.groups)
self.maxDiff = None
self.assertEqual(groups.dumps(), json.dumps([
json.dumps({
'paths': [
'/dev/input/event1',
],
'names': [
'Foo Device'
],
'types': [KEYBOARD],
'key': 'Foo Device'
}),
json.dumps({
'paths': [
'/dev/input/event11',
'/dev/input/event10',
'/dev/input/event13'
],
'names': [
'Foo Device foo',
'Foo Device',
'Foo Device'
],
'types': [KEYBOARD, MOUSE],
'key': 'Foo Device 2'
}),
json.dumps({
'paths': ['/dev/input/event20'],
'names': ['Bar Device'],
'types': [KEYBOARD],
'key': 'Bar Device'
}),
json.dumps({
'paths': ['/dev/input/event30'],
'names': ['gamepad'],
'types': [GAMEPAD],
'key': 'gamepad'
}),
json.dumps({
'paths': ['/dev/input/event40'],
'names': ['key-mapper Bar Device'],
'types': [KEYBOARD],
'key': 'key-mapper Bar Device'
}),
]))
groups2 = json.dumps([
group.dumps() for group in
groups.filter(include_keymapper=True)
])
self.assertEqual(
groups.dumps(),
json.dumps(
[
json.dumps(
{
"paths": [
"/dev/input/event1",
],
"names": ["Foo Device"],
"types": [KEYBOARD],
"key": "Foo Device",
}
),
json.dumps(
{
"paths": [
"/dev/input/event11",
"/dev/input/event10",
"/dev/input/event13",
],
"names": ["Foo Device foo", "Foo Device", "Foo Device"],
"types": [KEYBOARD, MOUSE],
"key": "Foo Device 2",
}
),
json.dumps(
{
"paths": ["/dev/input/event20"],
"names": ["Bar Device"],
"types": [KEYBOARD],
"key": "Bar Device",
}
),
json.dumps(
{
"paths": ["/dev/input/event30"],
"names": ["gamepad"],
"types": [GAMEPAD],
"key": "gamepad",
}
),
json.dumps(
{
"paths": ["/dev/input/event40"],
"names": ["key-mapper Bar Device"],
"types": [KEYBOARD],
"key": "key-mapper Bar Device",
}
),
]
),
)
groups2 = json.dumps(
[group.dumps() for group in groups.filter(include_keymapper=True)]
)
self.assertEqual(pipe.groups, groups2)
def test_list_group_names(self):
self.assertListEqual(groups.list_group_names(), [
'Foo Device',
'Foo Device',
'Bar Device',
'gamepad',
])
self.assertListEqual(
groups.list_group_names(),
[
"Foo Device",
"Foo Device",
"Bar Device",
"gamepad",
],
)
def test_filter(self):
# by default no key-mapper devices are present
filtered = groups.filter()
keys = [group.key for group in filtered]
self.assertIn('Foo Device 2', keys)
self.assertNotIn('key-mapper Bar Device', keys)
self.assertIn("Foo Device 2", keys)
self.assertNotIn("key-mapper Bar Device", keys)
def test_skip_camera(self):
fixtures['/foo/bar'] = {
'name': 'camera', 'phys': 'abcd1',
'info': evdev.DeviceInfo(1, 2, 3, 4),
'capabilities': {
evdev.ecodes.EV_KEY: [
evdev.ecodes.KEY_CAMERA
]
}
fixtures["/foo/bar"] = {
"name": "camera",
"phys": "abcd1",
"info": evdev.DeviceInfo(1, 2, 3, 4),
"capabilities": {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_CAMERA]},
}
groups.refresh()
self.assertIsNone(groups.find(name='camera'))
self.assertIsNotNone(groups.find(name='gamepad'))
self.assertIsNone(groups.find(name="camera"))
self.assertIsNotNone(groups.find(name="gamepad"))
def test_device_with_only_ev_abs(self):
# could be anything, a lot of devices have ABS_X capabilities,
# so it is not treated as gamepad joystick and since it also
# doesn't have key capabilities, there is nothing to map.
fixtures['/foo/bar'] = {
'name': 'qux', 'phys': 'abcd2',
'info': evdev.DeviceInfo(1, 2, 3, 4),
'capabilities': {
evdev.ecodes.EV_ABS: [
evdev.ecodes.ABS_X
]
}
fixtures["/foo/bar"] = {
"name": "qux",
"phys": "abcd2",
"info": evdev.DeviceInfo(1, 2, 3, 4),
"capabilities": {evdev.ecodes.EV_ABS: [evdev.ecodes.ABS_X]},
}
groups.refresh()
self.assertIsNotNone(groups.find(name='gamepad'))
self.assertIsNone(groups.find(name='qux'))
self.assertIsNotNone(groups.find(name="gamepad"))
self.assertIsNone(groups.find(name="qux"))
# verify this test even works at all
fixtures['/foo/bar']['capabilities'][EV_KEY] = [KEY_A]
fixtures["/foo/bar"]["capabilities"][EV_KEY] = [KEY_A]
groups.refresh()
self.assertIsNotNone(groups.find(name='qux'))
self.assertIsNotNone(groups.find(name="qux"))
def test_duplicate_device(self):
fixtures['/dev/input/event20']['name'] = 'Foo Device'
fixtures["/dev/input/event20"]["name"] = "Foo Device"
groups.refresh()
group1 = groups.find(key='Foo Device')
group2 = groups.find(key='Foo Device 2')
group3 = groups.find(key='Foo Device 3')
group1 = groups.find(key="Foo Device")
group2 = groups.find(key="Foo Device 2")
group3 = groups.find(key="Foo Device 3")
self.assertIn('/dev/input/event1', group1.paths)
self.assertIn('/dev/input/event10', group2.paths)
self.assertIn('/dev/input/event20', group3.paths)
self.assertIn("/dev/input/event1", group1.paths)
self.assertIn("/dev/input/event10", group2.paths)
self.assertIn("/dev/input/event20", group3.paths)
self.assertEqual(group1.key, 'Foo Device')
self.assertEqual(group2.key, 'Foo Device 2')
self.assertEqual(group3.key, 'Foo Device 3')
self.assertEqual(group1.key, "Foo Device")
self.assertEqual(group2.key, "Foo Device 2")
self.assertEqual(group3.key, "Foo Device 3")
self.assertEqual(group1.name, 'Foo Device')
self.assertEqual(group2.name, 'Foo Device')
self.assertEqual(group3.name, 'Foo Device')
self.assertEqual(group1.name, "Foo Device")
self.assertEqual(group2.name, "Foo Device")
self.assertEqual(group3.name, "Foo Device")
def test_classify(self):
# properly detects if the device is a gamepad
@ -206,57 +220,92 @@ class TestGroups(unittest.TestCase):
"""gamepads"""
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_A]
})), GAMEPAD)
self.assertEqual(
classify(
FakeDevice(
{
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_A],
}
)
),
GAMEPAD,
)
"""mice"""
self.assertEqual(classify(FakeDevice({
EV_REL: [evdev.ecodes.REL_X, evdev.ecodes.REL_Y, evdev.ecodes.REL_WHEEL],
EV_KEY: [evdev.ecodes.BTN_LEFT]
})), MOUSE)
self.assertEqual(
classify(
FakeDevice(
{
EV_REL: [
evdev.ecodes.REL_X,
evdev.ecodes.REL_Y,
evdev.ecodes.REL_WHEEL,
],
EV_KEY: [evdev.ecodes.BTN_LEFT],
}
)
),
MOUSE,
)
"""keyboard"""
self.assertEqual(classify(FakeDevice({
EV_KEY: [evdev.ecodes.KEY_A]
})), KEYBOARD)
self.assertEqual(classify(FakeDevice({EV_KEY: [evdev.ecodes.KEY_A]})), KEYBOARD)
"""touchpads"""
self.assertEqual(classify(FakeDevice({
EV_KEY: [evdev.ecodes.KEY_A],
EV_ABS: [evdev.ecodes.ABS_MT_POSITION_X]
})), TOUCHPAD)
self.assertEqual(
classify(
FakeDevice(
{
EV_KEY: [evdev.ecodes.KEY_A],
EV_ABS: [evdev.ecodes.ABS_MT_POSITION_X],
}
)
),
TOUCHPAD,
)
"""graphics tablets"""
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_STYLUS]
})), GRAPHICS_TABLET)
self.assertEqual(
classify(
FakeDevice(
{
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_STYLUS],
}
)
),
GRAPHICS_TABLET,
)
"""weird combos"""
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.KEY_1]
})), UNKNOWN)
self.assertEqual(
classify(
FakeDevice(
{
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.KEY_1],
}
)
),
UNKNOWN,
)
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X],
EV_KEY: [evdev.ecodes.BTN_A]
})), UNKNOWN)
self.assertEqual(
classify(
FakeDevice({EV_ABS: [evdev.ecodes.ABS_X], EV_KEY: [evdev.ecodes.BTN_A]})
),
UNKNOWN,
)
self.assertEqual(classify(FakeDevice({
EV_KEY: [evdev.ecodes.BTN_A]
})), UNKNOWN)
self.assertEqual(classify(FakeDevice({EV_KEY: [evdev.ecodes.BTN_A]})), UNKNOWN)
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X]
})), UNKNOWN)
self.assertEqual(classify(FakeDevice({EV_ABS: [evdev.ecodes.ABS_X]})), UNKNOWN)
if __name__ == "__main__":

@ -25,35 +25,71 @@ import time
import copy
import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_LEFT, \
KEY_A, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, BTN_A, ABS_X, ABS_Y, \
ABS_Z, ABS_RZ, ABS_VOLUME, KEY_B, KEY_C
from keymapper.injection.injector import Injector, is_in_capabilities, \
STARTING, RUNNING, STOPPED, NO_GRAB, UNKNOWN
from keymapper.injection.numlock import is_numlock_on, set_numlock, \
ensure_numlock
from keymapper.state import custom_mapping, system_mapping
from evdev.ecodes import (
EV_REL,
EV_KEY,
EV_ABS,
ABS_HAT0X,
BTN_LEFT,
KEY_A,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
BTN_A,
ABS_X,
ABS_Y,
ABS_Z,
ABS_RZ,
ABS_VOLUME,
KEY_B,
KEY_C,
)
from keymapper.injection.consumers.event_producer import EventProducer
from keymapper.injection.injector import (
Injector,
is_in_capabilities,
STARTING,
RUNNING,
STOPPED,
NO_GRAB,
UNKNOWN,
)
from keymapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock
from keymapper.system_mapping import system_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME
from keymapper.config import config, NONE, MOUSE, WHEEL, BUTTONS
from keymapper.key import Key
from keymapper.injection.macros import parse
from keymapper.injection.macros.parse import parse
from keymapper.injection.context import Context
from keymapper.groups import groups, classify, GAMEPAD
from tests.test import new_event, push_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice, uinputs, \
keyboard_keys, MIN_ABS
class TestInjector(unittest.TestCase):
new_gamepad_path = '/dev/input/event100'
from tests.test import (
new_event,
push_events,
fixtures,
EVENT_READ_TIMEOUT,
uinput_write_history_pipe,
MAX_ABS,
quick_cleanup,
read_write_history_pipe,
InputDevice,
uinputs,
keyboard_keys,
MIN_ABS,
)
class TestInjector(unittest.IsolatedAsyncioTestCase):
new_gamepad_path = "/dev/input/event100"
@classmethod
def setUpClass(cls):
cls.injector = None
cls.grab = evdev.InputDevice.grab
quick_cleanup()
def setUp(self):
self.failed = 0
@ -75,13 +111,21 @@ class TestInjector(unittest.TestCase):
quick_cleanup()
def find_event_producer(self):
# this object became somewhat a pain to retreive
return [
consumer
for consumer in self.injector._consumer_controls[0]._consumers
if isinstance(consumer, EventProducer)
][0]
def test_grab(self):
# path is from the fixtures
path = '/dev/input/event10'
path = "/dev/input/event10"
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
custom_mapping.change(Key(EV_KEY, 10, 1), "a")
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
# this test needs to pass around all other constraints of
# _grab_device
self.injector.context = Context(custom_mapping)
@ -90,14 +134,14 @@ class TestInjector(unittest.TestCase):
self.assertFalse(gamepad)
self.assertEqual(self.failed, 2)
# success on the third try
self.assertEqual(device.name, fixtures[path]['name'])
self.assertEqual(device.name, fixtures[path]["name"])
def test_fail_grab(self):
self.make_it_fail = 999
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
custom_mapping.change(Key(EV_KEY, 10, 1), "a")
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
path = '/dev/input/event10'
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
path = "/dev/input/event10"
self.injector.context = Context(custom_mapping)
device = self.injector._grab_device(path)
self.assertIsNone(device)
@ -113,25 +157,25 @@ class TestInjector(unittest.TestCase):
self.assertEqual(self.injector.get_state(), NO_GRAB)
def test_grab_device_1(self):
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), "a")
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
_grab_device = self.injector._grab_device
# doesn't have the required capability
self.assertIsNone(_grab_device('/dev/input/event10'))
self.assertIsNone(_grab_device("/dev/input/event10"))
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
self.assertIsNotNone(_grab_device('/dev/input/event30'))
self.assertIsNotNone(_grab_device("/dev/input/event30"))
# this doesn't exist
self.assertIsNone(_grab_device('/dev/input/event1234'))
self.assertIsNone(_grab_device("/dev/input/event1234"))
def test_gamepad_capabilities(self):
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
# give the injector a reason to grab the device
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
self.injector.context = Context(custom_mapping)
path = '/dev/input/event30'
path = "/dev/input/event30"
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
self.assertIsNotNone(device)
@ -151,17 +195,17 @@ class TestInjector(unittest.TestCase):
def test_gamepad_purpose_none(self):
# forward abs joystick events
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
config.set('gamepad.joystick.right_purpose', NONE)
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", NONE)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = '/dev/input/event30'
path = "/dev/input/event30"
device = self.injector._grab_device(path)
self.assertIsNone(device) # no capability is used, so it won't grab
custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a')
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "a")
device = self.injector._grab_device(path)
self.assertIsNotNone(device)
gamepad = classify(device) == GAMEPAD
@ -171,13 +215,13 @@ class TestInjector(unittest.TestCase):
def test_gamepad_purpose_none_2(self):
# forward abs joystick events for the left joystick only
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
config.set('gamepad.joystick.right_purpose', MOUSE)
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", MOUSE)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = '/dev/input/event30'
path = "/dev/input/event30"
device = self.injector._grab_device(path)
# the right joystick maps as mouse, so it is grabbed
# even with an empty mapping
@ -188,7 +232,7 @@ class TestInjector(unittest.TestCase):
self.assertNotIn(EV_ABS, capabilities)
self.assertIn(EV_REL, capabilities)
custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a')
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "a")
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
self.assertIsNotNone(device)
@ -201,20 +245,22 @@ class TestInjector(unittest.TestCase):
def test_adds_ev_key(self):
# for some reason, having any EV_KEY capability is needed to
# be able to control the mouse. it probably wants the mouse click.
custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a')
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "a")
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
self.injector.context = Context(custom_mapping)
"""ABS device without any key capability"""
path = self.new_gamepad_path
gamepad_template = copy.deepcopy(fixtures['/dev/input/event30'])
gamepad_template = copy.deepcopy(fixtures["/dev/input/event30"])
fixtures[path] = {
'name': 'qux 2', 'phys': 'abcd', 'info': '1234',
'capabilities': gamepad_template['capabilities']
"name": "qux 2",
"phys": "abcd",
"info": "1234",
"capabilities": gamepad_template["capabilities"],
}
del fixtures[path]['capabilities'][EV_KEY]
del fixtures[path]["capabilities"][EV_KEY]
device = self.injector._grab_device(path)
# no reason to grab, BTN_A capability is missing in the device
self.assertIsNone(device)
@ -222,13 +268,15 @@ class TestInjector(unittest.TestCase):
"""ABS device with a btn_mouse capability"""
path = self.new_gamepad_path
gamepad_template = copy.deepcopy(fixtures['/dev/input/event30'])
gamepad_template = copy.deepcopy(fixtures["/dev/input/event30"])
fixtures[path] = {
'name': 'qux 3', 'phys': 'abcd', 'info': '1234',
'capabilities': gamepad_template['capabilities']
"name": "qux 3",
"phys": "abcd",
"info": "1234",
"capabilities": gamepad_template["capabilities"],
}
fixtures[path]['capabilities'][EV_KEY].append(BTN_LEFT)
fixtures[path]['capabilities'][EV_KEY].append(KEY_A)
fixtures[path]["capabilities"][EV_KEY].append(BTN_LEFT)
fixtures[path]["capabilities"][EV_KEY].append(KEY_A)
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
capabilities = self.injector._construct_capabilities(gamepad)
@ -238,7 +286,7 @@ class TestInjector(unittest.TestCase):
"""a gamepad"""
path = '/dev/input/event30'
path = "/dev/input/event30"
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
self.assertIn(EV_KEY, device.capabilities())
@ -250,21 +298,21 @@ class TestInjector(unittest.TestCase):
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
custom_mapping.change(Key(EV_KEY, 10, 1), "a")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = '/dev/input/event11'
path = "/dev/input/event11"
device = self.injector._grab_device(path)
self.assertIsNone(device)
self.assertEqual(self.failed, 0)
def test_skip_unknown_device(self):
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
custom_mapping.change(Key(EV_KEY, 10, 1), "a")
# skips a device because its capabilities are not used in the mapping
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = '/dev/input/event11'
path = "/dev/input/event11"
device = self.injector._grab_device(path)
# skips the device alltogether, so no grab attempts fail
@ -297,23 +345,26 @@ class TestInjector(unittest.TestCase):
def test_gamepad_to_mouse(self):
# maps gamepad joystick events to mouse events
config.set('gamepad.joystick.non_linearity', 1)
config.set("gamepad.joystick.non_linearity", 1)
pointer_speed = 80
config.set('gamepad.joystick.pointer_speed', pointer_speed)
config.set('gamepad.joystick.left_purpose', MOUSE)
config.set("gamepad.joystick.pointer_speed", pointer_speed)
config.set("gamepad.joystick.left_purpose", MOUSE)
# they need to sum up before something is written
divisor = 10
x = MAX_ABS / pointer_speed / divisor
y = MAX_ABS / pointer_speed / divisor
push_events('gamepad', [
new_event(EV_ABS, ABS_X, x),
new_event(EV_ABS, ABS_Y, y),
new_event(EV_ABS, ABS_X, -x),
new_event(EV_ABS, ABS_Y, -y),
])
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
push_events(
"gamepad",
[
new_event(EV_ABS, ABS_X, x),
new_event(EV_ABS, ABS_Y, y),
new_event(EV_ABS, ABS_X, -x),
new_event(EV_ABS, ABS_Y, -y),
],
)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
@ -328,7 +379,7 @@ class TestInjector(unittest.TestCase):
if history[0][0] == EV_ABS:
raise AssertionError(
'The injector probably just forwarded them unchanged'
"The injector probably just forwarded them unchanged"
# possibly in addition to writing mouse events
)
@ -346,22 +397,26 @@ class TestInjector(unittest.TestCase):
self.assertEqual(len(history), count_x + count_y)
def test_gamepad_forward_joysticks(self):
push_events('gamepad', [
# should forward them unmodified
new_event(EV_ABS, ABS_X, 10),
new_event(EV_ABS, ABS_Y, 20),
new_event(EV_ABS, ABS_X, -30),
new_event(EV_ABS, ABS_Y, -40),
new_event(EV_KEY, BTN_A, 1),
new_event(EV_KEY, BTN_A, 0)
] * 2)
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
custom_mapping.set('gamepad.joystick.right_purpose', NONE)
push_events(
"gamepad",
[
# should forward them unmodified
new_event(EV_ABS, ABS_X, 10),
new_event(EV_ABS, ABS_Y, 20),
new_event(EV_ABS, ABS_X, -30),
new_event(EV_ABS, ABS_Y, -40),
new_event(EV_KEY, BTN_A, 1),
new_event(EV_KEY, BTN_A, 0),
]
* 2,
)
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
custom_mapping.set("gamepad.joystick.right_purpose", NONE)
# BTN_A -> 77
custom_mapping.change(Key((1, BTN_A, 1)), 'b')
system_mapping._set('b', 77)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
custom_mapping.change(Key((1, BTN_A, 1)), "b")
system_mapping._set("b", 77)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
@ -382,16 +437,19 @@ class TestInjector(unittest.TestCase):
# map one of the triggers to BTN_NORTH, while the other one
# should be forwarded unchanged
value = MAX_ABS // 2
push_events('gamepad', [
new_event(EV_ABS, ABS_Z, value),
new_event(EV_ABS, ABS_RZ, value),
])
push_events(
"gamepad",
[
new_event(EV_ABS, ABS_Z, value),
new_event(EV_ABS, ABS_RZ, value),
],
)
# ABS_Z -> 77
# ABS_RZ is not mapped
custom_mapping.change(Key((EV_ABS, ABS_Z, 1)), 'b')
system_mapping._set('b', 77)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
custom_mapping.change(Key((EV_ABS, ABS_Z, 1)), "b")
system_mapping._set("b", 77)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
@ -404,105 +462,96 @@ class TestInjector(unittest.TestCase):
self.assertEqual(history.count((EV_KEY, 77, 1)), 1)
self.assertEqual(history.count((EV_ABS, ABS_RZ, value)), 1)
@mock.patch('evdev.InputDevice.ungrab')
@mock.patch("evdev.InputDevice.ungrab")
def test_gamepad_to_mouse_event_producer(self, ungrab_patch):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.set('gamepad.joystick.right_purpose', NONE)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
custom_mapping.set("gamepad.joystick.right_purpose", NONE)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
# the stop message will be available in the pipe right away,
# so run won't block and just stop. all the stuff
# will be initialized though, so that stuff can be tested
self.injector.stop_injecting()
# the context serves no purpose in the main process
# the context serves no purpose in the main process (which runs the
# tests). The context is only accessible in the newly created process.
self.assertIsNone(self.injector.context)
# not in a process because this doesn't call start, so the
# event_producer state can be checked
self.injector.run()
# not in a process, so the event_producer state can be checked
self.assertEqual(self.injector._event_producer.abs_range[0], MIN_ABS)
self.assertEqual(self.injector._event_producer.abs_range[1], MAX_ABS)
event_producer = self.find_event_producer()
self.assertEqual(event_producer._abs_range[0], MIN_ABS)
self.assertEqual(event_producer._abs_range[1], MAX_ABS)
self.assertEqual(
self.injector.context.mapping.get('gamepad.joystick.left_purpose'),
MOUSE
self.injector.context.mapping.get("gamepad.joystick.left_purpose"), MOUSE
)
self.assertEqual(ungrab_patch.call_count, 1)
def test_gamepad_to_buttons_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
custom_mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.injector = Injector(groups.find(name='gamepad'), custom_mapping)
def test_device1_not_a_gamepad(self):
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
custom_mapping.set("gamepad.joystick.right_purpose", WHEEL)
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.stop_injecting()
self.injector.run()
self.assertIsNone(self.injector._event_producer.abs_range)
def test_device1_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
self.injector.stop_injecting()
self.injector.run()
# not a gamepad, so _event_producer is not initialized for that.
# it can still debounce stuff though
self.assertIsNone(self.injector._event_producer.abs_range)
# not a gamepad, so nothing should happen
self.assertEqual(len(self.injector._consumer_controls), 0)
def test_get_udev_name(self):
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
suffix = 'mapped'
prefix = 'key-mapper'
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
suffix = "mapped"
prefix = "key-mapper"
expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}'
self.assertEqual(len(expected), 80)
self.assertEqual(
self.injector.get_udev_name('a' * 100, suffix),
expected
)
self.assertEqual(self.injector.get_udev_name("a" * 100, suffix), expected)
self.injector.device = 'abcd'
self.injector.device = "abcd"
self.assertEqual(
self.injector.get_udev_name('abcd', 'forwarded'),
'key-mapper abcd forwarded'
self.injector.get_udev_name("abcd", "forwarded"),
"key-mapper abcd forwarded",
)
@mock.patch('evdev.InputDevice.ungrab')
@mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch):
custom_mapping.change(Key(EV_KEY, KEY_A, 1), 'c')
custom_mapping.change(Key(EV_REL, REL_HWHEEL, 1), 'k(b)')
self.injector = Injector(groups.find(key='Foo Device 2'), custom_mapping)
custom_mapping.change(Key(EV_KEY, KEY_A, 1), "c")
custom_mapping.change(Key(EV_REL, REL_HWHEEL, 1), "k(b)")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.stop_injecting()
self.injector.run()
self.assertEqual(
self.injector.context.mapping.get_symbol(Key(EV_KEY, KEY_A, 1)),
'c'
self.injector.context.mapping.get_symbol(Key(EV_KEY, KEY_A, 1)), "c"
)
self.assertEqual(
self.injector.context.key_to_code[((EV_KEY, KEY_A, 1),)],
KEY_C
self.injector.context.key_to_code[((EV_KEY, KEY_A, 1),)], KEY_C
)
self.assertEqual(
self.injector.context.mapping.get_symbol(Key(EV_REL, REL_HWHEEL, 1)),
'k(b)'
self.injector.context.mapping.get_symbol(Key(EV_REL, REL_HWHEEL, 1)), "k(b)"
)
self.assertEqual(
self.injector.context.macros[((EV_REL, REL_HWHEEL, 1),)].code,
'k(b)'
self.injector.context.macros[((EV_REL, REL_HWHEEL, 1),)].code, "k(b)"
)
self.assertListEqual(
sorted(uinputs.keys()),
sorted([
# reading and preventing original events from reaching the
# display server
'key-mapper Foo Device foo forwarded',
'key-mapper Foo Device forwarded',
# injection
'key-mapper Foo Device 2 mapped'
])
sorted(
[
# reading and preventing original events from reaching the
# display server
"key-mapper Foo Device foo forwarded",
"key-mapper Foo Device forwarded",
# injection
"key-mapper Foo Device 2 mapped",
]
),
)
forwarded_foo = uinputs.get('key-mapper Foo Device foo forwarded')
forwarded = uinputs.get('key-mapper Foo Device forwarded')
mapped = uinputs.get('key-mapper Foo Device 2 mapped')
forwarded_foo = uinputs.get("key-mapper Foo Device foo forwarded")
forwarded = uinputs.get("key-mapper Foo Device forwarded")
mapped = uinputs.get("key-mapper Foo Device 2 mapped")
self.assertIsNotNone(forwarded_foo)
self.assertIsNotNone(forwarded)
self.assertIsNotNone(mapped)
@ -518,10 +567,7 @@ class TestInjector(unittest.TestCase):
# copies capabilities for all other forwarded devices
self.assertIn(EV_REL, forwarded_foo.capabilities())
self.assertIn(EV_KEY, forwarded.capabilities())
self.assertEqual(
sorted(forwarded.capabilities()[EV_KEY]),
keyboard_keys
)
self.assertEqual(sorted(forwarded.capabilities()[EV_KEY]), keyboard_keys)
self.assertEqual(ungrab_patch.call_count, 2)
@ -531,37 +577,40 @@ class TestInjector(unittest.TestCase):
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')
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')
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)
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)
@ -641,14 +690,14 @@ class TestInjector(unittest.TestCase):
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)')
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)
system_mapping._set("w", code_w)
system_mapping._set("d", code_d)
def do_stuff():
if self.injector is not None:
@ -658,18 +707,21 @@ class TestInjector(unittest.TestCase):
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),
])
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)
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')
input = InputDevice("/dev/input/event30")
self.injector._grab_device = lambda *args: input
self.injector.start()
@ -687,7 +739,7 @@ class TestInjector(unittest.TestCase):
"""yes"""
with mock.patch('keymapper.utils.should_map_as_btn', lambda *_: True):
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)
@ -708,29 +760,28 @@ class TestInjector(unittest.TestCase):
# 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')
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)
])
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')
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())
@ -779,8 +830,8 @@ class TestInjector(unittest.TestCase):
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
# a combination
mapping.change(Key(ev_1, ev_2, ev_3), 'k(a)')
self.injector = Injector(groups.find(key='Foo Device 2'), mapping)
mapping.change(Key(ev_1, ev_2, ev_3), "k(a)")
self.injector = Injector(groups.find(key="Foo Device 2"), mapping)
history = []
@ -793,9 +844,7 @@ class TestInjector(unittest.TestCase):
raise Stop()
with mock.patch.object(
self.injector,
'_construct_capabilities',
_construct_capabilities
self.injector, "_construct_capabilities", _construct_capabilities
):
try:
self.injector.run()
@ -807,8 +856,8 @@ class TestInjector(unittest.TestCase):
# first argument of the first call
macros = self.injector.context.macros
self.assertEqual(len(macros), 2)
self.assertEqual(macros[(ev_1, ev_2, ev_3)].code, 'k(a)')
self.assertEqual(macros[(ev_2, ev_1, ev_3)].code, 'k(a)')
self.assertEqual(macros[(ev_1, ev_2, ev_3)].code, "k(a)")
self.assertEqual(macros[(ev_2, ev_1, ev_3)].code, "k(a)")
def test_key_to_code(self):
mapping = Mapping()
@ -816,16 +865,16 @@ class TestInjector(unittest.TestCase):
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
ev_4 = (EV_KEY, 44, 1)
mapping.change(Key(ev_1), 'a')
mapping.change(Key(ev_1), "a")
# a combination
mapping.change(Key(ev_2, ev_3, ev_4), 'b')
self.assertEqual(mapping.get_symbol(Key(ev_2, ev_3, ev_4)), 'b')
mapping.change(Key(ev_2, ev_3, ev_4), "b")
self.assertEqual(mapping.get_symbol(Key(ev_2, ev_3, ev_4)), "b")
system_mapping.clear()
system_mapping._set('a', 51)
system_mapping._set('b', 52)
system_mapping._set("a", 51)
system_mapping._set("b", 52)
injector = Injector(groups.find(key='Foo Device 2'), mapping)
injector = Injector(groups.find(key="Foo Device 2"), mapping)
injector.context = Context(mapping)
self.assertEqual(injector.context.key_to_code.get((ev_1,)), 51)
# permutations to make matching combinations easier
@ -835,28 +884,26 @@ class TestInjector(unittest.TestCase):
def test_is_in_capabilities(self):
key = Key(1, 2, 1)
capabilities = {
1: [9, 2, 5]
}
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
key = Key((1, 2, 1), (1, 3, 1))
capabilities = {
1: [9, 2, 5]
}
capabilities = {1: [9, 2, 5]}
# only one of the codes of the combination is required.
# The goal is to make combinations across those sub-devices possible,
# that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities))
key = Key((1, 2, 1), (1, 5, 1))
capabilities = {
1: [9, 2, 5]
}
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
class TestModifyCapabilities(unittest.TestCase):
@classmethod
def setUpClass(cls):
quick_cleanup()
def setUp(self):
class FakeDevice:
def __init__(self):
@ -864,16 +911,30 @@ class TestModifyCapabilities(unittest.TestCase):
evdev.ecodes.EV_SYN: [1, 2, 3],
evdev.ecodes.EV_FF: [1, 2, 3],
EV_ABS: [
(1, evdev.AbsInfo(
value=None, min=None, max=1234, fuzz=None,
flat=None, resolution=None
)),
(2, evdev.AbsInfo(
value=None, min=50, max=2345, fuzz=None,
flat=None, resolution=None
)),
3
]
(
1,
evdev.AbsInfo(
value=None,
min=None,
max=1234,
fuzz=None,
flat=None,
resolution=None,
),
),
(
2,
evdev.AbsInfo(
value=None,
min=50,
max=2345,
fuzz=None,
flat=None,
resolution=None,
),
),
3,
],
}
def capabilities(self, absinfo=False):
@ -881,23 +942,23 @@ class TestModifyCapabilities(unittest.TestCase):
return self._capabilities
mapping = Mapping()
mapping.change(Key(EV_KEY, 80, 1), 'a')
mapping.change(Key(EV_KEY, 80, 1), "a")
mapping.change(Key(EV_KEY, 81, 1), DISABLE_NAME)
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, mapping)
mapping.change(Key(EV_KEY, 60, 111), macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change(Key(EV_REL, 1234, 3), 'b')
mapping.change(Key(EV_REL, 1234, 3), "b")
self.a = system_mapping.get('a')
self.shift_l = system_mapping.get('ShIfT_L')
self.a = system_mapping.get("a")
self.shift_l = system_mapping.get("ShIfT_L")
self.one = system_mapping.get(1)
self.two = system_mapping.get('2')
self.left = system_mapping.get('BtN_lEfT')
self.two = system_mapping.get("2")
self.left = system_mapping.get("BtN_lEfT")
self.fake_device = FakeDevice()
self.mapping = mapping
self.macro = macro
@ -917,8 +978,8 @@ class TestModifyCapabilities(unittest.TestCase):
def test_construct_capabilities(self):
self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code)
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.injector.context = Context(self.mapping)
capabilities = self.injector._construct_capabilities(gamepad=False)
@ -936,10 +997,10 @@ class TestModifyCapabilities(unittest.TestCase):
def test_copy_capabilities(self):
self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code)
# I don't know what ABS_VOLUME is, for now I would like to just always
# remove it until somebody complains, since its presence broke stuff
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.fake_device._capabilities = {
EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))],
EV_KEY: [1, 2, 3],
@ -959,10 +1020,10 @@ class TestModifyCapabilities(unittest.TestCase):
def test_construct_capabilities_gamepad(self):
self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code)
config.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
config.set("gamepad.joystick.left_purpose", MOUSE)
self.mapping.set("gamepad.joystick.right_purpose", WHEEL)
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.injector.context = Context(self.mapping)
self.assertTrue(self.injector.context.maps_joystick())
self.assertTrue(self.injector.context.joystick_as_mouse())
@ -981,10 +1042,10 @@ class TestModifyCapabilities(unittest.TestCase):
def test_construct_capabilities_gamepad_none_none(self):
self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code)
config.set('gamepad.joystick.left_purpose', NONE)
self.mapping.set('gamepad.joystick.right_purpose', NONE)
config.set("gamepad.joystick.left_purpose", NONE)
self.mapping.set("gamepad.joystick.right_purpose", NONE)
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.injector.context = Context(self.mapping)
self.assertFalse(self.injector.context.maps_joystick())
self.assertFalse(self.injector.context.joystick_as_mouse())
@ -998,10 +1059,10 @@ class TestModifyCapabilities(unittest.TestCase):
def test_construct_capabilities_gamepad_buttons_buttons(self):
self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code)
config.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
config.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.injector.context = Context(self.mapping)
self.assertTrue(self.injector.context.maps_joystick())
self.assertFalse(self.injector.context.joystick_as_mouse())
@ -1017,10 +1078,10 @@ class TestModifyCapabilities(unittest.TestCase):
self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code)
# those settings shouldn't have an effect with gamepad=False
config.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
config.set("gamepad.joystick.left_purpose", BUTTONS)
self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
self.injector = Injector(groups.find(name='foo'), self.mapping)
self.injector = Injector(groups.find(name="foo"), self.mapping)
self.injector.context = Context(self.mapping)
capabilities = self.injector._construct_capabilities(gamepad=False)

File diff suppressed because it is too large Load Diff

@ -47,21 +47,21 @@ class TestSocket(unittest.TestCase):
self.assertFalse(s2.poll())
self.assertEqual(s2.recv(), None)
server = Server('/tmp/key-mapper-test/socket1')
client = Client('/tmp/key-mapper-test/socket1')
server = Server("/tmp/key-mapper-test/socket1")
client = Client("/tmp/key-mapper-test/socket1")
test(server, client)
client = Client('/tmp/key-mapper-test/socket2')
server = Server('/tmp/key-mapper-test/socket2')
client = Client("/tmp/key-mapper-test/socket2")
server = Server("/tmp/key-mapper-test/socket2")
test(client, server)
def test_not_connected_1(self):
# client discards old message, because it might have had a purpose
# for a different client and not for the current one
server = Server('/tmp/key-mapper-test/socket3')
server = Server("/tmp/key-mapper-test/socket3")
server.send(1)
client = Client('/tmp/key-mapper-test/socket3')
client = Client("/tmp/key-mapper-test/socket3")
server.send(2)
self.assertTrue(client.poll())
@ -70,10 +70,10 @@ class TestSocket(unittest.TestCase):
self.assertEqual(client.recv(), None)
def test_not_connected_2(self):
client = Client('/tmp/key-mapper-test/socket4')
client = Client("/tmp/key-mapper-test/socket4")
client.send(1)
server = Server('/tmp/key-mapper-test/socket4')
server = Server("/tmp/key-mapper-test/socket4")
client.send(2)
self.assertTrue(server.poll())
@ -83,8 +83,8 @@ class TestSocket(unittest.TestCase):
def test_select(self):
"""is compatible to select.select"""
server = Server('/tmp/key-mapper-test/socket6')
client = Client('/tmp/key-mapper-test/socket6')
server = Server("/tmp/key-mapper-test/socket6")
client = Client("/tmp/key-mapper-test/socket6")
server.send(1)
ready = select.select([client], [], [], 0)[0][0]
@ -95,7 +95,7 @@ class TestSocket(unittest.TestCase):
self.assertEqual(ready, server)
def test_base_abstract(self):
self.assertRaises(NotImplementedError, lambda: Base('foo'))
self.assertRaises(NotImplementedError, lambda: Base("foo"))
self.assertRaises(NotImplementedError, lambda: Base.connect(None))
self.assertRaises(NotImplementedError, lambda: Base.reconnect(None))
self.assertRaises(NotImplementedError, lambda: Base.fileno(None))
@ -103,7 +103,7 @@ class TestSocket(unittest.TestCase):
class TestPipe(unittest.TestCase):
def test_pipe_single(self):
p1 = Pipe('/tmp/key-mapper-test/pipe')
p1 = Pipe(f"/tmp/key-mapper-test/pipe")
self.assertEqual(p1.recv(), None)
p1.send(1)
@ -123,8 +123,8 @@ class TestPipe(unittest.TestCase):
self.assertEqual(p1.recv(), None)
def test_pipe_duo(self):
p1 = Pipe('/tmp/key-mapper-test/pipe')
p2 = Pipe('/tmp/key-mapper-test/pipe')
p1 = Pipe(f"/tmp/key-mapper-test/pipe")
p2 = Pipe(f"/tmp/key-mapper-test/pipe")
self.assertEqual(p2.recv(), None)
p1.send(1)

@ -30,20 +30,20 @@ class TestKey(unittest.TestCase):
def test_key(self):
# its very similar to regular tuples, but with some extra stuff
key_1 = Key((1, 3, 1), (1, 5, 1))
self.assertEqual(str(key_1), 'Key((1, 3, 1), (1, 5, 1))')
self.assertEqual(str(key_1), "Key((1, 3, 1), (1, 5, 1))")
self.assertEqual(len(key_1), 2)
self.assertEqual(key_1[0], (1, 3, 1))
self.assertEqual(key_1[1], (1, 5, 1))
self.assertEqual(hash(key_1), hash(((1, 3, 1), (1, 5, 1))))
key_2 = Key((1, 3, 1))
self.assertEqual(str(key_2), 'Key((1, 3, 1),)')
self.assertEqual(str(key_2), "Key((1, 3, 1),)")
self.assertEqual(len(key_2), 1)
self.assertNotEqual(key_2, key_1)
self.assertNotEqual(hash(key_2), hash(key_1))
key_3 = Key(1, 3, 1)
self.assertEqual(str(key_3), 'Key((1, 3, 1),)')
self.assertEqual(str(key_3), "Key((1, 3, 1),)")
self.assertEqual(len(key_3), 1)
self.assertEqual(key_3, key_2)
self.assertEqual(key_3, (1, 3, 1))
@ -51,13 +51,13 @@ class TestKey(unittest.TestCase):
self.assertEqual(hash(key_3), hash((1, 3, 1)))
key_4 = Key(key_3)
self.assertEqual(str(key_4), 'Key((1, 3, 1),)')
self.assertEqual(str(key_4), "Key((1, 3, 1),)")
self.assertEqual(len(key_4), 1)
self.assertEqual(key_4, key_3)
self.assertEqual(hash(key_4), hash(key_3))
key_5 = Key(key_4, key_4, (1, 7, 1))
self.assertEqual(str(key_5), 'Key((1, 3, 1), (1, 3, 1), (1, 7, 1))')
self.assertEqual(str(key_5), "Key((1, 3, 1), (1, 3, 1), (1, 7, 1))")
self.assertEqual(len(key_5), 3)
self.assertNotEqual(key_5, key_4)
self.assertNotEqual(hash(key_5), hash(key_4))
@ -76,13 +76,9 @@ class TestKey(unittest.TestCase):
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))
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_is_problematic(self):
key_1 = Key((1, KEY_LEFTSHIFT, 1), (1, 5, 1))
@ -106,10 +102,10 @@ class TestKey(unittest.TestCase):
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")))
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

File diff suppressed because it is too large Load Diff

@ -24,8 +24,7 @@ import shutil
import unittest
import logging
from keymapper.logger import logger, add_filehandler, update_verbosity, \
log_info
from keymapper.logger import logger, add_filehandler, update_verbosity, log_info
from keymapper.paths import remove
from tests.test import tmp
@ -37,105 +36,106 @@ class TestLogger(unittest.TestCase):
# remove the file handler
logger.handlers = [
handler for handler in logger.handlers
handler
for handler in logger.handlers
if not isinstance(logger.handlers, logging.FileHandler)
]
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
remove(path)
def test_key_spam(self):
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
add_filehandler(path)
logger.key_spam(((1, 2, 1),), 'foo %s bar', 1234)
logger.key_spam(((1, 200, -1), (1, 5, 1)), 'foo %s', (1, 2))
with open(path, 'r') as f:
logger.key_spam(((1, 2, 1),), "foo %s bar", 1234)
logger.key_spam(((1, 200, -1), (1, 5, 1)), "foo %s", (1, 2))
with open(path, "r") as f:
content = f.read().lower()
self.assertIn('((1, 2, 1)) ------------------- foo 1234 bar', content)
self.assertIn('((1, 200, -1), (1, 5, 1)) ----- foo (1, 2)', content)
self.assertIn("((1, 2, 1)) ------------------- foo 1234 bar", content)
self.assertIn("((1, 200, -1), (1, 5, 1)) ----- foo (1, 2)", content)
def test_log_info(self):
update_verbosity(debug=False)
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
add_filehandler(path)
log_info()
with open(path, 'r') as f:
with open(path, "r") as f:
content = f.read().lower()
self.assertIn('key-mapper', content)
self.assertIn("key-mapper", content)
def test_makes_path(self):
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
if os.path.exists(path):
shutil.rmtree(path)
new_path = os.path.join(tmp, 'logger-test', 'a', 'b', 'c')
new_path = os.path.join(tmp, "logger-test", "a", "b", "c")
add_filehandler(new_path)
self.assertTrue(os.path.exists(new_path))
def test_clears_log(self):
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
os.makedirs(os.path.dirname(path), exist_ok=True)
os.mknod(path)
with open(path, 'w') as f:
f.write('foo')
add_filehandler(os.path.join(tmp, 'logger-test'))
with open(path, 'r') as f:
self.assertEqual(f.read(), '')
with open(path, "w") as f:
f.write("foo")
add_filehandler(os.path.join(tmp, "logger-test"))
with open(path, "r") as f:
self.assertEqual(f.read(), "")
def test_debug(self):
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
add_filehandler(path)
logger.error('abc')
logger.warning('foo')
logger.info('123')
logger.debug('456')
logger.spam('789')
with open(path, 'r') as f:
logger.error("abc")
logger.warning("foo")
logger.info("123")
logger.debug("456")
logger.spam("789")
with open(path, "r") as f:
content = f.read().lower()
self.assertIn('logger.py', content)
self.assertIn("logger.py", content)
self.assertIn('error', content)
self.assertIn('abc', content)
self.assertIn("error", content)
self.assertIn("abc", content)
self.assertIn('warn', content)
self.assertIn('foo', content)
self.assertIn("warn", content)
self.assertIn("foo", content)
self.assertIn('info', content)
self.assertIn('123', content)
self.assertIn("info", content)
self.assertIn("123", content)
self.assertIn('debug', content)
self.assertIn('456', content)
self.assertIn("debug", content)
self.assertIn("456", content)
self.assertIn('spam', content)
self.assertIn('789', content)
self.assertIn("spam", content)
self.assertIn("789", content)
def test_default(self):
path = os.path.join(tmp, 'logger-test')
path = os.path.join(tmp, "logger-test")
add_filehandler(path)
update_verbosity(debug=False)
logger.error('abc')
logger.warning('foo')
logger.info('123')
logger.debug('456')
logger.spam('789')
with open(path, 'r') as f:
logger.error("abc")
logger.warning("foo")
logger.info("123")
logger.debug("456")
logger.spam("789")
with open(path, "r") as f:
content = f.read().lower()
self.assertNotIn('logger.py', content)
self.assertNotIn('line', content)
self.assertNotIn("logger.py", content)
self.assertNotIn("line", content)
self.assertIn('error', content)
self.assertIn('abc', content)
self.assertIn("error", content)
self.assertIn("abc", content)
self.assertIn('warn', content)
self.assertIn('foo', content)
self.assertIn("warn", content)
self.assertIn("foo", content)
self.assertNotIn('info', content)
self.assertIn('123', content)
self.assertNotIn("info", content)
self.assertIn("123", content)
self.assertNotIn('debug', content)
self.assertNotIn('456', content)
self.assertNotIn("debug", content)
self.assertNotIn("456", content)
self.assertNotIn('spam', content)
self.assertNotIn('789', content)
self.assertNotIn("spam", content)
self.assertNotIn("789", content)
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

@ -25,8 +25,8 @@ import json
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A
from keymapper.mapping import Mapping
from keymapper.state import SystemMapping, XMODMAP_FILENAME
from keymapper.mapping import Mapping, split_key
from keymapper.system_mapping import SystemMapping, XMODMAP_FILENAME
from keymapper.config import config
from keymapper.paths import get_preset_path
from keymapper.key import Key
@ -38,18 +38,19 @@ class TestSystemMapping(unittest.TestCase):
def tearDown(self):
quick_cleanup()
def test_split_key(self):
self.assertEqual(split_key("1,2,3"), (1, 2, 3))
self.assertEqual(split_key("1,2"), (1, 2, 1))
self.assertIsNone(split_key("1"))
self.assertIsNone(split_key("1,a,2"))
self.assertIsNone(split_key("1,a"))
def test_update(self):
system_mapping = SystemMapping()
system_mapping.update({
'foo1': 101,
'bar1': 102
})
system_mapping.update({
'foo2': 201,
'bar2': 202
})
self.assertEqual(system_mapping.get('foo1'), 101)
self.assertEqual(system_mapping.get('bar2'), 202)
system_mapping.update({"foo1": 101, "bar1": 102})
system_mapping.update({"foo2": 201, "bar2": 202})
self.assertEqual(system_mapping.get("foo1"), 101)
self.assertEqual(system_mapping.get("bar2"), 202)
def test_xmodmap_file(self):
system_mapping = SystemMapping()
@ -58,73 +59,70 @@ class TestSystemMapping(unittest.TestCase):
system_mapping.populate()
self.assertTrue(os.path.exists(path))
with open(path, 'r') as file:
with open(path, "r") as file:
content = json.load(file)
self.assertEqual(content['a'], KEY_A)
self.assertEqual(content["a"], KEY_A)
# only xmodmap stuff should be present
self.assertNotIn('key_a', content)
self.assertNotIn('KEY_A', content)
self.assertNotIn('disable', content)
self.assertNotIn("key_a", content)
self.assertNotIn("KEY_A", content)
self.assertNotIn("disable", content)
def test_correct_case(self):
system_mapping = SystemMapping()
system_mapping.clear()
system_mapping._set('A', 31)
system_mapping._set('a', 32)
system_mapping._set('abcd_B', 33)
system_mapping._set("A", 31)
system_mapping._set("a", 32)
system_mapping._set("abcd_B", 33)
self.assertEqual(system_mapping.correct_case('a'), 'a')
self.assertEqual(system_mapping.correct_case('A'), 'A')
self.assertEqual(system_mapping.correct_case('ABCD_b'), 'abcd_B')
self.assertEqual(system_mapping.correct_case("a"), "a")
self.assertEqual(system_mapping.correct_case("A"), "A")
self.assertEqual(system_mapping.correct_case("ABCD_b"), "abcd_B")
# unknown stuff is returned as is
self.assertEqual(system_mapping.correct_case('FOo'), 'FOo')
self.assertEqual(system_mapping.correct_case("FOo"), "FOo")
self.assertEqual(system_mapping.get('A'), 31)
self.assertEqual(system_mapping.get('a'), 32)
self.assertEqual(system_mapping.get('ABCD_b'), 33)
self.assertEqual(system_mapping.get('abcd_B'), 33)
self.assertEqual(system_mapping.get("A"), 31)
self.assertEqual(system_mapping.get("a"), 32)
self.assertEqual(system_mapping.get("ABCD_b"), 33)
self.assertEqual(system_mapping.get("abcd_B"), 33)
def test_system_mapping(self):
system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100)
# this is case-insensitive
self.assertEqual(system_mapping.get('1'), 2)
self.assertEqual(system_mapping.get('KeY_1'), 2)
self.assertEqual(system_mapping.get("1"), 2)
self.assertEqual(system_mapping.get("KeY_1"), 2)
self.assertEqual(system_mapping.get('AlT_L'), 56)
self.assertEqual(system_mapping.get('KEy_LEFtALT'), 56)
self.assertEqual(system_mapping.get("AlT_L"), 56)
self.assertEqual(system_mapping.get("KEy_LEFtALT"), 56)
self.assertEqual(system_mapping.get('kEY_LeFTSHIFT'), 42)
self.assertEqual(system_mapping.get('ShiFt_L'), 42)
self.assertEqual(system_mapping.get("kEY_LeFTSHIFT"), 42)
self.assertEqual(system_mapping.get("ShiFt_L"), 42)
self.assertEqual(system_mapping.get('BTN_left'), 272)
self.assertEqual(system_mapping.get("BTN_left"), 272)
self.assertIsNotNone(system_mapping.get('KEY_KP4'))
self.assertEqual(
system_mapping.get('KP_Left'),
system_mapping.get('KEY_KP4')
)
self.assertIsNotNone(system_mapping.get("KEY_KP4"))
self.assertEqual(system_mapping.get("KP_Left"), system_mapping.get("KEY_KP4"))
# this only lists the correct casing,
# includes linux constants and xmodmap symbols
names = system_mapping.list_names()
self.assertIn('2', names)
self.assertIn('c', names)
self.assertIn('KEY_3', names)
self.assertNotIn('key_3', names)
self.assertIn('KP_Down', names)
self.assertNotIn('kp_down', names)
self.assertIn("2", names)
self.assertIn("c", names)
self.assertIn("KEY_3", names)
self.assertNotIn("key_3", names)
self.assertIn("KP_Down", names)
self.assertNotIn("kp_down", names)
names = system_mapping._mapping.keys()
self.assertIn('F4', names)
self.assertNotIn('f4', names)
self.assertIn('BTN_RIGHT', names)
self.assertNotIn('btn_right', names)
self.assertIn('KEY_KP7', names)
self.assertIn('KP_Home', names)
self.assertNotIn('kp_home', names)
self.assertIn("F4", names)
self.assertNotIn("f4", names)
self.assertIn("BTN_RIGHT", names)
self.assertNotIn("btn_right", names)
self.assertIn("KEY_KP7", names)
self.assertIn("KP_Home", names)
self.assertNotIn("kp_home", names)
self.assertEqual(system_mapping.get('disable'), -1)
self.assertEqual(system_mapping.get("disable"), -1)
class TestMapping(unittest.TestCase):
@ -136,65 +134,65 @@ class TestMapping(unittest.TestCase):
quick_cleanup()
def test_config(self):
self.mapping.save(get_preset_path('foo', 'bar2'))
self.mapping.save(get_preset_path("foo", "bar2"))
self.assertEqual(self.mapping.get('a'), None)
self.assertEqual(self.mapping.get("a"), None)
self.assertFalse(self.mapping.changed)
self.mapping.set('a', 1)
self.assertEqual(self.mapping.get('a'), 1)
self.mapping.set("a", 1)
self.assertEqual(self.mapping.get("a"), 1)
self.assertTrue(self.mapping.changed)
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")
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)
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(Key(EV_KEY, 81, 1), ' a ')
self.mapping.set('mapping.a', 2)
self.mapping.change(Key(EV_KEY, 81, 1), " a ")
self.mapping.set("mapping.a", 2)
self.assertEqual(self.mapping.num_saved_keys, 0)
self.mapping.save(get_preset_path('foo', 'bar'))
self.mapping.save(get_preset_path("foo", "bar"))
self.assertEqual(self.mapping.num_saved_keys, len(self.mapping))
self.assertFalse(self.mapping.changed)
self.mapping.load(get_preset_path('foo', 'bar'))
self.assertEqual(self.mapping.get_symbol(Key(EV_KEY, 81, 1)), 'a')
self.assertIsNone(self.mapping.get('mapping.a'))
self.mapping.load(get_preset_path("foo", "bar"))
self.assertEqual(self.mapping.get_symbol(Key(EV_KEY, 81, 1)), "a")
self.assertIsNone(self.mapping.get("mapping.a"))
self.assertFalse(self.mapping.changed)
# loading a different preset also removes the configs from memory
self.mapping.remove('a')
self.mapping.remove("a")
self.assertTrue(self.mapping.changed)
self.mapping.set('a.b.c', 6)
self.mapping.load(get_preset_path('foo', 'bar2'))
self.assertIsNone(self.mapping.get('a.b.c'))
self.mapping.set("a.b.c", 6)
self.mapping.load(get_preset_path("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)
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 = Key(EV_KEY, 1, 1)
ev_2 = Key(EV_KEY, 2, 0)
mapping1 = Mapping()
mapping1.change(ev_1, ' a')
mapping1.change(ev_1, " a")
mapping2 = mapping1.clone()
mapping1.change(ev_2, 'b ')
mapping1.change(ev_2, "b ")
self.assertEqual(mapping1.get_symbol(ev_1), 'a')
self.assertEqual(mapping1.get_symbol(ev_2), 'b')
self.assertEqual(mapping1.get_symbol(ev_1), "a")
self.assertEqual(mapping1.get_symbol(ev_2), "b")
self.assertEqual(mapping2.get_symbol(ev_1), 'a')
self.assertEqual(mapping2.get_symbol(ev_1), "a")
self.assertIsNone(mapping2.get_symbol(ev_2))
self.assertIsNone(mapping2.get_symbol(Key(EV_KEY, 2, 3)))
@ -205,56 +203,58 @@ class TestMapping(unittest.TestCase):
two = Key(EV_KEY, 11, 1)
three = Key(EV_KEY, 12, 1)
self.mapping.change(one, '1')
self.mapping.change(two, '2')
self.mapping.change(Key(two, three), '3')
self.mapping._config['foo'] = 'bar'
self.mapping.save(get_preset_path('Foo Device', 'test'))
self.mapping.change(one, "1")
self.mapping.change(two, "2")
self.mapping.change(Key(two, three), "3")
self.mapping._config["foo"] = "bar"
self.mapping.save(get_preset_path("Foo Device", "test"))
path = os.path.join(tmp, 'presets', 'Foo Device', 'test.json')
path = os.path.join(tmp, "presets", "Foo Device", "test.json")
self.assertTrue(os.path.exists(path))
loaded = Mapping()
self.assertEqual(len(loaded), 0)
loaded.load(get_preset_path('Foo Device', 'test'))
loaded.load(get_preset_path("Foo Device", "test"))
self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.get_symbol(one), '1')
self.assertEqual(loaded.get_symbol(two), '2')
self.assertEqual(loaded.get_symbol(Key(two, three)), '3')
self.assertEqual(loaded._config['foo'], 'bar')
self.assertEqual(loaded.get_symbol(one), "1")
self.assertEqual(loaded.get_symbol(two), "2")
self.assertEqual(loaded.get_symbol(Key(two, three)), "3")
self.assertEqual(loaded._config["foo"], "bar")
def test_save_load_2(self):
# loads mappings with only (type, code) as the key by using 1 as value,
# loads combinations chained with +
path = os.path.join(tmp, 'presets', 'Foo Device', 'test.json')
path = os.path.join(tmp, "presets", "Foo Device", "test.json")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as file:
json.dump({
'mapping': {
f'{EV_KEY},3': 'a',
f'{EV_ABS},{ABS_HAT0X},-1': 'b',
f'{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1': 'c',
# ignored because broken
f'3,1,1,2': 'e',
f'3': 'e',
f',,+3,1,2': 'g',
f'': 'h',
}
}, file)
with open(path, "w") as file:
json.dump(
{
"mapping": {
f"{EV_KEY},3": "a",
f"{EV_ABS},{ABS_HAT0X},-1": "b",
f"{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1": "c",
# ignored because broken
f"3,1,1,2": "e",
f"3": "e",
f",,+3,1,2": "g",
f"": "h",
}
},
file,
)
loaded = Mapping()
self.assertEqual(loaded.num_saved_keys, 0)
loaded.load(get_preset_path('Foo Device', 'test'))
loaded.load(get_preset_path("Foo Device", "test"))
self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.num_saved_keys, 3)
self.assertEqual(loaded.get_symbol(Key(EV_KEY, 3, 1)), 'a')
self.assertEqual(loaded.get_symbol(Key(EV_ABS, ABS_HAT0X, -1)), 'b')
self.assertEqual(loaded.get_symbol(Key(
(EV_ABS, 1, 1),
(EV_ABS, 2, -1),
Key(EV_ABS, 3, 1))
), 'c')
self.assertEqual(loaded.get_symbol(Key(EV_KEY, 3, 1)), "a")
self.assertEqual(loaded.get_symbol(Key(EV_ABS, ABS_HAT0X, -1)), "b")
self.assertEqual(
loaded.get_symbol(Key((EV_ABS, 1, 1), (EV_ABS, 2, -1), Key(EV_ABS, 3, 1))),
"c",
)
def test_change(self):
# the reader would not report values like 111 or 222, only 1 or -1.
@ -265,32 +265,32 @@ class TestMapping(unittest.TestCase):
ev_4 = Key(EV_ABS, 1, 111)
# 1 is not assigned yet, ignore it
self.mapping.change(ev_1, 'a', ev_2)
self.mapping.change(ev_1, "a", ev_2)
self.assertTrue(self.mapping.changed)
self.assertIsNone(self.mapping.get_symbol(ev_2))
self.assertEqual(self.mapping.get_symbol(ev_1), 'a')
self.assertEqual(self.mapping.get_symbol(ev_1), "a")
self.assertEqual(len(self.mapping), 1)
# change ev_1 to ev_3 and change a to b
self.mapping.change(ev_3, 'b', ev_1)
self.mapping.change(ev_3, "b", ev_1)
self.assertIsNone(self.mapping.get_symbol(ev_1))
self.assertEqual(self.mapping.get_symbol(ev_3), 'b')
self.assertEqual(self.mapping.get_symbol(ev_3), "b")
self.assertEqual(len(self.mapping), 1)
# add 4
self.mapping.change(ev_4, 'c', None)
self.assertEqual(self.mapping.get_symbol(ev_3), 'b')
self.assertEqual(self.mapping.get_symbol(ev_4), 'c')
self.mapping.change(ev_4, "c", None)
self.assertEqual(self.mapping.get_symbol(ev_3), "b")
self.assertEqual(self.mapping.get_symbol(ev_4), "c")
self.assertEqual(len(self.mapping), 2)
# change the mapping of 4 to d
self.mapping.change(ev_4, 'd', None)
self.assertEqual(self.mapping.get_symbol(ev_4), 'd')
self.mapping.change(ev_4, "d", None)
self.assertEqual(self.mapping.get_symbol(ev_4), "d")
self.assertEqual(len(self.mapping), 2)
# this also works in the same way
self.mapping.change(ev_4, 'e', ev_4)
self.assertEqual(self.mapping.get_symbol(ev_4), 'e')
self.mapping.change(ev_4, "e", ev_4)
self.assertEqual(self.mapping.get_symbol(ev_4), "e")
self.assertEqual(len(self.mapping), 2)
self.assertEqual(self.mapping.num_saved_keys, 0)
@ -304,23 +304,23 @@ class TestMapping(unittest.TestCase):
combi_2 = Key(ev_2, ev_1, ev_3)
combi_3 = Key(ev_1, ev_2, ev_4)
self.mapping.change(combi_1, 'a')
self.assertEqual(self.mapping.get_symbol(combi_1), 'a')
self.assertEqual(self.mapping.get_symbol(combi_2), 'a')
self.mapping.change(combi_1, "a")
self.assertEqual(self.mapping.get_symbol(combi_1), "a")
self.assertEqual(self.mapping.get_symbol(combi_2), "a")
# since combi_1 and combi_2 are equivalent, a changes to b
self.mapping.change(combi_2, 'b')
self.assertEqual(self.mapping.get_symbol(combi_1), 'b')
self.assertEqual(self.mapping.get_symbol(combi_2), 'b')
self.mapping.change(combi_2, "b")
self.assertEqual(self.mapping.get_symbol(combi_1), "b")
self.assertEqual(self.mapping.get_symbol(combi_2), "b")
self.mapping.change(combi_3, 'c')
self.assertEqual(self.mapping.get_symbol(combi_1), 'b')
self.assertEqual(self.mapping.get_symbol(combi_2), 'b')
self.assertEqual(self.mapping.get_symbol(combi_3), 'c')
self.mapping.change(combi_3, "c")
self.assertEqual(self.mapping.get_symbol(combi_1), "b")
self.assertEqual(self.mapping.get_symbol(combi_2), "b")
self.assertEqual(self.mapping.get_symbol(combi_3), "c")
self.mapping.change(combi_3, 'c', combi_1)
self.mapping.change(combi_3, "c", combi_1)
self.assertIsNone(self.mapping.get_symbol(combi_1))
self.assertIsNone(self.mapping.get_symbol(combi_2))
self.assertEqual(self.mapping.get_symbol(combi_3), 'c')
self.assertEqual(self.mapping.get_symbol(combi_3), "c")
def test_clear(self):
# does nothing
@ -333,45 +333,45 @@ class TestMapping(unittest.TestCase):
self.assertFalse(self.mapping.changed)
self.assertEqual(len(self.mapping), 0)
self.mapping._mapping[ev_1] = 'b'
self.mapping._mapping[ev_1] = "b"
self.assertEqual(len(self.mapping), 1)
self.mapping.clear(ev_1)
self.assertEqual(len(self.mapping), 0)
self.assertTrue(self.mapping.changed)
self.mapping.change(ev_4, 'KEY_KP1', None)
self.mapping.change(ev_4, "KEY_KP1", None)
self.assertTrue(self.mapping.changed)
self.mapping.change(ev_3, 'KEY_KP2', None)
self.mapping.change(ev_2, 'KEY_KP3', None)
self.mapping.change(ev_3, "KEY_KP2", None)
self.mapping.change(ev_2, "KEY_KP3", None)
self.assertEqual(len(self.mapping), 3)
self.mapping.clear(ev_3)
self.assertEqual(len(self.mapping), 2)
self.assertEqual(self.mapping.get_symbol(ev_4), 'KEY_KP1')
self.assertEqual(self.mapping.get_symbol(ev_4), "KEY_KP1")
self.assertIsNone(self.mapping.get_symbol(ev_3))
self.assertEqual(self.mapping.get_symbol(ev_2), 'KEY_KP3')
self.assertEqual(self.mapping.get_symbol(ev_2), "KEY_KP3")
def test_empty(self):
self.mapping.change(Key(EV_KEY, 10, 1), '1')
self.mapping.change(Key(EV_KEY, 11, 1), '2')
self.mapping.change(Key(EV_KEY, 12, 1), '3')
self.mapping.change(Key(EV_KEY, 10, 1), "1")
self.mapping.change(Key(EV_KEY, 11, 1), "2")
self.mapping.change(Key(EV_KEY, 12, 1), "3")
self.assertEqual(len(self.mapping), 3)
self.mapping.empty()
self.assertEqual(len(self.mapping), 0)
def test_dangerously_mapped_btn_left(self):
self.mapping.change(Key.btn_left(), '1')
self.mapping.change(Key.btn_left(), "1")
self.assertTrue(self.mapping.dangerously_mapped_btn_left())
self.mapping.change(Key(EV_KEY, 41, 1), '2')
self.mapping.change(Key(EV_KEY, 41, 1), "2")
self.assertTrue(self.mapping.dangerously_mapped_btn_left())
self.mapping.change(Key(EV_KEY, 42, 1), 'btn_left')
self.mapping.change(Key(EV_KEY, 42, 1), "btn_left")
self.assertFalse(self.mapping.dangerously_mapped_btn_left())
self.mapping.change(Key(EV_KEY, 42, 1), 'BTN_Left')
self.mapping.change(Key(EV_KEY, 42, 1), "BTN_Left")
self.assertFalse(self.mapping.dangerously_mapped_btn_left())
self.mapping.change(Key(EV_KEY, 42, 1), '3')
self.mapping.change(Key(EV_KEY, 42, 1), "3")
self.assertTrue(self.mapping.dangerously_mapped_btn_left())

@ -36,22 +36,23 @@ class TestPaths(unittest.TestCase):
quick_cleanup()
def test_touch(self):
touch('/tmp/a/b/c/d/e')
self.assertTrue(os.path.exists('/tmp/a/b/c/d/e'))
self.assertTrue(os.path.isfile('/tmp/a/b/c/d/e'))
self.assertRaises(ValueError, lambda: touch('/tmp/a/b/c/d/f/'))
touch("/tmp/a/b/c/d/e")
self.assertTrue(os.path.exists("/tmp/a/b/c/d/e"))
self.assertTrue(os.path.isfile("/tmp/a/b/c/d/e"))
self.assertRaises(ValueError, lambda: touch("/tmp/a/b/c/d/f/"))
def test_mkdir(self):
mkdir('/tmp/b/c/d/e')
self.assertTrue(os.path.exists('/tmp/b/c/d/e'))
self.assertTrue(os.path.isdir('/tmp/b/c/d/e'))
mkdir("/tmp/b/c/d/e")
self.assertTrue(os.path.exists("/tmp/b/c/d/e"))
self.assertTrue(os.path.isdir("/tmp/b/c/d/e"))
def test_get_preset_path(self):
self.assertEqual(get_preset_path(), os.path.join(tmp, 'presets'))
self.assertEqual(get_preset_path('a'), os.path.join(tmp, 'presets/a'))
self.assertEqual(get_preset_path('a', 'b'), os.path.join(tmp, 'presets/a/b.json'))
self.assertEqual(get_preset_path(), os.path.join(tmp, "presets"))
self.assertEqual(get_preset_path("a"), os.path.join(tmp, "presets/a"))
self.assertEqual(
get_preset_path("a", "b"), os.path.join(tmp, "presets/a/b.json")
)
def test_get_config_path(self):
self.assertEqual(get_config_path(), tmp)
self.assertEqual(get_config_path('a', 'b'), os.path.join(tmp, 'a/b'))
self.assertEqual(get_config_path("a", "b"), os.path.join(tmp, "a/b"))

@ -24,51 +24,64 @@ import unittest
import shutil
import time
from keymapper.presets import find_newest_preset, rename_preset, \
get_any_preset, delete_preset, get_available_preset_name, get_presets, \
migrate_path
from keymapper.presets import (
find_newest_preset,
rename_preset,
get_any_preset,
delete_preset,
get_available_preset_name,
get_presets,
migrate_path,
)
from keymapper.paths import CONFIG_PATH, get_preset_path, touch, mkdir
from keymapper.state import custom_mapping
from keymapper.gui.custom_mapping import custom_mapping
from tests.test import tmp
def create_preset(group_name, name='new preset'):
def create_preset(group_name, name="new preset"):
name = get_available_preset_name(group_name, name)
custom_mapping.empty()
custom_mapping.save(get_preset_path(group_name, name))
PRESETS = os.path.join(CONFIG_PATH, 'presets')
PRESETS = os.path.join(CONFIG_PATH, "presets")
class TestPresets(unittest.TestCase):
def test_get_available_preset_name(self):
# no filename conflict
self.assertEqual(get_available_preset_name('_', 'qux 2'), 'qux 2')
touch(get_preset_path('_', 'qux 5'))
self.assertEqual(get_available_preset_name('_', 'qux 5'), 'qux 6')
touch(get_preset_path('_', 'qux'))
self.assertEqual(get_available_preset_name('_', 'qux'), 'qux 2')
touch(get_preset_path('_', 'qux1'))
self.assertEqual(get_available_preset_name('_', 'qux1'), 'qux1 2')
touch(get_preset_path('_', 'qux 2 3'))
self.assertEqual(get_available_preset_name('_', 'qux 2 3'), 'qux 2 4')
touch(get_preset_path('_', 'qux 5'))
self.assertEqual(get_available_preset_name('_', 'qux 5', True), 'qux 5 copy')
touch(get_preset_path('_', 'qux 5 copy'))
self.assertEqual(get_available_preset_name('_', 'qux 5', True), 'qux 5 copy 2')
touch(get_preset_path('_', 'qux 5 copy 2'))
self.assertEqual(get_available_preset_name('_', 'qux 5', True), 'qux 5 copy 3')
touch(get_preset_path('_', 'qux 5copy'))
self.assertEqual(get_available_preset_name('_', 'qux 5copy', True), 'qux 5copy copy')
touch(get_preset_path('_', 'qux 5copy 2'))
self.assertEqual(get_available_preset_name('_', 'qux 5copy 2', True), 'qux 5copy 2 copy')
touch(get_preset_path('_', 'qux 5copy 2 copy'))
self.assertEqual(get_available_preset_name('_', 'qux 5copy 2 copy', True), 'qux 5copy 2 copy 2')
self.assertEqual(get_available_preset_name("_", "qux 2"), "qux 2")
touch(get_preset_path("_", "qux 5"))
self.assertEqual(get_available_preset_name("_", "qux 5"), "qux 6")
touch(get_preset_path("_", "qux"))
self.assertEqual(get_available_preset_name("_", "qux"), "qux 2")
touch(get_preset_path("_", "qux1"))
self.assertEqual(get_available_preset_name("_", "qux1"), "qux1 2")
touch(get_preset_path("_", "qux 2 3"))
self.assertEqual(get_available_preset_name("_", "qux 2 3"), "qux 2 4")
touch(get_preset_path("_", "qux 5"))
self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy")
touch(get_preset_path("_", "qux 5 copy"))
self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy 2")
touch(get_preset_path("_", "qux 5 copy 2"))
self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy 3")
touch(get_preset_path("_", "qux 5copy"))
self.assertEqual(
get_available_preset_name("_", "qux 5copy", True), "qux 5copy copy"
)
touch(get_preset_path("_", "qux 5copy 2"))
self.assertEqual(
get_available_preset_name("_", "qux 5copy 2", True), "qux 5copy 2 copy"
)
touch(get_preset_path("_", "qux 5copy 2 copy"))
self.assertEqual(
get_available_preset_name("_", "qux 5copy 2 copy", True),
"qux 5copy 2 copy 2",
)
class TestMigrate(unittest.TestCase):
@ -76,34 +89,42 @@ class TestMigrate(unittest.TestCase):
if os.path.exists(tmp):
shutil.rmtree(tmp)
touch(os.path.join(tmp, 'foo1', 'bar1.json'))
touch(os.path.join(tmp, 'foo2', 'bar2.json'))
touch(os.path.join(tmp, "foo1", "bar1.json"))
touch(os.path.join(tmp, "foo2", "bar2.json"))
migrate_path()
self.assertFalse(os.path.exists(os.path.join(tmp, 'foo1', 'bar1.json')))
self.assertFalse(os.path.exists(os.path.join(tmp, 'foo2', 'bar2.json')))
self.assertFalse(os.path.exists(os.path.join(tmp, "foo1", "bar1.json")))
self.assertFalse(os.path.exists(os.path.join(tmp, "foo2", "bar2.json")))
self.assertTrue(os.path.exists(os.path.join(tmp, 'presets', 'foo1', 'bar1.json')))
self.assertTrue(os.path.exists(os.path.join(tmp, 'presets', 'foo2', 'bar2.json')))
self.assertTrue(
os.path.exists(os.path.join(tmp, "presets", "foo1", "bar1.json"))
)
self.assertTrue(
os.path.exists(os.path.join(tmp, "presets", "foo2", "bar2.json"))
)
def test_doesnt_migrate(self):
if os.path.exists(tmp):
shutil.rmtree(tmp)
touch(os.path.join(tmp, 'foo1', 'bar1.json'))
touch(os.path.join(tmp, 'foo2', 'bar2.json'))
touch(os.path.join(tmp, "foo1", "bar1.json"))
touch(os.path.join(tmp, "foo2", "bar2.json"))
# already migrated
mkdir(os.path.join(tmp, 'presets'))
mkdir(os.path.join(tmp, "presets"))
migrate_path()
self.assertTrue(os.path.exists(os.path.join(tmp, 'foo1', 'bar1.json')))
self.assertTrue(os.path.exists(os.path.join(tmp, 'foo2', 'bar2.json')))
self.assertTrue(os.path.exists(os.path.join(tmp, "foo1", "bar1.json")))
self.assertTrue(os.path.exists(os.path.join(tmp, "foo2", "bar2.json")))
self.assertFalse(os.path.exists(os.path.join(tmp, 'presets', 'foo1', 'bar1.json')))
self.assertFalse(os.path.exists(os.path.join(tmp, 'presets', 'foo2', 'bar2.json')))
self.assertFalse(
os.path.exists(os.path.join(tmp, "presets", "foo1", "bar1.json"))
)
self.assertFalse(
os.path.exists(os.path.join(tmp, "presets", "foo2", "bar2.json"))
)
class TestCreatePreset(unittest.TestCase):
@ -112,24 +133,24 @@ class TestCreatePreset(unittest.TestCase):
shutil.rmtree(tmp)
def test_create_preset_1(self):
self.assertEqual(get_any_preset(), ('Foo Device', None))
create_preset('Foo Device')
self.assertEqual(get_any_preset(), ('Foo Device', 'new preset'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/new preset.json'))
self.assertEqual(get_any_preset(), ("Foo Device", None))
create_preset("Foo Device")
self.assertEqual(get_any_preset(), ("Foo Device", "new preset"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
def test_create_preset_2(self):
create_preset('Foo Device')
create_preset('Foo Device')
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/new preset.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/new preset 2.json'))
create_preset("Foo Device")
create_preset("Foo Device")
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset 2.json"))
def test_create_preset_3(self):
create_preset('Foo Device', 'pre set')
create_preset('Foo Device', 'pre set')
create_preset('Foo Device', 'pre set')
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/pre set.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/pre set 2.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/pre set 3.json'))
create_preset("Foo Device", "pre set")
create_preset("Foo Device", "pre set")
create_preset("Foo Device", "pre set")
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set 2.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set 3.json"))
class TestDeletePreset(unittest.TestCase):
@ -138,17 +159,17 @@ class TestDeletePreset(unittest.TestCase):
shutil.rmtree(tmp)
def test_delete_preset(self):
create_preset('Foo Device')
create_preset('Foo Device')
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/new preset.json'))
delete_preset('Foo Device', 'new preset')
self.assertFalse(os.path.exists(f'{PRESETS}/Foo Device/new preset.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device'))
delete_preset('Foo Device', 'new preset 2')
self.assertFalse(os.path.exists(f'{PRESETS}/Foo Device/new preset.json'))
self.assertFalse(os.path.exists(f'{PRESETS}/Foo Device/new preset 2.json'))
create_preset("Foo Device")
create_preset("Foo Device")
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
delete_preset("Foo Device", "new preset")
self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device"))
delete_preset("Foo Device", "new preset 2")
self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset 2.json"))
# if no preset in the directory, remove the directory
self.assertFalse(os.path.exists(f'{PRESETS}/Foo Device'))
self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device"))
class TestRenamePreset(unittest.TestCase):
@ -157,15 +178,15 @@ class TestRenamePreset(unittest.TestCase):
shutil.rmtree(tmp)
def test_rename_preset(self):
create_preset('Foo Device', 'preset 1')
create_preset('Foo Device', 'preset 2')
create_preset('Foo Device', 'foobar')
rename_preset('Foo Device', 'preset 1', 'foobar')
rename_preset('Foo Device', 'preset 2', 'foobar')
self.assertFalse(os.path.exists(f'{PRESETS}/Foo Device/preset 1.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/foobar.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/foobar 2.json'))
self.assertTrue(os.path.exists(f'{PRESETS}/Foo Device/foobar 3.json'))
create_preset("Foo Device", "preset 1")
create_preset("Foo Device", "preset 2")
create_preset("Foo Device", "foobar")
rename_preset("Foo Device", "preset 1", "foobar")
rename_preset("Foo Device", "preset 2", "foobar")
self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/preset 1.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar 2.json"))
self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar 3.json"))
class TestFindPresets(unittest.TestCase):
@ -174,67 +195,64 @@ class TestFindPresets(unittest.TestCase):
shutil.rmtree(tmp)
def test_get_presets(self):
os.makedirs(os.path.join(PRESETS, '1234'))
os.makedirs(os.path.join(PRESETS, "1234"))
os.mknod(os.path.join(PRESETS, '1234', 'picture.png'))
self.assertEqual(len(get_presets('1234')), 0)
os.mknod(os.path.join(PRESETS, "1234", "picture.png"))
self.assertEqual(len(get_presets("1234")), 0)
os.mknod(os.path.join(PRESETS, '1234', 'foo bar 1.json'))
os.mknod(os.path.join(PRESETS, "1234", "foo bar 1.json"))
time.sleep(0.01)
os.mknod(os.path.join(PRESETS, '1234', 'foo bar 2.json'))
os.mknod(os.path.join(PRESETS, "1234", "foo bar 2.json"))
# the newest to the front
self.assertListEqual(get_presets('1234'), ['foo bar 2', 'foo bar 1'])
self.assertListEqual(get_presets("1234"), ["foo bar 2", "foo bar 1"])
def test_find_newest_preset_1(self):
create_preset('Foo Device', 'preset 1')
create_preset("Foo Device", "preset 1")
time.sleep(0.01)
create_preset('Bar Device', 'preset 2')
create_preset("Bar Device", "preset 2")
# not a preset, ignore
time.sleep(0.01)
path = os.path.join(PRESETS, 'Bar Device', 'picture.png')
path = os.path.join(PRESETS, "Bar Device", "picture.png")
os.mknod(path)
self.assertEqual(find_newest_preset(), ('Bar Device', 'preset 2'))
self.assertEqual(find_newest_preset(), ("Bar Device", "preset 2"))
def test_find_newest_preset_2(self):
os.makedirs(f'{PRESETS}/Foo Device')
os.makedirs(f"{PRESETS}/Foo Device")
time.sleep(0.01)
os.makedirs(f'{PRESETS}/device_2')
os.makedirs(f"{PRESETS}/device_2")
# takes the first one that the test-fake returns
self.assertEqual(find_newest_preset(), ('Foo Device', None))
self.assertEqual(find_newest_preset(), ("Foo Device", None))
def test_find_newest_preset_3(self):
os.makedirs(f'{PRESETS}/Foo Device')
self.assertEqual(find_newest_preset(), ('Foo Device', None))
os.makedirs(f"{PRESETS}/Foo Device")
self.assertEqual(find_newest_preset(), ("Foo Device", None))
def test_find_newest_preset_4(self):
create_preset('Foo Device', 'preset 1')
self.assertEqual(find_newest_preset(), ('Foo Device', 'preset 1'))
create_preset("Foo Device", "preset 1")
self.assertEqual(find_newest_preset(), ("Foo Device", "preset 1"))
def test_find_newest_preset_5(self):
create_preset('Foo Device', 'preset 1')
create_preset("Foo Device", "preset 1")
time.sleep(0.01)
create_preset('unknown device 3', 'preset 3')
self.assertEqual(find_newest_preset(), ('Foo Device', 'preset 1'))
create_preset("unknown device 3", "preset 3")
self.assertEqual(find_newest_preset(), ("Foo Device", "preset 1"))
def test_find_newest_preset_6(self):
# takes the first one that the test-fake returns
self.assertEqual(find_newest_preset(), ('Foo Device', None))
self.assertEqual(find_newest_preset(), ("Foo Device", None))
def test_find_newest_preset_7(self):
self.assertEqual(find_newest_preset('Foo Device'), ('Foo Device', None))
self.assertEqual(find_newest_preset("Foo Device"), ("Foo Device", None))
def test_find_newest_preset_8(self):
create_preset('Foo Device', 'preset 1')
create_preset("Foo Device", "preset 1")
time.sleep(0.01)
create_preset('Foo Device', 'preset 3')
create_preset("Foo Device", "preset 3")
time.sleep(0.01)
create_preset('Bar Device', 'preset 2')
self.assertEqual(
find_newest_preset('Foo Device'),
('Foo Device', 'preset 3')
)
create_preset("Bar Device", "preset 2")
self.assertEqual(find_newest_preset("Foo Device"), ("Foo Device", "preset 3"))
if __name__ == "__main__":

@ -24,19 +24,38 @@ from unittest import mock
import time
import multiprocessing
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_COMMA, \
BTN_TOOL_DOUBLETAP, ABS_Z, ABS_Y, KEY_A, \
EV_REL, REL_WHEEL, REL_X, ABS_X, ABS_RZ
from evdev.ecodes import (
EV_KEY,
EV_ABS,
ABS_HAT0X,
KEY_COMMA,
BTN_TOOL_DOUBLETAP,
ABS_Z,
ABS_Y,
KEY_A,
EV_REL,
REL_WHEEL,
REL_X,
ABS_X,
ABS_RZ,
)
from keymapper.gui.reader import reader, will_report_up
from keymapper.state import custom_mapping
from keymapper.gui.custom_mapping import custom_mapping
from keymapper.config import BUTTONS, MOUSE
from keymapper.key import Key
from keymapper.gui.helper import RootHelper
from keymapper.groups import groups
from tests.test import new_event, push_events, send_event_to_reader, \
EVENT_READ_TIMEOUT, START_READING_DELAY, quick_cleanup, MAX_ABS
from tests.test import (
new_event,
push_events,
send_event_to_reader,
EVENT_READ_TIMEOUT,
START_READING_DELAY,
quick_cleanup,
MAX_ABS,
)
CODE_1 = 100
@ -83,10 +102,12 @@ class TestReader(unittest.TestCase):
def test_reading_1(self):
# a single event
push_events('Foo Device 2', [new_event(EV_ABS, ABS_HAT0X, 1)])
push_events('Foo Device 2', [new_event(EV_ABS, REL_X, 1)]) # mouse movements are ignored
push_events("Foo Device 2", [new_event(EV_ABS, ABS_HAT0X, 1)])
push_events(
"Foo Device 2", [new_event(EV_ABS, REL_X, 1)]
) # mouse movements are ignored
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(0.2)
self.assertEqual(reader.read(), (EV_ABS, ABS_HAT0X, 1))
self.assertEqual(reader.read(), None)
@ -95,7 +116,7 @@ class TestReader(unittest.TestCase):
def test_reading_wheel(self):
# will be treated as released automatically at some point
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
send_event_to_reader(new_event(EV_REL, REL_WHEEL, 0))
self.assertIsNone(reader.read())
@ -145,7 +166,7 @@ class TestReader(unittest.TestCase):
while len(reader._unreleased) == 2:
read = reader.read()
if i == 100:
raise AssertionError('Did not release the wheel')
raise AssertionError("Did not release the wheel")
i += 1
# and only the comma remains. However, a changed combination is
# only returned when a new key is pressed. Only then the pressed
@ -174,7 +195,7 @@ class TestReader(unittest.TestCase):
self.assertEqual(reader.read(), None)
self.create_helper()
self.assertEqual(reader.read(), None)
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
self.assertEqual(reader.read(), None)
send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1))
@ -191,21 +212,29 @@ class TestReader(unittest.TestCase):
self.assertEqual(reader.read(), None)
def test_change_device(self):
push_events('Foo Device 2', [
new_event(EV_KEY, 1, 1),
] * 100)
push_events('Bar Device', [
new_event(EV_KEY, 2, 1),
] * 100)
push_events(
"Foo Device 2",
[
new_event(EV_KEY, 1, 1),
]
* 100,
)
push_events(
"Bar Device",
[
new_event(EV_KEY, 2, 1),
]
* 100,
)
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(0.1)
self.assertEqual(reader.read(), Key(EV_KEY, 1, 1))
reader.start_reading(groups.find(name='Bar Device'))
reader.start_reading(groups.find(name="Bar Device"))
# it's plausible that right after sending the new read command more
# events from the old device might still appear. Give the helper
@ -218,23 +247,26 @@ class TestReader(unittest.TestCase):
def test_reading_2(self):
# a combination of events
push_events('Foo Device 2', [
new_event(EV_KEY, CODE_1, 1, 10000.1234),
new_event(EV_KEY, CODE_3, 1, 10001.1234),
new_event(EV_ABS, ABS_HAT0X, -1, 10002.1234)
])
push_events(
"Foo Device 2",
[
new_event(EV_KEY, CODE_1, 1, 10000.1234),
new_event(EV_KEY, CODE_3, 1, 10001.1234),
new_event(EV_ABS, ABS_HAT0X, -1, 10002.1234),
],
)
pipe = multiprocessing.Pipe()
def refresh():
# from within the helper process notify this test that
# refresh was called as expected
pipe[1].send('refreshed')
pipe[1].send("refreshed")
with mock.patch.object(groups, 'refresh', refresh):
with mock.patch.object(groups, "refresh", refresh):
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
# sending anything arbitrary does not stop the helper
reader._commands.send(856794)
@ -242,39 +274,32 @@ class TestReader(unittest.TestCase):
# but it makes it look for new devices because maybe its list of
# groups is not up-to-date
self.assertTrue(pipe[0].poll())
self.assertEqual(pipe[0].recv(), 'refreshed')
self.assertEqual(pipe[0].recv(), "refreshed")
self.assertEqual(reader.read(), (
(EV_KEY, CODE_1, 1),
(EV_KEY, CODE_3, 1),
(EV_ABS, ABS_HAT0X, -1)
))
self.assertEqual(
reader.read(),
((EV_KEY, CODE_1, 1), (EV_KEY, CODE_3, 1), (EV_ABS, ABS_HAT0X, -1)),
)
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 3)
def test_reading_3(self):
self.create_helper()
# a combination of events via Socket with reads inbetween
reader.start_reading(groups.find(name='gamepad'))
reader.start_reading(groups.find(name="gamepad"))
send_event_to_reader(new_event(EV_KEY, CODE_1, 1, 1001))
self.assertEqual(reader.read(), (
(EV_KEY, CODE_1, 1)
))
self.assertEqual(reader.read(), ((EV_KEY, CODE_1, 1)))
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
custom_mapping.set("gamepad.joystick.left_purpose", BUTTONS)
send_event_to_reader(new_event(EV_ABS, ABS_Y, 1, 1002))
self.assertEqual(reader.read(), (
(EV_KEY, CODE_1, 1),
(EV_ABS, ABS_Y, 1)
))
self.assertEqual(reader.read(), ((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1)))
send_event_to_reader(new_event(EV_ABS, ABS_HAT0X, -1, 1003))
self.assertEqual(reader.read(), (
(EV_KEY, CODE_1, 1),
(EV_ABS, ABS_Y, 1),
(EV_ABS, ABS_HAT0X, -1)
))
self.assertEqual(
reader.read(),
((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1), (EV_ABS, ABS_HAT0X, -1)),
)
# adding duplicate down events won't report a different combination.
# import for triggers, as they keep reporting more down-events before
@ -296,35 +321,36 @@ class TestReader(unittest.TestCase):
def test_reads_joysticks(self):
# if their purpose is "buttons"
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
push_events('gamepad', [
new_event(EV_ABS, ABS_Y, MAX_ABS),
# the value of that one is interpreted as release, because
# it is too small
new_event(EV_ABS, ABS_X, MAX_ABS // 10)
])
custom_mapping.set("gamepad.joystick.left_purpose", BUTTONS)
push_events(
"gamepad",
[
new_event(EV_ABS, ABS_Y, MAX_ABS),
# the value of that one is interpreted as release, because
# it is too small
new_event(EV_ABS, ABS_X, MAX_ABS // 10),
],
)
self.create_helper()
reader.start_reading(groups.find(name='gamepad'))
reader.start_reading(groups.find(name="gamepad"))
time.sleep(0.2)
self.assertEqual(reader.read(), (EV_ABS, ABS_Y, 1))
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 1)
reader._unreleased = {}
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
push_events('gamepad', [
new_event(EV_ABS, ABS_Y, MAX_ABS)
])
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
push_events("gamepad", [new_event(EV_ABS, ABS_Y, MAX_ABS)])
self.create_helper()
reader.start_reading(groups.find(name='gamepad'))
reader.start_reading(groups.find(name="gamepad"))
time.sleep(0.1)
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 0)
def test_combine_triggers(self):
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
i = 0
@ -354,13 +380,16 @@ class TestReader(unittest.TestCase):
self.assertEqual(reader.read(), None)
def test_blacklisted_events(self):
push_events('Foo Device 2', [
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
])
push_events(
"Foo Device 2",
[
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
],
)
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(0.1)
self.assertEqual(reader.read(), (EV_KEY, CODE_2, 1))
self.assertEqual(reader.read(), None)
@ -368,25 +397,28 @@ class TestReader(unittest.TestCase):
def test_ignore_value_2(self):
# this is not a combination, because (EV_KEY CODE_3, 2) is ignored
push_events('Foo Device 2', [
new_event(EV_ABS, ABS_HAT0X, 1),
new_event(EV_KEY, CODE_3, 2)
])
push_events(
"Foo Device 2",
[new_event(EV_ABS, ABS_HAT0X, 1), new_event(EV_KEY, CODE_3, 2)],
)
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(0.2)
self.assertEqual(reader.read(), (EV_ABS, ABS_HAT0X, 1))
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 1)
def test_reading_ignore_up(self):
push_events('Foo Device 2', [
new_event(EV_KEY, CODE_1, 0, 10),
new_event(EV_KEY, CODE_2, 1, 11),
new_event(EV_KEY, CODE_3, 0, 12),
])
push_events(
"Foo Device 2",
[
new_event(EV_KEY, CODE_1, 0, 10),
new_event(EV_KEY, CODE_2, 1, 11),
new_event(EV_KEY, CODE_3, 0, 12),
],
)
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(0.1)
self.assertEqual(reader.read(), (EV_KEY, CODE_2, 1))
self.assertEqual(reader.read(), None)
@ -412,13 +444,16 @@ class TestReader(unittest.TestCase):
self.assertIsNone(reader.get_unreleased_keys())
def test_wrong_device(self):
push_events('Foo Device 2', [
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1)
])
push_events(
"Foo Device 2",
[
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1),
],
)
self.create_helper()
reader.start_reading(groups.find(name='Bar Device'))
reader.start_reading(groups.find(name="Bar Device"))
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 0)
@ -428,26 +463,33 @@ class TestReader(unittest.TestCase):
# representative for the original key. As long as this is not
# intentionally programmed it won't even do that. But it was at some
# point.
push_events('key-mapper Bar Device', [
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1)
])
push_events(
"key-mapper Bar Device",
[
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1),
],
)
self.create_helper()
reader.start_reading(groups.find(name='Bar Device'))
reader.start_reading(groups.find(name="Bar Device"))
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(reader.read(), None)
self.assertEqual(len(reader._unreleased), 0)
def test_clear(self):
push_events('Foo Device 2', [
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1)
] * 15)
push_events(
"Foo Device 2",
[
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1),
]
* 15,
)
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT * 3)
reader.read()
@ -469,18 +511,18 @@ class TestReader(unittest.TestCase):
self.tearDown()
def test_switch_device(self):
push_events('Bar Device', [new_event(EV_KEY, CODE_1, 1)])
push_events('Foo Device 2', [new_event(EV_KEY, CODE_3, 1)])
push_events("Bar Device", [new_event(EV_KEY, CODE_1, 1)])
push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
self.create_helper()
reader.start_reading(groups.find(name='Bar Device'))
reader.start_reading(groups.find(name="Bar Device"))
self.assertFalse(reader._results.poll())
self.assertEqual(reader.group.name, 'Bar Device')
self.assertEqual(reader.group.name, "Bar Device")
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertTrue(reader._results.poll())
reader.start_reading(groups.find(key='Foo Device 2'))
self.assertEqual(reader.group.name, 'Foo Device')
reader.start_reading(groups.find(key="Foo Device 2"))
self.assertEqual(reader.group.name, "Foo Device")
self.assertFalse(reader._results.poll()) # pipe resets
time.sleep(EVENT_READ_TIMEOUT * 5)
@ -492,9 +534,9 @@ class TestReader(unittest.TestCase):
def test_terminate(self):
self.create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
push_events('Foo Device 2', [new_event(EV_KEY, CODE_3, 1)])
push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT)
self.assertTrue(reader._results.poll())
@ -503,7 +545,7 @@ class TestReader(unittest.TestCase):
time.sleep(EVENT_READ_TIMEOUT)
# no new events arrive after terminating
push_events('Foo Device 2', [new_event(EV_KEY, CODE_3, 1)])
push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
time.sleep(EVENT_READ_TIMEOUT * 3)
self.assertFalse(reader._results.poll())
@ -521,19 +563,13 @@ class TestReader(unittest.TestCase):
self.assertFalse(reader.are_new_devices_available())
# send the same devices again
reader._get_event({
'type': 'groups',
'message': groups.dumps()
})
reader._get_event({"type": "groups", "message": groups.dumps()})
self.assertFalse(reader.are_new_devices_available())
# send changed devices
message = groups.dumps()
message = message.replace('Foo Device', 'foo_device')
reader._get_event({
'type': 'groups',
'message': message
})
message = message.replace("Foo Device", "foo_device")
reader._get_event({"type": "groups", "message": message})
self.assertTrue(reader.are_new_devices_available())
self.assertFalse(reader.are_new_devices_available())

@ -31,19 +31,27 @@ from keymapper.groups import groups
from keymapper.gui.reader import reader
from keymapper.gui.helper import RootHelper
from tests.test import InputDevice, quick_cleanup, cleanup, fixtures,\
new_event, push_events, EVENT_READ_TIMEOUT, START_READING_DELAY
from tests.test import (
InputDevice,
quick_cleanup,
cleanup,
fixtures,
new_event,
push_events,
EVENT_READ_TIMEOUT,
START_READING_DELAY,
)
class TestTest(unittest.TestCase):
def test_stubs(self):
self.assertIsNotNone(groups.find(key='Foo Device 2'))
self.assertIsNotNone(groups.find(key="Foo Device 2"))
def tearDown(self):
quick_cleanup()
def test_fake_capabilities(self):
device = InputDevice('/dev/input/event30')
device = InputDevice("/dev/input/event30")
capabilities = device.capabilities(absinfo=False)
self.assertIsInstance(capabilities, dict)
self.assertIsInstance(capabilities[EV_ABS], list)
@ -62,18 +70,18 @@ class TestTest(unittest.TestCase):
def test_restore_fixtures(self):
fixtures[1] = [1234]
del fixtures['/dev/input/event11']
del fixtures["/dev/input/event11"]
cleanup()
self.assertIsNone(fixtures.get(1))
self.assertIsNotNone(fixtures.get('/dev/input/event11'))
self.assertIsNotNone(fixtures.get("/dev/input/event11"))
def test_restore_os_environ(self):
os.environ['foo'] = 'bar'
del os.environ['USER']
os.environ["foo"] = "bar"
del os.environ["USER"]
environ = os.environ
cleanup()
self.assertIn('USER', environ)
self.assertNotIn('foo', environ)
self.assertIn("USER", environ)
self.assertNotIn("foo", environ)
def test_push_events(self):
"""Test that push_event works properly between helper and reader.
@ -81,6 +89,7 @@ class TestTest(unittest.TestCase):
Using push_events after the helper is already forked should work,
as well as using push_event twice
"""
def create_helper():
# this will cause pending events to be copied over to the helper
# process
@ -101,10 +110,10 @@ class TestTest(unittest.TestCase):
event = new_event(EV_KEY, 102, 1)
create_helper()
reader.start_reading(groups.find(key='Foo Device 2'))
reader.start_reading(groups.find(key="Foo Device 2"))
time.sleep(START_READING_DELAY)
push_events('Foo Device 2', [event])
push_events("Foo Device 2", [event])
wait_for_results()
self.assertTrue(reader._results.poll())
@ -113,7 +122,7 @@ class TestTest(unittest.TestCase):
# can push more events to the helper that is inside a separate
# process, which end up being sent to the reader
push_events('Foo Device 2', [event])
push_events("Foo Device 2", [event])
wait_for_results()
self.assertTrue(reader._results.poll())

@ -23,7 +23,7 @@ import os
import unittest
from unittest import mock
from keymapper.user import get_user
from keymapper.user import get_user, get_home
from tests.test import quick_cleanup
@ -37,18 +37,28 @@ class TestUser(unittest.TestCase):
quick_cleanup()
def test_get_user(self):
with mock.patch('os.getlogin', lambda: 'foo'):
self.assertEqual(get_user(), 'foo')
with mock.patch("os.getlogin", lambda: "foo"):
self.assertEqual(get_user(), "foo")
with mock.patch('os.getlogin', lambda: 'root'):
self.assertEqual(get_user(), 'root')
with mock.patch("os.getlogin", lambda: "root"):
self.assertEqual(get_user(), "root")
with mock.patch('os.getlogin', lambda: _raise(OSError())):
os.environ['USER'] = 'root'
os.environ['SUDO_USER'] = 'qux'
self.assertEqual(get_user(), 'qux')
property_mock = mock.Mock()
property_mock.configure_mock(pw_name="quix")
with mock.patch("os.getlogin", lambda: _raise(OSError())), mock.patch(
"pwd.getpwuid", return_value=property_mock
):
os.environ["USER"] = "root"
os.environ["SUDO_USER"] = "qux"
self.assertEqual(get_user(), "qux")
os.environ['USER'] = 'root'
del os.environ['SUDO_USER']
os.environ['PKEXEC_UID'] = '1000'
self.assertNotEqual(get_user(), 'root')
os.environ["USER"] = "root"
del os.environ["SUDO_USER"]
os.environ["PKEXEC_UID"] = "1000"
self.assertNotEqual(get_user(), "root")
def test_get_home(self):
property_mock = mock.Mock()
property_mock.configure_mock(pw_dir="/custom/home/foo")
with mock.patch("pwd.getpwnam", return_value=property_mock):
self.assertEqual(get_home("foo"), "/custom/home/foo")

@ -1,108 +1,108 @@
xmodmap = (
b'keycode 8 =\nkeycode 9 = Escape NoSymbol Escape\nkeycode 10 = 1 exclam 1 exclam onesuperior exclamdown ones'
b'uperior\nkeycode 11 = 2 quotedbl 2 quotedbl twosuperior oneeighth twosuperior\nkeycode 12 = 3 section 3 sectio'
b'n threesuperior sterling threesuperior\nkeycode 13 = 4 dollar 4 dollar onequarter currency onequarter\nkeycode '
b' 14 = 5 percent 5 percent onehalf threeeighths onehalf\nkeycode 15 = 6 ampersand 6 ampersand notsign fiveeighth'
b's notsign\nkeycode 16 = 7 slash 7 slash braceleft seveneighths braceleft\nkeycode 17 = 8 parenleft 8 parenleft'
b' bracketleft trademark bracketleft\nkeycode 18 = 9 parenright 9 parenright bracketright plusminus bracketright'
b'\nkeycode 19 = 0 equal 0 equal braceright degree braceright\nkeycode 20 = ssharp question ssharp question back'
b'slash questiondown U1E9E\nkeycode 21 = dead_acute dead_grave dead_acute dead_grave dead_cedilla dead_ogonek dea'
b'd_cedilla\nkeycode 22 = BackSpace BackSpace BackSpace BackSpace\nkeycode 23 = Tab ISO_Left_Tab Tab ISO_Left_Ta'
b'b\nkeycode 24 = q Q q Q at Greek_OMEGA at\nkeycode 25 = w W w W lstroke Lstroke lstroke\nkeycode 26 = e E e E'
b' EuroSign EuroSign EuroSign\nkeycode 27 = r R r R paragraph registered paragraph\nkeycode 28 = t T t T tslash '
b'Tslash tslash\nkeycode 29 = z Z z Z leftarrow yen leftarrow\nkeycode 30 = u U u U downarrow uparrow downarrow'
b'\nkeycode 31 = i I i I rightarrow idotless rightarrow\nkeycode 32 = o O o O oslash Oslash oslash\nkeycode 33 '
b'= p P p P thorn THORN thorn\nkeycode 34 = udiaeresis Udiaeresis udiaeresis Udiaeresis dead_diaeresis dead_above'
b'ring dead_diaeresis\nkeycode 35 = plus asterisk plus asterisk asciitilde macron asciitilde\nkeycode 36 = Retur'
b'n NoSymbol Return\nkeycode 37 = Control_L NoSymbol Control_L\nkeycode 38 = a A a A ae AE ae\nkeycode 39 = s S'
b' s S U017F U1E9E U017F\nkeycode 40 = d D d D eth ETH eth\nkeycode 41 = f F f F dstroke ordfeminine dstroke\nke'
b'ycode 42 = g G g G eng ENG eng\nkeycode 43 = h H h H hstroke Hstroke hstroke\nkeycode 44 = j J j J dead_below'
b'dot dead_abovedot dead_belowdot\nkeycode 45 = k K k K kra ampersand kra\nkeycode 46 = l L l L lstroke Lstroke '
b'lstroke\nkeycode 47 = odiaeresis Odiaeresis odiaeresis Odiaeresis dead_doubleacute dead_belowdot dead_doubleacu'
b'te\nkeycode 48 = adiaeresis Adiaeresis adiaeresis Adiaeresis dead_circumflex dead_caron dead_circumflex\nkeycod'
b'e 49 = dead_circumflex degree dead_circumflex degree U2032 U2033 U2032\nkeycode 50 = Shift_L NoSymbol Shift_L'
b'\nkeycode 51 = numbersign apostrophe numbersign apostrophe rightsinglequotemark dead_breve rightsinglequotemark'
b'\nkeycode 52 = y Y y Y guillemotright U203A guillemotright\nkeycode 53 = x X x X guillemotleft U2039 guillemot'
b'left\nkeycode 54 = c C c C cent copyright cent\nkeycode 55 = v V v V doublelowquotemark singlelowquotemark dou'
b'blelowquotemark\nkeycode 56 = b B b B leftdoublequotemark leftsinglequotemark leftdoublequotemark\nkeycode 57 '
b'= n N n N rightdoublequotemark rightsinglequotemark rightdoublequotemark\nkeycode 58 = m M m M mu masculine mu'
b'\nkeycode 59 = comma semicolon comma semicolon periodcentered multiply periodcentered\nkeycode 60 = period col'
b'on period colon U2026 division U2026\nkeycode 61 = minus underscore minus underscore endash emdash endash\nkeyc'
b'ode 62 = Shift_R NoSymbol Shift_R\nkeycode 63 = KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP'
b'_Multiply XF86ClearGrab\nkeycode 64 = Alt_L Meta_L Alt_L Meta_L\nkeycode 65 = space NoSymbol space\nkeycode 6'
b'6 = Caps_Lock NoSymbol Caps_Lock\nkeycode 67 = F1 F1 F1 F1 F1 F1 XF86Switch_VT_1\nkeycode 68 = F2 F2 F2 F2 F2 '
b'F2 XF86Switch_VT_2\nkeycode 69 = F3 F3 F3 F3 F3 F3 XF86Switch_VT_3\nkeycode 70 = F4 F4 F4 F4 F4 F4 XF86Switch_'
b'VT_4\nkeycode 71 = F5 F5 F5 F5 F5 F5 XF86Switch_VT_5\nkeycode 72 = F6 F6 F6 F6 F6 F6 XF86Switch_VT_6\nkeycode '
b' 73 = F7 F7 F7 F7 F7 F7 XF86Switch_VT_7\nkeycode 74 = F8 F8 F8 F8 F8 F8 XF86Switch_VT_8\nkeycode 75 = F9 F9 F9'
b' F9 F9 F9 XF86Switch_VT_9\nkeycode 76 = F10 F10 F10 F10 F10 F10 XF86Switch_VT_10\nkeycode 77 = Num_Lock NoSymb'
b'ol Num_Lock\nkeycode 78 = Scroll_Lock NoSymbol Scroll_Lock\nkeycode 79 = KP_Home KP_7 KP_Home KP_7\nkeycode 8'
b'0 = KP_Up KP_8 KP_Up KP_8\nkeycode 81 = KP_Prior KP_9 KP_Prior KP_9\nkeycode 82 = KP_Subtract KP_Subtract KP_S'
b'ubtract KP_Subtract KP_Subtract KP_Subtract XF86Prev_VMode\nkeycode 83 = KP_Left KP_4 KP_Left KP_4\nkeycode 84'
b' = KP_Begin KP_5 KP_Begin KP_5\nkeycode 85 = KP_Right KP_6 KP_Right KP_6\nkeycode 86 = KP_Add KP_Add KP_Add KP'
b'_Add KP_Add KP_Add XF86Next_VMode\nkeycode 87 = KP_End KP_1 KP_End KP_1\nkeycode 88 = KP_Down KP_2 KP_Down KP_'
b'2\nkeycode 89 = KP_Next KP_3 KP_Next KP_3\nkeycode 90 = KP_Insert KP_0 KP_Insert KP_0\nkeycode 91 = KP_Delete'
b' KP_Separator KP_Delete KP_Separator\nkeycode 92 = ISO_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 93 =\nk'
b'eycode 94 = less greater less greater bar dead_belowmacron bar\nkeycode 95 = F11 F11 F11 F11 F11 F11 XF86Switc'
b'h_VT_11\nkeycode 96 = F12 F12 F12 F12 F12 F12 XF86Switch_VT_12\nkeycode 97 =\nkeycode 98 = Katakana NoSymbol '
b'Katakana\nkeycode 99 = Hiragana NoSymbol Hiragana\nkeycode 100 = Henkan_Mode NoSymbol Henkan_Mode\nkeycode 101 '
b'= Hiragana_Katakana NoSymbol Hiragana_Katakana\nkeycode 102 = Muhenkan NoSymbol Muhenkan\nkeycode 103 =\nkeycode'
b' 104 = KP_Enter NoSymbol KP_Enter\nkeycode 105 = Control_R NoSymbol Control_R\nkeycode 106 = KP_Divide KP_Divide'
b' KP_Divide KP_Divide KP_Divide KP_Divide XF86Ungrab\nkeycode 107 = Print Sys_Req Print Sys_Req\nkeycode 108 = IS'
b'O_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 109 = Linefeed NoSymbol Linefeed\nkeycode 110 = Home NoSymbol '
b'Home\nkeycode 111 = Up NoSymbol Up\nkeycode 112 = Prior NoSymbol Prior\nkeycode 113 = Left NoSymbol Left\nkeycod'
b'e 114 = Right NoSymbol Right\nkeycode 115 = End NoSymbol End\nkeycode 116 = Down NoSymbol Down\nkeycode 117 = Ne'
b'xt NoSymbol Next\nkeycode 118 = Insert NoSymbol Insert\nkeycode 119 = Delete NoSymbol Delete\nkeycode 120 =\nkey'
b'code 121 = XF86AudioMute NoSymbol XF86AudioMute\nkeycode 122 = XF86AudioLowerVolume NoSymbol XF86AudioLowerVolum'
b'e\nkeycode 123 = XF86AudioRaiseVolume NoSymbol XF86AudioRaiseVolume\nkeycode 124 = XF86PowerOff NoSymbol XF86Pow'
b'erOff\nkeycode 125 = KP_Equal NoSymbol KP_Equal\nkeycode 126 = plusminus NoSymbol plusminus\nkeycode 127 = Pause'
b' Break Pause Break\nkeycode 128 = XF86LaunchA NoSymbol XF86LaunchA\nkeycode 129 = KP_Decimal KP_Decimal KP_Decim'
b'al KP_Decimal\nkeycode 130 = Hangul NoSymbol Hangul\nkeycode 131 = Hangul_Hanja NoSymbol Hangul_Hanja\nkeycode 1'
b'32 =\nkeycode 133 = Super_L NoSymbol Super_L\nkeycode 134 = Super_R NoSymbol Super_R\nkeycode 135 = Menu NoSymbo'
b'l Menu\nkeycode 136 = Cancel NoSymbol Cancel\nkeycode 137 = Redo NoSymbol Redo\nkeycode 138 = SunProps NoSymbol '
b'SunProps\nkeycode 139 = Undo NoSymbol Undo\nkeycode 140 = SunFront NoSymbol SunFront\nkeycode 141 = XF86Copy NoS'
b'ymbol XF86Copy\nkeycode 142 = XF86Open NoSymbol XF86Open\nkeycode 143 = XF86Paste NoSymbol XF86Paste\nkeycode 14'
b'4 = Find NoSymbol Find\nkeycode 145 = XF86Cut NoSymbol XF86Cut\nkeycode 146 = Help NoSymbol Help\nkeycode 147 = '
b'XF86MenuKB NoSymbol XF86MenuKB\nkeycode 148 = XF86Calculator NoSymbol XF86Calculator\nkeycode 149 =\nkeycode 150'
b' = XF86Sleep NoSymbol XF86Sleep\nkeycode 151 = XF86WakeUp NoSymbol XF86WakeUp\nkeycode 152 = XF86Explorer NoSymb'
b'ol XF86Explorer\nkeycode 153 = XF86Send NoSymbol XF86Send\nkeycode 154 =\nkeycode 155 = XF86Xfer NoSymbol XF86Xf'
b'er\nkeycode 156 = XF86Launch1 NoSymbol XF86Launch1\nkeycode 157 = XF86Launch2 NoSymbol XF86Launch2\nkeycode 158 '
b'= XF86WWW NoSymbol XF86WWW\nkeycode 159 = XF86DOS NoSymbol XF86DOS\nkeycode 160 = XF86ScreenSaver NoSymbol XF86S'
b'creenSaver\nkeycode 161 = XF86RotateWindows NoSymbol XF86RotateWindows\nkeycode 162 = XF86TaskPane NoSymbol XF86'
b'TaskPane\nkeycode 163 = XF86Mail NoSymbol XF86Mail\nkeycode 164 = XF86Favorites NoSymbol XF86Favorites\nkeycode '
b'165 = XF86MyComputer NoSymbol XF86MyComputer\nkeycode 166 = XF86Back NoSymbol XF86Back\nkeycode 167 = XF86Forwar'
b'd NoSymbol XF86Forward\nkeycode 168 =\nkeycode 169 = XF86Eject NoSymbol XF86Eject\nkeycode 170 = XF86Eject XF86E'
b'ject XF86Eject XF86Eject\nkeycode 171 = XF86AudioNext NoSymbol XF86AudioNext\nkeycode 172 = XF86AudioPlay XF86Au'
b'dioPause XF86AudioPlay XF86AudioPause\nkeycode 173 = XF86AudioPrev NoSymbol XF86AudioPrev\nkeycode 174 = XF86Aud'
b'ioStop XF86Eject XF86AudioStop XF86Eject\nkeycode 175 = XF86AudioRecord NoSymbol XF86AudioRecord\nkeycode 176 = '
b'XF86AudioRewind NoSymbol XF86AudioRewind\nkeycode 177 = XF86Phone NoSymbol XF86Phone\nkeycode 178 =\nkeycode 179'
b' = XF86Tools NoSymbol XF86Tools\nkeycode 180 = XF86HomePage NoSymbol XF86HomePage\nkeycode 181 = XF86Reload NoSy'
b'mbol XF86Reload\nkeycode 182 = XF86Close NoSymbol XF86Close\nkeycode 183 =\nkeycode 184 =\nkeycode 185 = XF86Scr'
b'ollUp NoSymbol XF86ScrollUp\nkeycode 186 = XF86ScrollDown NoSymbol XF86ScrollDown\nkeycode 187 = parenleft NoSym'
b'bol parenleft\nkeycode 188 = parenright NoSymbol parenright\nkeycode 189 = XF86New NoSymbol XF86New\nkeycode 190'
b' = Redo NoSymbol Redo\nkeycode 191 = XF86Tools NoSymbol XF86Tools\nkeycode 192 = XF86Launch5 NoSymbol XF86Launch'
b'5\nkeycode 193 = XF86Launch6 NoSymbol XF86Launch6\nkeycode 194 = XF86Launch7 NoSymbol XF86Launch7\nkeycode 195 ='
b' XF86Launch8 NoSymbol XF86Launch8\nkeycode 196 = XF86Launch9 NoSymbol XF86Launch9\nkeycode 197 =\nkeycode 198 = '
b'XF86AudioMicMute NoSymbol XF86AudioMicMute\nkeycode 199 = XF86TouchpadToggle NoSymbol XF86TouchpadToggle\nkeycod'
b'e 200 = XF86TouchpadOn NoSymbol XF86TouchpadOn\nkeycode 201 = XF86TouchpadOff NoSymbol XF86TouchpadOff\nkeycode '
b'202 =\nkeycode 203 = Mode_switch NoSymbol Mode_switch\nkeycode 204 = NoSymbol Alt_L NoSymbol Alt_L\nkeycode 205 '
b'= NoSymbol Meta_L NoSymbol Meta_L\nkeycode 206 = NoSymbol Super_L NoSymbol Super_L\nkeycode 207 = NoSymbol Hyper'
b'_L NoSymbol Hyper_L\nkeycode 208 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 209 = XF86AudioPause NoSymbol X'
b'F86AudioPause\nkeycode 210 = XF86Launch3 NoSymbol XF86Launch3\nkeycode 211 = XF86Launch4 NoSymbol XF86Launch4\nk'
b'eycode 212 = XF86LaunchB NoSymbol XF86LaunchB\nkeycode 213 = XF86Suspend NoSymbol XF86Suspend\nkeycode 214 = XF8'
b'6Close NoSymbol XF86Close\nkeycode 215 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 216 = XF86AudioForward No'
b'Symbol XF86AudioForward\nkeycode 217 =\nkeycode 218 = Print NoSymbol Print\nkeycode 219 =\nkeycode 220 = XF86Web'
b'Cam NoSymbol XF86WebCam\nkeycode 221 = XF86AudioPreset NoSymbol XF86AudioPreset\nkeycode 222 =\nkeycode 223 = XF'
b'86Mail NoSymbol XF86Mail\nkeycode 224 = XF86Messenger NoSymbol XF86Messenger\nkeycode 225 = XF86Search NoSymbol '
b'XF86Search\nkeycode 226 = XF86Go NoSymbol XF86Go\nkeycode 227 = XF86Finance NoSymbol XF86Finance\nkeycode 228 = '
b'XF86Game NoSymbol XF86Game\nkeycode 229 = XF86Shop NoSymbol XF86Shop\nkeycode 230 =\nkeycode 231 = Cancel NoSymb'
b'ol Cancel\nkeycode 232 = XF86MonBrightnessDown NoSymbol XF86MonBrightnessDown\nkeycode 233 = XF86MonBrightnessUp'
b' NoSymbol XF86MonBrightnessUp\nkeycode 234 = XF86AudioMedia NoSymbol XF86AudioMedia\nkeycode 235 = XF86Display N'
b'oSymbol XF86Display\nkeycode 236 = XF86KbdLightOnOff NoSymbol XF86KbdLightOnOff\nkeycode 237 = XF86KbdBrightness'
b'Down NoSymbol XF86KbdBrightnessDown\nkeycode 238 = XF86KbdBrightnessUp NoSymbol XF86KbdBrightnessUp\nkeycode 239'
b' = XF86Send NoSymbol XF86Send\nkeycode 240 = XF86Reply NoSymbol XF86Reply\nkeycode 241 = XF86MailForward NoSymbo'
b'l XF86MailForward\nkeycode 242 = XF86Save NoSymbol XF86Save\nkeycode 243 = XF86Documents NoSymbol XF86Documents'
b'\nkeycode 244 = XF86Battery NoSymbol XF86Battery\nkeycode 245 = XF86Bluetooth NoSymbol XF86Bluetooth\nkeycode 24'
b'6 = XF86WLAN NoSymbol XF86WLAN\nkeycode 247 =\nkeycode 248 =\nkeycode 249 =\nkeycode 250 =\nkeycode 251 = XF86Mo'
b'nBrightnessCycle NoSymbol XF86MonBrightnessCycle\nkeycode 252 =\nkeycode 253 =\nkeycode 254 = XF86WWAN NoSymbol '
b'XF86WWAN\nkeycode 255 = XF86RFKill NoSymbol XF86RFKill\n'
b"keycode 8 =\nkeycode 9 = Escape NoSymbol Escape\nkeycode 10 = 1 exclam 1 exclam onesuperior exclamdown ones"
b"uperior\nkeycode 11 = 2 quotedbl 2 quotedbl twosuperior oneeighth twosuperior\nkeycode 12 = 3 section 3 sectio"
b"n threesuperior sterling threesuperior\nkeycode 13 = 4 dollar 4 dollar onequarter currency onequarter\nkeycode "
b" 14 = 5 percent 5 percent onehalf threeeighths onehalf\nkeycode 15 = 6 ampersand 6 ampersand notsign fiveeighth"
b"s notsign\nkeycode 16 = 7 slash 7 slash braceleft seveneighths braceleft\nkeycode 17 = 8 parenleft 8 parenleft"
b" bracketleft trademark bracketleft\nkeycode 18 = 9 parenright 9 parenright bracketright plusminus bracketright"
b"\nkeycode 19 = 0 equal 0 equal braceright degree braceright\nkeycode 20 = ssharp question ssharp question back"
b"slash questiondown U1E9E\nkeycode 21 = dead_acute dead_grave dead_acute dead_grave dead_cedilla dead_ogonek dea"
b"d_cedilla\nkeycode 22 = BackSpace BackSpace BackSpace BackSpace\nkeycode 23 = Tab ISO_Left_Tab Tab ISO_Left_Ta"
b"b\nkeycode 24 = q Q q Q at Greek_OMEGA at\nkeycode 25 = w W w W lstroke Lstroke lstroke\nkeycode 26 = e E e E"
b" EuroSign EuroSign EuroSign\nkeycode 27 = r R r R paragraph registered paragraph\nkeycode 28 = t T t T tslash "
b"Tslash tslash\nkeycode 29 = z Z z Z leftarrow yen leftarrow\nkeycode 30 = u U u U downarrow uparrow downarrow"
b"\nkeycode 31 = i I i I rightarrow idotless rightarrow\nkeycode 32 = o O o O oslash Oslash oslash\nkeycode 33 "
b"= p P p P thorn THORN thorn\nkeycode 34 = udiaeresis Udiaeresis udiaeresis Udiaeresis dead_diaeresis dead_above"
b"ring dead_diaeresis\nkeycode 35 = plus asterisk plus asterisk asciitilde macron asciitilde\nkeycode 36 = Retur"
b"n NoSymbol Return\nkeycode 37 = Control_L NoSymbol Control_L\nkeycode 38 = a A a A ae AE ae\nkeycode 39 = s S"
b" s S U017F U1E9E U017F\nkeycode 40 = d D d D eth ETH eth\nkeycode 41 = f F f F dstroke ordfeminine dstroke\nke"
b"ycode 42 = g G g G eng ENG eng\nkeycode 43 = h H h H hstroke Hstroke hstroke\nkeycode 44 = j J j J dead_below"
b"dot dead_abovedot dead_belowdot\nkeycode 45 = k K k K kra ampersand kra\nkeycode 46 = l L l L lstroke Lstroke "
b"lstroke\nkeycode 47 = odiaeresis Odiaeresis odiaeresis Odiaeresis dead_doubleacute dead_belowdot dead_doubleacu"
b"te\nkeycode 48 = adiaeresis Adiaeresis adiaeresis Adiaeresis dead_circumflex dead_caron dead_circumflex\nkeycod"
b"e 49 = dead_circumflex degree dead_circumflex degree U2032 U2033 U2032\nkeycode 50 = Shift_L NoSymbol Shift_L"
b"\nkeycode 51 = numbersign apostrophe numbersign apostrophe rightsinglequotemark dead_breve rightsinglequotemark"
b"\nkeycode 52 = y Y y Y guillemotright U203A guillemotright\nkeycode 53 = x X x X guillemotleft U2039 guillemot"
b"left\nkeycode 54 = c C c C cent copyright cent\nkeycode 55 = v V v V doublelowquotemark singlelowquotemark dou"
b"blelowquotemark\nkeycode 56 = b B b B leftdoublequotemark leftsinglequotemark leftdoublequotemark\nkeycode 57 "
b"= n N n N rightdoublequotemark rightsinglequotemark rightdoublequotemark\nkeycode 58 = m M m M mu masculine mu"
b"\nkeycode 59 = comma semicolon comma semicolon periodcentered multiply periodcentered\nkeycode 60 = period col"
b"on period colon U2026 division U2026\nkeycode 61 = minus underscore minus underscore endash emdash endash\nkeyc"
b"ode 62 = Shift_R NoSymbol Shift_R\nkeycode 63 = KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP"
b"_Multiply XF86ClearGrab\nkeycode 64 = Alt_L Meta_L Alt_L Meta_L\nkeycode 65 = space NoSymbol space\nkeycode 6"
b"6 = Caps_Lock NoSymbol Caps_Lock\nkeycode 67 = F1 F1 F1 F1 F1 F1 XF86Switch_VT_1\nkeycode 68 = F2 F2 F2 F2 F2 "
b"F2 XF86Switch_VT_2\nkeycode 69 = F3 F3 F3 F3 F3 F3 XF86Switch_VT_3\nkeycode 70 = F4 F4 F4 F4 F4 F4 XF86Switch_"
b"VT_4\nkeycode 71 = F5 F5 F5 F5 F5 F5 XF86Switch_VT_5\nkeycode 72 = F6 F6 F6 F6 F6 F6 XF86Switch_VT_6\nkeycode "
b" 73 = F7 F7 F7 F7 F7 F7 XF86Switch_VT_7\nkeycode 74 = F8 F8 F8 F8 F8 F8 XF86Switch_VT_8\nkeycode 75 = F9 F9 F9"
b" F9 F9 F9 XF86Switch_VT_9\nkeycode 76 = F10 F10 F10 F10 F10 F10 XF86Switch_VT_10\nkeycode 77 = Num_Lock NoSymb"
b"ol Num_Lock\nkeycode 78 = Scroll_Lock NoSymbol Scroll_Lock\nkeycode 79 = KP_Home KP_7 KP_Home KP_7\nkeycode 8"
b"0 = KP_Up KP_8 KP_Up KP_8\nkeycode 81 = KP_Prior KP_9 KP_Prior KP_9\nkeycode 82 = KP_Subtract KP_Subtract KP_S"
b"ubtract KP_Subtract KP_Subtract KP_Subtract XF86Prev_VMode\nkeycode 83 = KP_Left KP_4 KP_Left KP_4\nkeycode 84"
b" = KP_Begin KP_5 KP_Begin KP_5\nkeycode 85 = KP_Right KP_6 KP_Right KP_6\nkeycode 86 = KP_Add KP_Add KP_Add KP"
b"_Add KP_Add KP_Add XF86Next_VMode\nkeycode 87 = KP_End KP_1 KP_End KP_1\nkeycode 88 = KP_Down KP_2 KP_Down KP_"
b"2\nkeycode 89 = KP_Next KP_3 KP_Next KP_3\nkeycode 90 = KP_Insert KP_0 KP_Insert KP_0\nkeycode 91 = KP_Delete"
b" KP_Separator KP_Delete KP_Separator\nkeycode 92 = ISO_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 93 =\nk"
b"eycode 94 = less greater less greater bar dead_belowmacron bar\nkeycode 95 = F11 F11 F11 F11 F11 F11 XF86Switc"
b"h_VT_11\nkeycode 96 = F12 F12 F12 F12 F12 F12 XF86Switch_VT_12\nkeycode 97 =\nkeycode 98 = Katakana NoSymbol "
b"Katakana\nkeycode 99 = Hiragana NoSymbol Hiragana\nkeycode 100 = Henkan_Mode NoSymbol Henkan_Mode\nkeycode 101 "
b"= Hiragana_Katakana NoSymbol Hiragana_Katakana\nkeycode 102 = Muhenkan NoSymbol Muhenkan\nkeycode 103 =\nkeycode"
b" 104 = KP_Enter NoSymbol KP_Enter\nkeycode 105 = Control_R NoSymbol Control_R\nkeycode 106 = KP_Divide KP_Divide"
b" KP_Divide KP_Divide KP_Divide KP_Divide XF86Ungrab\nkeycode 107 = Print Sys_Req Print Sys_Req\nkeycode 108 = IS"
b"O_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 109 = Linefeed NoSymbol Linefeed\nkeycode 110 = Home NoSymbol "
b"Home\nkeycode 111 = Up NoSymbol Up\nkeycode 112 = Prior NoSymbol Prior\nkeycode 113 = Left NoSymbol Left\nkeycod"
b"e 114 = Right NoSymbol Right\nkeycode 115 = End NoSymbol End\nkeycode 116 = Down NoSymbol Down\nkeycode 117 = Ne"
b"xt NoSymbol Next\nkeycode 118 = Insert NoSymbol Insert\nkeycode 119 = Delete NoSymbol Delete\nkeycode 120 =\nkey"
b"code 121 = XF86AudioMute NoSymbol XF86AudioMute\nkeycode 122 = XF86AudioLowerVolume NoSymbol XF86AudioLowerVolum"
b"e\nkeycode 123 = XF86AudioRaiseVolume NoSymbol XF86AudioRaiseVolume\nkeycode 124 = XF86PowerOff NoSymbol XF86Pow"
b"erOff\nkeycode 125 = KP_Equal NoSymbol KP_Equal\nkeycode 126 = plusminus NoSymbol plusminus\nkeycode 127 = Pause"
b" Break Pause Break\nkeycode 128 = XF86LaunchA NoSymbol XF86LaunchA\nkeycode 129 = KP_Decimal KP_Decimal KP_Decim"
b"al KP_Decimal\nkeycode 130 = Hangul NoSymbol Hangul\nkeycode 131 = Hangul_Hanja NoSymbol Hangul_Hanja\nkeycode 1"
b"32 =\nkeycode 133 = Super_L NoSymbol Super_L\nkeycode 134 = Super_R NoSymbol Super_R\nkeycode 135 = Menu NoSymbo"
b"l Menu\nkeycode 136 = Cancel NoSymbol Cancel\nkeycode 137 = Redo NoSymbol Redo\nkeycode 138 = SunProps NoSymbol "
b"SunProps\nkeycode 139 = Undo NoSymbol Undo\nkeycode 140 = SunFront NoSymbol SunFront\nkeycode 141 = XF86Copy NoS"
b"ymbol XF86Copy\nkeycode 142 = XF86Open NoSymbol XF86Open\nkeycode 143 = XF86Paste NoSymbol XF86Paste\nkeycode 14"
b"4 = Find NoSymbol Find\nkeycode 145 = XF86Cut NoSymbol XF86Cut\nkeycode 146 = Help NoSymbol Help\nkeycode 147 = "
b"XF86MenuKB NoSymbol XF86MenuKB\nkeycode 148 = XF86Calculator NoSymbol XF86Calculator\nkeycode 149 =\nkeycode 150"
b" = XF86Sleep NoSymbol XF86Sleep\nkeycode 151 = XF86WakeUp NoSymbol XF86WakeUp\nkeycode 152 = XF86Explorer NoSymb"
b"ol XF86Explorer\nkeycode 153 = XF86Send NoSymbol XF86Send\nkeycode 154 =\nkeycode 155 = XF86Xfer NoSymbol XF86Xf"
b"er\nkeycode 156 = XF86Launch1 NoSymbol XF86Launch1\nkeycode 157 = XF86Launch2 NoSymbol XF86Launch2\nkeycode 158 "
b"= XF86WWW NoSymbol XF86WWW\nkeycode 159 = XF86DOS NoSymbol XF86DOS\nkeycode 160 = XF86ScreenSaver NoSymbol XF86S"
b"creenSaver\nkeycode 161 = XF86RotateWindows NoSymbol XF86RotateWindows\nkeycode 162 = XF86TaskPane NoSymbol XF86"
b"TaskPane\nkeycode 163 = XF86Mail NoSymbol XF86Mail\nkeycode 164 = XF86Favorites NoSymbol XF86Favorites\nkeycode "
b"165 = XF86MyComputer NoSymbol XF86MyComputer\nkeycode 166 = XF86Back NoSymbol XF86Back\nkeycode 167 = XF86Forwar"
b"d NoSymbol XF86Forward\nkeycode 168 =\nkeycode 169 = XF86Eject NoSymbol XF86Eject\nkeycode 170 = XF86Eject XF86E"
b"ject XF86Eject XF86Eject\nkeycode 171 = XF86AudioNext NoSymbol XF86AudioNext\nkeycode 172 = XF86AudioPlay XF86Au"
b"dioPause XF86AudioPlay XF86AudioPause\nkeycode 173 = XF86AudioPrev NoSymbol XF86AudioPrev\nkeycode 174 = XF86Aud"
b"ioStop XF86Eject XF86AudioStop XF86Eject\nkeycode 175 = XF86AudioRecord NoSymbol XF86AudioRecord\nkeycode 176 = "
b"XF86AudioRewind NoSymbol XF86AudioRewind\nkeycode 177 = XF86Phone NoSymbol XF86Phone\nkeycode 178 =\nkeycode 179"
b" = XF86Tools NoSymbol XF86Tools\nkeycode 180 = XF86HomePage NoSymbol XF86HomePage\nkeycode 181 = XF86Reload NoSy"
b"mbol XF86Reload\nkeycode 182 = XF86Close NoSymbol XF86Close\nkeycode 183 =\nkeycode 184 =\nkeycode 185 = XF86Scr"
b"ollUp NoSymbol XF86ScrollUp\nkeycode 186 = XF86ScrollDown NoSymbol XF86ScrollDown\nkeycode 187 = parenleft NoSym"
b"bol parenleft\nkeycode 188 = parenright NoSymbol parenright\nkeycode 189 = XF86New NoSymbol XF86New\nkeycode 190"
b" = Redo NoSymbol Redo\nkeycode 191 = XF86Tools NoSymbol XF86Tools\nkeycode 192 = XF86Launch5 NoSymbol XF86Launch"
b"5\nkeycode 193 = XF86Launch6 NoSymbol XF86Launch6\nkeycode 194 = XF86Launch7 NoSymbol XF86Launch7\nkeycode 195 ="
b" XF86Launch8 NoSymbol XF86Launch8\nkeycode 196 = XF86Launch9 NoSymbol XF86Launch9\nkeycode 197 =\nkeycode 198 = "
b"XF86AudioMicMute NoSymbol XF86AudioMicMute\nkeycode 199 = XF86TouchpadToggle NoSymbol XF86TouchpadToggle\nkeycod"
b"e 200 = XF86TouchpadOn NoSymbol XF86TouchpadOn\nkeycode 201 = XF86TouchpadOff NoSymbol XF86TouchpadOff\nkeycode "
b"202 =\nkeycode 203 = Mode_switch NoSymbol Mode_switch\nkeycode 204 = NoSymbol Alt_L NoSymbol Alt_L\nkeycode 205 "
b"= NoSymbol Meta_L NoSymbol Meta_L\nkeycode 206 = NoSymbol Super_L NoSymbol Super_L\nkeycode 207 = NoSymbol Hyper"
b"_L NoSymbol Hyper_L\nkeycode 208 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 209 = XF86AudioPause NoSymbol X"
b"F86AudioPause\nkeycode 210 = XF86Launch3 NoSymbol XF86Launch3\nkeycode 211 = XF86Launch4 NoSymbol XF86Launch4\nk"
b"eycode 212 = XF86LaunchB NoSymbol XF86LaunchB\nkeycode 213 = XF86Suspend NoSymbol XF86Suspend\nkeycode 214 = XF8"
b"6Close NoSymbol XF86Close\nkeycode 215 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 216 = XF86AudioForward No"
b"Symbol XF86AudioForward\nkeycode 217 =\nkeycode 218 = Print NoSymbol Print\nkeycode 219 =\nkeycode 220 = XF86Web"
b"Cam NoSymbol XF86WebCam\nkeycode 221 = XF86AudioPreset NoSymbol XF86AudioPreset\nkeycode 222 =\nkeycode 223 = XF"
b"86Mail NoSymbol XF86Mail\nkeycode 224 = XF86Messenger NoSymbol XF86Messenger\nkeycode 225 = XF86Search NoSymbol "
b"XF86Search\nkeycode 226 = XF86Go NoSymbol XF86Go\nkeycode 227 = XF86Finance NoSymbol XF86Finance\nkeycode 228 = "
b"XF86Game NoSymbol XF86Game\nkeycode 229 = XF86Shop NoSymbol XF86Shop\nkeycode 230 =\nkeycode 231 = Cancel NoSymb"
b"ol Cancel\nkeycode 232 = XF86MonBrightnessDown NoSymbol XF86MonBrightnessDown\nkeycode 233 = XF86MonBrightnessUp"
b" NoSymbol XF86MonBrightnessUp\nkeycode 234 = XF86AudioMedia NoSymbol XF86AudioMedia\nkeycode 235 = XF86Display N"
b"oSymbol XF86Display\nkeycode 236 = XF86KbdLightOnOff NoSymbol XF86KbdLightOnOff\nkeycode 237 = XF86KbdBrightness"
b"Down NoSymbol XF86KbdBrightnessDown\nkeycode 238 = XF86KbdBrightnessUp NoSymbol XF86KbdBrightnessUp\nkeycode 239"
b" = XF86Send NoSymbol XF86Send\nkeycode 240 = XF86Reply NoSymbol XF86Reply\nkeycode 241 = XF86MailForward NoSymbo"
b"l XF86MailForward\nkeycode 242 = XF86Save NoSymbol XF86Save\nkeycode 243 = XF86Documents NoSymbol XF86Documents"
b"\nkeycode 244 = XF86Battery NoSymbol XF86Battery\nkeycode 245 = XF86Bluetooth NoSymbol XF86Bluetooth\nkeycode 24"
b"6 = XF86WLAN NoSymbol XF86WLAN\nkeycode 247 =\nkeycode 248 =\nkeycode 249 =\nkeycode 250 =\nkeycode 251 = XF86Mo"
b"nBrightnessCycle NoSymbol XF86MonBrightnessCycle\nkeycode 252 =\nkeycode 253 =\nkeycode 254 = XF86WWAN NoSymbol "
b"XF86WWAN\nkeycode 255 = XF86RFKill NoSymbol XF86RFKill\n"
)

Loading…
Cancel
Save