This commit is contained in:
sezanzeb 2020-11-18 22:06:54 +01:00
parent 022950e3b5
commit f18b013513
10 changed files with 60 additions and 627 deletions

View File

@ -35,7 +35,6 @@ from gi.repository import Gtk
from keymapper.logger import logger, update_verbosity, log_info
from keymapper.gtk.error import ErrorDialog
from keymapper.gtk.window import Window
from keymapper.state import initialize
if __name__ == '__main__':
@ -50,8 +49,6 @@ if __name__ == '__main__':
update_verbosity(options.debug)
log_info()
initialize()
window = Window()
def stop_injecting():

View File

@ -1,222 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.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/>.
"""Code that is not used anymore, but might be in the future.
Currently it is not needed to create symbols files in xkb. Which is a pity
considering all the work put into this. This stuff is even unittested.
Resources:
[1] https://wiki.archlinux.org/index.php/Keyboard_input
[2] http://people.uleth.ca/~daniel.odonnell/Blog/custom-keyboard-in-linuxx11
[3] https://www.x.org/releases/X11R7.7/doc/xorg-docs/input/XKB-Enhancing.html
"""
import os
import re
import stat
from keymapper.logger import logger
from keymapper.data import get_data_path
from keymapper.state import custom_mapping, internal_mapping
from keymapper.paths import KEYCODES_PATH
permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH
MAX_KEYCODE = 255
MIN_KEYCODE = 8
# the path that contains ALL symbols, not just ours
X11_SYMBOLS = '/usr/share/X11/xkb/symbols'
# should not contain spaces
# getlogin gets the user who ran sudo
USERS_SYMBOLS = os.path.join(
'/usr/share/X11/xkb/symbols/key-mapper',
os.getlogin().replace(' ', '_')
)
def get_usr_path(device=None, preset=None):
"""Get the path to the config file in /usr.
This folder is a symlink and the files are in ~/.config/key-mapper
If preset is omitted, returns the folder for the device.
"""
if device is None:
return USERS_SYMBOLS
device = device.strip()
if preset is not None:
preset = preset.strip()
return os.path.join(USERS_SYMBOLS, device, preset).replace(' ', '_')
if device is not None:
return os.path.join(USERS_SYMBOLS, device.replace(' ', '_'))
DEFAULT_SYMBOLS = get_usr_path('default')
def create_preset(device, name=None):
"""Create an empty preset and return the potentially incremented name.
Automatically avoids file conflicts by adding a number to the name
if needed.
"""
if name is None:
name = 'new preset'
# find a name that is not already taken
if os.path.exists(get_usr_path(device, name)):
i = 2
while os.path.exists(get_usr_path(device, f'{name} {i}')):
i += 1
name = f'{name} {i}'
path = get_usr_path(device, name)
if not os.path.exists(path):
logger.info('Creating new file %s', path)
os.makedirs(os.path.dirname(path), exist_ok=True)
os.mknod(path)
# add the same permissions as other symbol files, only root may write.
os.chmod(path, permissions)
return name
def get_preset_name(device, preset=None):
"""Get the name for that preset that is used for the setxkbmap command."""
# It's the relative path starting from X11/xkb/symbols and must not
# contain spaces
name = get_usr_path(device, preset)[len(X11_SYMBOLS) + 1:]
assert ' ' not in name
return name
DEFAULT_SYMBOLS_NAME = get_preset_name('default')
EMPTY_SYMBOLS_NAME = get_preset_name('empty')
def create_setxkbmap_config(device, preset):
"""Generate a config file for setxkbmap.
Parameters
----------
device : string
preset : string
"""
if len(custom_mapping) == 0:
logger.debug('Got empty mappings')
return None
create_identity_mapping()
create_default_symbols()
device_path = get_usr_path(device)
if not os.path.exists(device_path):
logger.info('Creating directory "%s"', device_path)
os.makedirs(device_path, exist_ok=True)
preset_path = get_usr_path(device, preset)
if not os.path.exists(preset_path):
logger.info('Creating config file "%s"', preset_path)
os.mknod(preset_path)
logger.info('Writing key mappings to %s', preset_path)
with open(preset_path, 'w') as f:
contents = generate_symbols(get_preset_name(device, preset))
if contents is not None:
f.write(contents)
def parse_symbols_file(device, preset):
"""Parse a symbols file populate the mapping.
Existing mappings are overwritten if there are conflicts.
"""
path = get_usr_path(device, preset)
if not os.path.exists(path):
logger.debug(
'Tried to load non existing preset "%s" for %s',
preset, device
)
custom_mapping.empty()
custom_mapping.changed = False
return
with open(path, 'r') as f:
# from "key <12> { [ 1 ] };" extract 12 and 1,
# from "key <12> { [ a, A ] };" extract 12 and [a, A]
# avoid lines that start with special characters
# (might be comments)
# And only find those lines that have a system-keycode written
# after them, because I need that one to show in the ui. (Might
# be deprecated.)
content = f.read()
result = re.findall(
r'\n\s+?key <(.+?)>.+?\[\s+(.+?)\s+\]\s+?}; // (\d+)',
content
)
logger.debug('Found %d mappings in preset "%s"', len(result), preset)
for target_keycode, character, system_keycode in result:
custom_mapping.change(
previous_keycode=None,
new_keycode=system_keycode,
character=character
)
custom_mapping.changed = False
def create_default_symbols():
"""Parse the output of xmodmap and create a default symbols file.
Since xmodmap may print mappings that have already been modified by
key-mapper, this should be done only once after the installation.
This is needed because all our keycode aliases in the symbols files
are "<int>", whereas the others are <AB01> and such, so they are not
compatible.
"""
if os.path.exists(DEFAULT_SYMBOLS):
logger.debug('Found the default mapping at %s', DEFAULT_SYMBOLS)
return
contents = generate_symbols(DEFAULT_SYMBOLS_NAME, None, system_mapping)
if not os.path.exists(DEFAULT_SYMBOLS):
logger.info('Creating %s', DEFAULT_SYMBOLS)
os.makedirs(os.path.dirname(DEFAULT_SYMBOLS), exist_ok=True)
os.mknod(DEFAULT_SYMBOLS)
os.chmod(DEFAULT_SYMBOLS, permissions)
with open(DEFAULT_SYMBOLS, 'w') as f:
if contents is not None:
logger.info('Updating default mappings')
f.write(contents)
else:
logger.error('Failed to write default mappings')

