This script creates a persistent virtual gamepad from a physical one, designed to be resilient to device reconnections. Its primary goal is to ensure that applications see a stable gamepad device even if the physical gamepad is unplugged and plugged back in. This makes it particularly useful for providing stable gamepad access to containerized applications, such as those running in Docker, without needing to restart containers on device changes.
The script continuously monitors for the specified gamepad device. When the device is detected, it creates a virtual gamepad device using evdev and UInput. It then forwards all events from the real gamepad to this virtual device. Symlinks are created in predictable locations (/tmp/gamepad-event and /tmp/gamepad-js by default) that point to the active event and joystick interfaces of this virtual gamepad. Applications within the Docker container can then be configured to use these symlinks, ensuring they always point to a valid device.
If the real gamepad disconnects, the script waits for it to reappear and then re-establishes the link, all without interrupting the virtual device that the containerized application is using (though no events will be forwarded while the real device is disconnected).
- Install
uvif you haven't already (see uv's installation guide). - Clone this repository:
git clone <repository_url> # TODO: Replace <repository_url> with the actual URL cd <repository_directory> # TODO: Replace <repository_directory> with the actual directory name
- Set up the Python environment and install dependencies:
This project includes a
uv venv # Creates a virtual environment. Uses .python-version (if present and Python available) or a compatible Python. uv pip install .[dev] # Installs runtime and dev dependencies into the project's virtual environment.
.python-versionfile (specifying Python 3.13) to guideuv. If Python 3.13 is not installed but managed byuv(e.g. viauv python install 3.13),uv venvwill use it. Otherwise, ensure a compatible Python version (>=3.7) is available.
The recommended way to run the script is using uv run, which automatically manages the Python environment:
uv run gamepad-mapper -- [OPTIONS]The script is registered as gamepad-mapper in pyproject.toml. For example, to specify a custom device link:
uv run gamepad-mapper -- --device-link /dev/input/by-id/your-device-id --event-path /tmp/custom-eventNote: The -- is important to separate options for uv run itself from options intended for gamepad-mapper.
Available options for gamepad-mapper:
--device-link: Path to the real gamepad device link.- Default:
/dev/input/by-id/usb-1038_SteelSeries_Stratus_Duo-event-joystick
- Default:
--event-path: Desired path for the event symlink.- Default:
/tmp/gamepad-event
- Default:
--js-path: Desired path for the joystick symlink.- Default:
/tmp/gamepad-js
- Default:
--virtual-name: Name for the virtual gamepad device.- Default:
VirtualGamepad
- Default:
- Inside the Docker container:
-
The
gamepad.pyscript creates symlinks on the host machine (e.g.,/tmp/gamepad-eventand/tmp/gamepad-jsby default). To make the gamepad accessible inside your container, you should use the--deviceflag withpodman run(or the equivalent for other container runtimes). This flag will map the actual gamepad device node (to which the symlink points) into your container.Example using default symlink paths:
podman run --rm -it \ --device=/tmp/gamepad-event \ --device=/tmp/gamepad-js \ your-container-imageIf you used custom paths for the symlinks when running
gamepad.py(e.g.,/opt/gamepad/eventand/opt/gamepad/js), you would adjust the--deviceflags accordingly:podman run --rm -it \ --device=/opt/gamepad/event \ --device=/opt/gamepad/js \ your-container-imageInside the container, your application can then access the gamepad directly at
/tmp/gamepad-event(or/opt/gamepad/event, etc.), as the--deviceflag typically makes the device available at the same path within the container as specified on the host. -
Configure your application to use the gamepad device via these paths within the container (e.g.,
/tmp/gamepad-event).
-
- Resilient to Reconnections: No need to restart your Docker container if the gamepad is reconnected.
- Stable Device Path: Provides stable symlinks to the gamepad events, even if the underlying
/dev/input/event*number changes. - Customizable: Device paths and names can be configured via command-line arguments.
The initial version of this script was developed with assistance from ChatGPT (model o3-mini-high). Subsequent refinements and feature development were contributed by Jules (Google).
- Detailed testing strategy and implementation.
A detailed testing strategy, including plans for unit and integration tests, is outlined in TESTING.md.
The tests for this project are written using pytest.
-
Install Dependencies: First, ensure you have installed the development dependencies. If you followed the installation steps above,
uv pip install .[dev]will have installedpytest. -
Execute Tests: To run the tests, navigate to the root of the project directory and execute:
uv run pytest tests/
Alternatively, if your shell is configured to use executables from the virtual environment (e.g., after manual activation, though not required for
uv run),pytest tests/would also work. -
Permissions for Integration Tests: The integration test
test_event_forwardingcreates virtual input devices usingevdev.UInputand thus requires write access to/dev/uinput.- If your user account does not have the necessary permissions, this test will be automatically skipped.
- To enable this test, ensure your user is in the appropriate group (commonly
inputoruinput) or thatudevrules grant access. Consult your system's documentation forudevconfiguration. For example, you might add your user to theinputgroup:sudo usermod -aG input $USER(requires logout/login to take effect), or create a udev rule. - Running tests with
sudoto bypass permission checks is discouraged for security reasons. It's better to configure user permissions correctly.