overthrowing the functionality completely

pull/14/head
sezanzeb 4 years ago
parent 9bf255c42b
commit 151a1f3173

@ -1,7 +1,7 @@
# The problems with overwriting keys
Branches for all that stuff exist to archive it instead of loosing it forever.
Look for branches called "fifth", "fourth", etc.
Branches for some of that stuff exist to archive it instead of loosing it
forever.
**Initial target** You write a symbols file based on your specified mapping,
and that's pretty much it. There were two mappings: The first one is in the
@ -13,13 +13,15 @@ and writes 1/! on keycode 10, then you would not be able to write ! by
pressing that mouse button and that keyboard button at the same time.
Keycodes may not clash.
This was quite mature, pretty much finished.
**The second idea** was to write special keycodes known only to key-mapper
(256 - 511) into the input device of your mouse in /dev/input, and map
those to SHIFT and such, whenever a button is clicked. A mapping would have
existed to prevent the original keycode 10 from writing a 1. But X/Linux seem
to ignore anything greater than 255 for regular keyboard events, or even
crash in some cases. Mouse click buttons can use those high keycodes though,
but they cannot be remapped, which I guess is another indicator of that.
but they cannot be remapped.
**The third idea** is to create a new input device that uses 8 - 255, just
like other layouts, and key-mapper always tries to use the same keycodes for
@ -39,7 +41,8 @@ SHIFT as already used in the system default. The pipeline is like this:
But this is a rather complicated approach. The mapping of 10 -> 50 would
have to be stored somewhere as well. It would make the mess of configuration
files already needed for xkb even worse.
files already needed for xkb even worse. This idea was not considered for
long, so no "third" branch exists.
**Fourth idea**: Based on the first idea, instead of using keycodes greater
than 255, use unused keycodes starting from 255, going down. Issues existed
@ -52,22 +55,10 @@ linux just completely ignores some keycodes. 140 works, 145 won't, 150 works.
**Fifth idea**: Instead of writing xkb symbol files, just disable all
mouse buttons with a single symbol file. Key-mapper listens for key events
in /dev and then writes the mapped keycode into /dev. For example, if 10
should be mapped to Shift_L, xkb configs would disable key 10 and key-mapper
would write 50 into /dev, which is Shift_L in xmodmaps output. This sounds
incredibly simple and makes me throw away tons of code.
# The various mappings
There were two mappings: The first one is in the keycodes file and contains
"<10> = 10", which is super redundant but needed for xkb. The second one
mapped "<10>" to characters, modifiers, etc. using symbol files in xkb.
The third mapping reads the input keycodes from your mouse (also known as
system_keycode here) and writes a different one into /dev (also known as
target_keycode here). It is explained above why.
in /dev and then writes the mapped keycode into a new device in /dev. For
example, if 10 should be mapped to Shift_L, xkb configs would disable
key 10 and key-mapper would write 50 into /dev, which is Shift_L in xmodmaps
output. This sounds incredibly simple and makes me throw away tons of code.
# How I would have liked it to be

