Skip to content

Commit

Permalink
Reset unreleased keys of macros after stopping
Browse files Browse the repository at this point in the history
  • Loading branch information
sezanzeb committed Jan 1, 2025
1 parent b4852f1 commit 1122c8b
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 205 deletions.
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/autoloading-not-working.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ Please install the newest version from source to see if the problem has already
4. `sudo ls -l /proc/1/exe` to check if you are using systemd
5. `cat ~/.config/input-remapper-2/config.json` to see if the "autoload" config is written correctly
6. `systemctl status input-remapper -n 50` the service has to be running
7. `journalctl -b | grep input-remapper`

**Testing the setup**

1. `input-remapper-control --command hello`
2. `sudo pkill -f input-remapper-service && sudo input-remapper-service -d & sleep 2 && input-remapper-control --command autoload`, are your keys mapped now?
3. (while the previous command is still running) `sudo evtest` and search for a device suffixed by "mapped". Select it, does it report any events? Share the output.
3. If it still doesn't work, run (while the previous command is still running) `sudo evtest` and search for a device suffixed by "mapped". Select it, does it report any events? Share the output.
4. `sudo udevadm control --log-priority=debug && sudo udevadm control --reload-rules && journalctl -f | grep input-remapper`, now plug in the device that should autoload
6 changes: 5 additions & 1 deletion inputremapper/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,11 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool:
self.stop_injecting(group_key)

try:
injector = Injector(group, preset, self.mapping_parser)
injector = Injector(
group,
preset,
self.mapping_parser,
)
injector.start()
self.injectors[group.key] = injector
except OSError:
Expand Down
6 changes: 3 additions & 3 deletions inputremapper/gui/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,9 @@ def update_combination(self, combination: InputCombination):
CTX_WARNING,
_("ctrl, alt and shift may not combine properly"),
_(
"Your system might reinterpret combinations "
+ "with those after they are injected, and by doing so "
+ "break them."
"Your system might reinterpret combinations with those after they "
+ "are injected, and by doing so break them. Play around with the "
+ 'advanced "Release Input" toggle.'
),
)

Expand Down
3 changes: 0 additions & 3 deletions inputremapper/injection/event_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
EventListener,
NotifyCallback,
)
from inputremapper.injection.panic_counter import PanicCounter
from inputremapper.input_event import InputEvent
from inputremapper.logging.logger import logger
from inputremapper.utils import get_device_hash, DeviceHash
Expand Down Expand Up @@ -75,7 +74,6 @@ def __init__(
self._source = source
self.context = context
self.stop_event = stop_event
self.panic_counter = PanicCounter()

def stop(self):
"""Stop the reader."""
Expand Down Expand Up @@ -194,7 +192,6 @@ async def run(self):
)

async for event in self.read_loop():
await self.panic_counter.track(event)
try:
# Fire and forget, so that handlers and listeners can take their time,
# if they want to wait for something special to happen.
Expand Down
26 changes: 16 additions & 10 deletions inputremapper/injection/injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,19 +347,25 @@ async def _msg_listener(self) -> None:
await frame_available.wait()
frame_available.clear()
msg = self._msg_pipe[0].recv()

if msg == InjectorCommand.CLOSE:
logger.debug("Received close signal")
self._stop_event.set()
# give the event pipeline some time to reset devices
# before shutting the loop down
await asyncio.sleep(0.1)

# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop.stop()
self._msg_pipe[0].send(InjectorState.STOPPED)
await self._close()
return

async def _close(self):
logger.debug("Received close signal")
self._stop_event.set()
# give the event pipeline some time to reset devices
# before shutting the loop down
await asyncio.sleep(0.1)

# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop = asyncio.get_event_loop()
loop.stop()

self._msg_pipe[0].send(InjectorState.STOPPED)

def _create_forwarding_device(self, source: evdev.InputDevice) -> evdev.UInput:
# copy as much information as possible, because libinput uses the extra
# information to enable certain features like "Disable touchpad while
Expand Down
23 changes: 20 additions & 3 deletions inputremapper/injection/mapping_handlers/macro_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import asyncio
import traceback
from typing import Dict, Callable
from typing import Dict, Callable, Tuple

from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping
Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(
context: ContextProtocol,
):
super().__init__(combination, mapping, global_uinputs)
self._pressed_keys: Dict[Tuple[int, int], int] = {}
self._active = False
assert self.mapping.output_symbol is not None
self._macro = Parser.parse(self.mapping.output_symbol, context, mapping)
Expand Down Expand Up @@ -82,8 +83,11 @@ def notify(self, event: InputEvent, *_, **__) -> bool:

def handler(type_, code, value) -> None:
"""Handler for macros."""
self._remember_pressed_keys((type_, code, value))

self.global_uinputs.write(
(type_, code, value), self.mapping.target_uinput
(type_, code, value),
self.mapping.target_uinput,
)

asyncio.ensure_future(self.run_macro(handler))
Expand All @@ -96,10 +100,23 @@ def handler(type_, code, value) -> None:

def reset(self) -> None:
self._active = False
self._macro.release_trigger()

# To avoid a key hanging forever. Can be pretty annoying, especially if it is
# a modifier that makes you unable to interact with your system.
for (type, code), value in self._pressed_keys.items():
if value == 1:
logger.debug("Releasing key %s", (type, code, value))
self.global_uinputs.write(
(type, code, 0),
self.mapping.target_uinput,
)

def needs_wrapping(self) -> bool:
return True

def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return {InputCombination(self.input_configs): HandlerEnums.combination}

