Exemplo n.º 1
0
    def autoload_single(self, device):
        """Inject the configured autoload preset for the device.

        If the preset is already being injected, it won't autoload it again.

        Parameters
        ----------
        device : str
            The name of the device as indexed in get_devices()
        """
        if device.startswith('/dev/input/'):
            # this is only here to avoid confusing console output,
            # block invalid requests before any logs are written.
            # Those requests are rejected later anyway.
            try:
                name = evdev.InputDevice(device).name
                if 'key-mapper' in name:
                    return
            except OSError:
                return

        logger.info('Request to autoload for "%s"', device)

        if self.config_dir is None:
            logger.error(
                'Tried to autoload %s without configuring the daemon first '
                'via set_config_dir.', device)
            return

        self._autoload(device)
Exemplo n.º 2
0
    def save(self, path):
        """Dump as JSON into home."""
        logger.info('Saving preset to %s', path)

        touch(path)

        with open(path, 'w') as file:
            if self._config.get('mapping') is not None:
                logger.error(
                    '"mapping" is reserved and cannot be used as config key')
            preset_dict = self._config

            # make sure to keep the option to add metadata if ever needed,
            # so put the mapping into a special key
            json_ready_mapping = {}
            # tuple keys are not possible in json, encode them as string
            for key, value in self._mapping.items():
                new_key = '+'.join([
                    ','.join([str(value) for value in sub_key])
                    for sub_key in key
                ])
                json_ready_mapping[new_key] = value

            preset_dict['mapping'] = json_ready_mapping
            json.dump(preset_dict, file, indent=4)
            file.write('\n')

        self.changed = False
Exemplo n.º 3
0
    def test_default(self):
        path = add_filehandler(os.path.join(tmp, 'logger-test'))
        update_verbosity(debug=False)
        logger.error('abc')
        logger.warning('foo')
        logger.info('123')
        logger.debug('456')
        logger.spam('789')
        with open(path, 'r') as f:
            content = f.read().lower()
            self.assertNotIn('logger.py', content)
            self.assertNotIn('line', content)

            self.assertIn('error', content)
            self.assertIn('abc', content)

            self.assertIn('warn', content)
            self.assertIn('foo', content)

            self.assertNotIn('info', content)
            self.assertIn('123', content)

            self.assertNotIn('debug', content)
            self.assertNotIn('456', content)

            self.assertNotIn('spam', content)
            self.assertNotIn('789', content)
Exemplo n.º 4
0
    def load_config(self, path=None):
        """Load the config from the file system.

        Parameters
        ----------
        path : string or None
            If set, will change the path to load from and save to.
        """
        if path is not None:
            if not os.path.exists(path):
                logger.error('Config at "%s" not found', path)
                return

            self.path = path

        self.clear_config()

        if not os.path.exists(self.path):
            # treated like an empty config
            logger.debug('Config "%s" doesn\'t exist yet', self.path)
            self.clear_config()
            self._config = copy.deepcopy(INITIAL_CONFIG)
            self.save_config()
            return

        with open(self.path, 'r') as file:
            try:
                self._config.update(json.load(file))
                logger.info('Loaded config from "%s"', self.path)
            except json.decoder.JSONDecodeError as error:
                logger.error('Failed to parse config "%s": %s. Using defaults',
                             self.path, str(error))
Exemplo n.º 5
0
    def test_debug(self):
        path = os.path.join(tmp, 'logger-test')
        add_filehandler(path)
        logger.error('abc')
        logger.warning('foo')
        logger.info('123')
        logger.debug('456')
        logger.spam('789')
        with open(path, 'r') as f:
            content = f.read().lower()
            self.assertIn('logger.py', content)

            self.assertIn('error', content)
            self.assertIn('abc', content)

            self.assertIn('warn', content)
            self.assertIn('foo', content)

            self.assertIn('info', content)
            self.assertIn('123', content)

            self.assertIn('debug', content)
            self.assertIn('456', content)

            self.assertIn('spam', content)
            self.assertIn('789', content)
