chore(repo): baseline du fork input-remapper (upstream v2.2.1)
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>
This commit is contained in:
0
inputremapper/injection/macros/__init__.py
Normal file
0
inputremapper/injection/macros/__init__.py
Normal file
320
inputremapper/injection/macros/argument.py
Normal file
320
inputremapper/injection/macros/argument.py
Normal file
@ -0,0 +1,320 @@
|
||||
#!/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}"'
|
||||
)
|
||||
)
|
||||
126
inputremapper/injection/macros/macro.py
Normal file
126
inputremapper/injection/macros/macro.py
Normal file
@ -0,0 +1,126 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""Executes more complex patterns of keystrokes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import List, Callable, Optional, TYPE_CHECKING
|
||||
|
||||
from inputremapper.ipc.shared_dict import SharedDict
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.injection.context import Context
|
||||
from inputremapper.configs.mapping import Mapping
|
||||
|
||||
InjectEventCallback = Callable[[int, int, int], None]
|
||||
|
||||
macro_variables = SharedDict()
|
||||
|
||||
|
||||
class Macro:
|
||||
"""Chains tasks (like `modify` or `repeat`).
|
||||
|
||||
Tasks may have child_macros. Running a Macro runs Tasks, which in turn may run
|
||||
their child_macros based on certain conditions (depending on the Task).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: Optional[str],
|
||||
context: Optional[Context] = None,
|
||||
mapping: Optional[Mapping] = None,
|
||||
):
|
||||
"""Create a macro instance that can be populated with tasks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code
|
||||
The original parsed code, for logging purposes.
|
||||
context : Context
|
||||
mapping : UIMapping
|
||||
"""
|
||||
self.code = code
|
||||
self.context = context
|
||||
self.mapping = mapping
|
||||
|
||||
# List of coroutines that will be called sequentially.
|
||||
# This is the compiled code
|
||||
self.tasks: List[Task] = []
|
||||
|
||||
self.running = False
|
||||
|
||||
self.keystroke_sleep_ms = None
|
||||
|
||||
async def run(self, callback: InjectEventCallback):
|
||||
"""Run the macro.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
callback
|
||||
Will receive int type, code and value for an event to write
|
||||
"""
|
||||
if not callable(callback):
|
||||
raise ValueError("handler is not callable")
|
||||
|
||||
if self.running:
|
||||
logger.error('Tried to run already running macro "%s"', self.code)
|
||||
return
|
||||
|
||||
self.keystroke_sleep_ms = self.mapping.macro_key_sleep_ms
|
||||
|
||||
self.running = True
|
||||
|
||||
try:
|
||||
for task in self.tasks:
|
||||
coroutine = task.run(callback)
|
||||
if asyncio.iscoroutine(coroutine):
|
||||
await coroutine
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
# done
|
||||
self.running = False
|
||||
|
||||
def press_trigger(self):
|
||||
"""The user pressed the trigger key down."""
|
||||
for task in self.tasks:
|
||||
task.press_trigger()
|
||||
|
||||
def release_trigger(self):
|
||||
"""The user released the trigger key."""
|
||||
for task in self.tasks:
|
||||
task.release_trigger()
|
||||
|
||||
async def _keycode_pause(self, _=None):
|
||||
"""To add a pause between keystrokes.
|
||||
|
||||
This was needed at some point because it appeared that injecting keys too
|
||||
fast will prevent them from working. It probably depends on the environment.
|
||||
"""
|
||||
await asyncio.sleep(self.keystroke_sleep_ms / 1000)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Macro "{self.code}" at {hex(id(self))}>'
|
||||
|
||||
def add_task(self, task):
|
||||
self.tasks.append(task)
|
||||
476
inputremapper/injection/macros/parse.py
Normal file
476
inputremapper/injection/macros/parse.py
Normal file
@ -0,0 +1,476 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
"""Parse macro code"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Optional, Any, Type, TYPE_CHECKING, Dict, List
|
||||
|
||||
from inputremapper.configs.validation_errors import MacroError
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.raw_value import RawValue
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.injection.macros.tasks.add import AddTask
|
||||
from inputremapper.injection.macros.tasks.event import EventTask
|
||||
from inputremapper.injection.macros.tasks.hold import HoldTask
|
||||
from inputremapper.injection.macros.tasks.hold_keys import HoldKeysTask
|
||||
from inputremapper.injection.macros.tasks.if_eq import IfEqTask
|
||||
from inputremapper.injection.macros.tasks.if_led import IfNumlockTask, IfCapslockTask
|
||||
from inputremapper.injection.macros.tasks.if_single import IfSingleTask
|
||||
from inputremapper.injection.macros.tasks.if_tap import IfTapTask
|
||||
from inputremapper.injection.macros.tasks.ifeq import DeprecatedIfEqTask
|
||||
from inputremapper.injection.macros.tasks.key import KeyTask
|
||||
from inputremapper.injection.macros.tasks.key_down import KeyDownTask
|
||||
from inputremapper.injection.macros.tasks.key_up import KeyUpTask
|
||||
from inputremapper.injection.macros.tasks.mod_tap import ModTapTask
|
||||
from inputremapper.injection.macros.tasks.modify import ModifyTask
|
||||
from inputremapper.injection.macros.tasks.mouse import MouseTask
|
||||
from inputremapper.injection.macros.tasks.parallel import ParallelTask
|
||||
from inputremapper.injection.macros.tasks.mouse_xy import MouseXYTask
|
||||
from inputremapper.injection.macros.tasks.repeat import RepeatTask
|
||||
from inputremapper.injection.macros.tasks.set import SetTask
|
||||
from inputremapper.injection.macros.tasks.toggle import ToggleTask
|
||||
from inputremapper.injection.macros.tasks.wait import WaitTask
|
||||
from inputremapper.injection.macros.tasks.wheel import WheelTask
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inputremapper.injection.context import Context
|
||||
from inputremapper.configs.mapping import Mapping
|
||||
|
||||
|
||||
class Parser:
|
||||
TASK_CLASSES: dict[str, type[Task]] = {
|
||||
"modify": ModifyTask,
|
||||
"repeat": RepeatTask,
|
||||
"toggle": ToggleTask,
|
||||
"key": KeyTask,
|
||||
"key_down": KeyDownTask,
|
||||
"key_up": KeyUpTask,
|
||||
"event": EventTask,
|
||||
"wait": WaitTask,
|
||||
"hold": HoldTask,
|
||||
"hold_keys": HoldKeysTask,
|
||||
"mouse": MouseTask,
|
||||
"mouse_xy": MouseXYTask,
|
||||
"wheel": WheelTask,
|
||||
"if_eq": IfEqTask,
|
||||
"if_numlock": IfNumlockTask,
|
||||
"if_capslock": IfCapslockTask,
|
||||
"set": SetTask,
|
||||
"if_tap": IfTapTask,
|
||||
"if_single": IfSingleTask,
|
||||
"add": AddTask,
|
||||
"mod_tap": ModTapTask,
|
||||
"parallel": ParallelTask,
|
||||
# Those are only kept for backwards compatibility with old macros. The space for
|
||||
# writing macro was very constrained in the past, so shorthands were introduced:
|
||||
"m": ModifyTask,
|
||||
"r": RepeatTask,
|
||||
"k": KeyTask,
|
||||
"e": EventTask,
|
||||
"w": WaitTask,
|
||||
"h": HoldTask,
|
||||
# It was not possible to adjust ifeq to support variables without breaking old
|
||||
# macros, so this function is deprecated and if_eq introduced. Kept for backwards
|
||||
# compatibility:
|
||||
"ifeq": DeprecatedIfEqTask,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def is_this_a_macro(output: Any):
|
||||
"""Figure out if this is a macro."""
|
||||
if not isinstance(output, str):
|
||||
return False
|
||||
|
||||
if "+" in output.strip():
|
||||
# for example "a + b"
|
||||
return True
|
||||
|
||||
return "(" in output and ")" in output and len(output) >= 4
|
||||
|
||||
@staticmethod
|
||||
def _extract_args(inner: str):
|
||||
"""Extract parameters from the inner contents of a call.
|
||||
|
||||
This does not parse them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
inner
|
||||
for example '1, r, r(2, k(a))' should result in ['1', 'r', 'r(2, k(a))']
|
||||
"""
|
||||
inner = inner.strip()
|
||||
brackets = 0
|
||||
params = []
|
||||
start = 0
|
||||
string = False
|
||||
for position, char in enumerate(inner):
|
||||
# ignore anything between string quotes
|
||||
if char == '"':
|
||||
string = not string
|
||||
if string:
|
||||
continue
|
||||
|
||||
# ignore commas inside child macros
|
||||
if char == "(":
|
||||
brackets += 1
|
||||
if char == ")":
|
||||
brackets -= 1
|
||||
if char == "," and brackets == 0:
|
||||
# , potentially starts another parameter, but only if
|
||||
# the current brackets are all closed.
|
||||
params.append(inner[start:position].strip())
|
||||
# skip the comma
|
||||
start = position + 1
|
||||
|
||||
# one last parameter
|
||||
params.append(inner[start:].strip())
|
||||
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def _count_brackets(macro):
|
||||
"""Find where the first opening bracket closes."""
|
||||
openings = macro.count("(")
|
||||
closings = macro.count(")")
|
||||
if openings != closings:
|
||||
raise MacroError(
|
||||
macro, f"Found {openings} opening and {closings} closing brackets"
|
||||
)
|
||||
|
||||
brackets = 0
|
||||
position = 0
|
||||
for char in macro:
|
||||
position += 1
|
||||
if char == "(":
|
||||
brackets += 1
|
||||
continue
|
||||
|
||||
if char == ")":
|
||||
brackets -= 1
|
||||
if brackets == 0:
|
||||
# the closing bracket of the call
|
||||
break
|
||||
|
||||
return position
|
||||
|
||||
@staticmethod
|
||||
def _split_keyword_arg(param):
|
||||
"""Split "foo=bar" into "foo" and "bar".
|
||||
|
||||
If not a keyward param, return None and the param.
|
||||
"""
|
||||
if re.match(r"[a-zA-Z_][a-zA-Z_\d]*=.+", param):
|
||||
split = param.split("=", 1)
|
||||
return split[0], split[1]
|
||||
|
||||
return None, param
|
||||
|
||||
@staticmethod
|
||||
def _validate_keyword_argument_names(
|
||||
keyword_args: Dict[str, Any],
|
||||
task_class: Type[Task],
|
||||
) -> None:
|
||||
for keyword_arg in keyword_args:
|
||||
for argument in task_class.argument_configs:
|
||||
if argument.name == keyword_arg:
|
||||
break
|
||||
else:
|
||||
raise MacroError(msg=f"Unknown keyword argument {keyword_arg}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_recurse(
|
||||
code: str,
|
||||
context: Optional[Context],
|
||||
mapping: Mapping,
|
||||
verbose: bool,
|
||||
macro_instance: Optional[Macro] = None,
|
||||
depth: int = 0,
|
||||
) -> RawValue:
|
||||
"""Handle a subset of the macro, e.g. one parameter or function call.
|
||||
|
||||
Not using eval for security reasons.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code
|
||||
Just like parse. A single parameter or the complete macro as string.
|
||||
Comments and redundant whitespace characters are expected to be removed already.
|
||||
Example:
|
||||
- "parallel(key(a),key(b).key($foo))"
|
||||
- "key(a)"
|
||||
- "a"
|
||||
- "key(b).key($foo)"
|
||||
- "b"
|
||||
- "key($foo)"
|
||||
- "$foo"
|
||||
context : Context
|
||||
macro_instance
|
||||
A macro instance to add tasks to. This is the output of the parser, and is
|
||||
organized like a tree.
|
||||
depth
|
||||
For logging porposes
|
||||
"""
|
||||
assert isinstance(code, str)
|
||||
assert isinstance(depth, int)
|
||||
|
||||
def debug(*args, **kwargs):
|
||||
if verbose:
|
||||
logger.debug(*args, **kwargs)
|
||||
|
||||
space = " " * depth
|
||||
|
||||
code = code.strip()
|
||||
|
||||
# is it another macro?
|
||||
task_call_match = re.match(r"^(\w+)\(", code)
|
||||
task_name = task_call_match[1] if task_call_match else None
|
||||
|
||||
if task_name is None:
|
||||
# It is probably either a key name like KEY_A or a variable name as in `set(var,1)`,
|
||||
# both won't contain special characters that can break macro syntax so they don't
|
||||
# have to be wrapped in quotes. The argument configuration of the tasks will
|
||||
# detemrine how to parse it.
|
||||
debug("%svalue %s", space, code)
|
||||
return RawValue(value=code)
|
||||
|
||||
if macro_instance is None:
|
||||
# start a new chain
|
||||
macro_instance = Macro(code, context, mapping)
|
||||
else:
|
||||
# chain this call to the existing instance
|
||||
assert isinstance(macro_instance, Macro)
|
||||
|
||||
task_class = Parser.TASK_CLASSES.get(task_name)
|
||||
if task_class is None:
|
||||
raise MacroError(code, f"Unknown function {task_name}")
|
||||
|
||||
# get all the stuff inbetween
|
||||
closing_bracket_position = Parser._count_brackets(code) - 1
|
||||
inner = code[code.index("(") + 1 : closing_bracket_position]
|
||||
debug("%scalls %s with %s", space, task_name, inner)
|
||||
|
||||
# split "3, foo=a(2, k(a).w(10))" into arguments
|
||||
raw_string_args = Parser._extract_args(inner)
|
||||
|
||||
# parse and sort the params
|
||||
positional_args: List[RawValue] = []
|
||||
keyword_args: Dict[str, RawValue] = {}
|
||||
for param in raw_string_args:
|
||||
key, value = Parser._split_keyword_arg(param)
|
||||
parsed = Parser._parse_recurse(
|
||||
value.strip(),
|
||||
context,
|
||||
mapping,
|
||||
verbose,
|
||||
None,
|
||||
depth + 1,
|
||||
)
|
||||
if key is None:
|
||||
if len(keyword_args) > 0:
|
||||
msg = f'Positional argument "{key}" follows keyword argument'
|
||||
raise MacroError(code, msg)
|
||||
positional_args.append(parsed)
|
||||
else:
|
||||
if key in keyword_args:
|
||||
raise MacroError(code, f'The "{key}" argument was specified twice')
|
||||
keyword_args[key] = parsed
|
||||
|
||||
debug(
|
||||
"%sadd call to %s with %s, %s",
|
||||
space,
|
||||
task_name,
|
||||
positional_args,
|
||||
keyword_args,
|
||||
)
|
||||
|
||||
Parser._validate_keyword_argument_names(
|
||||
keyword_args,
|
||||
task_class,
|
||||
)
|
||||
Parser._validate_num_args(
|
||||
code,
|
||||
task_name,
|
||||
task_class,
|
||||
raw_string_args,
|
||||
)
|
||||
|
||||
try:
|
||||
task = task_class(
|
||||
positional_args,
|
||||
keyword_args,
|
||||
context,
|
||||
mapping,
|
||||
)
|
||||
macro_instance.add_task(task)
|
||||
except TypeError as exception:
|
||||
raise MacroError(msg=str(exception)) from exception
|
||||
|
||||
# is after this another call? Chain it to the macro_instance
|
||||
more_code_exists = len(code) > closing_bracket_position + 1
|
||||
if more_code_exists:
|
||||
next_char = code[closing_bracket_position + 1]
|
||||
statement_closed = next_char == "."
|
||||
|
||||
if statement_closed:
|
||||
# skip over the ")."
|
||||
chain = code[closing_bracket_position + 2 :]
|
||||
debug("%sfollowed by %s", space, chain)
|
||||
Parser._parse_recurse(
|
||||
chain,
|
||||
context,
|
||||
mapping,
|
||||
verbose,
|
||||
macro_instance,
|
||||
depth,
|
||||
)
|
||||
elif re.match(r"[a-zA-Z_]", next_char):
|
||||
# something like foo()bar
|
||||
raise MacroError(
|
||||
code,
|
||||
f'Expected a "." to follow after '
|
||||
f"{code[:closing_bracket_position + 1]}",
|
||||
)
|
||||
|
||||
return RawValue(value=macro_instance)
|
||||
|
||||
@staticmethod
|
||||
def _validate_num_args(
|
||||
code: str,
|
||||
task_name: str,
|
||||
task_class: Type[Task],
|
||||
raw_string_args: List[str],
|
||||
) -> None:
|
||||
min_args, max_args = task_class.get_num_parameters()
|
||||
num_provided_args = len(raw_string_args)
|
||||
if num_provided_args < min_args or num_provided_args > max_args:
|
||||
if min_args != max_args:
|
||||
msg = (
|
||||
f"{task_name} takes between {min_args} and {max_args}, "
|
||||
f"not {num_provided_args} parameters"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f"{task_name} takes {min_args}, not {num_provided_args} parameters"
|
||||
)
|
||||
|
||||
raise MacroError(code, msg)
|
||||
|
||||
@staticmethod
|
||||
def handle_plus_syntax(macro):
|
||||
"""Transform a + b + c to hold_keys(a,b,c)."""
|
||||
if "+" not in macro:
|
||||
return macro
|
||||
|
||||
if "(" in macro or ")" in macro:
|
||||
raise MacroError(macro, f'Mixing "+" and macros is unsupported: "{ macro}"')
|
||||
|
||||
chunks = [chunk.strip() for chunk in macro.split("+")]
|
||||
|
||||
if "" in chunks:
|
||||
raise MacroError(f'Invalid syntax for "{macro}"')
|
||||
|
||||
output = f"hold_keys({','.join(chunks)})"
|
||||
|
||||
logger.debug('Transformed "%s" to "%s"', macro, output)
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def remove_whitespaces(macro, delimiter='"'):
|
||||
"""Remove whitespaces, tabs, newlines and such outside of string quotes."""
|
||||
result = ""
|
||||
for i, chunk in enumerate(macro.split(delimiter)):
|
||||
# every second chunk is inside string quotes
|
||||
if i % 2 == 0:
|
||||
result += re.sub(r"\s", "", chunk)
|
||||
else:
|
||||
result += chunk
|
||||
result += delimiter
|
||||
|
||||
# one extra delimiter was added
|
||||
return result[: -len(delimiter)]
|
||||
|
||||
@staticmethod
|
||||
def remove_comments(macro):
|
||||
"""Remove comments from the macro and return the resulting code."""
|
||||
# keep hashtags inside quotes intact
|
||||
result = ""
|
||||
|
||||
for i, line in enumerate(macro.split("\n")):
|
||||
for j, chunk in enumerate(line.split('"')):
|
||||
if j > 0:
|
||||
# add back the string quote
|
||||
chunk = f'"{chunk}'
|
||||
|
||||
# every second chunk is inside string quotes
|
||||
if j % 2 == 0 and "#" in chunk:
|
||||
# everything from now on is a comment and can be ignored
|
||||
result += chunk.split("#")[0]
|
||||
break
|
||||
else:
|
||||
result += chunk
|
||||
|
||||
if i < macro.count("\n"):
|
||||
result += "\n"
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def clean(code):
|
||||
"""Remove everything irrelevant for the macro."""
|
||||
return Parser.remove_whitespaces(
|
||||
Parser.remove_comments(code),
|
||||
'"',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse(macro: str, context=None, mapping=None, verbose: bool = True) -> Macro:
|
||||
"""Parse and generate a Macro that can be run as often as you want.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
macro
|
||||
"repeat(3, key(a).wait(10))"
|
||||
"repeat(2, key(a).key(KEY_A)).key(b)"
|
||||
"wait(1000).modify(Shift_L, repeat(2, k(a))).wait(10, 20).key(b)"
|
||||
context : Context, or None for use in Frontend
|
||||
mapping
|
||||
the mapping for the macro, or None for use in Frontend
|
||||
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 = Parser.clean(macro)
|
||||
macro = Parser.handle_plus_syntax(macro)
|
||||
|
||||
macro_obj = Parser._parse_recurse(
|
||||
macro,
|
||||
context,
|
||||
mapping,
|
||||
verbose,
|
||||
).value
|
||||
if not isinstance(macro_obj, Macro):
|
||||
raise MacroError(macro, "The provided code was not a macro")
|
||||
|
||||
return macro_obj
|
||||
35
inputremapper/injection/macros/raw_value.py
Normal file
35
inputremapper/injection/macros/raw_value.py
Normal file
@ -0,0 +1,35 @@
|
||||
#!/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 dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
|
||||
|
||||
@dataclass
|
||||
class RawValue:
|
||||
"""Store a value exactly as it is in the macro-code. Values still have to be
|
||||
validated according to the ArgumentConfig of the Tasks.
|
||||
|
||||
Child-macros are passed as Macro objects though.
|
||||
"""
|
||||
|
||||
value: Union[str, Macro]
|
||||
248
inputremapper/injection/macros/task.py
Normal file
248
inputremapper/injection/macros/task.py
Normal file
@ -0,0 +1,248 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
from itertools import chain
|
||||
from typing import List, Dict, TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
from inputremapper.configs.validation_errors import MacroError
|
||||
from inputremapper.injection.macros.argument import (
|
||||
Argument,
|
||||
ArgumentConfig,
|
||||
ArgumentFlags,
|
||||
)
|
||||
from inputremapper.injection.macros.macro import Macro, InjectEventCallback
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inputremapper.injection.mapping_handlers.mapping_handler import EventListener
|
||||
from inputremapper.injection.macros.raw_value import RawValue
|
||||
from inputremapper.injection.context import Context
|
||||
from inputremapper.configs.mapping import Mapping
|
||||
|
||||
|
||||
class Task:
|
||||
"""Base Class for functions like `if_eq` or `key`
|
||||
|
||||
A macro like `key(a).key(b)` will contain two instances of this class.
|
||||
"""
|
||||
|
||||
argument_configs: List[ArgumentConfig]
|
||||
arguments: Dict[str, Argument]
|
||||
mapping: Mapping
|
||||
|
||||
# The context is None during frontend-parsing/validation I believe
|
||||
context: Optional[Context]
|
||||
|
||||
child_macros: List[Macro]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
positional_args: List[RawValue],
|
||||
keyword_args: Dict[str, RawValue],
|
||||
context: Optional[Context],
|
||||
mapping: Mapping,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.mapping = mapping
|
||||
self.child_macros = []
|
||||
|
||||
self._validate_argument_configs()
|
||||
|
||||
self.arguments = {
|
||||
argument_config.name: Argument(argument_config, mapping)
|
||||
for argument_config in self.argument_configs
|
||||
}
|
||||
|
||||
self._setup_asyncio_events()
|
||||
|
||||
for argument in self.arguments.values():
|
||||
self._initialize_argument(argument, keyword_args, positional_args)
|
||||
|
||||
self._initialize_spread_arg(positional_args)
|
||||
|
||||
for raw_value in chain(keyword_args.values(), positional_args):
|
||||
if isinstance(raw_value.value, Macro):
|
||||
self.child_macros.append(raw_value.value)
|
||||
|
||||
async def run(self, callback: InjectEventCallback) -> None:
|
||||
"""Macro logic goes here.
|
||||
|
||||
Call the callback with the type, code and value that should be injected.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_event_listener(self, listener: EventListener) -> None:
|
||||
"""Listeners get each event from the source device.
|
||||
|
||||
After all listeners are done, the event will go into the mapping handlers.
|
||||
|
||||
Make sure to remove your event_listener once you are done.
|
||||
"""
|
||||
# The context will be there when the macro is parsed by the service
|
||||
assert self.context is not None
|
||||
self.context.listeners.add(listener)
|
||||
|
||||
def remove_event_listener(self, listener: EventListener) -> None:
|
||||
assert self.context is not None
|
||||
self.context.listeners.remove(listener)
|
||||
|
||||
@classmethod
|
||||
def get_macro_argument_names(cls):
|
||||
return [argument_config.name for argument_config in cls.argument_configs]
|
||||
|
||||
@classmethod
|
||||
def get_num_parameters(cls) -> Tuple[int, Union[int, float]]:
|
||||
"""Get the number of required parameters and the maximum number of parameters."""
|
||||
min_num_args = 0
|
||||
argument_configs = cls.argument_configs
|
||||
max_num_args: Union[int, float] = len(argument_configs)
|
||||
for argument_config in argument_configs:
|
||||
if argument_config.position == ArgumentFlags.spread:
|
||||
# 0 or more
|
||||
max_num_args = float("inf")
|
||||
continue
|
||||
|
||||
if argument_config.is_required():
|
||||
min_num_args += 1
|
||||
|
||||
return min_num_args, max_num_args
|
||||
|
||||
def get_argument(self, argument_name) -> Argument:
|
||||
return self.arguments[argument_name]
|
||||
|
||||
def press_trigger(self) -> None:
|
||||
"""The user pressed the trigger key down."""
|
||||
for macro in self.child_macros:
|
||||
macro.press_trigger()
|
||||
|
||||
if self.is_holding():
|
||||
logger.error("Already holding")
|
||||
return
|
||||
|
||||
self._trigger_release_event.clear()
|
||||
self._trigger_press_event.set()
|
||||
|
||||
def release_trigger(self) -> None:
|
||||
"""The user released the trigger key."""
|
||||
if not self.is_holding():
|
||||
return
|
||||
|
||||
self._trigger_release_event.set()
|
||||
self._trigger_press_event.clear()
|
||||
|
||||
for macro in self.child_macros:
|
||||
macro.release_trigger()
|
||||
|
||||
def is_holding(self) -> bool:
|
||||
"""Check if the macro is waiting for a key to be released."""
|
||||
return not self._trigger_release_event.is_set()
|
||||
|
||||
async def keycode_pause(self, _=None) -> None:
|
||||
"""To add a pause between keystrokes.
|
||||
|
||||
This was needed at some point because it appeared that injecting keys too
|
||||
fast will prevent them from working. It probably depends on the environment.
|
||||
"""
|
||||
await asyncio.sleep(self.mapping.macro_key_sleep_ms / 1000)
|
||||
|
||||
def _initialize_spread_arg(
|
||||
self,
|
||||
positional_args: List[RawValue],
|
||||
) -> None:
|
||||
"""Put all positional arguments that aren't used into the spread argument."""
|
||||
spread_argument: Optional[Argument] = None
|
||||
for argument in self.arguments.values():
|
||||
if argument.position == ArgumentFlags.spread:
|
||||
spread_argument = argument
|
||||
break
|
||||
|
||||
if spread_argument is None:
|
||||
return
|
||||
|
||||
remaining_positional_args = [*positional_args]
|
||||
|
||||
for argument in self.arguments.values():
|
||||
if argument.position != ArgumentFlags.spread and argument.position < len(
|
||||
remaining_positional_args
|
||||
):
|
||||
del remaining_positional_args[argument.position]
|
||||
|
||||
spread_argument.initialize_variables(remaining_positional_args)
|
||||
|
||||
def _find_argument_by_position(self, position: int) -> Optional[Argument]:
|
||||
for argument in self.arguments.values():
|
||||
if argument.position == position:
|
||||
return argument
|
||||
|
||||
return None
|
||||
|
||||
def _setup_asyncio_events(self) -> None:
|
||||
# Can be used to wait for the press and release of the input event/key, that is
|
||||
# configured as the trigger of the macro, via asyncio.
|
||||
self._trigger_release_event = asyncio.Event()
|
||||
self._trigger_press_event = asyncio.Event()
|
||||
# released by default
|
||||
self._trigger_release_event.set()
|
||||
self._trigger_press_event.clear()
|
||||
|
||||
def _initialize_argument(
|
||||
self,
|
||||
argument: Argument,
|
||||
keyword_args: Dict[str, RawValue],
|
||||
positional_args: List[RawValue],
|
||||
) -> None:
|
||||
if argument.position == ArgumentFlags.spread:
|
||||
# Will get all the remaining positional arguments afterward.
|
||||
return
|
||||
|
||||
for name, value in keyword_args.items():
|
||||
if argument.name == name:
|
||||
argument.initialize_variable(value)
|
||||
return
|
||||
|
||||
if argument.position < len(positional_args):
|
||||
argument.initialize_variable(positional_args[argument.position])
|
||||
return
|
||||
|
||||
if not argument.is_required():
|
||||
argument.initialize_default()
|
||||
return
|
||||
|
||||
# This shouldn't be possible, the parser should have ensured things are valid
|
||||
# already.
|
||||
raise MacroError(f"Could not initialize argument {argument.name}")
|
||||
|
||||
def _validate_argument_configs(self):
|
||||
# Might help during development
|
||||
positions = set()
|
||||
names = set()
|
||||
for argument_config in self.argument_configs:
|
||||
position = argument_config.position
|
||||
if position in positions:
|
||||
raise MacroError(f"Duplicate position {positions} in ArgumentConfig")
|
||||
positions.add(position)
|
||||
|
||||
name = argument_config.name
|
||||
if name in names:
|
||||
raise MacroError(f"Duplicate name {name} in ArgumentConfig")
|
||||
names.add(name)
|
||||
0
inputremapper/injection/macros/tasks/__init__.py
Normal file
0
inputremapper/injection/macros/tasks/__init__.py
Normal file
72
inputremapper/injection/macros/tasks/add.py
Normal file
72
inputremapper/injection/macros/tasks/add.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/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 inputremapper.configs.validation_errors import MacroError
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
|
||||
class AddTask(Task):
|
||||
"""Add a number to a variable."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="variable",
|
||||
position=0,
|
||||
types=[int, float, None],
|
||||
is_variable_name=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="value",
|
||||
position=1,
|
||||
types=[int, float],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
argument = self.get_argument("variable")
|
||||
try:
|
||||
current = argument.get_value()
|
||||
except MacroError:
|
||||
return
|
||||
|
||||
if current is None:
|
||||
logger.debug(
|
||||
'"%s" initialized with 0',
|
||||
self.arguments["variable"].get_variable_name(),
|
||||
)
|
||||
argument.set_value(0)
|
||||
current = 0
|
||||
|
||||
addend = self.get_argument("value").get_value()
|
||||
|
||||
if not isinstance(current, (int, float)):
|
||||
logger.error(
|
||||
'Expected variable "%s" to contain a number, but got "%s"',
|
||||
argument.get_value(),
|
||||
current,
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug("%s += %s", current, addend)
|
||||
argument.set_value(current + addend)
|
||||
64
inputremapper/injection/macros/tasks/event.py
Normal file
64
inputremapper/injection/macros/tasks/event.py
Normal file
@ -0,0 +1,64 @@
|
||||
#!/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 evdev.ecodes import ecodes
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class EventTask(Task):
|
||||
"""Write any event.
|
||||
|
||||
For example event(EV_KEY, KEY_A, 1)
|
||||
"""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="type",
|
||||
position=0,
|
||||
types=[str, int],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="code",
|
||||
position=1,
|
||||
types=[str, int],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="value",
|
||||
position=2,
|
||||
types=[int],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
type_ = self.get_argument("type").get_value()
|
||||
code = self.get_argument("code").get_value()
|
||||
value = self.get_argument("value").get_value()
|
||||
|
||||
if isinstance(type_, str):
|
||||
type_ = ecodes[type_.upper()]
|
||||
if isinstance(code, str):
|
||||
code = ecodes[code.upper()]
|
||||
|
||||
callback(type_, code, value)
|
||||
await self.keycode_pause()
|
||||
71
inputremapper/injection/macros/tasks/hold.py
Normal file
71
inputremapper/injection/macros/tasks/hold.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
|
||||
from evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class HoldTask(Task):
|
||||
"""Loop the macro until the trigger-key is released."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="macro",
|
||||
position=0,
|
||||
types=[Macro, str, None],
|
||||
)
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
macro = self.get_argument("macro").get_value()
|
||||
|
||||
if macro is None:
|
||||
await self._trigger_release_event.wait()
|
||||
return
|
||||
|
||||
if isinstance(macro, str):
|
||||
# if macro is a key name, hold down the key while the
|
||||
# keyboard key is physically held down
|
||||
symbol = macro
|
||||
self.get_argument("macro").assert_is_symbol(symbol)
|
||||
|
||||
code = keyboard_layout.get(symbol)
|
||||
callback(EV_KEY, code, 1)
|
||||
await self._trigger_release_event.wait()
|
||||
callback(EV_KEY, code, 0)
|
||||
|
||||
if isinstance(macro, Macro):
|
||||
# repeat the macro forever while the key is held down
|
||||
while self.is_holding():
|
||||
# run the child macro completely to avoid
|
||||
# not-releasing any key
|
||||
await macro.run(callback)
|
||||
# prevent input-remapper from freezing up and making the system hard
|
||||
# to control, by adding 1ms of asyncio.sleep during which the event
|
||||
# loop can do other things
|
||||
await asyncio.sleep(1 / 1000)
|
||||
55
inputremapper/injection/macros/tasks/hold_keys.py
Normal file
55
inputremapper/injection/macros/tasks/hold_keys.py
Normal file
@ -0,0 +1,55 @@
|
||||
#!/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 evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig, ArgumentFlags
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class HoldKeysTask(Task):
|
||||
"""Hold down multiple keys, equivalent to `a + b + c + ...`."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="*symbols",
|
||||
position=ArgumentFlags.spread,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
)
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
symbols = self.get_argument("*symbols").get_values()
|
||||
|
||||
codes = [keyboard_layout.get(symbol) for symbol in symbols]
|
||||
|
||||
for code in codes:
|
||||
callback(EV_KEY, code, 1)
|
||||
await self.keycode_pause()
|
||||
|
||||
await self._trigger_release_event.wait()
|
||||
|
||||
for code in codes[::-1]:
|
||||
callback(EV_KEY, code, 0)
|
||||
await self.keycode_pause()
|
||||
66
inputremapper/injection/macros/tasks/if_eq.py
Normal file
66
inputremapper/injection/macros/tasks/if_eq.py
Normal file
@ -0,0 +1,66 @@
|
||||
#!/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 inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class IfEqTask(Task):
|
||||
"""Compare two values."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="value_1",
|
||||
position=0,
|
||||
types=[str, int, float],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="value_2",
|
||||
position=1,
|
||||
types=[str, int, float],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="then",
|
||||
position=2,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="else",
|
||||
position=3,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
value_1 = self.get_argument("value_1").get_value()
|
||||
value_2 = self.get_argument("value_2").get_value()
|
||||
then = self.get_argument("then").get_value()
|
||||
else_ = self.get_argument("else").get_value()
|
||||
|
||||
if value_1 == value_2:
|
||||
if then is not None:
|
||||
await then.run(callback)
|
||||
elif else_ is not None:
|
||||
await else_.run(callback)
|
||||
71
inputremapper/injection/macros/tasks/if_led.py
Normal file
71
inputremapper/injection/macros/tasks/if_led.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/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 evdev.ecodes import (
|
||||
LED_NUML,
|
||||
LED_CAPSL,
|
||||
)
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class IfLedTask(Task):
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="then",
|
||||
position=0,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="else",
|
||||
position=1,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
]
|
||||
|
||||
led_code = None
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
then = self.get_argument("then").get_value()
|
||||
else_ = self.get_argument("else").get_value()
|
||||
|
||||
# self.context is only None when the frontend is parsing the macro
|
||||
assert self.context is not None
|
||||
led_on = self.led_code in self.context.get_leds()
|
||||
|
||||
if led_on:
|
||||
if then is not None:
|
||||
await then.run(callback)
|
||||
elif else_ is not None:
|
||||
await else_.run(callback)
|
||||
|
||||
|
||||
class IfNumlockTask(IfLedTask):
|
||||
led_code = LED_NUML
|
||||
|
||||
|
||||
class IfCapslockTask(IfLedTask):
|
||||
led_code = LED_CAPSL
|
||||
96
inputremapper/injection/macros/tasks/if_single.py
Normal file
96
inputremapper/injection/macros/tasks/if_single.py
Normal file
@ -0,0 +1,96 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
|
||||
from evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.input_event import InputEvent
|
||||
|
||||
|
||||
class IfSingleTask(Task):
|
||||
"""If a key was pressed without combining it."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="then",
|
||||
position=0,
|
||||
types=[Macro, None],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="else",
|
||||
position=1,
|
||||
types=[Macro, None],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="timeout",
|
||||
position=2,
|
||||
types=[int, float, None],
|
||||
default=None,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
assert self.context is not None
|
||||
another_key_pressed_event = asyncio.Event()
|
||||
then = self.get_argument("then").get_value()
|
||||
else_ = self.get_argument("else").get_value()
|
||||
|
||||
async def listener(event: InputEvent) -> None:
|
||||
if event.type != EV_KEY:
|
||||
# Ignore anything that is not a key
|
||||
return
|
||||
|
||||
if event.is_pressed():
|
||||
# Another key was pressed
|
||||
another_key_pressed_event.set()
|
||||
return
|
||||
|
||||
self.add_event_listener(listener)
|
||||
|
||||
timeout = self.get_argument("timeout").get_value()
|
||||
|
||||
# Wait for anything of importance to happen, that would determine the
|
||||
# outcome of the if_single macro.
|
||||
await asyncio.wait(
|
||||
[
|
||||
asyncio.Task(another_key_pressed_event.wait()),
|
||||
asyncio.Task(self._trigger_release_event.wait()),
|
||||
],
|
||||
timeout=timeout / 1000 if timeout else None,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
self.remove_event_listener(listener)
|
||||
|
||||
if not self.is_holding():
|
||||
if then:
|
||||
await then.run(callback)
|
||||
else:
|
||||
# If the trigger has not been released, then `await asyncio.wait` above
|
||||
# could only have finished waiting due to a timeout, or because another
|
||||
# key was pressed.
|
||||
if else_:
|
||||
await else_.run(callback)
|
||||
79
inputremapper/injection/macros/tasks/if_tap.py
Normal file
79
inputremapper/injection/macros/tasks/if_tap.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class IfTapTask(Task):
|
||||
"""If a key was pressed quickly.
|
||||
|
||||
macro key pressed -> if_tap starts -> key released -> then
|
||||
|
||||
macro key pressed -> released (does other stuff in the meantime)
|
||||
-> if_tap starts -> pressed -> released -> then
|
||||
"""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="then",
|
||||
position=0,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="else",
|
||||
position=1,
|
||||
types=[Macro, None],
|
||||
default=None,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="timeout",
|
||||
position=2,
|
||||
types=[int, float],
|
||||
default=300,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
then = self.get_argument("then").get_value()
|
||||
else_ = self.get_argument("else").get_value()
|
||||
timeout = self.get_argument("timeout").get_value() / 1000
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self._wait(), timeout)
|
||||
if then:
|
||||
await then.run(callback)
|
||||
except asyncio.TimeoutError:
|
||||
if else_:
|
||||
await else_.run(callback)
|
||||
|
||||
async def _wait(self):
|
||||
"""Wait for a release, or if nothing pressed yet, a press and release."""
|
||||
if self.is_holding():
|
||||
await self._trigger_release_event.wait()
|
||||
else:
|
||||
await self._trigger_press_event.wait()
|
||||
await self._trigger_release_event.wait()
|
||||
72
inputremapper/injection/macros/tasks/ifeq.py
Normal file
72
inputremapper/injection/macros/tasks/ifeq.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/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 inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class DeprecatedIfEqTask(Task):
|
||||
"""Old version of if_eq, kept for compatibility reasons.
|
||||
|
||||
This can't support a comparison like ifeq("foo", $blub) with blub containing
|
||||
"foo" without breaking old functionality, because "foo" is treated as a
|
||||
variable name.
|
||||
"""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="variable",
|
||||
position=0,
|
||||
types=[str, float, int, None],
|
||||
is_variable_name=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="value",
|
||||
position=1,
|
||||
types=[str, float, int, None],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="then",
|
||||
position=2,
|
||||
types=[Macro, None],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="else",
|
||||
position=3,
|
||||
types=[Macro, None],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
actual_value = self.get_argument("variable").get_value()
|
||||
value = self.get_argument("value").get_value()
|
||||
then = self.get_argument("then").get_value()
|
||||
else_ = self.get_argument("else").get_value()
|
||||
|
||||
# The old ifeq function became somewhat incompatible with the new macro code.
|
||||
# I need to compare them as strings to keep this working.
|
||||
if str(actual_value) == str(value):
|
||||
if then is not None:
|
||||
await then.run(callback)
|
||||
elif else_ is not None:
|
||||
await else_.run(callback)
|
||||
49
inputremapper/injection/macros/tasks/key.py
Normal file
49
inputremapper/injection/macros/tasks/key.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/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 evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class KeyTask(Task):
|
||||
"""Write the symbol."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="symbol",
|
||||
position=0,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
)
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
symbol = self.get_argument("symbol").get_value()
|
||||
code = keyboard_layout.get(symbol)
|
||||
|
||||
callback(EV_KEY, code, 1)
|
||||
await self.keycode_pause()
|
||||
callback(EV_KEY, code, 0)
|
||||
await self.keycode_pause()
|
||||
45
inputremapper/injection/macros/tasks/key_down.py
Normal file
45
inputremapper/injection/macros/tasks/key_down.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/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 evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class KeyDownTask(Task):
|
||||
"""Press the symbol."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="symbol",
|
||||
position=0,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
)
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
symbol = self.get_argument("symbol").get_value()
|
||||
code = keyboard_layout.get(symbol)
|
||||
callback(EV_KEY, code, 1)
|
||||
45
inputremapper/injection/macros/tasks/key_up.py
Normal file
45
inputremapper/injection/macros/tasks/key_up.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/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 evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class KeyUpTask(Task):
|
||||
"""Release the symbol."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="symbol",
|
||||
position=0,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
)
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
symbol = self.get_argument("symbol").get_value()
|
||||
code = keyboard_layout.get(symbol)
|
||||
callback(EV_KEY, code, 0)
|
||||
140
inputremapper/injection/macros/tasks/mod_tap.py
Normal file
140
inputremapper/injection/macros/tasks/mod_tap.py
Normal file
@ -0,0 +1,140 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from typing import Deque
|
||||
|
||||
from evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.input_event import InputEvent
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
|
||||
class ModTapTask(Task):
|
||||
"""If pressed long enough in combination with other keys, it turns into a modifier.
|
||||
|
||||
Can be used to make home-row-modifiers.
|
||||
|
||||
Works similar to the default of
|
||||
https://github.com/qmk/qmk_firmware/blob/78a0adfbb4d2c4e12f93f2a62ded0020d406243e/docs/tap_hold.md#comparison-comparison
|
||||
"""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="default",
|
||||
position=0,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="modifier",
|
||||
position=1,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="tapping_term",
|
||||
position=2,
|
||||
types=[int, float],
|
||||
default=200,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
tapping_term = self.get_argument("tapping_term").get_value() / 1000
|
||||
jamming_asyncio_events: Deque[asyncio.Event] = deque()
|
||||
|
||||
async def listener(event: InputEvent) -> None:
|
||||
trigger = self.mapping.input_combination[-1]
|
||||
if event.type_and_code == trigger.type_and_code:
|
||||
# We don't block the event that would set _trigger_release_event.
|
||||
return
|
||||
|
||||
if event.type != EV_KEY:
|
||||
return
|
||||
|
||||
asyncio_event = asyncio.Event()
|
||||
jamming_asyncio_events.append(asyncio_event)
|
||||
# Make the EventReader wait until the mod_tap macro allows it to continue
|
||||
# processing the event. Because we want to wait until mod_tap injected the
|
||||
# modifier.
|
||||
await asyncio_event.wait()
|
||||
|
||||
self.add_event_listener(listener)
|
||||
|
||||
timeout = asyncio.Task(asyncio.sleep(tapping_term))
|
||||
await asyncio.wait(
|
||||
[asyncio.Task(self._trigger_release_event.wait()), timeout],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
has_timed_out = timeout.done()
|
||||
|
||||
if has_timed_out:
|
||||
# The timeout happened before the trigger got released.
|
||||
# We therefore modify stuff.
|
||||
symbol = self.get_argument("modifier").get_value()
|
||||
logger.debug("Modifying with %s", symbol)
|
||||
else:
|
||||
# The trigger got released before the timeout.
|
||||
# We therefore do not modify stuff.
|
||||
symbol = self.get_argument("default").get_value()
|
||||
logger.debug("Writing default %s", symbol)
|
||||
|
||||
code = keyboard_layout.get(symbol)
|
||||
callback(EV_KEY, code, 1)
|
||||
await self.keycode_pause()
|
||||
|
||||
# Now that we know if the key was pressed with the intention of modifying other
|
||||
# keys, we can let the jammed keys go on their journey through the handlers.
|
||||
# Those other handlers may map them to other keys and stuff.
|
||||
while len(jamming_asyncio_events) > 0:
|
||||
asyncio_event = jamming_asyncio_events.popleft()
|
||||
asyncio_event.set()
|
||||
await self.keycode_pause()
|
||||
await self.throttle()
|
||||
# While we are emptying the queue, more events might still arrive and add
|
||||
# to the queue.
|
||||
|
||||
# We remove this as late as possible, because if more keys are pressed while
|
||||
# jamming_asyncio_events is still being taken care of, they should wait until
|
||||
# all is done. This ensures the order of all events that are pressed, until
|
||||
# mod_tap is completely finished.
|
||||
self.remove_event_listener(listener)
|
||||
|
||||
# Keep the modifier pressed until the input/trigger is released
|
||||
await self._trigger_release_event.wait()
|
||||
callback(EV_KEY, code, 0)
|
||||
|
||||
await self.keycode_pause()
|
||||
|
||||
async def throttle(self) -> None:
|
||||
# In case the keycode_pause ist set to 0ms, we need to give the event handlers
|
||||
# a chance to inject the withheld events, before we go on. This ensures the
|
||||
# correct order of injections. Since we are using asyncio, something like
|
||||
# `callback(EV_KEY, code, 0)` might be faster than the event handlers, even if
|
||||
# it is the last step of the macro.
|
||||
if self.mapping.macro_key_sleep_ms == 0:
|
||||
await asyncio.sleep(0.01)
|
||||
57
inputremapper/injection/macros/tasks/modify.py
Normal file
57
inputremapper/injection/macros/tasks/modify.py
Normal file
@ -0,0 +1,57 @@
|
||||
#!/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 evdev.ecodes import EV_KEY
|
||||
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class ModifyTask(Task):
|
||||
"""Do stuff while a modifier is activated."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="modifier",
|
||||
position=0,
|
||||
types=[str],
|
||||
is_symbol=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="macro",
|
||||
position=1,
|
||||
types=[Macro],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
modifier = self.get_argument("modifier").get_value()
|
||||
code = keyboard_layout.get(modifier)
|
||||
macro = self.get_argument("macro").get_value()
|
||||
|
||||
callback(EV_KEY, code, 1)
|
||||
await self.keycode_pause()
|
||||
await macro.run(callback)
|
||||
callback(EV_KEY, code, 0)
|
||||
await self.keycode_pause()
|
||||
69
inputremapper/injection/macros/tasks/mouse.py
Normal file
69
inputremapper/injection/macros/tasks/mouse.py
Normal file
@ -0,0 +1,69 @@
|
||||
#!/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 evdev._ecodes import REL_Y, REL_X
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import InjectEventCallback
|
||||
from inputremapper.injection.macros.tasks.mouse_xy import MouseXYTask
|
||||
|
||||
|
||||
class MouseTask(MouseXYTask):
|
||||
"""Move the mouse cursor."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="direction",
|
||||
position=0,
|
||||
types=[str],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="speed",
|
||||
position=1,
|
||||
types=[int, float],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="acceleration",
|
||||
position=2,
|
||||
types=[int, float],
|
||||
default=1,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback: InjectEventCallback) -> None:
|
||||
direction = self.get_argument("direction").get_value()
|
||||
speed = self.get_argument("speed").get_value()
|
||||
acceleration = self.get_argument("acceleration").get_value()
|
||||
|
||||
code, direction = {
|
||||
"up": (REL_Y, -1),
|
||||
"down": (REL_Y, 1),
|
||||
"left": (REL_X, -1),
|
||||
"right": (REL_X, 1),
|
||||
}[direction.lower()]
|
||||
|
||||
await self.axis(
|
||||
code,
|
||||
direction * speed,
|
||||
acceleration,
|
||||
callback,
|
||||
)
|
||||
99
inputremapper/injection/macros/tasks/mouse_xy.py
Normal file
99
inputremapper/injection/macros/tasks/mouse_xy.py
Normal file
@ -0,0 +1,99 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
from typing import Union
|
||||
|
||||
from evdev._ecodes import REL_Y, REL_X
|
||||
from evdev.ecodes import EV_REL
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import InjectEventCallback
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.injection.macros.tasks.util import precise_iteration_frequency
|
||||
|
||||
|
||||
class MouseXYTask(Task):
|
||||
"""Move the mouse cursor."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="x",
|
||||
position=0,
|
||||
types=[int, float],
|
||||
default=0,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="y",
|
||||
position=1,
|
||||
types=[int, float],
|
||||
default=0,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="acceleration",
|
||||
position=2,
|
||||
types=[int, float],
|
||||
default=1,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback: InjectEventCallback) -> None:
|
||||
x = self.get_argument("x").get_value()
|
||||
y = self.get_argument("y").get_value()
|
||||
acceleration = self.get_argument("acceleration").get_value()
|
||||
await asyncio.gather(
|
||||
self.axis(REL_X, x, acceleration, callback),
|
||||
self.axis(REL_Y, y, acceleration, callback),
|
||||
)
|
||||
|
||||
async def axis(
|
||||
self,
|
||||
code: int,
|
||||
speed: Union[int, float],
|
||||
fractional_acceleration: Union[int, float],
|
||||
callback: InjectEventCallback,
|
||||
) -> None:
|
||||
acceleration = speed * fractional_acceleration
|
||||
direction = -1 if speed < 0 else 1
|
||||
current_speed = 0.0
|
||||
displacement_accumulator = 0.0
|
||||
displacement = 0
|
||||
if acceleration <= 0:
|
||||
displacement = int(speed)
|
||||
|
||||
async for _ in precise_iteration_frequency(self.mapping.rel_rate):
|
||||
if not self.is_holding():
|
||||
return
|
||||
|
||||
# Cursors can only move by integers. To get smooth acceleration for
|
||||
# small acceleration values, the cursor needs to move by a pixel every
|
||||
# few iterations. This can be achieved by remembering the decimal
|
||||
# places that were cast away, and using them for the next iteration.
|
||||
if acceleration:
|
||||
current_speed += acceleration
|
||||
current_speed = direction * min(abs(current_speed), abs(speed))
|
||||
displacement_accumulator += current_speed
|
||||
displacement = int(displacement_accumulator)
|
||||
displacement_accumulator -= displacement
|
||||
|
||||
if displacement != 0:
|
||||
callback(EV_REL, code, displacement)
|
||||
45
inputremapper/injection/macros/tasks/parallel.py
Normal file
45
inputremapper/injection/macros/tasks/parallel.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig, ArgumentFlags
|
||||
from inputremapper.injection.macros.macro import Macro, InjectEventCallback
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class ParallelTask(Task):
|
||||
"""Run all provided macros in parallel."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="*macros",
|
||||
position=ArgumentFlags.spread,
|
||||
types=[Macro],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback: InjectEventCallback) -> None:
|
||||
macros: List[Macro] = self.get_argument("*macros").get_values()
|
||||
coroutines = [macro.run(callback) for macro in macros]
|
||||
await asyncio.gather(*coroutines)
|
||||
49
inputremapper/injection/macros/tasks/repeat.py
Normal file
49
inputremapper/injection/macros/tasks/repeat.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/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 inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class RepeatTask(Task):
|
||||
"""Repeat macros."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="repeats",
|
||||
position=0,
|
||||
types=[int],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="macro",
|
||||
position=1,
|
||||
types=[Macro],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
repeats = self.get_argument("repeats").get_value()
|
||||
macro = self.get_argument("macro").get_value()
|
||||
|
||||
for _ in range(repeats):
|
||||
await macro.run(callback)
|
||||
49
inputremapper/injection/macros/tasks/set.py
Normal file
49
inputremapper/injection/macros/tasks/set.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/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 inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import macro_variables
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class SetTask(Task):
|
||||
"""Set a variable to a certain value."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="variable",
|
||||
position=0,
|
||||
types=[str, float, int, None],
|
||||
is_variable_name=True,
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="value",
|
||||
position=1,
|
||||
types=[str, float, int, None],
|
||||
default=None,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
value = self.get_argument("value").get_value()
|
||||
assert macro_variables.is_alive()
|
||||
self.arguments["variable"].set_value(value)
|
||||
56
inputremapper/injection/macros/tasks/toggle.py
Normal file
56
inputremapper/injection/macros/tasks/toggle.py
Normal file
@ -0,0 +1,56 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.macro import Macro
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class ToggleTask(Task):
|
||||
"""Toggle macros."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="macro",
|
||||
position=0,
|
||||
types=[Macro],
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.trigger_count = 0
|
||||
|
||||
def press_trigger(self) -> None:
|
||||
Task.press_trigger(self)
|
||||
self.trigger_count += 1
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
macro = self.get_argument("macro").get_value()
|
||||
|
||||
while self.trigger_count <= 1:
|
||||
await macro.run(callback)
|
||||
await asyncio.sleep(1 / 1000)
|
||||
|
||||
self.trigger_count = 0
|
||||
45
inputremapper/injection/macros/tasks/util.py
Normal file
45
inputremapper/injection/macros/tasks/util.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import AsyncIterator
|
||||
|
||||
|
||||
async def precise_iteration_frequency(frequency: float) -> AsyncIterator[None]:
|
||||
"""A generator to iterate over in a fixed frequency.
|
||||
|
||||
asyncio.sleep might end up sleeping too long, for whatever reason. Maybe there are
|
||||
other async function calls that take longer than expected in the background.
|
||||
"""
|
||||
sleep = 1 / frequency
|
||||
corrected_sleep = sleep
|
||||
error = 0.0
|
||||
|
||||
while True:
|
||||
start = time.time()
|
||||
|
||||
yield
|
||||
|
||||
corrected_sleep -= error
|
||||
await asyncio.sleep(corrected_sleep)
|
||||
error = (time.time() - start) - sleep
|
||||
54
inputremapper/injection/macros/tasks/wait.py
Normal file
54
inputremapper/injection/macros/tasks/wait.py
Normal file
@ -0,0 +1,54 @@
|
||||
#!/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
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
|
||||
|
||||
class WaitTask(Task):
|
||||
"""Wait time in milliseconds."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="time",
|
||||
position=0,
|
||||
types=[float, int],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="max_time",
|
||||
position=1,
|
||||
types=[float, int, None],
|
||||
default=None,
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
time = self.get_argument("time").get_value()
|
||||
max_time = self.get_argument("max_time").get_value()
|
||||
|
||||
if max_time is not None and max_time > time:
|
||||
time = random.uniform(time, max_time)
|
||||
|
||||
await asyncio.sleep(time / 1000)
|
||||
76
inputremapper/injection/macros/tasks/wheel.py
Normal file
76
inputremapper/injection/macros/tasks/wheel.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/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
|
||||
|
||||
import math
|
||||
|
||||
from evdev.ecodes import (
|
||||
EV_REL,
|
||||
REL_WHEEL_HI_RES,
|
||||
REL_HWHEEL_HI_RES,
|
||||
REL_WHEEL,
|
||||
REL_HWHEEL,
|
||||
)
|
||||
|
||||
from inputremapper.injection.macros.argument import ArgumentConfig
|
||||
from inputremapper.injection.macros.task import Task
|
||||
from inputremapper.injection.macros.tasks.util import precise_iteration_frequency
|
||||
|
||||
|
||||
class WheelTask(Task):
|
||||
"""Move the scroll wheel."""
|
||||
|
||||
argument_configs = [
|
||||
ArgumentConfig(
|
||||
name="direction",
|
||||
position=0,
|
||||
types=[str],
|
||||
),
|
||||
ArgumentConfig(
|
||||
name="speed",
|
||||
position=1,
|
||||
types=[int, float],
|
||||
),
|
||||
]
|
||||
|
||||
async def run(self, callback) -> None:
|
||||
direction = self.get_argument("direction").get_value()
|
||||
|
||||
# 120, see https://www.kernel.org/doc/html/latest/input/event-codes.html#ev-rel
|
||||
code, value = {
|
||||
"up": ([REL_WHEEL, REL_WHEEL_HI_RES], [1 / 120, 1]),
|
||||
"down": ([REL_WHEEL, REL_WHEEL_HI_RES], [-1 / 120, -1]),
|
||||
"left": ([REL_HWHEEL, REL_HWHEEL_HI_RES], [1 / 120, 1]),
|
||||
"right": ([REL_HWHEEL, REL_HWHEEL_HI_RES], [-1 / 120, -1]),
|
||||
}[direction.lower()]
|
||||
|
||||
speed = self.get_argument("speed").get_value()
|
||||
remainder = [0.0, 0.0]
|
||||
|
||||
async for _ in precise_iteration_frequency(self.mapping.rel_rate):
|
||||
if not self.is_holding():
|
||||
return
|
||||
|
||||
for i in range(0, 2):
|
||||
float_value = value[i] * speed + remainder[i]
|
||||
remainder[i] = math.fmod(float_value, 1)
|
||||
if abs(float_value) >= 1:
|
||||
callback(EV_REL, code[i], int(float_value))
|
||||
90
inputremapper/injection/macros/variable.py
Normal file
90
inputremapper/injection/macros/variable.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/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/>.
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from inputremapper.configs.validation_errors import MacroError
|
||||
from inputremapper.injection.macros.macro import macro_variables
|
||||
|
||||
|
||||
class Variable:
|
||||
"""Something that the user passed into a macro function as parameter.
|
||||
|
||||
The value should already be parsed and validated, if const=True, according to the
|
||||
argument_configs of a given Task.
|
||||
|
||||
Examples:
|
||||
- const string "KEY_A" in `key(KEY_A)`
|
||||
- non-const string "foo" in `repeat($foo, key(KEY_A))` (`value` is the name here)
|
||||
- const Macro `key(a)` in `repeat(1, key(a))`
|
||||
- const int 1 in `repeat(1, key(a))
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any, const: bool) -> None:
|
||||
if not const and not isinstance(value, str):
|
||||
raise MacroError(f"Variables require a string name, not {value}")
|
||||
|
||||
self.value = value
|
||||
self.const = const
|
||||
|
||||
if not const:
|
||||
self.validate_variable_name()
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""If the variable is not const, return its name."""
|
||||
assert not self.const
|
||||
assert isinstance(self.value, str)
|
||||
return self.value
|
||||
|
||||
def get_value(self) -> Any:
|
||||
"""Get the variables value from the common variable storage process."""
|
||||
if self.const:
|
||||
return self.value
|
||||
|
||||
return macro_variables.get(self.value)
|
||||
|
||||
def set_value(self, value: Any) -> None:
|
||||
"""Set the variables value across all macros."""
|
||||
assert not self.const
|
||||
macro_variables[self.value] = value
|
||||
|
||||
def validate_variable_name(self) -> None:
|
||||
"""Check if this is a legit variable name.
|
||||
|
||||
Because they could clash with language features. If the macro can be
|
||||
parsed at all due to a problematic choice of a variable name.
|
||||
|
||||
Allowed examples: "foo", "Foo1234_", "_foo_1234"
|
||||
Not allowed: "1_foo", "foo=blub", "$foo", "foo,1234", "foo()"
|
||||
"""
|
||||
if not isinstance(self.value, str) or not re.match(
|
||||
r"^[A-Za-z_][A-Za-z_0-9]*$", self.value
|
||||
):
|
||||
raise MacroError(msg=f'"{self.value}" is not a legit variable name')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Variable "{self.value}" const={self.const} at {hex(id(self))}>'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Variable):
|
||||
return False
|
||||
|
||||
return self.const == other.const and self.value == other.value
|
||||
Reference in New Issue
Block a user