diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml
new file mode 100644
index 0000000..e879179
--- /dev/null
+++ b/.github/workflows/install.yaml
@@ -0,0 +1,29 @@
+name: Test install
+
+on:
+ - push
+ - pull_request
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ include:
+ - os: ubuntu-latest
+ python-version: "3.9"
+
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install python-evdev
+ run: |
+ python -m pip install -v .
+ (cd /tmp && python -c "import evdev.ecodes; print(evdev.ecodes)")
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..20d254b
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,27 @@
+name: Lint
+
+on:
+ - push
+ - pull_request
+
+jobs:
+ pylint:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ["3.14"]
+
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Check for pylint errors
+ run: |
+ python -m pip install pylint setuptools
+ python setup.py build
+ python -m pylint --verbose -E build/lib*/evdev
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..073d524
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,28 @@
+name: Test
+
+on:
+ - push
+ - pull_request
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ["3.14"]
+
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Run pytest tests
+ # sudo required to write to uinputs
+ run: |
+ sudo python -m pip install pytest setuptools
+ sudo python -m pip install -e .
+ sudo python -m pytest tests
diff --git a/.gitignore b/.gitignore
index 7623229..70ac303 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,17 +5,24 @@
develop-eggs/
dist/
build/
+wheelhouse/
dropin.cache
pip-log.txt
.installed.cfg
.coverage
tags
TAGS
-evdev/*.so
-evdev/ecodes.c
-doc/_build
+.#*
__pycache__
+.pytest_cache
+.ruff_cache
+.venv
+uv.lock
-evdev/_ecodes.py
-evdev/_input.py
-evdev/_uinput.py
+src/evdev/*.so
+src/evdev/ecodes.c
+src/evdev/ecodes.pyi
+docs/_build
+src/evdev/_ecodes.py
+src/evdev/_input.py
+src/evdev/_uinput.py
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..4a50dff
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,15 @@
+# https://docs.readthedocs.io/en/stable/config-file/v2.html
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.12"
+
+sphinx:
+ configuration: docs/conf.py
+
+python:
+ install:
+ - requirements: requirements-dev.txt
+ - path: .
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index ec85c1d..8482b07 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012-2013 Georgi Valkov. All rights reserved.
+Copyright (c) 2012-2025 Georgi Valkov. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -6,7 +6,7 @@ met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
-
+
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
@@ -16,13 +16,14 @@ met:
be used to endorse or promote products derived from this software
without specific prior written permission.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL GEORGI VALKOV BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
index fffc1f5..be2be3d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,5 @@
-# make github and sdist happy
-include README.rst
-include evdev/genecodes.py
-include LICENSE
+# The _ecodes extension module source file needs to be generated against the
+# evdev headers of the running kernel. Refer to the 'build_ecodes' distutils
+# command in setup.py.
+exclude src/evdev/ecodes.c
+include src/evdev/ecodes.py
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9746040
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# evdev
+
+
+
+
+
+
+
+This package provides bindings to the generic input event interface in Linux.
+The *evdev* interface serves the purpose of passing events generated in the
+kernel directly to userspace through character devices that are typically
+located in `/dev/input/`.
+
+This package also comes with bindings to *uinput*, the userspace input
+subsystem. *Uinput* allows userspace programs to create and handle input devices
+that can inject events directly into the input subsystem.
+
+***Documentation:***
+https://python-evdev.readthedocs.io/en/latest/
+
+***Development:***
+https://github.com/gvalkov/python-evdev
+
+***Package:***
+https://pypi.python.org/pypi/evdev
+
+***Changelog:***
+https://python-evdev.readthedocs.io/en/latest/changelog.html
\ No newline at end of file
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 46f8d67..0000000
--- a/README.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-*evdev*
--------
-
-This package provides bindings to the generic input event interface in
-Linux. The *evdev* interface serves the purpose of passing events
-generated in the kernel directly to userspace through character
-devices that are typically located in ``/dev/input/``.
-
-This package also comes with bindings to *uinput*, the userspace input
-subsystem. *Uinput* allows userspace programs to create and handle
-input devices that can inject events directly into the input
-subsystem.
-
-Documentation:
- http://python-evdev.readthedocs.org/en/latest/
-
-Development:
- https://github.com/gvalkov/python-evdev
-
-Package:
- http://pypi.python.org/pypi/evdev
-
-Changelog:
- http://python-evdev.readthedocs.org/en/latest/changelog.html
diff --git a/bin/evtest.py b/bin/evtest.py
deleted file mode 100755
index ae37673..0000000
--- a/bin/evtest.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-
-'''
-evdev example - input device event monitor
-'''
-
-
-from sys import argv, exit
-from select import select
-from evdev import ecodes, InputDevice, list_devices, AbsInfo
-
-
-usage = 'usage: evtest []'
-evfmt = 'time {:<16} type {} ({}), code {:<4} ({}), value {}'
-device_dir = '/dev/input/'
-query_type = None
-query_value = None
-
-
-def select_device():
- '''Select a device from the list of accessible input devices.'''
-
- devices = [InputDevice(i) for i in reversed(list_devices(device_dir))]
- if not devices:
- print('error: no input devices found (do you have rw permission on /dev/input/*?)')
- exit(1)
-
- dev_fmt = '{0:<3} {1.fn:<20} {1.name:<35} {1.phys}'
- dev_lns = [dev_fmt.format(n, d) for n, d in enumerate(devices)]
-
- print('ID {:<20} {:<35} {}'.format('Device', 'Name', 'Phys'))
- print('-' * len(max(dev_lns, key=len)))
- print('\n'.join(dev_lns))
- print('')
-
- choice = input('Select device [0-{}]:'.format(len(dev_lns)-1))
- return devices[int(choice)]
-
-
-def print_event(e):
- if e.type == ecodes.EV_SYN:
- if e.code == ecodes.SYN_MT_REPORT:
- print('time {:<16} +++++++++ {} ++++++++'.format(e.timestamp(), ecodes.SYN[e.code]))
- else:
- print('time {:<16} --------- {} --------'.format(e.timestamp(), ecodes.SYN[e.code]))
- else:
- if e.type in ecodes.bytype:
- codename = ecodes.bytype[e.type][e.code]
- else:
- codename = '?'
-
- print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value))
-
-
-if len(argv) == 1:
- device = select_device()
-
-elif len(argv) == 2:
- device = InputDevice(argv[1])
-
-elif len(argv) == 4:
- device = InputDevice(argv[1])
- query_type = argv[2]
- query_value = argv[3]
-else:
- print(usage)
- exit(1)
-
-capabs = device.capabilities(verbose=True)
-
-print('Device name: {.name}'.format(device))
-print('Device info: {.info}'.format(device))
-print('Repeat settings: {}'.format(device.repeat))
-
-if ('EV_LED', ecodes.EV_LED) in capabs:
- print('Active LEDs: {}\n'.format(','.join(i[0] for i in device.leds(True))))
-
-print('Device capabilities:')
-for type, codes in capabs.items():
- print(' Type {} {}:'.format(*type))
- for i in codes:
- # i <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272)
- if isinstance(i[1], AbsInfo):
- print(' Code {:<4} {}:'.format(*i[0]))
- print(' {}'.format(i[1]))
- else:
- # multiple names may resolve to one value
- s = ', '.join(i[0]) if isinstance(i[0], list) else i[0]
- print(' Code {:<4} {}'.format(s, i[1]))
- print('')
-
-
-print('Listening for events ...\n')
-while True:
- r, w, e = select([device], [], [])
-
- for ev in device.read():
- print_event(ev)
diff --git a/doc/apidoc.rst b/doc/apidoc.rst
deleted file mode 100644
index 48e4329..0000000
--- a/doc/apidoc.rst
+++ /dev/null
@@ -1,40 +0,0 @@
-API documentation
------------------
-
-``events``
-============
-
-.. automodule:: evdev.events
- :members: InputEvent, KeyEvent, AbsEvent, RelEvent, SynEvent, event_factory
- :undoc-members:
- :member-order: groupwise
-
-``device``
-============
-
-.. automodule:: evdev.device
- :members: InputDevice, DeviceInfo, AbsInfo, KbdInfo
- :undoc-members:
- :special-members:
- :member-order: groupwise
-
-``uinput``
-============
-
-.. autoclass:: evdev.uinput.UInput
- :members:
- :special-members:
- :member-order: groupwise
-
-``util``
-==========
-
-.. automodule:: evdev.util
- :members: list_devices, is_device, categorize, resolve_ecodes
- :member-order: groupwise
-
-``ecodes``
-============
-
-.. automodule:: evdev.ecodes
- :members:
diff --git a/doc/changelog.rst b/doc/changelog.rst
deleted file mode 100644
index 2b3b09f..0000000
--- a/doc/changelog.rst
+++ /dev/null
@@ -1,173 +0,0 @@
-Changelog
-=========
-
-0.4.4 (Jun 04, 2014)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - Calling ``InputDevice.read_one()`` should always return
- ``None``, when there is nothing to be read, even in case of a
- ``EAGAIN`` errno (thanks JPP).
-
-0.4.3 (Dec 19, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - Silence ``OSError`` in destructor (thanks `@polyphemus`_).
-
- - Make ``InputDevice.close()`` work in cases in which stdin (fd 0)
- has been closed (thanks `@polyphemus`_).
-
-0.4.2 (Dec 13, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Enhancements:
- - Rework documentation and docstrings.
-
-Fixes:
- - Call ``InputDevice.close()`` in ``InputDevice.__del__()``.
-
-0.4.1 (Jul 24, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - Fix reference counting in ``device_read``, ``device_read_many``
- and ``ioctl_capabilities``.
-
-0.4.0 (Jul 01, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Enhancements:
- - Add ``FF_*`` and ``FF_STATUS`` codes to ``ecodes`` (thanks `@bgilbert`_).
-
- - Reverse event code mappings (``ecodes.{KEY,FF,REL,ABS}`` and
- etc.) will now map to a list of codes, whenever a value
- corresponds to multiple codes::
-
- >>> ecodes.KEY[152]
- ... ['KEY_COFFEE', 'KEY_SCREENLOCK']
- >>> ecodes.KEY[30]
- ... 'KEY_A'
-
- - Set the state of a LED through ``device.set_led()`` (thanks
- `@accek`_). ``device.fd`` is opened in ``O_RDWR`` mode from now on.
-
-Fixes:
- - Fix segfault in ``device_read_many()`` (thanks `@bgilbert`_).
-
-0.3.3 (May 29, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - Raise ``IOError`` from ``device_read()`` and ``device_read_many()`` when
- ``read()`` fails.
-
- - Several stability and style changes (thank you debian code reviewers).
-
-0.3.2 (Apr 05, 2013)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - Fix vendor id and product id order in ``DeviceInfo`` (thanks `@kived`_).
-
-0.3.1 (Nov 23, 2012)
-^^^^^^^^^^^^^^^^^^^^
-
-Fixes:
- - ``device.read()`` will return an empty tuple if the device has
- nothing to offer (instead of segfaulting).
-
- - Exclude unnecessary package data in sdist and bdist.
-
-0.3.0 (Nov 06, 2012)
-^^^^^^^^^^^^^^^^^^^^
-
-Enhancements:
- - Add ability to set/get auto-repeat settings with ``EVIOC{SG}REP``.
-
- - Add ``device.version`` - the value of ``EVIOCGVERSION``.
-
- - Add ``device.read_loop()``.
-
- - Add ``device.grab()`` and ``device.ungrab()`` - exposes ``EVIOCGRAB``.
-
- - Add ``device.leds`` - exposes ``EVIOCGLED``.
-
- - Replace ``DeviceInfo`` class with a namedtuple.
-
-Fixes:
- - ``device.read_one()`` was dropping events.
-
- - Rename ``AbsData`` to ``AbsInfo`` (as in ``struct input_absinfo``).
-
-
-0.2.0 (Aug 22, 2012)
-^^^^^^^^^^^^^^^^^^^^
-
-Enhancements:
- - Add the ability to set arbitrary device capabilities on uinput
- devices (defaults to all ``EV_KEY`` ecodes).
-
- - Add ``UInput.device`` which is an open ``InputDevice`` to the
- input device that uinput 'spawns'.
-
- - Add ``UInput.capabilities()`` which is just a shortcut to
- ``UInput.device.capabilities()``.
-
- - Rename ``UInput.write()`` to ``UInput.write_event()``.
-
- - Add a simpler ``UInput.write(type, code, value)`` method.
-
- - Make all ``UInput`` constructor arguments optional (default
- device name is now ``py-evdev-uinput``).
-
- - Add the ability to set ``absmin``, ``absmax``, ``absfuzz`` and
- ``absflat`` when specifying the uinput device's capabilities.
-
- - Remove the ``nophys`` argument - if a device fails the
- ``EVIOCGPHYS`` ioctl, phys will equal the empty string.
-
- - Make ``InputDevice.capabilities()`` perform a ``EVIOCGABS`` ioctl
- for devices that support ``EV_ABS`` and return that info wrapped in
- an ``AbsData`` namedtuple.
-
- - Split ``ioctl_devinfo`` into ``ioctl_devinfo`` and
- ``ioctl_capabilities``.
-
- - Split ``uinput_open()`` to ``uinput_open()`` and ``uinput_create()``
-
- - Add more uinput usage examples and documentation.
-
- - Rewrite uinput tests.
-
- - Remove ``mouserel`` and ``mouseabs`` from ``UInput``.
-
- - Tie the sphinx version and release to the distutils version.
-
- - Set 'methods-before-attributes' sorting in the docs.
-
-
-Fixes:
- - Remove ``KEY_CNT`` and ``KEY_MAX`` from ``ecodes.keys``.
-
-
-0.1.1 (May 18, 2012)
-^^^^^^^^^^^^^^^^^^^^
-
-Enhancements:
- - Add ``events.keys``, which is a combination of all ``BTN_`` and
- ``KEY_`` event codes.
-
-Fixes:
- - ``ecodes.c`` was not generated when installing through ``pip``.
-
-
-0.1.0 (May 17, 2012)
-^^^^^^^^^^^^^^^^^^^^
-
-*Initial Release*
-
-.. _`@polyphemus`: https://github.com/polyphemus
-.. _`@bgilbert`: https://github.com/bgilbert
-.. _`@accek`: https://github.com/accek
-.. _`@kived`: https://github.com/kived
diff --git a/doc/index.rst b/doc/index.rst
deleted file mode 100644
index 1c53e02..0000000
--- a/doc/index.rst
+++ /dev/null
@@ -1,71 +0,0 @@
-*evdev* documentation
----------------------
-
-This package provides bindings to the generic input event interface in
-Linux. The *evdev* interface serves the purpose of passing events
-generated in the kernel directly to userspace through character
-devices that are typically located in ``/dev/input/``.
-
-This package also comes with bindings to *uinput*, the userspace input
-subsystem. *Uinput* allows userspace programs to create and handle
-input devices that can inject events directly into the input
-subsystem.
-
-Please refer to the :doc:`tutorial ` and the :doc:`apidoc
-` for usage information.
-
-Contents
-========
-
-.. toctree::
- :maxdepth: 1
-
- tutorial
- install
- apidoc
- changelog
-
-
-Similar Projects
-================
-
-* `python-uinput`_
-* `ruby-evdev`_
-* `evdev`_ (ctypes)
-
-
-License
-=======
-
-Package :mod:`evdev` is released under the terms of the `Revised BSD License`_.
-
-
-Todo
-====
-
-* Use libudev to find the uinput device node as well as the other input
- devices. Their locations are currently assumed to be ``/dev/uinput`` and
- ``/dev/input/*``.
-
-* More tests.
-
-* Better uinput support (setting device capabilities as in `python-uinput`_)
-
-* Expose more input subsystem functionality (``EVIOCSKEYCODE``, ``EVIOCGREP`` etc)
-
-* Figure out if using ``linux/input.h`` and other kernel headers in your
- userspace program binds it to the GPL2.
-
-
-Indices and Tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
-
-.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-evdev/master/LICENSE
-.. _python-uinput: https://github.com/tuomasjjrasanen/python-uinput
-.. _ruby-evdev: http://technofetish.net/repos/buffaloplay/ruby_evdev/doc/
-.. _evdev: http://svn.navi.cx/misc/trunk/python/evdev/
diff --git a/doc/install.rst b/doc/install.rst
deleted file mode 100644
index 1b386ab..0000000
--- a/doc/install.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-Installation
-============
-
-Before installing :mod:`evdev`, make sure that the Python and Linux
-kernel headers are installed on your system.
-
-On a Debian compatible OS:
-
-.. code-block:: bash
-
- $ apt-get install python-dev
- $ apt-get install linux-headers-$(uname -r)
-
-On a Redhat compatible OS:
-
-.. code-block:: bash
-
- $ yum install python-devel
- $ yum install kernel-headers-$(uname -r)
-
-The latest stable version can be installed from pypi_, while the
-development version can be installed from github_:
-
-.. code-block:: bash
-
- $ pip install evdev # latest stable version
- $ pip install git+git://github.com/gvalkov/python-evdev.git # latest development version
-
-:mod:`evdev` can also be installed like any other :mod:`setuptools`
-package.
-
-.. code-block:: bash
-
- $ git clone github.com/gvalkov/python-evdev.git
- $ cd python-evdev
- $ git checkout $versiontag
- $ python setup.py install
-
-The :mod:`evdev` package works with CPython **>= 2.7**.
-
-.. _pypi: http://pypi.python.org/pypi/evdev
-.. _github: https://github.com/gvalkov/python-evdev
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
deleted file mode 100644
index 1c0349a..0000000
--- a/doc/tutorial.rst
+++ /dev/null
@@ -1,250 +0,0 @@
-Tutorial
---------
-
-Listing accessible event devices
-================================
-
-::
-
- >>> from evdev import InputDevice, list_devices
-
- >>> devices = map(InputDevice, list_devices())
-
- >>> for dev in devices:
- ... print( '%-20s %-32s %s' % (dev.fn, dev.name, dev.phys) )
- /dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0
- /dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0
-
-
-Listing device capabilities
-===========================
-
-::
-
- >>> dev = InputDevice('/dev/input/event0')
-
- >>> print(dev)
- device /dev/input/event0, name "Dell USB Optical Mouse", phys "usb-0000:00:12.0-2/input0"
-
- >>> dev.capabilities()
- ... { 0: [0, 1, 2], 1: [272, 273, 274, 275], 2: [0, 1, 6, 8], 4: [4] }
-
- >>> dev.capabilities(verbose=True)
- ... { ('EV_SYN', 0): [('SYN_REPORT', 0), ('SYN_CONFIG', 1), ('SYN_MT_REPORT', 2)],
- ... ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274), ('BTN_SIDE', 275)], ...
-
-
-Listing device capabilities for devices with absolute axes
-==========================================================
-
-::
-
- >>> dev = InputDevice('/dev/input/event7')
-
- >>> print(dev)
- device /dev/input/event7, name "Wacom Bamboo 2FG 4x5 Finger", phys ""
-
- >>> dev.capabilities()
- ... { 1: [272, 273, 277, 278, 325, 330, 333] ,
- ... 3: [(0, AbsInfo(min=0, max=15360, fuzz=128, flat=0)),
- ... (1, AbsInfo(min=0, max=10240, fuzz=128, flat=0))] }
-
- >>> dev.capabilities(verbose=True)
- ... { ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ...],
- ... ('EV_ABS', 3): [(('ABS_X', 0), AbsInfo(min=0, max=15360, fuzz=128, flat=0)),
- ... (('ABS_Y', 1), AbsInfo(min=0, max=10240, fuzz=128, flat=0)),] }
-
- >>> dev.capabilities(absinfo=False)
- ... { 1: [272, 273, 277, 278, 325, 330, 333],
- ... 3: [0, 1, 47, 53, 54, 57] }
-
-
-Getting and setting LED states
-==============================
-
-::
-
- >>> dev.leds(verbose=True)
- ... [('LED_NUML', 0), ('LED_CAPSL', 1)]
-
- >>> dev.leds()
- ... [0, 1]
-
- >>> dev.set_led(ecodes.LED_NUML, 1) # enable numlock
- >>> dev.set_led(ecodes.LED_NUML, 0) # disable numlock
-
-
-Accessing input subsystem constants
-===================================
-
-::
-
- >>> from evdev import ecodes
- >>> ecodes.KEY_A, ecodes.ecodes['KEY_A']
- ... (30, 30)
- >>> ecodes.KEY[30]
- ... 'KEY_A'
- >>> ecodes.bytype[ecodes.EV_KEY][30]
- ... 'KEY_A'
- # a single value in the reverse mappings may correspond to multiple codes
- >>> ecodes.KEY[152]
- ... ['KEY_COFFEE', 'KEY_SCREENLOCK']
-
-
-Reading events
-==============
-
-::
-
- >>> from evdev import InputDevice, categorize, ecodes
- >>> dev = InputDevice('/dev/input/event1')
-
- >>> print(dev)
- device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0"
-
- >>> for event in dev.read_loop():
- ... if event.type == ecodes.EV_KEY:
- ... print(categorize(event))
- ... # pressing 'a' and holding 'space'
- key event at 1337016188.396030, 30 (KEY_A), down
- key event at 1337016188.492033, 30 (KEY_A), up
- key event at 1337016189.772129, 57 (KEY_SPACE), down
- key event at 1337016190.275396, 57 (KEY_SPACE), hold
- key event at 1337016190.284160, 57 (KEY_SPACE), up
-
-
-Reading events from multiple devices
-====================================
-
-::
-
- >>> from evdev import InputDevice
- >>> from select import select
-
- >>> devices = map(InputDevice, ('/dev/input/event1', '/dev/input/event2'))
- >>> devices = {dev.fd : dev for dev in devices}
-
- >>> for dev in devices.values(): print(dev)
- device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0"
- device /dev/input/event2, name "Logitech USB Laser Mouse", phys "usb-0000:00:12.0-2/input0"
-
- >>> while True:
- ... r,w,x = select(devices, [], [])
- ... for fd in r:
- ... for event in devices[fd].read():
- ... print(event)
- event at 1351116708.002230, code 01, type 02, val 01
- event at 1351116708.002234, code 00, type 00, val 00
- event at 1351116708.782231, code 04, type 04, val 458782
- event at 1351116708.782237, code 02, type 01, val 01
-
-
-Reading events with asyncore
-============================
-
-::
-
- >>> from asyncore import file_dispatcher, loop
- >>> from evdev import InputDevice, categorize, ecodes
- >>> dev = InputDevice('/dev/input/event1')
-
- >>> class InputDeviceDispatcher(file_dispatcher):
- ... def __init__(self, device):
- ... self.device = device
- ... file_dispatcher.__init__(self, device)
- ...
- ... def recv(self, ign=None):
- ... return self.device.read()
- ...
- ... def handle_read(self):
- ... for event in self.recv():
- ... print(repr(event))
-
- >>> InputDeviceDispatcher(dev)
- >>> loop()
- InputEvent(1337255905L, 358854L, 1, 30, 0L)
- InputEvent(1337255905L, 358857L, 0, 0, 0L)
-
-
-Getting exclusive access to a device
-====================================
-
-::
-
- >>> dev.grab() # become the sole recipient of all incoming input events
- >>> dev.ungrab()
-
-
-Associating classes with event types
-====================================
-
-::
-
- >>> from evdev import categorize, event_factory, ecodes
-
- >>> class SynEvent(object):
- ... def __init__(self, event):
- ... ...
-
- >>> event_factory[ecodes.EV_SYN] = SynEvent
-
-See :mod:`events ` for more information.
-
-
-Injecting events
-================
-
-::
-
- >>> from evdev import UInput, ecodes as e
-
- >>> ui = UInput()
-
- >>> # accepts only KEY_* events by default
- >>> ui.write(e.EV_KEY, e.KEY_A, 1) # KEY_A down
- >>> ui.write(e.EV_KEY, e.KEY_A, 0) # KEY_A up
- >>> ui.syn()
-
- >>> ui.close()
-
-
-Injecting events (2)
-====================
-
-::
-
- >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1)
- >>> with UInput() as ui:
- ... ui.write_event(ev)
- ... ui.syn()
-
-
-Specifying ``uinput`` device options
-======================================
-::
-
- >>> from evdev import UInput, AbsInfo, ecodes as e
-
- >>> cap = {
- ... e.EV_KEY : [e.KEY_A, e.KEY_B],
- ... e.EV_ABS : [
- ... (e.ABS_X, AbsInfo(min=0, max=255, fuzz=0, flat=0)),
- ... (e.ABS_Y, AbsInfo(0, 255, 0, 0)),
- ... (e.ABS_MT_POSITION_X, (0, 255, 128, 0)) ]
- ... }
-
- >>> ui = UInput(cap, name='example-device', version=0x3)
- >>> print(ui)
- name "example-device", bus "BUS_USB", vendor "0001", product "0001", version "0003"
- event types: EV_KEY EV_ABS EV_SYN
-
- >>> print(ui.capabilities())
- ... { 0: [0, 1, 3], 1: [30, 48],
- ... 3: [(0, AbsInfo(min=0, max=255, fuzz=0, flat=0)),
- ... (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)),
- ... (53, AbsInfo(min=0, max=255, fuzz=128, flat=0))] }
-
- >>> # move mouse cursor
- >>> ui.write(e.EV_ABS, e.ABS_X, 20)
- >>> ui.write(e.EV_ABS, e.ABS_Y, 20)
- >>> ui.syn()
diff --git a/doc/Makefile b/docs/Makefile
similarity index 95%
rename from doc/Makefile
rename to docs/Makefile
index 3af3204..68b86e6 100644
--- a/doc/Makefile
+++ b/docs/Makefile
@@ -41,10 +41,12 @@ help:
clean:
-rm -rf $(BUILDDIR)/*
+
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+ @sed -i $(BUILDDIR)/html/index.html -e 's,Synopsis,Python Bindings,'
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@@ -152,6 +154,13 @@ doctest:
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
+
+# .PHONY: news.rst
+# news.rst: changelog.rst
+# cat $< | grep -FB1 '^^^^^^^^^' \
+# | sed -e '/^\^/d;/--/d' -e 's,\(.*\)(\(.*\)),* **\2**: Version \1released.\n,' \
+# > $@
+
$(BUILDDIR)/html/evdev-doc.zip: html
cd $(BUILDDIR)/html/ && zip -x \*.zip -r evdev-doc.zip .
@echo `readlink -f $@`
diff --git a/doc/_static/.keep b/docs/_static/.keep
similarity index 100%
rename from doc/_static/.keep
rename to docs/_static/.keep
diff --git a/doc/_templates/.keep b/docs/_templates/.keep
similarity index 100%
rename from doc/_templates/.keep
rename to docs/_templates/.keep
diff --git a/docs/apidoc.rst b/docs/apidoc.rst
new file mode 100644
index 0000000..ba59e90
--- /dev/null
+++ b/docs/apidoc.rst
@@ -0,0 +1,75 @@
+API Reference
+-------------
+
+``events``
+============
+
+.. automodule:: evdev.events
+ :members: InputEvent, KeyEvent, AbsEvent, RelEvent, SynEvent, event_factory
+ :undoc-members:
+ :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__
+ :member-order: bysource
+
+
+``eventio``
+============
+
+.. automodule:: evdev.eventio
+ :members: EventIO
+ :undoc-members:
+ :special-members:
+ :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__
+ :member-order: bysource
+
+``eventio_async``
+=================
+
+.. automodule:: evdev.eventio_async
+ :members: EventIO
+ :undoc-members:
+ :special-members:
+ :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__
+ :member-order: bysource
+
+``device``
+============
+
+.. automodule:: evdev.device
+ :members: InputDevice, DeviceInfo, AbsInfo, KbdInfo
+ :undoc-members:
+ :special-members:
+ :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__
+ :member-order: bysource
+
+``uinput``
+============
+
+.. automodule:: evdev.uinput
+ :members: UInput
+ :special-members:
+ :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__
+ :member-order: bysource
+
+``util``
+==========
+
+.. automodule:: evdev.util
+ :members: list_devices, is_device, categorize, resolve_ecodes, resolve_ecodes_dict
+ :member-order: bysource
+
+``ecodes``
+============
+
+.. automodule:: evdev.ecodes
+ :members:
+ :exclude-members: __module__, keys, ecodes, bytype
+ :member-order: bysource
+
+.. autodata:: evdev.ecodes.keys
+ :annotation: {0: 'KEY_RESERVED', 1: 'KEY_ESC', 2: 'KEY_1', ...}
+
+.. autodata:: evdev.ecodes.ecodes
+ :annotation: {'KEY_END': 107, 'FF_RUMBLE': 80, 'KEY_KPDOT': 83, 'KEY_CNT': 768, ...}'
+
+.. autodata:: evdev.ecodes.bytype
+ :annotation: {0: {0: 'SYN_REPORT', 1: 'SYN_CONFIG', 2: 'SYN_MT_REPORT', 3: 'SYN_DROPPED'}, ...}
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..bcf1636
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,530 @@
+Changelog
+---------
+
+1.9.3 (Feb 05, 2025)
+====================
+
+- Fix several memory leaks in ``input.c``.
+
+- Raise the minimum supported Python version to 3.9 and the setuptools version to 77.0.
+
+
+1.9.2 (May 01, 2025)
+====================
+
+- Add the "--reproducible" build option which removes the build date and used headers from the
+ generated ``ecodes.c``. Example usage::
+
+ python -m build --config-setting=--build-option='build_ecodes --reproducible' -n
+
+- Use ``Generic`` to set precise type for ``InputDevice.path``.
+
+
+1.9.1 (Feb 22, 2025)
+====================
+
+- Fix fox missing ``UI_FF`` constants in generated ``ecodes.py``.
+
+- More type annotations.
+
+
+1.9.0 (Feb 08, 2025)
+====================
+
+- Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build.
+
+- Slightly faster reading of events in ``device.read()`` and ``device.read_one()``.
+
+- Fix FreeBSD support.
+
+- Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead).
+
+- Improve type hint coverage and add a ``py.typed`` file to the sdist.
+
+
+1.8.0 (Jan 25, 2025)
+====================
+
+- Binary wheels are now provided by the `evdev-binary `_ package.
+ The package is compiled on manylinux_2_28 against kernel 4.18.
+
+- The ``evdev.ecodes`` module is now generated at install time and contains only constants. This allows type
+ checking and introspection of the ``evdev.ecodes`` module, without having to execute it first. The old
+ module is available as ``evdev.ecodes_runtime``. In case generation of the static ``ecodes.py`` fails, the
+ install process falls back to using ``ecodes_runtime.py`` as ``ecodes.py``.
+
+- Reverse mappings in ``evdev.ecodes`` that point to more than one value are now tuples instead of lists. For example::
+
+ >>> ecodes.KEY[153]
+ ('KEY_DIRECTION', 'KEY_ROTATE_DISPLAY')
+
+- Raise the minimum supported Python version to 3.8.
+
+- Fix keyboard delay and repeat being swapped (#227).
+
+- Move the ``syn()`` convenience method from ``InputDevice`` to ``EventIO`` (#224).
+
+
+1.7.1 (May 8, 2024)
+====================
+
+- Provide fallback value for ``FF_MAX_EFFECTS``, which fixes the build on EL 7 (#219).
+
+- Add ``#ifdef`` guards around ``UI_GET_SYSNAME`` to improve kernel compatibility (#218) .
+
+- Wait up to two seconds for uinput devices to appear. (#215)
+
+
+1.7.0 (Feb 18, 2024)
+====================
+
+- Respect the ``CPATH/C_INCLUDE_PATH`` environment variables during install.
+
+- Add the uniq address to the string representation of ``InputDevice``.
+
+- Improved method for finding the device node corresponding to a uinput device (`#206 `_).
+
+- Repository TLC (reformatted with ruff, fixed linting warnings, moved packaging metadata to ``pyproject.toml`` etc.).
+
+
+1.6.1 (Jan 20, 2023)
+====================
+
+- Fix generation of ``ecodes.c`` when the path to ``sys.executable`` contains spaces.
+
+
+1.6.0 (Jul 17, 2022)
+====================
+
+- Fix Python 3.11 compatibility (`#174 `_).
+
+
+1.5.0 (Mar 24, 2022)
+====================
+
+- Fix documentation (`#163 `_, `#160 `_).
+
+- Re-enable TTY echo at evtest exit (`#155 `_).
+
+- Fix ``ImportError: sys.meta_path is None, Python is likely shutting down`` (`#154 `_).
+
+- Closing the input device file descriptor in ``InputDevice.close()`` now
+ happens in the main thread, instead of in a new thread (reverts `#146
+ `_).
+
+- Fix ``util.find_ecodes_by_regex`` not working across all supported Python versions (`#152 `_).
+
+
+
+1.4.0 (Jan 16, 2021)
+====================
+
+- Fix ``InputDevice.set_absinfo`` to allow setting parameters to zero.
+
+- Fix off-by-one in ``ioctl_EVIOCG_bits``, which causes value at the end of the
+ list to not be reported back (`#131 `_).
+
+- Fix ``set_absinfo`` to allow setting parameters to zero (`#128 `_).
+
+- Fix leak when returning ``BlockingIOError`` from a read (`#143 `_).
+
+- Fix "There is no current event loop in thread" error for non asyncio code
+ (`#146 `_).
+
+- Prevent ``InputDevice`` destructor from blocking (`#145 `_).
+
+- Add missing return codes to ``os.strerror()`` calls and fix force feedback
+ example in docs (`#138 `_).
+
+- Add the ``util.find_ecodes_by_regex()`` helper function.
+
+
+
+1.3.0 (Jan 12, 2020)
+====================
+
+- Fix build on 32bit arches with 64bit time_t
+
+- Add functionality to query device properties. See ``InputDevice.input_props``
+ and the ``input_props`` argument to ``Uinput``.
+
+- ``KeyEvent`` received an ``allow_unknown`` constructor argument, which
+ determines what will happen when an event code cannot be mapped to a keycode.
+ The default and behavior so far has been to raise ``KeyError``. If set to
+ ``True``, the keycode will be set to the event code formatted as a hex number.
+
+- Add ``InputDevice.set_absinfo()`` and ``InputDevice.absinfo()``.
+
+- Instruct the asyncio event loop to stop monitoring the fd of the input device
+ when the device is closed.
+
+
+1.2.0 (Apr 7, 2019)
+====================
+
+- Add UInput support for the resolution parameter in AbsInfo. This brings
+ support for the new method of uinput device setup, which was `introduced in
+ Linux 4.5`_ (thanks to `@LinusCDE`_).
+
+- Vendor and product identifiers can be greater or equal to `0x8000` (thanks
+ `@ivaradi`_).
+
+
+1.1.2 (Sep 1, 2018)
+====================
+
+- Fix installation on kernels <= 4.4.
+
+- Fix uinput creation ignoring absinfo settings.
+
+
+1.1.0 (Aug 27, 2018)
+====================
+
+- Add support for handling force-feedback effect uploads (many thanks to `@ndreys`_).
+
+- Fix typo preventing ff effects that need left coefficients from working.
+
+
+1.0.0 (Jun 02, 2018)
+====================
+
+- Prevent ``Uinput`` device creation raising ``Objects/longobject.c:415: bad
+ argument to internal function`` when a non-complete ``AbsInfo`` structure is
+ passed. All missing ``AbsInfo`` fields are set to 0.
+
+- Fix ``Uinput`` device creation raising ``KeyError`` when a capability filtered
+ by default is not present.
+
+- The ``InputDevice.fn`` attribute was deprecated in favor of ``InputDevice.path``.
+ Using the former will show a ``DeprecationWarning``, but would otherwise continue
+ working as before.
+
+- Fix ``InputDevice`` comparison raising ``AttributeError`` due to a non-existant
+ ``path`` attribute.
+
+- Fix asyncio support in Python 3.5+.
+
+- Uploading FF effect now works both on Python 2.7 and Python 3+.
+
+- Remove the ``asyncore`` example from the tutorial.
+
+
+0.8.1 (Mar 24, 2018)
+====================
+
+- Fix Python 2 compatibility issue in with ``Uinput.from_device``.
+
+- Fix minor `evdev.evtest` formatting issue.
+
+
+0.8.0 (Mar 22, 2018)
+====================
+
+- Fix ``InputDevice`` comparison on Python 2.
+
+- The device path is now considered when comparing two devices.
+
+- Fix ``UInput.from_device`` not correctly merging the capabilities of
+ selected devices.
+
+- The list of excluded event types in ``UInput.from_device`` is now
+ configurable. For example::
+
+ UInput.from_device(dev, filtered_types=(EV_SYN, EV_FF))
+
+ In addition, ``ecodes.EV_FF`` is now excluded by default.
+
+- Add a context manager for grabbing access to a device -
+ ``InputDevice.grab_context``. For example::
+
+ with dev.grab_context():
+ pass
+
+- Add the ``InputDevice.uniq`` attribute, which contains the unique identifier
+ of the device. As with ``phys``, this attribute may be empty (i.e. `''`).
+
+
+0.7.0 (Jun 16, 2017)
+====================
+
+- ``InputDevice`` now accepts objects that support the path protocol.
+ For example::
+
+ pth = pathlib.Path('/dev/input/event0')
+ dev = evdev.InputDevice(pth)
+
+- Support path protocol in ``InputDevice``. This means that ``InputDevice``
+ instances can be passed to callers that expect a ``os.PathLike`` object.
+
+- Exceptions raised during ``InputDevice.async_read()`` (and similar) are now
+ handled properly (i.e. an exception is set on the returned future instead of
+ leaking that exception into the event loop) (Fixes `#67`_).
+
+
+0.6.4 (Oct 07, 2016)
+====================
+
+- Exclude ``ecodes.c`` from source distribution (Fixes `#63`_).
+
+
+0.6.3 (Oct 06, 2016)
+====================
+
+- Add the ``UInput.from_device`` class method, which allows uinput device to be
+ created with the capabiltiies of one or more existing input devices::
+
+ ui = UInput.from_device('/dev/input1', '/dev/input2', **constructor_kwargs)
+
+- Add the ``build_ecodes`` distutils command, which generates the ``ecodes.c``
+ extension module. The new way of overwriting the evdev header locations is::
+
+ python setup.py build \
+ build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \
+ build_ext --include-dirs path/ \
+ install
+
+ The ``build*`` and ``install`` commands no longer have to be part of the same
+ command-line (i.e. running ``install`` will reuse the outputs of the last
+ ``build``).
+
+
+0.6.1 (Jun 04, 2016)
+====================
+
+- Disable tty echoing while evtest is running.
+- Allow evtest to listen to more than one devices.
+
+- The setup.py script now allows the location of the input header files to be
+ overwritten. For example::
+
+ python setup.py build_ext \
+ --evdev-headers path/input.h:path/input-event-codes.h \
+ --include-dirs path/ \
+ install
+
+
+0.6.0 (Feb 14, 2016)
+====================
+
+- Asyncio and async/await support (many thanks to `@paulo-raca`_).
+- Add the ability to set the `phys` property of uinput devices (thanks `@paulo-raca`_).
+- Add a generic :func:`InputDevice.set` method (thanks `@paulo-raca`_).
+- Distribute the evtest script along with evdev.
+- Fix issue with generating :mod:`ecodes.c` in recent kernels (``>= 4.4.0``).
+- Fix absinfo item indexes in :func:`UInput.uinput_create()` (thanks `@forsenonlhaimaisentito`_).
+- More robust comparison of :class:`InputDevice` objects (thanks `@isia`_).
+
+
+0.5.0 (Jun 16, 2015)
+====================
+
+- Write access to the input device is no longer mandatory. Evdev will
+ first try to open the device for reading and writing and fallback to
+ read-only. Methods that require write access (e.g. :func:`set_led()`)
+ will raise :class:`EvdevError` if the device is open only for reading.
+
+
+0.4.7 (Oct 07, 2014)
+====================
+
+- Fallback to distutils if setuptools is not available.
+
+
+0.4.6 (Oct 07, 2014)
+====================
+
+- Rework documentation and docstrings once more.
+
+- Fix install on Python 3.4 (works around issue21121_).
+
+- Fix :func:`ioctl()` requested buffer size (thanks Jakub Wojciech Klama).
+
+
+0.4.5 (Jul 06, 2014)
+====================
+
+- Add method for returning a list of the currently active keys -
+ :func:`InputDevice.active_keys()` (thanks `@spasche`_).
+
+- Fix a potential buffer overflow in :func:`ioctl_capabilities()` (thanks `@spasche`_).
+
+
+0.4.4 (Jun 04, 2014)
+====================
+
+- Calling :func:`InputDevice.read_one()` should always return ``None``,
+ when there is nothing to be read, even in case of a ``EAGAIN`` errno
+ (thanks JPP).
+
+
+0.4.3 (Dec 19, 2013)
+====================
+
+- Silence :class:`OSError` in destructor (thanks `@polyphemus`_).
+
+- Make :func:`InputDevice.close()` work in cases in which stdin (fd 0)
+ has been closed (thanks `@polyphemus`_).
+
+
+0.4.2 (Dec 13, 2013)
+====================
+
+- Rework documentation and docstrings.
+
+- Call :func:`InputDevice.close()` from :func:`InputDevice.__del__()`.
+
+
+0.4.1 (Jul 24, 2013)
+====================
+
+- Fix reference counting in :func:`InputDevice.device_read()`,
+ :func:`InputDevice.device_read_many()` and :func:`ioctl_capabilities`.
+
+
+0.4.0 (Jul 01, 2013)
+====================
+
+- Add ``FF_*`` and ``FF_STATUS`` codes to :func:`ecodes` (thanks `@bgilbert`_).
+
+- Reverse event code mappings (``ecodes.{KEY,FF,REL,ABS}`` and etc.)
+ will now map to a list of codes, whenever a value corresponds to
+ multiple codes::
+
+ >>> ecodes.KEY[152]
+ ... ['KEY_COFFEE', 'KEY_SCREENLOCK']
+ >>> ecodes.KEY[30]
+ ... 'KEY_A'
+
+- Set the state of a LED through :func:`InputDevice.set_led()` (thanks
+ `@accek`_).
+
+- Open :attr:`InputDevice.fd` in ``O_RDWR`` mode from now on.
+
+- Fix segfault in :func:`InputDevice.device_read_many()` (thanks `@bgilbert`_).
+
+
+0.3.3 (May 29, 2013)
+====================
+
+- Raise :class:`IOError` from :func:`InputDevice.device_read()` and
+ :func:`InputDevice.device_read_many()` when :func:`InputDevice.read()`
+ fails.
+
+- Several stability and style changes (thank you debian code reviewers).
+
+
+0.3.2 (Apr 05, 2013)
+====================
+
+- Fix vendor id and product id order in :func:`DeviceInfo` (thanks `@kived`_).
+
+
+0.3.1 (Nov 23, 2012)
+====================
+
+- :func:`InputDevice.read()` will return an empty tuple if the device
+ has nothing to offer (instead of segfaulting).
+
+- Exclude unnecessary package data in sdist and bdist.
+
+
+0.3.0 (Nov 06, 2012)
+====================
+
+- Add ability to set/get auto-repeat settings with ``EVIOC{SG}REP``.
+
+- Add :func:`InputDevice.version` - the value of ``EVIOCGVERSION``.
+
+- Add :func:`InputDevice.read_loop()`.
+
+- Add :func:`InputDevice.grab()` and :func:`InputDevice.ungrab()` -
+ exposes ``EVIOCGRAB``.
+
+- Add :func:`InputDevice.leds` - exposes ``EVIOCGLED``.
+
+- Replace :class:`DeviceInfo` class with a namedtuple.
+
+- Prevent :func:`InputDevice.read_one()` from skipping events.
+
+- Rename :class:`AbsData` to :class:`AbsInfo` (as in ``struct input_absinfo``).
+
+
+0.2.0 (Aug 22, 2012)
+====================
+
+- Add the ability to set arbitrary device capabilities on uinput
+ devices (defaults to all ``EV_KEY`` ecodes).
+
+- Add :attr:`UInput.device` which is an open :class:`InputDevice` to
+ the input device that uinput 'spawns'.
+
+- Add :func:`UInput.capabilities()` which is just a shortcut to
+ :func:`UInput.device.capabilities()`.
+
+- Rename :func:`UInput.write()` to :func:`UInput.write_event()`.
+
+- Add a simpler :func:`UInput.write(type, code, value)` method.
+
+- Make all :func:`UInput` constructor arguments optional (default
+ device name is now ``py-evdev-uinput``).
+
+- Add the ability to set ``absmin``, ``absmax``, ``absfuzz`` and
+ ``absflat`` when specifying the uinput device's capabilities.
+
+- Remove the ``nophys`` argument - if a device fails the
+ ``EVIOCGPHYS`` ioctl, phys will equal the empty string.
+
+- Make :func:`InputDevice.capabilities()` perform a ``EVIOCGABS``
+ ioctl for devices that support ``EV_ABS`` and return that info
+ wrapped in an ``AbsData`` namedtuple.
+
+- Split ``ioctl_devinfo`` into ``ioctl_devinfo`` and
+ ``ioctl_capabilities``.
+
+- Split :func:`UInput.uinput_open()` to :func:`UInput.uinput_open()`
+ and :func:`UInput.uinput_create()`
+
+- Add more uinput usage examples and documentation.
+
+- Rewrite uinput tests.
+
+- Remove ``mouserel`` and ``mouseabs`` from :class:`UInput`.
+
+- Tie the sphinx version and release to the distutils version.
+
+- Set 'methods-before-attributes' sorting in the docs.
+
+- Remove ``KEY_CNT`` and ``KEY_MAX`` from :func:`ecodes.keys`.
+
+
+0.1.1 (May 18, 2012)
+====================
+
+- Add ``events.keys``, which is a combination of all ``BTN_`` and
+ ``KEY_`` event codes.
+
+- ``ecodes.c`` was not generated when installing through ``pip``.
+
+
+0.1.0 (May 17, 2012)
+====================
+
+*Initial Release*
+
+.. _`@polyphemus`: https://github.com/polyphemus
+.. _`@bgilbert`: https://github.com/bgilbert
+.. _`@accek`: https://github.com/accek
+.. _`@kived`: https://github.com/kived
+.. _`@spasche`: https://github.com/spasche
+.. _`@isia`: https://github.com/isia
+.. _`@forsenonlhaimaisentito`: https://github.com/forsenonlhaimaisentito
+.. _`@paulo-raca`: https://github.com/paulo-raca
+.. _`@ndreys`: https://github.com/ndreys
+.. _`@LinusCDE`: https://github.com/gvalkov/python-evdev/pulls/LinusCDE
+.. _`@ivaradi`: https://github.com/gvalkov/python-evdev/pull/104
+
+.. _`introduced in Linux 4.5`: https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75
+.. _issue21121: http://bugs.python.org/issue21121
+.. _`#63`: https://github.com/gvalkov/python-evdev/issues/63
+.. _`#67`: https://github.com/gvalkov/python-evdev/issues/67
diff --git a/doc/conf.py b/docs/conf.py
similarity index 68%
rename from doc/conf.py
rename to docs/conf.py
index ee8ec57..0be06b3 100644
--- a/doc/conf.py
+++ b/docs/conf.py
@@ -1,97 +1,108 @@
-# -*- coding: utf-8 -*-
-
-import sys, os
+import os
+import sys
+import sphinx_rtd_theme
# Check if readthedocs is building us
-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..'))
-from setup import kw
# Trick autodoc into running without having built the extension modules.
if on_rtd:
- with open('../evdev/_ecodes.py', 'w') as fh:
- fh.write('''
+ with open("../src/evdev/_ecodes.py", "w") as fh:
+ fh.write(
+ """
KEY = ABS = REL = SW = MSC = LED = REP = SND = SYN = FF = FF_STATUS = BTN_A = KEY_A = 1
EV_KEY = EV_ABS = EV_REL = EV_SW = EV_MSC = EV_LED = EV_REP = 1
EV_SND = EV_SYN = EV_FF = EV_FF_STATUS = FF_STATUS = 1
-KEY_MAX, KEY_CNT = 1, 2''')
+KEY_MAX, KEY_CNT = 1, 2"""
+ )
- with open('../evdev/_input.py', 'w'): pass
- with open('../evdev/_uinput.py', 'w'): pass
+ with open("../src/evdev/_input.py", "w"):
+ pass
+ with open("../src/evdev/_uinput.py", "w"):
+ pass
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx']
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.napoleon",
+ "sphinx_rtd_theme",
+ "sphinx_copybutton",
+]
+
+autodoc_member_order = "bysource"
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
# The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = u'python-evdev'
-copyright = u'2012-2013, Georgi Valkov'
+project = "python-evdev"
+copyright = "2012-2025, Georgi Valkov and contributors"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
-release = kw['version']
+release = "1.9.3"
# The short X.Y version.
version = release
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-#language = None
+# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
-#today = ''
+# today = ''
# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
-#show_authors = False
+# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+# pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
@@ -99,138 +110,127 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-if not on_rtd:
- import sphinx_rtd_theme
- html_theme = 'sphinx_rtd_theme'
- html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
-else:
- html_theme = 'default'
+html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
-#html_theme_options = {}
+# html_theme_options = {"full_logo" : True}
# Add any paths that contain custom themes here, relative to this directory.
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
-#html_title = None
+html_title = "Python-evdev"
# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
+html_short_title = "evdev"
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-#html_logo = None
+# html_logo = ''
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-#html_favicon = None
+# html_favicon = None
-# Add any paths that contain custom static files (such as style sheets) here,
+# Add any paths that contain custom static files (such as style sheets here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
# If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
# If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
# If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
# Output file base name for HTML help builder.
-htmlhelp_basename = 'python-evdev-doc'
+htmlhelp_basename = "python-evdev-doc"
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'python-evdev.tex', u'evdev documentation',
- u'Georgi Valkov', 'manual'),
+ ("index", "python-evdev.tex", "evdev documentation", "Georgi Valkov", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
-#latex_logo = None
+# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
# If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
# If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
# Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
# If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- ('index', 'python-evdev', u'python-evdev Documentation',
- [u'Georgi Valkov'], 1)
-]
+man_pages = [("index", "python-evdev", "python-evdev Documentation", ["Georgi Valkov"], 1)]
# If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
@@ -239,18 +239,29 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'python-evdev', u'python-evdev Documentation',
- u'Georgi Valkov', 'evdev', 'Bindings for the linux input handling subsystem.',
- 'Miscellaneous'),
+ (
+ "index",
+ "python-evdev",
+ "python-evdev Documentation",
+ "Georgi Valkov",
+ "evdev",
+ "Bindings for the linux input handling subsystem.",
+ "Miscellaneous",
+ ),
]
-intersphinx_mapping = {'python': ('http://docs.python.org/3.3', None)}
+intersphinx_mapping = {"python": ("http://docs.python.org/3", None)}
# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
# If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
+
+# Copybutton config
+# copybutton_prompt_text = r">>> "
+# copybutton_prompt_is_regexp = True
+# copybutton_only_copy_prompt_lines = True
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..e7c861e
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,46 @@
+Introduction
+------------
+
+This package provides bindings to the generic input event interface in
+Linux. The *evdev* interface serves the purpose of passing events
+generated in the kernel directly to userspace through character
+devices that are typically located in ``/dev/input/``.
+
+This package also comes with bindings to *uinput*, the userspace input
+subsystem. *Uinput* allows userspace programs to create and handle
+input devices that can inject events directly into the input
+subsystem.
+
+In other words, *python-evdev* allows you to read and write input
+events on Linux. An event can be a key or button press, a mouse
+movement or a tap on a touchscreen.
+
+
+.. toctree::
+ :caption: Installation
+ :maxdepth: 2
+
+ install
+
+.. toctree::
+ :caption: Usage
+
+ usage
+ tutorial
+ apidoc
+
+.. toctree::
+ :caption: Project
+ :maxdepth: 2
+
+ scope
+ changelog
+
+
+License
+-------
+
+This package is released under the terms of the `Revised BSD License`_.
+
+.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-evdev/master/LICENSE
+.. _evdev: http://svn.navi.cx/misc/trunk/python/evdev/
diff --git a/docs/install.rst b/docs/install.rst
new file mode 100644
index 0000000..f93e0b8
--- /dev/null
+++ b/docs/install.rst
@@ -0,0 +1,100 @@
+From an OS package
+==================
+
+Python-evdev has been packaged for the following distributions:
+
+.. raw:: html
+
+
+
+
+
+Consult the documentation of your OS package manager for installation instructions.
+
+
+From source
+===========
+
+The latest stable version of *python-evdev* can be installed from pypi_,
+provided that you have a compiler, pip_ and the Python and Linux development
+headers installed on your system. Installing these is distribution specific and
+typically falls into one of the following:
+
+On a Debian compatible OS:
+
+.. code-block:: bash
+
+ $ apt install python-dev python-pip gcc
+ $ apt install linux-headers-$(uname -r)
+
+On a Redhat compatible OS:
+
+.. code-block:: bash
+
+ $ dnf install python-devel python-pip gcc
+ $ dnf install kernel-headers-$(uname -r)
+
+On Arch Linux and derivatives:
+
+.. code-block:: bash
+
+ $ pacman -S core/linux-api-headers python-pip gcc
+
+Once all OS dependencies are available, you may install *python-evdev* using
+pip_, preferably in a [virtualenv]_:
+
+.. code-block:: bash
+
+ # Install globally (not recommended).
+ $ sudo python3 -m pip install evdev
+
+ # Install for the current user.
+ $ python3 -m pip install --user evdev
+
+ # Install in a virtual environment.
+ $ python3 -m venv abc
+ $ source abc/bin/activate
+ $ python3 -m pip install evdev
+
+
+Specifying header locations
+---------------------------
+
+By default, the setup script will look for the ``input.h`` and
+``input-event-codes.h`` [#f1]_ header files ``/usr/include/linux``.
+
+You may use the ``--evdev-headers`` option to the ``build_ext`` setuptools
+command to the location of these header files. It accepts one or more
+colon-separated paths. For example:
+
+.. code-block:: bash
+
+ $ python setup.py build_ext \
+ --evdev-headers buildroot/input.h:buildroot/input-event-codes.h \
+ --include-dirs buildroot/ \
+ install # or any other command (e.g. develop, bdist, bdist_wheel)
+
+
+From a binary package
+=====================
+
+You may choose to install a precompiled version of *python-evdev* from pypi. The
+`evdev-binary`_ package provides binary wheels that have been compiled on EL8
+against the 4.18.0 kernel headers.
+
+.. code-block:: bash
+
+ $ python3 -m pip install evdev-binary
+
+While the evdev interface is stable, the precompiled version may not be fully
+compatible or expose all the features of your running kernel. For best results,
+it is recommended to use an OS package or to install from source.
+
+
+.. [#f1] ``input-event-codes.h`` is found only in recent kernel versions.
+.. _pypi: http://pypi.python.org/pypi/evdev
+.. _evdev-binary: http://pypi.python.org/pypi/evdev-binary
+.. _github: https://github.com/gvalkov/python-evdev
+.. _pip: http://pip.readthedocs.org/en/latest/installing.html
+.. _example: https://github.com/gvalkov/python-evdev/tree/master/examples
+.. _virtualenv: https://docs.python.org/3/library/venv.html
diff --git a/docs/scope.rst b/docs/scope.rst
new file mode 100644
index 0000000..66a887d
--- /dev/null
+++ b/docs/scope.rst
@@ -0,0 +1,31 @@
+Scope and status
+----------------
+
+Python-evdev exposes most of the more common interfaces defined in the evdev
+subsystem. Reading and injecting events is well supported and has been tested
+with nearly all event types.
+
+The basic functionality for reading and uploading force-feedback events is
+there, but it has not been exercised sufficiently. A major shortcoming of the
+uinput wrapper is that it does not support force-feedback devices at all (see
+issue `#23`_).
+
+Some characters, such as ``:`` (colon), cannot be easily injected (see issue
+`#7`_), Translating them into UInput events would require knowing the kernel
+keyboard translation table, which is beyond the scope of python-evdev. Please
+look into the following projects if you need more complete or convenient input
+injection support.
+
+- python-uinput_
+- uinput-mapper_
+- pynput_
+- pygame_ (cross-platform)
+
+
+.. _python-uinput: https://github.com/tuomasjjrasanen/python-uinput
+.. _uinput-mapper: https://github.com/MerlijnWajer/uinput-mapper
+.. _pynput: https://github.com/moses-palmer/pynput
+.. _pygame: http://www.pygame.org/
+
+.. _`#7`: https://github.com/gvalkov/python-evdev/issues/7
+.. _`#23`: https://github.com/gvalkov/python-evdev/pull/23
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
new file mode 100644
index 0000000..04ae42f
--- /dev/null
+++ b/docs/tutorial.rst
@@ -0,0 +1,508 @@
+Tutorial
+--------
+
+
+Listing accessible event devices
+================================
+
+::
+
+ >>> import evdev
+
+ >>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
+ >>> for device in devices:
+ >>> print(device.path, device.name, device.phys)
+ /dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0
+ /dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0
+
+
+Listing device capabilities
+===========================
+
+::
+
+ >>> import evdev
+
+ >>> device = evdev.InputDevice('/dev/input/event0')
+ >>> print(device)
+ device /dev/input/event0, name "Dell USB Optical Mouse", phys "usb-0000:00:12.0-2/input0"
+
+ >>> device.capabilities()
+ ... { 0: [0, 1, 2], 1: [272, 273, 274, 275], 2: [0, 1, 6, 8], 4: [4] }
+
+ >>> device.capabilities(verbose=True)
+ ... { ('EV_SYN', 0): [('SYN_REPORT', 0), ('SYN_CONFIG', 1), ('SYN_MT_REPORT', 2)],
+ ... ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274), ('BTN_SIDE', 275)], ...
+
+
+Listing device capabilities (devices with absolute axes)
+========================================================
+
+::
+
+ >>> import evdev
+
+ >>> device = evdev.InputDevice('/dev/input/event7')
+ >>> print(device)
+ device /dev/input/event7, name "Wacom Bamboo 2FG 4x5 Finger", phys ""
+
+ >>> device.capabilities()
+ ... { 1: [272, 273, 277, 278, 325, 330, 333] ,
+ ... 3: [(0, AbsInfo(min=0, max=15360, fuzz=128, flat=0)),
+ ... (1, AbsInfo(min=0, max=10240, fuzz=128, flat=0))] }
+
+ >>> device.capabilities(verbose=True)
+ ... { ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ...],
+ ... ('EV_ABS', 3): [(('ABS_X', 0), AbsInfo(min=0, max=15360, fuzz=128, flat=0)),
+ ... (('ABS_Y', 1), AbsInfo(min=0, max=10240, fuzz=128, flat=0)),] }
+
+ >>> device.capabilities(absinfo=False)
+ ... { 1: [272, 273, 277, 278, 325, 330, 333],
+ ... 3: [0, 1, 47, 53, 54, 57] }
+
+
+Getting and setting LED states
+==============================
+
+::
+
+ >>> dev.leds(verbose=True)
+ ... [('LED_NUML', 0), ('LED_CAPSL', 1)]
+
+ >>> dev.leds()
+ ... [0, 1]
+
+ >>> dev.set_led(ecodes.LED_NUML, 1) # enable numlock
+ >>> dev.set_led(ecodes.LED_NUML, 0) # disable numlock
+
+
+Getting currently active keys
+=============================
+
+::
+
+ >>> dev.active_keys(verbose=True)
+ ... [('KEY_3', 4), ('KEY_LEFTSHIFT', 42)]
+
+ >>> dev.active_keys()
+ ... [4, 42]
+
+
+Reading events
+==============
+
+Reading events from a single device in an endless loop.
+
+::
+
+ >>> from evdev import InputDevice, categorize, ecodes
+ >>> dev = InputDevice('/dev/input/event1')
+
+ >>> print(dev)
+ device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0"
+
+ >>> for event in dev.read_loop():
+ ... if event.type == ecodes.EV_KEY:
+ ... print(categorize(event))
+ ... # pressing 'a' and holding 'space'
+ key event at 1337016188.396030, 30 (KEY_A), down
+ key event at 1337016188.492033, 30 (KEY_A), up
+ key event at 1337016189.772129, 57 (KEY_SPACE), down
+ key event at 1337016190.275396, 57 (KEY_SPACE), hold
+ key event at 1337016190.284160, 57 (KEY_SPACE), up
+
+
+Reading events (using :mod:`asyncio`)
+======================================
+
+.. note::
+
+ This requires Python 3.5+ for the async/await keywords.
+
+
+::
+
+ >>> import asyncio
+ >>> from evdev import InputDevice
+
+ >>> dev = InputDevice('/dev/input/event1')
+
+ >>> async def main(dev):
+ ... async for ev in dev.async_read_loop():
+ ... print(repr(ev))
+
+ >>> asyncio.run(main(dev))
+ InputEvent(1527363738, 348740, 4, 4, 458792)
+ InputEvent(1527363738, 348740, 1, 28, 0)
+ InputEvent(1527363738, 348740, 0, 0, 0)
+
+
+Reading events from multiple devices (using :mod:`select`)
+==========================================================
+
+::
+
+ >>> from evdev import InputDevice
+ >>> from select import select
+
+ # A mapping of file descriptors (integers) to InputDevice instances.
+ >>> devices = map(InputDevice, ('/dev/input/event1', '/dev/input/event2'))
+ >>> devices = {dev.fd: dev for dev in devices}
+
+ >>> for dev in devices.values(): print(dev)
+ device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0"
+ device /dev/input/event2, name "Logitech USB Laser Mouse", phys "usb-0000:00:12.0-2/input0"
+
+ >>> while True:
+ ... r, w, x = select(devices, [], [])
+ ... for fd in r:
+ ... for event in devices[fd].read():
+ ... print(event)
+ event at 1351116708.002230, code 01, type 02, val 01
+ event at 1351116708.002234, code 00, type 00, val 00
+ event at 1351116708.782231, code 04, type 04, val 458782
+ event at 1351116708.782237, code 02, type 01, val 01
+
+
+Reading events from multiple devices (using :mod:`selectors`)
+=============================================================
+
+This can also be achieved using the :mod:`selectors` module in Python 3.4:
+
+::
+
+ from evdev import InputDevice
+ from selectors import DefaultSelector, EVENT_READ
+
+ selector = DefaultSelector()
+
+ mouse = InputDevice('/dev/input/event1')
+ keybd = InputDevice('/dev/input/event2')
+
+ # This works because InputDevice has a `fileno()` method.
+ selector.register(mouse, EVENT_READ)
+ selector.register(keybd, EVENT_READ)
+
+ while True:
+ for key, mask in selector.select():
+ device = key.fileobj
+ for event in device.read():
+ print(event)
+
+
+Reading events from multiple devices (using :mod:`asyncio`)
+===========================================================
+
+Yet another possibility is the :mod:`asyncio` module from Python 3.4:
+
+::
+
+ import asyncio, evdev
+
+ @asyncio.coroutine
+ def print_events(device):
+ while True:
+ events = yield from device.async_read()
+ for event in events:
+ print(device.path, evdev.categorize(event), sep=': ')
+
+ mouse = evdev.InputDevice('/dev/input/eventX')
+ keybd = evdev.InputDevice('/dev/input/eventY')
+
+ for device in mouse, keybd:
+ asyncio.async(print_events(device))
+
+ loop = asyncio.get_event_loop()
+ loop.run_forever()
+
+Since Python 3.5, the `async/await`_ syntax makes this even simpler:
+
+::
+
+ import asyncio, evdev
+
+ mouse = evdev.InputDevice('/dev/input/event4')
+ keybd = evdev.InputDevice('/dev/input/event5')
+
+ async def print_events(device):
+ async for event in device.async_read_loop():
+ print(device.path, evdev.categorize(event), sep=': ')
+
+ for device in mouse, keybd:
+ asyncio.ensure_future(print_events(device))
+
+ loop = asyncio.get_event_loop()
+ loop.run_forever()
+
+
+Accessing evdev constants
+=========================
+
+::
+
+ >>> from evdev import ecodes
+
+ >>> ecodes.KEY_A, ecodes.ecodes['KEY_A']
+ ... (30, 30)
+ >>> ecodes.KEY[30]
+ ... 'KEY_A'
+ >>> ecodes.bytype[ecodes.EV_KEY][30]
+ ... 'KEY_A'
+ >>> ecodes.KEY[152] # a single value may correspond to multiple codes
+ ... ['KEY_COFFEE', 'KEY_SCREENLOCK']
+
+
+Searching event codes by regex
+==============================
+
+::
+
+ >>> from evdev import util
+
+ >>> res = util.find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)')
+ >>> res
+ ... {1: [411], 3: [10]}
+ >>> util.resolve_ecodes_dict(res)
+ ... {('EV_KEY', 1): [('KEY_BREAK', 411)], ('EV_ABS', 3): [('ABS_BRAKE', 10)]}
+
+
+Getting exclusive access to a device
+====================================
+
+::
+
+ >>> dev.grab() # become the sole recipient of all incoming input events
+ >>> dev.ungrab()
+
+This functionality is also available as a context manager.
+
+::
+
+ >>> with dev.grab_context():
+ ... pass
+
+
+Associating classes with event types
+====================================
+
+::
+
+ >>> from evdev import categorize, event_factory, ecodes
+
+ >>> class SynEvent:
+ ... def __init__(self, event):
+ ... ...
+
+ >>> event_factory[ecodes.EV_SYN] = SynEvent
+
+See :mod:`events ` for more information.
+
+Injecting input
+===============
+
+::
+
+ >>> from evdev import UInput, ecodes as e
+
+ >>> ui = UInput()
+
+ >>> # accepts only KEY_* events by default
+ >>> ui.write(e.EV_KEY, e.KEY_A, 1) # KEY_A down
+ >>> ui.write(e.EV_KEY, e.KEY_A, 0) # KEY_A up
+ >>> ui.syn()
+
+ >>> ui.close()
+
+
+Injecting events (using a context manager)
+==========================================
+
+::
+
+ >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1)
+ >>> with UInput() as ui:
+ ... ui.write_event(ev)
+ ... ui.syn()
+
+
+Specifying ``uinput`` device options
+====================================
+
+.. note::
+
+ ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created.
+
+::
+
+ >>> from evdev import UInput, AbsInfo, ecodes as e
+
+ >>> cap = {
+ ... e.EV_KEY : [e.KEY_A, e.KEY_B],
+ ... e.EV_ABS : [
+ ... (e.ABS_X, AbsInfo(value=0, min=0, max=255,
+ ... fuzz=0, flat=0, resolution=0)),
+ ... (e.ABS_Y, AbsInfo(0, 0, 255, 0, 0, 0)),
+ ... (e.ABS_MT_POSITION_X, (0, 128, 255, 0)) ]
+ ... }
+
+ >>> ui = UInput(cap, name='example-device', version=0x3)
+ >>> print(ui)
+ name "example-device", bus "BUS_USB", vendor "0001", product "0001", version "0003"
+ event types: EV_KEY EV_ABS EV_SYN
+
+ >>> print(ui.capabilities())
+ {0: [0, 1, 3],
+ 1: [30, 48],
+ 3: [(0, AbsInfo(value=0, min=0, max=0, fuzz=255, flat=0, resolution=0)),
+ (1, AbsInfo(value=0, min=0, max=0, fuzz=255, flat=0, resolution=0)),
+ (53, AbsInfo(value=0, min=0, max=255, fuzz=128, flat=0, resolution=0))]}
+
+ >>> # move mouse cursor
+ >>> ui.write(e.EV_ABS, e.ABS_X, 20)
+ >>> ui.write(e.EV_ABS, e.ABS_Y, 20)
+ >>> ui.syn()
+
+
+Create ``uinput`` device with capabilities of another device
+================================================================
+
+::
+
+ >>> from evdev import UInput, InputDevice
+
+ >>> mouse = InputDevice('/dev/input/event1')
+ >>> keybd = '/dev/input/event2'
+
+ >>> ui = UInput.from_device(mouse, keybd, name='keyboard-mouse-device')
+ >>> ui.capabilities(verbose=True).keys()
+ dict_keys([('EV_LED', 17), ('EV_KEY', 1), ('EV_SYN', 0), ('EV_REL', 2), ('EV_MSC', 4)])
+
+
+.. _`async/await`: https://docs.python.org/3/library/asyncio-task.html
+
+Create ``uinput`` device capable of receiving FF-effects
+========================================================
+
+::
+
+ import asyncio
+ from evdev import UInput, categorize, ecodes
+
+ cap = {
+ ecodes.EV_FF: [ecodes.FF_RUMBLE ],
+ ecodes.EV_KEY: [ecodes.KEY_A, ecodes.KEY_B]
+ }
+
+ ui = UInput(cap, name='test-controller', version=0x3)
+
+ async def print_events(device):
+ async for event in device.async_read_loop():
+ print(categorize(event))
+
+ # Wait for an EV_UINPUT event that will signal us that an
+ # effect upload/erase operation is in progress.
+ if event.type != ecodes.EV_UINPUT:
+ continue
+
+ if event.code == ecodes.UI_FF_UPLOAD:
+ upload = device.begin_upload(event.value)
+ upload.retval = 0
+
+ print(f'[upload] effect_id: {upload.effect.id}, type: {upload.effect.type}')
+ device.end_upload(upload)
+
+ elif event.code == ecodes.UI_FF_ERASE:
+ erase = device.begin_erase(event.value)
+ print(f'[erase] effect_id {erase.effect_id}')
+
+ erase.retval = 0
+ device.end_erase(erase)
+
+ asyncio.ensure_future(print_events(ui))
+ loop = asyncio.get_event_loop()
+ loop.run_forever()
+
+
+Injecting an FF-event into first FF-capable device found
+========================================================
+
+::
+
+ from evdev import ecodes, InputDevice, ff, list_devices
+ import time
+
+ # Find first EV_FF capable event device (that we have permissions to use).
+ for name in list_devices():
+ dev = InputDevice(name)
+ if ecodes.EV_FF in dev.capabilities():
+ break
+
+ rumble = ff.Rumble(strong_magnitude=0x0000, weak_magnitude=0xffff)
+ effect_type = ff.EffectType(ff_rumble_effect=rumble)
+ duration_ms = 1000
+
+ effect = ff.Effect(
+ ecodes.FF_RUMBLE, -1, 0,
+ ff.Trigger(0, 0),
+ ff.Replay(duration_ms, 0),
+ effect_type
+ )
+
+ repeat_count = 1
+ effect_id = dev.upload_effect(effect)
+ dev.write(ecodes.EV_FF, effect_id, repeat_count)
+ time.sleep(duration_ms / 1000)
+ dev.erase_effect(effect_id)
+
+
+Forwarding force-feedback from uinput to a real device
+======================================================
+
+::
+
+ import evdev
+ from evdev import ecodes as e
+
+ # Find first EV_FF capable event device (that we have permissions to use).
+ for name in evdev.list_devices():
+ dev = evdev.InputDevice(name)
+ if e.EV_FF in dev.capabilities():
+ break
+ # To ensure forwarding works correctly it is important that `max_effects`
+ # of the uinput device is <= `dev.ff_effects_count`.
+ # `from_device()` will do this automatically, but in some situations you may
+ # want to set the `max_effects` parameter manually, such as when using `Uinput()`.
+ # `filtered_types` is specified as by default EV_FF events are filtered
+ uinput = evdev.UInput.from_device(dev, filtered_types=[e.EV_SYN])
+
+ # Keeps track of which effects have been uploaded to the device
+ effects = set()
+
+ for event in uinput.read_loop():
+
+ # Handle the special uinput events
+ if event.type == e.EV_UINPUT:
+
+ if event.code == e.UI_FF_UPLOAD:
+ upload = uinput.begin_upload(event.value)
+
+ # Checks if this is a new effect
+ if upload.effect.id not in effects:
+ effects.add(upload.effect.id)
+ # Setting id to 1 indicates that a new effect must be allocated
+ upload.effect.id = -1
+
+ dev.upload_effect(upload.effect)
+ upload.retval = 0
+ uinput.end_upload(upload)
+
+ elif event.code == e.UI_FF_ERASE:
+ erase = uinput.begin_erase(event.value)
+ erase.retval = 0
+ dev.erase_effect(erase.effect_id)
+ effects.remove(erase.effect_id)
+ uinput.end_erase(erase)
+
+ # Forward writes to actual rumble device.
+ elif event.type == e.EV_FF:
+ dev.write(event.type, event.code, event.value)
diff --git a/docs/usage.rst b/docs/usage.rst
new file mode 100644
index 0000000..f3d4d39
--- /dev/null
+++ b/docs/usage.rst
@@ -0,0 +1,78 @@
+Quick Start
+-----------
+
+
+Listing accessible event devices
+================================
+
+::
+
+ >>> import evdev
+
+ >>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
+ >>> for device in devices:
+ ... print(device.path, device.name, device.phys)
+ /dev/input/event1 USB Keyboard usb-0000:00:12.1-2/input0
+ /dev/input/event0 USB Optical Mouse usb-0000:00:12.0-2/input0
+
+.. note::
+
+ If you do not see any devices, ensure that your user is in the
+ correct group (typically ``input``) to have read/write access.
+
+
+Reading events from a device
+============================
+
+::
+
+ >>> import evdev
+
+ >>> device = evdev.InputDevice('/dev/input/event1')
+ >>> print(device)
+ device /dev/input/event1, name "USB Keyboard", phys "usb-0000:00:12.1-2/input0"
+
+ >>> for event in device.read_loop():
+ ... if event.type == evdev.ecodes.EV_KEY:
+ ... print(evdev.categorize(event))
+ ... # pressing 'a' and holding 'space'
+ key event at 1337016188.396030, 30 (KEY_A), down
+ key event at 1337016188.492033, 30 (KEY_A), up
+ key event at 1337016189.772129, 57 (KEY_SPACE), down
+ key event at 1337016190.275396, 57 (KEY_SPACE), hold
+ key event at 1337016190.284160, 57 (KEY_SPACE), up
+
+
+Accessing event codes
+=====================
+
+The ``evdev.ecodes`` module provides reverse and forward mappings between the
+names and values of the event subsystem constants.
+
+::
+
+ >>> from evdev import ecodes
+
+ >>> ecodes.KEY_A
+ ... 30
+ >>> ecodes.ecodes['KEY_A']
+ ... 30
+ >>> ecodes.KEY[30]
+ ... 'KEY_A'
+ >>> ecodes.bytype[ecodes.EV_KEY][30]
+ ... 'KEY_A'
+
+ # A single value may correspond to multiple event codes.
+ >>> ecodes.KEY[152]
+ ... ['KEY_COFFEE', 'KEY_SCREENLOCK']
+
+
+Listing and monitoring input devices
+====================================
+
+The *python-evdev* package also comes with a small command-line program for
+listing and monitoring input devices:
+
+.. code-block:: bash
+
+ $ python -m evdev.evtest
diff --git a/evdev/__init__.py b/evdev/__init__.py
deleted file mode 100644
index bb9a241..0000000
--- a/evdev/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# encoding: utf-8
-
-# Gather everything into a convenient namespace
-
-from evdev.device import DeviceInfo, InputDevice, AbsInfo
-from evdev.events import InputEvent, KeyEvent, RelEvent, SynEvent, AbsEvent, event_factory
-from evdev.uinput import UInput, UInputError
-from evdev.util import list_devices, categorize, resolve_ecodes
-from evdev import ecodes
-from evdev import ff
diff --git a/evdev/device.py b/evdev/device.py
deleted file mode 100644
index a83b4ee..0000000
--- a/evdev/device.py
+++ /dev/null
@@ -1,312 +0,0 @@
-# encoding: utf-8
-
-import os
-from select import select
-from collections import namedtuple
-
-from evdev import _input, _uinput, ecodes, util
-from evdev.events import InputEvent
-
-
-_AbsInfo = namedtuple('AbsInfo', ['value', 'min', 'max', 'fuzz', 'flat', 'resolution'])
-_KbdInfo = namedtuple('KbdInfo', ['repeat', 'delay'])
-_DeviceInfo = namedtuple('DeviceInfo', ['bustype', 'vendor', 'product', 'version'])
-
-
-class AbsInfo(_AbsInfo):
- '''
- A ``namedtuple`` for storing absolut axis information -
- corresponds to the ``input_absinfo`` struct:
-
- - value
- Latest reported value for the axis.
-
- - min
- Specifies minimum value for the axis.
-
- - max
- Specifies maximum value for the axis.
-
- - fuzz
- Specifies fuzz value that is used to filter noise from the
- event stream.
-
- - flat
- Values that are within this value will be discarded by joydev
- interface and reported as 0 instead.
-
- - resolution
- Specifies resolution for the values reported for the axis.
- Resolution for main axes (``ABS_X, ABS_Y, ABS_Z``) is reported
- in units per millimeter (units/mm), resolution for rotational
- axes (``ABS_RX, ABS_RY, ABS_RZ``) is reported in units per
- radian.
-
- .. note: The input core does not clamp reported values to the
- ``[minimum, maximum]`` limits, such task is left to userspace.
- '''
-
- def __str__(self):
- return 'val {}, min {}, max {}, fuzz {}, flat {}, res {}'.format(*self)
-
-
-class KbdInfo(_KbdInfo):
- '''
- Keyboard repeat rate:
-
- - repeat:
- Keyboard repeat rate in characters per second.
-
- - delay:
- Amount of time that a key must be depressed before it will start
- to repeat (in milliseconds).
- '''
-
- def __str__(self):
- return 'repeat {}, delay {}'.format(*self)
-
-
-class DeviceInfo(_DeviceInfo):
- def __str__(self):
- msg = 'bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}'
- return msg.format(*self)
-
-
-class InputDevice(object):
- '''
- A linux input device from which input events can be read.
- '''
-
- __slots__ = ('fn', 'fd', 'info', 'name', 'phys', '_rawcapabilities',
- 'version', 'ff_effects_count')
-
- def __init__(self, dev):
- '''
- :param dev: path to input device
- '''
-
- #: Path to input device.
- self.fn = dev
-
- #: A non-blocking file descriptor to the device file.
- self.fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK)
-
- # Returns (bustype, vendor, product, version, name, phys, capabilities).
- info_res = _input.ioctl_devinfo(self.fd)
-
- #: A :class:`DeviceInfo ` instance.
- self.info = DeviceInfo(*info_res[:4])
-
- #: The name of the event device.
- self.name = info_res[4]
-
- #: The physical topology of the device.
- self.phys = info_res[5]
-
- #: The evdev protocol version.
- self.version = _input.ioctl_EVIOCGVERSION(self.fd)
-
- #: The raw dictionary of device capabilities - see `:func:capabilities()`.
- self._rawcapabilities = _input.ioctl_capabilities(self.fd)
-
- #: The number of force feedback effects the device can keep in its memory.
- self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd)
-
- def __del__(self):
- if hasattr(self, 'fd') and self.fd is not None:
- try:
- self.close()
- except OSError:
- pass
-
- def _capabilities(self, absinfo=True):
- res = {}
- for etype, ecodes in self._rawcapabilities.items():
- for code in ecodes:
- l = res.setdefault(etype, [])
- if isinstance(code, tuple):
- if absinfo:
- a = code[1] # (0, 0, 0, 255, 0, 0)
- i = AbsInfo(*a)
- l.append((code[0], i))
- else:
- l.append(code[0])
- else:
- l.append(code)
-
- return res
-
- def capabilities(self, verbose=False, absinfo=True):
- '''
- Return the event types that this device supports as a mapping of
- supported event types to lists of handled event codes. Example::
-
- { 1: [272, 273, 274],
- 2: [0, 1, 6, 8] }
-
- If ``verbose`` is ``True``, event codes and types will be resolved
- to their names. Example::
-
- { ('EV_KEY', 1): [('BTN_MOUSE', 272),
- ('BTN_RIGHT', 273),
- ('BTN_MIDDLE', 273)],
- ('EV_REL', 2): [('REL_X', 0),
- ('REL_Y', 1),
- ('REL_HWHEEL', 6),
- ('REL_WHEEL', 8)] }
-
- Unknown codes or types will be resolved to ``'?'``.
-
- If ``absinfo`` is ``True``, the list of capabilities will also
- include absolute axis information in the form of
- :class:`AbsInfo` instances::
-
- { 3: [ (0, AbsInfo(min=0, max=255, fuzz=0, flat=0)),
- (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]}
-
- Combined with ``verbose`` the above becomes::
-
- { ('EV_ABS', 3): [ (('ABS_X', 0), AbsInfo(min=0, max=255, fuzz=0, flat=0)),
- (('ABS_Y', 1), AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]}
-
- '''
-
- if verbose:
- return dict(util.resolve_ecodes(self._capabilities(absinfo)))
- else:
- return self._capabilities(absinfo)
-
- def leds(self, verbose=False):
- '''
- Return currently set LED keys. Example::
-
- [0, 1, 8, 9]
-
- If ``verbose`` is ``True``, event codes are resolved to
- their names. Unknown codes are resolved to ``'?'``. Example::
-
- [('LED_NUML', 0), ('LED_CAPSL', 1), ('LED_MISC', 8), ('LED_MAIL', 9)]
-
- '''
- leds = _input.get_sw_led_snd(self.fd, ecodes.EV_LED)
- if verbose:
- return [(ecodes.LED[l] if l in ecodes.LED else '?', l) for l in leds]
-
- return leds
-
- def set_led(self, led_num, value):
- '''
- Set the state of the selected LED. Example::
-
- device.set_led(ecodes.LED_NUML, 1)
-
- ..
- '''
- _uinput.write(self.fd, ecodes.EV_LED, led_num, value)
-
- def __eq__(self, other):
- '''Two devices are considered equal if their :data:`info` attributes are equal.'''
- return self.info == other.info
-
- def __str__(self):
- msg = 'device {}, name "{}", phys "{}"'
- return msg.format(self.fn, self.name, self.phys)
-
- def __repr__(self):
- msg = (self.__class__.__name__, self.fn)
- return '{}({!r})'.format(*msg)
-
- def close(self):
- if self.fd > -1:
- try:
- os.close(self.fd)
- finally:
- self.fd = -1
-
- def fileno(self):
- '''
- Return the file descriptor to the open event device. This
- makes it possible to pass pass ``InputDevice`` instances
- directly to :func:`select.select()` and
- :class:`asyncore.file_dispatcher`.'''
-
- return self.fd
-
- def read_one(self):
- '''
- Read and return a single input event as an instance of
- :class:`InputEvent `.
-
- Return ``None`` if there are no pending input events.
- '''
-
- # event -> (sec, usec, type, code, val)
- event = _input.device_read(self.fd)
-
- if event:
- return InputEvent(*event)
-
- def read_loop(self):
- '''Enter an endless ``select()`` loop that yields input events.'''
-
- while True:
- r, w, x = select([self.fd], [], [])
- for event in self.read():
- yield event
-
- def read(self):
- '''
- Read multiple input events from device. Return a generator
- object that yields :class:`InputEvent
- ` instances.
- '''
-
- # events -> [(sec, usec, type, code, val), ...]
- events = _input.device_read_many(self.fd)
-
- for i in events:
- yield InputEvent(*i)
-
- def grab(self):
- '''
- Grab input device using ``EVIOCGRAB`` - other applications will
- be unable to receive events until the device is released. Only
- one process can hold a ``EVIOCGRAB`` on a device.
-
- .. warning:: Grabbing an already grabbed device will raise an
- ``IOError``.'''
-
- _input.ioctl_EVIOCGRAB(self.fd, 1)
-
- def ungrab(self):
- '''Release device if it has been already grabbed (uses
- `EVIOCGRAB`).
-
- .. warning:: Releasing an already released device will raise an
- ``IOError('Invalid argument')``.'''
-
- _input.ioctl_EVIOCGRAB(self.fd, 0)
-
- def upload_effect(self, effect):
- '''Upload a force feedback effect to a force feedback device.'''
-
- data = bytes(buffer(effect)[:])
- ff_id = _input.upload_effect(self.fd, data)
- return ff_id
-
- def erase_effect(self, ff_id):
- '''Erase a force effect from a force feedback device. This
- also stops the effect.'''
-
- _input.erase_effect(self.fd, ff_id)
-
- @property
- def repeat(self):
- '''Get or set the keyboard repeat rate (in characters per
- minute) and delay (in milliseconds).'''
-
- return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd))
-
- @repeat.setter
- def repeat(self, value):
- return _input.ioctl_EVIOCSREP(self.fd, *value)
diff --git a/evdev/ecodes.sh b/evdev/ecodes.sh
deleted file mode 100755
index 8408e3f..0000000
--- a/evdev/ecodes.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/bin/bash
-
-# Generate a Python extension module that exports macros from
-# /usr/include/linux/input.h
-
-# This script is obsolete. Please use `python -m evdev.genecodes`.
-
-header=${1:-/usr/include/linux/input.h}
-[[ ! -e $header ]] && echo "no such file: $header" && exit 1
-
-
-function codes () {
- awk '
- /#define +(KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_/ {
- print " PyModule_AddIntMacro(m, "$2");"
- }' ${header}
-}
-
-cat << EOF
-#include
-#include
-
-/* Automatically generated by evdev/ecodes.sh */
-
-#define MODULE_NAME "_ecodes"
-#define MODULE_HELP "linux/input.h macros"
-
-static PyMethodDef MethodTable[] = {
- { NULL, NULL, 0, NULL}
-};
-
-#if PY_MAJOR_VERSION >= 3
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- MODULE_NAME,
- MODULE_HELP,
- -1, /* m_size */
- MethodTable, /* m_methods */
- NULL, /* m_reload */
- NULL, /* m_traverse */
- NULL, /* m_clear */
- NULL, /* m_free */
-};
-#endif
-
-static PyObject *
-moduleinit(void)
-{
-
-#if PY_MAJOR_VERSION >= 3
- PyObject* m = PyModule_Create(&moduledef);
-#else
- PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP);
-#endif
-
- if (m == NULL) return NULL;
-
-$(codes)
-
- return m;
-}
-
-#if PY_MAJOR_VERSION >= 3
-PyMODINIT_FUNC
-PyInit__ecodes(void)
-{
- return moduleinit();
-}
-#else
-PyMODINIT_FUNC
-init_ecodes(void)
-{
- moduleinit();
-}
-#endif
-EOF
diff --git a/evdev/events.py b/evdev/events.py
deleted file mode 100644
index 8383d01..0000000
--- a/evdev/events.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# encoding: utf-8
-
-'''
-This module provides the :class:`InputEvent` class, which closely
-resembles the ``input_event`` struct defined in ``linux/input.h``:
-
-.. code-block:: c
-
- struct input_event {
- struct timeval time;
- __u16 type;
- __u16 code;
- __s32 value;
- };
-
-This module also defines :class:`InputEvent` sub-classes that know
-more about the different types of events (key, abs, rel etc). The
-:data:`event_factory` dictionary maps event types to these classes.
-
-Assuming you use the :func:`evdev.util.categorize()` function to
-categorize events according to their type, adding or replacing a class
-for a specific event type becomes a matter of modifying
-:data:`event_factory`.
-
-All classes in this module have reasonable ``str()`` and ``repr()``
-methods::
-
- >>> print(event)
- event at 1337197425.477827, code 04, type 04, val 458792
- >>> print(repr(event))
- InputEvent(1337197425L, 477827L, 4, 4, 458792L)
-
- >>> print(key_event)
- key event at 1337197425.477835, 28 (KEY_ENTER), up
- >>> print(repr(key_event))
- KeyEvent(InputEvent(1337197425L, 477835L, 1, 28, 0L))
-'''
-
-# event type descriptions have been taken mot-a-mot from:
-# http://www.kernel.org/doc/Documentation/input/event-codes.txt
-
-from evdev.ecodes import keys, KEY, SYN, REL, ABS, EV_KEY, EV_REL, EV_ABS, EV_SYN
-
-
-class InputEvent(object):
- '''A generic input event.'''
-
- __slots__ = 'sec', 'usec', 'type', 'code', 'value'
-
- def __init__(self, sec, usec, type, code, value):
- #: Time in seconds since epoch at which event occurred.
- self.sec = sec
-
- #: Microsecond portion of the timestamp.
- self.usec = usec
-
- #: Event type - one of ``ecodes.EV_*``.
- self.type = type
-
- #: Event code related to the event type.
- self.code = code
-
- #: Event value related to the event type.
- self.value = value
-
- def timestamp(self):
- '''Return event timestamp as a float.'''
- return self.sec + (self.usec / 1000000.0)
-
- def __str__(s):
- msg = 'event at {:f}, code {:02d}, type {:02d}, val {:02d}'
- return msg.format(s.timestamp(), s.code, s.type, s.value)
-
- def __repr__(s):
- msg = '{}({!r}, {!r}, {!r}, {!r}, {!r})'
- return msg.format(s.__class__.__name__,
- s.sec, s.usec, s.type, s.code, s.value)
-
-
-class KeyEvent(object):
- '''An event generated by a keyboard, button or other key-like devices.'''
-
- key_up = 0x0
- key_down = 0x1
- key_hold = 0x2
-
- __slots__ = 'scancode', 'keycode', 'keystate', 'event'
-
- def __init__(self, event):
- if event.value == 0:
- self.keystate = KeyEvent.key_up
- elif event.value == 2:
- self.keystate = KeyEvent.key_hold
- elif event.value == 1:
- self.keystate = KeyEvent.key_down
-
- self.keycode = keys[event.code] # :todo:
- self.scancode = event.code
-
- #: Reference to an :class:`InputEvent` instance.
- self.event = event
-
- def __str__(self):
- try:
- ks = ('up', 'down', 'hold')[self.keystate]
- except IndexError:
- ks = 'unknown'
-
- msg = 'key event at {:f}, {} ({}), {}'
- return msg.format(self.event.timestamp(),
- self.scancode, self.keycode, ks)
-
- def __repr__(s):
- return '{}({!r})'.format(s.__class__.__name__, s.event)
-
-
-class RelEvent(object):
- '''A relative axis event (e.g moving the mouse 5 units to the left).'''
-
- __slots__ = 'event'
-
- def __init__(self, event):
- #: Reference to an :class:`InputEvent` instance.
- self.event = event
-
- def __str__(self):
- msg = 'relative axis event at {:f}, {} '
- return msg.format(self.event.timestamp(), REL[self.event.code])
-
- def __repr__(s):
- return '{}({!r})'.format(s.__class__.__name__, s.event)
-
-
-class AbsEvent(object):
- '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).'''
-
- __slots__ = 'event'
-
- def __init__(self, event):
- #: Reference to an :class:`InputEvent` instance.
- self.event = event
-
- def __str__(self):
- msg = 'absolute axis event at {:f}, {} '
- return msg.format(self.event.timestamp(), ABS[self.event.code])
-
- def __repr__(s):
- return '{}({!r})'.format(s.__class__.__name__, s.event)
-
-
-class SynEvent(object):
- '''
- A synchronization event. Synchronization events are used as
- markers to separate event. Used as markers to separate
- events. Events may be separated in time or in space, such as with
- the multitouch protocol.
- '''
-
- __slots__ = 'event'
-
- def __init__(self, event):
- #: Reference to an :class:`InputEvent` instance.
- self.event = event
-
- def __str__(self):
- msg = 'synchronization event at {:f}, {} '
- return msg.format(self.event.timestamp(), SYN[self.event.code])
-
- def __repr__(s):
- return '{}({!r})'.format(s.__class__.__name__, s.event)
-
-
-#: A mapping of event types to :class:`InputEvent` sub-classes. Used
-#: by:func:`evdev.util.categorize()`
-event_factory = {
- EV_KEY: KeyEvent,
- EV_REL: RelEvent,
- EV_ABS: AbsEvent,
- EV_SYN: SynEvent,
-}
-
-
-__all__ = ('InputEvent', 'KeyEvent', 'RelEvent', 'SynEvent',
- 'AbsEvent', 'event_factory')
diff --git a/evdev/genecodes.py b/evdev/genecodes.py
deleted file mode 100755
index 8572920..0000000
--- a/evdev/genecodes.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8; -*-
-
-'''
-Generate a Python extension module that exports macros from
-/usr/include/linux/input.h
-'''
-
-import os, sys, re
-
-
-template = r'''
-#include
-#include
-
-/* Automatically generated by evdev.genecodes */
-/* Generated on %s */
-
-#define MODULE_NAME "_ecodes"
-#define MODULE_HELP "linux/input.h macros"
-
-static PyMethodDef MethodTable[] = {
- { NULL, NULL, 0, NULL}
-};
-
-#if PY_MAJOR_VERSION >= 3
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- MODULE_NAME,
- MODULE_HELP,
- -1, /* m_size */
- MethodTable, /* m_methods */
- NULL, /* m_reload */
- NULL, /* m_traverse */
- NULL, /* m_clear */
- NULL, /* m_free */
-};
-#endif
-
-static PyObject *
-moduleinit(void)
-{
-
-#if PY_MAJOR_VERSION >= 3
- PyObject* m = PyModule_Create(&moduledef);
-#else
- PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP);
-#endif
-
- if (m == NULL) return NULL;
-
-%s
-
- return m;
-}
-
-#if PY_MAJOR_VERSION >= 3
-PyMODINIT_FUNC
-PyInit__ecodes(void)
-{
- return moduleinit();
-}
-#else
-PyMODINIT_FUNC
-init_ecodes(void)
-{
- moduleinit();
-}
-#endif
-'''
-
-header = '/usr/include/linux/input.h' if len(sys.argv) == 1 else sys.argv[1]
-regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_\w+)'
-regex = re.compile(regex)
-
-if not os.path.exists(header):
- print('no such file: %s' % header)
- sys.exit(1)
-
-def getmacros():
- for line in open(header):
- macro = regex.search(line)
- if macro:
- yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1)
-
-uname = list(os.uname()); del uname[1]
-uname = ' '.join(uname)
-
-macros = os.linesep.join(getmacros())
-print(template % (uname, macros))
diff --git a/evdev/uinput.c b/evdev/uinput.c
deleted file mode 100644
index c885ed7..0000000
--- a/evdev/uinput.c
+++ /dev/null
@@ -1,251 +0,0 @@
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-
-
-int _uinput_close(int fd)
-{
- if (ioctl(fd, UI_DEV_DESTROY) < 0) {
- int oerrno = errno;
- close(fd);
- errno = oerrno;
- return -1;
- }
-
- return close(fd);
-}
-
-
-static PyObject *
-uinput_open(PyObject *self, PyObject *args)
-{
- const char* devnode;
-
- int ret = PyArg_ParseTuple(args, "s", &devnode);
- if (!ret) return NULL;
-
- int fd = open(devnode, O_WRONLY | O_NONBLOCK);
- if (fd < 0) {
- PyErr_SetString(PyExc_IOError, "could not open uinput device in write mode");
- return NULL;
- }
-
- return Py_BuildValue("i", fd);
-}
-
-
-static PyObject *
-uinput_create(PyObject *self, PyObject *args) {
- int fd, len, i, abscode;
- __u16 vendor, product, version, bustype;
-
- PyObject *absinfo = NULL, *item = NULL;
-
- struct uinput_user_dev uidev;
- const char* name;
-
- int ret = PyArg_ParseTuple(args, "ishhhhO", &fd, &name, &vendor,
- &product, &version, &bustype, &absinfo);
- if (!ret) return NULL;
-
- memset(&uidev, 0, sizeof(uidev));
- strncpy(uidev.name, name, UINPUT_MAX_NAME_SIZE);
- uidev.id.vendor = vendor;
- uidev.id.product = product;
- uidev.id.version = version;
- uidev.id.bustype = bustype;
-
- len = PyList_Size(absinfo);
- for (i=0; i (ABS_X, 0, 255, 0, 0)
- item = PyList_GetItem(absinfo, i);
- abscode = (int)PyLong_AsLong(PyList_GetItem(item, 0));
-
- uidev.absmin[abscode] = PyLong_AsLong(PyList_GetItem(item, 1));
- uidev.absmax[abscode] = PyLong_AsLong(PyList_GetItem(item, 2));
- uidev.absfuzz[abscode] = PyLong_AsLong(PyList_GetItem(item, 3));
- uidev.absflat[abscode] = PyLong_AsLong(PyList_GetItem(item, 4));
- }
-
- if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev))
- goto on_err;
-
- /* if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) */
- /* goto on_err; */
- /* int i; */
- /* for (i=0; i= 3
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- MODULE_NAME,
- MODULE_HELP,
- -1, /* m_size */
- MethodTable, /* m_methods */
- NULL, /* m_reload */
- NULL, /* m_traverse */
- NULL, /* m_clear */
- NULL, /* m_free */
-};
-
-static PyObject *
-moduleinit(void)
-{
- PyObject* m = PyModule_Create(&moduledef);
- if (m == NULL) return NULL;
-
- PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE);
- return m;
-}
-
-PyMODINIT_FUNC
-PyInit__uinput(void)
-{
- return moduleinit();
-}
-
-#else
-static PyObject *
-moduleinit(void)
-{
- PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP);
- if (m == NULL) return NULL;
-
- PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE);
- return m;
-}
-
-PyMODINIT_FUNC
-init_uinput(void)
-{
- moduleinit();
-}
-#endif
diff --git a/evdev/uinput.py b/evdev/uinput.py
deleted file mode 100644
index 30c3a06..0000000
--- a/evdev/uinput.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# encoding: utf-8
-
-import os
-import stat
-import time
-
-from evdev import _uinput
-from evdev import ecodes, util, device
-
-
-class UInputError(Exception):
- pass
-
-
-class UInput(object):
- '''
- A userland input device and that can inject input events into the
- linux input subsystem.
- '''
-
- __slots__ = (
- 'name', 'vendor', 'product', 'version', 'bustype',
- 'events', 'devnode', 'fd', 'device',
- )
-
- def __init__(self,
- events=None,
- name='py-evdev-uinput',
- vendor=0x1, product=0x1, version=0x1, bustype=0x3,
- devnode='/dev/uinput'):
- '''
- :param events: the event types and codes that the uinput
- device will be able to inject - defaults to all
- key codes.
-
- :type events: dictionary of event types mapping to lists of
- event codes.
-
- :param name: the name of the input device.
- :param vendor: vendor identifier.
- :param product: product identifier.
- :param version: version identifier.
- :param bustype: bustype identifier.
-
- .. note:: If you do not specify any events, the uinput device
- will be able to inject only ``KEY_*`` and ``BTN_*``
- event codes.
- '''
-
- self.name = name #: Uinput device name.
- self.vendor = vendor #: Device vendor identifier.
- self.product = product #: Device product identifier.
- self.version = version #: Device version identifier.
- self.bustype = bustype #: Device bustype - eg. ``BUS_USB``.
- self.devnode = devnode #: Uinput device node - eg. ``/dev/uinput/``.
-
- if not events:
- events = {ecodes.EV_KEY: ecodes.keys.keys()}
-
- # the min, max, fuzz and flat values for the absolute axis for
- # a given code
- absinfo = []
-
- self._verify()
-
- #: Write-only, non-blocking file descriptor to the uinput device node.
- self.fd = _uinput.open(devnode)
-
- # set device capabilities
- for etype, codes in events.items():
- for code in codes:
- # handle max, min, fuzz, flat
- if isinstance(code, (tuple, list, device.AbsInfo)):
- # flatten (ABS_Y, (0, 255, 0, 0)) to (ABS_Y, 0, 255, 0, 0)
- f = [code[0]]; f += code[1]
- absinfo.append(f)
- code = code[0]
-
- #:todo: a lot of unnecessary packing/unpacking
- _uinput.enable(self.fd, etype, code)
-
- # create uinput device
- _uinput.create(self.fd, name, vendor, product, version, bustype, absinfo)
-
- #: An :class:`InputDevice ` instance
- #: for the fake input device. ``None`` if the device cannot be
- #: opened for reading and writing.
- self.device = self._find_device()
-
- def __enter__(self):
- return self
-
- def __exit__(self, type, value, tb):
- if hasattr(self, 'fd'):
- self.close()
-
- def __repr__(self):
- # :todo:
- v = (repr(getattr(self, i)) for i in
- ('name', 'bustype', 'vendor', 'product', 'version'))
- return '{}({})'.format(self.__class__.__name__, ', '.join(v))
-
- def __str__(self):
- msg = ('name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}"\n'
- 'event types: {}')
-
- evtypes = [i[0] for i in self.capabilities(True).keys()]
- msg = msg.format(self.name, ecodes.BUS[self.bustype],
- self.vendor, self.product,
- self.version, ' '.join(evtypes))
-
- return msg
-
- def close(self):
- # close the associated InputDevice, if it was previously opened
- if self.device is not None:
- self.device.close()
-
- # destroy the uinput device
- if self.fd > -1:
- _uinput.close(self.fd)
- self.fd = -1
-
- def write_event(self, event):
- '''
- Inject an input event into the input subsystem. Events are
- queued until a synchronization event is received.
-
- :param event: InputEvent instance or an object with an
- ``event`` attribute (:class:`KeyEvent
- `, :class:`RelEvent
- ` etc).
-
- Example::
-
- ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1)
- ui.write_event(ev)
- '''
-
- if hasattr(event, 'event'):
- event = event.event
-
- self.write(event.type, event.code, event.value)
-
- def write(self, etype, code, value):
- '''
- Inject an input event into the input subsystem. Events are
- queued until a synchronization event is received.
-
- :param etype: event type (eg. ``EV_KEY``).
- :param code: event code (eg. ``KEY_A``).
- :param value: event value (eg. 0 1 2 - depends on event type).
-
- Example::
-
- ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down
- ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up
- '''
-
- _uinput.write(self.fd, etype, code, value)
-
- def syn(self):
- '''
- Inject a ``SYN_REPORT`` event into the input subsystem. Events
- queued by :func:`write()` will be fired. If possible, events
- will be merged into an 'atomic' event.
- '''
-
- _uinput.write(self.fd, ecodes.EV_SYN, ecodes.SYN_REPORT, 0)
-
- def capabilities(self, verbose=False, absinfo=True):
- '''See :func:`capabilities `.'''
- if self.device is None:
- raise UInputError('input device not opened - cannot read capabilites')
-
- return self.device.capabilities(verbose, absinfo)
-
- def _verify(self):
- '''
- Verify that an uinput device exists and is readable and writable
- by the current process.
- '''
-
- try:
- m = os.stat(self.devnode)[stat.ST_MODE]
- if not stat.S_ISCHR(m):
- raise
- except (IndexError, OSError):
- msg = '"{}" does not exist or is not a character device file '\
- '- verify that the uinput module is loaded'
- raise UInputError(msg.format(self.devnode))
-
- if not os.access(self.devnode, os.W_OK):
- msg = '"{}" cannot be opened for writing'
- raise UInputError(msg.format(self.devnode))
-
- if len(self.name) > _uinput.maxnamelen:
- msg = 'uinput device name must not be longer than {} characters'
- raise UInputError(msg.format(_uinput.maxnamelen))
-
- def _find_device(self):
- #:bug: the device node might not be immediately available
- time.sleep(0.1)
-
- for fn in util.list_devices('/dev/input/'):
- d = device.InputDevice(fn)
- if d.name == self.name:
- return d
diff --git a/evdev/util.py b/evdev/util.py
deleted file mode 100644
index ccc6b58..0000000
--- a/evdev/util.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# encoding: utf-8
-
-import os
-import stat
-import glob
-
-from evdev import ecodes
-from evdev.events import event_factory
-
-
-def list_devices(input_device_dir='/dev/input'):
- '''List readable, character devices.'''
-
- fns = glob.glob('{}/event*'.format(input_device_dir))
- fns = list(filter(is_device, fns))
-
- return fns
-
-
-def is_device(fn):
- '''Check if ``fn`` is a readable and writable character device.'''
-
- if not os.path.exists(fn):
- return False
-
- m = os.stat(fn)[stat.ST_MODE]
- if not stat.S_ISCHR(m):
- return False
-
- if not os.access(fn, os.R_OK | os.W_OK):
- return False
-
- return True
-
-
-def categorize(event):
- '''
- Categorize an event according to its type.
-
- The :data:`event_factory ` dictionary
- maps event types to sub-classes of :class:`InputEvent
- `. If there is no corresponding key, the
- event is returned as it is.
- '''
-
- if event.type in event_factory:
- return event_factory[event.type](event)
- else:
- return event
-
-
-def resolve_ecodes(typecodemap, unknown='?'):
- '''
- Resolve event codes and types to their verbose names.
-
- :param typecodemap: mapping of event types to lists of event codes.
- :param unknown: symbol to which unknown types or codes will be resolved.
-
- Example::
-
- resolve_ecodes({ 1: [272, 273, 274] })
- { ('EV_KEY', 1): [('BTN_MOUSE', 272),
- ('BTN_RIGHT', 273),
- ('BTN_MIDDLE', 274)] }
-
- If typecodemap contains absolute axis info (instances of
- :class:`AbsInfo ` ) the result would look
- like::
-
- resolve_ecodes({ 3: [(0, AbsInfo(...))] })
- { ('EV_ABS', 3L): [(('ABS_X', 0L), AbsInfo(...))] }
- '''
-
- for etype, codes in typecodemap.items():
- type_name = ecodes.EV[etype]
-
- # ecodes.keys are a combination of KEY_ and BTN_ codes
- if etype == ecodes.EV_KEY:
- code_names = ecodes.keys
- else:
- code_names = getattr(ecodes, type_name.split('_')[-1])
-
- res = []
- for i in codes:
- # elements with AbsInfo(), eg { 3 : [(0, AbsInfo(...)), (1, AbsInfo(...))] }
- if isinstance(i, tuple):
- l = ((code_names[i[0]], i[0]), i[1]) if i[0] in code_names \
- else ((unknown, i[0]), i[1])
-
- # just ecodes { 0 : [0, 1, 3], 1 : [30, 48] }
- else:
- l = (code_names[i], i) if i in code_names else (unknown, i)
-
- res.append(l)
-
- yield (type_name, etype), res
-
-
-__all__ = ('list_devices', 'is_device', 'categorize', 'resolve_ecodes')
diff --git a/bin/udev-example.py b/examples/udev-example.py
similarity index 78%
rename from bin/udev-example.py
rename to examples/udev-example.py
index 12a617c..8e827f6 100755
--- a/bin/udev-example.py
+++ b/examples/udev-example.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
-'''
+"""
This is an example of using pyudev[1] alongside evdev.
[1]: https://pyudev.readthedocs.org/
-'''
+"""
import functools
import pyudev
@@ -13,7 +13,7 @@
context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
-monitor.filter_by(subsystem='input')
+monitor.filter_by(subsystem="input")
monitor.start()
fds = {monitor.fileno(): monitor}
@@ -32,16 +32,16 @@
break
# find the device we're interested in and add it to fds
- for name in (i['NAME'] for i in udev.ancestors if 'NAME' in i):
+ for name in (i["NAME"] for i in udev.ancestors if "NAME" in i):
# I used a virtual input device for this test - you
# should adapt this to your needs
- if u'py-evdev-uinput' in name:
- if udev.action == u'add':
- print('Device added: %s' % udev)
+ if "py-evdev-uinput" in name:
+ if udev.action == "add":
+ print("Device added: %s" % udev)
fds[dev.fd] = InputDevice(udev.device_node)
break
- if udev.action == u'remove':
- print('Device removed: %s' % udev)
+ if udev.action == "remove":
+ print("Device removed: %s" % udev)
def helper():
global fds
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d0b4f7c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,56 @@
+[build-system]
+requires = ["setuptools>=77.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "evdev"
+version = "1.9.3"
+description = "Bindings to the Linux input handling subsystem"
+keywords = ["evdev", "input", "uinput"]
+readme = "README.md"
+license = "BSD-3-Clause"
+requires-python = ">=3.9"
+authors = [
+ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" },
+]
+maintainers = [
+ { name="Tobi", email="proxima@sezanzeb.de" },
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Programming Language :: Python :: 3",
+ "Operating System :: POSIX :: Linux",
+ "Intended Audience :: Developers",
+ "Topic :: Software Development :: Libraries",
+ "Programming Language :: Python :: Implementation :: CPython",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/gvalkov/python-evdev"
+
+[tool.ruff]
+line-length = 120
+
+[tool.ruff.lint]
+ignore = ["E265", "E241", "F403", "F401", "E401", "E731"]
+
+[tool.bumpversion]
+current_version = "1.9.3"
+commit = true
+tag = true
+allow_dirty = true
+
+[[tool.bumpversion.files]]
+filename = "pyproject.toml"
+
+[[tool.bumpversion.files]]
+filename = "docs/conf.py"
+
+[tool.pylint.'MESSAGES CONTROL']
+disable = """
+ no-member,
+"""
+
+[tool.pylint.typecheck]
+generated-members = ["evdev.ecodes.*"]
+ignored-modules= ["evdev._*"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..725ad7f
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,10 @@
+pytest
+Sphinx
+sphinx-copybutton ~= 0.5.2
+sphinx-rtd-theme
+ruff
+bump-my-version ~= 0.17.4
+build
+twine
+cibuildwheel
+setuptools
diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh
new file mode 100755
index 0000000..bbdae6c
--- /dev/null
+++ b/scripts/build-binary.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -o allexport
+set -o nounset
+
+CIBW_MANYLINUX_X86_64_IMAGE="manylinux_2_28"
+CIBW_MANYLINUX_I686_IMAGE="manylinux_2_28"
+CIBW_CONTAINER_ENGINE="podman"
+CIBW_SKIP="cp36-*"
+CIBW_ARCHS_LINUX="auto64"
+CIBW_BEFORE_ALL_LINUX=./scripts/cibw-before.sh
+CIBW_TEST_COMMAND="python -c 'import evdev; print(evdev)'"
+CIBW_ENVIRONMENT="PACKAGE_NAME=evdev-binary"
+
+exec cibuildwheel
\ No newline at end of file
diff --git a/scripts/cibw-before.sh b/scripts/cibw-before.sh
new file mode 100755
index 0000000..25220d4
--- /dev/null
+++ b/scripts/cibw-before.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+
+if [ -n "$PACKAGE_NAME" ]; then
+ sed -i -re 's,^(name = ")evdev("),\1'${PACKAGE_NAME}'\2,' pyproject.toml
+fi
\ No newline at end of file
diff --git a/setup.py b/setup.py
index c153d78..1f6eaac 100755
--- a/setup.py
+++ b/setup.py
@@ -1,123 +1,135 @@
-#!/usr/bin/env python
-# encoding: utf-8
-
import os
import sys
+import shutil
import textwrap
+import platform
+from pathlib import Path
+from subprocess import run
-from os.path import abspath, dirname, join as pjoin
-from distutils.command.build import build
-from setuptools.command.develop import develop
-from setuptools.command.bdist_egg import bdist_egg
from setuptools import setup, Extension, Command
+from setuptools.command import build_ext as _build_ext
-here = abspath(dirname(__file__))
-
-classifiers = (
- 'Development Status :: 5 - Production/Stable',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
- 'Operating System :: POSIX :: Linux',
- 'Intended Audience :: Developers',
- 'Topic :: Software Development :: Libraries',
- 'License :: OSI Approved :: BSD License',
- 'Programming Language :: Python :: Implementation :: CPython',
-)
-
-cflags = ['-std=c99']
-input_c = Extension('evdev._input', sources=['evdev/input.c'], extra_compile_args=cflags)
-uinput_c = Extension('evdev._uinput', sources=['evdev/uinput.c'], extra_compile_args=cflags)
-ecodes_c = Extension('evdev._ecodes', sources=['evdev/ecodes.c'], extra_compile_args=cflags)
+curdir = Path(__file__).resolve().parent
+ecodes_c_path = curdir / "src/evdev/ecodes.c"
-kw = {
- 'name': 'evdev',
- 'version': '0.4.4',
- 'description': 'Bindings for the linux input handling subsystem',
- 'long_description': open(pjoin(here, 'README.rst')).read(),
+def create_ecodes(headers=None, reproducible=False):
+ if not headers:
+ include_paths = set()
+ cpath = os.environ.get("CPATH", "").strip()
+ c_inc_path = os.environ.get("C_INCLUDE_PATH", "").strip()
- 'author': 'Georgi Valkov',
- 'author_email': 'georgi.t.valkov@gmail.com',
- 'license': 'Revised BSD License',
+ if cpath:
+ include_paths.update(cpath.split(":"))
+ if c_inc_path:
+ include_paths.update(c_inc_path.split(":"))
- 'keywords': 'evdev input uinput',
- 'classifiers': classifiers,
- 'url': 'https://github.com/gvalkov/python-evdev',
+ include_paths.add("/usr/include")
+ if platform.system().lower() == "freebsd":
+ files = ["dev/evdev/input.h", "dev/evdev/input-event-codes.h", "dev/evdev/uinput.h"]
+ else:
+ files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"]
- 'packages': ['evdev'],
- 'ext_modules': [input_c, uinput_c, ecodes_c],
- 'tests_require': ['pytest'],
+ headers = [os.path.join(path, file) for path in include_paths for file in files]
- 'include_package_data': False,
- 'zip_safe': True,
- 'cmdclass': {},
-}
+ headers = [header for header in headers if os.path.isfile(header)]
+ if not headers:
+ msg = """\
+ The 'linux/input.h' and 'linux/input-event-codes.h' include files
+ are missing. You will have to install the kernel header files in
+ order to continue:
+ dnf install kernel-headers-$(uname -r)
+ apt-get install linux-headers-$(uname -r)
+ emerge sys-kernel/linux-headers
+ pacman -S kernel-headers
-def create_ecodes():
- # :todo: expose as a command option
- header = '/usr/include/linux/input.h'
+ In case they are installed in a non-standard location, you may use
+ the '--evdev-headers' option to specify one or more colon-separated
+ paths. For example:
- if not os.path.isfile(header):
- msg = '''\
- The linux/input.h header file is missing. You will have to
- install the headers for your kernel in order to continue:
+ python setup.py \\
+ build \\
+ build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \\
+ build_ext --include-dirs path/ \\
+ install
- yum install kernel-headers-$(uname -r)
- apt-get intall linux-headers-$(uname -r)
- pacman -S kernel-headers\n\n'''
+ If you want to avoid building this package from source, then please consider
+ installing the `evdev-binary` package instead. Keep in mind that it may not be
+ fully compatible with, or support all the features of your current kernel.
+ """
sys.stderr.write(textwrap.dedent(msg))
sys.exit(1)
- from subprocess import check_call
+ print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers)))
+ with ecodes_c_path.open("w") as fh:
+ cmd = [sys.executable, "src/evdev/genecodes_c.py"]
+ if reproducible:
+ cmd.append("--reproducible")
+ cmd.extend(["--ecodes", *headers])
+ run(cmd, check=True, stdout=fh)
- print('writing ecodes.c (using %s)' % header)
- cmd = '%s genecodes.py %s > ecodes.c' % (sys.executable, header)
- check_call(cmd, cwd="%s/evdev" % here, shell=True)
+class build_ecodes(Command):
+ description = "generate ecodes.c"
-# :todo: figure out a smarter way to do this
-# :note: subclassing build_ext doesn't really cut it
-class BuildCommand(build):
- def run(self):
- create_ecodes()
- build.run(self)
-
-class DevelopCommand(develop):
- def run(self):
- create_ecodes()
- develop.run(self)
+ user_options = [
+ ("evdev-headers=", None, "colon-separated paths to input subsystem headers"),
+ ("reproducible", None, "hide host details (host/paths) to create a reproducible output"),
+ ]
-class BdistEggCommand(bdist_egg):
- def run(self):
- create_ecodes()
- bdist_egg.run(self)
-
-
-class PyTest(Command):
- '''setup.py test -> py.test tests'''
-
- user_options = []
def initialize_options(self):
- pass
+ self.evdev_headers = None
+ self.reproducible = False
def finalize_options(self):
- pass
+ if self.evdev_headers:
+ self.evdev_headers = self.evdev_headers.split(":")
+ if self.reproducible is None:
+ self.reproducible = False
def run(self):
- from subprocess import call
- errno = call(('py.test', 'tests'))
- raise SystemExit(errno)
+ create_ecodes(self.evdev_headers, reproducible=self.reproducible)
+
+class build_ext(_build_ext.build_ext):
+ def has_ecodes(self):
+ if ecodes_c_path.exists():
+ print("ecodes.c already exists ... skipping build_ecodes")
+ return False
+ return True
-kw['cmdclass']['test'] = PyTest
-kw['cmdclass']['build'] = BuildCommand
-kw['cmdclass']['develop'] = DevelopCommand
-kw['cmdclass']['bdist_egg'] = BdistEggCommand
+ def generate_ecodes_py(self):
+ ecodes_py = Path(self.build_lib) / "evdev/ecodes.py"
+ print(f"writing {ecodes_py}")
+ with ecodes_py.open("w") as fh:
+ cmd = [sys.executable, "-B", "src/evdev/genecodes_py.py"]
+ res = run(cmd, env={"PYTHONPATH": self.build_lib}, stdout=fh)
-if __name__ == '__main__':
- setup(**kw)
+ if res.returncode != 0:
+ print(f"failed to generate static {ecodes_py} - will use ecodes_runtime.py")
+ shutil.copy("src/evdev/ecodes_runtime.py", ecodes_py)
+
+ def run(self):
+ for cmd_name in self.get_sub_commands():
+ self.run_command(cmd_name)
+ _build_ext.build_ext.run(self)
+ self.generate_ecodes_py()
+
+ sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands
+
+
+cflags = ["-std=c99", "-Wno-error=declaration-after-statement"]
+setup(
+ ext_modules=[
+ Extension("evdev._input", sources=["src/evdev/input.c"], extra_compile_args=cflags),
+ Extension("evdev._uinput", sources=["src/evdev/uinput.c"], extra_compile_args=cflags),
+ Extension("evdev._ecodes", sources=["src/evdev/ecodes.c"], extra_compile_args=cflags),
+ ],
+ cmdclass={
+ "build_ext": build_ext,
+ "build_ecodes": build_ecodes,
+ },
+)
diff --git a/src/evdev/__init__.py b/src/evdev/__init__.py
new file mode 100644
index 0000000..bae0fec
--- /dev/null
+++ b/src/evdev/__init__.py
@@ -0,0 +1,39 @@
+# --------------------------------------------------------------------------
+# Gather everything into a single, convenient namespace.
+# --------------------------------------------------------------------------
+
+# The superfluous "import name as name" syntax is here to satisfy mypy's attrs-defined rule.
+# Alternatively all exported objects can be listed in __all__.
+
+from . import (
+ ecodes as ecodes,
+ ff as ff,
+)
+
+from .device import (
+ AbsInfo as AbsInfo,
+ DeviceInfo as DeviceInfo,
+ EvdevError as EvdevError,
+ InputDevice as InputDevice,
+)
+
+from .events import (
+ AbsEvent as AbsEvent,
+ InputEvent as InputEvent,
+ KeyEvent as KeyEvent,
+ RelEvent as RelEvent,
+ SynEvent as SynEvent,
+ event_factory as event_factory,
+)
+
+from .uinput import (
+ UInput as UInput,
+ UInputError as UInputError,
+)
+
+from .util import (
+ categorize as categorize,
+ list_devices as list_devices,
+ resolve_ecodes as resolve_ecodes,
+ resolve_ecodes_dict as resolve_ecodes_dict,
+)
diff --git a/src/evdev/device.py b/src/evdev/device.py
new file mode 100644
index 0000000..a7f9b92
--- /dev/null
+++ b/src/evdev/device.py
@@ -0,0 +1,440 @@
+import contextlib
+import os
+from typing import Dict, Generic, Iterator, List, Literal, NamedTuple, Tuple, TypeVar, Union, overload
+
+from . import _input, ecodes, util
+
+try:
+ from .eventio_async import EvdevError, EventIO
+except ImportError:
+ from .eventio import EvdevError, EventIO
+
+_AnyStr = TypeVar("_AnyStr", str, bytes)
+
+
+class AbsInfo(NamedTuple):
+ """Absolute axis information.
+
+ A ``namedtuple`` with absolute axis information -
+ corresponds to the ``input_absinfo`` struct:
+
+ Attributes
+ ---------
+ value
+ Latest reported value for the axis.
+
+ min
+ Specifies minimum value for the axis.
+
+ max
+ Specifies maximum value for the axis.
+
+ fuzz
+ Specifies fuzz value that is used to filter noise from the
+ event stream.
+
+ flat
+ Values that are within this value will be discarded by joydev
+ interface and reported as 0 instead.
+
+ resolution
+ Specifies resolution for the values reported for the axis.
+ Resolution for main axes (``ABS_X, ABS_Y, ABS_Z``) is reported
+ in units per millimeter (units/mm), resolution for rotational
+ axes (``ABS_RX, ABS_RY, ABS_RZ``) is reported in units per
+ radian.
+
+ Note
+ ----
+ The input core does not clamp reported values to the ``[minimum,
+ maximum]`` limits, such task is left to userspace.
+
+ """
+
+ value: int
+ min: int
+ max: int
+ fuzz: int
+ flat: int
+ resolution: int
+
+ def __str__(self):
+ return "value {}, min {}, max {}, fuzz {}, flat {}, res {}".format(*self) # pylint: disable=not-an-iterable
+
+
+class KbdInfo(NamedTuple):
+ """Keyboard repeat rate.
+
+ Attributes
+ ----------
+ delay
+ Amount of time that a key must be depressed before it will start
+ to repeat (in milliseconds).
+
+ repeat
+ Keyboard repeat rate in characters per second.
+ """
+
+ delay: int
+ repeat: int
+
+ def __str__(self):
+ return "delay {}, repeat {}".format(self.delay, self.repeat)
+
+
+class DeviceInfo(NamedTuple):
+ """
+ Attributes
+ ----------
+ bustype
+ vendor
+ product
+ version
+ """
+
+ bustype: int
+ vendor: int
+ product: int
+ version: int
+
+ def __str__(self) -> str:
+ msg = "bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}"
+ return msg.format(*self) # pylint: disable=not-an-iterable
+
+
+class InputDevice(EventIO, Generic[_AnyStr]):
+ """
+ A linux input device from which input events can be read.
+ """
+
+ __slots__ = ("path", "fd", "info", "name", "phys", "uniq", "_rawcapabilities", "version", "ff_effects_count")
+
+ def __init__(self, dev: Union[_AnyStr, "os.PathLike[_AnyStr]"]):
+ """
+ Arguments
+ ---------
+ dev : str|bytes|PathLike
+ Path to input device
+ """
+
+ #: Path to input device.
+ self.path: _AnyStr = dev if not hasattr(dev, "__fspath__") else dev.__fspath__()
+
+ # Certain operations are possible only when the device is opened in read-write mode.
+ try:
+ fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK)
+ except OSError:
+ fd = os.open(dev, os.O_RDONLY | os.O_NONBLOCK)
+
+ #: A non-blocking file descriptor to the device file.
+ self.fd: int = fd
+
+ # Returns (bustype, vendor, product, version, name, phys, capabilities).
+ info_res = _input.ioctl_devinfo(self.fd)
+
+ #: A :class:`DeviceInfo ` instance.
+ self.info = DeviceInfo(*info_res[:4])
+
+ #: The name of the event device.
+ self.name: str = info_res[4]
+
+ #: The physical topology of the device.
+ self.phys: str = info_res[5]
+
+ #: The unique identifier of the device.
+ self.uniq: str = info_res[6]
+
+ #: The evdev protocol version.
+ self.version: int = _input.ioctl_EVIOCGVERSION(self.fd)
+
+ #: The raw dictionary of device capabilities - see `:func:capabilities()`.
+ self._rawcapabilities = _input.ioctl_capabilities(self.fd)
+
+ #: The number of force feedback effects the device can keep in its memory.
+ self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd)
+
+ def __del__(self) -> None:
+ if hasattr(self, "fd") and self.fd is not None:
+ try:
+ self.close()
+ except (OSError, ImportError, AttributeError):
+ pass
+
+ def _capabilities(self, absinfo: bool = True):
+ res = {}
+
+ for etype, _ecodes in self._rawcapabilities.items():
+ for code in _ecodes:
+ l = res.setdefault(etype, [])
+ if isinstance(code, tuple):
+ if absinfo:
+ a = code[1] # (0, 0, 0, 255, 0, 0)
+ i = AbsInfo(*a)
+ l.append((code[0], i))
+ else:
+ l.append(code[0])
+ else:
+ l.append(code)
+
+ return res
+
+ @overload
+ def capabilities(self, verbose: Literal[False] = ..., absinfo: bool = ...) -> Dict[int, List[int]]:
+ ...
+ @overload
+ def capabilities(self, verbose: Literal[True], absinfo: bool = ...) -> Dict[Tuple[str, int], List[Tuple[str, int]]]:
+ ...
+ def capabilities(self, verbose: bool = False, absinfo: bool = True) -> Union[Dict[int, List[int]], Dict[Tuple[str, int], List[Tuple[str, int]]]]:
+ """
+ Return the event types that this device supports as a mapping of
+ supported event types to lists of handled event codes.
+
+ Example
+ --------
+ >>> device.capabilities()
+ { 1: [272, 273, 274],
+ 2: [0, 1, 6, 8] }
+
+ If ``verbose`` is ``True``, event codes and types will be resolved
+ to their names.
+
+ ::
+
+ { ('EV_KEY', 1): [('BTN_MOUSE', 272),
+ ('BTN_RIGHT', 273),
+ ('BTN_MIDDLE', 273)],
+ ('EV_REL', 2): [('REL_X', 0),
+ ('REL_Y', 1),
+ ('REL_HWHEEL', 6),
+ ('REL_WHEEL', 8)] }
+
+ Unknown codes or types will be resolved to ``'?'``.
+
+ If ``absinfo`` is ``True``, the list of capabilities will also
+ include absolute axis information in the form of
+ :class:`AbsInfo` instances::
+
+ { 3: [ (0, AbsInfo(min=0, max=255, fuzz=0, flat=0)),
+ (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]}
+
+ Combined with ``verbose`` the above becomes::
+
+ { ('EV_ABS', 3): [ (('ABS_X', 0), AbsInfo(min=0, max=255, fuzz=0, flat=0)),
+ (('ABS_Y', 1), AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]}
+
+ """
+
+ if verbose:
+ return dict(util.resolve_ecodes_dict(self._capabilities(absinfo)))
+ else:
+ return self._capabilities(absinfo)
+
+ def input_props(self, verbose: bool = False):
+ """
+ Get device properties and quirks.
+
+ Example
+ -------
+ >>> device.input_props()
+ [0, 5]
+
+ If ``verbose`` is ``True``, input properties are resolved to their
+ names. Unknown codes are resolved to ``'?'``::
+
+ [('INPUT_PROP_POINTER', 0), ('INPUT_PROP_POINTING_STICK', 5)]
+
+ """
+ props = _input.ioctl_EVIOCGPROP(self.fd)
+ if verbose:
+ return util.resolve_ecodes(ecodes.INPUT_PROP, props)
+
+ return props
+
+ def leds(self, verbose: bool = False):
+ """
+ Return currently set LED keys.
+
+ Example
+ -------
+ >>> device.leds()
+ [0, 1, 8, 9]
+
+ If ``verbose`` is ``True``, event codes are resolved to their
+ names. Unknown codes are resolved to ``'?'``::
+
+ [('LED_NUML', 0), ('LED_CAPSL', 1), ('LED_MISC', 8), ('LED_MAIL', 9)]
+
+ """
+ leds = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_LED)
+ if verbose:
+ return util.resolve_ecodes(ecodes.LED, leds)
+
+ return leds
+
+ def set_led(self, led_num: int, value: int) -> None:
+ """
+ Set the state of the selected LED.
+
+ Example
+ -------
+ >>> device.set_led(ecodes.LED_NUML, 1)
+ """
+ self.write(ecodes.EV_LED, led_num, value)
+
+ def __eq__(self, other):
+ """
+ Two devices are equal if their :data:`info` attributes are equal.
+ """
+ return isinstance(other, self.__class__) and self.info == other.info and self.path == other.path
+
+ def __str__(self) -> str:
+ msg = 'device {}, name "{}", phys "{}", uniq "{}"'
+ return msg.format(self.path, self.name, self.phys, self.uniq or "")
+
+ def __repr__(self) -> str:
+ msg = (self.__class__.__name__, self.path)
+ return "{}({!r})".format(*msg)
+
+ def __fspath__(self):
+ return self.path
+
+ def close(self) -> None:
+ if self.fd > -1:
+ try:
+ super().close()
+ os.close(self.fd)
+ finally:
+ self.fd = -1
+
+ def grab(self) -> None:
+ """
+ Grab input device using ``EVIOCGRAB`` - other applications will
+ be unable to receive events until the device is released. Only
+ one process can hold a ``EVIOCGRAB`` on a device.
+
+ Warning
+ -------
+ Grabbing an already grabbed device will raise an ``OSError``.
+ """
+
+ _input.ioctl_EVIOCGRAB(self.fd, 1)
+
+ def ungrab(self) -> None:
+ """
+ Release device if it has been already grabbed (uses `EVIOCGRAB`).
+
+ Warning
+ -------
+ Releasing an already released device will raise an
+ ``OSError('Invalid argument')``.
+ """
+
+ _input.ioctl_EVIOCGRAB(self.fd, 0)
+
+ @contextlib.contextmanager
+ def grab_context(self) -> Iterator[None]:
+ """
+ A context manager for the duration of which only the current
+ process will be able to receive events from the device.
+ """
+ self.grab()
+ yield
+ self.ungrab()
+
+ def upload_effect(self, effect: "ff.Effect"):
+ """
+ Upload a force feedback effect to a force feedback device.
+ """
+
+ data = memoryview(effect).tobytes()
+ ff_id = _input.upload_effect(self.fd, data)
+ return ff_id
+
+ def erase_effect(self, ff_id) -> None:
+ """
+ Erase a force effect from a force feedback device. This also
+ stops the effect.
+ """
+
+ _input.erase_effect(self.fd, ff_id)
+
+ @property
+ def repeat(self):
+ """
+ Get or set the keyboard repeat rate (in characters per
+ minute) and delay (in milliseconds).
+ """
+
+ return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd))
+
+ @repeat.setter
+ def repeat(self, value: Tuple[int, int]):
+ return _input.ioctl_EVIOCSREP(self.fd, *value)
+
+ def active_keys(self, verbose: bool = False):
+ """
+ Return currently active keys.
+
+ Example
+ -------
+
+ >>> device.active_keys()
+ [1, 42]
+
+ If ``verbose`` is ``True``, key codes are resolved to their
+ verbose names. Unknown codes are resolved to ``'?'``. For
+ example::
+
+ [('KEY_ESC', 1), ('KEY_LEFTSHIFT', 42)]
+
+ """
+ active_keys = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_KEY)
+ if verbose:
+ return util.resolve_ecodes(ecodes.KEY, active_keys)
+
+ return active_keys
+
+ def absinfo(self, axis_num: int):
+ """
+ Return current :class:`AbsInfo` for input device axis
+
+ Arguments
+ ---------
+ axis_num : int
+ EV_ABS keycode (example :attr:`ecodes.ABS_X`)
+
+ Example
+ -------
+ >>> device.absinfo(ecodes.ABS_X)
+ AbsInfo(value=1501, min=-32768, max=32767, fuzz=0, flat=128, resolution=0)
+ """
+ return AbsInfo(*_input.ioctl_EVIOCGABS(self.fd, axis_num))
+
+ def set_absinfo(self, axis_num: int, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None) -> None:
+ """
+ Update :class:`AbsInfo` values. Only specified values will be overwritten.
+
+ Arguments
+ ---------
+ axis_num : int
+ EV_ABS keycode (example :attr:`ecodes.ABS_X`)
+
+ Example
+ -------
+ >>> device.set_absinfo(ecodes.ABS_X, min=-2000, max=2000)
+
+ You can also unpack AbsInfo tuple that will overwrite all values
+
+ >>> device.set_absinfo(ecodes.ABS_Y, *AbsInfo(0, -2000, 2000, 0, 15, 0))
+ """
+
+ cur_absinfo = self.absinfo(axis_num)
+ new_absinfo = AbsInfo(
+ value if value is not None else cur_absinfo.value,
+ min if min is not None else cur_absinfo.min,
+ max if max is not None else cur_absinfo.max,
+ fuzz if fuzz is not None else cur_absinfo.fuzz,
+ flat if flat is not None else cur_absinfo.flat,
+ resolution if resolution is not None else cur_absinfo.resolution,
+ )
+ _input.ioctl_EVIOCSABS(self.fd, axis_num, new_absinfo)
diff --git a/src/evdev/ecodes.py b/src/evdev/ecodes.py
new file mode 100644
index 0000000..fd4afc4
--- /dev/null
+++ b/src/evdev/ecodes.py
@@ -0,0 +1,5 @@
+# When installed, this module is replaced by an ecodes.py generated at
+# build time by genecodes_py.py (see build_ext in setup.py).
+
+# This stub exists to make development of evdev itself more convenient.
+from .ecodes_runtime import *
diff --git a/evdev/ecodes.py b/src/evdev/ecodes_runtime.py
similarity index 68%
rename from evdev/ecodes.py
rename to src/evdev/ecodes_runtime.py
index c94b0c1..47f3b23 100644
--- a/evdev/ecodes.py
+++ b/src/evdev/ecodes_runtime.py
@@ -1,15 +1,15 @@
-# encoding: utf-8
-
-'''
-This modules exposes the integer constants defined in ``linux/input.h``.
+# pylint: disable=undefined-variable
+"""
+This modules exposes the integer constants defined in ``linux/input.h`` and
+``linux/input-event-codes.h``.
Exposed constants::
KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV,
- BUS, SYN, FF, FF_STATUS
+ BUS, SYN, FF, FF_STATUS, INPUT_PROP
-This module also provides numerous reverse and forward mappings that are best
-illustrated by a few examples::
+This module also provides reverse and forward mappings of the names and values
+of the above mentioned constants::
>>> evdev.ecodes.KEY_A
30
@@ -29,29 +29,30 @@
>>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0]
'REL_X'
-Values in reverse mappings may point to one or more ecodes. For example::
+Keep in mind that values in reverse mappings may point to one or more event
+codes. For example::
>>> evdev.ecodes.FF[80]
- ['FF_EFFECT_MIN', 'FF_RUMBLE']
+ ('FF_EFFECT_MIN', 'FF_RUMBLE')
>>> evdev.ecodes.FF[81]
'FF_PERIODIC'
-'''
+"""
from inspect import getmembers
-from evdev import _ecodes
+from . import _ecodes
#: Mapping of names to values.
ecodes = {}
-prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF'
-prev_prefix = ''
+prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP UI_FF".split()
+prev_prefix = ""
g = globals()
# eg. code: 'REL_Z', val: 2
for code, val in getmembers(_ecodes):
- for prefix in prefixes.split(): # eg. 'REL'
+ for prefix in prefixes: # eg. 'REL'
if code.startswith(prefix):
ecodes[code] = val
# FF_STATUS codes should not appear in the FF reverse mapping
@@ -70,6 +71,15 @@
prev_prefix = prefix
+
+# Convert lists to tuples.
+k, v = None, None
+for prefix in prefixes:
+ for k, v in g[prefix].items():
+ if isinstance(v, list):
+ g[prefix][k] = tuple(v)
+
+
#: Keys are a combination of all BTN and KEY codes.
keys = {}
keys.update(BTN)
@@ -85,16 +95,17 @@
_ecodes.EV_KEY: keys,
_ecodes.EV_ABS: ABS,
_ecodes.EV_REL: REL,
- _ecodes.EV_SW: SW,
+ _ecodes.EV_SW: SW,
_ecodes.EV_MSC: MSC,
_ecodes.EV_LED: LED,
_ecodes.EV_REP: REP,
_ecodes.EV_SND: SND,
_ecodes.EV_SYN: SYN,
- _ecodes.EV_FF: FF,
- _ecodes.EV_FF_STATUS: FF_STATUS, }
+ _ecodes.EV_FF: FF,
+ _ecodes.EV_FF_STATUS: FF_STATUS,
+}
from evdev._ecodes import *
# cheaper than whitelisting in an __all__
-del code, val, prefix, getmembers, g, d, prefixes, prev_prefix
+del code, val, prefix, getmembers, g, d, k, v, prefixes, prev_prefix
diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py
new file mode 100644
index 0000000..bdb91a4
--- /dev/null
+++ b/src/evdev/eventio.py
@@ -0,0 +1,152 @@
+import fcntl
+import functools
+import os
+import select
+from typing import Iterator, Union
+
+from . import _input, _uinput, ecodes
+from .events import InputEvent
+
+
+# --------------------------------------------------------------------------
+class EvdevError(Exception):
+ pass
+
+
+class EventIO:
+ """
+ Base class for reading and writing input events.
+
+ This class is used by :class:`InputDevice` and :class:`UInput`.
+
+ - On, :class:`InputDevice` it used for reading user-generated events (e.g.
+ key presses, mouse movements) and writing feedback events (e.g. leds,
+ beeps).
+
+ - On, :class:`UInput` it used for writing user-generated events (e.g.
+ key presses, mouse movements) and reading feedback events (e.g. leds,
+ beeps).
+ """
+
+ def fileno(self):
+ """
+ Return the file descriptor to the open event device. This makes
+ it possible to pass instances directly to :func:`select.select()` and
+ :class:`asyncore.file_dispatcher`.
+ """
+ return self.fd
+
+ def read_loop(self) -> Iterator[InputEvent]:
+ """
+ Enter an endless :func:`select.select()` loop that yields input events.
+ """
+
+ while True:
+ r, w, x = select.select([self.fd], [], [])
+ for event in self.read():
+ yield event
+
+ def read_one(self) -> Union[InputEvent, None]:
+ """
+ Read and return a single input event as an instance of
+ :class:`InputEvent `.
+
+ Return ``None`` if there are no pending input events.
+ """
+
+ # event -> (sec, usec, type, code, val)
+ event = _input.device_read(self.fd)
+
+ if event:
+ return InputEvent(*event)
+
+ def read(self) -> Iterator[InputEvent]:
+ """
+ Read multiple input events from device. Return a generator object that
+ yields :class:`InputEvent ` instances. Raises
+ `BlockingIOError` if there are no available events at the moment.
+ """
+
+ # events -> ((sec, usec, type, code, val), ...)
+ events = _input.device_read_many(self.fd)
+
+ for event in events:
+ yield InputEvent(*event)
+
+ # pylint: disable=no-self-argument
+ def need_write(func):
+ """
+ Decorator that raises :class:`EvdevError` if there is no write access to the
+ input device.
+ """
+
+ @functools.wraps(func)
+ def wrapper(*args):
+ fd = args[0].fd
+ if fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_RDWR:
+ # pylint: disable=not-callable
+ return func(*args)
+ msg = 'no write access to device "%s"' % args[0].path
+ raise EvdevError(msg)
+
+ return wrapper
+
+ def write_event(self, event):
+ """
+ Inject an input event into the input subsystem. Events are
+ queued until a synchronization event is received.
+
+ Arguments
+ ---------
+ event: InputEvent
+ InputEvent instance or an object with an ``event`` attribute
+ (:class:`KeyEvent `, :class:`RelEvent
+ ` etc).
+
+ Example
+ -------
+ >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1)
+ >>> ui.write_event(ev)
+ """
+
+ if hasattr(event, "event"):
+ event = event.event
+
+ self.write(event.type, event.code, event.value)
+
+ @need_write
+ def write(self, etype: int, code: int, value: int):
+ """
+ Inject an input event into the input subsystem. Events are
+ queued until a synchronization event is received.
+
+ Arguments
+ ---------
+ etype
+ event type (e.g. ``EV_KEY``).
+
+ code
+ event code (e.g. ``KEY_A``).
+
+ value
+ event value (e.g. 0 1 2 - depends on event type).
+
+ Example
+ ---------
+ >>> ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down
+ >>> ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up
+ """
+
+ _uinput.write(self.fd, etype, code, value)
+
+ def syn(self):
+ """
+ Inject a ``SYN_REPORT`` event into the input subsystem. Events
+ queued by :func:`write()` will be fired. If possible, events
+ will be merged into an 'atomic' event.
+ """
+
+ self.write(ecodes.EV_SYN, ecodes.SYN_REPORT, 0)
+
+ def close(self):
+ pass
diff --git a/src/evdev/eventio_async.py b/src/evdev/eventio_async.py
new file mode 100644
index 0000000..4af1aab
--- /dev/null
+++ b/src/evdev/eventio_async.py
@@ -0,0 +1,106 @@
+import asyncio
+import select
+import sys
+
+from . import eventio
+from .events import InputEvent
+
+# needed for compatibility
+from .eventio import EvdevError
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing import Any as Self
+
+
+class ReadIterator:
+ def __init__(self, device):
+ self.current_batch = iter(())
+ self.device = device
+
+ # Standard iterator protocol.
+ def __iter__(self) -> Self:
+ return self
+
+ def __next__(self) -> InputEvent:
+ try:
+ # Read from the previous batch of events.
+ return next(self.current_batch)
+ except StopIteration:
+ r, w, x = select.select([self.device.fd], [], [])
+ self.current_batch = self.device.read()
+ return next(self.current_batch)
+
+ def __aiter__(self) -> Self:
+ return self
+
+ def __anext__(self) -> "asyncio.Future[InputEvent]":
+ future = asyncio.Future()
+ try:
+ # Read from the previous batch of events.
+ future.set_result(next(self.current_batch))
+ except StopIteration:
+
+ def next_batch_ready(batch):
+ try:
+ self.current_batch = batch.result()
+ future.set_result(next(self.current_batch))
+ except Exception as e:
+ future.set_exception(e)
+
+ self.device.async_read().add_done_callback(next_batch_ready)
+ return future
+
+
+class EventIO(eventio.EventIO):
+ def _do_when_readable(self, callback):
+ loop = asyncio.get_event_loop()
+
+ def ready():
+ loop.remove_reader(self.fileno())
+ callback()
+
+ loop.add_reader(self.fileno(), ready)
+
+ def _set_result(self, future, cb):
+ try:
+ future.set_result(cb())
+ except Exception as error:
+ future.set_exception(error)
+
+ def async_read_one(self):
+ """
+ Asyncio coroutine to read and return a single input event as
+ an instance of :class:`InputEvent `.
+ """
+ future = asyncio.Future()
+ self._do_when_readable(lambda: self._set_result(future, self.read_one))
+ return future
+
+ def async_read(self):
+ """
+ Asyncio coroutine to read multiple input events from device. Return
+ a generator object that yields :class:`InputEvent `
+ instances.
+ """
+ future = asyncio.Future()
+ self._do_when_readable(lambda: self._set_result(future, self.read))
+ return future
+
+ def async_read_loop(self) -> ReadIterator:
+ """
+ Return an iterator that yields input events. This iterator is
+ compatible with the ``async for`` syntax.
+
+ """
+ return ReadIterator(self)
+
+ def close(self):
+ try:
+ loop = asyncio.get_event_loop()
+ loop.remove_reader(self.fileno())
+ except RuntimeError:
+ # no event loop present, so there is nothing to
+ # remove the reader from. Ignore
+ pass
diff --git a/src/evdev/events.py b/src/evdev/events.py
new file mode 100644
index 0000000..922bfe6
--- /dev/null
+++ b/src/evdev/events.py
@@ -0,0 +1,192 @@
+"""
+This module provides the :class:`InputEvent` class, which closely
+resembles the ``input_event`` struct defined in ``linux/input.h``:
+
+.. code-block:: c
+
+ struct input_event {
+ struct timeval time;
+ __u16 type;
+ __u16 code;
+ __s32 value;
+ };
+
+This module also defines several :class:`InputEvent` sub-classes that
+know more about the different types of events (key, abs, rel etc). The
+:data:`event_factory` dictionary maps event types to these classes.
+
+Assuming you use the :func:`evdev.util.categorize()` function to
+categorize events according to their type, adding or replacing a class
+for a specific event type becomes a matter of modifying
+:data:`event_factory`.
+
+All classes in this module have reasonable ``str()`` and ``repr()``
+methods::
+
+ >>> print(event)
+ event at 1337197425.477827, code 04, type 04, val 458792
+ >>> print(repr(event))
+ InputEvent(1337197425L, 477827L, 4, 4, 458792L)
+
+ >>> print(key_event)
+ key event at 1337197425.477835, 28 (KEY_ENTER), up
+ >>> print(repr(key_event))
+ KeyEvent(InputEvent(1337197425L, 477835L, 1, 28, 0L))
+"""
+
+# event type descriptions have been taken mot-a-mot from:
+# http://www.kernel.org/doc/Documentation/input/event-codes.txt
+
+# pylint: disable=no-name-in-module
+from typing import Final
+from .ecodes import ABS, EV_ABS, EV_KEY, EV_REL, EV_SYN, KEY, REL, SYN, keys
+
+
+class InputEvent:
+ """A generic input event."""
+
+ __slots__ = "sec", "usec", "type", "code", "value"
+
+ def __init__(self, sec, usec, type, code, value):
+ #: Time in seconds since epoch at which event occurred.
+ self.sec: int = sec
+
+ #: Microsecond portion of the timestamp.
+ self.usec: int = usec
+
+ #: Event type - one of ``ecodes.EV_*``.
+ self.type: int = type
+
+ #: Event code related to the event type.
+ self.code: int = code
+
+ #: Event value related to the event type.
+ self.value: int = value
+
+ def timestamp(self) -> float:
+ """Return event timestamp as a float."""
+ return self.sec + (self.usec / 1000000.0)
+
+ def __str__(self):
+ msg = "event at {:f}, code {:02d}, type {:02d}, val {:02d}"
+ return msg.format(self.timestamp(), self.code, self.type, self.value)
+
+ def __repr__(self):
+ msg = "{}({!r}, {!r}, {!r}, {!r}, {!r})"
+ return msg.format(self.__class__.__name__, self.sec, self.usec, self.type, self.code, self.value)
+
+
+class KeyEvent:
+ """An event generated by a keyboard, button or other key-like devices."""
+
+ key_up: Final[int] = 0x0
+ key_down: Final[int] = 0x1
+ key_hold: Final[int] = 0x2
+
+ __slots__ = "scancode", "keycode", "keystate", "event"
+
+ def __init__(self, event: InputEvent, allow_unknown: bool = False):
+ """
+ The ``allow_unknown`` argument determines what to do in the event of an event code
+ for which a key code cannot be found. If ``False`` a ``KeyError`` will be raised.
+ If ``True`` the keycode will be set to the hex value of the event code.
+ """
+
+ self.scancode: int = event.code
+
+ if event.value == 0:
+ self.keystate = KeyEvent.key_up
+ elif event.value == 2:
+ self.keystate = KeyEvent.key_hold
+ elif event.value == 1:
+ self.keystate = KeyEvent.key_down
+
+ try:
+ self.keycode = keys[event.code]
+ except KeyError:
+ if allow_unknown:
+ self.keycode = "0x{:02X}".format(event.code)
+ else:
+ raise
+
+ #: Reference to an :class:`InputEvent` instance.
+ self.event: InputEvent = event
+
+ def __str__(self):
+ try:
+ ks = ("up", "down", "hold")[self.keystate]
+ except IndexError:
+ ks = "unknown"
+
+ msg = "key event at {:f}, {} ({}), {}"
+ return msg.format(self.event.timestamp(), self.scancode, self.keycode, ks)
+
+ def __repr__(self):
+ return "{}({!r})".format(self.__class__.__name__, self.event)
+
+
+class RelEvent:
+ """A relative axis event (e.g moving the mouse 5 units to the left)."""
+
+ __slots__ = "event"
+
+ def __init__(self, event: InputEvent):
+ #: Reference to an :class:`InputEvent` instance.
+ self.event: InputEvent = event
+
+ def __str__(self):
+ msg = "relative axis event at {:f}, {}"
+ return msg.format(self.event.timestamp(), REL[self.event.code])
+
+ def __repr__(self):
+ return "{}({!r})".format(self.__class__.__name__, self.event)
+
+
+class AbsEvent:
+ """An absolute axis event (e.g the coordinates of a tap on a touchscreen)."""
+
+ __slots__ = "event"
+
+ def __init__(self, event: InputEvent):
+ #: Reference to an :class:`InputEvent` instance.
+ self.event: InputEvent = event
+
+ def __str__(self):
+ msg = "absolute axis event at {:f}, {}"
+ return msg.format(self.event.timestamp(), ABS[self.event.code])
+
+ def __repr__(self):
+ return "{}({!r})".format(self.__class__.__name__, self.event)
+
+
+class SynEvent:
+ """
+ A synchronization event. Used as markers to separate events. Events may be
+ separated in time or in space, such as with the multitouch protocol.
+ """
+
+ __slots__ = "event"
+
+ def __init__(self, event: InputEvent):
+ #: Reference to an :class:`InputEvent` instance.
+ self.event: InputEvent = event
+
+ def __str__(self):
+ msg = "synchronization event at {:f}, {}"
+ return msg.format(self.event.timestamp(), SYN[self.event.code])
+
+ def __repr__(self):
+ return "{}({!r})".format(self.__class__.__name__, self.event)
+
+
+#: A mapping of event types to :class:`InputEvent` sub-classes. Used
+#: by :func:`evdev.util.categorize()`
+event_factory = {
+ EV_KEY: KeyEvent,
+ EV_REL: RelEvent,
+ EV_ABS: AbsEvent,
+ EV_SYN: SynEvent,
+}
+
+
+__all__ = ("InputEvent", "KeyEvent", "RelEvent", "SynEvent", "AbsEvent", "event_factory")
diff --git a/src/evdev/evtest.py b/src/evdev/evtest.py
new file mode 100644
index 0000000..6ea3bb5
--- /dev/null
+++ b/src/evdev/evtest.py
@@ -0,0 +1,181 @@
+"""
+Usage: evtest [options] [, ...]
+
+Input device enumerator and event monitor.
+
+Running evtest without any arguments will let you select
+from a list of all readable input devices.
+
+Options:
+ -h, --help Show this help message and exit.
+ -c, --capabilities List device capabilities and exit.
+ -g, --grab Other applications will not receive events from
+ the selected devices while evtest is running.
+
+Examples:
+ evtest /dev/input/event0 /dev/input/event1
+"""
+
+import atexit
+import optparse
+import re
+import select
+import sys
+import termios
+
+from . import AbsInfo, InputDevice, ecodes, list_devices
+
+
+def parseopt():
+ parser = optparse.OptionParser(add_help_option=False)
+ parser.add_option("-h", "--help", action="store_true")
+ parser.add_option("-g", "--grab", action="store_true")
+ parser.add_option("-c", "--capabilities", action="store_true")
+ return parser.parse_args()
+
+
+def main():
+ opts, devices = parseopt()
+ if opts.help:
+ print(__doc__.strip())
+ return 0
+
+ if not devices:
+ devices = select_devices()
+ else:
+ devices = [InputDevice(path) for path in devices]
+
+ if opts.capabilities:
+ for device in devices:
+ print_capabilities(device)
+ return 0
+
+ if opts.grab:
+ for device in devices:
+ device.grab()
+
+ # Disable tty echoing if stdin is a tty.
+ if sys.stdin.isatty():
+ toggle_tty_echo(sys.stdin, enable=False)
+ atexit.register(toggle_tty_echo, sys.stdin, enable=True)
+
+ print("Listening for events (press ctrl-c to exit) ...")
+ fd_to_device = {dev.fd: dev for dev in devices}
+ while True:
+ r, w, e = select.select(fd_to_device, [], [])
+
+ for fd in r:
+ for event in fd_to_device[fd].read():
+ print_event(event)
+
+
+def select_devices(device_dir="/dev/input"):
+ """
+ Select one or more devices from a list of accessible input devices.
+ """
+
+ def devicenum(device_path):
+ digits = re.findall(r"\d+$", device_path)
+ return [int(i) for i in digits]
+
+ devices = sorted(list_devices(device_dir), key=devicenum)
+ devices = [InputDevice(path) for path in devices]
+ if not devices:
+ msg = "error: no input devices found (do you have rw permission on %s/*?)"
+ print(msg % device_dir, file=sys.stderr)
+ sys.exit(1)
+
+ dev_format = "{0:<3} {1.path:<20} {1.name:<35} {1.phys:<35} {1.uniq:<4}"
+ dev_lines = [dev_format.format(num, dev) for num, dev in enumerate(devices)]
+
+ print("ID {:<20} {:<35} {:<35} {}".format("Device", "Name", "Phys", "Uniq"))
+ print("-" * len(max(dev_lines, key=len)))
+ print("\n".join(dev_lines))
+ print()
+
+ choices = input("Select devices [0-%s]: " % (len(dev_lines) - 1))
+
+ try:
+ choices = choices.split()
+ choices = [devices[int(num)] for num in choices]
+ except ValueError:
+ choices = None
+
+ if not choices:
+ msg = "error: invalid input - please enter one or more numbers separated by spaces"
+ print(msg, file=sys.stderr)
+ sys.exit(1)
+
+ return choices
+
+
+def print_capabilities(device):
+ capabilities = device.capabilities(verbose=True)
+ input_props = device.input_props(verbose=True)
+
+ print("Device name: {.name}".format(device))
+ print("Device info: {.info}".format(device))
+ print("Repeat settings: {}\n".format(device.repeat))
+
+ if ("EV_LED", ecodes.EV_LED) in capabilities:
+ leds = ",".join(i[0] for i in device.leds(True))
+ print("Active LEDs: %s" % leds)
+
+ active_keys = ",".join(k[0] for k in device.active_keys(True))
+ print("Active keys: %s\n" % active_keys)
+
+ if input_props:
+ print("Input properties:")
+ for type, code in input_props:
+ print(" %s %s" % (type, code))
+ print()
+
+ print("Device capabilities:")
+ for type, codes in capabilities.items():
+ print(" Type {} {}:".format(*type))
+ for code in codes:
+ # code <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272)
+ if isinstance(code[1], AbsInfo):
+ print(" Code {:<4} {}:".format(*code[0]))
+ print(" {}".format(code[1]))
+ else:
+ # Multiple names may resolve to one value.
+ s = ", ".join(code[0]) if isinstance(code[0], list) else code[0]
+ print(" Code {:<4} {}".format(s, code[1]))
+ print("")
+
+
+def print_event(e):
+ if e.type == ecodes.EV_SYN:
+ if e.code == ecodes.SYN_MT_REPORT:
+ msg = "time {:<17} +++++++++++++ {} +++++++++++++"
+ elif e.code == ecodes.SYN_DROPPED:
+ msg = "time {:<17} !!!!!!!!!!!!! {} !!!!!!!!!!!!!"
+ else:
+ msg = "time {:<17} ------------- {} -------------"
+ print(msg.format(e.timestamp(), ecodes.SYN[e.code]))
+ else:
+ if e.type in ecodes.bytype:
+ codename = ecodes.bytype[e.type][e.code]
+ else:
+ codename = "?"
+
+ evfmt = "time {:<17} type {} ({}), code {:<4} ({}), value {}"
+ print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value))
+
+
+def toggle_tty_echo(fh, enable=True):
+ flags = termios.tcgetattr(fh.fileno())
+ if enable:
+ flags[3] |= termios.ECHO
+ else:
+ flags[3] &= ~termios.ECHO
+ termios.tcsetattr(fh.fileno(), termios.TCSANOW, flags)
+
+
+if __name__ == "__main__":
+ try:
+ ret = main()
+ except (KeyboardInterrupt, EOFError):
+ ret = 0
+ sys.exit(ret)
diff --git a/evdev/ff.py b/src/evdev/ff.py
similarity index 64%
rename from evdev/ff.py
rename to src/evdev/ff.py
index e4598d7..260c362 100644
--- a/evdev/ff.py
+++ b/src/evdev/ff.py
@@ -1,42 +1,42 @@
-# encoding: utf-8
-
import ctypes
-from evdev import ecodes
+from . import ecodes
-_u8 = ctypes.c_uint8
+_u8 = ctypes.c_uint8
_u16 = ctypes.c_uint16
_u32 = ctypes.c_uint32
_s16 = ctypes.c_int16
+_s32 = ctypes.c_int32
+
class Replay(ctypes.Structure):
- '''
+ """
Defines scheduling of the force-feedback effect
@length: duration of the effect
@delay: delay before effect should start playing
- '''
+ """
_fields_ = [
- ('length', _u16),
- ('delay', _u16),
+ ("length", _u16),
+ ("delay", _u16),
]
class Trigger(ctypes.Structure):
- '''
+ """
Defines what triggers the force-feedback effect
@button: number of the button triggering the effect
@interval: controls how soon the effect can be re-triggered
- '''
+ """
_fields_ = [
- ('button', _u16),
- ('interval', _u16),
+ ("button", _u16),
+ ("interval", _u16),
]
class Envelope(ctypes.Structure):
- '''
+ """
Generic force-feedback effect envelope
@attack_length: duration of the attack (ms)
@attack_level: level at the beginning of the attack
@@ -47,46 +47,46 @@ class Envelope(ctypes.Structure):
envelope force-feedback core will convert to positive/negative
value based on polarity of the default level of the effect.
Valid range for the attack and fade levels is 0x0000 - 0x7fff
- '''
+ """
_fields_ = [
- ('attach_length', _u16),
- ('attack_level', _u16),
- ('fade_length', _u16),
- ('fade_level', _u16),
+ ("attack_length", _u16),
+ ("attack_level", _u16),
+ ("fade_length", _u16),
+ ("fade_level", _u16),
]
class Constant(ctypes.Structure):
- '''
+ """
Defines parameters of a constant force-feedback effect
@level: strength of the effect; may be negative
@envelope: envelope data
- '''
+ """
_fields_ = [
- ('level', _s16),
- ('ff_envelope', Envelope),
+ ("level", _s16),
+ ("ff_envelope", Envelope),
]
class Ramp(ctypes.Structure):
- '''
+ """
Defines parameters of a ramp force-feedback effect
@start_level: beginning strength of the effect; may be negative
@end_level: final strength of the effect; may be negative
@envelope: envelope data
- '''
+ """
_fields_ = [
- ('start_level', _s16),
- ('end_level', _s16),
- ('ff_envelope', Envelope),
+ ("start_level", _s16),
+ ("end_level", _s16),
+ ("ff_envelope", Envelope),
]
class Condition(ctypes.Structure):
- '''
+ """
Defines a spring or friction force-feedback effect
@right_saturation: maximum level when joystick moved all way to the right
@left_saturation: same for the left side
@@ -94,20 +94,20 @@ class Condition(ctypes.Structure):
@left_coeff: same for the left side
@deadband: size of the dead zone, where no force is produced
@center: position of the dead zone
- '''
+ """
_fields_ = [
- ('right_saturation', _u16),
- ('left_saturation', _u16),
- ('right_coeff', _s16),
- ('left_foeff', _s16),
- ('deadband', _u16),
- ('center', _s16),
+ ("right_saturation", _u16),
+ ("left_saturation", _u16),
+ ("right_coeff", _s16),
+ ("left_coeff", _s16),
+ ("deadband", _u16),
+ ("center", _s16),
]
class Periodic(ctypes.Structure):
- '''
+ """
Defines parameters of a periodic force-feedback effect
@waveform: kind of the effect (wave)
@period: period of the wave (ms)
@@ -117,56 +117,74 @@ class Periodic(ctypes.Structure):
@envelope: envelope data
@custom_len: number of samples (FF_CUSTOM only)
@custom_data: buffer of samples (FF_CUSTOM only)
- '''
+ """
_fields_ = [
- ('waveform', _u16),
- ('period', _u16),
- ('magnitude', _s16),
- ('offset', _s16),
- ('phase', _u16),
- ('envelope', Envelope),
- ('custom_len', _u32),
- ('custom_data', ctypes.POINTER(_s16)),
+ ("waveform", _u16),
+ ("period", _u16),
+ ("magnitude", _s16),
+ ("offset", _s16),
+ ("phase", _u16),
+ ("envelope", Envelope),
+ ("custom_len", _u32),
+ ("custom_data", ctypes.POINTER(_s16)),
]
class Rumble(ctypes.Structure):
- '''
+ """
Defines parameters of a periodic force-feedback effect
@strong_magnitude: magnitude of the heavy motor
@weak_magnitude: magnitude of the light one
Some rumble pads have two motors of different weight. Strong_magnitude
represents the magnitude of the vibration generated by the heavy one.
- '''
+ """
_fields_ = [
- ('strong_magnitude', _u16),
- ('weak_magnitude', _u16),
+ ("strong_magnitude", _u16),
+ ("weak_magnitude", _u16),
]
class EffectType(ctypes.Union):
_fields_ = [
- ('ff_constant_effect', Constant),
- ('ff_ramp_effect', Ramp),
- ('ff_periodic_effect', Periodic),
- ('ff_condition_effect', Condition * 2), # one for each axis
- ('ff_rumble_effect', Rumble),
+ ("ff_constant_effect", Constant),
+ ("ff_ramp_effect", Ramp),
+ ("ff_periodic_effect", Periodic),
+ ("ff_condition_effect", Condition * 2), # one for each axis
+ ("ff_rumble_effect", Rumble),
]
class Effect(ctypes.Structure):
_fields_ = [
- ('type', _u16),
- ('id', _s16),
- ('direction', _u16),
- ('ff_trigger', Trigger),
- ('ff_replay', Replay),
- ('u', EffectType)
+ ("type", _u16),
+ ("id", _s16),
+ ("direction", _u16),
+ ("ff_trigger", Trigger),
+ ("ff_replay", Replay),
+ ("u", EffectType),
+ ]
+
+
+class UInputUpload(ctypes.Structure):
+ _fields_ = [
+ ("request_id", _u32),
+ ("retval", _s32),
+ ("effect", Effect),
+ ("old", Effect),
]
+
+class UInputErase(ctypes.Structure):
+ _fields_ = [
+ ("request_id", _u32),
+ ("retval", _s32),
+ ("effect_id", _u32),
+ ]
+
+
# ff_types = {
# ecodes.FF_CONSTANT,
# ecodes.FF_PERIODIC,
diff --git a/src/evdev/genecodes_c.py b/src/evdev/genecodes_c.py
new file mode 100644
index 0000000..15a6693
--- /dev/null
+++ b/src/evdev/genecodes_c.py
@@ -0,0 +1,147 @@
+"""
+Generate a Python extension module with the constants defined in linux/input.h.
+"""
+
+import getopt
+import os
+import re
+import sys
+
+# -----------------------------------------------------------------------------
+# The default header file locations to try.
+headers = [
+ "/usr/include/linux/input.h",
+ "/usr/include/linux/input-event-codes.h",
+ "/usr/include/linux/uinput.h",
+]
+
+opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs", "reproducible"])
+if not opts:
+ print("usage: genecodes.py [--ecodes|--stubs] [--reproducible] ")
+ exit(2)
+
+if args:
+ headers = args
+
+reproducible = ("--reproducible", "") in opts
+
+
+# -----------------------------------------------------------------------------
+macro_regex = r"#define\s+((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)"
+macro_regex = re.compile(macro_regex)
+
+if reproducible:
+ uname = "hidden for reproducibility"
+else:
+ # Uname without hostname.
+ uname = list(os.uname())
+ uname = " ".join((uname[0], *uname[2:]))
+
+
+# -----------------------------------------------------------------------------
+template_ecodes = r"""
+#include
+#ifdef __FreeBSD__
+#include
+#include
+#else
+#include
+#include
+#endif
+
+/* Automatically generated by evdev.genecodes */
+/* Generated on %s */
+/* Generated from %s */
+
+#define MODULE_NAME "_ecodes"
+#define MODULE_HELP "linux/input.h macros"
+
+static PyMethodDef MethodTable[] = {
+ { NULL, NULL, 0, NULL}
+};
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ MODULE_NAME,
+ MODULE_HELP,
+ -1, /* m_size */
+ MethodTable, /* m_methods */
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL, /* m_free */
+};
+
+PyMODINIT_FUNC
+PyInit__ecodes(void)
+{
+ PyObject* m = PyModule_Create(&moduledef);
+ if (m == NULL) return NULL;
+
+%s
+
+ return m;
+}
+"""
+
+
+template_stubs = r"""
+# Automatically generated by evdev.genecodes
+# Generated on %s
+# Generated from %s
+
+# pylint: skip-file
+
+ecodes: dict[str, int]
+keys: dict[int, str|list[str]]
+bytype: dict[int, dict[int, str|list[str]]]
+
+KEY: dict[int, str|list[str]]
+ABS: dict[int, str|list[str]]
+REL: dict[int, str|list[str]]
+SW: dict[int, str|list[str]]
+MSC: dict[int, str|list[str]]
+LED: dict[int, str|list[str]]
+BTN: dict[int, str|list[str]]
+REP: dict[int, str|list[str]]
+SND: dict[int, str|list[str]]
+ID: dict[int, str|list[str]]
+EV: dict[int, str|list[str]]
+BUS: dict[int, str|list[str]]
+SYN: dict[int, str|list[str]]
+FF_STATUS: dict[int, str|list[str]]
+FF_INPUT_PROP: dict[int, str|list[str]]
+
+%s
+"""
+
+
+def parse_headers(headers=headers):
+ for header in headers:
+ try:
+ fh = open(header)
+ except (IOError, OSError):
+ continue
+
+ for line in fh:
+ macro = macro_regex.search(line)
+ if macro:
+ yield macro.group(1)
+
+
+all_macros = list(parse_headers())
+if not all_macros:
+ print("no input macros found in: %s" % " ".join(headers), file=sys.stderr)
+ sys.exit(1)
+
+# pylint: disable=possibly-used-before-assignment, used-before-assignment
+if ("--ecodes", "") in opts:
+ body = (" PyModule_AddIntMacro(m, %s);" % macro for macro in all_macros)
+ template = template_ecodes
+elif ("--stubs", "") in opts:
+ body = ("%s: int" % macro for macro in all_macros)
+ template = template_stubs
+
+body = os.linesep.join(body)
+text = template % (uname, headers if not reproducible else ["hidden for reproducibility"], body)
+print(text.strip())
diff --git a/src/evdev/genecodes_py.py b/src/evdev/genecodes_py.py
new file mode 100644
index 0000000..f00020c
--- /dev/null
+++ b/src/evdev/genecodes_py.py
@@ -0,0 +1,54 @@
+import sys
+from unittest import mock
+from pprint import PrettyPrinter
+
+sys.modules["evdev.ecodes"] = mock.Mock()
+from evdev import ecodes_runtime as ecodes
+
+pprint = PrettyPrinter(indent=2, sort_dicts=True, width=120).pprint
+
+
+print("# Automatically generated by evdev.genecodes_py")
+print()
+print('"""')
+print(ecodes.__doc__.strip())
+print('"""')
+
+print()
+print("from typing import Final, Dict, Tuple, Union")
+print()
+
+for name, value in ecodes.ecodes.items():
+ print(f"{name}: Final[int] = {value}")
+print()
+
+entries = [
+ ("ecodes", "Dict[str, int]", "#: Mapping of names to values."),
+ ("bytype", "Dict[int, Dict[int, Union[str, Tuple[str]]]]", "#: Mapping of event types to other value/name mappings."),
+ ("keys", "Dict[int, Union[str, Tuple[str]]]", "#: Keys are a combination of all BTN and KEY codes."),
+ ("KEY", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("ABS", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("REL", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("SW", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("MSC", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("LED", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("BTN", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("REP", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("SND", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("ID", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("EV", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("BUS", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("SYN", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("FF", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("UI_FF", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("FF_STATUS", "Dict[int, Union[str, Tuple[str]]]", None),
+ ("INPUT_PROP", "Dict[int, Union[str, Tuple[str]]]", None)
+]
+
+for key, annotation, doc in entries:
+ if doc:
+ print(doc)
+
+ print(f"{key}: {annotation} = ", end="")
+ pprint(getattr(ecodes, key))
+ print()
\ No newline at end of file
diff --git a/evdev/input.c b/src/evdev/input.c
similarity index 62%
rename from evdev/input.c
rename to src/evdev/input.c
index 6a62f17..894db22 100644
--- a/evdev/input.c
+++ b/src/evdev/input.c
@@ -18,7 +18,16 @@
#include
#include
+#ifdef __FreeBSD__
+#include
+#else
#include
+#endif
+
+#ifndef input_event_sec
+#define input_event_sec time.tv_sec
+#define input_event_usec time.tv_usec
+#endif
#define MAX_NAME_SIZE 256
@@ -37,12 +46,10 @@ int test_bit(const char* bitmask, int bit) {
static PyObject *
device_read(PyObject *self, PyObject *args)
{
- int fd;
struct input_event event;
// get device file descriptor (O_RDONLY|O_NONBLOCK)
- if (PyArg_ParseTuple(args, "i", &fd) < 0)
- return NULL;
+ int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0));
int n = read(fd, &event, sizeof(event));
@@ -52,19 +59,16 @@ device_read(PyObject *self, PyObject *args)
return Py_None;
}
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
- PyObject* sec = PyLong_FromLong(event.time.tv_sec);
- PyObject* usec = PyLong_FromLong(event.time.tv_usec);
- PyObject* val = PyLong_FromLong(event.value);
- PyObject* py_input_event = NULL;
-
- py_input_event = Py_BuildValue("OOhhO", sec, usec, event.type, event.code, val);
- Py_DECREF(sec);
- Py_DECREF(usec);
- Py_DECREF(val);
+ PyObject *py_input_event = PyTuple_New(5);
+ PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event.input_event_sec));
+ PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event.input_event_usec));
+ PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event.type));
+ PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event.code));
+ PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event.value));
return py_input_event;
}
@@ -74,17 +78,8 @@ device_read(PyObject *self, PyObject *args)
static PyObject *
device_read_many(PyObject *self, PyObject *args)
{
- int fd, i;
-
// get device file descriptor (O_RDONLY|O_NONBLOCK)
- int ret = PyArg_ParseTuple(args, "i", &fd);
- if (!ret) return NULL;
-
- PyObject* event_list = PyList_New(0);
- PyObject* py_input_event = NULL;
- PyObject* sec = NULL;
- PyObject* usec = NULL;
- PyObject* val = NULL;
+ int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0));
struct input_event event[64];
@@ -92,45 +87,25 @@ device_read_many(PyObject *self, PyObject *args)
ssize_t nread = read(fd, event, event_size*64);
if (nread < 0) {
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
- // Construct a list of event tuples, which we'll make sense of in Python
- for (i = 0 ; i < nread/event_size ; i++) {
- sec = PyLong_FromLong(event[i].time.tv_sec);
- usec = PyLong_FromLong(event[i].time.tv_usec);
- val = PyLong_FromLong(event[i].value);
-
- py_input_event = Py_BuildValue("OOhhO", sec, usec, event[i].type, event[i].code, val);
- PyList_Append(event_list, py_input_event);
-
- Py_DECREF(py_input_event);
- Py_DECREF(sec);
- Py_DECREF(usec);
- Py_DECREF(val);
+ // Construct a tuple of event tuples. Each tuple is the arguments to InputEvent.
+ size_t num_events = nread / event_size;
+
+ PyObject* events = PyTuple_New(num_events);
+ for (size_t i = 0 ; i < num_events; i++) {
+ PyObject *py_input_event = PyTuple_New(5);
+ PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event[i].input_event_sec));
+ PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event[i].input_event_usec));
+ PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event[i].type));
+ PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event[i].code));
+ PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event[i].value));
+ PyTuple_SET_ITEM(events, i, py_input_event);
}
- return event_list;
-}
-
-
-// Unpack a single event (this is essentially a struct.unpack(), without having
-// to worry about word size.
-static PyObject *
-event_unpack(PyObject *self, PyObject *args)
-{
- struct input_event event;
-
- const char *data;
- int len;
-
- int ret = PyArg_ParseTuple(args, "s#", &data, &len);
- if (!ret) return NULL;
-
- memcpy(&event, data, sizeof(event));
-
- Py_RETURN_NONE;
+ return events;
}
@@ -139,7 +114,7 @@ static PyObject *
ioctl_capabilities(PyObject *self, PyObject *args)
{
int fd, ev_type, ev_code;
- char ev_bits[EV_MAX/8], code_bits[KEY_MAX/8];
+ char ev_bits[EV_MAX/8 + 1], code_bits[KEY_MAX/8 + 1];
struct input_absinfo absinfo;
int ret = PyArg_ParseTuple(args, "i", &fd);
@@ -160,7 +135,7 @@ ioctl_capabilities(PyObject *self, PyObject *args)
memset(&ev_bits, 0, sizeof(ev_bits));
- if (ioctl(_fd, EVIOCGBIT(0, EV_MAX), ev_bits) < 0)
+ if (ioctl(_fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) < 0)
goto on_err;
// Build a dictionary of the device's capabilities
@@ -171,7 +146,7 @@ ioctl_capabilities(PyObject *self, PyObject *args)
eventcodes = PyList_New(0);
memset(&code_bits, 0, sizeof(code_bits));
- ioctl(_fd, EVIOCGBIT(ev_type, KEY_MAX), code_bits);
+ ioctl(_fd, EVIOCGBIT(ev_type, sizeof(code_bits)), code_bits);
for (ev_code = 0; ev_code < KEY_MAX; ev_code++) {
if (test_bit(code_bits, ev_code)) {
@@ -217,7 +192,12 @@ ioctl_capabilities(PyObject *self, PyObject *args)
return capabilities;
on_err:
- PyErr_SetFromErrno(PyExc_IOError);
+ Py_XDECREF(capabilities);
+ Py_XDECREF(eventcodes);
+ Py_XDECREF(capability);
+ Py_XDECREF(py_absinfo);
+ Py_XDECREF(absitem);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
@@ -231,6 +211,7 @@ ioctl_devinfo(PyObject *self, PyObject *args)
struct input_id iid;
char name[MAX_NAME_SIZE];
char phys[MAX_NAME_SIZE] = {0};
+ char uniq[MAX_NAME_SIZE] = {0};
int ret = PyArg_ParseTuple(args, "i", &fd);
if (!ret) return NULL;
@@ -243,12 +224,73 @@ ioctl_devinfo(PyObject *self, PyObject *args)
// Some devices do not have a physical topology associated with them
ioctl(fd, EVIOCGPHYS(sizeof(phys)), phys);
- return Py_BuildValue("hhhhss", iid.bustype, iid.vendor, iid.product, iid.version,
- name, phys);
+ // Some kernels have started reporting bluetooth controller MACs as phys.
+ // This lets us get the real physical address. As with phys, it may be blank.
+ ioctl(fd, EVIOCGUNIQ(sizeof(uniq)), uniq);
+
+ return Py_BuildValue("hhhhsss", iid.bustype, iid.vendor, iid.product, iid.version,
+ name, phys, uniq);
on_err:
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+
+static PyObject *
+ioctl_EVIOCGABS(PyObject *self, PyObject *args)
+{
+ int fd, ev_code;
+ struct input_absinfo absinfo;
+ PyObject* py_absinfo = NULL;
+
+ int ret = PyArg_ParseTuple(args, "ii", &fd, &ev_code);
+ if (!ret) return NULL;
+
+ memset(&absinfo, 0, sizeof(absinfo));
+ ret = ioctl(fd, EVIOCGABS(ev_code), &absinfo);
+ if (ret == -1) {
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
+ }
+
+ py_absinfo = Py_BuildValue("(iiiiii)",
+ absinfo.value,
+ absinfo.minimum,
+ absinfo.maximum,
+ absinfo.fuzz,
+ absinfo.flat,
+ absinfo.resolution);
+ return py_absinfo;
+}
+
+
+static PyObject *
+ioctl_EVIOCSABS(PyObject *self, PyObject *args)
+{
+ int fd, ev_code;
+ struct input_absinfo absinfo;
+
+ int ret = PyArg_ParseTuple(args,
+ "ii(iiiiii)",
+ &fd,
+ &ev_code,
+ &absinfo.value,
+ &absinfo.minimum,
+ &absinfo.maximum,
+ &absinfo.fuzz,
+ &absinfo.flat,
+ &absinfo.resolution);
+ if (!ret) return NULL;
+
+ ret = ioctl(fd, EVIOCSABS(ev_code), &absinfo);
+ if (ret == -1) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
}
@@ -256,12 +298,15 @@ static PyObject *
ioctl_EVIOCGREP(PyObject *self, PyObject *args)
{
int fd, ret;
- unsigned int rep[2] = {0};
+ unsigned int rep[REP_CNT] = {0};
ret = PyArg_ParseTuple(args, "i", &fd);
if (!ret) return NULL;
- ioctl(fd, EVIOCGREP, &rep);
- return Py_BuildValue("(ii)", rep[0], rep[1]);
+ ret = ioctl(fd, EVIOCGREP, &rep);
+ if (ret == -1)
+ return NULL;
+
+ return Py_BuildValue("(ii)", rep[REP_DELAY], rep[REP_PERIOD]);
}
@@ -269,12 +314,15 @@ static PyObject *
ioctl_EVIOCSREP(PyObject *self, PyObject *args)
{
int fd, ret;
- unsigned int rep[2] = {0};
+ unsigned int rep[REP_CNT] = {0};
ret = PyArg_ParseTuple(args, "iii", &fd, &rep[0], &rep[1]);
if (!ret) return NULL;
ret = ioctl(fd, EVIOCSREP, &rep);
+ if (ret == -1)
+ return NULL;
+
return Py_BuildValue("i", ret);
}
@@ -287,6 +335,9 @@ ioctl_EVIOCGVERSION(PyObject *self, PyObject *args)
if (!ret) return NULL;
ret = ioctl(fd, EVIOCGVERSION, &res);
+ if (ret == -1)
+ return NULL;
+
return Py_BuildValue("i", res);
}
@@ -300,7 +351,7 @@ ioctl_EVIOCGRAB(PyObject *self, PyObject *args)
ret = ioctl(fd, EVIOCGRAB, (intptr_t)flag);
if (ret != 0) {
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
@@ -309,38 +360,54 @@ ioctl_EVIOCGRAB(PyObject *self, PyObject *args)
}
-// todo: this function needs a better name
static PyObject *
-get_sw_led_snd(PyObject *self, PyObject *args)
+ioctl_EVIOCG_bits(PyObject *self, PyObject *args)
{
- int i, max, fd, evtype, ret;
- PyObject* res = PyList_New(0);
+ int max, fd, evtype, ret;
ret = PyArg_ParseTuple(args, "ii", &fd, &evtype);
if (!ret) return NULL;
- if (evtype == EV_LED)
- max = LED_MAX;
- else if (evtype == EV_SW)
- max = SW_MAX;
- else if (evtype == EV_SND)
- max = SND_MAX;
- else
+ switch (evtype) {
+ case EV_LED:
+ max = LED_MAX; break;
+ case EV_SND:
+ max = SND_MAX; break;
+ case EV_KEY:
+ max = KEY_MAX; break;
+ case EV_SW:
+ max = SW_MAX; break;
+ default:
return NULL;
+ }
char bytes[(max+7)/8];
memset(bytes, 0, sizeof bytes);
- if (evtype == EV_LED)
+ switch (evtype) {
+ case EV_LED:
ret = ioctl(fd, EVIOCGLED(sizeof(bytes)), &bytes);
- else if (evtype == EV_SW)
- ret = ioctl(fd, EVIOCGSW(sizeof(bytes)), &bytes);
- else if (evtype == EV_SND)
+ break;
+ case EV_SND:
ret = ioctl(fd, EVIOCGSND(sizeof(bytes)), &bytes);
+ break;
+ case EV_KEY:
+ ret = ioctl(fd, EVIOCGKEY(sizeof(bytes)), &bytes);
+ break;
+ case EV_SW:
+ ret = ioctl(fd, EVIOCGSW(sizeof(bytes)), &bytes);
+ break;
+ }
- for (i=0 ; iu.constant.envelope.fade_length,
effect->u.constant.envelope.fade_level);
break;
+ case FF_RUMBLE:
+ fprintf(stderr, " rumble: (%d, %d)\n",
+ effect->u.rumble.strong_magnitude,
+ effect->u.rumble.weak_magnitude);
+ break;
}
}
@@ -401,7 +476,7 @@ upload_effect(PyObject *self, PyObject *args)
ret = ioctl(fd, EVIOCSFF, &effect);
if (ret != 0) {
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
@@ -420,7 +495,7 @@ erase_effect(PyObject *self, PyObject *args)
long ff_id = PyLong_AsLong(ff_id_obj);
ret = ioctl(fd, EVIOCRMFF, ff_id);
if (ret != 0) {
- PyErr_SetFromErrno(PyExc_IOError);
+ PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
@@ -428,17 +503,47 @@ erase_effect(PyObject *self, PyObject *args)
return Py_None;
}
+static PyObject *
+ioctl_EVIOCGPROP(PyObject *self, PyObject *args)
+{
+ int fd, ret;
+
+ ret = PyArg_ParseTuple(args, "i", &fd);
+ if (!ret) return NULL;
+
+ char bytes[(INPUT_PROP_MAX+7)/8];
+ memset(bytes, 0, sizeof bytes);
+
+ ret = ioctl(fd, EVIOCGPROP(sizeof(bytes)), &bytes);
+
+ if (ret == -1)
+ return NULL;
+
+ PyObject* res = PyList_New(0);
+ for (int i=0; i= 3
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
- MODULE_NAME,
- MODULE_HELP,
+ "_input",
+ "Python bindings to certain linux input subsystem functions",
-1, /* m_size */
MethodTable, /* m_methods */
NULL, /* m_reload */
@@ -477,19 +578,3 @@ PyInit__input(void)
{
return moduleinit();
}
-
-#else
-static PyObject *
-moduleinit(void)
-{
- PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP);
- if (m == NULL) return NULL;
- return m;
-}
-
-PyMODINIT_FUNC
-init_input(void)
-{
- moduleinit();
-}
-#endif
diff --git a/src/evdev/py.typed b/src/evdev/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/evdev/uinput.c b/src/evdev/uinput.c
new file mode 100644
index 0000000..8d2c096
--- /dev/null
+++ b/src/evdev/uinput.c
@@ -0,0 +1,417 @@
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __FreeBSD__
+#include
+#include
+#else
+#include
+#include
+#endif
+
+#ifndef input_event_sec
+#define input_event_sec time.tv_sec
+#define input_event_usec time.tv_usec
+#endif
+
+// Workaround for installing on kernels newer than 4.4.
+#ifndef FF_MAX_EFFECTS
+#define FF_MAX_EFFECTS FF_GAIN;
+#endif
+
+int _uinput_close(int fd)
+{
+ if (ioctl(fd, UI_DEV_DESTROY) < 0) {
+ int oerrno = errno;
+ close(fd);
+ errno = oerrno;
+ return -1;
+ }
+
+ return close(fd);
+}
+
+
+static PyObject *
+uinput_open(PyObject *self, PyObject *args)
+{
+ const char* devnode;
+
+ int ret = PyArg_ParseTuple(args, "s", &devnode);
+ if (!ret) return NULL;
+
+ int fd = open(devnode, O_RDWR | O_NONBLOCK);
+ if (fd < 0) {
+ PyErr_SetString(PyExc_OSError, "could not open uinput device in write mode");
+ return NULL;
+ }
+
+ return Py_BuildValue("i", fd);
+}
+
+
+static PyObject *
+uinput_set_phys(PyObject *self, PyObject *args)
+{
+ int fd;
+ const char* phys;
+
+ int ret = PyArg_ParseTuple(args, "is", &fd, &phys);
+ if (!ret) return NULL;
+
+ if (ioctl(fd, UI_SET_PHYS, phys) < 0)
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+static PyObject *
+uinput_set_prop(PyObject *self, PyObject *args)
+{
+ int fd;
+ uint16_t prop;
+
+ int ret = PyArg_ParseTuple(args, "ih", &fd, &prop);
+ if (!ret) return NULL;
+
+ if (ioctl(fd, UI_SET_PROPBIT, prop) < 0)
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+static PyObject *
+uinput_get_sysname(PyObject *self, PyObject *args)
+{
+ int fd;
+ char sysname[64];
+
+ int ret = PyArg_ParseTuple(args, "i", &fd);
+ if (!ret) return NULL;
+
+ #ifdef UI_GET_SYSNAME
+ if (ioctl(fd, UI_GET_SYSNAME(sizeof(sysname)), &sysname) < 0)
+ goto on_err;
+
+ return Py_BuildValue("s", &sysname);
+ #endif
+
+ on_err:
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+// Different kernel versions have different device setup methods. You can read
+// more about it here:
+// https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75
+
+// Setup function for kernel >= v4.5
+#if defined(UI_DEV_SETUP) && defined(UI_ABS_SETUP)
+static PyObject *
+uinput_setup(PyObject *self, PyObject *args) {
+ int fd, len, i;
+ uint16_t vendor, product, version, bustype;
+ uint32_t max_effects;
+
+ PyObject *absinfo = NULL, *item = NULL;
+
+ struct uinput_abs_setup abs_setup;
+
+ const char* name;
+ int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor,
+ &product, &version, &bustype, &absinfo, &max_effects);
+ if (!ret) return NULL;
+
+ // Setup absinfo:
+ len = PyList_Size(absinfo);
+ for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0)
+ item = PyList_GetItem(absinfo, i);
+
+ memset(&abs_setup, 0, sizeof(abs_setup)); // Clear struct
+ abs_setup.code = PyLong_AsLong(PyList_GetItem(item, 0));
+ abs_setup.absinfo.value = PyLong_AsLong(PyList_GetItem(item, 1));
+ abs_setup.absinfo.minimum = PyLong_AsLong(PyList_GetItem(item, 2));
+ abs_setup.absinfo.maximum = PyLong_AsLong(PyList_GetItem(item, 3));
+ abs_setup.absinfo.fuzz = PyLong_AsLong(PyList_GetItem(item, 4));
+ abs_setup.absinfo.flat = PyLong_AsLong(PyList_GetItem(item, 5));
+ abs_setup.absinfo.resolution = PyLong_AsLong(PyList_GetItem(item, 6));
+
+ if(ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0)
+ goto on_err;
+ }
+
+ // Setup evdev:
+ struct uinput_setup usetup;
+
+ memset(&usetup, 0, sizeof(usetup));
+ strncpy(usetup.name, name, sizeof(usetup.name) - 1);
+ usetup.id.vendor = vendor;
+ usetup.id.product = product;
+ usetup.id.version = version;
+ usetup.id.bustype = bustype;
+ usetup.ff_effects_max = max_effects;
+
+ if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0)
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+// Fallback setup function (Linux <= 4.5 and FreeBSD).
+#else
+static PyObject *
+uinput_setup(PyObject *self, PyObject *args) {
+ int fd, len, i, abscode;
+ uint16_t vendor, product, version, bustype;
+ uint32_t max_effects;
+
+ PyObject *absinfo = NULL, *item = NULL;
+
+ struct uinput_user_dev uidev;
+ const char* name;
+
+ int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor,
+ &product, &version, &bustype, &absinfo, &max_effects);
+ if (!ret) return NULL;
+
+ memset(&uidev, 0, sizeof(uidev));
+ strncpy(uidev.name, name, sizeof(uidev.name) - 1);
+ uidev.id.vendor = vendor;
+ uidev.id.product = product;
+ uidev.id.version = version;
+ uidev.id.bustype = bustype;
+ uidev.ff_effects_max = max_effects;
+
+ len = PyList_Size(absinfo);
+ for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0)
+ item = PyList_GetItem(absinfo, i);
+ abscode = (int)PyLong_AsLong(PyList_GetItem(item, 0));
+
+ /* min/max/fuzz/flat start from index 2 because index 1 is value */
+ uidev.absmin[abscode] = PyLong_AsLong(PyList_GetItem(item, 2));
+ uidev.absmax[abscode] = PyLong_AsLong(PyList_GetItem(item, 3));
+ uidev.absfuzz[abscode] = PyLong_AsLong(PyList_GetItem(item, 4));
+ uidev.absflat[abscode] = PyLong_AsLong(PyList_GetItem(item, 5));
+ }
+
+ if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev))
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+#endif
+
+
+static PyObject *
+uinput_create(PyObject *self, PyObject *args)
+{
+ int fd;
+
+ int ret = PyArg_ParseTuple(args, "i", &fd);
+ if (!ret) return NULL;
+
+ if (ioctl(fd, UI_DEV_CREATE) < 0)
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+
+static PyObject *
+uinput_close(PyObject *self, PyObject *args)
+{
+ int fd;
+
+ int ret = PyArg_ParseTuple(args, "i", &fd);
+ if (!ret) return NULL;
+
+ if (_uinput_close(fd) < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ Py_RETURN_NONE;
+}
+
+
+static PyObject *
+uinput_write(PyObject *self, PyObject *args)
+{
+ int fd, type, code, value;
+
+ int ret = PyArg_ParseTuple(args, "iiii", &fd, &type, &code, &value);
+ if (!ret) return NULL;
+
+ struct input_event event;
+ struct timeval tval;
+ memset(&event, 0, sizeof(event));
+ gettimeofday(&tval, 0);
+ event.input_event_usec = tval.tv_usec;
+ event.input_event_sec = tval.tv_sec;
+ event.type = type;
+ event.code = code;
+ event.value = value;
+
+ if (write(fd, &event, sizeof(event)) != sizeof(event)) {
+ // @todo: elaborate
+ // PyErr_SetString(PyExc_OSError, "error writing event to uinput device");
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ Py_RETURN_NONE;
+}
+
+
+static PyObject *
+uinput_enable_event(PyObject *self, PyObject *args)
+{
+ int fd;
+ uint16_t type, code;
+ unsigned long req;
+
+ int ret = PyArg_ParseTuple(args, "ihh", &fd, &type, &code);
+ if (!ret) return NULL;
+
+ switch (type) {
+ case EV_KEY: req = UI_SET_KEYBIT; break;
+ case EV_ABS: req = UI_SET_ABSBIT; break;
+ case EV_REL: req = UI_SET_RELBIT; break;
+ case EV_MSC: req = UI_SET_MSCBIT; break;
+ case EV_SW: req = UI_SET_SWBIT; break;
+ case EV_LED: req = UI_SET_LEDBIT; break;
+ case EV_FF: req = UI_SET_FFBIT; break;
+ case EV_SND: req = UI_SET_SNDBIT; break;
+ default:
+ errno = EINVAL;
+ goto on_err;
+ }
+
+ if (ioctl(fd, UI_SET_EVBIT, type) < 0)
+ goto on_err;
+
+ if (ioctl(fd, req, code) < 0)
+ goto on_err;
+
+ Py_RETURN_NONE;
+
+ on_err:
+ _uinput_close(fd);
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+}
+
+int _uinput_begin_upload(int fd, struct uinput_ff_upload *upload)
+{
+ return ioctl(fd, UI_BEGIN_FF_UPLOAD, upload);
+}
+
+int _uinput_end_upload(int fd, struct uinput_ff_upload *upload)
+{
+ return ioctl(fd, UI_END_FF_UPLOAD, upload);
+}
+
+int _uinput_begin_erase(int fd, struct uinput_ff_erase *upload)
+{
+ return ioctl(fd, UI_BEGIN_FF_ERASE, upload);
+}
+
+int _uinput_end_erase(int fd, struct uinput_ff_erase *upload)
+{
+ return ioctl(fd, UI_END_FF_ERASE, upload);
+}
+
+
+static PyMethodDef MethodTable[] = {
+ { "open", uinput_open, METH_VARARGS,
+ "Open uinput device node."},
+
+ { "setup", uinput_setup, METH_VARARGS,
+ "Set an uinput device up."},
+
+ { "create", uinput_create, METH_VARARGS,
+ "Create an uinput device."},
+
+ { "close", uinput_close, METH_VARARGS,
+ "Destroy uinput device."},
+
+ { "write", uinput_write, METH_VARARGS,
+ "Write event to uinput device."},
+
+ { "enable", uinput_enable_event, METH_VARARGS,
+ "Enable a type of event."},
+
+ { "set_phys", uinput_set_phys, METH_VARARGS,
+ "Set physical path"},
+
+ { "get_sysname", uinput_get_sysname, METH_VARARGS,
+ "Obtain the sysname of the uinput device."},
+
+ { "set_prop", uinput_set_prop, METH_VARARGS,
+ "Set device input property"},
+
+ { NULL, NULL, 0, NULL}
+};
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_uinput",
+ "Python bindings for parts of linux/uinput.c",
+ -1, /* m_size */
+ MethodTable, /* m_methods */
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL, /* m_free */
+};
+
+static PyObject *
+moduleinit(void)
+{
+ PyObject* m = PyModule_Create(&moduledef);
+ if (m == NULL) return NULL;
+
+ PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE);
+ return m;
+}
+
+PyMODINIT_FUNC
+PyInit__uinput(void)
+{
+ return moduleinit();
+}
diff --git a/src/evdev/uinput.py b/src/evdev/uinput.py
new file mode 100644
index 0000000..2c69c2b
--- /dev/null
+++ b/src/evdev/uinput.py
@@ -0,0 +1,375 @@
+import ctypes
+import os
+import platform
+import re
+import stat
+import time
+from collections import defaultdict
+from typing import Union, Tuple, Dict, Sequence, Optional
+
+from . import _uinput, ecodes, ff, util
+from .device import InputDevice, AbsInfo
+from .events import InputEvent
+
+try:
+ from evdev.eventio_async import EventIO
+except ImportError:
+ from evdev.eventio import EventIO
+
+
+class UInputError(Exception):
+ pass
+
+
+class UInput(EventIO):
+ """
+ A userland input device and that can inject input events into the
+ linux input subsystem.
+ """
+
+ __slots__ = (
+ "name",
+ "vendor",
+ "product",
+ "version",
+ "bustype",
+ "events",
+ "devnode",
+ "fd",
+ "device",
+ )
+
+ @classmethod
+ def from_device(
+ cls,
+ *devices: Union[InputDevice, Union[str, bytes, os.PathLike]],
+ filtered_types: Tuple[int] = (ecodes.EV_SYN, ecodes.EV_FF),
+ **kwargs,
+ ):
+ """
+ Create an UInput device with the capabilities of one or more input
+ devices.
+
+ Arguments
+ ---------
+ devices : InputDevice|str
+ Varargs of InputDevice instances or paths to input devices.
+
+ filtered_types : Tuple[event type codes]
+ Event types to exclude from the capabilities of the uinput device.
+
+ **kwargs
+ Keyword arguments to UInput constructor (i.e. name, vendor etc.).
+ """
+
+ device_instances = []
+ for dev in devices:
+ if not isinstance(dev, InputDevice):
+ dev = InputDevice(str(dev))
+ device_instances.append(dev)
+
+ all_capabilities = defaultdict(set)
+
+ if "max_effects" not in kwargs:
+ kwargs["max_effects"] = min([dev.ff_effects_count for dev in device_instances])
+
+ # Merge the capabilities of all devices into one dictionary.
+ for dev in device_instances:
+ for ev_type, ev_codes in dev.capabilities().items():
+ all_capabilities[ev_type].update(ev_codes)
+
+ for evtype in filtered_types:
+ if evtype in all_capabilities:
+ del all_capabilities[evtype]
+
+ return cls(events=all_capabilities, **kwargs)
+
+ def __init__(
+ self,
+ events: Optional[Dict[int, Sequence[int]]] = None,
+ name: str = "py-evdev-uinput",
+ vendor: int = 0x1,
+ product: int = 0x1,
+ version: int = 0x1,
+ bustype: int = 0x3,
+ devnode: str = "/dev/uinput",
+ phys: str = "py-evdev-uinput",
+ input_props=None,
+ # CentOS 7 has sufficiently old headers that FF_MAX_EFFECTS is not defined there,
+ # which causes the whole module to fail loading. Fallback on a hardcoded value of
+ # FF_MAX_EFFECTS if it is not defined in the ecodes.
+ max_effects=ecodes.ecodes.get("FF_MAX_EFFECTS", 96),
+ ):
+ """
+ Arguments
+ ---------
+ events : dict
+ Dictionary of event types mapping to lists of event codes. The
+ event types and codes that the uinput device will be able to
+ inject - defaults to all key codes.
+
+ name
+ The name of the input device.
+
+ vendor
+ Vendor identifier.
+
+ product
+ Product identifier.
+
+ version
+ Version identifier.
+
+ bustype
+ Bustype identifier.
+
+ phys
+ Physical path.
+
+ input_props
+ Input properties and quirks.
+
+ max_effects
+ Maximum simultaneous force-feedback effects.
+
+ Note
+ ----
+ If you do not specify any events, the uinput device will be able
+ to inject only ``KEY_*`` and ``BTN_*`` event codes.
+ """
+
+ self.name: str = name #: Uinput device name.
+ self.vendor: int = vendor #: Device vendor identifier.
+ self.product: int = product #: Device product identifier.
+ self.version: int = version #: Device version identifier.
+ self.bustype: int = bustype #: Device bustype - e.g. ``BUS_USB``.
+ self.phys: str = phys #: Uinput device physical path.
+ self.devnode: str = devnode #: Uinput device node - e.g. ``/dev/uinput/``.
+
+ if not events:
+ events = {ecodes.EV_KEY: ecodes.keys.keys()}
+
+ self._verify()
+
+ #: Write-only, non-blocking file descriptor to the uinput device node.
+ self.fd = _uinput.open(devnode)
+
+ # Prepare the list of events for passing to _uinput.enable and _uinput.setup.
+ absinfo, prepared_events = self._prepare_events(events)
+
+ # Set phys name
+ _uinput.set_phys(self.fd, phys)
+
+ # Set properties
+ input_props = input_props or []
+ for prop in input_props:
+ _uinput.set_prop(self.fd, prop)
+
+ for etype, code in prepared_events:
+ _uinput.enable(self.fd, etype, code)
+
+ _uinput.setup(self.fd, name, vendor, product, version, bustype, absinfo, max_effects)
+
+ # Create the uinput device.
+ _uinput.create(self.fd)
+
+ self.dll = ctypes.CDLL(_uinput.__file__)
+ self.dll._uinput_begin_upload.restype = ctypes.c_int
+ self.dll._uinput_end_upload.restype = ctypes.c_int
+
+ #: An :class:`InputDevice ` instance
+ #: for the fake input device. ``None`` if the device cannot be
+ #: opened for reading and writing.
+ self.device: InputDevice = self._find_device(self.fd)
+
+ def _prepare_events(self, events):
+ """Prepare events for passing to _uinput.enable and _uinput.setup"""
+ absinfo, prepared_events = [], []
+ for etype, codes in events.items():
+ for code in codes:
+ # Handle max, min, fuzz, flat.
+ if isinstance(code, (tuple, list, AbsInfo)):
+ # Flatten (ABS_Y, (0, 255, 0, 0, 0, 0)) to (ABS_Y, 0, 255, 0, 0, 0, 0).
+ f = [code[0]]
+ f.extend(code[1])
+ # Ensure the tuple is always 6 ints long, since uinput.c:uinput_create
+ # does little in the way of checking the length.
+ f.extend([0] * (6 - len(code[1])))
+ absinfo.append(f)
+ code = code[0]
+ prepared_events.append((etype, code))
+ return absinfo, prepared_events
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, tb):
+ if hasattr(self, "fd"):
+ self.close()
+
+ def __repr__(self):
+ # TODO:
+ v = (repr(getattr(self, i)) for i in ("name", "bustype", "vendor", "product", "version", "phys"))
+ return "{}({})".format(self.__class__.__name__, ", ".join(v))
+
+ def __str__(self):
+ msg = 'name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\nevent types: {}'
+
+ evtypes = [i[0] for i in self.capabilities(True).keys()]
+ msg = msg.format(
+ self.name, ecodes.BUS[self.bustype], self.vendor, self.product, self.version, self.phys, " ".join(evtypes)
+ )
+
+ return msg
+
+ def close(self):
+ # Close the associated InputDevice, if it was previously opened.
+ if self.device is not None:
+ self.device.close()
+
+ # Destroy the uinput device.
+ if self.fd > -1:
+ _uinput.close(self.fd)
+ self.fd = -1
+
+ def capabilities(self, verbose: bool = False, absinfo: bool = True):
+ """See :func:`capabilities `."""
+ if self.device is None:
+ raise UInputError("input device not opened - cannot read capabilities")
+
+ return self.device.capabilities(verbose, absinfo)
+
+ def begin_upload(self, effect_id):
+ upload = ff.UInputUpload()
+ upload.effect_id = effect_id
+
+ ret = self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload))
+ if ret:
+ raise UInputError("Failed to begin uinput upload: " + os.strerror(ret))
+
+ return upload
+
+ def end_upload(self, upload):
+ ret = self.dll._uinput_end_upload(self.fd, ctypes.byref(upload))
+ if ret:
+ raise UInputError("Failed to end uinput upload: " + os.strerror(ret))
+
+ def begin_erase(self, effect_id):
+ erase = ff.UInputErase()
+ erase.effect_id = effect_id
+
+ ret = self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase))
+ if ret:
+ raise UInputError("Failed to begin uinput erase: " + os.strerror(ret))
+ return erase
+
+ def end_erase(self, erase):
+ ret = self.dll._uinput_end_erase(self.fd, ctypes.byref(erase))
+ if ret:
+ raise UInputError("Failed to end uinput erase: " + os.strerror(ret))
+
+ def _verify(self):
+ """
+ Verify that an uinput device exists and is readable and writable
+ by the current process.
+ """
+ try:
+ m = os.stat(self.devnode)[stat.ST_MODE]
+ assert stat.S_ISCHR(m)
+ except (IndexError, OSError, AssertionError):
+ msg = '"{}" does not exist or is not a character device file - verify that the uinput module is loaded'
+ raise UInputError(msg.format(self.devnode))
+
+ if not os.access(self.devnode, os.W_OK):
+ msg = '"{}" cannot be opened for writing'
+ raise UInputError(msg.format(self.devnode))
+
+ if len(self.name) > _uinput.maxnamelen:
+ msg = "uinput device name must not be longer than {} characters"
+ raise UInputError(msg.format(_uinput.maxnamelen))
+
+ def _find_device(self, fd: int) -> InputDevice:
+ """
+ Tries to find the device node. Will delegate this task to one of
+ several platform-specific functions.
+ """
+ if platform.system() == "Linux":
+ try:
+ sysname = _uinput.get_sysname(fd)
+ return self._find_device_linux(sysname)
+ except OSError:
+ # UI_GET_SYSNAME returned an error code. We're likely dealing with
+ # an old kernel. Guess the device based on the filesystem.
+ pass
+
+ # If we're not running or Linux or the above method fails for any reason,
+ # use the generic fallback method.
+ return self._find_device_fallback()
+
+ def _find_device_linux(self, sysname: str) -> InputDevice:
+ """
+ Tries to find the device node when running on Linux.
+ """
+
+ syspath = f"/sys/devices/virtual/input/{sysname}"
+
+ # The sysfs entry for event devices should contain exactly one folder
+ # whose name matches the format "event[0-9]+". It is then assumed that
+ # the device node in /dev/input uses the same name.
+ regex = re.compile("event[0-9]+")
+ for entry in os.listdir(syspath):
+ if regex.fullmatch(entry):
+ device_path = f"/dev/input/{entry}"
+ break
+ else: # no break
+ raise FileNotFoundError()
+
+ # It is possible that there is some delay before /dev/input/event* shows
+ # up on old systems that do not use devtmpfs, so if the device cannot be
+ # found, wait for a short amount and then try again once.
+ #
+ # Furthermore, even if devtmpfs is in use, it is possible that the device
+ # does show up immediately, but without the correct permissions that
+ # still need to be set by udev. Wait for up to two seconds for either the
+ # device to show up or the permissions to be set.
+ for attempt in range(19):
+ try:
+ return InputDevice(device_path)
+ except (FileNotFoundError, PermissionError):
+ time.sleep(0.1)
+
+ # Last attempt. If this fails, whatever exception the last attempt raises
+ # shall be the exception that this function raises.
+ return InputDevice(device_path)
+
+ def _find_device_fallback(self) -> Union[InputDevice, None]:
+ """
+ Tries to find the device node when UI_GET_SYSNAME is not available or
+ we're running on a system sufficiently exotic that we do not know how
+ to interpret its return value.
+ """
+ #:bug: the device node might not be immediately available
+ time.sleep(0.1)
+
+ # There could also be another device with the same name already present,
+ # make sure to select the newest one.
+ # Strictly speaking, we cannot be certain that everything returned by list_devices()
+ # ends at event[0-9]+: it might return something like "/dev/input/events_all". Find
+ # the devices that have the expected structure and extract their device number.
+ path_number_pairs = []
+ regex = re.compile("/dev/input/event([0-9]+)")
+ for path in util.list_devices("/dev/input/"):
+ regex_match = regex.fullmatch(path)
+ if not regex_match:
+ continue
+ device_number = int(regex_match[1])
+ path_number_pairs.append((path, device_number))
+
+ # The modification date of the devnode is not reliable unfortunately, so we
+ # are sorting by the number in the name
+ path_number_pairs.sort(key=lambda pair: pair[1], reverse=True)
+
+ for path, _ in path_number_pairs:
+ d = InputDevice(path)
+ if d.name == self.name:
+ return d
diff --git a/src/evdev/util.py b/src/evdev/util.py
new file mode 100644
index 0000000..db89a22
--- /dev/null
+++ b/src/evdev/util.py
@@ -0,0 +1,146 @@
+import collections
+import glob
+import os
+import re
+import stat
+from typing import Union, List
+
+from . import ecodes
+from .events import InputEvent, event_factory, KeyEvent, RelEvent, AbsEvent, SynEvent
+
+
+def list_devices(input_device_dir: Union[str, bytes, os.PathLike] = "/dev/input") -> List[str]:
+ """List readable character devices in ``input_device_dir``."""
+
+ fns = glob.glob("{}/event*".format(input_device_dir))
+ return list(filter(is_device, fns))
+
+
+def is_device(fn: Union[str, bytes, os.PathLike]) -> bool:
+ """Check if ``fn`` is a readable and writable character device."""
+
+ if not os.path.exists(fn):
+ return False
+
+ m = os.stat(fn)[stat.ST_MODE]
+ if not stat.S_ISCHR(m):
+ return False
+
+ if not os.access(fn, os.R_OK | os.W_OK):
+ return False
+
+ return True
+
+
+def categorize(event: InputEvent) -> Union[InputEvent, KeyEvent, RelEvent, AbsEvent, SynEvent]:
+ """
+ Categorize an event according to its type.
+
+ The :data:`event_factory ` dictionary
+ maps event types to sub-classes of :class:`InputEvent
+ `. If the event cannot be categorized, it
+ is returned unmodified."""
+
+ if event.type in event_factory:
+ return event_factory[event.type](event)
+ else:
+ return event
+
+
+def resolve_ecodes_dict(typecodemap, unknown="?"):
+ """
+ Resolve event codes and types to their verbose names.
+
+ :param typecodemap: mapping of event types to lists of event codes.
+ :param unknown: symbol to which unknown types or codes will be resolved.
+
+ Example
+ -------
+ >>> resolve_ecodes_dict({ 1: [272, 273, 274] })
+ { ('EV_KEY', 1): [('BTN_MOUSE', 272),
+ ('BTN_RIGHT', 273),
+ ('BTN_MIDDLE', 274)] }
+
+ If ``typecodemap`` contains absolute axis info (instances of
+ :class:`AbsInfo ` ) the result would look
+ like:
+
+ >>> resolve_ecodes_dict({ 3: [(0, AbsInfo(...))] })
+ { ('EV_ABS', 3L): [(('ABS_X', 0L), AbsInfo(...))] }
+ """
+
+ for etype, codes in typecodemap.items():
+ type_name = ecodes.EV[etype]
+
+ # ecodes.keys are a combination of KEY_ and BTN_ codes
+ if etype == ecodes.EV_KEY:
+ ecode_dict = ecodes.keys
+ else:
+ ecode_dict = getattr(ecodes, type_name.split("_")[-1])
+
+ resolved = resolve_ecodes(ecode_dict, codes, unknown)
+ yield (type_name, etype), resolved
+
+
+def resolve_ecodes(ecode_dict, ecode_list, unknown="?"):
+ """
+ Resolve event codes and types to their verbose names.
+
+ Example
+ -------
+ >>> resolve_ecodes(ecodes.BTN, [272, 273, 274])
+ [(['BTN_LEFT', 'BTN_MOUSE'], 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274)]
+ """
+ res = []
+ for ecode in ecode_list:
+ # elements with AbsInfo(), eg { 3 : [(0, AbsInfo(...)), (1, AbsInfo(...))] }
+ if isinstance(ecode, tuple):
+ if ecode[0] in ecode_dict:
+ l = ((ecode_dict[ecode[0]], ecode[0]), ecode[1])
+ else:
+ l = ((unknown, ecode[0]), ecode[1])
+
+ # just ecodes, e.g: { 0 : [0, 1, 3], 1 : [30, 48] }
+ else:
+ if ecode in ecode_dict:
+ l = (ecode_dict[ecode], ecode)
+ else:
+ l = (unknown, ecode)
+ res.append(l)
+
+ return res
+
+
+def find_ecodes_by_regex(regex):
+ """
+ Find ecodes matching a regex and return a mapping of event type to event codes.
+
+ regex can be a pattern string or a compiled regular expression object.
+
+ Example
+ -------
+ >>> find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)')
+ {1: [411], 3: [10]}
+ >>> res = find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)')
+ >>> resolve_ecodes_dict(res)
+ {
+ ('EV_KEY', 1): [('KEY_BREAK', 411)],
+ ('EV_ABS', 3): [('ABS_BRAKE', 10)]
+ }
+ """
+
+ regex = re.compile(regex) # re.compile is idempotent
+ result = collections.defaultdict(list)
+
+ for type_code, codes in ecodes.bytype.items():
+ for code, names in codes.items():
+ names = (names,) if isinstance(names, str) else names
+ for name in names:
+ if regex.match(name):
+ result[type_code].append(code)
+ break
+
+ return dict(result)
+
+
+__all__ = ("list_devices", "is_device", "categorize", "resolve_ecodes", "resolve_ecodes_dict", "find_ecodes_by_regex")
diff --git a/tests/test_ecodes.py b/tests/test_ecodes.py
index 622a06f..5c3e38d 100644
--- a/tests/test_ecodes.py
+++ b/tests/test_ecodes.py
@@ -1,12 +1,14 @@
-# encoding: utf-8
-
from evdev import ecodes
+from evdev import ecodes_runtime
+
+
+prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF UI_FF"
-prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF'
-def to_tuples(l):
+def to_tuples(val):
t = lambda x: tuple(x) if isinstance(x, list) else x
- return map(t, l)
+ return map(t, val)
+
def test_equality():
keys = []
@@ -15,12 +17,25 @@ def test_equality():
assert set(keys) == set(ecodes.ecodes.values())
+
def test_access():
- assert ecodes.KEY_A == ecodes.ecodes['KEY_A'] == ecodes.KEY_A
- assert ecodes.KEY[ecodes.ecodes['KEY_A']] == 'KEY_A'
- assert ecodes.REL[0] == 'REL_X'
+ assert ecodes.KEY_A == ecodes.ecodes["KEY_A"] == ecodes.KEY_A
+ assert ecodes.KEY[ecodes.ecodes["KEY_A"]] == "KEY_A"
+ assert ecodes.REL[0] == "REL_X"
+
def test_overlap():
vals_ff = set(to_tuples(ecodes.FF.values()))
vals_ff_status = set(to_tuples(ecodes.FF_STATUS.values()))
- assert bool(vals_ff & vals_ff_status) == False
+ assert bool(vals_ff & vals_ff_status) is False
+
+
+def test_generated():
+ e_run = vars(ecodes_runtime)
+ e_gen = vars(ecodes)
+
+ def keys(v):
+ res = {k for k in v.keys() if not k.startswith("_") and not k[1].islower()}
+ return res
+
+ assert keys(e_run) == keys(e_gen)
\ No newline at end of file
diff --git a/tests/test_events.py b/tests/test_events.py
index d0717f2..f0f456c 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -16,6 +16,7 @@ def test_categorize():
e = events.InputEvent(1036996631, 984417, ecodes.EV_MSC, 0, 0)
assert e == util.categorize(e)
+
def test_keyevent():
e = events.InputEvent(1036996631, 984417, ecodes.EV_KEY, ecodes.KEY_A, 2)
k = events.KeyEvent(e)
@@ -23,5 +24,4 @@ def test_keyevent():
assert k.keystate == events.KeyEvent.key_hold
assert k.event == e
assert k.scancode == ecodes.KEY_A
- assert k.keycode == 'KEY_A' # :todo:
-
+ assert k.keycode == "KEY_A" # :todo:
diff --git a/tests/test_uinput.py b/tests/test_uinput.py
index 69945ea..666361f 100644
--- a/tests/test_uinput.py
+++ b/tests/test_uinput.py
@@ -1,83 +1,95 @@
# encoding: utf-8
-
+import os
+import stat
from select import select
-from pytest import raises
+from unittest.mock import patch
-from evdev import uinput, ecodes, events, device, util
+import pytest
+from pytest import raises, fixture
+from evdev import uinput, ecodes, device, UInputError
+# -----------------------------------------------------------------------------
uinput_options = {
- 'name' : 'test-py-evdev-uinput',
- 'bustype' : ecodes.BUS_USB,
- 'vendor' : 0x1100,
- 'product' : 0x2200,
- 'version' : 0x3300,
+ "name": "test-py-evdev-uinput",
+ "bustype": ecodes.BUS_USB,
+ "vendor": 0x1100,
+ "product": 0x2200,
+ "version": 0x3300,
}
-def pytest_funcarg__c(request):
+@fixture
+def c():
return uinput_options.copy()
def device_exists(bustype, vendor, product, version):
- match = 'I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx' % \
- (bustype, vendor, product, version)
+ match = "I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx"
+ match = match % (bustype, vendor, product, version)
- for line in open('/proc/bus/input/devices'):
- if line.strip() == match: return True
+ for line in open("/proc/bus/input/devices"):
+ if line.strip() == match:
+ return True
return False
+# -----------------------------------------------------------------------------
def test_open(c):
ui = uinput.UInput(**c)
- args = (c['bustype'], c['vendor'], c['product'], c['version'])
+ args = (c["bustype"], c["vendor"], c["product"], c["version"])
assert device_exists(*args)
ui.close()
assert not device_exists(*args)
+
def test_open_context(c):
- args = (c['bustype'], c['vendor'], c['product'], c['version'])
+ args = (c["bustype"], c["vendor"], c["product"], c["version"])
with uinput.UInput(**c):
assert device_exists(*args)
assert not device_exists(*args)
+
def test_maxnamelen(c):
with raises(uinput.UInputError):
- c['name'] = 'a' * 150
+ c["name"] = "a" * 150
uinput.UInput(**c)
+
def test_enable_events(c):
e = ecodes
- c['events'] = {e.EV_KEY : [e.KEY_A, e.KEY_B, e.KEY_C]}
+ c["events"] = {e.EV_KEY: [e.KEY_A, e.KEY_B, e.KEY_C]}
with uinput.UInput(**c) as ui:
cap = ui.capabilities()
assert e.EV_KEY in cap
- assert sorted(cap[e.EV_KEY]) == sorted(c['events'][e.EV_KEY])
+ assert sorted(cap[e.EV_KEY]) == sorted(c["events"][e.EV_KEY])
+
def test_abs_values(c):
e = ecodes
- c['events'] = {
- e.EV_KEY : [e.KEY_A, e.KEY_B],
- e.EV_ABS : [(e.ABS_X, (0, 255, 0, 0)),
- (e.ABS_Y, device.AbsInfo(0, 255, 5, 10, 0, 0))],
+ c = {
+ e.EV_KEY: [e.KEY_A, e.KEY_B],
+ e.EV_ABS: [(e.ABS_X, (0, 0, 255, 0, 0)), (e.ABS_Y, device.AbsInfo(0, 0, 255, 5, 10, 0))],
}
- with uinput.UInput(**c) as ui:
+ with uinput.UInput(events=c) as ui:
c = ui.capabilities()
abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0)
assert c[e.EV_ABS][0] == (0, abs)
+
abs = device.AbsInfo(value=0, min=0, max=255, fuzz=5, flat=10, resolution=0)
assert c[e.EV_ABS][1] == (1, abs)
c = ui.capabilities(verbose=True)
abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0)
- assert c[('EV_ABS', 3)][0] == (('ABS_X', 0), abs)
+ assert c[("EV_ABS", 3)][0] == (("ABS_X", 0), abs)
c = ui.capabilities(verbose=False, absinfo=False)
assert c[e.EV_ABS] == list((0, 1))
+
def test_write(c):
with uinput.UInput(**c) as ui:
d = ui.device
@@ -87,12 +99,12 @@ def test_write(c):
r, w, x = select([d], [d], [])
if w and not wrote:
- ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down
- ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down
- ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up
- ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down
- ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold
- ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up
+ ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down
+ ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down
+ ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up
+ ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down
+ ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold
+ ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up
ui.syn()
wrote = True
@@ -105,3 +117,21 @@ def test_write(c):
assert evs[3].code == ecodes.KEY_A and evs[3].value == 2
assert evs[4].code == ecodes.KEY_A and evs[4].value == 0
break
+
+
+@patch.object(stat, 'S_ISCHR', return_value=False)
+def test_not_a_character_device(ischr_mock, c):
+ with pytest.raises(UInputError, match='not a character device file'):
+ uinput.UInput(**c)
+
+@patch.object(stat, 'S_ISCHR', return_value=True)
+@patch.object(os, 'stat', side_effect=OSError())
+def test_not_a_character_device_2(stat_mock, ischr_mock, c):
+ with pytest.raises(UInputError, match='not a character device file'):
+ uinput.UInput(**c)
+
+@patch.object(stat, 'S_ISCHR', return_value=True)
+@patch.object(os, 'stat', return_value=[])
+def test_not_a_character_device_3(stat_mock, ischr_mock, c):
+ with pytest.raises(UInputError, match='not a character device file'):
+ uinput.UInput(**c)
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..7112927
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,21 @@
+from evdev import util
+
+
+def test_match_ecodes_a():
+ res = util.find_ecodes_by_regex("KEY_ZOOM.*")
+ assert res == {1: [372, 418, 419, 420]}
+ assert dict(util.resolve_ecodes_dict(res)) == {
+ ("EV_KEY", 1): [
+ (("KEY_FULL_SCREEN", "KEY_ZOOM"), 372),
+ ("KEY_ZOOMIN", 418),
+ ("KEY_ZOOMOUT", 419),
+ ("KEY_ZOOMRESET", 420),
+ ]
+ }
+
+ res = util.find_ecodes_by_regex(r"(ABS|KEY)_BR(AKE|EAK)")
+ assert res == {1: [411], 3: [10]}
+ assert dict(util.resolve_ecodes_dict(res)) == {
+ ("EV_KEY", 1): [("KEY_BREAK", 411)],
+ ("EV_ABS", 3): [("ABS_BRAKE", 10)],
+ }