Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Key, Text and Paste action support for X11 and Mac OS using pynput #72

Merged
merged 8 commits into from
Apr 23, 2019

Conversation

drmfinlay
Copy link
Member

Re: #8, #66.

I've implemented the Key, Text and Paste actions for Mac OS and Linux/X11 using pynput. Dragonfly's documentation has been updated to mention Mac OS and X11 support for Key and Text. Dragonfly's Mouse action can also be implemented with pynput.

Windows keyboard support is unaffected by these changes; dragonfly's own win32 keyboard implementation is still used.

The dragonfly/action/keyboard.py file is now a sub-package that imports different Keyboard, KeySymbols and Typeable classes depending on the platform. The virtual key codes in typeables.py have been moved into separate files for win32 and pynput. This design allows supporting other platforms in the future (e.g. Wayland) or using different libraries for current platforms. A warning message will be printed if the current platform isn't supported.

As there are quite a few changes in this PR, it would be great if others could look over it and/or test it to make sure I haven't broken anything.

X11

pynput doesn't currently include virtual key codes for numpad and media keys on X11. I've added them in manually using key symbols defined in a few Xorg header files on my system. The media keys appear to be vendor-specific and might not work with all X11 server implementations, but should work on Debian-based systems like Ubuntu. The F13-24 keys don't work properly for some reason, but I don't think this is a big problem. All the other keys seem to work correctly from my testing.

The "XDG_SESSION_TYPE" environment variable must be set to "x11" for X11 keyboard support to work. This variable is typically set automatically, but if it isn't set for some reason, it can be set manually in the ~/.profile file or elsewhere.

Mac OS

Some keys don't work properly on Mac OS. Some have been removed/rebound by Apple, e.g. insert, pause, and scroll/num lock. Some just don't get typed properly. For example ":" will be typed as "a". There is a pynput issue on that which I might try to fix myself, as I suspect I know what the problem is.

Lots of warning messages about unsupported keys will be printed when dragonfly is imported on a mac. This is normal. ValueErrors will also be raised if unsupported keys are typed.

Dragonfly's typeable and keyboard related files have been heavily
modified to do this:

- The *dragonfly/actions/keyboard.py* file is now a sub package
  instead.
- The Windows Keyboard class has been moved to a new file:
  *dragonfly/actions/keyboard/_win32.py*.
- KeySymbols classes for supported platforms have been defined that
  map common key names to platform-specific key symbols, e.g.
  "LSUPER" -> "win32con.VK_LWIN".
- "from dragonfly import Keyboard, Typeable" (or equivalent) will
  now import classes for the current platform.
Python 2.7 raises errors in 'except' blocks as expected,
but Python 3 doesn't. This commit changes Key to check if the
typeable exists using 'get()' instead of catching a KeyError.
This is possible now that the Key and Text actions are working.
The system clipboard is used through the pyperclip Clipboard class
on Mac OS and X11.

This commit also fixes a bug where None can be passed to
pyperclip.copy() and raise an error.
@drmfinlay drmfinlay added Enhancement Enhancement existing feature Linux/X11 Issue regarding Linux or X11 support MacOS Issue regarding Apple PC OS labels Apr 14, 2019
@drmfinlay drmfinlay self-assigned this Apr 15, 2019
The "xev" program is used to capture key presses, so it must be
installed. This script is not a unit test file.
@drmfinlay
Copy link
Member Author

I've added a test script for X11 using the xev program. It will need to be installed for the script to work. It should print two lines from xev per key press: one down event and one up event. For example:

    state 0x0, keycode 65 (keysym 0x20, space), same_screen NO,
    state 0x0, keycode 65 (keysym 0x20, space), same_screen NO,

I've added a delay so that the output can be observed as the keys are pressed. The script will press keys like insert, numlock, caps lock, etc twice to attempt to maintain state as it was before the script was started.