View File

@ -1,146 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.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/>.
"""Parsing and running CLI tools."""
import os
import re
import subprocess
from keymapper.logger import logger, is_debug
from keymapper.getdevices import get_devices
def get_system_layout_locale():
"""Get the system wide configured default keyboard layout locale."""
localectl = subprocess.check_output(
['localectl', 'status']
).decode().split('\n')
# example:
# System Locale: LANG=en_GB.UTF-8
# VC Keymap: tmp
# X11 Layout: de
# X11 Model: pc105
return [
line for line in localectl
if 'X11 Layout' in line
][0].split(': ')[-1]
def apply_symbols(device, name=None, keycodes=None):
"""Apply a symbols configuration to the device.
Parameters
----------
device : string
A device, should be a key of get_devices
name : string
This is the name of the symbols to apply. For example "de",
"key-mapper-empty" or "key-mapper-dev"
keycodes : string
This is the name of the keycodes file needed for that. If you don't
provide the correct one, X will crash. For example "key-mapper",
which is the "identity mapping", or "de"
"""
if get_devices().get(device) is None:
# maybe you should run refresh_devices
logger.error('Tried to apply symbols on unknown device "%s"', device)
return
if name is None:
name = get_system_layout_locale()
logger.debug('Applying symbols "%s" to device "%s"', name, device)
# sanity check one
symbols_path = os.path.join('/usr/share/X11/xkb/symbols', name)
if not os.path.exists(symbols_path):
logger.error('Symbols file "%s" doesn\'t exist', symbols_path)
return
with open(symbols_path, 'r') as f:
if f.read() == '':
logger.error('Tried to load empty symbols %s', symbols_path)
return
if keycodes is not None:
# sanity check two
keycodes_path = os.path.join('/usr/share/X11/xkb/keycodes', keycodes)
if not os.path.exists(keycodes_path):
logger.error('keycodes "%s" don\'t exist', keycodes_path)
return
with open(keycodes_path, 'r') as f:
if f.read() == '':
logger.error('Found empty keycodes "%s"', keycodes_path)
return
cmd = ['setxkbmap', '-layout', name]
if keycodes is not None:
cmd += ['-keycodes', keycodes]
# apply it to every device that hangs on the same usb port, because I
# have no idea how to figure out which one of those 3 devices that are
# all named after my mouse to use.
group = get_devices()[device]
for xinput_name, xinput_id in get_xinput_id_mapping():
if xinput_name not in group['devices']:
# only all virtual devices of the same hardware device
continue
device_cmd = cmd + ['-device', str(xinput_id)]
logger.debug('Running `%s`', ' '.join(device_cmd))
output = subprocess.run(device_cmd, capture_output=True)
output = output.stderr.decode().strip()
if output != '':
logger.debug2(output)
def get_xinput_id_mapping():
"""Run xinput and get a list of name, id tuplies.
The ids are needed for setxkbmap. There might be duplicate names with
different ids.
"""
names = subprocess.check_output(
['xinput', 'list', '--name-only']
).decode().split('\n')
ids = subprocess.check_output(
['xinput', 'list', '--id-only']
).decode().split('\n')
names = [name for name in names if name != '']
ids = [int(id) for id in ids if id != '']
return zip(names, ids)
def parse_xmodmap(mapping):
"""Read the output of xmodmap into a mapping."""
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
# TODO is this tested?
for keycode, characters in mappings:
# this is the "array" format needed for symbols files
character = ', '.join(characters.split())
mapping.change(
previous_keycode=None,
new_keycode=int(keycode),
character=character
)

