Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions data/69-input-remapper-forwarded.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# helpful commands:
# udevadm monitor --property
# udevadm info --query=all --name=/dev/input/event3
#
# Forwarded input-remapper devices are virtual, so systemd's persistent-input
# rules do not assign ID_BUS to them. Without ID_BUS, 70-mouse.rules will not
# query hwdb and properties such as MOUSE_DPI get lost, changing pointer feel.
ACTION=="add", SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="input-remapper/*", ENV{ID_BUS}=="", ENV{ID_BUS}="usb"
2 changes: 1 addition & 1 deletion data/99-input-remapper.rules
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
# journalctl -f
# to get available variables:
# udevadm monitor --environment --udev --subsystem input
ACTION=="add", SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_PATH}!="platform-sound", RUN+="/bin/input-remapper-control --command autoload --device $env{DEVNAME}"
ACTION=="add", SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_PATH}!="platform-sound", ATTRS{phys}!="input-remapper*", RUN+="/bin/input-remapper-control --command autoload --device $env{DEVNAME}"
29 changes: 14 additions & 15 deletions inputremapper/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ def classify(device) -> DeviceType:
DENYLIST = [".*Yubico.*YubiKey.*", "Eee PC WMI hotkeys"]


def is_inputremapper_device(device: evdev.InputDevice) -> bool:
"""Return whether the device was created by input-remapper."""
name = str(device.name or "")
phys = str(device.phys or "")
return name.startswith("input-remapper") or phys.startswith("input-remapper")


def is_denylisted(device: evdev.InputDevice):
"""Check if a device should not be used in input-remapper.

Expand Down Expand Up @@ -375,6 +382,10 @@ def run(self):
if device.name == "Power Button":
continue

if is_inputremapper_device(device):
logger.debug('Skipping input-remapper device "%s"', device.name)
continue

device_type = classify(device)

if device_type == DeviceType.CAMERA:
Expand Down Expand Up @@ -481,17 +492,9 @@ def refresh(self):
keys = [f'"{group.key}"' for group in self._groups]
logger.info("Found %s", ", ".join(keys))

def filter(self, include_inputremapper: bool = False) -> List[_Group]:
"""Filter groups."""
result = []
for group in self._groups:
name = group.name
if not include_inputremapper and name.startswith("input-remapper"):
continue

result.append(group)

return result
def get_groups(self) -> List[_Group]:
"""Return groups."""
return list(self._groups)

def set_groups(self, new_groups: List[_Group]):
"""Overwrite all groups."""
Expand Down Expand Up @@ -525,7 +528,6 @@ def find(
name: Optional[str] = None,
key: Optional[str] = None,
path: Optional[str] = None,
include_inputremapper: bool = False,
) -> Optional[_Group]:
"""Find a group that matches the provided parameters.

