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
tests/lib/__init__.py
Normal file
0
tests/lib/__init__.py
Normal file
172
tests/lib/cleanup.py
Normal file
172
tests/lib/cleanup.py
Normal file
@ -0,0 +1,172 @@
|
||||
#!/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 copy
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from pickle import UnpicklingError
|
||||
|
||||
import psutil
|
||||
|
||||
from tests.lib.constants import EVENT_READ_TIMEOUT
|
||||
from tests.lib.fixtures import fixtures
|
||||
from tests.lib.logger import logger
|
||||
from tests.lib.patches import uinputs
|
||||
from tests.lib.pipes import (
|
||||
uinput_write_history_pipe,
|
||||
uinput_write_history,
|
||||
pending_events,
|
||||
setup_pipe,
|
||||
)
|
||||
from tests.lib.tmp import tmp
|
||||
|
||||
# TODO on it. You don't need a framework for this by the way:
|
||||
# don't import anything from input_remapper gloablly here, because some files execute
|
||||
# code when imported, which can screw up patches. I wish we had a dependency injection
|
||||
# framework that patches together the dependencies during runtime...
|
||||
|
||||
|
||||
environ_copy = copy.deepcopy(os.environ)
|
||||
|
||||
|
||||
def join_children():
|
||||
"""Wait for child processes to exit. Stop them if it takes too long."""
|
||||
this = psutil.Process(os.getpid())
|
||||
|
||||
i = 0
|
||||
time.sleep(EVENT_READ_TIMEOUT)
|
||||
children = this.children(recursive=True)
|
||||
while len([c for c in children if c.status() != "zombie"]) > 0:
|
||||
for child in children:
|
||||
if i > 10:
|
||||
child.kill()
|
||||
logger.info("Killed pid %s because it didn't finish in time", child.pid)
|
||||
|
||||
children = this.children(recursive=True)
|
||||
time.sleep(EVENT_READ_TIMEOUT)
|
||||
i += 1
|
||||
|
||||
|
||||
def clear_write_history():
|
||||
"""Empty the history in preparation for the next test."""
|
||||
while len(uinput_write_history) > 0:
|
||||
uinput_write_history.pop()
|
||||
while uinput_write_history_pipe[0].poll():
|
||||
uinput_write_history_pipe[0].recv()
|
||||
|
||||
|
||||
def quick_cleanup(log=True):
|
||||
"""Reset the applications state."""
|
||||
# TODO no:
|
||||
# Reminder: before patches are applied in test.py, no inputremapper module
|
||||
# may be imported. So tests.lib imports them just-in-time in functions instead.
|
||||
from inputremapper.injection.macros.macro import macro_variables
|
||||
from inputremapper.configs.keyboard_layout import keyboard_layout
|
||||
from inputremapper.gui.utils import debounce_manager
|
||||
|
||||
if log:
|
||||
logger.info("Quick cleanup...")
|
||||
|
||||
debounce_manager.stop_all()
|
||||
|
||||
for device in list(pending_events.keys()):
|
||||
try:
|
||||
while pending_events[device][1].poll():
|
||||
pending_events[device][1].recv()
|
||||
except (UnpicklingError, EOFError):
|
||||
pass
|
||||
|
||||
# setup new pipes for the next test
|
||||
pending_events[device][1].close()
|
||||
pending_events[device][0].close()
|
||||
del pending_events[device]
|
||||
setup_pipe(device)
|
||||
|
||||
try:
|
||||
if asyncio.get_event_loop().is_running():
|
||||
for task in asyncio.all_tasks():
|
||||
task.cancel()
|
||||
except RuntimeError:
|
||||
# happens when the event loop disappears for magical reasons
|
||||
# create a fresh event loop
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
|
||||
if macro_variables.process is not None and not macro_variables.process.is_alive():
|
||||
# nothing should stop the process during runtime, if it has been started by
|
||||
# the injector once
|
||||
raise AssertionError("the SharedDict manager is not running anymore")
|
||||
|
||||
if macro_variables.process is not None:
|
||||
macro_variables._stop()
|
||||
|
||||
join_children()
|
||||
|
||||
macro_variables.start()
|
||||
|
||||
if os.path.exists(tmp):
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
keyboard_layout.populate()
|
||||
|
||||
clear_write_history()
|
||||
|
||||
for name in list(uinputs.keys()):
|
||||
del uinputs[name]
|
||||
|
||||
# for device in list(active_macros.keys()):
|
||||
# del active_macros[device]
|
||||
# for device in list(unreleased.keys()):
|
||||
# del unreleased[device]
|
||||
fixtures.reset()
|
||||
os.environ.update(environ_copy)
|
||||
for device in list(os.environ.keys()):
|
||||
if device not in environ_copy:
|
||||
del os.environ[device]
|
||||
|
||||
for _, pipe in pending_events.values():
|
||||
assert not pipe.poll()
|
||||
|
||||
assert macro_variables.is_alive(1)
|
||||
|
||||
if log:
|
||||
logger.info("Quick cleanup done")
|
||||
|
||||
|
||||
def cleanup():
|
||||
"""Reset the applications state.
|
||||
|
||||
Using this is slower, usually quick_cleanup() is sufficient.
|
||||
"""
|
||||
from inputremapper.groups import groups
|
||||
|
||||
logger.info("Cleanup...")
|
||||
|
||||
os.system("pkill -f input-remapper-service")
|
||||
os.system("pkill -f input-remapper-control")
|
||||
time.sleep(0.05)
|
||||
|
||||
quick_cleanup(log=False)
|
||||
groups.refresh()
|
||||
|
||||
logger.info("Cleanup done")
|
||||
28
tests/lib/constants.py
Normal file
28
tests/lib/constants.py
Normal file
@ -0,0 +1,28 @@
|
||||
#!/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/>.
|
||||
|
||||
# give tests some time to test stuff while the process
|
||||
# is still running
|
||||
EVENT_READ_TIMEOUT = 0.01
|
||||
|
||||
# based on experience how much time passes at most until
|
||||
# the reader-service starts receiving previously pushed events after a
|
||||
# call to start_reading
|
||||
START_READING_DELAY = 0.05
|
||||
36
tests/lib/fixture_pipes.py
Normal file
36
tests/lib/fixture_pipes.py
Normal file
@ -0,0 +1,36 @@
|
||||
#!/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 tests.lib.fixtures import fixtures
|
||||
from tests.lib.pipes import setup_pipe, close_pipe
|
||||
|
||||
|
||||
def create_fixture_pipes():
|
||||
# make sure those pipes exist before any process (the reader-service) gets forked,
|
||||
# so that events can be pushed after the fork.
|
||||
for _fixture in fixtures:
|
||||
setup_pipe(_fixture)
|
||||
|
||||
|
||||
def remove_fixture_pipes():
|
||||
for _fixture in fixtures:
|
||||
close_pipe(_fixture)
|
||||
420
tests/lib/fixtures.py
Normal file
420
tests/lib/fixtures.py
Normal file
@ -0,0 +1,420 @@
|
||||
#!/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 dataclasses
|
||||
import json
|
||||
import time
|
||||
from hashlib import md5
|
||||
from typing import Dict, Optional
|
||||
|
||||
import evdev
|
||||
|
||||
from inputremapper.configs.input_config import InputCombination
|
||||
from inputremapper.configs.mapping import Mapping
|
||||
from inputremapper.configs.paths import PathUtils
|
||||
from inputremapper.configs.preset import Preset
|
||||
from tests.lib.logger import logger
|
||||
|
||||
# input-remapper is only interested in devices that have EV_KEY, add some
|
||||
# random other stuff to test that they are ignored.
|
||||
phys_foo = "usb-0000:03:00.0-1/input2"
|
||||
info_foo = evdev.device.DeviceInfo(1, 1, 1, 1)
|
||||
|
||||
keyboard_keys = sorted(evdev.ecodes.keys.keys())[:255]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Fixture:
|
||||
path: str
|
||||
capabilities: Dict = dataclasses.field(default_factory=dict)
|
||||
name: str = "unset"
|
||||
info: evdev.device.DeviceInfo = evdev.device.DeviceInfo(None, None, None, None)
|
||||
phys: str = "unset"
|
||||
group_key: Optional[str] = None
|
||||
|
||||
# for joysticks and such
|
||||
min_abs: int = -(2**15)
|
||||
max_abs: int = 2**15
|
||||
|
||||
# uniq is typically empty
|
||||
uniq: str = ""
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.path)
|
||||
|
||||
def get_device_hash(self):
|
||||
s = str(self.capabilities) + self.name
|
||||
device_hash = md5(s.encode()).hexdigest()
|
||||
logger.info(
|
||||
'Hash for fixture "%s" "%s": "%s"',
|
||||
self.path,
|
||||
self.name,
|
||||
device_hash,
|
||||
)
|
||||
return device_hash
|
||||
|
||||
|
||||
class _Fixtures:
|
||||
"""contains all predefined Fixtures.
|
||||
Can be extended with new Fixtures during runtime"""
|
||||
|
||||
dev_input_event1 = Fixture(
|
||||
capabilities={
|
||||
evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A],
|
||||
},
|
||||
phys="usb-0000:03:00.0-0/input1",
|
||||
info=info_foo,
|
||||
name="Foo Device",
|
||||
path="/dev/input/event1",
|
||||
)
|
||||
# Another "Foo Device", which will get an incremented key.
|
||||
# If possible write tests using this one, because name != key here and
|
||||
# that would be important to test as well. Otherwise, the tests can't
|
||||
# see if the groups correct attribute is used in functions and paths.
|
||||
dev_input_event11 = Fixture(
|
||||
capabilities={
|
||||
evdev.ecodes.EV_KEY: [
|
||||
evdev.ecodes.BTN_LEFT,
|
||||
evdev.ecodes.BTN_TOOL_DOUBLETAP,
|
||||
],
|
||||
evdev.ecodes.EV_REL: [
|
||||
evdev.ecodes.REL_X,
|
||||
evdev.ecodes.REL_Y,
|
||||
evdev.ecodes.REL_WHEEL,
|
||||
evdev.ecodes.REL_HWHEEL,
|
||||
],
|
||||
},
|
||||
phys=f"{phys_foo}/input2",
|
||||
info=info_foo,
|
||||
name="Foo Device foo",
|
||||
group_key="Foo Device 2", # expected key
|
||||
path="/dev/input/event11",
|
||||
)
|
||||
dev_input_event10 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
|
||||
phys=f"{phys_foo}/input3",
|
||||
info=info_foo,
|
||||
name="Foo Device",
|
||||
group_key="Foo Device 2",
|
||||
path="/dev/input/event10",
|
||||
)
|
||||
dev_input_event13 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: [], evdev.ecodes.EV_SYN: []},
|
||||
phys=f"{phys_foo}/input1",
|
||||
info=info_foo,
|
||||
name="Foo Device",
|
||||
group_key="Foo Device 2",
|
||||
path="/dev/input/event13",
|
||||
)
|
||||
dev_input_event14 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_SYN: []},
|
||||
phys=f"{phys_foo}/input0",
|
||||
info=info_foo,
|
||||
name="Foo Device qux",
|
||||
group_key="Foo Device 2",
|
||||
path="/dev/input/event14",
|
||||
)
|
||||
dev_input_event15 = Fixture(
|
||||
capabilities={
|
||||
evdev.ecodes.EV_SYN: [],
|
||||
evdev.ecodes.EV_ABS: [
|
||||
evdev.ecodes.ABS_X,
|
||||
evdev.ecodes.ABS_Y,
|
||||
evdev.ecodes.ABS_RX,
|
||||
evdev.ecodes.ABS_RY,
|
||||
evdev.ecodes.ABS_Z,
|
||||
evdev.ecodes.ABS_RZ,
|
||||
evdev.ecodes.ABS_HAT0X,
|
||||
evdev.ecodes.ABS_HAT0Y,
|
||||
],
|
||||
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_A],
|
||||
},
|
||||
phys=f"{phys_foo}/input4",
|
||||
info=info_foo,
|
||||
name="Foo Device bar",
|
||||
group_key="Foo Device 2",
|
||||
path="/dev/input/event15",
|
||||
)
|
||||
# Bar Device
|
||||
dev_input_event20 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
|
||||
phys="usb-0000:03:00.0-2/input1",
|
||||
info=evdev.device.DeviceInfo(2, 1, 2, 1),
|
||||
name="Bar Device",
|
||||
path="/dev/input/event20",
|
||||
)
|
||||
dev_input_event30 = Fixture(
|
||||
capabilities={
|
||||
evdev.ecodes.EV_SYN: [],
|
||||
evdev.ecodes.EV_ABS: [
|
||||
evdev.ecodes.ABS_X,
|
||||
evdev.ecodes.ABS_Y,
|
||||
evdev.ecodes.ABS_RX,
|
||||
evdev.ecodes.ABS_RY,
|
||||
evdev.ecodes.ABS_Z,
|
||||
evdev.ecodes.ABS_RZ,
|
||||
evdev.ecodes.ABS_HAT0X,
|
||||
evdev.ecodes.ABS_HAT0Y,
|
||||
],
|
||||
evdev.ecodes.EV_KEY: [
|
||||
evdev.ecodes.BTN_A,
|
||||
evdev.ecodes.BTN_B,
|
||||
evdev.ecodes.BTN_X,
|
||||
evdev.ecodes.BTN_Y,
|
||||
],
|
||||
},
|
||||
phys="", # this is empty sometimes
|
||||
info=evdev.device.DeviceInfo(3, 1, 3, 1),
|
||||
name="gamepad",
|
||||
path="/dev/input/event30",
|
||||
)
|
||||
# device that is completely ignored
|
||||
dev_input_event31 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_SYN: []},
|
||||
phys="usb-0000:03:00.0-4/input1",
|
||||
info=evdev.device.DeviceInfo(4, 1, 4, 1),
|
||||
name="Power Button",
|
||||
path="/dev/input/event31",
|
||||
)
|
||||
# input-remapper devices are not displayed in the ui, some instance
|
||||
# of input-remapper started injecting, apparently.
|
||||
dev_input_event40 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
|
||||
phys="input-remapper/input1",
|
||||
info=evdev.device.DeviceInfo(5, 1, 5, 1),
|
||||
name="input-remapper Bar Device",
|
||||
path="/dev/input/event40",
|
||||
)
|
||||
# denylisted
|
||||
dev_input_event51 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
|
||||
phys="usb-0000:03:00.0-5/input1",
|
||||
info=evdev.device.DeviceInfo(6, 1, 6, 1),
|
||||
name="YuBiCofooYuBiKeYbar",
|
||||
path="/dev/input/event51",
|
||||
)
|
||||
# name requires sanitation
|
||||
dev_input_event52 = Fixture(
|
||||
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
|
||||
phys="usb-0000:03:00.0-3/input1",
|
||||
info=evdev.device.DeviceInfo(2, 1, 2, 1),
|
||||
name="Qux/[Device]?",
|
||||
path="/dev/input/event52",
|
||||
)
|
||||
dev_input_event32 = Fixture(
|
||||
capabilities={
|
||||
evdev.ecodes.EV_SYN: [],
|
||||
evdev.ecodes.EV_ABS: [
|
||||
evdev.ecodes.ABS_X,
|
||||
evdev.ecodes.ABS_Y,
|
||||
evdev.ecodes.ABS_RX,
|
||||
evdev.ecodes.ABS_RY,
|
||||
evdev.ecodes.ABS_Z,
|
||||
evdev.ecodes.ABS_RZ,
|
||||
evdev.ecodes.ABS_HAT0X,
|
||||
evdev.ecodes.ABS_HAT0Y,
|
||||
],
|
||||
evdev.ecodes.EV_KEY: [
|
||||
evdev.ecodes.BTN_A,
|
||||
evdev.ecodes.BTN_B,
|
||||
evdev.ecodes.BTN_X,
|
||||
evdev.ecodes.BTN_Y,
|
||||
],
|
||||
},
|
||||
phys="", # this is empty sometimes
|
||||
info=evdev.device.DeviceInfo(3, 1, 3, 1),
|
||||
name="gamepad abs 0 to 256",
|
||||
path="/dev/input/event32",
|
||||
min_abs=0,
|
||||
max_abs=256,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
self._iter = [
|
||||
self.dev_input_event1,
|
||||
self.dev_input_event11,
|
||||
self.dev_input_event10,
|
||||
self.dev_input_event13,
|
||||
self.dev_input_event14,
|
||||
self.dev_input_event15,
|
||||
self.dev_input_event20,
|
||||
self.dev_input_event30,
|
||||
self.dev_input_event31,
|
||||
self.dev_input_event40,
|
||||
self.dev_input_event51,
|
||||
self.dev_input_event52,
|
||||
self.dev_input_event32,
|
||||
]
|
||||
|
||||
def get_fixture(self, path: str) -> Fixture:
|
||||
"""get a Fixture by it's unique /dev/input/eventX path"""
|
||||
for fixture in self._iter:
|
||||
if fixture.path == path:
|
||||
return fixture
|
||||
|
||||
raise KeyError(f"Could not find fixture with path {path}")
|
||||
|
||||
def add_fixture(self, value: Fixture | dict) -> None:
|
||||
if isinstance(value, Fixture):
|
||||
value = value.__dict__
|
||||
|
||||
key = value["path"]
|
||||
if isinstance(value, Fixture):
|
||||
self._iter.append(value)
|
||||
elif isinstance(value, dict):
|
||||
self._iter.append(Fixture(**value))
|
||||
|
||||
def remove_fixture(self, path: str) -> None:
|
||||
index = 0
|
||||
for i, fixture in enumerate(self._iter):
|
||||
if fixture.path == path:
|
||||
index = i
|
||||
|
||||
del self._iter[index]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._iter)
|
||||
|
||||
def get_paths(self):
|
||||
"""Get a list of all available device paths."""
|
||||
paths = []
|
||||
for fixture in self._iter:
|
||||
paths.append(fixture.path)
|
||||
|
||||
return paths
|
||||
|
||||
def get(self, item) -> Optional[Fixture]:
|
||||
try:
|
||||
return self.get_fixture(item)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def foo_device_1_1(self):
|
||||
return self.get_fixture("/dev/input/event1")
|
||||
|
||||
@property
|
||||
def foo_device_2_mouse(self):
|
||||
return self.get_fixture("/dev/input/event11")
|
||||
|
||||
@property
|
||||
def foo_device_2_keyboard(self):
|
||||
return self.get_fixture("/dev/input/event10")
|
||||
|
||||
@property
|
||||
def foo_device_2_13(self):
|
||||
return self.get_fixture("/dev/input/event13")
|
||||
|
||||
@property
|
||||
def foo_device_2_qux(self):
|
||||
return self.get_fixture("/dev/input/event14")
|
||||
|
||||
@property
|
||||
def foo_device_2_gamepad(self):
|
||||
return self.get_fixture("/dev/input/event15")
|
||||
|
||||
@property
|
||||
def bar_device(self):
|
||||
return self.get_fixture("/dev/input/event20")
|
||||
|
||||
@property
|
||||
def gamepad(self):
|
||||
return self.get_fixture("/dev/input/event30")
|
||||
|
||||
@property
|
||||
def power_button(self):
|
||||
return self.get_fixture("/dev/input/event31")
|
||||
|
||||
@property
|
||||
def input_remapper_bar_device(self):
|
||||
return self.get_fixture("/dev/input/event40")
|
||||
|
||||
@property
|
||||
def YuBiCofooYuBiKeYbar(self):
|
||||
return self.get_fixture("/dev/input/event51")
|
||||
|
||||
@property
|
||||
def QuxSlashDeviceQuestionmark(self):
|
||||
return self.get_fixture("/dev/input/event52")
|
||||
|
||||
@property
|
||||
def gamepad_abs_0_to_256(self):
|
||||
return self.get_fixture("/dev/input/event32")
|
||||
|
||||
|
||||
fixtures = _Fixtures()
|
||||
|
||||
|
||||
def new_event(type, code, value, timestamp):
|
||||
"""Create a new InputEvent.
|
||||
|
||||
Handy because of the annoying sec and usec arguments of the regular
|
||||
evdev.InputEvent constructor.
|
||||
|
||||
Prefer using `InputEvent.key()`, `InputEvent.abs()`, `InputEvent.rel()` or just
|
||||
`InputEvent(0, 0, 1234, 2345, 3456)`.
|
||||
"""
|
||||
from inputremapper.input_event import InputEvent
|
||||
|
||||
if timestamp is None:
|
||||
timestamp = time.time()
|
||||
|
||||
sec = int(timestamp)
|
||||
usec = timestamp % 1 * 1000000
|
||||
event = InputEvent(sec, usec, type, code, value)
|
||||
return event
|
||||
|
||||
|
||||
def prepare_presets():
|
||||
"""prepare a few presets for use in tests
|
||||
"Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload
|
||||
"""
|
||||
preset1 = Preset(PathUtils.get_preset_path("Foo Device", "preset1"))
|
||||
preset1.add(
|
||||
Mapping.from_combination(
|
||||
InputCombination.from_tuples((1, 1)),
|
||||
output_symbol="b",
|
||||
)
|
||||
)
|
||||
preset1.add(Mapping.from_combination(InputCombination.from_tuples((1, 2))))
|
||||
preset1.save()
|
||||
|
||||
time.sleep(0.1)
|
||||
preset2 = Preset(PathUtils.get_preset_path("Foo Device", "preset2"))
|
||||
preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 3))))
|
||||
preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 4))))
|
||||
preset2.save()
|
||||
|
||||
# make sure the timestamp of preset 3 is the newest,
|
||||
# so that it will be automatically loaded by the GUI
|
||||
time.sleep(0.1)
|
||||
preset3 = Preset(PathUtils.get_preset_path("Foo Device", "preset3"))
|
||||
preset3.add(Mapping.from_combination(InputCombination.from_tuples((1, 5))))
|
||||
preset3.save()
|
||||
|
||||
with open(PathUtils.get_config_path("config.json"), "w") as file:
|
||||
json.dump({"autoload": {"Foo Device 2": "preset2"}}, file, indent=4)
|
||||
|
||||
return preset1, preset2, preset3
|
||||
32
tests/lib/is_service_running.py
Normal file
32
tests/lib/is_service_running.py
Normal file
@ -0,0 +1,32 @@
|
||||
#!/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 subprocess
|
||||
|
||||
|
||||
def is_service_running():
|
||||
"""Check if the daemon is running."""
|
||||
try:
|
||||
subprocess.check_output(["pgrep", "-f", "input-remapper-service"])
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
54
tests/lib/logger.py
Normal file
54
tests/lib/logger.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 sys
|
||||
import traceback
|
||||
import tracemalloc
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
|
||||
tracemalloc.start()
|
||||
|
||||
logger = logging.getLogger("input-remapper-test")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter("\033[90mTest: %(message)s\033[0m"))
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def update_inputremapper_verbosity():
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
logger.update_verbosity(True)
|
||||
|
||||
|
||||
def warn_with_traceback(message, category, filename, lineno, file=None, line=None):
|
||||
log = file if hasattr(file, "write") else sys.stderr
|
||||
traceback.print_stack(file=log)
|
||||
log.write(warnings.formatwarning(message, category, filename, lineno, line))
|
||||
|
||||
|
||||
def patch_warnings():
|
||||
# show traceback
|
||||
warnings.showwarning = warn_with_traceback
|
||||
warnings.simplefilter("always")
|
||||
413
tests/lib/patches.py
Normal file
413
tests/lib/patches.py
Normal file
@ -0,0 +1,413 @@
|
||||
#!/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 copy
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pickle import UnpicklingError
|
||||
from unittest.mock import patch
|
||||
import atexit
|
||||
|
||||
import evdev
|
||||
|
||||
from inputremapper.utils import get_evdev_constant_name
|
||||
from tests.lib.constants import EVENT_READ_TIMEOUT
|
||||
from tests.lib.fixtures import Fixture, fixtures, new_event
|
||||
from tests.lib.pipes import (
|
||||
setup_pipe,
|
||||
push_events,
|
||||
uinput_write_history,
|
||||
uinput_write_history_pipe,
|
||||
pending_events,
|
||||
)
|
||||
from tests.lib.xmodmap import xmodmap
|
||||
from tests.lib.tmp import tmp
|
||||
from tests.lib.logger import logger
|
||||
|
||||
|
||||
def patch_paths():
|
||||
from inputremapper.user import UserUtils
|
||||
|
||||
return [
|
||||
patch.object(UserUtils, "home", tmp),
|
||||
patch.dict(os.environ, {"XDG_CONFIG_HOME": os.path.join(tmp, ".config")}),
|
||||
]
|
||||
|
||||
|
||||
class InputDevice:
|
||||
# expose as existing attribute, otherwise the patch for
|
||||
# evdev < 1.0.0 will crash the test
|
||||
path = None
|
||||
|
||||
def __init__(self, path):
|
||||
if path != "justdoit" and not fixtures.get(path):
|
||||
# beware that fixtures keys and the path attribute of a fixture can
|
||||
# theoretically be different. I don't know if this is the case right now
|
||||
logger.error(
|
||||
'path "%s" was not found in fixtures. available: %s',
|
||||
path,
|
||||
list(fixtures.get_paths()),
|
||||
)
|
||||
raise FileNotFoundError()
|
||||
if path == "justdoit":
|
||||
self._fixture = Fixture(path="justdoit")
|
||||
else:
|
||||
self._fixture = fixtures.get_fixture(path)
|
||||
|
||||
self.path = path
|
||||
self.phys = self._fixture.phys
|
||||
self.info = self._fixture.info
|
||||
self.name = self._fixture.name
|
||||
self.uniq = self._fixture.uniq
|
||||
|
||||
# this property exists only for test purposes and is not part of
|
||||
# the original evdev.InputDevice class
|
||||
self.group_key = self._fixture.group_key or self._fixture.name
|
||||
|
||||
# ensure a pipe exists to make this object act like
|
||||
# it is reading events from a device
|
||||
setup_pipe(self._fixture)
|
||||
|
||||
self.fd = pending_events[self._fixture][1].fileno()
|
||||
|
||||
def push_events(self, events):
|
||||
push_events(self._fixture, events)
|
||||
|
||||
def fileno(self):
|
||||
"""Compatibility to select.select."""
|
||||
return self.fd
|
||||
|
||||
def log(self, key, msg):
|
||||
logger.info('%s "%s" "%s" %s', msg, self.name, self.path, key)
|
||||
|
||||
def absinfo(self, *args):
|
||||
raise Exception("Ubuntus version of evdev doesn't support .absinfo")
|
||||
|
||||
def grab(self):
|
||||
logger.info("grab %s %s", self.name, self.path)
|
||||
|
||||
def ungrab(self):
|
||||
logger.info("ungrab %s %s", self.name, self.path)
|
||||
|
||||
async def async_read_loop(self):
|
||||
logger.info("starting read loop for %s", self.path)
|
||||
new_frame = asyncio.Event()
|
||||
asyncio.get_running_loop().add_reader(self.fd, new_frame.set)
|
||||
while True:
|
||||
await new_frame.wait()
|
||||
new_frame.clear()
|
||||
if not pending_events[self._fixture][1].poll():
|
||||
# todo: why? why do we need this?
|
||||
# sometimes this happens, as if a other process calls recv on
|
||||
# the pipe
|
||||
continue
|
||||
|
||||
event = pending_events[self._fixture][1].recv()
|
||||
logger.info("got %s at %s", event, self.path)
|
||||
yield event
|
||||
|
||||
def read(self):
|
||||
# the patched fake InputDevice objects read anything pending from
|
||||
# that group.
|
||||
# To be realistic it would have to check if the provided
|
||||
# element is in its capabilities.
|
||||
if self.group_key not in pending_events:
|
||||
self.log("no events to read", self.group_key)
|
||||
return
|
||||
|
||||
# consume all of them
|
||||
while pending_events[self._fixture][1].poll():
|
||||
event = pending_events[self._fixture][1].recv()
|
||||
self.log(event, "read")
|
||||
yield event
|
||||
time.sleep(EVENT_READ_TIMEOUT)
|
||||
|
||||
def read_loop(self):
|
||||
"""Endless loop that yields events."""
|
||||
while True:
|
||||
event = pending_events[self._fixture][1].recv()
|
||||
if event is not None:
|
||||
self.log(event, "read_loop")
|
||||
yield event
|
||||
time.sleep(EVENT_READ_TIMEOUT)
|
||||
|
||||
def read_one(self):
|
||||
"""Read one event or none if nothing available."""
|
||||
if not pending_events.get(self._fixture):
|
||||
return None
|
||||
|
||||
if not pending_events[self._fixture][1].poll():
|
||||
return None
|
||||
|
||||
try:
|
||||
event = pending_events[self._fixture][1].recv()
|
||||
except (UnpicklingError, EOFError):
|
||||
# failed in tests sometimes
|
||||
return None
|
||||
|
||||
self.log(event, "read_one")
|
||||
return event
|
||||
|
||||
def capabilities(self, absinfo=True, verbose=False):
|
||||
result = copy.deepcopy(self._fixture.capabilities)
|
||||
|
||||
if absinfo and evdev.ecodes.EV_ABS in result:
|
||||
absinfo_obj = evdev.AbsInfo(
|
||||
value=None,
|
||||
min=self._fixture.min_abs,
|
||||
fuzz=None,
|
||||
flat=None,
|
||||
resolution=None,
|
||||
max=self._fixture.max_abs,
|
||||
)
|
||||
|
||||
ev_abs = []
|
||||
for ev_code in result[evdev.ecodes.EV_ABS]:
|
||||
if ev_code in range(0x10, 0x18): # ABS_HAT0X - ABS_HAT3Y
|
||||
absinfo_obj = evdev.AbsInfo(
|
||||
value=None,
|
||||
min=-1,
|
||||
fuzz=None,
|
||||
flat=None,
|
||||
resolution=None,
|
||||
max=1,
|
||||
)
|
||||
ev_abs.append((ev_code, absinfo_obj))
|
||||
|
||||
result[evdev.ecodes.EV_ABS] = ev_abs
|
||||
|
||||
return result
|
||||
|
||||
def input_props(self):
|
||||
return []
|
||||
|
||||
def leds(self):
|
||||
return []
|
||||
|
||||
|
||||
uinputs = {}
|
||||
|
||||
|
||||
class UInputMock:
|
||||
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
|
||||
uinputs[name] = self
|
||||
|
||||
def capabilities(self, verbose=False, absinfo=True):
|
||||
if absinfo or 3 not in self.events:
|
||||
return self.events
|
||||
else:
|
||||
events = self.events.copy()
|
||||
events[3] = [code for code, _ in self.events[3]]
|
||||
return events
|
||||
|
||||
def write(self, type, code, value):
|
||||
self.write_count += 1
|
||||
event = new_event(type, code, value, time.time())
|
||||
uinput_write_history.append(event)
|
||||
uinput_write_history_pipe[1].send(event)
|
||||
self.write_history.append(event)
|
||||
logger.info(
|
||||
'%s %s written to "%s"',
|
||||
(type, code, value),
|
||||
get_evdev_constant_name(type, code),
|
||||
self.name,
|
||||
)
|
||||
|
||||
def syn(self):
|
||||
pass
|
||||
|
||||
|
||||
def patch_evdev():
|
||||
def list_devices():
|
||||
return [fixture_.path for fixture_ in fixtures]
|
||||
|
||||
class PatchedInputEvent(evdev.InputEvent):
|
||||
def __init__(self, sec, usec, type, code, value):
|
||||
self.t = (type, code, value)
|
||||
super().__init__(sec, usec, type, code, value)
|
||||
|
||||
def copy(self):
|
||||
return PatchedInputEvent(
|
||||
self.sec,
|
||||
self.usec,
|
||||
self.type,
|
||||
self.code,
|
||||
self.value,
|
||||
)
|
||||
|
||||
return [
|
||||
patch.object(evdev, "list_devices", list_devices),
|
||||
patch.object(evdev, "InputDevice", InputDevice),
|
||||
patch.object(evdev.UInput, "capabilities", UInputMock.capabilities),
|
||||
patch.object(evdev.UInput, "write", UInputMock.write),
|
||||
patch.object(evdev.UInput, "syn", UInputMock.syn),
|
||||
patch.object(evdev.UInput, "__init__", UInputMock.__init__),
|
||||
patch.object(evdev, "InputEvent", PatchedInputEvent),
|
||||
]
|
||||
|
||||
|
||||
def patch_events():
|
||||
# improve logging of stuff
|
||||
return patch.object(
|
||||
evdev.InputEvent,
|
||||
"__str__",
|
||||
lambda self: (f"InputEvent{(self.type, self.code, self.value)}"),
|
||||
)
|
||||
|
||||
|
||||
def patch_os_system():
|
||||
"""Avoid running pkexec."""
|
||||
original_system = os.system
|
||||
|
||||
def system(command):
|
||||
if "pkexec" in command:
|
||||
# because it
|
||||
# - will open a window for user input
|
||||
# - has no knowledge of the fixtures and patches
|
||||
raise Exception("Write patches to avoid running pkexec stuff")
|
||||
return original_system(command)
|
||||
|
||||
return patch.object(os, "system", system)
|
||||
|
||||
|
||||
def patch_atexit_register():
|
||||
"""Avoid adding tons of redundant atexit handlers that we don't need anyway.
|
||||
Otherwise we get lots of logs at the end of gui tests that bury the test result.
|
||||
"""
|
||||
return patch.object(atexit, "register")
|
||||
|
||||
|
||||
def patch_check_output():
|
||||
"""Xmodmap -pke should always return a fixed set of symbols.
|
||||
|
||||
On some installations the `xmodmap` command might be missig completely,
|
||||
which would break the tests.
|
||||
"""
|
||||
original_check_output = subprocess.check_output
|
||||
|
||||
def check_output(command, *args, **kwargs):
|
||||
if "xmodmap" in command and "-pke" in command:
|
||||
return xmodmap
|
||||
return original_check_output(command, *args, **kwargs)
|
||||
|
||||
return patch.object(subprocess, "check_output", check_output)
|
||||
|
||||
|
||||
def patch_regrab_timeout():
|
||||
# no need for a high number in tests
|
||||
from inputremapper.injection.injector import Injector
|
||||
|
||||
return patch.object(Injector, "regrab_timeout", 0.05)
|
||||
|
||||
|
||||
def is_running_patch():
|
||||
logger.info("is_running is patched to always return True")
|
||||
return True
|
||||
|
||||
|
||||
def patch_is_running():
|
||||
from inputremapper.gui.reader_service import ReaderService
|
||||
|
||||
return patch.object(ReaderService, "is_running", is_running_patch)
|
||||
|
||||
|
||||
def patch_enable_all_logs():
|
||||
from inputremapper.logging.logger import Logger
|
||||
|
||||
return patch.object(Logger, "analog_log_threshold", 0)
|
||||
|
||||
|
||||
class FakeDaemonProxy:
|
||||
def __init__(self):
|
||||
self.calls = {
|
||||
"stop_injecting": [],
|
||||
"get_state": [],
|
||||
"start_injecting": [],
|
||||
"stop_all": 0,
|
||||
"set_config_dir": [],
|
||||
"autoload": 0,
|
||||
"autoload_single": [],
|
||||
"hello": [],
|
||||
"quit": 0,
|
||||
}
|
||||
|
||||
def stop_injecting(self, group_key: str) -> None:
|
||||
self.calls["stop_injecting"].append(group_key)
|
||||
|
||||
def get_state(self, group_key: str):
|
||||
from inputremapper.injection.injector import InjectorState
|
||||
|
||||
self.calls["get_state"].append(group_key)
|
||||
return InjectorState.STOPPED
|
||||
|
||||
def start_injecting(self, group_key: str, preset: str) -> bool:
|
||||
self.calls["start_injecting"].append((group_key, preset))
|
||||
return True
|
||||
|
||||
def stop_all(self) -> None:
|
||||
self.calls["stop_all"] += 1
|
||||
|
||||
def set_config_dir(self, config_dir: str) -> None:
|
||||
self.calls["set_config_dir"].append(config_dir)
|
||||
|
||||
def autoload(self) -> None:
|
||||
self.calls["autoload"] += 1
|
||||
|
||||
def autoload_single(self, group_key: str) -> None:
|
||||
self.calls["autoload_single"].append(group_key)
|
||||
|
||||
def hello(self, out: str) -> str:
|
||||
self.calls["hello"].append(out)
|
||||
return out
|
||||
|
||||
def quit(self):
|
||||
self.calls["quit"] += 1
|
||||
|
||||
|
||||
def create_patches():
|
||||
return [
|
||||
# Sketchy, they only work because the whole modules are imported, instead of
|
||||
# importing `check_output` and `system` from the module.
|
||||
*patch_evdev(),
|
||||
patch_os_system(),
|
||||
patch_atexit_register(),
|
||||
patch_check_output(),
|
||||
# Those are comfortably wrapped in a class, and are therefore easy to patch
|
||||
*patch_paths(),
|
||||
patch_regrab_timeout(),
|
||||
patch_is_running(),
|
||||
patch_events(),
|
||||
patch_enable_all_logs(),
|
||||
]
|
||||
96
tests/lib/pipes.py
Normal file
96
tests/lib/pipes.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/>.
|
||||
|
||||
"""Reading events from fixtures, making fixtures act like they are sending events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
from multiprocessing.connection import Connection
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from tests.lib.fixtures import Fixture
|
||||
from tests.lib.logger import logger
|
||||
|
||||
uinput_write_history = []
|
||||
# for tests that makes the injector create its processes
|
||||
uinput_write_history_pipe = multiprocessing.Pipe()
|
||||
pending_events: Dict[Fixture, Tuple[Connection, Connection]] = {}
|
||||
|
||||
|
||||
def read_write_history_pipe():
|
||||
"""Convert the write history from the pipe to some easier to manage list."""
|
||||
history = []
|
||||
while uinput_write_history_pipe[0].poll():
|
||||
event = uinput_write_history_pipe[0].recv()
|
||||
history.append((event.type, event.code, event.value))
|
||||
return history
|
||||
|
||||
|
||||
def setup_pipe(fixture: Fixture):
|
||||
"""Create a pipe that can be used to send events to the reader-service,
|
||||
which in turn will be sent to the reader-client
|
||||
"""
|
||||
if pending_events.get(fixture) is None:
|
||||
pending_events[fixture] = multiprocessing.Pipe()
|
||||
|
||||
|
||||
def close_pipe(fixture: Fixture):
|
||||
if fixture in pending_events:
|
||||
pipe1, pipe2 = pending_events[fixture]
|
||||
pipe1.close()
|
||||
pipe2.close()
|
||||
del pending_events[fixture]
|
||||
|
||||
|
||||
def get_events():
|
||||
"""Get all events written by the injector."""
|
||||
return uinput_write_history
|
||||
|
||||
|
||||
def push_event(fixture: Fixture, event, force: bool = False):
|
||||
"""Make a device act like it is reading events from evdev.
|
||||
|
||||
push_event is like hitting a key on a keyboard for stuff that reads from
|
||||
evdev.InputDevice (which is patched in test.py to work that way)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fixture
|
||||
For example 'Foo Device'
|
||||
event
|
||||
The InputEvent to send
|
||||
force
|
||||
don't check if the event is in fixture.capabilities
|
||||
"""
|
||||
setup_pipe(fixture)
|
||||
if not force and (
|
||||
not fixture.capabilities.get(event.type)
|
||||
or event.code not in fixture.capabilities[event.type]
|
||||
):
|
||||
raise AssertionError(f"Fixture {fixture.path} cannot send {event}")
|
||||
logger.info("Simulating %s for %s", event, fixture.path)
|
||||
pending_events[fixture][0].send(event)
|
||||
|
||||
|
||||
def push_events(fixture: Fixture, events, force=False):
|
||||
"""Push multiple events."""
|
||||
for event in events:
|
||||
push_event(fixture, event, force)
|
||||
26
tests/lib/spy.py
Normal file
26
tests/lib/spy.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/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 unittest.mock import patch
|
||||
|
||||
|
||||
def spy(obj, name):
|
||||
"""Convenient wrapper for patch.object(..., ..., wraps=...)."""
|
||||
return patch.object(obj, name, wraps=obj.__getattribute__(name))
|
||||
109
tests/lib/test_setup.py
Normal file
109
tests/lib/test_setup.py
Normal file
@ -0,0 +1,109 @@
|
||||
#!/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 os
|
||||
import tracemalloc
|
||||
|
||||
from tests.lib.cleanup import cleanup, quick_cleanup
|
||||
from tests.lib.fixture_pipes import create_fixture_pipes, remove_fixture_pipes
|
||||
from tests.lib.is_service_running import is_service_running
|
||||
from tests.lib.logger import update_inputremapper_verbosity, logger
|
||||
from tests.lib.patches import create_patches
|
||||
|
||||
|
||||
def test_setup(cls):
|
||||
"""A class decorator to
|
||||
- apply the patches to all tests
|
||||
- check if the deamon is already running
|
||||
- create pipes to send events to the reader service
|
||||
- reset stuff automatically
|
||||
"""
|
||||
original_setUp = cls.setUp
|
||||
original_tearDown = cls.tearDown
|
||||
original_setUpClass = cls.setUpClass
|
||||
original_tearDownClass = cls.tearDownClass
|
||||
|
||||
tracemalloc.start()
|
||||
os.environ["UNITTEST"] = "1"
|
||||
update_inputremapper_verbosity()
|
||||
|
||||
patches = create_patches()
|
||||
|
||||
def resetPatches():
|
||||
# In case some patches carry a state (I don't remember, idk), stop and start
|
||||
# them from scratch
|
||||
for patch in patches:
|
||||
patch.stop()
|
||||
|
||||
for patch in patches:
|
||||
patch.start()
|
||||
|
||||
def setUpClass():
|
||||
logger.info("setUpClass %s", cls)
|
||||
|
||||
if is_service_running():
|
||||
# let tests control daemon existance
|
||||
raise Exception("Expected the service not to be running already.")
|
||||
|
||||
create_fixture_pipes()
|
||||
|
||||
# I don't know. Somehow tearDownClass is called before the test, so lets
|
||||
# make sure the patches are started already when the class is set up, so that
|
||||
# an unpatched `prepare_all` doesn't take ages to finish, and doesn't do funky
|
||||
# stuff with the real evdev.
|
||||
resetPatches()
|
||||
|
||||
original_setUpClass()
|
||||
|
||||
def tearDownClass():
|
||||
logger.info("tearDownClass %s", cls)
|
||||
original_tearDownClass()
|
||||
|
||||
remove_fixture_pipes()
|
||||
|
||||
# Do the more thorough cleanup only after all tests of classes, because it
|
||||
# slows tests down. If this is required after each test, call it in your
|
||||
# tearDown method.
|
||||
cleanup()
|
||||
|
||||
def setUp(self):
|
||||
logger.info("setUp %s", cls)
|
||||
|
||||
resetPatches()
|
||||
|
||||
original_setUp(self)
|
||||
|
||||
def tearDown(self):
|
||||
logger.info("tearDown %s", cls)
|
||||
|
||||
original_tearDown(self)
|
||||
|
||||
quick_cleanup()
|
||||
|
||||
resetPatches()
|
||||
|
||||
cls.setUp = setUp
|
||||
cls.tearDown = tearDown
|
||||
cls.setUpClass = setUpClass
|
||||
cls.tearDownClass = tearDownClass
|
||||
|
||||
return cls
|
||||
30
tests/lib/tmp.py
Normal file
30
tests/lib/tmp.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/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 tempfile
|
||||
from tests.lib.logger import logger
|
||||
|
||||
# When it gets garbage collected it cleans up the temporary directory so it needs to
|
||||
# stay reachable while the tests are ran.
|
||||
temporary_directory = tempfile.TemporaryDirectory(prefix="input-remapper-test")
|
||||
tmp = temporary_directory.name
|
||||
logger.info('tmp at "%s"', tmp)
|
||||
128
tests/lib/xmodmap.py
Normal file
128
tests/lib/xmodmap.py
Normal file
@ -0,0 +1,128 @@
|
||||
#!/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/>.
|
||||
|
||||
xmodmap = (
|
||||
b"keycode 8 =\nkeycode 9 = Escape NoSymbol Escape\nkeycode 10 = 1 exclam 1 exclam onesuperior exclamdown ones"
|
||||
b"uperior\nkeycode 11 = 2 quotedbl 2 quotedbl twosuperior oneeighth twosuperior\nkeycode 12 = 3 section 3 sectio"
|
||||
b"n threesuperior sterling threesuperior\nkeycode 13 = 4 dollar 4 dollar onequarter currency onequarter\nkeycode "
|
||||
b" 14 = 5 percent 5 percent onehalf threeeighths onehalf\nkeycode 15 = 6 ampersand 6 ampersand notsign fiveeighth"
|
||||
b"s notsign\nkeycode 16 = 7 slash 7 slash braceleft seveneighths braceleft\nkeycode 17 = 8 parenleft 8 parenleft"
|
||||
b" bracketleft trademark bracketleft\nkeycode 18 = 9 parenright 9 parenright bracketright plusminus bracketright"
|
||||
b"\nkeycode 19 = 0 equal 0 equal braceright degree braceright\nkeycode 20 = ssharp question ssharp question back"
|
||||
b"slash questiondown U1E9E\nkeycode 21 = dead_acute dead_grave dead_acute dead_grave dead_cedilla dead_ogonek dea"
|
||||
b"d_cedilla\nkeycode 22 = BackSpace BackSpace BackSpace BackSpace\nkeycode 23 = Tab ISO_Left_Tab Tab ISO_Left_Ta"
|
||||
b"b\nkeycode 24 = q Q q Q at Greek_OMEGA at\nkeycode 25 = w W w W lstroke Lstroke lstroke\nkeycode 26 = e E e E"
|
||||
b" EuroSign EuroSign EuroSign\nkeycode 27 = r R r R paragraph registered paragraph\nkeycode 28 = t T t T tslash "
|
||||
b"Tslash tslash\nkeycode 29 = z Z z Z leftarrow yen leftarrow\nkeycode 30 = u U u U downarrow uparrow downarrow"
|
||||
b"\nkeycode 31 = i I i I rightarrow idotless rightarrow\nkeycode 32 = o O o O oslash Oslash oslash\nkeycode 33 "
|
||||
b"= p P p P thorn THORN thorn\nkeycode 34 = udiaeresis Udiaeresis udiaeresis Udiaeresis dead_diaeresis dead_above"
|
||||
b"ring dead_diaeresis\nkeycode 35 = plus asterisk plus asterisk asciitilde macron asciitilde\nkeycode 36 = Retur"
|
||||
b"n NoSymbol Return\nkeycode 37 = Control_L NoSymbol Control_L\nkeycode 38 = a A a A ae AE ae\nkeycode 39 = s S"
|
||||
b" s S U017F U1E9E U017F\nkeycode 40 = d D d D eth ETH eth\nkeycode 41 = f F f F dstroke ordfeminine dstroke\nke"
|
||||
b"ycode 42 = g G g G eng ENG eng\nkeycode 43 = h H h H hstroke Hstroke hstroke\nkeycode 44 = j J j J dead_below"
|
||||
b"dot dead_abovedot dead_belowdot\nkeycode 45 = k K k K kra ampersand kra\nkeycode 46 = l L l L lstroke Lstroke "
|
||||
b"lstroke\nkeycode 47 = odiaeresis Odiaeresis odiaeresis Odiaeresis dead_doubleacute dead_belowdot dead_doubleacu"
|
||||
b"te\nkeycode 48 = adiaeresis Adiaeresis adiaeresis Adiaeresis dead_circumflex dead_caron dead_circumflex\nkeycod"
|
||||
b"e 49 = dead_circumflex degree dead_circumflex degree U2032 U2033 U2032\nkeycode 50 = Shift_L NoSymbol Shift_L"
|
||||
b"\nkeycode 51 = numbersign apostrophe numbersign apostrophe rightsinglequotemark dead_breve rightsinglequotemark"
|
||||
b"\nkeycode 52 = y Y y Y guillemotright U203A guillemotright\nkeycode 53 = x X x X guillemotleft U2039 guillemot"
|
||||
b"left\nkeycode 54 = c C c C cent copyright cent\nkeycode 55 = v V v V doublelowquotemark singlelowquotemark dou"
|
||||
b"blelowquotemark\nkeycode 56 = b B b B leftdoublequotemark leftsinglequotemark leftdoublequotemark\nkeycode 57 "
|
||||
b"= n N n N rightdoublequotemark rightsinglequotemark rightdoublequotemark\nkeycode 58 = m M m M mu masculine mu"
|
||||
b"\nkeycode 59 = comma semicolon comma semicolon periodcentered multiply periodcentered\nkeycode 60 = period col"
|
||||
b"on period colon U2026 division U2026\nkeycode 61 = minus underscore minus underscore endash emdash endash\nkeyc"
|
||||
b"ode 62 = Shift_R NoSymbol Shift_R\nkeycode 63 = KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP_Multiply KP"
|
||||
b"_Multiply XF86ClearGrab\nkeycode 64 = Alt_L Meta_L Alt_L Meta_L\nkeycode 65 = space NoSymbol space\nkeycode 6"
|
||||
b"6 = Caps_Lock NoSymbol Caps_Lock\nkeycode 67 = F1 F1 F1 F1 F1 F1 XF86Switch_VT_1\nkeycode 68 = F2 F2 F2 F2 F2 "
|
||||
b"F2 XF86Switch_VT_2\nkeycode 69 = F3 F3 F3 F3 F3 F3 XF86Switch_VT_3\nkeycode 70 = F4 F4 F4 F4 F4 F4 XF86Switch_"
|
||||
b"VT_4\nkeycode 71 = F5 F5 F5 F5 F5 F5 XF86Switch_VT_5\nkeycode 72 = F6 F6 F6 F6 F6 F6 XF86Switch_VT_6\nkeycode "
|
||||
b" 73 = F7 F7 F7 F7 F7 F7 XF86Switch_VT_7\nkeycode 74 = F8 F8 F8 F8 F8 F8 XF86Switch_VT_8\nkeycode 75 = F9 F9 F9"
|
||||
b" F9 F9 F9 XF86Switch_VT_9\nkeycode 76 = F10 F10 F10 F10 F10 F10 XF86Switch_VT_10\nkeycode 77 = Num_Lock NoSymb"
|
||||
b"ol Num_Lock\nkeycode 78 = Scroll_Lock NoSymbol Scroll_Lock\nkeycode 79 = KP_Home KP_7 KP_Home KP_7\nkeycode 8"
|
||||
b"0 = KP_Up KP_8 KP_Up KP_8\nkeycode 81 = KP_Prior KP_9 KP_Prior KP_9\nkeycode 82 = KP_Subtract KP_Subtract KP_S"
|
||||
b"ubtract KP_Subtract KP_Subtract KP_Subtract XF86Prev_VMode\nkeycode 83 = KP_Left KP_4 KP_Left KP_4\nkeycode 84"
|
||||
b" = KP_Begin KP_5 KP_Begin KP_5\nkeycode 85 = KP_Right KP_6 KP_Right KP_6\nkeycode 86 = KP_Add KP_Add KP_Add KP"
|
||||
b"_Add KP_Add KP_Add XF86Next_VMode\nkeycode 87 = KP_End KP_1 KP_End KP_1\nkeycode 88 = KP_Down KP_2 KP_Down KP_"
|
||||
b"2\nkeycode 89 = KP_Next KP_3 KP_Next KP_3\nkeycode 90 = KP_Insert KP_0 KP_Insert KP_0\nkeycode 91 = KP_Delete"
|
||||
b" KP_Separator KP_Delete KP_Separator\nkeycode 92 = ISO_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 93 =\nk"
|
||||
b"eycode 94 = less greater less greater bar dead_belowmacron bar\nkeycode 95 = F11 F11 F11 F11 F11 F11 XF86Switc"
|
||||
b"h_VT_11\nkeycode 96 = F12 F12 F12 F12 F12 F12 XF86Switch_VT_12\nkeycode 97 =\nkeycode 98 = Katakana NoSymbol "
|
||||
b"Katakana\nkeycode 99 = Hiragana NoSymbol Hiragana\nkeycode 100 = Henkan_Mode NoSymbol Henkan_Mode\nkeycode 101 "
|
||||
b"= Hiragana_Katakana NoSymbol Hiragana_Katakana\nkeycode 102 = Muhenkan NoSymbol Muhenkan\nkeycode 103 =\nkeycode"
|
||||
b" 104 = KP_Enter NoSymbol KP_Enter\nkeycode 105 = Control_R NoSymbol Control_R\nkeycode 106 = KP_Divide KP_Divide"
|
||||
b" KP_Divide KP_Divide KP_Divide KP_Divide XF86Ungrab\nkeycode 107 = Print Sys_Req Print Sys_Req\nkeycode 108 = IS"
|
||||
b"O_Level3_Shift NoSymbol ISO_Level3_Shift\nkeycode 109 = Linefeed NoSymbol Linefeed\nkeycode 110 = Home NoSymbol "
|
||||
b"Home\nkeycode 111 = Up NoSymbol Up\nkeycode 112 = Prior NoSymbol Prior\nkeycode 113 = Left NoSymbol Left\nkeycod"
|
||||
b"e 114 = Right NoSymbol Right\nkeycode 115 = End NoSymbol End\nkeycode 116 = Down NoSymbol Down\nkeycode 117 = Ne"
|
||||
b"xt NoSymbol Next\nkeycode 118 = Insert NoSymbol Insert\nkeycode 119 = Delete NoSymbol Delete\nkeycode 120 =\nkey"
|
||||
b"code 121 = XF86AudioMute NoSymbol XF86AudioMute\nkeycode 122 = XF86AudioLowerVolume NoSymbol XF86AudioLowerVolum"
|
||||
b"e\nkeycode 123 = XF86AudioRaiseVolume NoSymbol XF86AudioRaiseVolume\nkeycode 124 = XF86PowerOff NoSymbol XF86Pow"
|
||||
b"erOff\nkeycode 125 = KP_Equal NoSymbol KP_Equal\nkeycode 126 = plusminus NoSymbol plusminus\nkeycode 127 = Pause"
|
||||
b" Break Pause Break\nkeycode 128 = XF86LaunchA NoSymbol XF86LaunchA\nkeycode 129 = KP_Decimal KP_Decimal KP_Decim"
|
||||
b"al KP_Decimal\nkeycode 130 = Hangul NoSymbol Hangul\nkeycode 131 = Hangul_Hanja NoSymbol Hangul_Hanja\nkeycode 1"
|
||||
b"32 =\nkeycode 133 = Super_L NoSymbol Super_L\nkeycode 134 = Super_R NoSymbol Super_R\nkeycode 135 = Menu NoSymbo"
|
||||
b"l Menu\nkeycode 136 = Cancel NoSymbol Cancel\nkeycode 137 = Redo NoSymbol Redo\nkeycode 138 = SunProps NoSymbol "
|
||||
b"SunProps\nkeycode 139 = Undo NoSymbol Undo\nkeycode 140 = SunFront NoSymbol SunFront\nkeycode 141 = XF86Copy NoS"
|
||||
b"ymbol XF86Copy\nkeycode 142 = XF86Open NoSymbol XF86Open\nkeycode 143 = XF86Paste NoSymbol XF86Paste\nkeycode 14"
|
||||
b"4 = Find NoSymbol Find\nkeycode 145 = XF86Cut NoSymbol XF86Cut\nkeycode 146 = Help NoSymbol Help\nkeycode 147 = "
|
||||
b"XF86MenuKB NoSymbol XF86MenuKB\nkeycode 148 = XF86Calculator NoSymbol XF86Calculator\nkeycode 149 =\nkeycode 150"
|
||||
b" = XF86Sleep NoSymbol XF86Sleep\nkeycode 151 = XF86WakeUp NoSymbol XF86WakeUp\nkeycode 152 = XF86Explorer NoSymb"
|
||||
b"ol XF86Explorer\nkeycode 153 = XF86Send NoSymbol XF86Send\nkeycode 154 =\nkeycode 155 = XF86Xfer NoSymbol XF86Xf"
|
||||
b"er\nkeycode 156 = XF86Launch1 NoSymbol XF86Launch1\nkeycode 157 = XF86Launch2 NoSymbol XF86Launch2\nkeycode 158 "
|
||||
b"= XF86WWW NoSymbol XF86WWW\nkeycode 159 = XF86DOS NoSymbol XF86DOS\nkeycode 160 = XF86ScreenSaver NoSymbol XF86S"
|
||||
b"creenSaver\nkeycode 161 = XF86RotateWindows NoSymbol XF86RotateWindows\nkeycode 162 = XF86TaskPane NoSymbol XF86"
|
||||
b"TaskPane\nkeycode 163 = XF86Mail NoSymbol XF86Mail\nkeycode 164 = XF86Favorites NoSymbol XF86Favorites\nkeycode "
|
||||
b"165 = XF86MyComputer NoSymbol XF86MyComputer\nkeycode 166 = XF86Back NoSymbol XF86Back\nkeycode 167 = XF86Forwar"
|
||||
b"d NoSymbol XF86Forward\nkeycode 168 =\nkeycode 169 = XF86Eject NoSymbol XF86Eject\nkeycode 170 = XF86Eject XF86E"
|
||||
b"ject XF86Eject XF86Eject\nkeycode 171 = XF86AudioNext NoSymbol XF86AudioNext\nkeycode 172 = XF86AudioPlay XF86Au"
|
||||
b"dioPause XF86AudioPlay XF86AudioPause\nkeycode 173 = XF86AudioPrev NoSymbol XF86AudioPrev\nkeycode 174 = XF86Aud"
|
||||
b"ioStop XF86Eject XF86AudioStop XF86Eject\nkeycode 175 = XF86AudioRecord NoSymbol XF86AudioRecord\nkeycode 176 = "
|
||||
b"XF86AudioRewind NoSymbol XF86AudioRewind\nkeycode 177 = XF86Phone NoSymbol XF86Phone\nkeycode 178 =\nkeycode 179"
|
||||
b" = XF86Tools NoSymbol XF86Tools\nkeycode 180 = XF86HomePage NoSymbol XF86HomePage\nkeycode 181 = XF86Reload NoSy"
|
||||
b"mbol XF86Reload\nkeycode 182 = XF86Close NoSymbol XF86Close\nkeycode 183 =\nkeycode 184 =\nkeycode 185 = XF86Scr"
|
||||
b"ollUp NoSymbol XF86ScrollUp\nkeycode 186 = XF86ScrollDown NoSymbol XF86ScrollDown\nkeycode 187 = parenleft NoSym"
|
||||
b"bol parenleft\nkeycode 188 = parenright NoSymbol parenright\nkeycode 189 = XF86New NoSymbol XF86New\nkeycode 190"
|
||||
b" = Redo NoSymbol Redo\nkeycode 191 = XF86Tools NoSymbol XF86Tools\nkeycode 192 = XF86Launch5 NoSymbol XF86Launch"
|
||||
b"5\nkeycode 193 = XF86Launch6 NoSymbol XF86Launch6\nkeycode 194 = XF86Launch7 NoSymbol XF86Launch7\nkeycode 195 ="
|
||||
b" XF86Launch8 NoSymbol XF86Launch8\nkeycode 196 = XF86Launch9 NoSymbol XF86Launch9\nkeycode 197 =\nkeycode 198 = "
|
||||
b"XF86AudioMicMute NoSymbol XF86AudioMicMute\nkeycode 199 = XF86TouchpadToggle NoSymbol XF86TouchpadToggle\nkeycod"
|
||||
b"e 200 = XF86TouchpadOn NoSymbol XF86TouchpadOn\nkeycode 201 = XF86TouchpadOff NoSymbol XF86TouchpadOff\nkeycode "
|
||||
b"202 =\nkeycode 203 = Mode_switch NoSymbol Mode_switch\nkeycode 204 = NoSymbol Alt_L NoSymbol Alt_L\nkeycode 205 "
|
||||
b"= NoSymbol Meta_L NoSymbol Meta_L\nkeycode 206 = NoSymbol Super_L NoSymbol Super_L\nkeycode 207 = NoSymbol Hyper"
|
||||
b"_L NoSymbol Hyper_L\nkeycode 208 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 209 = XF86AudioPause NoSymbol X"
|
||||
b"F86AudioPause\nkeycode 210 = XF86Launch3 NoSymbol XF86Launch3\nkeycode 211 = XF86Launch4 NoSymbol XF86Launch4\nk"
|
||||
b"eycode 212 = XF86LaunchB NoSymbol XF86LaunchB\nkeycode 213 = XF86Suspend NoSymbol XF86Suspend\nkeycode 214 = XF8"
|
||||
b"6Close NoSymbol XF86Close\nkeycode 215 = XF86AudioPlay NoSymbol XF86AudioPlay\nkeycode 216 = XF86AudioForward No"
|
||||
b"Symbol XF86AudioForward\nkeycode 217 =\nkeycode 218 = Print NoSymbol Print\nkeycode 219 =\nkeycode 220 = XF86Web"
|
||||
b"Cam NoSymbol XF86WebCam\nkeycode 221 = XF86AudioPreset NoSymbol XF86AudioPreset\nkeycode 222 =\nkeycode 223 = XF"
|
||||
b"86Mail NoSymbol XF86Mail\nkeycode 224 = XF86Messenger NoSymbol XF86Messenger\nkeycode 225 = XF86Search NoSymbol "
|
||||
b"XF86Search\nkeycode 226 = XF86Go NoSymbol XF86Go\nkeycode 227 = XF86Finance NoSymbol XF86Finance\nkeycode 228 = "
|
||||
b"XF86Game NoSymbol XF86Game\nkeycode 229 = XF86Shop NoSymbol XF86Shop\nkeycode 230 =\nkeycode 231 = Cancel NoSymb"
|
||||
b"ol Cancel\nkeycode 232 = XF86MonBrightnessDown NoSymbol XF86MonBrightnessDown\nkeycode 233 = XF86MonBrightnessUp"
|
||||
b" NoSymbol XF86MonBrightnessUp\nkeycode 234 = XF86AudioMedia NoSymbol XF86AudioMedia\nkeycode 235 = XF86Display N"
|
||||
b"oSymbol XF86Display\nkeycode 236 = XF86KbdLightOnOff NoSymbol XF86KbdLightOnOff\nkeycode 237 = XF86KbdBrightness"
|
||||
b"Down NoSymbol XF86KbdBrightnessDown\nkeycode 238 = XF86KbdBrightnessUp NoSymbol XF86KbdBrightnessUp\nkeycode 239"
|
||||
b" = XF86Send NoSymbol XF86Send\nkeycode 240 = XF86Reply NoSymbol XF86Reply\nkeycode 241 = XF86MailForward NoSymbo"
|
||||
b"l XF86MailForward\nkeycode 242 = XF86Save NoSymbol XF86Save\nkeycode 243 = XF86Documents NoSymbol XF86Documents"
|
||||
b"\nkeycode 244 = XF86Battery NoSymbol XF86Battery\nkeycode 245 = XF86Bluetooth NoSymbol XF86Bluetooth\nkeycode 24"
|
||||
b"6 = XF86WLAN NoSymbol XF86WLAN\nkeycode 247 =\nkeycode 248 =\nkeycode 249 =\nkeycode 250 =\nkeycode 251 = XF86Mo"
|
||||
b"nBrightnessCycle NoSymbol XF86MonBrightnessCycle\nkeycode 252 =\nkeycode 253 =\nkeycode 254 = XF86WWAN NoSymbol "
|
||||
b"XF86WWAN\nkeycode 255 = XF86RFKill NoSymbol XF86RFKill\n"
|
||||
)
|
||||
Reference in New Issue
Block a user