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