diff --git a/HELP.md b/HELP.md
index 665df0d7..c416d682 100644
--- a/HELP.md
+++ b/HELP.md
@@ -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
diff --git a/keymapper/X.py b/keymapper/archive.py
similarity index 70%
rename from keymapper/X.py
rename to keymapper/archive.py
index 4b6b90cb..0a4912ca 100644
--- a/keymapper/X.py
+++ b/keymapper/archive.py
@@ -19,11 +19,9 @@
# along with key-mapper. If not, see .
-"""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.
diff --git a/keymapper/cli.py b/keymapper/cli.py
new file mode 100644
index 00000000..f7a5ae95
--- /dev/null
+++ b/keymapper/cli.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# key-mapper - GUI for device specific keyboard mappings
+# Copyright (C) 2020 sezanzeb
+#
+# 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 .
+
+
+"""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()
diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py
new file mode 100644
index 00000000..a92a2c6b
--- /dev/null
+++ b/keymapper/getdevices.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# key-mapper - GUI for device specific keyboard mappings
+# Copyright (C) 2020 sezanzeb
+#
+# 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 .
+
+
+"""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
diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py
index 6c367ce6..28d04c25 100644
--- a/keymapper/gtk/row.py
+++ b/keymapper/gtk/row.py
@@ -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):
diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py
index ee8cb1fb..cb573d5e 100755
--- a/keymapper/gtk/window.py
+++ b/keymapper/gtk/window.py
@@ -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()
diff --git a/keymapper/linux.py b/keymapper/linux.py
index 8e42305a..13c60cb8 100644
--- a/keymapper/linux.py
+++ b/keymapper/linux.py
@@ -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
diff --git a/keymapper/mapping.py b/keymapper/mapping.py
index 2f48196a..abbfb8d5 100644
--- a/keymapper/mapping.py
+++ b/keymapper/mapping.py
@@ -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
diff --git a/keymapper/paths.py b/keymapper/paths.py
index 2e23274d..af858592 100644
--- a/keymapper/paths.py
+++ b/keymapper/paths.py
@@ -60,3 +60,4 @@ def get_usr_path(device=None, preset=None):
DEFAULT_SYMBOLS = get_usr_path('default')
+EMPTY_SYMBOLS = get_usr_path('empty')
diff --git a/keymapper/presets.py b/keymapper/presets.py
index 15ca36db..2161ea6c 100644
--- a/keymapper/presets.py
+++ b/keymapper/presets.py
@@ -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):
diff --git a/tests/testcases/X.py b/tests/testcases/X.py
index 91ba198a..97167010 100644
--- a/tests/testcases/X.py
+++ b/tests/testcases/X.py
@@ -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)
diff --git a/tests/testcases/config.py b/tests/testcases/config.py
index 0e10f0c1..7d3e7c26 100644
--- a/tests/testcases/config.py
+++ b/tests/testcases/config.py
@@ -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
diff --git a/tests/testcases/presets.py b/tests/testcases/presets.py
index 4bd1d0d1..5d92e7c4 100644
--- a/tests/testcases/presets.py
+++ b/tests/testcases/presets.py
@@ -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
diff --git a/tests/testcases/test.py b/tests/testcases/test.py
index fad8775f..935a8bc9 100644
--- a/tests/testcases/test.py
+++ b/tests/testcases/test.py
@@ -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