wip, tests failing, stuff broken

xkb
sezanzeb 4 years ago committed by sezanzeb
parent c969236e5f
commit 8a496644eb

@ -80,25 +80,55 @@ class SingleKeyMapping:
return character if character else None
def start_watching_keycodes(self, *args):
print('start_watching_keycodes')
"""Start to periodically check if a keycode has been pressed.
This is different from just listening for text input events
(as in Gtk.Entry), since keys may not write characters into the form
because they are not mapped. Furthermore their keycode is needed,
not their mapped character."""
keycode_reader.clear()
GLib.timeout_add(1000 / 30, self.get_newest_keycode)
def get_newest_keycode(self):
def iterate():
self.check_newest_keycode()
return self.keycode.is_focus()
GLib.timeout_add(1000 / 30, iterate)
def check_newest_keycode(self):
"""Check if a keycode has been pressed and if so, display it."""
new_keycode = keycode_reader.read()
previous_keycode = self.get_keycode()
character = self.get_character()
# TODO this is broken af
if new_keycode is not None:
try:
mapping.change(previous_keycode, new_keycode, character)
self.keycode.set_label(str(new_keycode))
except KeyError as e:
# Show error in status bar
logger.info(str(e)[1:-1])
# no input
if new_keycode is None:
return
return self.keycode.is_focus()
# keycode didn't change, do nothing
if new_keycode == previous_keycode:
return
# keycode is already set by some other row
if mapping.get(new_keycode) is not None:
logger.info('Keycode %s is already mapped', new_keycode)
return
# it's legal to display the keycode
self.keycode.set_label(str(new_keycode))
# the character is empty and therefore the mapping is not complete
if character is None:
return
# else, the keycode has changed, the character is set, all good
mapping.change(previous_keycode, new_keycode, character)
def on_character_input_change(self, entry):
keycode = self.get_keycode()
character = self.get_character()
if keycode is not None:
mapping.change(None, keycode, character)
def put_together(self, keycode, character):
"""Create all GTK widgets."""
@ -134,6 +164,10 @@ class SingleKeyMapping:
character_input.set_has_frame(False)
if character is not None:
character_input.set_text(character)
character_input.connect(
'changed',
self.on_character_input_change
)
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
row.set_homogeneous(True)
@ -176,11 +210,8 @@ class Window:
self.select_newest_preset()
print(1)
css_provider = Gtk.CssProvider()
print(2)
css_provider.load_from_path(get_data_path('style.css'))
print(3)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
@ -242,9 +273,7 @@ class Window:
if new_name != '' and new_name != self.selected_preset:
rename_preset(self.selected_device, self.selected_preset, new_name)
self.populate_presets()
self.update_mappings()
self.update_config()
self.save_config()
def on_delete_preset_clicked(self, button):
"""Delete a preset from the file system."""
@ -253,7 +282,6 @@ class Window:
def on_apply_preset_clicked(self, button):
"""Apply a preset without saving changes."""
self.update_mappings()
logger.debug(
'Applying preset "%s" for "%s"',
self.selected_preset,
@ -280,7 +308,7 @@ class Window:
new_preset = create_preset(self.selected_device)
self.get('preset_selection').append(new_preset, new_preset)
self.get('preset_selection').set_active_id(new_preset)
self.update_config()
self.save_config()
def on_select_preset(self, dropdown):
"""Show the mappings of the preset."""
@ -331,7 +359,7 @@ class Window:
window = self.get('window')
window.resize(window.get_size()[0], 1)
def update_config(self):
def save_config(self):
"""Write changes to disk"""
if self.selected_device is None or self.selected_preset is None:
return

@ -56,9 +56,22 @@ class Mapping:
"""Iterate over tuples of unique keycodes and their character."""
return iter(self._mapping.items())
def __len__(self):
return len(self._mapping)
def load(self, device, preset):
"""Parse the X config to replace the current mapping with that one."""
with open(get_home_path(device, preset), 'r') as f:
path = get_home_path(device, preset)
if not os.path.exists(path):
logger.debug(
'Tried to load non existing preset "%s" for %s',
preset, device
)
self._mapping = {}
return
with open(path, 'r') as f:
# from "key <12> { [ 1 ] };" extract 12 and 1,
# avoid lines that start with special characters
# (might be comments)
@ -71,23 +84,45 @@ class Mapping:
}
def change(self, previous_keycode, new_keycode, character):
"""Change a mapping. Return True on success."""
"""Replace the mapping of a keycode with a different one.
Return True on success.
Parameters
----------
previous_keycode : int or None
new_keycode : int
character : string
"""
print('change', previous_keycode, new_keycode, character)
if new_keycode and character and new_keycode != previous_keycode:
self.add(new_keycode, character)
# clear previous mapping of that code, because the line
# representing that one will now represent a different one.
self.clear(previous_keycode)
if new_keycode != previous_keycode:
# clear previous mapping of that code, because the line
# representing that one will now represent a different one.
self.clear(previous_keycode)
return True
return False
def clear(self, keycode):
"""Remove a keycode from the mapping."""
"""Remove a keycode from the mapping.
Parameters
----------
keycode : int
"""
print('clear', id(self), keycode, self._mapping.get(keycode))
if self._mapping.get(keycode) is not None:
del self._mapping[keycode]
def add(self, keycode, character):
"""Add a mapping."""
"""Add a mapping for a hardware key to a character.
Parameters
----------
keycode : int
character : string
"""
if self._mapping.get(keycode) is not None:
raise KeyError(
f'Keycode {keycode} is already mapped '
@ -96,6 +131,12 @@ class Mapping:
self._mapping[keycode] = character
def get(self, keycode):
"""Read the character that is mapped to this keycode.
Parameters
----------
keycode : int
"""
return self._mapping.get(keycode)
@ -114,7 +155,6 @@ def ensure_symlink():
def create_preset(device, name=None):
"""Create an empty preset and return the name."""
existing_names = get_presets(device)
if name is None:
name = 'new preset'
@ -125,12 +165,16 @@ def create_preset(device, name=None):
i += 1
name = f'{name} {i}'
os.mknod(get_home_path(device, name))
path = get_home_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)
ensure_symlink()
return name
def create_setxkbmap_config(device, preset, mappings):
def create_setxkbmap_config(device, preset, mapping):
"""Generate a config file for setxkbmap.
The file is created in ~/.config/key-mapper/<device>/<preset> and,
@ -144,10 +188,10 @@ def create_setxkbmap_config(device, preset, mappings):
----------
device : string
preset : string
mappings : list
List of (keycode, character) tuples
mapping : Mapping
"""
if len(mappings) == 0:
if len(mapping) == 0:
print(mapping._mapping)
logger.debug('Got empty mappings')
return None
@ -167,7 +211,7 @@ def create_setxkbmap_config(device, preset, mappings):
logger.info('Writing key mappings')
with open(home_preset_path, 'w') as f:
contents = generate_symbols_file_content(device, preset, mappings)
contents = generate_symbols_file_content(device, preset, mapping)
if contents is not None:
f.write(contents)
@ -246,6 +290,7 @@ def generate_symbols_file_content(device, preset, mapping):
preset : string
mapping : Mapping
"""
# TODO test this function
system_default = 'us' # TODO get the system default
# If the symbols file contains key codes that are not present in
@ -256,7 +301,7 @@ def generate_symbols_file_content(device, preset, mapping):
keycodes = re.findall(r'<.+?>', f.read())
xkb_symbols = []
for keycode, character in mapping.iter():
for keycode, character in mapping:
if f'<{keycode}>' not in keycodes:
logger.error(f'Unknown keycode <{keycode}> for "{character}"')
# continue, otherwise X would crash when loading

@ -22,7 +22,6 @@
"""Device stuff that is independent from the display server."""
import re
import subprocess
import evdev
@ -66,13 +65,11 @@ class KeycodeReader:
pass
def start_reading(self, device):
"""Start a loop that keeps reading keycodes.
"""Tell the evdev lib to start looking for keycodes.
This keeps the main loop running, however, it is blocking for the
function that calls this until stop_reading is called from somewhere
else.
If read is called without prior start_reading, no keycodes
will be available.
"""
# start the next one
logger.debug('Starting reading keycodes for %s', device)
# Watch over each one of the potentially multiple devices per hardware

