#!/usr/bin/python3 # -*- coding: utf-8 -*- # key-mapper - GUI for device specific keyboard mappings # Copyright (C) 2021 sezanzeb # # 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 . """Create some singleton objects that are needed for the app to work.""" import stat import re import json import subprocess import evdev from keymapper.logger import logger from keymapper.mapping import Mapping, DISABLE_NAME, DISABLE_CODE from keymapper.paths import get_config_path, touch, USER # xkb uses keycodes that are 8 higher than those from evdev XKB_KEYCODE_OFFSET = 8 XMODMAP_FILENAME = 'xmodmap.json' class SystemMapping: """Stores information about all available keycodes.""" def __init__(self): """Construct the system_mapping.""" self._mapping = {} self._xmodmap = {} self._case_insensitive_mapping = {} self.populate() def list_names(self): """Return an array of all possible names in the mapping.""" return self._mapping.keys() def correct_case(self, character): """Return the correct casing for a character.""" if character in self._mapping: return character # only if not e.g. both "a" and "A" are in the mapping return self._case_insensitive_mapping.get(character.lower(), character) def populate(self): """Get a mapping of all available names to their keycodes.""" logger.debug('Gathering available keycodes') self.clear() xmodmap_dict = {} try: xmodmap = subprocess.check_output( ['xmodmap', '-pke'], stderr=subprocess.STDOUT ).decode() xmodmap = xmodmap self._xmodmap = re.findall(r'(\d+) = (.+)\n', xmodmap + '\n') xmodmap_dict = self._find_legit_mappings() except (subprocess.CalledProcessError, FileNotFoundError): # might be within a tty pass if USER != 'root': # write this stuff into the key-mapper config directory, because # the systemd service won't know the user sessions xmodmap path = get_config_path(XMODMAP_FILENAME) touch(path) with open(path, 'w') as file: logger.debug('Writing "%s"', path) json.dump(xmodmap_dict, file, indent=4) for name, code in xmodmap_dict.items(): self._set(name, code) for name, ecode in evdev.ecodes.ecodes.items(): if name.startswith('KEY') or name.startswith('BTN'): self._set(name, ecode) self._set(DISABLE_NAME, DISABLE_CODE) def update(self, mapping): """Update this with new keys. Parameters ---------- mapping : dict maps from name to code. Make sure your keys are lowercase. """ for name, code in mapping.items(): self._set(name, code) def _set(self, name, code): """Map name to code.""" self._mapping[str(name)] = code self._case_insensitive_mapping[str(name).lower()] = name def get(self, name): """Return the code mapped to the key.""" # the correct casing should be shown when asking the system_mapping # for stuff. indexing case insensitive to support old < 0.8.1 presets. if name not in self._mapping: # only if not e.g. both "a" and "A" are in the mapping name = self._case_insensitive_mapping.get(str(name).lower()) return self._mapping.get(name) def clear(self): """Remove all mapped keys. Only needed for tests.""" keys = list(self._mapping.keys()) for key in keys: del self._mapping[key] def get_name(self, code): """Get the first matching name for the code.""" for entry in self._xmodmap: if int(entry[0]) - XKB_KEYCODE_OFFSET == code: return entry[1].split()[0] return None def _find_legit_mappings(self): """From the parsed xmodmap list find usable symbols and their codes.""" xmodmap_dict = {} for keycode, names in self._xmodmap: # there might be multiple, like: # keycode 64 = Alt_L Meta_L Alt_L Meta_L # keycode 204 = NoSymbol Alt_L NoSymbol Alt_L # Alt_L should map to code 64. Writing code 204 only works # if a modifier is applied at the same time. So take the first # one. name = names.split()[0] xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET for keycode, names in self._xmodmap: # but since KP may be mapped like KP_Home KP_7 KP_Home KP_7, # make another pass and add all of them if they don't already # exist. don't overwrite any keycodes. for name in names.split(): if 'KP_' in name and xmodmap_dict.get(name) is None: xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET return xmodmap_dict # one mapping object for the GUI application custom_mapping = Mapping() # this mapping represents the xmodmap output, which stays constant system_mapping = SystemMapping() # permissions for files created in /usr _PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH