KarsMulder / evsieve

A utility for mapping events from Linux event devices.
GNU General Public License v2.0
199 stars 11 forks source link

Remap combo of mouse wheel and keyboard modifier #18

Closed Ashark closed 2 years ago

Ashark commented 2 years ago

I am trying to do a remapping of event like alt + wheel up to ctrl + wheel up. I did the following:

$ sudo evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
    --toggle rel:wheel @alt-up @alt-down id=alt \
    --hook   key:leftalt:1 toggle=alt:2 \
    --hook   key:leftalt:0 toggle=alt:1 \
    --map    rel:wheel@alt-down key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 \
    --print  @alt-down @alt-up \
    --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

/dev/input/event8 is my keyboard, /dev/input/event5 is my mouse. I combined them into hybrid keyboard+mouse virtual device. I used the idea from #9 to combine several source events.

And as far as I can tell from the printed events, the remapping is done correctly. But still the KDE does not react properly. I.e. I press and hold the alt, then scroll wheel up, then release alt. And I expect the text to be zoomed in (i.e. ctrl + wheel up). But it does not happen.

I also tried replacing rel:wheel with rel:wheel_hi_res. Also tried with key:leftalt:0 because I thought it may be in pressed state with the combo output, resulting in ctrl + alt + wheel up. Also tried even --block key:leftalt. Nothing helped.

Can you suggest what I am doing wrong? Also, see the question that requires this solution.

KarsMulder commented 2 years ago

It appears that there are three independent problems going on.

The first problem is that the above script maps alt+scroll to ctrl+alt+scroll rather than just ctrl+scroll. As I tested on my own KDE, the combination ctrl+alt+scroll does not cause any zoom in/out to happen. Thus, it is necessary to temporarily release the alt key:

sudo evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
    --toggle rel:wheel @alt-up @alt-down id=alt \
    --hook   key:leftalt:1 toggle=alt:2 \
    --hook   key:leftalt:0 toggle=alt:1 \
    --map    rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
    --print  @alt-down @alt-up \
    --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

The second problem appears to be that the capability of rel:wheel_hi_res events trips up KDE as well, because that event does not get sandwiched between the leftctrl events. This solution is a bit inelegant, but if you don't really need the high resolution wheel, blocking it solves the issue:

sudo evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
    --toggle rel:wheel @alt-up @alt-down id=alt \
    --hook   key:leftalt:1 toggle=alt:2 \
    --hook   key:leftalt:0 toggle=alt:1 \
    --map    rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
    --block  rel:wheel_hi_res \
    --print  @alt-down @alt-up \
    --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

(Duplicating the --map line with rel:wheel replaced with rel:wheel_hi_res does not seem to solve the issue, maybe because having ctrl_up, ctrl_down events sandwiched between the rel:wheel and rel:wheel_hi_res trips KDE up?)

The third reason it doesn't work issue is a problem with evsieve itself, related to issue #14; evsieve 1.3.1 and earlier send the whole sequence key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 within a single synchronisation report, which seems to trip some programs up.

In the past I was unsure whether I should classify this as a bug or as a feature for the sake of backwards compatibility, but this issue adds further evidence towards this behaviour really being a bug. It is already fixed on the main (development) branch.

... so in conclusion, the second script above works for me when using the in-development version of evsieve.

I feel like I have to refer to the in-development version too often recently. Development has been slow lately because I haven't been putting much into it recently, but I suppose I should try to put some more time towards getting a 1.4 version of evsieve released.

Ashark commented 2 years ago

Thanks! I can confirm your example worked. I also tried the same but for another modifiert, i.e. ctrl + wheel up to alt + wheel up and it also works. But how can I do a simultaneous map? I.e. ctrl + wheel up to alt + wheel up alt + wheel up to ctrl + wheel up

I tried it like this:

sudo evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
    --toggle rel:wheel @alt-up @alt-down id=alt \
    --toggle rel:wheel @ctrl-up @ctrl-down id=ctrl \
    --hook   key:leftalt:1 toggle=alt:2 \
    --hook   key:leftalt:0 toggle=alt:1 \
    --hook   key:leftctrl:1 toggle=ctrl:2 \
    --hook   key:leftctrl:0 toggle=ctrl:1 \
    --map    yield rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
    --map    yield rel:wheel@ctrl-down key:leftctrl:0 key:leftalt:1 key:leftalt:2 rel:wheel key:leftalt:0 key:leftctrl:1 \
    --block  rel:wheel_hi_res \
    --print  @alt-down @alt-up @ctrl-down @ctrl-up \
    --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

but it does not work. In prints I can see a domain ctrl-up and no any other domains, even when I press ctrl. If I swap two toggle lines, then it is the same, but with alt-up.

KarsMulder commented 2 years ago

Each event has only a single domain; the second --toggle overwrites the domain set by the first --toggle. Try reordering the arguments to achieve your desired effect:

