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:
2026-06-18 00:47:12 +02:00
parent 1d790a78fb
commit 0ab77d2a64
257 changed files with 51859 additions and 0 deletions

0
tests/lib/__init__.py Normal file
View File

172
tests/lib/cleanup.py Normal file
View 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
View 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

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
)