still wont work. again.

pull/14/head
sezanzeb 4 years ago
parent efe7a38fae
commit 37bc7b5d69

@ -35,6 +35,7 @@ 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__':
@ -49,6 +50,8 @@ if __name__ == '__main__':
update_verbosity(options.debug)
log_info()
initialize()
window = Window()
def stop_injecting():

@ -0,0 +1,4 @@
default
xkb_symbols "basic" {
name[Group1]="key-mapper/empty";
};

@ -4,7 +4,6 @@
// /usr/share/X11/xkb/keycodes/key-mapper
default xkb_symbols "basic" {{
{include}
name[Group1] = "{name}";
{xkb_symbols}
}};

@ -37,7 +37,8 @@ import stat
from keymapper.logger import logger
from keymapper.data import get_data_path
from keymapper.mapping import custom_mapping, system_mapping, Mapping
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
@ -55,10 +56,6 @@ USERS_SYMBOLS = os.path.join(
os.getlogin().replace(' ', '_')
)
# those are the same for every preset and user, they are needed to make the
# presets work.
KEYCODES_PATH = '/usr/share/X11/xkb/keycodes/key-mapper'
def get_usr_path(device=None, preset=None):
"""Get the path to the config file in /usr.
@ -111,6 +108,19 @@ def create_preset(device, name=None):
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.
@ -143,134 +153,6 @@ def create_setxkbmap_config(device, preset):
f.write(contents)
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_identity_mapping():
"""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>"
This has the added benefit that keycodes reported by xev can be
identified in the symbols file.
The identity mapping is provided to '-keycodes' of setxkbmap.
"""
if os.path.exists(KEYCODES_PATH):
logger.debug('Found the keycodes file at %s', KEYCODES_PATH)
return
xkb_keycodes = []
for keycode in range(MIN_KEYCODE, MAX_KEYCODE + 1):
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 keycodes:
keycodes.write(result)
def generate_symbols(
name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_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
----------
name : string
Usually what `get_preset_name` returns
include : string or None
If another preset should be included. Defaults to the default
preset. Use None to avoid including.
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 system_keycode, (target_keycode, character) in mapping:
if f'<{system_keycode}>' not in keycodes:
logger.error(f'Unknown code <{system_keycode}> for "{character}"')
# don't append that one, otherwise X would crash when loading
continue
# key-mapper will write target_keycode into /dev, while
# system_keycode should do nothing to avoid a duplicate keystroke.
if target_keycode is not None:
if f'<{target_keycode}>' not in keycodes:
logger.error(
f'Unknown code <{target_keycode}> for "{character}"'
)
# don't append that one, otherwise X would crash when loading
continue
xkb_symbols.append(
f'key <{system_keycode}> {{ [ ] }}; '
)
xkb_symbols.append(
f'key <{target_keycode}> {{ [ {character} ] }}; '
f'// {system_keycode}'
)
continue
xkb_symbols.append(
f'key <{system_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=name,
xkb_symbols='\n '.join(xkb_symbols),
include=f'include "{include}"' if include else ''
)
return result
def parse_symbols_file(device, preset):
"""Parse a symbols file populate the mapping.