@ -23,6 +23,7 @@
import os
import time
import glob
from keymapper.paths import CONFIG_PATH, get_home_path
@ -159,3 +160,6 @@ def rename_preset(device, old_preset_name, new_preset_name):
get_home_path(device, old_preset_name),
get_home_path(device, new_preset_name)
)
# set the modification date to now
now = time.time()
os.utime(get_home_path(device, new_preset_name), (now, now))

@ -25,13 +25,14 @@
import sys
import unittest
# quickly fake some stuff before any other file gets imported and initialized
# quickly fake some stuff before any other file gets a chance to import
# the original version
from keymapper import paths
paths.SYMBOLS_PATH = '/tmp/key-mapper-test/symbols'
paths.CONFIG_PATH = '/tmp/key-mapper-test/.config'
from keymapper import presets
presets.get_devices = lambda: ({
from keymapper import linux
linux._devices = {
'device 1': {
'paths': [
'/dev/input/event10',
@ -48,10 +49,18 @@ presets.get_devices = lambda: ({
'paths': ['/dev/input/event3'],
'names': ['device 2']
}
})
}
linux.get_devices = lambda: linux._devices
from keymapper.logger import update_verbosity
# some class function stubs.
# can be overwritten in tests as well at any time.
linux.KeycodeReader.start_reading = lambda *args: None
linux.KeycodeReader.read = lambda *args: None
tmp = '/tmp/key-mapper-test'
if __name__ == "__main__":
update_verbosity(True)

@ -20,6 +20,8 @@
import sys
import time
import os
import unittest
from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec
@ -30,6 +32,8 @@ import shutil
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from test import tmp
def gtk_iteration():
"""Iterate while events are pending."""
@ -71,6 +75,8 @@ class Integration(unittest.TestCase):
Gtk.main_quit = lambda: None
def setUp(self):
if os.path.exists(tmp):
shutil.rmtree(tmp)
self.window = launch()
def tearDown(self):
@ -91,9 +97,43 @@ class Integration(unittest.TestCase):
def get_active_text(self):
return self.name
self.window.on_select_device(FakeDropdown('fakeDevice1'))
self.window.on_select_preset(FakeDropdown('fakePreset1'))
# TODO test meaningful stuff here
# created on start because the first device is selected and some empty
# preset prepared.
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset'))
self.assertEqual(self.window.selected_device, 'device 1')
self.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_create_preset_clicked(None)
gtk_iteration()
# until save is clicked, this is still not saved
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset'))
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/new_preset_2'))
self.assertEqual(self.window.selected_preset, 'new preset 2')
self.window.on_select_preset(FakeDropdown('new preset'))
gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset')
# now try to change the name
self.window.get('preset_name_input').set_text('abc 123')
gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset')
self.assertFalse(os.path.exists(f'{tmp}/symbols/device_1/abc_123'))
self.window.on_save_preset_clicked(None)
gtk_iteration()
self.assertEqual(self.window.selected_preset, 'abc 123')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/abc_123'))
# TODO test presence of file in {tmp}/keycodes
self.assertListEqual(
sorted(os.listdir(f'{tmp}/symbols')),
['device_1']
)
self.assertListEqual(
sorted(os.listdir(f'{tmp}/symbols/device_1')),
['abc_123', 'new_preset', 'new_preset_2']
)
if __name__ == "__main__":

@ -27,8 +27,7 @@ import time
from keymapper.presets import find_newest_preset, rename_preset
from keymapper.X import create_preset
tmp = '/tmp/key-mapper-test'
from test import tmp
class TestCreatePreset(unittest.TestCase):

@ -0,0 +1,33 @@
#!/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/>.
import unittest
from keymapper.linux import get_devices
class TestTest(unittest.TestCase):
def test_stubs(self):
self.assertIn('device 1', get_devices())
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save