sudo evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
    --hook   key:leftalt:1 toggle=alt:2 \
    --hook   key:leftalt:0 toggle=alt:1 \
    --hook   key:leftctrl:1 toggle=ctrl:2 \
    --hook   key:leftctrl:0 toggle=ctrl:1 \
    --toggle rel:wheel @alt-up @alt-down id=alt \
    --map    yield rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
    --toggle rel:wheel @ctrl-up @ctrl-down id=ctrl \
    --map    yield rel:wheel@ctrl-down key:leftctrl:0 key:leftalt:1 key:leftalt:2 rel:wheel key:leftalt:0 key:leftctrl:1 \
    --block  rel:wheel_hi_res \
    --print  @alt-down @alt-up @ctrl-down @ctrl-up \
    --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

The reason that --print isn't showing much useful information is because of the yield flags on the maps. All events except for rel:wheel@ctrl-up either (1) do not match any of the domains to be printed, or (2) are yielded and therefore invisible to everything except --output.

... whether it is sensible behaviour for --print to ignore yielded events is another philosophical question.

Ashark commented 2 years ago

Awesome! It works exactly how I want! Thank you so much for this utility! One more thing. Can you advise which is the best practice to toggle this remapping to normal one? I mean, when you launch this command, it takes about 1-2 seconds to create a virtual device. I want this remap to only apply when an application of interest is in focus. I know a kwin script could run a dbus commands. May this be used to interact with evsieve? In readme I do not see any "conditions" section. Or the only way is to manually interrupt / launch the remap command?

KarsMulder commented 2 years ago

That's an unusually long startup time. Tested using the script below, the startup time on my system is about 6\~9ms according to systemd, or about 15\~25ms according to the time utility.

#!/bin/bash