View File

@ -54,6 +54,7 @@ class _GetDevicesProcess(multiprocessing.Process):
def run(self):
"""Do what get_devices describes."""
logger.debug('Discovering device paths')
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
# group them together by usb device because there could be stuff like
@ -67,12 +68,20 @@ class _GetDevicesProcess(multiprocessing.Process):
continue
if evdev.ecodes.EV_REL in capabilities:
# skip devices that control movement
# skip devices that control movement, because I need to
# grab the device and
logger.debug(
'Skipping %s to avoid impairing mouse movement',
device.path
)
continue
usb = device.phys.split('/')[0]
if grouped.get(usb) is None:
grouped[usb] = []
logger.debug('Adding %s', device.path)
grouped[usb].append((device.name, device.path))
# now write down all the paths of that group

View File

@ -22,9 +22,7 @@
"""User Interface."""
import sys
import gi
import time
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk, Gdk, GLib
@ -35,7 +33,6 @@ from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset, get_available_preset_name
from keymapper.logger import logger
from keymapper.injector import KeycodeInjector
from keymapper.cli import apply_symbols
from keymapper.getdevices import get_devices
from keymapper.gtk.row import Row
from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK
@ -208,7 +205,6 @@ class Window:
"""Load the mapping."""
if self.keycode_reader is not None:
self.keycode_reader.stop_injecting()
apply_symbols(self.selected_device)
self.get('status_bar').push(
CTX_APPLY,
f'Applied the system default'

View File

@ -30,13 +30,12 @@ import asyncio
import evdev
from keymapper.logger import logger
from keymapper.cli import apply_symbols
from keymapper.getdevices import get_devices, refresh_devices
from keymapper.state import custom_mapping, internal_mapping, \
system_mapping, capabilities
from keymapper.getdevices import get_devices
from keymapper.state import custom_mapping, system_mapping
DEV_NAME = 'key-mapper'
DEVICE_CREATED = 1
def can_grab(path):
@ -71,23 +70,19 @@ class KeycodeInjector:
', '.join(paths)
)
apply_symbols(self.device, name='key-mapper-empty')
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
pipe = multiprocessing.Pipe()
worker = multiprocessing.Process(
target=self._start_injecting_worker,
args=(path, custom_mapping)
args=(path, custom_mapping, pipe[1])
)
worker.start()
# wait for the process to notify creation of the new injection
# device, to keep the logs in order.
pipe[0].recv()
self.processes.append(worker)
# it takes a little time for the key-mapper devices to appear
time.sleep(0.1)
refresh_devices()
apply_symbols(DEV_NAME, name='key-mapper-dev', keycodes='key-mapper')
def stop_injecting(self):
"""Stop injecting keycodes."""
# TODO test
@ -100,30 +95,24 @@ class KeycodeInjector:
process.terminate()
self.processes[i] = None
# apply the default layout back
apply_symbols(self.device)
def _start_injecting_worker(self, path, mapping):
def _start_injecting_worker(self, path, mapping, pipe):
"""Inject keycodes for one of the virtual devices."""
# TODO test
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
device = evdev.InputDevice(path)
"""try:
try:
# grab to avoid e.g. the disabled keycode of 10 to confuse X,
# especially when one of the buttons of your mouse also uses 10
device.grab()
except IOError:
logger.error('Cannot grab %s', path)"""
logger.error('Cannot grab %s', path)
# foo = evdev.InputDevice('/dev/input/event2')
keymapper_device = evdev.UInput(
name=DEV_NAME,
phys='key-mapper-uinput',
events={
evdev.ecodes.EV_KEY: [c - 8 for c in capabilities]
}
)
# copy the capabilities because the keymapper_device is going
# to act like the mouse
keymapper_device = evdev.UInput.from_device(device)
pipe.send(DEVICE_CREATED)
logger.debug(
'Started injecting into %s, fd %s',
@ -132,6 +121,12 @@ class KeycodeInjector:
for event in device.read_loop():
if event.type != evdev.ecodes.EV_KEY:
logger.spam(
'got type:%s code:%s value:%s, forward',
event.type, event.code, event.value
)
keymapper_device.write(event.type, event.code, event.value)
keymapper_device.syn()
continue
if event.value == 2:
@ -155,20 +150,8 @@ class KeycodeInjector:
character
)
continue
# turns out, if I don't sleep here X/Linux gets confused. Lets
# assume a mapping of 10 to z. Without sleep it would always
# result in 1z 1z 1z. Even though the empty xkb symbols file
# was applied on the mouse! And I really made sure `write` was
# not called twice. '1' just somewhow sneaks past the symbols.
# 0.0005 has many errors. 0.001 has them super rare.
# 5ms is still faster than anything on the planet so that's.
# fine. I came up with that after randomly poking around in,
# frustration. I don't know of any helpful resource that
# explains this
# TODO still needed? if yes, add to HELP.md
time.sleep(0.005)
logger.debug2(
logger.spam(
'got code:%s value:%s, maps to code:%s char:%s',
event.code + 8, event.value, target_keycode, character
)
@ -180,18 +163,4 @@ class KeycodeInjector:
event.value
)
# the second device that starts writing an event.value of 2 will
# take ownership of what is happening. Following example:
# (KB = keyboard, example devices)
# hold a on KB1:
# a-1, a-2, a-2, a-2, ...
# hold shift on KB2:
# shift-2, shift-2, shift-2, ...
# No a-2 on KB1 happening anymore. The xkb symbols of KB2 will
# be used! So if KB2 maps shift+a to b, it will write b, even
# though KB1 maps shift+a to c! And if you reverse this, hold
# shift on KB2 first and then a on KB1, the xkb mapping of KB1
# will take effect and write c!
# foo.write(evdev.ecodes.EV_SYN, evdev.ecodes.SYN_REPORT, 0)
keymapper_device.syn()