Expand All @@ -540,9 +542,6 @@ def find(
"/dev/input/event3"
"""
for group in self._groups:
if not include_inputremapper and group.name.startswith("input-remapper"):
continue

if name and group.name != name:
continue

Expand Down
2 changes: 1 addition & 1 deletion inputremapper/gui/data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def active_input_config(self) -> Optional[InputConfig]:

def get_group_keys(self) -> Tuple[GroupKey, ...]:
"""Get all group keys (plugged devices)."""
return tuple(group.key for group in self._reader_client.groups.filter())
return tuple(group.key for group in self._reader_client.groups.get_groups())

def get_preset_names(self) -> Tuple[Name, ...]:
"""Get all preset names for active_group and current user sorted by age."""
Expand Down
3 changes: 1 addition & 2 deletions inputremapper/gui/reader_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,7 @@ def refresh_groups(self):
def publish_groups(self):
"""Announce all known groups."""
groups: Dict[str, List[str]] = {
group.key: group.types or []
for group in self.groups.filter(include_inputremapper=False)
group.key: group.types or [] for group in self.groups.get_groups()
}
self.message_broker.publish(GroupsData(groups))

Expand Down
26 changes: 21 additions & 5 deletions inputremapper/injection/injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ def get_udev_name(name: str, suffix: str) -> str:
return name


def get_forward_phys(source: evdev.InputDevice) -> str:
"""Use a stable phys marker for forwarded devices.

The original phys path must not be reused because it makes the forwarded
device look like the hardware device to our autoload rule. However, using a
dedicated input-remapper phys marker still allows us to identify and ignore
the forwarded device elsewhere.
"""
if source.phys:
return f"{DEV_NAME}/{source.phys}"

return DEV_NAME


@dataclass(frozen=True)
class InjectorStateMessage:
message_type = MessageType.injector_state
Expand Down Expand Up @@ -372,12 +386,14 @@ def _create_forwarding_device(self, source: evdev.InputDevice) -> evdev.UInput:
# typing"
try:
forward_to = evdev.UInput(
name=get_udev_name(source.name, "forwarded"),
# Keep the original name so system hwdb rules can still match the
# virtual device and restore properties such as MOUSE_DPI.
name=source.name,
events=self._copy_capabilities(source),
# phys=source.phys, # this leads to confusion. the appearance of
# a uinput with this "phys" property causes the udev rule to
# autoload for the original device, overwriting our previous
# attempts at starting an injection.
# Reusing source.phys causes our autoload rule to treat the
# forwarded device as hardware. Prefix it so it stays
# distinguishable while still carrying some source identity.
phys=get_forward_phys(source),
vendor=source.info.vendor,
product=source.info.product,
version=source.info.version,
Expand Down
1 change: 1 addition & 0 deletions install/data_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def get_data_files() -> list[tuple[str, list[str]]]:
# which rendered the whole operating system unusable.
("usr/share/dbus-1/system.d/", ["data/inputremapper.Control.conf"]),
("etc/xdg/autostart/", ["data/input-remapper-autoload.desktop"]),
("usr/lib/udev/rules.d", ["data/69-input-remapper-forwarded.rules"]),
("usr/lib/udev/rules.d", ["data/99-input-remapper.rules"]),
("usr/bin/", ["bin/input-remapper-gtk"]),
("usr/bin/", ["bin/input-remapper-service"]),
Expand Down
5 changes: 4 additions & 1 deletion tests/lib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,15 @@ def leds(self):


class UInputMock:
def __init__(self, events=None, name="unnamed", *args, **kwargs):
def __init__(
self, events=None, name="unnamed", phys="py-evdev-uinput", *args, **kwargs
):
self.fd = 0
self.write_count = 0
self.device = InputDevice("justdoit")
self.name = name
self.events = events
self.phys = phys
self.write_history = []

global uinputs
Expand Down
39 changes: 26 additions & 13 deletions tests/unit/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
classify,
DeviceType,
_Group,
is_inputremapper_device,
)
from tests.lib.fixtures import fixtures, keyboard_keys
from tests.lib.test_setup import test_setup
Expand Down Expand Up @@ -123,14 +124,6 @@ def test_find_groups(self):
"key": "gamepad",
}
),
json.dumps(
{
"paths": ["/dev/input/event40"],
"names": ["input-remapper Bar Device"],
"types": [DeviceType.KEYBOARD],
"key": "input-remapper Bar Device",
}
),
json.dumps(
{
"paths": ["/dev/input/event52"],
Expand All @@ -143,9 +136,7 @@ def test_find_groups(self):
),
)

groups2 = json.dumps(
[group.dumps() for group in groups.filter(include_inputremapper=True)]
)
groups2 = json.dumps([group.dumps() for group in groups.get_groups()])
self.assertEqual(pipe.groups, groups2)

def test_list_group_names(self):
Expand All @@ -160,13 +151,35 @@ def test_list_group_names(self):
],
)

def test_filter(self):
def test_get_groups(self):
# by default no input-remapper devices are present
filtered = groups.filter()
filtered = groups.get_groups()
keys = [group.key for group in filtered]
self.assertIn("Foo Device 2", keys)
self.assertNotIn("input-remapper Bar Device", keys)

def test_skip_inputremapper_phys_devices(self):
fixtures["/foo/bar"] = {
"name": "Logitech G Pro",
"phys": "input-remapper/usb-0000:0f:00.3-4.2/input2:1",
"info": evdev.DeviceInfo(3, 0x046D, 0x4079, 0x0111),
"capabilities": {
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_LEFT],
evdev.ecodes.EV_REL: [
evdev.ecodes.REL_X,
evdev.ecodes.REL_Y,
evdev.ecodes.REL_WHEEL,
],
},
}

groups.refresh()
self.assertIsNone(groups.find(path="/foo/bar"))

def test_is_inputremapper_device(self):
device = evdev.InputDevice("/dev/input/event40")
self.assertTrue(is_inputremapper_device(device))

def test_skip_camera(self):
fixtures["/foo/bar"] = {
"name": "camera",
Expand Down
19 changes: 17 additions & 2 deletions tests/unit/test_injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
is_in_capabilities,
InjectorState,
get_udev_name,
get_forward_phys,
)
from inputremapper.injection.numlock import is_numlock_on
from inputremapper.input_event import InputEvent
Expand Down Expand Up @@ -287,6 +288,14 @@ def test_get_udev_name(self):
"input-remapper abcd forwarded",
)

def test_get_forward_phys(self):
path = "/dev/input/event11"
device = evdev.InputDevice(path)
self.assertEqual(
get_forward_phys(device),
"input-remapper/usb-0000:03:00.0-1/input2/input2",
)

@mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch):
preset = Preset()
Expand Down Expand Up @@ -359,15 +368,21 @@ def test_capabilities_and_uinput_presence(self, ungrab_patch):

# reading and preventing original events from reaching the
# display server
forwarded_foo = uinputs.get("input-remapper Foo Device foo forwarded")
forwarded = uinputs.get("input-remapper Foo Device forwarded")
forwarded_foo = uinputs.get("Foo Device foo")
forwarded = uinputs.get("Foo Device")
self.assertIsNotNone(forwarded_foo)
self.assertIsNotNone(forwarded)

# copies capabilities for all other forwarded devices
self.assertIn(EV_REL, forwarded_foo.capabilities())
self.assertIn(EV_KEY, forwarded.capabilities())
self.assertEqual(sorted(forwarded.capabilities()[EV_KEY]), keyboard_keys)
self.assertEqual(
forwarded_foo.phys, "input-remapper/usb-0000:03:00.0-1/input2/input2"
)
self.assertEqual(
forwarded.phys, "input-remapper/usb-0000:03:00.0-1/input2/input3"
)

self.assertEqual(ungrab_patch.call_count, 2)

Expand Down
8 changes: 0 additions & 8 deletions tests/unit/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,14 +905,6 @@ def test_are_new_groups_available(self):
"key": "gamepad",
}
),
json.dumps(
{
"paths": ["/dev/input/event40"],
"names": ["input-remapper Bar Device"],
"types": [DeviceType.KEYBOARD],
"key": "input-remapper Bar Device",
}
),
json.dumps(
{
"paths": ["/dev/input/event52"],
Expand Down
Loading