@ -28,7 +28,6 @@ import subprocess
from keymapper.logger import logger, is_debug
from keymapper.getdevices import get_devices
from keymapper.mapping import system_mapping
def get_system_layout_locale():
@ -47,61 +46,60 @@ def get_system_layout_locale():
][0].split(': ')[-1]
def setxkbmap(device, layout=None):
"""Apply a preset to the device.
def apply_symbols(device, name=None, keycodes=None):
"""Apply a symbols configuration to the device.
Parameters
----------
device : string
layout : string or None
For example 'de', passed to setxkbmap unmodified. If None, will
load the system default
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 layout is not None:
path = os.path.join('/usr/share/X11/xkb/symbols', layout)
if not os.path.exists(path):
logger.error('Symbols %s don\'t exist', path)
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
with open(path, 'r') as f:
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('Tried to load empty symbols %s', path)
logger.error('Found empty keycodes "%s"', keycodes_path)
return
logger.info('Applying layout "%s" on device %s', layout, device)
group = get_devices()[device]
if layout is None:
cmd = ['setxkbmap', '-layout', get_system_layout_locale()]
else:
cmd = ['setxkbmap', '-layout', layout, '-keycodes', 'key-mapper']
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.
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))
subprocess.run(device_cmd, capture_output=(not is_debug()))
def apply_empty_symbols(device):
"""Make the device not write any character anymore."""
logger.debug('Applying the empty symbols to %s', device)
group = get_devices()[device]
cmd = [
'setxkbmap',
'-layout', 'key-mapper/empty',
# '-keycodes', 'key-mapper'
]
# 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.
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
@ -109,7 +107,10 @@ def apply_empty_symbols(device):
device_cmd = cmd + ['-device', str(xinput_id)]
logger.debug('Running `%s`', ' '.join(device_cmd))
subprocess.run(device_cmd, capture_output=(not is_debug()))
output = subprocess.run(device_cmd, capture_output=True)
output = output.stderr.decode().strip()
if output != '':
logger.debug2(output)
def get_xinput_id_mapping():
@ -130,19 +131,16 @@ def get_xinput_id_mapping():
return zip(names, ids)
def parse_xmodmap():
"""Read the output of xmodmap as a Mapping object."""
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:
system_mapping.change(
# this is the "array" format needed for symbols files
character = ', '.join(characters.split())
mapping.change(
previous_keycode=None,
new_keycode=int(keycode),
character=characters.split()
character=character
)
# TODO verify that this is the system default and not changed when I
# setxkbmap my mouse
parse_xmodmap()

@ -32,7 +32,7 @@ from keymapper.logger import logger
_devices = None
class GetDevicesProcess(multiprocessing.Process):
class _GetDevicesProcess(multiprocessing.Process):
"""Process to get the devices that can be worked with.
Since InputDevice destructors take quite some time, do this
@ -84,6 +84,13 @@ class GetDevicesProcess(multiprocessing.Process):
self.pipe.send(result)
def refresh_devices():
"""Get new devices, e.g. new ones created by key-mapper."""
global _devices
_devices = None
return get_devices()
def get_devices():
"""Group devices and get relevant infos per group.
@ -99,7 +106,7 @@ def get_devices():
global _devices
if _devices is None:
pipe = multiprocessing.Pipe()
GetDevicesProcess(pipe[1]).start()
_GetDevicesProcess(pipe[1]).start()
# block until devices are available
_devices = pipe[0].recv()
logger.info('Found %s', ', '.join([f'"{name}"' for name in _devices]))

@ -27,7 +27,7 @@ gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from keymapper.mapping import custom_mapping
from keymapper.state import custom_mapping
from keymapper.logger import logger

@ -30,12 +30,12 @@ gi.require_version('GLib', '2.0')
from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path
from keymapper.mapping import custom_mapping
from keymapper.state import custom_mapping
from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset, get_available_preset_name
from keymapper.logger import logger
from keymapper.linux import KeycodeInjector
from keymapper.cli import setxkbmap
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 +208,7 @@ class Window:
"""Load the mapping."""
if self.keycode_reader is not None:
self.keycode_reader.stop_injecting()
setxkbmap(self.selected_device)
apply_symbols(self.selected_device)
self.get('status_bar').push(
CTX_APPLY,
f'Applied the system default'

@ -30,9 +30,13 @@ import asyncio
import evdev
from keymapper.logger import logger
from keymapper.cli import apply_empty_symbols, setxkbmap
from keymapper.getdevices import get_devices
from keymapper.mapping import custom_mapping, system_mapping
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
DEV_NAME = 'key-mapper'
def can_grab(path):
@ -55,6 +59,50 @@ class KeycodeInjector:
self.processes = []
self.start_injecting()
def start_injecting(self):
"""Read keycodes and inject the mapped character forever."""
self.stop_injecting()
paths = get_devices()[self.device]['paths']
logger.info(
'Starting injecting the mapping for %s on %s',
self.device,
', '.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:
worker = multiprocessing.Process(
target=self._start_injecting_worker,
args=(path, custom_mapping)
)
worker.start()
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
logger.info('Stopping injecting keycodes')
for i, process in enumerate(self.processes):
if process is None:
continue
if process.is_alive():
process.terminate()
self.processes[i] = None
# apply the default layout back
apply_symbols(self.device)
def _start_injecting_worker(self, path, mapping):
"""Inject keycodes for one of the virtual devices."""
# TODO test
@ -63,8 +111,11 @@ class KeycodeInjector:
device = evdev.InputDevice(path)
# foo = evdev.InputDevice('/dev/input/event2')
keymapper_device = evdev.UInput(
name='key-mapper',
phys='key-mapper-uinput'
name=DEV_NAME,
phys='key-mapper-uinput',
events={
evdev.ecodes.EV_KEY: [c - 8 for c in capabilities]
}
)
logger.debug(
@ -76,7 +127,9 @@ class KeycodeInjector:
if event.type != evdev.ecodes.EV_KEY:
continue
print('got', event.code, event.value, 'from device')
if event.code == 2:
# linux does them itself, no need to trigger them
continue
# this happens to report key codes that are 8 lower
# than the ones reported by xev and that X expects
@ -86,13 +139,12 @@ class KeycodeInjector:
if character is None:
# unknown keycode, forward it
target_keycode = input_keycode
continue
else:
target_keycode = system_mapping.get_keycode(character)
target_keycode = internal_mapping.get_keycode(character)
if target_keycode is None:
logger.error(
'Cannot find character %s in xmodmap',
'Cannot find character %s in the internal mapping',
character
)
continue
@ -106,24 +158,15 @@ class KeycodeInjector:
# fine. I came up with that after randomly poking around in,
# frustration. I don't know of any helpful resource that
# explains this
time.sleep(0.01)
"""if event.value == 2:
print('device simulated up', event.value, 0)
device.write(
evdev.ecodes.EV_KEY,
event.code,
0
)
device.write(evdev.ecodes.EV_SYN, evdev.ecodes.SYN_REPORT, 0)"""
# TODO still needed? if no, add to HELP.md
time.sleep(0.015)
logger.debug2(
'got code:%s value:%s, maps to code:%s char:%s',
event.code + 8, event.value, target_keycode, character
)
# TODO test for the stuff put into write
"""logger.debug(
'Injecting %s -> %s -> %s',
input_keycode,
character,
target_keycode,
)"""
print('km write', target_keycode - 8, event.value)
keymapper_device.write(
evdev.ecodes.EV_KEY,
target_keycode - 8,
@ -145,41 +188,3 @@ class KeycodeInjector:
# foo.write(evdev.ecodes.EV_SYN, evdev.ecodes.SYN_REPORT, 0)
keymapper_device.syn()
def start_injecting(self):
"""Read keycodes and inject the mapped character forever."""
self.stop_injecting()
paths = get_devices()[self.device]['paths']
logger.info(
'Starting injecting the mapping for %s on %s',
self.device,
', '.join(paths)
)
apply_empty_symbols(self.device)
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
worker = multiprocessing.Process(
target=self._start_injecting_worker,
args=(path, custom_mapping)
)
worker.start()
self.processes.append(worker)
def stop_injecting(self):
"""Stop injecting keycodes."""
# TODO test
logger.info('Stopping injecting keycodes')
for i, process in enumerate(self.processes):
if process is None:
continue
if process.is_alive():
process.terminate()
self.processes[i] = None
# apply the default layout back
setxkbmap(self.device)

@ -27,10 +27,21 @@ import logging
import pkg_resources
def debug2(self, message, *args, **kws):
if self.isEnabledFor(DEBUG2):
# https://stackoverflow.com/a/13638084
self._log(DEBUG2, message, args, **kws)
DEBUG2 = 5
logging.addLevelName(DEBUG2, "DEBUG2")
logging.Logger.debug2 = debug2
class Formatter(logging.Formatter):
"""Overwritten Formatter to print nicer logs."""
def format(self, record):
debug = logger.level == logging.DEBUG
debug = logger.level <= logging.DEBUG
if record.levelno == logging.INFO and not debug:
# if not launched with --debug, then don't print "INFO:"
self._style._fmt = '%(message)s' # noqa
@ -42,12 +53,18 @@ class Formatter(logging.Formatter):
logging.ERROR: 31,
logging.FATAL: 31,
logging.DEBUG: 36,
DEBUG2: 34,
logging.INFO: 32,
}.get(record.levelno, 0)
if debug:
self._style._fmt = ( # noqa
f'\033[{color}m%(levelname)s\033[0m: '
'%(filename)s, line %(lineno)d, %(message)s'
'\033[1m' # bold
f'\033[{color}m' # color
f'%(levelname)s'
'\033[0m' # end style
f'\033[{color}m' # color
': %(filename)s, line %(lineno)d, %(message)s'
'\033[0m' # end style
)
else:
self._style._fmt = ( # noqa
@ -61,10 +78,11 @@ handler = logging.StreamHandler()
handler.setFormatter(Formatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logging.getLogger('asyncio').setLevel(logging.WARNING)
def is_debug():
return logger.level == logging.DEBUG
return logger.level <= logging.DEBUG
def log_info():
@ -78,7 +96,7 @@ def log_info():
def update_verbosity(debug):
"""Set the logging verbosity according to the settings object."""
if debug:
logger.setLevel(logging.DEBUG)
logger.setLevel(DEBUG2)
else:
logger.setLevel(logging.INFO)

@ -28,6 +28,7 @@ import shutil
from keymapper.logger import logger
from keymapper.paths import get_config_path
from keymapper.cli import parse_xmodmap
class Mapping:
@ -75,10 +76,6 @@ class Mapping:
return False
if new_keycode and character:
if isinstance(character, list):
character = [c.lower() for c in character]
else:
character = character.lower()
self._mapping[new_keycode] = character
if new_keycode != previous_keycode:
# clear previous mapping of that code, because the line
@ -147,11 +144,11 @@ class Mapping:
def get_keycode(self, character):
"""Get the keycode for that character."""
character = character.lower()
# TODO prepare separate data structure for optimization
for keycode, mapping in self._mapping.items():
# note, that stored mappings are already lowercase
if isinstance(mapping, list):
if character in [c for c in mapping]:
if ', ' in mapping:
if character in [c.strip() for c in mapping.split(',')]:
return keycode
elif mapping == character:
return int(keycode)
@ -166,11 +163,3 @@ class Mapping:
keycode : int
"""
return self._mapping.get(keycode)
# one mapping object for the whole application that holds all
# customizations
custom_mapping = Mapping()
# one mapping that represents the xmodmap output
system_mapping = Mapping()

@ -24,9 +24,15 @@
import os
EMPTY_SYMBOLS = '/usr/share/X11/xkb/symbols/key-mapper-empty'
CONFIG = os.path.join('/home', os.getlogin(), '.config/key-mapper')
# the "identity mapping"
KEYCODES_PATH = '/usr/share/X11/xkb/keycodes/key-mapper'
# to make the device not write its default keys anymore
EMPTY_SYMBOLS = '/usr/share/X11/xkb/symbols/key-mapper-empty'
# to make key-mappers own /dev device have keys
SYMBOLS_PATH = '/usr/share/X11/xkb/symbols/key-mapper-dev'
def get_config_path(device=None, preset=None):
"""Get a path to the stored preset, or to store a preset to."""

@ -0,0 +1,225 @@
#!/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/>.
"""Create some files and objects that are needed for the app to work."""
import os
import re
import stat
import evdev
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
# 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()
system_mapping = Mapping()
capabilities = []
# 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)

@ -31,5 +31,6 @@ DistUtilsExtra.auto.setup(
('share/applications/', ['data/key-mapper.desktop']),
('share/key-mapper/', ['data/xkb_symbols_template']),
('share/polkit-1/actions/', ['data/org.key-mapper.policy']),
('share/X11/xkb/symbols/', ['data/key-mapper-empty']),
],
)

@ -32,7 +32,7 @@ import shutil
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.mapping import custom_mapping
from keymapper.state import custom_mapping
from keymapper.paths import CONFIG
from test import tmp

@ -21,7 +21,7 @@
import unittest
from keymapper.linux import GetDevicesProcess
from keymapper.injector import GetDevicesProcess
class TestLinux(unittest.TestCase):

Loading…
Cancel
Save