Exemplo n.º 6
0
 def show_device_mapping_status(self):
     """Figure out if this device is currently under keymappers control."""
     device = self.selected_device
     if self.dbus.is_injecting(device):
         logger.info('Device "%s" is currently mapped', device)
         self.get('apply_system_layout').set_opacity(1)
     else:
         self.get('apply_system_layout').set_opacity(0.4)
Exemplo n.º 7
0
    def stop_injecting(self):
        """Stop injecting keycodes.

        Can be safely called from the main procss.
        """
        logger.info('Stopping injecting keycodes for device "%s"', self.device)
        self._msg_pipe[1].send(CLOSE)
        self._state = STOPPED
Exemplo n.º 8
0
    def stop(self):
        """Stop all injections and end the service.

        Raises dbus.exceptions.DBusException in your main process.
        """
        logger.info('Stopping all injections')
        for injector in self.injectors.values():
            injector.stop_injecting()
Exemplo n.º 9
0
 def show_device_mapping_status(self):
     """Figure out if this device is currently under keymappers control."""
     group_key = self.group.key
     state = self.dbus.get_state(group_key)
     if state == RUNNING:
         logger.info('Group "%s" is currently mapped', group_key)
         self.get('apply_system_layout').set_opacity(1)
     else:
         self.get('apply_system_layout').set_opacity(0.4)
Exemplo n.º 10
0
    def save_config(self):
        """Save the config to the file system."""
        touch(self.path)

        with open(self.path, 'w') as file:
            json.dump(self._config, file, indent=4)
            logger.info('Saved config to %s', self.path)
            shutil.chown(self.path, USER, USER)
            file.write('\n')
Exemplo n.º 11
0
    def can_modify_mapping(self, *_):
        """Show a message if changing the mapping is not possible."""
        if not self.dbus.is_injecting(self.selected_device):
            return

        # because the device is in grab mode by the daemon and
        # therefore the original keycode inaccessible
        logger.info('Cannot change keycodes while injecting')
        self.show_status(CTX_ERROR, 'Use "Restore Defaults" before editing')
Exemplo n.º 12
0
    def __init__(self):
        self.path = os.path.join(CONFIG_PATH, 'config.json')

        # migrate from < 0.4.0, add the .json ending
        deprecated_path = os.path.join(CONFIG_PATH, 'config')
        if os.path.exists(deprecated_path) and not os.path.exists(self.path):
            logger.info('Moving "%s" to "%s"', deprecated_path, self.path)
            os.rename(os.path.join(CONFIG_PATH, 'config'), self.path)

        super().__init__()
Exemplo n.º 13
0
    def save_preset(self):
        """Write changes to presets to disk."""
        logger.info('Updating configs for "%s", "%s"', self.selected_device,
                    self.selected_preset)

        path = get_preset_path(self.selected_device, self.selected_preset)
        custom_mapping.save(path)

        custom_mapping.changed = False
        self.unhighlight_all_rows()
Exemplo n.º 14
0
def touch(path, log=True):
    """Create an empty file and all its parent dirs, give it to the user."""
    if os.path.exists(path):
        return

    if log:
        logger.info('Creating file "%s"', path)

    mkdir(os.path.dirname(path), log=False)

    os.mknod(path)
    shutil.chown(path, USER, USER)
Exemplo n.º 15
0
    def on_apply_preset_clicked(self, _):
        """Apply a preset without saving changes."""
        self.save_preset()

        if custom_mapping.num_saved_keys == 0:
            logger.error('Cannot apply empty preset file')
            # also helpful for first time use
            if custom_mapping.changed:
                self.show_status(
                    CTX_ERROR, 'You need to save your changes first',
                    'No mappings are stored in the preset .json file yet')
            else:
                self.show_status(CTX_ERROR,
                                 'You need to add keys and save first')
            return

        preset = self.selected_preset
        device = self.selected_device

        logger.info('Applying preset "%s" for "%s"', preset, device)

        if not self.button_left_warn:
            if custom_mapping.dangerously_mapped_btn_left():
                self.show_status(
                    CTX_ERROR, 'This would disable your click button',
                    'Map a button to BTN_LEFT to avoid this.\n'
                    'To overwrite this warning, press apply again.')
                self.button_left_warn = True
                return

        if not self.unreleased_warn:
            unreleased = reader.get_unreleased_keys()
            if unreleased is not None and unreleased != Key.btn_left():
                # it's super annoying if that happens and may break the user
                # input in such a way to prevent disabling the mapping
                logger.error(
                    'Tried to apply a preset while keys were held down: %s',
                    unreleased)
                self.show_status(
                    CTX_ERROR, 'Please release your pressed keys first',
                    'X11 will think they are held down forever otherwise.\n'
                    'To overwrite this warning, press apply again.')
                self.unreleased_warn = True
                return

        self.unreleased_warn = False
        self.button_left_warn = False
        self.dbus.set_config_dir(get_config_path())
        self.dbus.start_injecting(device, preset)

        self.show_status(CTX_APPLY, 'Starting injection...')

        GLib.timeout_add(100, self.show_injection_result)
Exemplo n.º 16
0
    def set_new_key(self, new_key):
        """Check if a keycode has been pressed and if so, display it.

        Parameters
        ----------
        new_key : Key
        """
        if new_key is not None and not isinstance(new_key, Key):
            raise TypeError('Expected new_key to be a Key object')

        # the newest_keycode is populated since the ui regularly polls it
        # in order to display it in the status bar.
        previous_key = self.get_key()

        # no input
        if new_key is None:
            return

        # it might end up being a key combination
        self.state = HOLDING

        # keycode didn't change, do nothing
        if new_key == previous_key:
            return

        # keycode is already set by some other row
        existing = custom_mapping.get_character(new_key)
        if existing is not None:
            msg = f'"{to_string(new_key)}" already mapped to "{existing}"'
            logger.info(msg)
            self.window.show_status(CTX_KEYCODE, msg)
            return

        # it's legal to display the keycode

        # always ask for get_child to set the label, otherwise line breaking
        # has to be configured again.
        self.set_keycode_input_label(to_string(new_key))

        self.key = new_key

        self.highlight()

        character = self.get_character()

        # 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
        custom_mapping.change(new_key=new_key,
                              character=character,
                              previous_key=previous_key)
Exemplo n.º 17
0
    def save_config(self):
        """Save the config to the file system."""
        if USER == 'root':
            logger.debug('Skipping config file creation for the root user')
            return

        touch(self.path)

        with open(self.path, 'w') as file:
            json.dump(self._config, file, indent=4)
            logger.info('Saved config to %s', self.path)
            file.write('\n')
Exemplo n.º 18
0
def rename_preset(device, old_preset_name, new_preset_name):
    """Rename one of the users presets while avoiding name conflicts."""
    if new_preset_name == old_preset_name:
        return None

    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_preset_path(device, old_preset_name),
              get_preset_path(device, new_preset_name))
    # set the modification date to now
    now = time.time()
    os.utime(get_preset_path(device, new_preset_name), (now, now))
    return new_preset_name
Exemplo n.º 19
0
    def load(self, path):
        """Load a dumped JSON from home to overwrite the mappings.

        Parameters
        path : string
            Path of the preset file
        """
        logger.info('Loading preset from "%s"', path)

        if not os.path.exists(path):
            raise FileNotFoundError(
                f'Tried to load non-existing preset "{path}"'
            )

        self.clear_config()

        with open(path, 'r') as file:
            preset_dict = json.load(file)

            if not isinstance(preset_dict.get('mapping'), dict):
                logger.error(
                    'Expected mapping to be a dict, but was %s. '
                    'Invalid preset config at "%s"',
                    preset_dict.get('mapping'), path
                )
                return

            for key, symbol in preset_dict['mapping'].items():
                try:
                    key = Key(*[
                        split_key(chunk) for chunk in key.split('+')
                        if chunk.strip() != ''
                    ])
                except ValueError as error:
                    logger.error(str(error))
                    continue

                if None in key:
                    continue

                logger.spam('%s maps to %s', key, symbol)
                self._mapping[key] = symbol

            # add any metadata of the mapping
            for key in preset_dict:
                if key == 'mapping':
                    continue
                self._config[key] = preset_dict[key]

        self.changed = False
        self.num_saved_keys = len(self)
Exemplo n.º 20
0
def delete_preset(device, preset):
    """Delete one of the users presets."""
    preset_path = get_preset_path(device, preset)
    if not os.path.exists(preset_path):
        logger.debug('Cannot remove non existing path "%s"', preset_path)
        return

    logger.info('Removing "%s"', preset_path)
    os.remove(preset_path)

    device_path = get_preset_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)
Exemplo n.º 21
0
def mkdir(path, log=True):
    """Create a folder, give it to the user."""
    if os.path.exists(path):
        return

    if log:
        logger.info('Creating dir "%s"', path)

    # give all newly created folders to the user.
    # e.g. if .config/key-mapper/mouse/ is created the latter two
    base = os.path.split(path)[0]
    mkdir(base, log=False)

    os.makedirs(path)
    shutil.chown(path, USER, USER)
Exemplo n.º 22
0
def touch(path, log=True):
    """Create an empty file and all its parent dirs, give it to the user."""
    if path.endswith('/'):
        raise ValueError(f'Expected path to not end with a slash: {path}')

    if os.path.exists(path):
        return

    if log:
        logger.info('Creating file "%s"', path)

    mkdir(os.path.dirname(path), log=False)

    os.mknod(path)
    chown(path)
Exemplo n.º 23
0
    def connect(cls, fallback=True):
        """Get an interface to start and stop injecting keystrokes.

        Parameters
        ----------
        fallback : bool
            If true, returns an instance of the daemon instead if it cannot
            connect
        """
        try:
            bus = SystemBus()
            interface = bus.get(BUS_NAME)
            logger.info('Connected to the service')
        except GLib.GError as error:
            if not fallback:
                logger.error('Service not running? %s', error)
                return None

            logger.info('Starting the service')
            # Blocks until pkexec is done asking for the password.
            # Runs via key-mapper-control so that auth_admin_keep works
            # for all pkexec calls of the gui
            debug = ' -d' if is_debug() else ''
            cmd = f'pkexec key-mapper-control --command start-daemon {debug}'

            # using pkexec will also cause the service to continue running in
            # the background after the gui has been closed, which will keep
            # the injections ongoing

            logger.debug('Running `%s`', cmd)
            os.system(cmd)
            time.sleep(0.2)

            # try a few times if the service was just started
            for attempt in range(3):
                try:
                    interface = bus.get(BUS_NAME)
                    break
                except GLib.GError as error:
                    logger.debug(
                        'Attempt %d to connect to the service failed: "%s"',
                        attempt + 1, error)
                time.sleep(0.2)
            else:
                logger.error('Failed to connect to the service')
                sys.exit(1)

        return interface
Exemplo n.º 24
0
    def set_autoload_preset(self, device, preset):
        """Set a preset to be automatically applied on start.

        Parameters
        ----------
        device : string
        preset : string or None
            if None, don't autoload something for this device.
        """
        if preset is not None:
            self.set(f'autoload.{device}', preset)
        else:
            logger.info(
                'Not loading injecting for "%s" automatically anmore',
                device
            )
            self.remove(f'autoload.{device}')