def _remember_pressed_keys(self, event: Tuple[int, int, int]) -> None:
type, code, value = event
self._pressed_keys[(type, code)] = value
71 changes: 0 additions & 71 deletions inputremapper/injection/panic_counter.py

This file was deleted.

4 changes: 0 additions & 4 deletions readme/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ To quickly restart input-remapper without pkexec prompts, you can use
sudo pkill -f input-remapper && sudo input-remapper-reader-service -d & sudo input-remapper-service -d & input-remapper-gtk -d
```

To stop an ongoing broken injection, you can try to type `inputremapperpanicquit` if
you can't control your terminal
anymore.

Linting
-------

Expand Down
8 changes: 5 additions & 3 deletions readme/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ Examples for particular devices and/or use cases:
- Modifiers `Alt_L` `Control_L` `Control_R` `Shift_L` `Shift_R`
- Mouse buttons `BTN_LEFT` `BTN_RIGHT` `BTN_MIDDLE` `BTN_SIDE` ...
- Multimedia keys `KEY_NEXTSONG` `KEY_PLAYPAUSE` `XF86AudioMicMute` ...
- Mouse scroll `wheel(down, 10)` `wheel(up, 10)`
- Mouse move `mouse(left, 1)` `mouse(right, 1)` `mouse(up, 1)` `mouse(down, 1)`

## Quick Overview of Macros
Mouse movements have to be performed by macros. See below.

## Short Macro Examples

- `key(BTN_LEFT)` a single mouse-click
- `key(1).key(2)` 1, 2
- `wheel(down, 10)` `wheel(up, 10)` Scroll while the input is pressed.
- `mouse(left, 5)` `mouse(right, 2)` `mouse(up, 1)` `mouse(down, 3)` Move the cursor while the input is pressed.
- `repeat(3, key(a).w(500))` a, a, a with 500ms pause
- `modify(Control_L, key(a).key(x))` CTRL + a, CTRL + x
- `key(1).hold(key(2)).key(3)` writes 1 2 2 ... 2 2 3 while the key is pressed
Expand Down
4 changes: 1 addition & 3 deletions readme/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ Bear in mind that anti-cheat software might detect macros in games.
> Acts like a pressed key. All names that are available in regular mappings can be used
> here.
>
> You don't have to use quotes around the symbol constants.
>
> ```ts
> key(symbol: str)
> ```
Expand Down Expand Up @@ -135,7 +133,7 @@ Bear in mind that anti-cheat software might detect macros in games.
### hold
> Executes the child macro repeatedly as long as the key is pressed down.
> Runs the child macro repeatedly as long as the input is pressed.
>
> ```ts
> hold(macro: Macro)
Expand Down
18 changes: 3 additions & 15 deletions readme/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,11 @@ If you later want to modify the Input of your mapping you need to use the
"Stop" button, so that the application can read your original input.
It would otherwise be invisible since the daemon maps it independently of the GUI.

## Panic

If input-remapper injected key-down events that never got released because of a bug or
a problematic macro (like `key_down(Shift_L))`), or you made a macro that somehow causes
other sorts of troubles, and you want to stop everything to regain control of your
system, you can type

```
inputremapperpanicquit
```

on your keyboard, which kills the input-remapper-service process. This will remove the
uinputs and thereby reset everything to normal (At least on Ubuntu/Plasma/Wayland).
You'll have to restart the gui after this.

## Troubleshooting

If your key is hanging due to a macro, unplug your device, and then plug it back in.
This should reset the key.

If stuff doesn't work, check the output of `input-remapper-gtk -d` and feel free
to [open up an issue here](https://github.com/sezanzeb/input-remapper/issues/new).
Make sure to not post any debug logs that were generated while you entered
Expand Down
1 change: 0 additions & 1 deletion tests/lib/cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def quick_cleanup(log=True):
from inputremapper.injection.macros.macro import macro_variables
from inputremapper.configs.keyboard_layout import keyboard_layout
from inputremapper.gui.utils import debounce_manager
from inputremapper.injection.global_uinputs import GlobalUInputs

if log:
logger.info("Quick cleanup...")
Expand Down
25 changes: 24 additions & 1 deletion tests/unit/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.

import unittest
from unittest.mock import patch

from evdev.ecodes import (
EV_REL,
Expand All @@ -34,6 +35,7 @@
from inputremapper.configs.preset import Preset
from inputremapper.injection.context import Context
from inputremapper.injection.global_uinputs import GlobalUInputs, UInput
from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser
from inputremapper.input_event import InputEvent
from tests.lib.test_setup import test_setup
Expand All @@ -59,7 +61,7 @@ def test_callbacks(self):

preset.add(
Mapping.from_combination(
InputCombination.from_tuples((1, 31)), "keyboard", "k(a)"
InputCombination.from_tuples((1, 31)), "keyboard", "key(a)"
)
)
preset.add(
Expand Down Expand Up @@ -114,6 +116,27 @@ def test_callbacks(self):
# 7 unique input events in the preset
self.assertEqual(7, len(context._handlers))

def test_reset(self):
global_uinputs = GlobalUInputs(UInput)
mapping_parser = MappingParser(global_uinputs)

preset = Preset()
preset.add(
Mapping.from_combination(
InputCombination.from_tuples((1, 31)),
"keyboard",
"key(a)",
)
)

context = Context(preset, {}, {}, mapping_parser)

self.assertEqual(1, len(context._handlers))

with patch.object(MacroHandler, "reset") as reset_mock:
context.reset()
reset_mock.assert_called_once()


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 1122c8b

Please sign in to comment.