works
parent
022950e3b5
commit
f18b013513
@ -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')
|
|
@ -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
|
|
||||||
)
|
|
Loading…
Reference in New Issue