renaming, keeping unmapped keys at their defaults

This commit is contained in:
sezanzeb 2020-11-10 23:14:28 +01:00
parent 96fe81a3cd
commit 52231d507e
14 changed files with 231 additions and 105 deletions

29
X11PATHS.md Normal file
View File

@ -0,0 +1,29 @@
# Folder Structure of Key Mapper
Stuff has to be placed in /usr/share/X11/xkb to my knowledge. In order to
be able to make backups of the configs, which would be expected in the
users home directory, this is symlinked to home.
Every user gets a path within that /usr directory which is very
unconventional, but it works. This way the presets of multiple users
don't clash.
This is how a single preset is stored. The path in /usr is a symlink, the
files are actually in home.
- /usr/share/X11/xkb/symbols/key-mapper/<user>/<device>/<preset>
- /home/<user>/.config/key-mapper/<device>/<preset>
This is where key-mapper stores the defaults. They are generated from the
parsed output of `xmodmap` and used to keep the unmapped keys at their system
defaults.
- /usr/share/X11/xkb/symbols/key-mapper/<user>/default
- /home/<user>/.config/key-mapper/default
Because the concept of "reasonable symbolic names" [3] doesn't apply
when mouse buttons are all over the place, an identity mapping
to make generating "symbols" files easier/possible exists.
Keycode 10 -> "<10>". This has the added benefit that keycodes reported
by xev can be identified in the symbols file.
- /usr/share/X11/xkb/keycodes/key-mapper
[3] https://www.x.org/releases/X11R7.7/doc/xorg-docs/input/XKB-Enhancing.html

View File

@ -481,10 +481,22 @@ LCTL, RCTL</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkListBox" id="key_list"> <object class="GtkScrolledWindow">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">True</property>
<property name="selection_mode">none</property> <child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="key_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
</object>
</child>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View File

@ -4,6 +4,7 @@
// /usr/share/X11/xkb/keycodes/key-mapper // /usr/share/X11/xkb/keycodes/key-mapper
default xkb_symbols "basic" {{ default xkb_symbols "basic" {{
{include}
name[Group1] = "{name}"; name[Group1] = "{name}";
{xkb_symbols} {xkb_symbols}
}}; }};

View File