@ -19,11 +19,9 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
"""Stuff that interacts with the X Server, be it commands or config files.
"""Quite some code that is not used anymore, but might be in the future.
TODO create a base class to standardize the interface if a different
display server should be supported. Or does wayland use the same
config files?
Currently it is not needed to create symbols files in xkb. Very sad.
Resources:
[1] https://wiki.archlinux.org/index.php/Keyboard_input
@ -35,19 +33,19 @@ Resources:
import os
import re
import stat
import subprocess
from keymapper.paths import get_usr_path, KEYCODES_PATH, DEFAULT_SYMBOLS, \
X11_SYMBOLS
from keymapper.logger import logger, is_debug
from keymapper.logger import logger
from keymapper.data import get_data_path
from keymapper.linux import get_devices
from keymapper.mapping import custom_mapping, system_mapping, \
Mapping, MIN_KEYCODE, MAX_KEYCODE
from keymapper.mapping import custom_mapping, system_mapping, Mapping
permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH
MAX_KEYCODE = 255
MIN_KEYCODE = 8
def create_preset(device, name=None):
"""Create an empty preset and return the potentially incremented name.
@ -119,75 +117,7 @@ def get_preset_name(device, preset=None):
DEFAULT_SYMBOLS_NAME = get_preset_name('default')
def apply_preset(device, preset):
"""Apply a preset to the device.
Parameters
----------
device : string
preset : string
"""
layout = get_preset_name(device, preset)
setxkbmap(device, layout)
def get_system_layout():
"""Get the system wide configured default keyboard layout."""
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 setxkbmap(device, layout):
"""Apply a preset to the device.
Parameters
----------
device : string
layout : string or None
For example 'de', passed to setxkbmap unmodified. If None, will
load the system default
"""
if layout is not None:
path = os.path.join(X11_SYMBOLS, layout)
if not os.path.exists(path):
logger.error('Symbols %s don\'t exist', path)
return
with open(path, 'r') as f:
if f.read() == '':
logger.error('Tried to load empty symbols %s', 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()]
else:
cmd = ['setxkbmap', '-layout', layout, '-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
continue
device_cmd = cmd + ['-device', str(xinput_id)]
logger.debug('Running `%s`', ' '.join(device_cmd))
subprocess.run(device_cmd, capture_output=(not is_debug()))
EMPTY_SYMBOLS_NAME = get_preset_name('empty')
def create_identity_mapping():
@ -197,15 +127,15 @@ def create_identity_mapping():
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 = []
maximum = MAX_KEYCODE
minimum = MIN_KEYCODE
for keycode in range(minimum, maximum + 1):
for keycode in range(MIN_KEYCODE, MAX_KEYCODE + 1):
xkb_keycodes.append(f'<{keycode}> = {keycode};')
template_path = get_data_path('xkb_keycodes_template')
@ -213,8 +143,8 @@ def create_identity_mapping():
template = template_file.read()
result = template.format(
minimum=minimum,
maximum=maximum,
minimum=MIN_KEYCODE,
maximum=MAX_KEYCODE,
xkb_keycodes='\n '.join(xkb_keycodes)
)
@ -229,7 +159,7 @@ def create_identity_mapping():
def generate_symbols(
name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping
name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping
):
"""Create config contents to be placed in /usr/share/X11/xkb/symbols.
@ -304,24 +234,6 @@ def generate_symbols(
return result
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_symbols_file(device, preset):
"""Parse a symbols file populate the mapping.
@ -344,7 +256,8 @@ def parse_symbols_file(device, preset):
# 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.
# 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+)',
@ -355,31 +268,11 @@ def parse_symbols_file(device, preset):
custom_mapping.change(
previous_keycode=None,
new_keycode=system_keycode,
character=character,
target_keycode=int(target_keycode)
character=character
)
custom_mapping.changed = False
def parse_xmodmap():
"""Read the output of xmodmap as a Mapping object."""
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(
previous_keycode=None,
new_keycode=int(keycode),
character=', '.join(characters.split()),
target_keycode=None
)
# TODO verify that this is the system default and not changed when I
# setxkbmap my mouse
parse_xmodmap()
def create_default_symbols():
"""Parse the output of xmodmap and create a default symbols file.

@ -0,0 +1,145 @@
#!/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.paths import X11_SYMBOLS
from keymapper.logger import logger, is_debug
from keymapper.getdevices import get_devices
from keymapper.mapping import system_mapping
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 setxkbmap(device, layout):
"""Apply a preset to the device.
Parameters
----------
device : string
layout : string or None
For example 'de', passed to setxkbmap unmodified. If None, will
load the system default
"""
if layout is not None:
path = os.path.join(X11_SYMBOLS, layout)
if not os.path.exists(path):
logger.error('Symbols %s don\'t exist', path)
return
with open(path, 'r') as f:
if f.read() == '':
logger.error('Tried to load empty symbols %s', 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']
# 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']
# 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 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():
"""Read the output of xmodmap as a Mapping object."""
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(
previous_keycode=None,
new_keycode=int(keycode),
character=characters.split()
)
# TODO verify that this is the system default and not changed when I
# setxkbmap my mouse
parse_xmodmap()

@ -0,0 +1,106 @@
#!/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/>.
"""Device and evdev stuff that is independent from the display server."""
import multiprocessing
import evdev
from keymapper.logger import logger
_devices = None
class GetDevicesProcess(multiprocessing.Process):
"""Process to get the devices that can be worked with.
Since InputDevice destructors take quite some time, do this
asynchronously so that they can take as much time as they want without
slowing down the initialization. To avoid evdevs asyncio stuff spamming
errors, do this with multiprocessing and not multithreading.
TODO to threading, make eventloop
"""
def __init__(self, pipe):
"""Construct the process.
Parameters
----------
pipe : multiprocessing.Pipe
used to communicate the result
"""
self.pipe = pipe
super().__init__()
def run(self):
"""Do what get_devices describes."""
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
# group them together by usb device because there could be stuff like
# "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control"
grouped = {}
for device in devices:
# only keyboard devices
# https://www.kernel.org/doc/html/latest/input/event-codes.html
if evdev.ecodes.EV_KEY not in device.capabilities().keys():
continue
usb = device.phys.split('/')[0]
if grouped.get(usb) is None:
grouped[usb] = []
grouped[usb].append((device.name, device.path))
# now write down all the paths of that group
result = {}
for group in grouped.values():
names = [entry[0] for entry in group]
devs = [entry[1] for entry in group]
shortest_name = sorted(names, key=len)[0]
result[shortest_name] = {
'paths': devs,
'devices': names
}
self.pipe.send(result)
def get_devices():
"""Group devices and get relevant infos per group.
Returns a list containing mappings of
{group_name: {paths: [paths], devices: [names]} for input devices.
For example, group_name could be "Logitech USB Keyboard", devices might
contain "Logitech USB Keyboard System Control" and "Logitech USB Keyboard".
paths is a list of files in /dev/input that belong to the devices.
They are grouped by usb port.
"""
global _devices
if _devices is None:
pipe = multiprocessing.Pipe()
GetDevicesProcess(pipe[1]).start()
# block until devices are available
_devices = pipe[0].recv()
logger.info('Found %s', ', '.join([f'"{name}"' for name in _devices]))
return _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, DONTMAP, GENERATE
from keymapper.mapping import custom_mapping
from keymapper.logger import logger
@ -72,8 +72,7 @@ class Row(Gtk.ListBoxRow):
custom_mapping.change(
previous_keycode=None,
new_keycode=keycode,
character=character,
target_keycode=GENERATE
character=character
)
def on_key_pressed(self, button, event):
@ -113,8 +112,7 @@ class Row(Gtk.ListBoxRow):
custom_mapping.change(
previous_keycode=previous_keycode,
new_keycode=new_keycode,
character=character,
target_keycode=GENERATE
character=character
)
def put_together(self, keycode, character):

@ -28,13 +28,13 @@ gi.require_version('GLib', '2.0')
from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path
from keymapper.X import create_setxkbmap_config, apply_preset, \
create_preset, custom_mapping, system_mapping, parse_symbols_file, \
setxkbmap
from keymapper.mapping import custom_mapping
from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset
from keymapper.logger import logger
from keymapper.linux import get_devices, KeycodeReader
from keymapper.linux import KeycodeReader
from keymapper.cli import setxkbmap
from keymapper.getdevices import get_devices
from keymapper.gtk.row import Row
from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK
@ -164,7 +164,9 @@ class Window:
presets = get_presets(device)
self.get('preset_name_input').set_text('')
if len(presets) == 0:
presets = [create_preset(device)]
# presets = [create_preset(device)]
# TODO create one empty preset
presets = []
else:
logger.debug('Presets for "%s": %s', device, ', '.join(presets))
preset_selection = self.get('preset_selection')
@ -243,12 +245,11 @@ class Window:
self.selected_preset,
self.selected_device
)
apply_preset(self.selected_device, self.selected_preset)
self.get('status_bar').push(
CTX_APPLY,
f'Applied "{self.selected_preset}"'
)
keycode_reader = KeycodeReader(self.selected_device)
KeycodeReader(self.selected_device)
def on_select_device(self, dropdown):
"""List all presets, create one if none exist yet."""
@ -275,7 +276,10 @@ class Window:
return
try:
new_preset = create_preset(self.selected_device)
# new_preset = create_preset(self.selected_device)
# TODO create a preset file, tell custom_mapping to do so
# or something
new_preset = 'new_preset'
self.get('preset_selection').append(new_preset, new_preset)
self.get('preset_selection').set_active_id(new_preset)
except PermissionError as e:
@ -300,7 +304,7 @@ class Window:
logger.debug('Selecting preset "%s"', preset)
self.selected_preset = preset
parse_symbols_file(self.selected_device, self.selected_preset)
# TODO load config into custom_mapping
key_list = self.get('key_list')
for keycode, output in custom_mapping:
@ -345,10 +349,7 @@ class Window:
self.selected_preset
)
create_setxkbmap_config(
self.selected_device,
self.selected_preset
)
# TODO tell the mapping to dump itself as JSON somewhere
custom_mapping.changed = False
self.unhighlight_all_rows()