time (systemd-run --service-type=notify --unit=test.service evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
   --hook   key:leftalt:1 toggle=alt:2 \
   --hook   key:leftalt:0 toggle=alt:1 \
   --hook   key:leftctrl:1 toggle=ctrl:2 \
   --hook   key:leftctrl:0 toggle=ctrl:1 \
   --toggle rel:wheel @alt-up @alt-down id=alt \
   --map    yield rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
   --toggle rel:wheel @ctrl-up @ctrl-down id=ctrl \
   --map    yield rel:wheel@ctrl-down key:leftctrl:0 key:leftalt:1 key:leftalt:2 rel:wheel key:leftalt:0 key:leftctrl:1 \
   --block  rel:wheel_hi_res \
   --print  @alt-down @alt-up @ctrl-down @ctrl-up \
   --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM" \
    && if [[ -e /dev/input/by-id/merged-virtual-KM ]] ; then echo "Existence of virtual output confirmed." ; fi )

systemd-analyze critical-chain test.service
systemctl stop test.service

Output:

Running as unit: test.service
Existence of virtual output confirmed.

real    0m0.016s
user    0m0.002s
sys     0m0.004s
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.

test.service +7ms
[SNIP]

That said, these measurements were taken when the evsieve binary had been accessed recently and was therefore in the kernel's memory cache of the filesystem; the startup time can become longer if the binary needs to be read from a physical hard disk first. Also, by default, grabbing the input devices is delayed until no keys are pressed on them, though that does not delay creation of the output devices.


Startup speed aside, starting/quitting evsieve every time you change your active window may nevertheless not be a good idea because programs may get confused about which keys are currently pressed each time evsieve starts/quits (e.g. if a key is pressed on a virtual output device and that device disappears, that key may be considered released even if it is still pressed on your real keyboard.)

I started working on another new and currently half-baked feature to solve this problem: the --control-fifo argument. The rest of this post describes the state as of commit 0ba0bf5d53fe179fe3644a1772ec125053337165, because I am considering changing some syntax/semantics of it.

It can be used to create a named pipe/FIFO on the filesystem from which evsieve reads commands. For example, the following script creates a --toggle called enabled which can yield events before they reach any later map, along with a control FIFO at /tmp/merged_virtual_KM:

evsieve --input /dev/input/event8 grab --input /dev/input/event5 grab \
   --hook   key:leftalt:1 toggle=alt:2 \
   --hook   key:leftalt:0 toggle=alt:1 \
   --hook   key:leftctrl:1 toggle=ctrl:2 \
   --hook   key:leftctrl:0 toggle=ctrl:1 \
   --toggle "" "" @yield-now id=enabled \
   --map    yield @yield-now "" \
   --toggle rel:wheel @alt-up @alt-down id=alt \
   --map    yield rel:wheel@alt-down key:leftalt:0 key:leftctrl:1 key:leftctrl:2 rel:wheel key:leftctrl:0 key:leftalt:1 \
   --toggle rel:wheel @ctrl-up @ctrl-down id=ctrl \
   --map    yield rel:wheel@ctrl-down key:leftctrl:0 key:leftalt:1 key:leftalt:2 rel:wheel key:leftalt:0 key:leftctrl:1 \
   --block  rel:wheel_hi_res \
   --print  @alt-down @alt-up @ctrl-down @ctrl-up \
   --control-fifo /tmp/merged_virtual_KM \
   --output create-link=/dev/input/by-id/merged-virtual-KM name="merged virtual KM"

Then, from another shell script, you can write toggle enabled:1 or toggle enabled:2 to /tmp/merged_virtual_KM to switch the state of that toggle:

# To enable, if this script is running as root.
echo "toggle enabled:1" > /tmp/merged_virtual_KM
# To disable.
echo "toggle enabled:2" > /tmp/merged_virtual_KM
# If this script is not running as root, use the following construct, because sudo doesn't capture the > operator.
echo "toggle enabled:1" | sudo tee /tmp/merged_virtual_KM > /dev/null

Currently, toggle is the only command accepted, this may be expanded in the future. This possibly has security implications, e.g. it may be desirable to be able to send events or even change the arguments of evsieve at runtime; if the control FIFO were to accept such commands, anyone able to write to the FIFO may be able to achieve arbitrary code execution as root. Currently the FIFO is created with mode 600, but I wonder if that is enough to solve all potential attacks.

Ashark commented 2 years ago

That's an unusually long startup time.

I was probably not precise in terminology. I guess technically it may be already created. But I was speaking about "startup delay" from user perspective of view. I would even say that it feels like taking 2-3 seconds before the mouse can be moved in KDE after starting the evsieve. I know ydotool even uses a daemon to solve this problem.

I started working on another new and currently half-baked feature to solve this problem: the --control-fifo argument.

That is awesome! Exactly the thing how I wanted to control it. I tested it and it already works. Except that it once have written that the socket was gone. But restarting helped. The resting part for kwin to write that toggle enabled:1 or toggle enabled:2 to /tmp/merged_virtual_KM is out of scope of this utility, and I will try to create such script myself. I will comment the link how to do it later in case of success.

Other than that, the question for remapping wheel event with keyboard modifier seems solved for me. So I will close it. Again, huge thanks for this utility!

KarsMulder commented 2 years ago

Except that it once have written that the socket was gone.

That shouldn't happen, at least not since commit 0ba0bf5d53fe179fe3644a1772ec125053337165 (it was a known issue with commit 4b10c50dca464f0cc4b307594ea8fbe133c9fb6b, but I already fixed that before posting in this issue yesterday.)

I can't seem to reproduce it myself; for me the FIFO just accepts any amount of commands.

If evsieve closes the FIFO, then it should print some error message to stderr. Closing the FIFO should result in a message Unknown error: the fifo at /tmp/merged_virtual_KM is no longer available. which may or may not be preceded by another message.

Can you please check the output of the evsieve script to see what error it prints? In particular, can you tell me which error precedes Unknown error: the fifo at /tmp/merged_virtual_KM is no longer available. or confirm that no other error message precedes that line?

Ashark commented 2 years ago

Yeah, I can reproduce. The scenario is the following. Launch the evsieve remap script. echo "toggle enabled:1" | sudo tee /tmp/merged_virtual_KM > /dev/null - nothing happens, as it is already enabled at launch echo "toggle enabled:2" | sudo tee /tmp/merged_virtual_KM > /dev/null - remapping disables, no error message yet echo "toggle enabled:1" | sudo tee /tmp/merged_virtual_KM > /dev/null - remapping enabled again. But in the evsieve terminal the following message is printed:

...
Event:  type:code = rel:wheel      value = 1           domain = ctrl-up
Event:  type:code = rel:wheel      value = -1          domain = ctrl-up
While polling commands from /tmp/merged_virtual_KM:
    While reading from the fifo /tmp/merged_virtual_KM:
        System error: resource temporarily unavailable (os error 11)
Unknown error: the fifo at /tmp/merged_virtual_KM is no longer available.

And also, after interrupting the evsieve and relaunching it, I got

While attempting to create a fifo at /tmp/merged_virtual_KM:
    System error: file exists (os error 17)

If I delete it and start all again, the scenario repeats. And sometimes the error is printed right after the second command (echo "toggle enabled:2" | sudo tee /tmp/merged_virtual_KM > /dev/null).

KarsMulder commented 2 years ago

This weekend I'm preoccupied with something, so I haven't given this the full investigation it deserves yet, but here is the current status:

As for the "file exists" error: if it happens again, use ls -l /tmp/merged_virtual_KM and check the first character of the output to see if it is a FIFO (in which case the output starts with prw-------) or not (in which case the output will probably start with -rw-r--r--).

If evsieve leaves a FIFO behind, then that is another bug. If it is a normal file, then it was probably a new file created by one of the echo ... | tee ... commands after evsieve deleted the FIFO that used to be there, in which case dealing with those is merely a not-yet-implemented feature.

(Not having yet decided how to deal with the already-existing-file problem is one of the reasons this feature is still half-baked.)