@ -38,11 +38,11 @@ import shutil
import subprocess import subprocess
from keymapper.paths import get_home_path, get_usr_path, KEYCODES_PATH, \ from keymapper.paths import get_home_path, get_usr_path, KEYCODES_PATH, \
CONFIG_PATH, SYMBOLS_PATH HOME_PATH, USERS_SYMBOLS, DEFAULT_SYMBOLS, X11_SYMBOLS
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.data import get_data_path from keymapper.data import get_data_path
from keymapper.linux import get_devices from keymapper.linux import get_devices
from keymapper.mapping import mapping from keymapper.mapping import custom_mapping, Mapping
def ensure_symlink(): def ensure_symlink():
@ -50,12 +50,19 @@ def ensure_symlink():
It provides the configs in /home to X11 in /usr. It provides the configs in /home to X11 in /usr.
""" """
if not os.path.exists(SYMBOLS_PATH): if not os.path.exists(HOME_PATH):
os.makedirs(HOME_PATH, exist_ok=True)
if not os.path.exists(USERS_SYMBOLS):
# link from /usr/share/X11/xkb/symbols/key-mapper/user to # link from /usr/share/X11/xkb/symbols/key-mapper/user to
# /home/user/.config/key-mapper # /home/user/.config/key-mapper
logger.info('Linking "%s" to "%s"', SYMBOLS_PATH, CONFIG_PATH) logger.info('Linking "%s" to "%s"', USERS_SYMBOLS, HOME_PATH)
os.makedirs(os.path.dirname(SYMBOLS_PATH), exist_ok=True) os.makedirs(os.path.dirname(USERS_SYMBOLS), exist_ok=True)
os.symlink(CONFIG_PATH, SYMBOLS_PATH, target_is_directory=True) os.symlink(HOME_PATH, USERS_SYMBOLS, target_is_directory=True)
elif not os.path.islink(USERS_SYMBOLS):
logger.error('Expected %s to be a symlink', USERS_SYMBOLS)
else:
logger.debug('Symlink %s exists', USERS_SYMBOLS)
def create_preset(device, name=None): def create_preset(device, name=None):
@ -78,7 +85,7 @@ def create_preset(device, name=None):
# give those files to the user # give those files to the user
user = os.getlogin() user = os.getlogin()
for root, dirs, files in os.walk(CONFIG_PATH): for root, dirs, files in os.walk(HOME_PATH):
shutil.chown(root, user, user) shutil.chown(root, user, user)
for file in files: for file in files:
shutil.chown(os.path.join(root, file), user, user) shutil.chown(os.path.join(root, file), user, user)
@ -102,11 +109,12 @@ def create_setxkbmap_config(device, preset):
device : string device : string
preset : string preset : string
""" """
if len(mapping) == 0: if len(custom_mapping) == 0:
logger.debug('Got empty mappings') logger.debug('Got empty mappings')
return None return None
create_identity_mapping() create_identity_mapping()
create_default_symbols()
home_device_path = get_home_path(device) home_device_path = get_home_path(device)
if not os.path.exists(home_device_path): if not os.path.exists(home_device_path):
@ -120,16 +128,28 @@ def create_setxkbmap_config(device, preset):
logger.info('Creating config file "%s"', home_preset_path) logger.info('Creating config file "%s"', home_preset_path)
os.mknod(home_preset_path) os.mknod(home_preset_path)
logger.info('Writing key mappings') logger.info('Writing key mappings to %s', home_preset_path)
with open(home_preset_path, 'w') as f: with open(home_preset_path, 'w') as f:
contents = generate_symbols_content(device, preset) contents = generate_symbols(get_preset_name(device, preset))
if contents is not None: if contents is not None:
f.write(contents) f.write(contents)
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 may not
# contain spaces
name = get_usr_path(device, preset)[len(X11_SYMBOLS) + 1:]
assert ' ' not in name
return name
DEFAULT_PRESET = get_preset_name('default')
def apply_preset(device, preset): def apply_preset(device, preset):
"""Apply a preset to the device.""" """Apply a preset to the device."""
logger.info('Applying the preset') logger.info('Applying preset "%s" on device %s', preset, device)
group = get_devices()[device] group = get_devices()[device]
# apply it to every device that hangs on the same usb port, because I # apply it to every device that hangs on the same usb port, because I
@ -140,17 +160,15 @@ def apply_preset(device, preset):
# only all virtual devices of the same hardware device # only all virtual devices of the same hardware device
continue continue
symbols = '/usr/share/X11/xkb/symbols/'
layout_path = get_usr_path(device, preset) layout_path = get_usr_path(device, preset)
with open(layout_path, 'r') as f: with open(layout_path, 'r') as f:
if f.read() == '': if f.read() == '':
logger.error('Tried to load empty config') logger.error('Tried to load empty config')
return return
layout_name = layout_path[len(symbols):]
cmd = [ cmd = [
'setxkbmap', 'setxkbmap',
'-layout', layout_name, '-layout', get_preset_name(device, preset),
'-keycodes', 'key-mapper', '-keycodes', 'key-mapper',
'-device', str(xinput_id) '-device', str(xinput_id)
] ]
@ -166,8 +184,9 @@ def create_identity_mapping():
This has the added benefit that keycodes reported by xev can be This has the added benefit that keycodes reported by xev can be
identified in the symbols file. identified in the symbols file.
""" """
# TODO don't create this again if it already exists, as soon as this if os.path.exists(KEYCODES_PATH):
# stuff is stable. logger.debug('Found the keycodes file at %s', KEYCODES_PATH)
return
xkb_keycodes = [] xkb_keycodes = []
# the maximum specified in /usr/share/X11/xkb/keycodes is usually 255 # the maximum specified in /usr/share/X11/xkb/keycodes is usually 255
@ -195,15 +214,23 @@ def create_identity_mapping():
keycodes.write(result) keycodes.write(result)
def generate_symbols_content(device, preset): def generate_symbols(name, include=DEFAULT_PRESET, mapping=custom_mapping):
"""Create config contents to be placed in /usr/share/X11/xkb/symbols. """Create config contents to be placed in /usr/share/X11/xkb/symbols.
This file contains the mapping of the preset as expected by X. It's the mapping of the preset as expected by X. This function does not
create the file.
Parameters Parameters
---------- ----------
device : string name : string
preset : string Usually what `get_preset_name` returns
include : string or None
If another preset should be included. Defaults to the default
preset. Use None to avoid including.
mapping : Mapping
If you need to create a symbols file for some other mapping you can
pass it to this parameter. By default the custom mapping will be
used that is also displayed in the user interface.
""" """
if len(mapping) == 0: if len(mapping) == 0:
raise ValueError('Mapping is empty') raise ValueError('Mapping is empty')
@ -212,6 +239,7 @@ def generate_symbols_content(device, preset):
# the keycodes file, THE WHOLE X SESSION WILL CRASH! # the keycodes file, THE WHOLE X SESSION WILL CRASH!
if not os.path.exists(KEYCODES_PATH): if not os.path.exists(KEYCODES_PATH):
raise FileNotFoundError('Expected the keycodes file to exist') raise FileNotFoundError('Expected the keycodes file to exist')
with open(KEYCODES_PATH, 'r') as f: with open(KEYCODES_PATH, 'r') as f:
keycodes = re.findall(r'<.+?>', f.read()) keycodes = re.findall(r'<.+?>', f.read())
@ -222,6 +250,7 @@ def generate_symbols_content(device, preset):
# don't append that one, otherwise X would crash when loading # don't append that one, otherwise X would crash when loading
continue continue
xkb_symbols.append(f'key <{keycode}> {{ [ {character} ] }};') xkb_symbols.append(f'key <{keycode}> {{ [ {character} ] }};')
if len(xkb_symbols) == 0: if len(xkb_symbols) == 0:
logger.error('Failed to populate xkb_symbols') logger.error('Failed to populate xkb_symbols')
return None return None
@ -231,8 +260,9 @@ def generate_symbols_content(device, preset):
template = template_file.read() template = template_file.read()
result = template.format( result = template.format(
name=f'{device}/{preset}', name=name,
xkb_symbols='\n '.join(xkb_symbols) xkb_symbols='\n '.join(xkb_symbols),
include=f'include "{include}"' if include else ''
) )
return result return result
@ -257,7 +287,10 @@ def get_xinput_id_mapping():
def parse_symbols_file(device, preset): def parse_symbols_file(device, preset):
"""Parse a symbols file and return the keycodes.""" """Parse a symbols file populate the mapping.
Existing mappings are overwritten if there are conflicts.
"""
path = get_home_path(device, preset) path = get_home_path(device, preset)
if not os.path.exists(path): if not os.path.exists(path):
@ -265,8 +298,8 @@ def parse_symbols_file(device, preset):
'Tried to load non existing preset "%s" for %s', 'Tried to load non existing preset "%s" for %s',
preset, device preset, device
) )
mapping.empty() custom_mapping.empty()
mapping.changed = False custom_mapping.changed = False
return return
with open(path, 'r') as f: with open(path, 'r') as f:
@ -276,5 +309,41 @@ def parse_symbols_file(device, preset):
result = re.findall(r'\n\s+?key <(.+?)>.+?\[\s+(\w+)', f.read()) result = re.findall(r'\n\s+?key <(.+?)>.+?\[\s+(\w+)', f.read())
logger.debug('Found %d mappings in this preset', len(result)) logger.debug('Found %d mappings in this preset', len(result))
for keycode, character in result: for keycode, character in result:
mapping.changed = False custom_mapping.changed = False
mapping.change(None, int(keycode), character) custom_mapping.change(None, int(keycode), character)
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
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
defaults = Mapping()
for keycode, characters in mappings:
# TODO support an array of values in mapping and test it
defaults.change(None, int(keycode), characters.split()[0])
ensure_symlink()
if not os.path.exists(DEFAULT_SYMBOLS):
logger.info('Creating %s', DEFAULT_SYMBOLS)
os.mknod(DEFAULT_SYMBOLS)
# TODO test that it is included in the config files
# TODO write test about it being created only if the path doesnt exist
with open(DEFAULT_SYMBOLS, 'w') as f:
contents = generate_symbols(DEFAULT_PRESET, None, defaults)
if contents is not None:
logger.info('Updating default mappings')
f.write(contents)

View File

@ -27,7 +27,7 @@ gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0') gi.require_version('GLib', '2.0')
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
from keymapper.mapping import mapping from keymapper.mapping import custom_mapping
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.linux import keycode_reader from keymapper.linux import keycode_reader
@ -87,7 +87,7 @@ class Row:
return return
# keycode is already set by some other row # keycode is already set by some other row
if mapping.get(new_keycode) is not None: if custom_mapping.get(new_keycode) is not None:
msg = f'Keycode {new_keycode} is already mapped' msg = f'Keycode {new_keycode} is already mapped'
logger.info(msg) logger.info(msg)
self.window.get('status_bar').push(CTX_KEYCODE, msg) self.window.get('status_bar').push(CTX_KEYCODE, msg)
@ -102,14 +102,14 @@ class Row:
return return
# else, the keycode has changed, the character is set, all good # else, the keycode has changed, the character is set, all good
mapping.change(previous_keycode, new_keycode, character) custom_mapping.change(previous_keycode, new_keycode, character)
def on_character_input_change(self, entry): def on_character_input_change(self, entry):
keycode = self.get_keycode() keycode = self.get_keycode()
character = self.get_character() character = self.get_character()
if keycode is not None: if keycode is not None:
mapping.change(None, keycode, character) custom_mapping.change(None, keycode, character)
def put_together(self, keycode, character): def put_together(self, keycode, character):
"""Create all GTK widgets.""" """Create all GTK widgets."""
@ -169,5 +169,5 @@ class Row:
"""Destroy the row and remove it from the config.""" """Destroy the row and remove it from the config."""
keycode = self.get_keycode() keycode = self.get_keycode()
if keycode is not None: if keycode is not None:
mapping.clear(keycode) custom_mapping.clear(keycode)
self.delete_callback(self) self.delete_callback(self)