View File

@ -27,15 +27,15 @@ import logging
import pkg_resources
def debug2(self, message, *args, **kws):
if self.isEnabledFor(DEBUG2):
def spam(self, message, *args, **kws):
if self.isEnabledFor(SPAM):
# https://stackoverflow.com/a/13638084
self._log(DEBUG2, message, args, **kws)
self._log(SPAM, message, args, **kws)
DEBUG2 = 5
logging.addLevelName(DEBUG2, "DEBUG2")
logging.Logger.debug2 = debug2
SPAM = 5
logging.addLevelName(SPAM, "SPAM")
logging.Logger.spam = spam
class Formatter(logging.Formatter):
@ -53,7 +53,7 @@ class Formatter(logging.Formatter):
logging.ERROR: 31,
logging.FATAL: 31,
logging.DEBUG: 36,
DEBUG2: 34,
SPAM: 34,
logging.INFO: 32,
}.get(record.levelno, 0)
if debug:
@ -96,7 +96,7 @@ def log_info():
def update_verbosity(debug):
"""Set the logging verbosity according to the settings object."""
if debug:
logger.setLevel(DEBUG2)
logger.setLevel(SPAM)
else:
logger.setLevel(logging.INFO)

View File

@ -28,7 +28,6 @@ import shutil
from keymapper.logger import logger
from keymapper.paths import get_config_path
from keymapper.cli import parse_xmodmap
class Mapping:

