Le dépôt ne contenait que README.md ; ajout de l'intégralité du code fonctionnel du fork comme socle stable de la branche de release. L'état runtime d'orchestration (.ideai/) est exclu du suivi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
321 lines
12 KiB
Python
321 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# input-remapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
|
#
|
|
# 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/>.
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Optional, Any, Union, List, Literal, Type, TYPE_CHECKING
|
|
|
|
from evdev._ecodes import EV_KEY
|
|
|
|
from inputremapper.configs.keyboard_layout import keyboard_layout
|
|
from inputremapper.configs.validation_errors import (
|
|
MacroError,
|
|
SymbolNotAvailableInTargetError,
|
|
)
|
|
from inputremapper.injection.global_uinputs import GlobalUInputs
|
|
from inputremapper.injection.macros.macro import Macro
|
|
from inputremapper.injection.macros.variable import Variable
|
|
|
|
if TYPE_CHECKING:
|
|
from inputremapper.injection.macros.raw_value import RawValue
|
|
from inputremapper.configs.mapping import Mapping
|
|
|
|
|
|
class ArgumentFlags(Enum):
|
|
# No default value is set, and the user has to provide one when using the macro
|
|
required = "required"
|
|
|
|
# If used, acts like foo(*bar)
|
|
spread = "spread"
|
|
|
|
|
|
@dataclass
|
|
class ArgumentConfig:
|
|
"""Definition what kind of arguments a task may take."""
|
|
|
|
position: Union[int, Literal[ArgumentFlags.spread]]
|
|
name: str
|
|
types: List[Optional[Type]]
|
|
is_symbol: bool = False
|
|
default: Any = ArgumentFlags.required
|
|
|
|
# If True, then the value (which should be a string), is the name of a non-constant
|
|
# variable. Tasks that overwrite their value need this, like `set`. The specified
|
|
# types are those that the current value of that variable may have. For `set` this
|
|
# doesn't matter, but something like `add` requires them to be numbers.
|
|
is_variable_name: bool = False
|
|
|
|
def is_required(self) -> bool:
|
|
return self.default == ArgumentFlags.required
|
|
|
|
def is_spread(self):
|
|
"""Does this Argument store all remaining Variables of a Task as a list?"""
|
|
return self.position == ArgumentFlags.spread
|
|
|
|
|
|
class Argument(ArgumentConfig):
|
|
"""Validation of variables and access to their value for Tasks during runtime."""
|
|
|
|
_variable: Optional[Variable] = None
|
|
|
|
# If the position is set to ArgumentFlags.spread, then _variables will be filled
|
|
# with all remaining positional arguments that were passed to a task.
|
|
_variables: List[Variable]
|
|
|
|
_mapping: Optional[Mapping] = None
|
|
|
|
def __init__(
|
|
self,
|
|
argument_config: ArgumentConfig,
|
|
mapping: Mapping,
|
|
) -> None:
|
|
# If a default of None is specified, but None is not an allowed type, then
|
|
# input-remapper has a bug here. Add "None" to your ArgumentConfig.types
|
|
assert not (
|
|
argument_config.default is None and None not in argument_config.types
|
|
)
|
|
|
|
self.position = argument_config.position
|
|
self.name = argument_config.name
|
|
self.types = argument_config.types
|
|
self.is_symbol = argument_config.is_symbol
|
|
self.default = argument_config.default
|
|
self.is_variable_name = argument_config.is_variable_name
|
|
|
|
self._mapping = mapping
|
|
self._variables = []
|
|
|
|
def initialize_variables(self, raw_values: List[RawValue]) -> None:
|
|
"""If the macro is supposed to contain multiple variables, set them.
|
|
Should be done during parsing."""
|
|
assert len(self._variables) == 0
|
|
assert self._variable is None
|
|
assert self.is_spread()
|
|
|
|
for raw_value in raw_values:
|
|
variable = self._parse_raw_value(raw_value)
|
|
self._variables.append(variable)
|
|
|
|
def initialize_variable(self, raw_value: RawValue) -> None:
|
|
"""Set the Arguments Variable. Done during parsing."""
|
|
assert len(self._variables) == 0
|
|
assert self._variable is None
|
|
assert not self.is_spread()
|
|
|
|
variable = self._parse_raw_value(raw_value)
|
|
self._variable = variable
|
|
|
|
def initialize_default(self) -> None:
|
|
"""Set the Arguments to its default value. Done during parsing."""
|
|
assert len(self._variables) == 0
|
|
assert self._variable is None
|
|
assert not self.is_spread()
|
|
|
|
variable = Variable(value=self.default, const=True)
|
|
self._variable = variable
|
|
|
|
def get_value(self) -> Any:
|
|
"""To ask for the current value of the variable during runtime."""
|
|
assert not self.is_spread(), f"Use .{self.get_values.__name__}()"
|
|
# If a user passed None as value, it should be a Variable(None, const=True) here.
|
|
# If not, a test or input-remapper is broken.
|
|
assert self._variable is not None
|
|
|
|
value = self._variable.get_value()
|
|
|
|
if not self._variable.const:
|
|
# Dynamic value. Hasn't been validated yet
|
|
value = self._validate_dynamic_value(self._variable)
|
|
|
|
return value
|
|
|
|
def get_values(self) -> List[Any]:
|
|
"""To ask for the current values of the variables during runtime."""
|
|
assert self.is_spread(), f"Use .{self.get_value.__name__}()"
|
|
|
|
values = []
|
|
for variable in self._variables:
|
|
if not variable.const:
|
|
values.append(self._validate_dynamic_value(variable))
|
|
else:
|
|
values.append(variable.get_value())
|
|
|
|
return values
|
|
|
|
def get_variable_name(self) -> str:
|
|
"""If the variable is not const, return its name."""
|
|
assert self._variable is not None
|
|
return self._variable.get_name()
|
|
|
|
def contains_macro(self) -> bool:
|
|
"""Does the underlying Variable contain another child-macro?"""
|
|
assert self._variable is not None
|
|
return isinstance(self._variable.get_value(), Macro)
|
|
|
|
def set_value(self, value: Any) -> Any:
|
|
"""To set the value of the underlying Variable during runtime.
|
|
Fails for constants."""
|
|
assert self._variable is not None
|
|
if self._variable.const:
|
|
raise Exception("Can't set value of a constant")
|
|
|
|
self._variable.set_value(value)
|
|
|
|
def assert_is_symbol(self, symbol: str) -> None:
|
|
"""Checks if the key/symbol-name is valid. Like "KEY_A" or "escape".
|
|
|
|
Using `is_symbol` on the ArgumentConfig is prefered, which causes it to
|
|
automatically do this for you. But some macros may be a bit more flexible,
|
|
and there we want to assert this ourselves only in certain cases."""
|
|
symbol = str(symbol)
|
|
code = keyboard_layout.get(symbol)
|
|
|
|
if code is None:
|
|
raise MacroError(msg=f'Unknown key "{symbol}"')
|
|
|
|
if self._mapping is not None:
|
|
target = self._mapping.target_uinput
|
|
if target is not None and not GlobalUInputs.can_default_uinput_emit(
|
|
target, EV_KEY, code
|
|
):
|
|
raise SymbolNotAvailableInTargetError(symbol, target)
|
|
|
|
def _parse_raw_value(self, raw_value: RawValue) -> Variable:
|
|
"""Validate and parse."""
|
|
value = raw_value.value
|
|
|
|
# The order of steps below matters.
|
|
|
|
if isinstance(value, Macro):
|
|
return Variable(value=value, const=True)
|
|
|
|
if self.is_variable_name:
|
|
# Treat this as a non-constant variable,
|
|
# even without a `$` in front of its name
|
|
if value.startswith('"'):
|
|
# Remove quotes from the string
|
|
value = value[1:-1]
|
|
return Variable(value=value, const=False)
|
|
|
|
if value.startswith("$"):
|
|
# Will be resolved during the macros runtime
|
|
return Variable(value=value[1:], const=False)
|
|
|
|
if self.is_symbol:
|
|
if value.startswith('"'):
|
|
value = value[1:-1]
|
|
self.assert_is_symbol(value)
|
|
return Variable(value=value, const=True)
|
|
|
|
if (value == "" or value == "None") and None in self.types:
|
|
# I think "" is the deprecated alternative to "None"
|
|
return Variable(value=None, const=True)
|
|
|
|
if value.startswith('"') and str in self.types:
|
|
# Something with explicit quotes should never be parsed as a number.
|
|
# Treat it as a string no matter the content.
|
|
value = value[1:-1]
|
|
return Variable(value=value, const=True)
|
|
|
|
if float in self.types and "." in value:
|
|
try:
|
|
return Variable(value=float(value), const=True)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if int in self.types:
|
|
try:
|
|
return Variable(value=int(value), const=True)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if not value.startswith('"') and ("(" in value or ")" in value):
|
|
# Looks like something that should have been a macro. It is not explicitly
|
|
# wrapped in quotes. Most likely an error. If it was a valid macro, the
|
|
# parser would have parsed it as such.
|
|
raise MacroError(
|
|
msg=f"A broken macro was passed as parameter to {self.name}"
|
|
)
|
|
|
|
if str in self.types:
|
|
# Treat as a string. Something like KEY_A in key(KEY_A)
|
|
return Variable(value=value, const=True)
|
|
|
|
raise self._type_error_factory(value)
|
|
|
|
def _validate_dynamic_value(self, variable: Variable) -> Any:
|
|
"""To make sure the value of a non-const variable, asked for at runtime, is
|
|
fitting for the given ArgumentConfig."""
|
|
# Most of the stuff has already been taken care of when, for example,
|
|
# the "1" of set(foo, 1), or the '"bar"' or set(foo, "bar") was parsed the
|
|
# first time. In the first case we get a number 1, and in the second a string
|
|
# `bar` without quotes
|
|
assert not variable.const
|
|
value = variable.get_value()
|
|
|
|
if self.is_symbol:
|
|
# value might be int `1`, which is a valid symbol for `key(1)`
|
|
value = str(value)
|
|
self.assert_is_symbol(value)
|
|
return value
|
|
|
|
if None in self.types and value is None:
|
|
return value
|
|
|
|
if type(value) in self.types:
|
|
return value
|
|
|
|
if type(value) not in self.types and str in self.types:
|
|
# `set` cannot make predictions where the variable will be used. Make sure
|
|
# the type is compatible, and turn numbers back into strings if need be.
|
|
return str(value)
|
|
|
|
# If the value is "1", we don't attempt to parse it as a number. This being a
|
|
# string means that something like `set(foo, "1")` was used, which enforces a
|
|
# string datatype. Otherwise, `set` would have already turned it into an int.
|
|
|
|
raise self._type_error_factory(value)
|
|
|
|
def _is_numeric_string(self, value: str) -> bool:
|
|
"""Check if the value can be turned into a number."""
|
|
try:
|
|
float(value)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def _type_error_factory(self, value: Any) -> MacroError:
|
|
formatted_types: List[str] = []
|
|
|
|
for type_ in self.types:
|
|
if type_ is None:
|
|
formatted_types.append("None")
|
|
else:
|
|
formatted_types.append(type_.__name__)
|
|
|
|
return MacroError(
|
|
msg=(
|
|
f'Expected "{self.name}" to be one of {formatted_types}, but got '
|
|
f'{type(value).__name__} "{value}"'
|
|
)
|
|
)
|