View File

@ -29,7 +29,7 @@ from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path from keymapper.data import get_data_path
from keymapper.X import create_setxkbmap_config, apply_preset, \ from keymapper.X import create_setxkbmap_config, apply_preset, \
create_preset, mapping, parse_symbols_file create_preset, custom_mapping, parse_symbols_file
from keymapper.presets import get_presets, find_newest_preset, \ from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset delete_preset, rename_preset
from keymapper.logger import logger from keymapper.logger import logger
@ -94,9 +94,9 @@ class Window:
rows = len(self.get('key_list').get_children()) rows = len(self.get('key_list').get_children())
# verify that all mappings are displayed # verify that all mappings are displayed
assert rows >= len(mapping) assert rows >= len(custom_mapping)
if rows == len(mapping): if rows == len(custom_mapping):
self.add_empty() self.add_empty()
return True return True
@ -141,7 +141,7 @@ class Window:
"""Remove all rows from the mappings table.""" """Remove all rows from the mappings table."""
key_list = self.get('key_list') key_list = self.get('key_list')
key_list.forall(key_list.remove) key_list.forall(key_list.remove)
mapping.empty() custom_mapping.empty()
def on_save_preset_clicked(self, button): def on_save_preset_clicked(self, button):
"""Save changes to a preset to the file system.""" """Save changes to a preset to the file system."""
@ -219,7 +219,7 @@ class Window:
parse_symbols_file(self.selected_device, self.selected_preset) parse_symbols_file(self.selected_device, self.selected_preset)
key_list = self.get('key_list') key_list = self.get('key_list')
for keycode, character in mapping: for keycode, character in custom_mapping:
single_key_mapping = Row( single_key_mapping = Row(
window=self, window=self,
delete_callback=self.on_row_removed, delete_callback=self.on_row_removed,

View File

@ -104,5 +104,6 @@ class Mapping:
return self._mapping.get(keycode) return self._mapping.get(keycode)
# one mapping object for the whole application # one mapping object for the whole application that holds all
mapping = Mapping() # customizations
custom_mapping = Mapping()

View File

@ -25,12 +25,15 @@
import os import os
# the path in home, is symlinked with SYMBOLS_PATH. # the path in home, is symlinked with USERS_SYMBOLS.
# getlogin gets the user who ran sudo # getlogin gets the user who ran sudo
CONFIG_PATH = os.path.join('/home', os.getlogin(), '.config/key-mapper') HOME_PATH = os.path.join('/home', os.getlogin(), '.config/key-mapper')
# the path that contains ALL symbols, not just ours
X11_SYMBOLS = '/usr/share/X11/xkb/symbols'
# should not contain spaces # should not contain spaces
SYMBOLS_PATH = os.path.join( USERS_SYMBOLS = os.path.join(
'/usr/share/X11/xkb/symbols/key-mapper', '/usr/share/X11/xkb/symbols/key-mapper',
os.getlogin().replace(' ', '_') os.getlogin().replace(' ', '_')
) )
@ -44,9 +47,12 @@ def get_home_path(device, preset=None):
device = device.strip() device = device.strip()
if preset is not None: if preset is not None:
preset = preset.strip() preset = preset.strip()
return os.path.join(CONFIG_PATH, device, preset).replace(' ', '_') return os.path.join(HOME_PATH, device, preset).replace(' ', '_')
else: else:
return os.path.join(CONFIG_PATH, device.replace(' ', '_')) return os.path.join(HOME_PATH, device.replace(' ', '_'))
DEFAULT_SYMBOLS = get_home_path('default')
def get_usr_path(device, preset=None): def get_usr_path(device, preset=None):
@ -59,6 +65,6 @@ def get_usr_path(device, preset=None):
device = device.strip() device = device.strip()
if preset is not None: if preset is not None:
preset = preset.strip() preset = preset.strip()
return os.path.join(SYMBOLS_PATH, device, preset).replace(' ', '_') return os.path.join(USERS_SYMBOLS, device, preset).replace(' ', '_')
else: else:
return os.path.join(SYMBOLS_PATH, device.replace(' ', '_')) return os.path.join(USERS_SYMBOLS, device.replace(' ', '_'))

View File

@ -26,7 +26,7 @@ import os
import time import time
import glob import glob
from keymapper.paths import get_home_path, CONFIG_PATH from keymapper.paths import get_home_path, HOME_PATH
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.linux import get_devices from keymapper.linux import get_devices
@ -78,7 +78,7 @@ def find_newest_preset(device=None):
# sort the oldest files to the front in order to use pop to get the newest # sort the oldest files to the front in order to use pop to get the newest
if device is None: if device is None:
paths = sorted( paths = sorted(
glob.glob(os.path.join(CONFIG_PATH, '*/*')), glob.glob(os.path.join(HOME_PATH, '*/*')),
key=os.path.getmtime key=os.path.getmtime
) )
else: else:
@ -132,7 +132,7 @@ def delete_preset(device, preset):
device_path = get_home_path(device) device_path = get_home_path(device)
if os.path.exists(device_path) and len(os.listdir(device_path)) == 0: if os.path.exists(device_path) and len(os.listdir(device_path)) == 0:
logger.debug('Removing empty dir "%s"', device_path) logger.debug('Removing empty dir "%s"', device_path)
os.remove(device_path) os.rmdir(device_path)
def rename_preset(device, old_preset_name, new_preset_name): def rename_preset(device, old_preset_name, new_preset_name):

View File

@ -28,10 +28,11 @@ import unittest
# quickly fake some stuff before any other file gets a chance to import # quickly fake some stuff before any other file gets a chance to import
# the original version # the original version
from keymapper import paths from keymapper import paths
paths.SYMBOLS_PATH = '/tmp/key-mapper-test/symbols' paths.X11_SYMBOLS = '/tmp/key-mapper-test/X11/symbols/key-mapper/user'
paths.CONFIG_PATH = '/tmp/key-mapper-test/.config' paths.USERS_SYMBOLS = '/tmp/key-mapper-test/X11/symbols/key-mapper/user'
paths.KEYCODES_PATH = '/tmp/key-mapper-test/keycodes/key-mapper' paths.DEFAULT_SYMBOLS = '/tmp/key-mapper-test/X11/symbols/key-mapper/user/default'
paths.HOME_PATH = '/tmp/key-mapper-test/user/.config'
paths.KEYCODES_PATH = '/tmp/key-mapper-test/X11/keycodes/key-mapper'
from keymapper import linux from keymapper import linux
linux._devices = { linux._devices = {

View File

@ -23,19 +23,19 @@ import os
import unittest import unittest
import shutil import shutil
from keymapper.X import mapping, generate_symbols_content, \ from keymapper.X import custom_mapping, generate_symbols, \
create_identity_mapping, create_setxkbmap_config, get_home_path create_identity_mapping, create_setxkbmap_config, get_home_path
from keymapper.paths import KEYCODES_PATH, SYMBOLS_PATH, CONFIG_PATH from keymapper.paths import KEYCODES_PATH, USERS_SYMBOLS, HOME_PATH
from test import tmp from test import tmp
class TestConfig(unittest.TestCase): class TestConfig(unittest.TestCase):
def setUp(self): def setUp(self):
mapping.empty() custom_mapping.empty()
mapping.change(None, 10, 'a') custom_mapping.change(None, 10, 'a')
mapping.change(None, 11, 'KP_1') custom_mapping.change(None, 11, 'KP_1')
mapping.change(None, 12, 3) custom_mapping.change(None, 12, 3)
if os.path.exists(tmp): if os.path.exists(tmp):
shutil.rmtree(tmp) shutil.rmtree(tmp)
@ -43,13 +43,13 @@ class TestConfig(unittest.TestCase):
create_setxkbmap_config('device a', 'preset b') create_setxkbmap_config('device a', 'preset b')
self.assertTrue(os.path.exists(os.path.join( self.assertTrue(os.path.exists(os.path.join(
CONFIG_PATH, HOME_PATH,
'device_a', 'device_a',
'preset_b' 'preset_b'
))) )))
self.assertTrue(os.path.exists(os.path.join( self.assertTrue(os.path.exists(os.path.join(
SYMBOLS_PATH, USERS_SYMBOLS,
'device_a', 'device_a',
'preset_b' 'preset_b'
))) )))
@ -65,12 +65,12 @@ class TestConfig(unittest.TestCase):
def test_generate_content(self): def test_generate_content(self):
self.assertRaises( self.assertRaises(
FileNotFoundError, FileNotFoundError,
generate_symbols_content, generate_symbols,
'device', 'preset' 'device', 'preset'
) )
# create the identity mapping, because it is required for # create the identity mapping, because it is required for
# generate_symbols_content # generate_symbols
create_identity_mapping() create_identity_mapping()
self.assertTrue(os.path.exists(KEYCODES_PATH)) self.assertTrue(os.path.exists(KEYCODES_PATH))
with open(KEYCODES_PATH, 'r') as f: with open(KEYCODES_PATH, 'r') as f:
@ -78,7 +78,7 @@ class TestConfig(unittest.TestCase):
self.assertIn('<8> = 8;', keycodes) self.assertIn('<8> = 8;', keycodes)
self.assertIn('<255> = 255;', keycodes) self.assertIn('<255> = 255;', keycodes)
content = generate_symbols_content('device', 'preset') content = generate_symbols('device/preset')
self.assertIn('key <10> { [ a ] };', content) self.assertIn('key <10> { [ a ] };', content)
self.assertIn('key <11> { [ KP_1 ] };', content) self.assertIn('key <11> { [ KP_1 ] };', content)
self.assertIn('key <12> { [ 3 ] };', content) self.assertIn('key <12> { [ 3 ] };', content)

View File

@ -32,7 +32,8 @@ import shutil
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
from keymapper.mapping import mapping from keymapper.mapping import custom_mapping
from keymapper.paths import USERS_SYMBOLS, HOME_PATH, KEYCODES_PATH
from test import tmp from test import tmp
@ -94,7 +95,7 @@ class Integration(unittest.TestCase):
rows = len(self.window.get('key_list').get_children()) rows = len(self.window.get('key_list').get_children())
self.assertEqual(rows, 1) self.assertEqual(rows, 1)
mapping.change(None, 13, 'a') custom_mapping.change(None, 13, 'a')
time.sleep(0.2) time.sleep(0.2)
gtk_iteration() gtk_iteration()
@ -102,18 +103,18 @@ class Integration(unittest.TestCase):
self.assertEqual(rows, 2) self.assertEqual(rows, 2)
def test_rename_and_save(self): def test_rename_and_save(self):
mapping.change(None, 14, 'a') custom_mapping.change(None, 14, 'a')
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertEqual(mapping.get(14), 'a') self.assertEqual(custom_mapping.get(14), 'a')
mapping.change(None, 14, 'b') custom_mapping.change(None, 14, 'b')
self.window.get('preset_name_input').set_text('asdf') self.window.get('preset_name_input').set_text('asdf')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertEqual(self.window.selected_preset, 'asdf') self.assertEqual(self.window.selected_preset, 'asdf')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/asdf')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/asdf'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/asdf')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/asdf'))
self.assertEqual(mapping.get(14), 'b') self.assertEqual(custom_mapping.get(14), 'b')
def test_select_device_and_preset(self): def test_select_device_and_preset(self):
class FakeDropdown(Gtk.ComboBoxText): class FakeDropdown(Gtk.ComboBoxText):
@ -125,15 +126,15 @@ class Integration(unittest.TestCase):
# created on start because the first device is selected and some empty # created on start because the first device is selected and some empty
# preset prepared. # preset prepared.
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset'))
self.assertEqual(self.window.selected_device, 'device 1') self.assertEqual(self.window.selected_device, 'device 1')
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
# create another one # create another one
self.window.on_create_preset_clicked(None) self.window.on_create_preset_clicked(None)
gtk_iteration() gtk_iteration()
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset'))
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset_2')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset_2'))
self.assertEqual(self.window.selected_preset, 'new preset 2') self.assertEqual(self.window.selected_preset, 'new preset 2')
self.window.on_select_preset(FakeDropdown('new preset')) self.window.on_select_preset(FakeDropdown('new preset'))
@ -141,7 +142,7 @@ class Integration(unittest.TestCase):
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.assertListEqual( self.assertListEqual(
sorted(os.listdir(f'{tmp}/symbols/device_1')), sorted(os.listdir(f'{USERS_SYMBOLS}/device_1')),
['new_preset', 'new_preset_2'] ['new_preset', 'new_preset_2']
) )
@ -149,20 +150,20 @@ class Integration(unittest.TestCase):
self.window.get('preset_name_input').set_text('abc 123') self.window.get('preset_name_input').set_text('abc 123')
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.assertFalse(os.path.exists(f'{tmp}/symbols/device_1/abc_123')) self.assertFalse(os.path.exists(f'{USERS_SYMBOLS}/device_1/abc_123'))
mapping.change(None, 10, '1') custom_mapping.change(None, 10, '1')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertTrue(os.path.exists(f'{tmp}/keycodes/key-mapper')) self.assertTrue(os.path.exists(KEYCODES_PATH))
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'abc 123') self.assertEqual(self.window.selected_preset, 'abc 123')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/abc_123')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/abc_123'))
self.assertListEqual( self.assertListEqual(
sorted(os.listdir(f'{tmp}/symbols')), sorted(os.listdir(USERS_SYMBOLS)),
['device_1'] ['default', 'device_1']
) )
self.assertListEqual( self.assertListEqual(
sorted(os.listdir(f'{tmp}/symbols/device_1')), sorted(os.listdir(f'{USERS_SYMBOLS}/device_1')),
['abc_123', 'new_preset_2'] ['abc_123', 'new_preset_2']
) )