View File

@ -22,204 +22,35 @@
"""Create some files and objects that are needed for the app to work."""
import os
import re
import stat
import evdev
import re
import subprocess
from keymapper.mapping import Mapping
from keymapper.cli import parse_xmodmap, apply_symbols
from keymapper.logger import logger
from keymapper.paths import KEYCODES_PATH, SYMBOLS_PATH
from keymapper.data import get_data_path
def parse_xmodmap(mapping):
"""Read the output of xmodmap into a mapping."""
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
# TODO is this tested?
for keycode, characters in mappings:
# this is the "array" format needed for symbols files
character = ', '.join(characters.split())
mapping.change(
previous_keycode=None,
new_keycode=int(keycode),
character=character
)
# one mapping object for the whole application that holds all
# customizations, as shown in the UI
custom_mapping = Mapping()
# this mapping is for the custom key-mapper /dev device. The keycode
# injector injects those keys to trigger the wanted character
internal_mapping = Mapping()
# this mapping represents the xmodmap output, which stays constant
system_mapping = Mapping()
capabilities = []
parse_xmodmap(system_mapping)
# permissions for files created in /usr
_PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH
def create_identity_mapping(keycodes=None):
"""Because the concept of "reasonable symbolic names" [3] doesn't apply
when mouse buttons are all over the place. Create an identity mapping
to make generating "symbols" files easier. Keycode 10 -> "<10>"
The identity mapping is provided to '-keycodes' of setxkbmap.
"""
logger.debug2('Available keycodes: %s', keycodes)
min_keycode = min(keycodes)
max_keycode = max(keycodes)
logger.debug(
'Creating the identity mapping. min: %s, max: %s',
min_keycode,
max_keycode
)
xkb_keycodes = []
if keycodes is None:
keycodes = range(min_keycode, max_keycode + 1)
for keycode in keycodes:
xkb_keycodes.append(f'<{keycode}> = {keycode};')
template_path = get_data_path('xkb_keycodes_template')
with open(template_path, 'r') as template_file:
template = template_file.read()
result = template.format(
minimum=min_keycode,
maximum=max_keycode,
xkb_keycodes='\n '.join(xkb_keycodes)
)
if not os.path.exists(KEYCODES_PATH):
logger.debug('Creating "%s"', KEYCODES_PATH)
os.makedirs(os.path.dirname(KEYCODES_PATH), exist_ok=True)
os.mknod(KEYCODES_PATH)
os.chmod(KEYCODES_PATH, _PERMISSIONS)
with open(KEYCODES_PATH, 'w') as f:
f.write(result)
def generate_symbols(mapping):
"""Create config contents to be placed in /usr/share/X11/xkb/symbols.
It's the mapping of the preset as expected by X. This function does not
create the file.
Parameters
----------
mapping : Mapping
If you need to create a symbols file for some other mapping you can
pass it to this parameter. By default the custom mapping will be
used that is also displayed in the user interface.
"""
if len(mapping) == 0:
raise ValueError('Mapping is empty')
# If the symbols file contains key codes that are not present in
# the keycodes file, THE WHOLE X SESSION WILL CRASH!
if not os.path.exists(KEYCODES_PATH):
raise FileNotFoundError('Expected the keycodes file to exist')
with open(KEYCODES_PATH, 'r') as f:
keycodes = re.findall(r'<.+?>', f.read())
xkb_symbols = []
for keycode, character in mapping:
if f'<{keycode}>' not in keycodes:
logger.error(f'Unknown code <{keycode}> for "{character}"')
# don't append that one, otherwise X would crash when loading
continue
xkb_symbols.append(
f'key <{keycode}> {{ [ {character} ] }}; '
)
if len(xkb_symbols) == 0:
logger.error('Failed to populate xkb_symbols')
return None
template_path = get_data_path('xkb_symbols_template')
with open(template_path, 'r') as template_file:
template = template_file.read()
result = template.format(
name='key-mapper',
xkb_symbols='\n '.join(xkb_symbols)
)
return result
def find_all_used_capabilities():
"""Find all capabilities of all devices that are already in use."""
base = '/dev/input'
inputs = os.listdir(base)
result = []
for input in inputs:
try:
device = evdev.InputDevice(os.path.join(base, input))
for codes in device.capabilities().values():
for code in codes:
if not isinstance(code, int):
continue
code += 8
if code not in result:
result.append(code)
except OSError:
pass
return result
find_all_used_capabilities()
def initialize():
"""Prepare all files and objects that are needed."""
# this mapping represents the xmodmap output, which stays constant
# TODO verify that this is the system default and not changed when I
# setxkbmap my mouse
parse_xmodmap(system_mapping)
# find keycodes that are unused in xmodmap.
i = 8
# used_codes = find_all_used_capabilities()
while len(capabilities) < len(system_mapping):
if system_mapping.get_character(i) is None:
capabilities.append(i)
# if i not in used_codes:
# capabilities.append(i)
i += 1
# basically copy the xmodmap system mapping into another one, but
# with keycodes that don't conflict, so that I'm free to use them
# whenever I want without worrying about my keyboards "1" and my
# mouses "whatever" to clash.
for i, (_, character) in enumerate(system_mapping):
internal_mapping.change(
previous_keycode=None,
new_keycode=capabilities[i],
character=character
)
"""# now take all holes between 8 and the maximum keycode in internal_mapping
# and fill them with none
for i in range(8, capabilities[-1]):
if i in capabilities:
continue
capabilities.append(i)
internal_mapping.change(
previous_keycode=None,
new_keycode=i,
character='none'
)"""
# assert len(system_mapping) == len(internal_mapping)
logger.debug('Prepared the internal mapping')
# Specify "keycode 300 belongs to mapping <300>", which is then used
# to map keycode 300 to a character.
create_identity_mapping(capabilities)
# now put the internal_mapping into a symbols file, which is applied
# on key-mappers own /dev input.
with open(SYMBOLS_PATH, 'w') as f:
contents = generate_symbols(internal_mapping)
if contents is not None:
f.write(contents)
logger.debug('Wrote symbols file %s', SYMBOLS_PATH)