mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-02 15:40:19 +00:00
works
This commit is contained in:
parent
d02e3eaa13
commit
b9787518cf
@ -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():
|
||||
|
@ -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')
|
146
keymapper/cli.py
146
keymapper/cli.py
@ -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
|
||||
)
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user