Errors can occur if keys are not included in the current XKB keymap (this is why the F13-24 keys didn't work for me) or if keys have been remapped (e.g. the compose key has been set to right alt).

@shervinemami
Copy link
Contributor

Hi @Danesprite, I'm not sure how you want me to test the script, but on a Linux (Linux Mint 19 Cinnamon based on Ubuntu 18.04) machine I cloned & installed dictation-toolbox/dragonfly.git, then ran the script with both Python 2.7.15rc1 and 3.6.7, then clicked on the xev window, then pressed a few keys. I got errors in both Python 2 and 3 but they are quite different, so I'm showing both here.

Python 2:

When I click on the xev window and press keys, it prints the 2 lines for each key, but after roughly 10 seconds it crashes with this error message while leaving the xev window open in the background:

/Linux/dragonfly/dragonfly/actions $ python _test_x11_text_key.py 
Please ensure xev is focused...
    state 0x0, keycode 43 (keysym 0x68, h), same_screen YES,
    state 0x0, keycode 44 (keysym 0x6a, j), same_screen YES,
    state 0x0, keycode 43 (keysym 0x68, h), same_screen YES,
    state 0x0, keycode 44 (keysym 0x6a, j), same_screen YES,
    state 0x0, keycode 45 (keysym 0x6b, k), same_screen YES,
    state 0x0, keycode 45 (keysym 0x6b, k), same_screen YES,
    state 0x0, keycode 46 (keysym 0x6c, l), same_screen YES,
    state 0x0, keycode 46 (keysym 0x6c, l), same_screen YES,
--------------------Starting Key tests--------------------
Done.
--------------------Starting Text tests-------------------
Traceback (most recent call last):
  File "_test_x11_text_key.py", line 129, in <module>
    main()
  File "_test_x11_text_key.py", line 121, in main
    Text(text, pause=0.1).execute()
  File "/usr/local/lib/python2.7/dist-packages/dragonfly2-0.12.0-py2.7.egg/dragonfly/os_dependent_mock.py", line 33, in mock_dyn_str_action
    return DynStrActionBase(*args, **kwargs)
TypeError: __init__() got an unexpected keyword argument 'pause'

Python3:

On startup it shows an error message but continues running anyway:

/Linux/dragonfly/dragonfly/actions $ python3 _test_x11_text_key.py 
Please ensure xev is focused...
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "_test_x11_text_key.py", line 102, in print_key_events
    if "keysym" in line:
TypeError: a bytes-like object is required, not 'str'

Then it shows some more error messages but still continues:

--------------------Starting Key tests--------------------
keyboard: Failed to type key code <65452>: [(BadValue(<Xlib.display._BaseDisplay object at 0x7f97e725c0b8>, b'\x00\x02\xab\x02\x00\x00\x00\x00\x02\x00\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), None)]
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/dragonfly2-0.12.0-py3.6.egg/dragonfly/actions/keyboard/_pynput.py", line 264, in send_keyboard_events
    cls._controller.touch(key, down)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_base.py", line 425, in touch
    self.press(key)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_base.py", line 379, in press
    self._handle(resolved, True)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_xorg.py", line 207, in _handle
    dm.keysym_to_keycode(key.vk))
  File "/usr/lib/python3.6/contextlib.py", line 88, in __exit__
    next(self.gen)
  File "/usr/local/lib/python3.6/dist-packages/pynput/_util/xorg.py", line 78, in display_manager
    raise X11Error(errors)
pynput._util.xorg.X11Error: [(BadValue(<Xlib.display._BaseDisplay object at 0x7f97e725c0b8>, b'\x00\x02\xab\x02\x00\x00\x00\x00\x02\x00\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), None)]
keyboard: Failed to type key code <65452>: [(BadValue(<Xlib.display._BaseDisplay object at 0x7f97e725c0b8>, b'\x00\x02\xad\x02\x00\x00\x00\x00\x02\x00\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), None)]
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/dragonfly2-0.12.0-py3.6.egg/dragonfly/actions/keyboard/_pynput.py", line 264, in send_keyboard_events
    cls._controller.touch(key, down)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_base.py", line 427, in touch
    self.release(key)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_base.py", line 412, in release
    self._handle(resolved, False)
  File "/usr/local/lib/python3.6/dist-packages/pynput/keyboard/_xorg.py", line 207, in _handle
    dm.keysym_to_keycode(key.vk))
  File "/usr/lib/python3.6/contextlib.py", line 88, in __exit__
    next(self.gen)
  File "/usr/local/lib/python3.6/dist-packages/pynput/_util/xorg.py", line 78, in display_manager
    raise X11Error(errors)
pynput._util.xorg.X11Error: [(BadValue(<Xlib.display._BaseDisplay object at 0x7f97e725c0b8>, b'\x00\x02\xad\x02\x00\x00\x00\x00\x02\x00\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), None)]

Then it seems to correctly press the many keyboard keys. I can confirm that it changes my console to fullscreen (by hitting F11) and opens my guake terminal (by hitting F12) just before it prints the alphabet:

^[OP^[OQ^[OR^[OS^[[15~^[[17~^[[18~^[[19~^[[20~^[[21~/Linux/dragonfly/dragonfly/actions ^[[1;5D^[[1;5H^[fDone.
--------------------Starting Text tests-------------------                                                                                                                                    
^AabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#%^&*()_+`~[]{}|\;'".<>?=,:-/                                                                                               
        Done.                                                                                                                                                                                 
Stopping xev.

But I notice that with Python 3, even if I keep pressing keys in the xev window the whole time it's running, none of my pressed keys are shown in the console. (Even though it did work in Python 2)

@drmfinlay
Copy link
Member Author

Hi Shervin. Thanks for testing this! :-)

The Python 2 error you got indicates the action-pynput branch version isn't being used. Try running python dragonfly/actions/_test_x11_text_key.py in the root directory of the clone so that the local version of dragonfly is imported. I'll fix dragonfly's mocking code to prevent errors like that; the Text action should have just done nothing in that case. Hopefully we won't need mocking for much longer though.

The first Python 3 error didn't occur for me because I tested with Python 3.5. I think the keys weren't showing in the console because the output from xev wasn't being printed after that error. I'll fix the error so that it works with bytes objects.

The second Python 3 error means that key code 65452 (0xffac) cannot be typed for some reason. This is the "KP_Separator" or "npsep" number pad key which typically types either "," or ".". The key's behaviour makes it a bit of a special case and I guess that pynput has some trouble simulating it. I don't think this is a big problem though because that key isn't something you would normally simulate. The error also doesn't occur for me on Debian 9, but does on Lubuntu, so it appears to be system-dependent.

Sorry about the fullscreen and guake terminal issues. I'll change the script to press F11 twice, but it would be difficult to make it work nicely in every environment.

@shervinemami
Copy link
Contributor

Oh you're right, I had installed the master branch on my system for Python2! After uninstalling it then running with Python 2 from the root folder, I get all the expected behaviour (ie: alphabet & Function keys pressed in the Key test and alphabet pressed in the Text test). It does show these errors, even though it does work:

    state 0x0, keycode 86 (keysym 0xffab, KP_Add), same_screen YES,
    state 0x0, keycode 86 (keysym 0xffab, KP_Add), same_screen YES,
keyboard: Failed to type key code <65452>: [(BadValue(), None)]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/dragonfly2-0.12.0-py2.7.egg/dragonfly/actions/keyboard/_pynput.py", line 264, in send_keyboard_events
    cls._controller.touch(key, down)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_base.py", line 425, in touch
    self.press(key)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_base.py", line 379, in press
    self._handle(resolved, True)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_xorg.py", line 207, in _handle
    dm.keysym_to_keycode(key.vk))
  File "/usr/lib/python2.7/contextlib.py", line 24, in __exit__
    self.gen.next()
  File "/usr/local/lib/python2.7/dist-packages/pynput/_util/xorg.py", line 78, in display_manager
    raise X11Error(errors)
X11Error: [(BadValue(), None)]
keyboard: Failed to type key code <65452>: [(BadValue(), None)]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/dragonfly2-0.12.0-py2.7.egg/dragonfly/actions/keyboard/_pynput.py", line 264, in send_keyboard_events
    cls._controller.touch(key, down)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_base.py", line 427, in touch
    self.release(key)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_base.py", line 412, in release
    self._handle(resolved, False)
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/_xorg.py", line 207, in _handle
    dm.keysym_to_keycode(key.vk))
  File "/usr/lib/python2.7/contextlib.py", line 24, in __exit__
    self.gen.next()
  File "/usr/local/lib/python2.7/dist-packages/pynput/_util/xorg.py", line 78, in display_manager
    raise X11Error(errors)
X11Error: [(BadValue(), None)]

(But seems to be working fine in Python 2 for all the other keys).

- Press a few more keys twice: F11, Super_L and Super_R.
- Decode lines from 'xev' before using them.
- Remove the "npsep" key because it sometimes doesn't work.
@drmfinlay
Copy link
Member Author

Good to hear it's all working apart from the separator key. I've fixed the bytes error, so the script should run with Python 3.6 now.

I've just removed the separator key from the tests. The "KP_Decimal" / "npdec" key should be good enough. One benefit of using xdo/xdotool instead is that it can type keys that aren't in the current key map.

@shervinemami
Copy link
Contributor

Yep I can confirm that both Python 2.7.15rc1 and 3.6.7 are running perfectly now :-)

@drmfinlay
Copy link
Member Author

Thanks again for the testing! I'll merge this soon. :-)

- Make each mocked action refer to a class instead.
- Replace mock_dyn_str_action with a mocked Mouse class, as that
  action is the only DynStrActionBase sub-class left that isn't
  implemented for other platforms.
@drmfinlay drmfinlay merged commit 2c4754d into master Apr 23, 2019
@drmfinlay drmfinlay deleted the action-pynput branch April 23, 2019 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement Enhancement existing feature Linux/X11 Issue regarding Linux or X11 support MacOS Issue regarding Apple PC OS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants