Improved error formatting in status bar (#635)

pull/373/head
Tobi 1 year ago committed by GitHub
parent 23af936688
commit b88b019ce0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -419,6 +419,7 @@ Shortcut: ctrl + del</property>
<object class="GtkSwitch" id="preset_autoload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Activate this to load the preset next time the device connects, or when the user logs in</property>
<property name="halign">start</property>
<property name="valign">center</property>
</object>
@ -850,13 +851,13 @@ Shortcut: ctrl + del</property>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Available output axes are affected by the Target setting.</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Available output axes are affected by the Target setting.</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">Output axis</property>
<property name="xalign">1</property>

@ -20,9 +20,9 @@
from __future__ import annotations
import enum
from typing import Optional, Callable, Tuple, TypeVar, Literal, Union, Any, Dict
from collections import namedtuple
from typing import Optional, Callable, Tuple, TypeVar, Union, Any, Dict
import evdev
import pkg_resources
from evdev.ecodes import (
EV_KEY,
@ -48,9 +48,21 @@ from pydantic import (
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME
from inputremapper.exceptions import MacroParsingError
from inputremapper.configs.validation_errors import (
OutputSymbolUnknownError,
SymbolNotAvailableInTargetError,
OnlyOneAnalogInputError,
TriggerPointInRangeError,
OutputSymbolVariantError,
MacroButTypeOrCodeSetError,
SymbolAndCodeMismatchError,
MissingMacroOrKeyError,
MissingOutputAxisError,
MacroParsingError,
)
from inputremapper.gui.gettext import _
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.injection.global_uinputs import can_default_uinput_emit
from inputremapper.injection.macros.parse import is_this_a_macro, parse
from inputremapper.utils import get_evdev_constant_name
@ -337,28 +349,39 @@ class Mapping(UIMapping):
"""If the mapping is valid."""
return True
@validator("output_symbol", pre=True)
def validate_symbol(cls, symbol):
@root_validator(pre=True)
def validate_symbol(cls, values):
"""Parse a macro to check for syntax errors."""
if not symbol:
return None
symbol = values.get("output_symbol")
if symbol == "":
values["output_symbol"] = None
return values
if symbol is None:
return values
symbol = symbol.strip()
values["output_symbol"] = symbol
if symbol == DISABLE_NAME:
return values
if is_this_a_macro(symbol):
try:
parse(symbol, verbose=False) # raises MacroParsingError
return symbol
except MacroParsingError as exception:
# pydantic only catches ValueError, TypeError, and AssertionError
raise ValueError(exception) from exception
if system_mapping.get(symbol) is not None:
return symbol
raise ValueError(
f'The output_symbol "{symbol}" is not a macro and not a valid keycode-name'
)
mapping_mock = namedtuple("Mapping", values.keys())(**values)
# raises MacroParsingError
parse(symbol, mapping=mapping_mock, verbose=False)
return values
code = system_mapping.get(symbol)
if code is None:
raise OutputSymbolUnknownError(symbol)
target = values.get("target_uinput")
if target is not None and not can_default_uinput_emit(target, EV_KEY, code):
raise SymbolNotAvailableInTargetError(symbol, target)
return values
@validator("input_combination")
def only_one_analog_input(cls, combination) -> InputCombination:
@ -367,10 +390,7 @@ class Mapping(UIMapping):
"""
analog_events = [event for event in combination if event.defines_analog_input]
if len(analog_events) > 1:
raise ValueError(
f"Cannot map a combination of multiple analog inputs: {analog_events}"
"add trigger points (event.value != 0) to map as a button"
)
raise OnlyOneAnalogInputError(analog_events)
return combination
@ -383,10 +403,7 @@ class Mapping(UIMapping):
and input_config.analog_threshold
and abs(input_config.analog_threshold) >= 100
):
raise ValueError(
f"{input_config = } maps an absolute axis to a button, but the trigger "
"point (event.analog_threshold) is not between -100[%] and 100[%]"
)
raise TriggerPointInRangeError(input_config)
return combination
@root_validator
@ -396,10 +413,7 @@ class Mapping(UIMapping):
o_type = values.get("output_type")
o_code = values.get("output_code")
if o_symbol is None and (o_type is None or o_code is None):
raise ValueError(
"Missing Argument: Mapping must either contain "
"`output_symbol` or `output_type` and `output_code`"
)
raise OutputSymbolVariantError()
return values
@root_validator
@ -409,24 +423,21 @@ class Mapping(UIMapping):
type_ = values.get("output_type")
code = values.get("output_code")
if symbol is None:
return values # type and code can be anything
# If symbol is "", then validate_symbol changes it to None
# type and code can be anything
return values
if type_ is None and code is None:
return values # we have a symbol: no type and code is fine
# we have a symbol: no type and code is fine
return values
if is_this_a_macro(symbol): # disallow output type and code for macros
if is_this_a_macro(symbol):
# disallow output type and code for macros
if type_ is not None or code is not None:
raise ValueError(
"output_symbol is a macro: output_type "
"and output_code must be None"
)
raise MacroButTypeOrCodeSetError()
if code is not None and code != system_mapping.get(symbol) or type_ != EV_KEY:
raise ValueError(
"output_symbol and output_code mismatch: "
f"output macro is {symbol} --> {system_mapping.get(symbol)} "
f"but output_code is {code} --> {system_mapping.get_name(code)} "
)
raise SymbolAndCodeMismatchError(symbol, code)
return values
@root_validator
@ -435,28 +446,21 @@ class Mapping(UIMapping):
And vice versa."""
assert isinstance(values.get("input_combination"), InputCombination)
combination: InputCombination = values["input_combination"]
use_as_analog = True in [event.defines_analog_input for event in combination]
analog_input_config = combination.find_analog_input_config()
use_as_analog = analog_input_config is not None
output_type = values.get("output_type")
output_symbol = values.get("output_symbol")
if not use_as_analog and not output_symbol and output_type != EV_KEY:
raise ValueError(
"missing macro or key: "
f'"{str(combination)}" is not used as analog input, '
f"but no output macro or key is programmed"
)
raise MissingMacroOrKeyError()
if (
use_as_analog
and output_type not in (EV_ABS, EV_REL)
and output_symbol != DISABLE_NAME
):
raise ValueError(
"Missing output axis: "
f'"{str(combination)}" is used as analog input, '
f"but the {output_type = } is not an axis "
)
raise MissingOutputAxisError(analog_input_config, output_type)
return values

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# This file is part of input-remapper.
#
# input-remapper 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.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""Exceptions that are thrown when configurations are incorrect."""
# can't merge this with exceptions.py, because I want error constructors here to
# be intelligent to avoid redundant code, and they need imports, which would cause
# circular imports.
# pydantic only catches ValueError, TypeError, and AssertionError
from __future__ import annotations
from typing import Optional
from evdev.ecodes import EV_KEY
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.injection.global_uinputs import find_fitting_default_uinputs
class OutputSymbolVariantError(ValueError):
def __init__(self):
super().__init__(
"Missing Argument: Mapping must either contain "
"`output_symbol` or `output_type` and `output_code`"
)
class TriggerPointInRangeError(ValueError):
def __init__(self, input_config):
super().__init__(
f"{input_config = } maps an absolute axis to a button, but the "
"trigger point (event.analog_threshold) is not between -100[%] "
"and 100[%]"
)
class OnlyOneAnalogInputError(ValueError):
def __init__(self, analog_events):
super().__init__(
f"Cannot map a combination of multiple analog inputs: {analog_events}"
"add trigger points (event.value != 0) to map as a button"
)
class SymbolNotAvailableInTargetError(ValueError):
def __init__(self, symbol, target):
code = system_mapping.get(symbol)
fitting_targets = find_fitting_default_uinputs(EV_KEY, code)
fitting_targets_string = '", "'.join(fitting_targets)
super().__init__(
f'The output_symbol "{symbol}" is not available for the "{target}" '
+ f'target. Try "{fitting_targets_string}".'
)
class OutputSymbolUnknownError(ValueError):
def __init__(self, symbol: str):
super().__init__(
f'The output_symbol "{symbol}" is not a macro and not a valid '
+ "keycode-name"
)
class MacroButTypeOrCodeSetError(ValueError):
def __init__(self):
super().__init__(
"output_symbol is a macro: output_type " "and output_code must be None"
)
class SymbolAndCodeMismatchError(ValueError):
def __init__(self, symbol, code):
super().__init__(
"output_symbol and output_code mismatch: "
f"output macro is {symbol} -> {system_mapping.get(symbol)} "
f"but output_code is {code} -> {system_mapping.get_name(code)} "
)
class MissingMacroOrKeyError(ValueError):
def __init__(self):
super().__init__("missing macro or key")
class MissingOutputAxisError(ValueError):
def __init__(self, analog_input_config, output_type):
super().__init__(
"Missing output axis: "
f'"{analog_input_config}" is used as analog input, '
f"but the {output_type = } is not an axis "
)
class MacroParsingError(ValueError):
"""Macro syntax errors."""
def __init__(self, symbol: Optional[str] = None, msg="Error while parsing a macro"):
self.symbol = symbol
super().__init__(msg)
def pydantify(error: type):
"""Generate a string as it would appear IN pydantic error types.
This does not include the base class name, which is transformed to snake case in
pydantic. Example pydantic error type: "value_error.foobar" for FooBarError.
"""
# See https://github.com/pydantic/pydantic/discussions/5112
lower_classname = error.__name__.lower()
if lower_classname.endswith("error"):
return lower_classname[: -len("error")]
return lower_classname

@ -20,8 +20,6 @@
"""Exceptions specific to inputremapper."""
from typing import Optional
class Error(Exception):
"""Base class for exceptions in inputremapper.
@ -44,14 +42,6 @@ class EventNotHandled(Error):
super().__init__(f"Event {event} can not be handled by the configured target")
class MacroParsingError(Error):
"""Macro syntax errors."""
def __init__(self, symbol: Optional[str] = None, msg="Error while parsing a macro"):
self.symbol = symbol
super().__init__(msg)
class MappingParsingError(Error):
"""Anything that goes wrong during the creation of handlers from the mapping."""

@ -24,9 +24,7 @@
from __future__ import annotations
import gi
from gi.repository import Gtk
from gi.repository import Gtk, Pango
from inputremapper.gui.controller import Controller
from inputremapper.gui.messages.message_broker import (
@ -79,18 +77,17 @@ class StatusBar:
self._error_icon = error_icon
self._warning_icon = warning_icon
label = self._gui.get_message_area().get_children()[0]
label.set_ellipsize(Pango.EllipsizeMode.END)
label.set_selectable(True)
self._message_broker.subscribe(MessageType.status_msg, self._on_status_update)
self._message_broker.subscribe(MessageType.init, self._on_init)
# keep track if there is an error or warning in the stack of statusbar
# unfortunately this is not exposed over the api
self._error = False
self._warning = False
def _on_init(self, _):
self._error_icon.hide()
self._warning_icon.hide()
def _on_status_update(self, data: StatusData):
"""Show a status message and set its tooltip.
@ -118,24 +115,21 @@ class StatusBar:
self._error_icon.show()
status_bar.set_tooltip_text("")
else:
if tooltip is None:
tooltip = message
return
self._error_icon.hide()
self._warning_icon.hide()
if tooltip is None:
tooltip = message
if context_id in (CTX_ERROR, CTX_MAPPING):
self._error_icon.show()
self._error = True
self._error_icon.hide()
self._warning_icon.hide()
if context_id == CTX_WARNING:
self._warning_icon.show()
self._warning = True
if context_id in (CTX_ERROR, CTX_MAPPING):
self._error_icon.show()
self._error = True
max_length = 135
if len(message) > max_length:
message = message[: max_length - 3] + "..."
if context_id == CTX_WARNING:
self._warning_icon.show()
self._warning = True
status_bar.push(context_id, message)
status_bar.set_tooltip_text(tooltip)
status_bar.push(context_id, message)
status_bar.set_tooltip_text(tooltip)

@ -38,9 +38,18 @@ from evdev.ecodes import EV_KEY, EV_REL, EV_ABS
from gi.repository import Gtk
from inputremapper.configs.mapping import MappingData, UIMapping
from inputremapper.configs.mapping import (
MappingData,
UIMapping,
MacroButTypeOrCodeSetError,
SymbolAndCodeMismatchError,
MissingOutputAxisError,
MissingMacroOrKeyError,
OutputSymbolVariantError,
)
from inputremapper.configs.paths import sanitize_path_component
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.validation_errors import pydantify
from inputremapper.exceptions import DataManagementError
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
from inputremapper.gui.gettext import _
@ -145,6 +154,7 @@ class Controller:
"""Send mapping ValidationErrors to the MessageBroker."""
if not self.data_manager.active_preset:
return
if self.data_manager.active_preset.is_valid():
self.message_broker.publish(StatusData(CTX_MAPPING))
return
@ -154,18 +164,36 @@ class Controller:
continue
position = mapping.format_name()
msg = _("Mapping error at %s, hover for info") % position
self.show_status(CTX_MAPPING, msg, self._get_ui_error_string(mapping))
error_strings = self._get_ui_error_strings(mapping)
tooltip = ""
if len(error_strings) == 0:
# shouldn't be possible to get to this point
logger.error("Expected an error")
return
elif len(error_strings) > 1:
msg = _('%d Mapping errors at "%s", hover for info') % (
len(error_strings),
position,
)
tooltip = " " + "\n ".join(error_strings)
else:
msg = f'"{position}": {error_strings[0]}'
tooltip = error_strings[0]
@staticmethod
def _get_ui_error_string(mapping: UIMapping) -> str:
"""Get a human readable error message from a mapping error."""
error_string = str(mapping.get_error())
self.show_status(
CTX_MAPPING,
msg.replace("\n", " "),
tooltip,
)
# check all the different error messages which are not useful for the user
@staticmethod
def format_error_message(mapping, error_type, error_message: str) -> str:
"""Check all the different error messages which are not useful for the user."""
# There is no more elegant way of comparing error_type with the base class.
# https://github.com/pydantic/pydantic/discussions/5112
if (
"output_symbol is a macro:" in error_string
or "output_symbol and output_code mismatch:" in error_string
pydantify(MacroButTypeOrCodeSetError) in error_type
or pydantify(SymbolAndCodeMismatchError) in error_type
) and mapping.input_combination.defines_analog_input:
return _(
"Remove the macro or key from the macro input field "
@ -173,15 +201,15 @@ class Controller:
)
if (
"output_symbol is a macro:" in error_string
or "output_symbol and output_code mismatch:" in error_string
pydantify(MacroButTypeOrCodeSetError) in error_type
or pydantify(SymbolAndCodeMismatchError) in error_type
) and not mapping.input_combination.defines_analog_input:
return _(
"Remove the Analog Output Axis when specifying a macro or key output"
)
if "missing output axis:" in error_string:
message = _(
if pydantify(MissingOutputAxisError) in error_type:
error_message = _(
"The input specifies an analog axis, but no output axis is selected."
)
if mapping.output_symbol is not None:
@ -190,27 +218,64 @@ class Controller:
for event in mapping.input_combination
if event.defines_analog_input
][0]
message += _(
error_message += _(
"\nIf you mean to create a key or macro mapping "
"go to the advanced input configuration"
' and set a "Trigger Threshold" for '
f'"{event.description()}"'
)
return message
return error_message
if "missing macro or key:" in error_string and mapping.output_symbol is None:
message = _(
if (
pydantify(MissingMacroOrKeyError) in error_type
and mapping.output_symbol is None
):
error_message = _(
"The input specifies a key or macro input, but no macro or key is "
"programmed."
)
if mapping.output_type in (EV_ABS, EV_REL):
message += _(
error_message += _(
"\nIf you mean to create an analog axis mapping go to the "
'advanced input configuration and set an input to "Use as Analog".'
)
return message
return error_message
return error_message
@staticmethod
def _get_ui_error_strings(mapping: UIMapping) -> List[str]:
"""Get a human readable error message from a mapping error."""
validation_error = mapping.get_error()
if validation_error is None:
return []
formatted_errors = []
for error in validation_error.errors():
if pydantify(OutputSymbolVariantError) in error["type"]:
# this is rather internal, when this error appears in the gui, there is
# also always another more readable error at the same time that explains
# this problem.
continue
error_string = f'"{mapping.format_name()}": '
error_message = error["msg"]
error_location = error["loc"][0]
if error_location != "__root__":
error_string += f"{error_location}: "
# check all the different error messages which are not useful for the user
formatted_errors.append(
Controller.format_error_message(
mapping,
error["type"],
error_message,
)
)
return error_string
return formatted_errors
def get_a_preset(self) -> str:
"""Attempts to get the newest preset in the current group
@ -611,9 +676,9 @@ class Controller:
self.show_status,
CTX_ERROR,
"The device was not grabbed",
"Either another application is already grabbing it or "
"Either another application is already grabbing it, "
"your preset doesn't contain anything that is sent by the "
"device.",
"device or your preset contains errors",
),
InjectorState.UPGRADE_EVDEV: partial(
self.show_status,

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Union, Tuple, Optional
from typing import Dict, Union, Tuple, Optional, List
import evdev
@ -59,6 +59,21 @@ DEFAULT_UINPUTS["keyboard + mouse"] = {
}
def can_default_uinput_emit(target: str, type_: int, code: int) -> bool:
"""Check if the uinput with the target name is capable of the event."""
capabilities = DEFAULT_UINPUTS.get(target, {}).get(type_)
return capabilities is not None and code in capabilities
def find_fitting_default_uinputs(type_: int, code: int) -> List[str]:
"""Find the names of default uinputs that are able to emit this event."""
return [
uinput
for uinput in DEFAULT_UINPUTS
if code in DEFAULT_UINPUTS[uinput].get(type_, [])
]
class UInput(evdev.UInput):
def __init__(self, *args, **kwargs):
name = kwargs["name"]

@ -55,7 +55,11 @@ from evdev.ecodes import (
)
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.exceptions import MacroParsingError
from inputremapper.configs.validation_errors import (
SymbolNotAvailableInTargetError,
MacroParsingError,
)
from inputremapper.injection.global_uinputs import can_default_uinput_emit
from inputremapper.ipc.shared_dict import SharedDict
from inputremapper.logger import logger
@ -125,21 +129,6 @@ def _type_check(value: Any, allowed_types, display_name=None, position=None) ->
)
def _type_check_symbol(keyname: Union[str, Variable]) -> int:
"""Same as _type_check, but checks if the key-name is valid."""
if isinstance(keyname, Variable):
# it is a variable and will be read at runtime
return keyname
symbol = str(keyname)
code = system_mapping.get(symbol)
if code is None:
raise MacroParsingError(msg=f'Unknown key "{symbol}"')
return code
def _type_check_variablename(name: str):
"""Check if this is a legit variable name.
@ -216,6 +205,8 @@ class Macro:
self.context = context
self.mapping = mapping
# TODO check if mapping is ever none by throwing an error
# List of coroutines that will be called sequentially.
# This is the compiled code
self.tasks: List[MacroTask] = []
@ -317,12 +308,12 @@ class Macro:
"""Write the symbol."""
# This is done to figure out if the macro is broken at compile time, because
# if KEY_A was unknown we can show this in the gui before the injection starts.
_type_check_symbol(symbol)
self._type_check_symbol(symbol)
async def task(handler: Callable):
# if the code is $foo, figure out the correct code now.
resolved_symbol = _resolve(symbol, [str])
code = _type_check_symbol(resolved_symbol)
code = self._type_check_symbol(resolved_symbol)
resolved_code = _resolve(code, [int])
handler(EV_KEY, resolved_code, 1)
@ -334,11 +325,11 @@ class Macro:
def add_key_down(self, symbol: str):
"""Press the symbol."""
_type_check_symbol(symbol)
self._type_check_symbol(symbol)
async def task(handler: Callable):
resolved_symbol = _resolve(symbol, [str])
code = _type_check_symbol(resolved_symbol)
code = self._type_check_symbol(resolved_symbol)
resolved_code = _resolve(code, [int])
handler(EV_KEY, resolved_code, 1)
@ -347,11 +338,11 @@ class Macro:
def add_key_up(self, symbol: str):
"""Release the symbol."""
_type_check_symbol(symbol)
self._type_check_symbol(symbol)
async def task(handler: Callable):
resolved_symbol = _resolve(symbol, [str])
code = _type_check_symbol(resolved_symbol)
code = self._type_check_symbol(resolved_symbol)
resolved_code = _resolve(code, [int])
handler(EV_KEY, resolved_code, 0)
@ -370,11 +361,11 @@ class Macro:
# if macro is a key name, hold down the key while the
# keyboard key is physically held down
symbol = macro
_type_check_symbol(symbol)
self._type_check_symbol(symbol)
async def task(handler: Callable):
resolved_symbol = _resolve(symbol, [str])
code = _type_check_symbol(resolved_symbol)
code = self._type_check_symbol(resolved_symbol)
resolved_code = _resolve(code, [int])
handler(EV_KEY, resolved_code, 1)
@ -405,14 +396,14 @@ class Macro:
macro
"""
_type_check(macro, [Macro], "modify", 2)
_type_check_symbol(modifier)
self._type_check_symbol(modifier)
self.child_macros.append(macro)
async def task(handler: Callable):
# TODO test var
resolved_modifier = _resolve(modifier, [str])
code = _type_check_symbol(resolved_modifier)
code = self._type_check_symbol(resolved_modifier)
handler(EV_KEY, code, 1)
await self._keycode_pause()
@ -425,11 +416,11 @@ class Macro:
def add_hold_keys(self, *symbols):
"""Hold down multiple keys, equivalent to `a + b + c + ...`."""
for symbol in symbols:
_type_check_symbol(symbol)
self._type_check_symbol(symbol)
async def task(handler: Callable):
resolved_symbols = [_resolve(symbol, [str]) for symbol in symbols]
codes = [_type_check_symbol(symbol) for symbol in resolved_symbols]
codes = [self._type_check_symbol(symbol) for symbol in resolved_symbols]
for code in codes:
handler(EV_KEY, code, 1)
@ -698,3 +689,22 @@ class Macro:
await else_.run(handler)
self.tasks.append(task)
def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, int]:
"""Same as _type_check, but checks if the key-name is valid."""
if isinstance(keyname, Variable):
# it is a variable and will be read at runtime
return keyname
symbol = str(keyname)
code = system_mapping.get(symbol)
if code is None:
raise MacroParsingError(msg=f'Unknown key "{symbol}"')
if self.mapping is not None:
target = self.mapping.target_uinput
if target is not None and not can_default_uinput_emit(target, EV_KEY, code):
raise SymbolNotAvailableInTargetError(symbol, target)
return code

@ -25,7 +25,7 @@ import inspect
import re
from typing import Optional, Any
from inputremapper.exceptions import MacroParsingError
from inputremapper.configs.validation_errors import MacroParsingError
from inputremapper.injection.macros.macro import Macro, Variable
from inputremapper.logger import logger
@ -452,6 +452,7 @@ def parse(macro: str, context=None, mapping=None, verbose: bool = True):
verbose
log the parsing True by default
"""
# TODO pass mapping in frontend and do the target check for keys?
logger.debug("parsing macro %s", macro.replace("\n", ""))
macro = clean(macro)
macro = handle_plus_syntax(macro)

@ -17,7 +17,7 @@
<text x="32.5" y="14">coverage</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">92%</text>
<text x="82.0" y="14">92%</text>
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">93%</text>
<text x="82.0" y="14">93%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -87,6 +87,9 @@ sudo pip install .
New badges, if needed, will be created in `readme/` and they
just need to be commited.
Beware that coverage can suffer if old files reside in your python path. Remove the build folder
and reinstall it.
## Translations
To regenerate the `po/input-remapper.pot` file, run

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">8.88</text>
<text x="62.0" y="14">8.88</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">8.87</text>
<text x="62.0" y="14">8.87</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -905,8 +905,8 @@ class TestGui(GuiTestBase):
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
# 4. update target to mouse
self.target_selection.set_active_id("mouse")
# 4. update target
self.target_selection.set_active_id("keyboard + mouse")
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping,
@ -915,20 +915,14 @@ class TestGui(GuiTestBase):
[InputConfig(type=1, code=30, origin_hash=origin)]
),
output_symbol="Shift_L",
target_uinput="mouse",
target_uinput="keyboard + mouse",
),
)
def test_show_status(self):
self.message_broker.publish(StatusData(0, "a" * 500))
gtk_iteration()
text = self.get_status_text()
self.assertIn("...", text)
self.message_broker.publish(StatusData(0, "b"))
gtk_iteration()
self.message_broker.publish(StatusData(0, "a"))
text = self.get_status_text()
self.assertNotIn("...", text)
self.assertEqual("a", text)
def test_hat_switch(self):
# load a device with more capabilities

@ -42,7 +42,10 @@ from evdev.ecodes import (
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.exceptions import MacroParsingError
from inputremapper.configs.validation_errors import (
MacroParsingError,
SymbolNotAvailableInTargetError,
)
from inputremapper.injection.context import Context
from inputremapper.injection.macros.macro import (
Macro,
@ -117,6 +120,7 @@ class MacroTestBase(unittest.IsolatedAsyncioTestCase):
class DummyMapping:
macro_key_sleep_ms = 10
rel_rate = 60
target_uinput = "keyboard + mouse"
class TestMacros(MacroTestBase):
@ -520,7 +524,7 @@ class TestMacros(MacroTestBase):
self.assertRaises(MacroParsingError, parse, "wait(a)", self.context)
parse("ifeq(a, 2, k(a),)", self.context) # no error
parse("ifeq(a, 2, , k(a))", self.context) # no error
parse("ifeq(a, 2, None, k(a))", self.context, True) # no error
parse("ifeq(a, 2, None, k(a))", self.context) # no error
self.assertRaises(MacroParsingError, parse, "ifeq(a, 2, 1,)", self.context)
self.assertRaises(MacroParsingError, parse, "ifeq(a, 2, , 2)", self.context)
parse("if_eq(2, $a, k(a),)", self.context) # no error
@ -547,6 +551,15 @@ class TestMacros(MacroTestBase):
parse("add(a, 1)", self.context) # no error
self.assertRaises(MacroParsingError, parse, "add(a, b)", self.context)
# wrong target for BTN_A
self.assertRaises(
SymbolNotAvailableInTargetError,
parse,
"key(BTN_A)",
self.context,
DummyMapping,
)
async def test_key(self):
code_a = system_mapping.get("a")
code_b = system_mapping.get("b")

@ -28,6 +28,7 @@ from evdev.ecodes import (
REL_Y,
REL_WHEEL,
REL_WHEEL_HI_RES,
KEY_1,
)
from pydantic import ValidationError
@ -366,63 +367,103 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
m = Mapping(**cfg)
self.assertTrue(m.is_valid())
def test_wrong_target(self):
mapping = Mapping(
input_combination=[{"type": EV_KEY, "code": KEY_1}],
target_uinput="keyboard",
output_symbol="a",
)
mapping.set_combination_changed_callback(lambda *args: None)
self.assertRaisesRegex(
ValidationError,
# the error should mention
# - the symbol
# - the current incorrect target
# - the target that works for this symbol
".*BTN_A.*keyboard.*gamepad",
mapping.__setattr__,
"output_symbol",
"BTN_A",
)
def test_wrong_target_for_macro(self):
mapping = Mapping(
input_combination=[{"type": EV_KEY, "code": KEY_1}],
target_uinput="keyboard",
output_symbol="key(a)",
)
mapping.set_combination_changed_callback(lambda *args: None)
self.assertRaisesRegex(
ValidationError,
# the error should mention
# - the symbol
# - the current incorrect target
# - the target that works for this symbol
".*BTN_A.*keyboard.*gamepad",
mapping.__setattr__,
"output_symbol",
"key(BTN_A)",
)
class TestUIMapping(unittest.IsolatedAsyncioTestCase):
def test_init(self):
"""should be able to initialize without an error"""
"""Should be able to initialize without throwing errors."""
UIMapping()
def test_is_valid(self):
"""should be invalid at first
and become valid once all data is provided"""
m = UIMapping()
self.assertFalse(m.is_valid())
"""Should be invalid at first and become valid once all data is provided."""
mapping = UIMapping()
self.assertFalse(mapping.is_valid())
m.input_combination = [{"type": 1, "code": 2}]
m.output_symbol = "a"
self.assertFalse(m.is_valid())
m.target_uinput = "keyboard"
self.assertTrue(m.is_valid())
mapping.input_combination = [{"type": EV_KEY, "code": KEY_1}]
mapping.output_symbol = "a"
self.assertFalse(mapping.is_valid())
mapping.target_uinput = "keyboard"
self.assertTrue(mapping.is_valid())
def test_updates_validation_error(self):
m = UIMapping()
self.assertGreaterEqual(len(m.get_error().errors()), 2)
m.input_combination = [{"type": 1, "code": 2}]
m.output_symbol = "a"
mapping = UIMapping()
self.assertGreaterEqual(len(mapping.get_error().errors()), 2)
mapping.input_combination = [{"type": EV_KEY, "code": KEY_1}]
mapping.output_symbol = "a"
self.assertIn(
"1 validation error for Mapping\ntarget_uinput", str(m.get_error())
"1 validation error for Mapping\ntarget_uinput",
str(mapping.get_error()),
)
m.target_uinput = "keyboard"
self.assertTrue(m.is_valid())
self.assertIsNone(m.get_error())
mapping.target_uinput = "keyboard"
self.assertTrue(mapping.is_valid())
self.assertIsNone(mapping.get_error())
def test_copy_returns_ui_mapping(self):
"""copy should also be a UIMapping with all the invalid data"""
m = UIMapping()
m2 = m.copy()
self.assertIsInstance(m2, UIMapping)
self.assertEqual(m2.input_combination, InputCombination.empty_combination())
self.assertIsNone(m2.output_symbol)
"""Copy should also be a UIMapping with all the invalid data."""
mapping = UIMapping()
mapping_2 = mapping.copy()
self.assertIsInstance(mapping_2, UIMapping)
self.assertEqual(
mapping_2.input_combination, InputCombination.empty_combination()
)
self.assertIsNone(mapping_2.output_symbol)
def test_get_bus_massage(self):
m = UIMapping()
m2 = m.get_bus_message()
self.assertEqual(m2.message_type, MessageType.mapping)
mapping = UIMapping()
mapping_2 = mapping.get_bus_message()
self.assertEqual(mapping_2.message_type, MessageType.mapping)
with self.assertRaises(TypeError):
# the massage should be immutable
m2.output_symbol = "a"
self.assertIsNone(m2.output_symbol)
mapping_2.output_symbol = "a"
self.assertIsNone(mapping_2.output_symbol)
# the original should be not immutable
m.output_symbol = "a"
self.assertEqual(m.output_symbol, "a")
mapping.output_symbol = "a"
self.assertEqual(mapping.output_symbol, "a")
def test_has_input_defined(self):
m = UIMapping()
self.assertFalse(m.has_input_defined())
m.input_combination = InputCombination([InputConfig(type=EV_KEY, code=1)])
self.assertTrue(m.has_input_defined())
mapping = UIMapping()
self.assertFalse(mapping.has_input_defined())
mapping.input_combination = InputCombination([InputConfig(type=EV_KEY, code=1)])
self.assertTrue(mapping.has_input_defined())
if __name__ == "__main__":

@ -426,7 +426,7 @@ class TestPreset(unittest.TestCase):
mapping.output_symbol = "BTN_Left"
self.assertFalse(self.preset.dangerously_mapped_btn_left())
mapping.target_uinput = "keyboard"
mapping.target_uinput = "keyboard + mouse"
mapping.output_symbol = "3"
self.assertTrue(self.preset.dangerously_mapped_btn_left())

Loading…
Cancel
Save