View File

@ -26,6 +26,7 @@ import time
from keymapper.presets import find_newest_preset, rename_preset from keymapper.presets import find_newest_preset, rename_preset
from keymapper.X import create_preset from keymapper.X import create_preset
from keymapper.paths import USERS_SYMBOLS, HOME_PATH
from test import tmp from test import tmp
@ -37,24 +38,24 @@ class TestCreatePreset(unittest.TestCase):
def test_create_preset_1(self): def test_create_preset_1(self):
create_preset('device 1') create_preset('device 1')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/new_preset')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/new_preset'))
def test_create_preset_2(self): def test_create_preset_2(self):
create_preset('device 1') create_preset('device 1')
create_preset('device 1') create_preset('device 1')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/new_preset')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/new_preset'))
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset_2')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset_2'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/new_preset_2')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/new_preset_2'))
def test_create_preset_3(self): def test_create_preset_3(self):
create_preset('device 1', 'pre set') create_preset('device 1', 'pre set')
create_preset('device 1', 'pre set') create_preset('device 1', 'pre set')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/pre_set')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/pre_set'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/pre_set')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/pre_set'))
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/pre_set_2')) self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/pre_set_2'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/pre_set_2')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/pre_set_2'))
class TestRenamePreset(unittest.TestCase): class TestRenamePreset(unittest.TestCase):
@ -62,9 +63,9 @@ class TestRenamePreset(unittest.TestCase):
create_preset('device 1', 'preset 1') create_preset('device 1', 'preset 1')
create_preset('device 1', 'foobar') create_preset('device 1', 'foobar')
rename_preset('device 1', 'preset 1', 'foobar') rename_preset('device 1', 'preset 1', 'foobar')
self.assertFalse(os.path.exists(f'{tmp}/.config/device_1/preset_1')) self.assertFalse(os.path.exists(f'{HOME_PATH}/device_1/preset_1'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/foobar')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/foobar'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/foobar_2')) self.assertTrue(os.path.exists(f'{HOME_PATH}/device_1/foobar_2'))
class TestFindPresets(unittest.TestCase): class TestFindPresets(unittest.TestCase):
@ -79,17 +80,17 @@ class TestFindPresets(unittest.TestCase):
self.assertEqual(find_newest_preset(), ('device 2', 'preset 2')) self.assertEqual(find_newest_preset(), ('device 2', 'preset 2'))
def test_find_newest_preset_2(self): def test_find_newest_preset_2(self):
os.makedirs(f'{tmp}/symbols/device_1') os.makedirs(f'{USERS_SYMBOLS}/device_1')
os.makedirs(f'{tmp}/.config/device_1') os.makedirs(f'{HOME_PATH}/device_1')
time.sleep(0.01) time.sleep(0.01)
os.makedirs(f'{tmp}/symbols/device_2') os.makedirs(f'{USERS_SYMBOLS}/device_2')
os.makedirs(f'{tmp}/.config/device_2') os.makedirs(f'{HOME_PATH}/device_2')
# takes the first one that the test-fake returns # takes the first one that the test-fake returns
self.assertEqual(find_newest_preset(), ('device 1', None)) self.assertEqual(find_newest_preset(), ('device 1', None))
def test_find_newest_preset_3(self): def test_find_newest_preset_3(self):
os.makedirs(f'{tmp}/symbols/device_1') os.makedirs(f'{USERS_SYMBOLS}/device_1')
os.makedirs(f'{tmp}/.config/device_1') os.makedirs(f'{HOME_PATH}/device_1')
self.assertEqual(find_newest_preset(), ('device 1', None)) self.assertEqual(find_newest_preset(), ('device 1', None))
def test_find_newest_preset_4(self): def test_find_newest_preset_4(self):

View File

@ -22,12 +22,17 @@
import unittest import unittest
from keymapper.linux import get_devices from keymapper.linux import get_devices
from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, DEFAULT_SYMBOLS
class TestTest(unittest.TestCase): class TestTest(unittest.TestCase):
def test_stubs(self): def test_stubs(self):
self.assertIn('device 1', get_devices()) self.assertIn('device 1', get_devices())
def test_paths(self):
self.assertTrue(USERS_SYMBOLS.startswith(X11_SYMBOLS))
self.assertTrue(DEFAULT_SYMBOLS.startswith(X11_SYMBOLS))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()