From ed2470863344f378c3c1a6756465f19d1f57fe20 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Wed, 18 Nov 2020 10:33:59 +0100 Subject: [PATCH] saving presets works again --- keymapper/archive/xkb.py | 39 ++++++++++++++++++++++++++++-- keymapper/cli.py | 3 +-- keymapper/gtk/window.py | 18 +++++++------- keymapper/mapping.py | 43 ++++++++++++++++++++++++++++++---- keymapper/paths.py | 42 +++++++-------------------------- keymapper/presets.py | 43 +++++++++++++++++++++------------- tests/testcases/config.py | 4 ++-- tests/testcases/integration.py | 21 ++++++++--------- tests/testcases/presets.py | 3 +-- tests/testcases/test.py | 2 +- 10 files changed, 133 insertions(+), 85 deletions(-) diff --git a/keymapper/archive/xkb.py b/keymapper/archive/xkb.py index ff14c012..bfcae6f8 100644 --- a/keymapper/archive/xkb.py +++ b/keymapper/archive/xkb.py @@ -35,8 +35,6 @@ import os import re import stat -from keymapper.paths import get_usr_path, KEYCODES_PATH, DEFAULT_SYMBOLS, \ - X11_SYMBOLS from keymapper.logger import logger from keymapper.data import get_data_path from keymapper.mapping import custom_mapping, system_mapping, Mapping @@ -47,6 +45,43 @@ 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(' ', '_') +) + +# those are the same for every preset and user, they are needed to make the +# presets work. +KEYCODES_PATH = '/usr/share/X11/xkb/keycodes/key-mapper' + + +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. diff --git a/keymapper/cli.py b/keymapper/cli.py index f7a5ae95..5d1a7910 100644 --- a/keymapper/cli.py +++ b/keymapper/cli.py @@ -26,7 +26,6 @@ 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 @@ -59,7 +58,7 @@ def setxkbmap(device, layout): load the system default """ if layout is not None: - path = os.path.join(X11_SYMBOLS, layout) + path = os.path.join('/usr/share/X11/xkb/symbols', layout) if not os.path.exists(path): logger.error('Symbols %s don\'t exist', path) return diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 45e63c33..e19fd339 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -30,7 +30,7 @@ from gi.repository import Gtk, Gdk, GLib from keymapper.data import get_data_path from keymapper.mapping import custom_mapping from keymapper.presets import get_presets, find_newest_preset, \ - delete_preset, rename_preset + delete_preset, rename_preset, get_available_preset_name from keymapper.logger import logger from keymapper.linux import KeycodeReader from keymapper.cli import setxkbmap @@ -164,9 +164,9 @@ class Window: presets = get_presets(device) self.get('preset_name_input').set_text('') if len(presets) == 0: - # presets = [create_preset(device)] - # TODO create one empty preset - presets = [] + new_preset = get_available_preset_name(self.selected_device) + custom_mapping.save(self.selected_device, new_preset) + presets = [new_preset] else: logger.debug('Presets for "%s": %s', device, ', '.join(presets)) preset_selection = self.get('preset_selection') @@ -276,10 +276,8 @@ class Window: return try: - # new_preset = create_preset(self.selected_device) - # TODO create a preset file, tell custom_mapping to clear itself - # and dump itself into a new file - new_preset = 'new_preset' + new_preset = get_available_preset_name(self.selected_device) + custom_mapping.save(self.selected_device, new_preset) self.get('preset_selection').append(new_preset, new_preset) self.get('preset_selection').set_active_id(new_preset) except PermissionError as e: @@ -304,7 +302,7 @@ class Window: logger.debug('Selecting preset "%s"', preset) self.selected_preset = preset - # TODO load config into custom_mapping + custom_mapping.load(self.selected_device, self.selected_preset) key_list = self.get('key_list') for keycode, output in custom_mapping: @@ -312,7 +310,7 @@ class Window: window=self, delete_callback=self.on_row_removed, keycode=keycode, - character=output[1] + character=output ) key_list.insert(single_key_mapping, -1) diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 1bbd6914..433bc935 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -22,9 +22,13 @@ """Contains and manages mappings.""" +import os import json +import shutil from keymapper.logger import logger +from keymapper.paths import get_config_path +from keymapper.presets import get_available_preset_name class Mapping: @@ -72,6 +76,10 @@ class Mapping: return False if new_keycode and character: + if isinstance(character, list): + character = [c.lower() for c in character] + else: + character = character.lower() self._mapping[new_keycode] = character if new_keycode != previous_keycode: # clear previous mapping of that code, because the line @@ -100,21 +108,46 @@ class Mapping: def load(self, device, preset): """Load a dumped JSON from home to overwrite the mappings.""" - # TODO + # TODO test + path = get_config_path(device, preset) + logger.info('Loading preset from %s', path) + + if not os.path.exists(path): + logger.error('Tried to load non-existing preset %s', path) + return + + with open(path, 'r') as f: + self._mapping = json.load(f) + + self.changed = False def save(self, device, preset): """Dump as JSON into home.""" - # TODO + # TODO test + path = get_config_path(device, preset) + logger.info('Saving preset to %s', path) + + if not os.path.exists(path): + logger.debug('Creating "%s"', path) + os.makedirs(os.path.dirname(path), exist_ok=True) + os.mknod(path) + # if this is done with sudo rights, give the file to the user + shutil.chown(path, os.getlogin()) + + with open(path, 'w') as f: + json.dump(self._mapping, f) + + self.changed = False 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(): + # note, that stored mappings are already lowercase if isinstance(mapping, list): - if character in [c.lower() for c in mapping]: + if character in [c for c in mapping]: return keycode - elif mapping.lower() == character: + elif mapping == character: return int(keycode) return None diff --git a/keymapper/paths.py b/keymapper/paths.py index af858592..30c0f273 100644 --- a/keymapper/paths.py +++ b/keymapper/paths.py @@ -24,40 +24,14 @@ import os -# the path that contains ALL symbols, not just ours -X11_SYMBOLS = '/usr/share/X11/xkb/symbols' +EMPTY_SYMBOLS = '/usr/share/X11/xkb/symbols/key-mapper-empty' +CONFIG = os.path.join('/home', os.getlogin(), '.config/key-mapper') -# 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(' ', '_') -) -# those are the same for every preset and user, they are needed to make the -# presets work. -KEYCODES_PATH = '/usr/share/X11/xkb/keycodes/key-mapper' - - -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. - """ +def get_config_path(device=None, preset=None): + """Get a path to the stored preset, or to store a preset to.""" 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') -EMPTY_SYMBOLS = get_usr_path('empty') + return CONFIG + if preset is None: + return os.path.join(CONFIG, device) + return os.path.join(CONFIG, device, preset) diff --git a/keymapper/presets.py b/keymapper/presets.py index 2161ea6c..c5270295 100644 --- a/keymapper/presets.py +++ b/keymapper/presets.py @@ -26,11 +26,25 @@ import os import time import glob -from keymapper.paths import get_usr_path, USERS_SYMBOLS +from keymapper.paths import get_config_path from keymapper.logger import logger from keymapper.getdevices import get_devices +def get_available_preset_name(device, preset='new preset'): + """Increment the preset name until it is available.""" + preset = preset.strip() + + # find a name that is not already taken + if os.path.exists(get_config_path(device, preset)): + i = 2 + while os.path.exists(get_config_path(device, f'{preset} {i}')): + i += 1 + return f'{preset} {i}' + + return preset + + def get_presets(device): """Get all configured presets for the device, sorted by modification date. @@ -38,7 +52,7 @@ def get_presets(device): ---------- device : string """ - device_folder = get_usr_path(device) + device_folder = get_config_path(device) if not os.path.exists(device_folder): os.makedirs(device_folder) presets = [ @@ -78,12 +92,12 @@ def find_newest_preset(device=None): # sort the oldest files to the front in order to use pop to get the newest if device is None: paths = sorted( - glob.glob(os.path.join(USERS_SYMBOLS, '*/*')), + glob.glob(os.path.join(get_config_path(), '*/*')), key=os.path.getmtime ) else: paths = sorted( - glob.glob(os.path.join(get_usr_path(device), '*')), + glob.glob(os.path.join(get_config_path(device), '*')), key=os.path.getmtime ) @@ -121,7 +135,7 @@ def find_newest_preset(device=None): def delete_preset(device, preset): """Delete a preset from the file system.""" - preset_path = get_usr_path(device, preset) + preset_path = get_config_path(device, preset) if not os.path.exists(preset_path): logger.debug('Cannot remove non existing path "%s"', preset_path) return @@ -129,7 +143,7 @@ def delete_preset(device, preset): logger.info('Removing "%s"', preset_path) os.remove(preset_path) - device_path = get_usr_path(device) + device_path = get_config_path(device) if os.path.exists(device_path) and len(os.listdir(device_path)) == 0: logger.debug('Removing empty dir "%s"', device_path) os.rmdir(device_path) @@ -137,18 +151,15 @@ def delete_preset(device, preset): def rename_preset(device, old_preset_name, new_preset_name): """Rename a preset while avoiding name conflicts.""" - new_preset_name = new_preset_name.strip() - # find a name that is not already taken - if os.path.exists(get_usr_path(device, new_preset_name)): - i = 2 - while os.path.exists(get_usr_path(device, f'{new_preset_name} {i}')): - i += 1 - new_preset_name = f'{new_preset_name} {i}' + if new_preset_name == old_preset_name: + return + + new_preset_name = get_available_preset_name(device, new_preset_name) logger.info('Moving "%s" to "%s"', old_preset_name, new_preset_name) os.rename( - get_usr_path(device, old_preset_name), - get_usr_path(device, new_preset_name) + get_config_path(device, old_preset_name), + get_config_path(device, new_preset_name) ) # set the modification date to now now = time.time() - os.utime(get_usr_path(device, new_preset_name), (now, now)) + os.utime(get_config_path(device, new_preset_name), (now, now)) diff --git a/tests/testcases/config.py b/tests/testcases/config.py index fde4aa19..e08ede2a 100644 --- a/tests/testcases/config.py +++ b/tests/testcases/config.py @@ -25,8 +25,8 @@ import shutil from keymapper.archive.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 + get_preset_name, create_default_symbols, parse_symbols_file, \ + get_usr_path, KEYCODES_PATH, USERS_SYMBOLS from test import tmp diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index f840763a..8f5f66d7 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -33,7 +33,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk from keymapper.mapping import custom_mapping -from keymapper.paths import USERS_SYMBOLS, KEYCODES_PATH +from keymapper.paths import CONFIG from test import tmp @@ -188,7 +188,7 @@ class Integration(unittest.TestCase): self.window.get('preset_name_input').set_text('asdf') self.window.on_save_preset_clicked(None) self.assertEqual(self.window.selected_preset, 'asdf') - self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/asdf')) + self.assertTrue(os.path.exists(f'{CONFIG}/device_1/asdf')) self.assertEqual(custom_mapping.get_character(14), 'b') def test_select_device_and_preset(self): @@ -204,15 +204,15 @@ class Integration(unittest.TestCase): # created on start because the first device is selected and some empty # preset prepared. - self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset')) + self.assertTrue(os.path.exists(f'{CONFIG}/device_1/new_preset')) self.assertEqual(self.window.selected_device, 'device 1') self.assertEqual(self.window.selected_preset, 'new preset') # create another one self.window.on_create_preset_clicked(None) gtk_iteration() - self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset')) - self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset_2')) + self.assertTrue(os.path.exists(f'{CONFIG}/device_1/new_preset')) + self.assertTrue(os.path.exists(f'{CONFIG}/device_1/new_preset_2')) self.assertEqual(self.window.selected_preset, 'new preset 2') self.window.on_select_preset(FakeDropdown('new preset')) @@ -220,7 +220,7 @@ class Integration(unittest.TestCase): self.assertEqual(self.window.selected_preset, 'new preset') self.assertListEqual( - sorted(os.listdir(f'{USERS_SYMBOLS}/device_1')), + sorted(os.listdir(f'{CONFIG}/device_1')), ['new_preset', 'new_preset_2'] ) @@ -228,19 +228,18 @@ class Integration(unittest.TestCase): 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'{USERS_SYMBOLS}/device_1/abc_123')) + self.assertFalse(os.path.exists(f'{CONFIG}/device_1/abc_123')) custom_mapping.change(None, 10, '1') self.window.on_save_preset_clicked(None) - self.assertTrue(os.path.exists(KEYCODES_PATH)) gtk_iteration() self.assertEqual(self.window.selected_preset, 'abc 123') - self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/abc_123')) + self.assertTrue(os.path.exists(f'{CONFIG}/device_1/abc_123')) self.assertListEqual( - sorted(os.listdir(USERS_SYMBOLS)), + sorted(os.listdir(CONFIG)), ['default', 'device_1'] ) self.assertListEqual( - sorted(os.listdir(f'{USERS_SYMBOLS}/device_1')), + sorted(os.listdir(f'{CONFIG}/device_1')), ['abc_123', 'new_preset_2'] ) diff --git a/tests/testcases/presets.py b/tests/testcases/presets.py index 3f087353..4a38cbb6 100644 --- a/tests/testcases/presets.py +++ b/tests/testcases/presets.py @@ -26,8 +26,7 @@ import time from keymapper.presets import find_newest_preset, rename_preset, \ get_any_preset, delete_preset -from keymapper.archive.xkb import create_preset -from keymapper.paths import USERS_SYMBOLS +from keymapper.archive.xkb import create_preset, USERS_SYMBOLS from test import tmp diff --git a/tests/testcases/test.py b/tests/testcases/test.py index 935a8bc9..a258b071 100644 --- a/tests/testcases/test.py +++ b/tests/testcases/test.py @@ -22,7 +22,7 @@ import unittest from keymapper.getdevices import get_devices -from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, \ +from keymapper.archive.xkb import USERS_SYMBOLS, X11_SYMBOLS, \ DEFAULT_SYMBOLS