You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
14 KiB
445 lines
14 KiB
# coding=utf-8
|
|
# pynput
|
|
# Copyright (C) 2015-2022 Moses Palmér
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Lesser General Public License as published by the Free
|
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
# later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
The keyboard implementation for *uinput*.
|
|
"""
|
|
|
|
# pylint: disable=C0111
|
|
# The documentation is extracted from the base classes
|
|
|
|
# pylint: disable=R0903
|
|
# We implement stubs
|
|
|
|
import enum
|
|
import errno
|
|
import functools
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
import evdev
|
|
|
|
from evdev.events import KeyEvent
|
|
|
|
from pynput._util import xorg_keysyms
|
|
from pynput._util.uinput import ListenerMixin
|
|
from . import _base
|
|
|
|
|
|
class KeyCode(_base.KeyCode):
|
|
_PLATFORM_EXTENSIONS = (
|
|
# The name for this key
|
|
'_x_name',
|
|
'_kernel_name',
|
|
)
|
|
|
|
# Be explicit about fields
|
|
_x_name = None
|
|
_kernel_name = None
|
|
# pylint: enable=W0212
|
|
|
|
@classmethod
|
|
def _from_name(cls, x_name, kernel_name, **kwargs):
|
|
"""Creates a key from a name.
|
|
|
|
:param str x_name: The X name.
|
|
|
|
:param str kernel_name: The kernel name.
|
|
|
|
:return: a key code
|
|
"""
|
|
try:
|
|
vk = getattr(evdev.ecodes, kernel_name)
|
|
except AttributeError:
|
|
vk = None
|
|
return cls.from_vk(
|
|
vk, _x_name=x_name, _kernel_name=kernel_name, **kwargs)
|
|
|
|
|
|
# pylint: disable=W0212
|
|
class Key(enum.Enum):
|
|
alt = KeyCode._from_name('Alt_L', 'KEY_LEFTALT')
|
|
alt_l = KeyCode._from_name('Alt_L', 'KEY_LEFTALT')
|
|
alt_r = KeyCode._from_name('Alt_R', 'KEY_RIGHTALT')
|
|
alt_gr = KeyCode._from_name('Mode_switch', 'KEY_RIGHTALT')
|
|
backspace = KeyCode._from_name('BackSpace', 'KEY_BACKSPACE')
|
|
caps_lock = KeyCode._from_name('Caps_Lock', 'KEY_CAPSLOCK')
|
|
cmd = KeyCode._from_name('Super_L', 'KEY_LEFTMETA')
|
|
cmd_l = KeyCode._from_name('Super_L', 'KEY_LEFTMETA')
|
|
cmd_r = KeyCode._from_name('Super_R', 'KEY_RIGHTMETA')
|
|
ctrl = KeyCode._from_name('Control_L', 'KEY_LEFTCTRL')
|
|
ctrl_l = KeyCode._from_name('Control_L', 'KEY_LEFTCTRL')
|
|
ctrl_r = KeyCode._from_name('Control_R', 'KEY_RIGHTCTRL')
|
|
delete = KeyCode._from_name('Delete', 'KEY_DELETE')
|
|
down = KeyCode._from_name('Down', 'KEY_DOWN')
|
|
end = KeyCode._from_name('End', 'KEY_END')
|
|
enter = KeyCode._from_name('Return', 'KEY_ENTER')
|
|
esc = KeyCode._from_name('Escape', 'KEY_ESC')
|
|
f1 = KeyCode._from_name('F1', 'KEY_F1')
|
|
f2 = KeyCode._from_name('F2', 'KEY_F2')
|
|
f3 = KeyCode._from_name('F3', 'KEY_F3')
|
|
f4 = KeyCode._from_name('F4', 'KEY_F4')
|
|
f5 = KeyCode._from_name('F5', 'KEY_F5')
|
|
f6 = KeyCode._from_name('F6', 'KEY_F6')
|
|
f7 = KeyCode._from_name('F7', 'KEY_F7')
|
|
f8 = KeyCode._from_name('F8', 'KEY_F8')
|
|
f9 = KeyCode._from_name('F9', 'KEY_F9')
|
|
f10 = KeyCode._from_name('F10', 'KEY_F10')
|
|
f11 = KeyCode._from_name('F11', 'KEY_F11')
|
|
f12 = KeyCode._from_name('F12', 'KEY_F12')
|
|
f13 = KeyCode._from_name('F13', 'KEY_F13')
|
|
f14 = KeyCode._from_name('F14', 'KEY_F14')
|
|
f15 = KeyCode._from_name('F15', 'KEY_F15')
|
|
f16 = KeyCode._from_name('F16', 'KEY_F16')
|
|
f17 = KeyCode._from_name('F17', 'KEY_F17')
|
|
f18 = KeyCode._from_name('F18', 'KEY_F18')
|
|
f19 = KeyCode._from_name('F19', 'KEY_F19')
|
|
f20 = KeyCode._from_name('F20', 'KEY_F20')
|
|
home = KeyCode._from_name('Home', 'KEY_HOME')
|
|
left = KeyCode._from_name('Left', 'KEY_LEFT')
|
|
page_down = KeyCode._from_name('Page_Down', 'KEY_PAGEDOWN')
|
|
page_up = KeyCode._from_name('Page_Up', 'KEY_PAGEUP')
|
|
right = KeyCode._from_name('Right', 'KEY_RIGHT')
|
|
shift = KeyCode._from_name('Shift_L', 'KEY_LEFTSHIFT')
|
|
shift_l = KeyCode._from_name('Shift_L', 'KEY_LEFTSHIFT')
|
|
shift_r = KeyCode._from_name('Shift_R', 'KEY_RIGHTSHIFT')
|
|
space = KeyCode._from_name('space', 'KEY_SPACE', char=' ')
|
|
tab = KeyCode._from_name('Tab', 'KEY_TAB', char='\t')
|
|
up = KeyCode._from_name('Up', 'KEY_UP')
|
|
|
|
media_play_pause = KeyCode._from_name('Play', 'KEY_PLAYPAUSE')
|
|
media_volume_mute = KeyCode._from_name('Mute', 'KEY_MUTE')
|
|
media_volume_down = KeyCode._from_name('LowerVolume', 'KEY_VOLUMEDOWN')
|
|
media_volume_up = KeyCode._from_name('RaiseVolume', 'KEY_VOLUMEUP')
|
|
media_previous = KeyCode._from_name('Prev', 'KEY_PREVIOUSSONG')
|
|
media_next = KeyCode._from_name('Next', 'KEY_NEXTSONG')
|
|
|
|
insert = KeyCode._from_name('Insert', 'KEY_INSERT')
|
|
menu = KeyCode._from_name('Menu', 'KEY_MENU')
|
|
num_lock = KeyCode._from_name('Num_Lock', 'KEY_NUMLOCK')
|
|
pause = KeyCode._from_name('Pause', 'KEY_PAUSE')
|
|
print_screen = KeyCode._from_name('Print', 'KEY_SYSRQ')
|
|
scroll_lock = KeyCode._from_name('Scroll_Lock', 'KEY_SCROLLLOCK')
|
|
# pylint: enable=W0212
|
|
|
|
|
|
class Layout(object):
|
|
"""A description of the keyboard layout.
|
|
"""
|
|
#: A regular expression to parse keycodes in the dumpkeys output
|
|
#:
|
|
#: The groups are: keycode number, key names.
|
|
KEYCODE_RE = re.compile(
|
|
r'keycode\s+(\d+)\s+=(.*)')
|
|
|
|
class Key(object):
|
|
"""A key in a keyboard layout.
|
|
"""
|
|
def __init__(self, normal, shifted, alt, alt_shifted):
|
|
self._values = (
|
|
normal,
|
|
shifted,
|
|
alt,
|
|
alt_shifted)
|
|
|
|
def __str__(self):
|
|
return ('<'
|
|
'normal: {}, '
|
|
'shifted: {}, '
|
|
'alternative: {}, '
|
|
'shifted alternative: {}>').format(
|
|
self.normal, self.shifted, self.alt, self.alt_shifted)
|
|
|
|
__repr__ = __str__
|
|
|
|
def __iter__(self):
|
|
return iter(self._values)
|
|
|
|
def __getitem__(self, i):
|
|
return self._values[i]
|
|
|
|
@property
|
|
def normal(self):
|
|
"""The normal key.
|
|
"""
|
|
return self._values[0]
|
|
|
|
@property
|
|
def shifted(self):
|
|
"""The shifted key.
|
|
"""
|
|
return self._values[1]
|
|
|
|
@property
|
|
def alt(self):
|
|
"""The alternative key.
|
|
"""
|
|
return self._values[2]
|
|
|
|
@property
|
|
def alt_shifted(self):
|
|
"""The shifted alternative key.
|
|
"""
|
|
return self._values[3]
|
|
|
|
def __init__(self):
|
|
def as_char(k):
|
|
return k.value.char if isinstance(k, Key) else k.char
|
|
self._vk_table = self._load()
|
|
self._char_table = {
|
|
as_char(key): (
|
|
vk,
|
|
set()
|
|
| {Key.shift} if i & 1 else set()
|
|
| {Key.alt_gr} if i & 2 else set())
|
|
for vk, keys in self._vk_table.items()
|
|
for i, key in enumerate(keys)
|
|
if key is not None and as_char(key) is not None}
|
|
|
|
def for_vk(self, vk, modifiers):
|
|
"""Reads a key for a virtual key code and modifier state.
|
|
|
|
:param int vk: The virtual key code.
|
|
|
|
:param set modifiers: A set of modifiers.
|
|
|
|
:return: a mapped key
|
|
|
|
:raises KeyError: if ``vk`` is an unknown key
|
|
"""
|
|
return self._vk_table[vk][
|
|
0
|
|
| (1 if Key.shift in modifiers else 0)
|
|
| (2 if Key.alt_gr in modifiers else 0)]
|
|
|
|
def for_char(self, char):
|
|
"""Reads a virtual key code and modifier state for a character.
|
|
|
|
:param str char: The character.
|
|
|
|
:return: the tuple ``(vk, modifiers)``
|
|
|
|
:raises KeyError: if ``vk`` is an unknown key
|
|
"""
|
|
return self._char_table[char]
|
|
|
|
@functools.lru_cache()
|
|
def _load(self):
|
|
"""Loads the keyboard layout.
|
|
|
|
For simplicity, we call out to the ``dumpkeys`` binary. In the future,
|
|
we may want to implement this ourselves.
|
|
"""
|
|
result = {}
|
|
for keycode, names in self.KEYCODE_RE.findall(
|
|
subprocess.check_output(
|
|
['dumpkeys', '--full-table', '--keys-only']).decode('utf-8')):
|
|
vk = int(keycode)
|
|
keys = tuple(
|
|
self._parse(vk, name)
|
|
for name in names.split()[:4])
|
|
if any(key is not None for key in keys):
|
|
result[vk] = self.Key(*keys)
|
|
return result
|
|
|
|
def _parse(self, vk, name):
|
|
"""Parses a single key from the ``dumpkeys`` output.
|
|
|
|
:param int vk: The key code.
|
|
|
|
:param str name: The key name.
|
|
|
|
:return: a key representation
|
|
"""
|
|
try:
|
|
# First try special keys...
|
|
return next(
|
|
key
|
|
for key in Key
|
|
if key.value._x_name == name)
|
|
except StopIteration:
|
|
# ...then characters...
|
|
try:
|
|
_, char = xorg_keysyms.SYMBOLS[name.lstrip('+')]
|
|
if char:
|
|
return KeyCode.from_char(char, vk=vk)
|
|
except KeyError:
|
|
pass
|
|
|
|
# ...and finally special dumpkeys names
|
|
try:
|
|
return KeyCode.from_char({
|
|
'one': '1',
|
|
'two': '2',
|
|
'three': '3',
|
|
'four': '4',
|
|
'five': '5',
|
|
'six': '6',
|
|
'seven': '7',
|
|
'eight': '8',
|
|
'nine': '9',
|
|
'zero': '0'}[name])
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
class Controller(_base.Controller):
|
|
_KeyCode = KeyCode
|
|
_Key = Key
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Controller, self).__init__(*args, **kwargs)
|
|
self._layout = LAYOUT
|
|
self._dev = evdev.UInput()
|
|
|
|
def __del__(self):
|
|
if hasattr(self, '_dev'):
|
|
self._dev.close()
|
|
|
|
def _handle(self, key, is_press):
|
|
# Resolve the key to a virtual key code and a possible set of required
|
|
# modifiers
|
|
try:
|
|
vk, required_modifiers = self._to_vk_and_modifiers(key)
|
|
except ValueError:
|
|
raise self.InvalidKeyException(key)
|
|
|
|
# Determine how we need to modify the modifier state
|
|
if is_press and required_modifiers is not None:
|
|
with self.modifiers as modifiers:
|
|
vk, required_modifiers = self._layout.for_char(key.char)
|
|
to_press = {
|
|
getattr(evdev.ecodes, key.value._kernel_name)
|
|
for key in (required_modifiers - modifiers)}
|
|
to_release = {
|
|
getattr(evdev.ecodes, key.value._kernel_name)
|
|
for key in (modifiers - required_modifiers)}
|
|
else:
|
|
to_release = set()
|
|
to_press = set()
|
|
|
|
# Update the modifier state, send the key, and finally release any
|
|
# modifiers
|
|
cleanup = []
|
|
try:
|
|
for k in to_release:
|
|
self._send(k, False)
|
|
cleanup.append((k, True))
|
|
for k in to_press:
|
|
self._send(k, True)
|
|
cleanup.append((k, False))
|
|
|
|
self._send(vk, is_press)
|
|
|
|
finally:
|
|
for e in reversed(cleanup):
|
|
# pylint: disable E722; we want to suppress exceptions
|
|
try:
|
|
self._send(*e)
|
|
except:
|
|
pass
|
|
# pylint: enable E722
|
|
|
|
self._dev.syn()
|
|
|
|
def _to_vk_and_modifiers(self, key):
|
|
"""Resolves a key to a virtual key code and a modifier set.
|
|
|
|
:param key: The key to resolve.
|
|
:type key: Key or KeyCode
|
|
|
|
:return: a virtual key code and possible required modifiers
|
|
"""
|
|
if hasattr(key, 'vk') and key.vk is not None:
|
|
return (key.vk, None)
|
|
elif hasattr(key, 'char') and key.char is not None:
|
|
return self._layout.for_char(key.char)
|
|
else:
|
|
raise ValueError(key)
|
|
|
|
def _send(self, vk, is_press):
|
|
"""Sends a virtual key event.
|
|
|
|
This method does not perform ``SYN``.
|
|
|
|
:param int vk: The virtual key.
|
|
|
|
:param bool is_press: Whether this is a press event.
|
|
"""
|
|
self._dev.write(evdev.ecodes.EV_KEY, vk, int(is_press))
|
|
|
|
|
|
class Listener(ListenerMixin, _base.Listener):
|
|
_EVENTS = (
|
|
evdev.ecodes.EV_KEY,)
|
|
|
|
#: A
|
|
_MODIFIERS = {
|
|
Key.alt.value.vk: Key.alt,
|
|
Key.alt_l.value.vk: Key.alt,
|
|
Key.alt_r.value.vk: Key.alt,
|
|
Key.alt_gr.value.vk: Key.alt_gr,
|
|
Key.shift.value.vk: Key.shift,
|
|
Key.shift_l.value.vk: Key.shift,
|
|
Key.shift_r.value.vk: Key.shift}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Listener, self).__init__(*args, **kwargs)
|
|
self._layout = LAYOUT
|
|
self._modifiers = set()
|
|
|
|
def _handle(self, event):
|
|
is_press = event.value in (KeyEvent.key_down, KeyEvent.key_hold)
|
|
vk = event.code
|
|
|
|
# Update the modifier state
|
|
if vk in self._MODIFIERS:
|
|
modifier = self._MODIFIERS[vk]
|
|
if is_press:
|
|
self._modifiers.add(modifier)
|
|
elif modifier in self._modifiers:
|
|
self._modifiers.remove(modifier)
|
|
|
|
# Attempt to map the virtual key code to a key
|
|
try:
|
|
key = self._layout.for_vk(vk, self._modifiers)
|
|
except KeyError:
|
|
try:
|
|
key = next(
|
|
key
|
|
for key in Key
|
|
if key.value.vk == vk)
|
|
except StopIteration:
|
|
key = KeyCode.from_vk(vk)
|
|
|
|
if is_press:
|
|
self.on_press(key)
|
|
else:
|
|
self.on_release(key)
|
|
|
|
|
|
try:
|
|
#: The keyboard layout.
|
|
LAYOUT = Layout()
|
|
except subprocess.CalledProcessError as e:
|
|
raise ImportError('failed to load keyboard layout: "' + str(e) + (
|
|
'"; please make sure you are root' if os.getuid() != 1 else '"'))
|
|
except OSError as e:
|
|
raise ImportError({
|
|
errno.ENOENT: 'the binary dumpkeys is not installed'}.get(
|
|
e.args[0],
|
|
str(e)))
|
|
|