@ -23,17 +23,16 @@
import subprocess
import multiprocessing
import time
import threading
import asyncio
import evdev
from keymapper.logger import logger
from keymapper.mapping import custom_mapping, MAX_KEYCODE, MIN_KEYCODE
DEVNODE = '/dev/keymapper'
from keymapper.cli import apply_empty_symbols
from keymapper.getdevices import get_devices
from keymapper.mapping import custom_mapping, system_mapping
def can_grab(path):
@ -67,15 +66,14 @@ class KeycodeReader:
pass
def start_injecting_worker(self, path):
"""Inject keycodes for one of the virtual devices.
This depends on a setxkbmap-loaded symbol file that contains
the mappings for keycodes in the range of MIN and MAX_KEYCODE.
"""
"""Inject keycodes for one of the virtual devices."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
device = evdev.InputDevice(path)
keymapper_device = evdev.UInput(devnode=DEVNODE)
keymapper_device = evdev.UInput(
name='key-mapper',
phys='key-mapper-uinput'
)
for event in device.read_loop():
if event.type != evdev.ecodes.EV_KEY:
@ -85,24 +83,38 @@ class KeycodeReader:
# than the ones reported by xev and that X expects
input_keycode = event.code + 8
if custom_mapping.get_keycode(input_keycode) is None:
# unknown keycode, skip
continue
target_keycode = custom_mapping.get_keycode(input_keycode)
if target_keycode > MAX_KEYCODE or target_keycode < MIN_KEYCODE:
continue
print('read', input_keycode, 'write', target_keycode, path)
character = custom_mapping.get_character(input_keycode)
if character is None:
# unknown keycode, forward it
target_keycode = input_keycode
else:
target_keycode = system_mapping.get_keycode(character)
if target_keycode is None:
logger.error(
'Cannot find character %s in xmodmap',
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
time.sleep(0.005)
# TODO test for the stuff put into write
keymapper_device.write(evdev.ecodes.EV_KEY, target_keycode, event.value)
keymapper_device.write(evdev.ecodes.EV_KEY, target_keycode - 8, event.value)
keymapper_device.syn()
def start_injecting(self):
"""Read keycodes and inject the mapped character forever."""
paths = _devices[self.device]['paths']
paths = get_devices()[self.device]['paths']
logger.debug(
'Starting injecting the mapping for %s on %s',
@ -110,6 +122,8 @@ class KeycodeReader:
', '.join(paths)
)
apply_empty_symbols(self.device)
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
threading.Thread(
@ -131,80 +145,3 @@ class KeycodeReader:
# than the ones reported by xev
newest_keycode = event.code + 8
return newest_keycode
_devices = None
class GetDevicesProcess(multiprocessing.Process):
"""Process to get the devices that can be worked with.
Since InputDevice destructors take quite some time, do this
asynchronously so that they can take as much time as they want without
slowing down the initialization. To avoid evdevs asyncio stuff spamming
errors, do this with multiprocessing and not multithreading.
TODO to threading, make eventloop
"""
def __init__(self, pipe):
"""Construct the process.
Parameters
----------
pipe : multiprocessing.Pipe
used to communicate the result
"""
self.pipe = pipe
super().__init__()
def run(self):
"""Do what get_devices describes."""
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
# group them together by usb device because there could be stuff like
# "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control"
grouped = {}
for device in devices:
# only keyboard devices
# https://www.kernel.org/doc/html/latest/input/event-codes.html
if evdev.ecodes.EV_KEY not in device.capabilities().keys():
continue
usb = device.phys.split('/')[0]
if grouped.get(usb) is None:
grouped[usb] = []
grouped[usb].append((device.name, device.path))
# now write down all the paths of that group
result = {}
for group in grouped.values():
names = [entry[0] for entry in group]
devs = [entry[1] for entry in group]
shortest_name = sorted(names, key=len)[0]
result[shortest_name] = {
'paths': devs,
'devices': names
}
self.pipe.send(result)
def get_devices():
"""Group devices and get relevant infos per group.
Returns a list containing mappings of
{group_name: {paths: [paths], devices: [names]} for input devices.
For example, group_name could be "Logitech USB Keyboard", devices might
contain "Logitech USB Keyboard System Control" and "Logitech USB Keyboard".
paths is a list of files in /dev/input that belong to the devices.
They are grouped by usb port.
"""
global _devices
if _devices is None:
pipe = multiprocessing.Pipe()
GetDevicesProcess(pipe[1]).start()
# block until devices are available
_devices = pipe[0].recv()
logger.info('Found %s', ', '.join([f'"{name}"' for name in _devices]))
return _devices

@ -25,43 +25,6 @@
from keymapper.logger import logger
# if MIN_KEYCODE < 255 and MAX_KEYCODE > 255: X crashes
# the maximum specified in /usr/share/X11/xkb/keycodes is usually 255
# and the minimum 8
MAX_KEYCODE = 255
MIN_KEYCODE = 8
# modes for change:
GENERATE = -1
DONTMAP = None
def get_input_keycode(keycode):
"""Same as get_output_keycode, but vice versa."""
return keycode - MIN_KEYCODE
def get_target_keycode():
# see HELP.md
for keycode in range(MAX_KEYCODE, MIN_KEYCODE - 1, -1):
# starting from the MAX_KEYCODE, find the first keycode that is
# unused in both custom_mapping and system_mapping.
if not (custom_mapping.has(keycode) or system_mapping.has(keycode)):
return keycode
# no unused keycode found, take the highest keycode that is unused
# in the current custom_mapping.
for keycode in range(MAX_KEYCODE, MIN_KEYCODE - 1, -1):
# starting from the MAX_KEYCODE, find the first keycode that is
# unused in both custom_mapping and system_mapping.
if not (custom_mapping.has(keycode)):
return keycode
logger.error('All %s keycodes are mapped!', MAX_KEYCODE - MIN_KEYCODE)
return None
class Mapping:
"""Contains and manages mappings.
@ -69,9 +32,6 @@ class Mapping:
character.
"""
def __init__(self):
# TODO this is a stupid data structure if there are two keys
# that should be unique individually. system_keycode and
# target_keycode. two _mapping objects maybe?
self._mapping = {}
self.changed = False
@ -82,22 +42,7 @@ class Mapping:
def __len__(self):
return len(self._mapping)
def find_keycode(self, character, case=False):
"""For a given character, find the used keycodes in the mapping."""
# TODO test
if not case:
character = character.lower()
for keycode, (mapped_keycode, mapped_character) in self._mapping:
# keycode is what the system would use for that key,
# mapped_keycode is what we use instead by writing into /dev,
# and mapped_character is what we expect to appear.
# mapped_character might be multiple things, like "a, A"
if not case:
mapped_character = mapped_character.lower()
if character in [c.strip() for c in mapped_character.split(',')]:
return keycode, mapped_keycode
def change(self, previous_keycode, new_keycode, character, target_keycode):
def change(self, previous_keycode, new_keycode, character):
"""Replace the mapping of a keycode with a different one.
Return True on success.
@ -112,31 +57,20 @@ class Mapping:
The source keycode, what the mouse would report without any
modification.
character : string or string[]
If an array of strings, will put something like { [ a, A ] };
into the symbols file.
target_keycode : int or None
Which keycode should be used for that key instead. If -1,
will figure out a new one. This is for stuff that happens
under the hood and the user won't see this unless they open
config files. If None, will only map new_keycode to character
without any in-between step.
A single character known to xkb, Examples: KP_1, Shift_L, a, B.
Can also be an array, which is used for reading the xkbmap output
completely.
"""
try:
new_keycode = int(new_keycode)
if target_keycode is not None:
target_keycode = int(target_keycode)
if previous_keycode is not None:
previous_keycode = int(previous_keycode)
except ValueError:
logger.error('Can only use numbers as keycodes')
return False
# TODO test
if target_keycode == GENERATE:
target_keycode = get_target_keycode()
if new_keycode and character:
self._mapping[new_keycode] = (target_keycode, str(character))
self._mapping[new_keycode] = character
if new_keycode != previous_keycode:
# clear previous mapping of that code, because the line
# representing that one will now represent a different one.
@ -162,9 +96,18 @@ class Mapping:
self._mapping = {}
self.changed = True
def get_keycode(self, keycode):
"""Read the output keycode that is mapped to this input keycode."""
return self._mapping.get(keycode, (None, None))[0]
def get_keycode(self, character):
"""Get the keycode for that character."""
# TODO prepare this with .lower() instead to make it faster
character = character.lower()
for keycode, mapping in self._mapping.items():
if isinstance(mapping, list):
if character in [c.lower() for c in mapping]:
return keycode
elif mapping.lower() == character:
return int(keycode)
return None
def get_character(self, keycode):
"""Read the character that is mapped to this keycode.
@ -173,22 +116,13 @@ class Mapping:
----------
keycode : int
"""
return self._mapping.get(keycode, (None, None))[1]
return self._mapping.get(keycode)
def has(self, keycode):
"""Check if this keycode is going to be a line in the symbols file."""
# TODO test
if self._mapping.get(keycode) is not None:
# the keycode that is disabled, because it is mapped to
# something else
return True
for _, (target_keycode, _) in self._mapping.items():
if target_keycode == keycode:
# the keycode that is actually being mapped
return True
return False
"""Check if this keycode is going to be a line in the symbols file.
TODO no symbols files anymore ^
"""
return self._mapping.get(keycode) is not None
# one mapping object for the whole application that holds all

@ -60,3 +60,4 @@ def get_usr_path(device=None, preset=None):
DEFAULT_SYMBOLS = get_usr_path('default')
EMPTY_SYMBOLS = get_usr_path('empty')

@ -28,7 +28,7 @@ import glob
from keymapper.paths import get_usr_path, USERS_SYMBOLS
from keymapper.logger import logger
from keymapper.linux import get_devices
from keymapper.getdevices import get_devices
def get_presets(device):

@ -22,12 +22,12 @@
import re
import unittest
from keymapper.X import get_system_layout
from keymapper.cli import get_system_layout_locale
class Test(unittest.TestCase):
def test_get_system_layout(self):
layout = get_system_layout()
def test_get_system_layout_locale(self):
layout = get_system_layout_locale()
self.assertGreater(len(layout), 0)
# should be all alphanumeric
match = re.findall(r'\w+', layout)

@ -23,7 +23,7 @@ import os
import unittest
import shutil
from keymapper.X import custom_mapping, generate_symbols, \
from keymapper.xkb import custom_mapping, generate_symbols, \
create_identity_mapping, create_setxkbmap_config, \
get_preset_name, create_default_symbols, parse_symbols_file
from keymapper.paths import get_usr_path, KEYCODES_PATH, USERS_SYMBOLS

@ -26,7 +26,7 @@ import time
from keymapper.presets import find_newest_preset, rename_preset, \
get_any_preset, delete_preset
from keymapper.X import create_preset
from keymapper.xkb import create_preset
from keymapper.paths import USERS_SYMBOLS
from test import tmp

@ -21,7 +21,7 @@
import unittest
from keymapper.linux import get_devices
from keymapper.getdevices import get_devices
from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, \
DEFAULT_SYMBOLS

Loading…
Cancel
Save