Exemplo n.º 25
0
    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']).decode()
            xmodmap = xmodmap.lower()
            mappings = re.findall(r'(\d+) = (.+)\n', xmodmap + '\n')
            for keycode, names in mappings:
                # 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 mappings:
                # 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 xmodmap_dict.get(name) is None:
                        xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET
        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.info('Writing "%s"', path)
                json.dump(xmodmap_dict, file, indent=4)

        self._mapping.update(xmodmap_dict)

        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)
Exemplo n.º 26
0
    def set(self, path, value):
        """Set a config key.

        Parameters
        ----------
        path : string or string[]
            For example 'macros.keystroke_sleep_ms'
            or ['macros', 'keystroke_sleep_ms']
        value : any
        """
        logger.info('Changing "%s" to "%s" in %s', path, value,
                    self.__class__.__name__)

        def callback(parent, child, chunk):
            parent[chunk] = value

        self._resolve(path, callback)
Exemplo n.º 27
0
    def _find_groups(self):
        """Look for devices and group them together.

        Since this needs to do some stuff with /dev and spawn processes the
        result is cached. Use refresh_groups if you need up to date
        devices.
        """
        pipe = multiprocessing.Pipe()
        _FindGroups(pipe[1]).start()
        # block until groups are available
        self.loads(pipe[0].recv())

        if len(self._groups) == 0:
            logger.debug('Did not find any input device')
        else:
            keys = [f'"{group.key}"' for group in self._groups]
            logger.info('Found %s', ', '.join(keys))
Exemplo n.º 28
0
    def set_autoload_preset(self, group_key, preset):
        """Set a preset to be automatically applied on start.

        Parameters
        ----------
        group_key : string
            the unique identifier of the group. This is used instead of the
            name to enable autoloading two different presets when two similar
            devices are connected.
        preset : string or None
            if None, don't autoload something for this device.
        """
        if preset is not None:
            self.set(['autoload', group_key], preset)
        else:
            logger.info('Not injecting for "%s" automatically anmore',
                        group_key)
            self.remove(['autoload', group_key])
Exemplo n.º 29
0
def parse(macro, mapping, return_errors=False):
    """parse and generate a _Macro that can be run as often as you want.

    If it could not be parsed, possibly due to syntax errors, will log the
    error and return None.

    Parameters
    ----------
    macro : string
        "r(3, k(a).w(10))"
        "r(2, k(a).k(-)).k(b)"
        "w(1000).m(Shift_L, r(2, k(a))).w(10, 20).k(b)"
    mapping : Mapping
        The preset object, needed for some config stuff
    return_errors : bool
        If True, returns errors as a string or None if parsing worked.
        If False, returns the parsed macro.
    """
    macro = handle_plus_syntax(macro)

    # whitespaces, tabs, newlines and such don't serve a purpose. make
    # the log output clearer and the parsing easier.
    macro = re.sub(r'\s', '', macro)

    if '"' in macro or "'" in macro:
        logger.info('Quotation marks in macros are not needed')
        macro = macro.replace('"', '').replace("'", '')

    if return_errors:
        logger.spam('checking the syntax of %s', macro)
    else:
        logger.spam('preparing macro %s for later execution', macro)

    try:
        macro_object = _parse_recurse(macro, mapping)
        return macro_object if not return_errors else None
    except Exception as error:
        logger.error('Failed to parse macro "%s": %s', macro, error.__repr__())
        # print the traceback in case this is a bug of key-mapper
        logger.debug(''.join(traceback.format_tb(error.__traceback__)).strip())
        return str(error) if return_errors else None
Exemplo n.º 30
0
    def autoload(self):
        """Load all autoloaded presets for the current config_dir.

        If the preset is already being injected, it won't autoload it again.
        """
        if self.config_dir is None:
            logger.error(
                'Tried to autoload without configuring the daemon first '
                'via set_config_dir.')
            return

        autoload_presets = list(config.iterate_autoload_presets())

        logger.info('Autoloading for all devices')

        if len(autoload_presets) == 0:
            logger.error('No presets configured to autoload')
            return

        for group_key, _ in autoload_presets:
            self._autoload(group_key)