boppreh / keyboard

Hook and simulate global keyboard events on Windows and Linux.
MIT License
3.8k stars 433 forks source link

Windows support #4

Closed fpp-gh closed 7 years ago

fpp-gh commented 8 years ago

Hi,

This pure-python, cross-platform lib is quite impressive. It could help a lot on workstations where adding stuff is severely restricted (ie no pip, etc.).

One thing I'm looking for is to reproduce (at least partly) the abbreviation expansion functionality found in Autohotkey (among others). This replaces text patterns as you type, without hotkeys (ie., type 'tm" followed by space and its gets replaced by a trademark symbol for example).

I have not been able to do this using the add_hotkey method (which is probably normal). Could there be a way to achieve it using the lower-level functions ?

TIA, fp

boppreh commented 8 years ago

Just to make sure: you are proposing a hotkey that detects a sequence of letters, then backspaces them and inserts another sequence of letters? Additionally, inserting arbitrary unicode characters will be a problem. It's OS specific for sure. I'll see what I can do.

And what part of add_hotkey didn't work? You should be able to insert anything on your keyboard using:

add_hotkey('t, m', lambda: write('\b\btrademark'))

Where "\b" is the backspace character.

fpp-gh commented 8 years ago

Just to make sure: you are proposing a hotkey that detects a sequence of letters, then backspaces them and inserts another sequence of letters?

Yes, that's what AutoHotKey does under Windows, a Linux equivalent being AutoKey (also in python).

inserting arbitrary unicode characters will be a problem. It's OS specific for sure.

Always a joy, yes. But in my specific use case it would apply only to Win7 workstations, so probably doable...

And what part of add_hotkey didn't work?

Aha, here comes the fun part. Actually your example worked just fine, as did the one in the README. (except that it would prevent you from typing 'atmosphere', for example, so in a majority of cases a trailing space is needed as a trigger :-) And then, mysteriously, it would not. Nor would anything else I tried afterwards, short of shutting down everything and starting again from scratch...

After much head-scratching, I finally realised I'd been tripped repeatedly (before opening the issue and just now) by this bad habit of mine : testing new stuff at random from the IPython prompt, based more on hunches than reading docs or code :-)

Specifically, I tended to not use spaces between keys (ie. 't,m' instead of 't, m', so the split doesn't work :-), and tried to guess (wrongly) the canonical name for 'space' before I thought of looking it up in the code...

So it happened several times that I would first type a correct example, which worked, then another malformed one. There is no error message or exception in theses cases. And not only does the malformed hotkey not work, of course, but the previous correct one(s) also stop working, and any correct new one you add afterwards doesn't work (and, it seems, can't be unloaded by remove_hotkey).

Once I stopped doing these stupid things everything worked reliably :-)

But I guess a bit more syntax checking and user feedback from add_hotkey would be useful to the unwary... maybe also a convenience function to unload all handlers and empty the dictionary when that happens ?

Anyway, thanks for your follow-up which led me to understand the error of my ways :)

boppreh commented 8 years ago

Oh, that's a very good feedback. Another user requested functionality similar to abbreviations, and I'm still working on it. It'll require a major rewrite of the add_hotkey function, and I'll keep these issues in mind.

boppreh commented 8 years ago

Created add_abbreviation(src, dst) function.

add_abbreviation('tm', '™')

It required a rewrite of the code that maps scan codes to characters (and vice versa), but the codebase is healthier for it. As a consequence the write function became much more powerful. You can now do write('💩'), and it should work on both Linux and Windows.

Since a large chunk of the backend was rewritten I'm not publishing a new version yet. Feel free to check out the code from GitHub and tell me if that's what you had in mind, and if you see any problems.

I haven't forgotten the feedback on hotkey creation. That one depends on the add_hotkey rewrite, coming soon.

fpp-gh commented 8 years ago

Wow, that was lightning fast !!

Will give it a try ASAP and report, thanks for listening !

fpp-gh commented 8 years ago

Okay, here is a first attempt at a report from the trenches... It was a slow day at work, so I spent quite some time trying out the new version.

The target workstation(s) are Win7 with stock Python 3.3.5, and tightly locked down. The only tools available are the good old REPL and Notepad, a dev's dream :-) So I also had my own laptop on the side, which has Win10 and Python 2.7.11.

The good news is that when it works, it works quite well, especially with Py3.

The bad news is that, like previously, there was much hair-pulling and wondering what the heck was going on... It seems to start working and then stop haphazardly from one run and the next, without any apparent difference (although there are so many possible detail changes that it's often hard to tell).

Worse, it tends to blow up the python interpreter on a whim, on both platforms. (I got started ages ago with python 1.5, and in all those years I'm sure I've never seen so many REPLs give up the ghost on me in a single day :-) I suspect that the ctypes interface to the underlying Windows DLL is quite brittle, and the slightest mismatch causes a crash.

There are some differences in the results between the two platforms, probably to the different versions of Windows, or Python, or both.

The Win10/Py27 setup crashed less, but unsurprisingly had more encoding issues. At first the package wouldn't import because winkeyboard.py had accented characters in some comments, and no encoding declaration. I use a language with accents, and as usual 90% of the time was spent trying to solve those problems, with mixed results. Prefixing the hotkey parameter strings with u'...' did wonders for the majority (like à é è ç). But try as I may, I have never succeeded with the circumflex accent (â ê û ô î), they just don't print. Often, but not always, they raise an exception like this :

Traceback (most recent call last):
 File "keyboard\generic.py", line 14, in invoke_handlers
     if handler(event):
 File "keyboard\keyboard.py", line 93, in handler
    callback(*args)
  File "keyboard\keyboard.py", line 113, in <lambda>
    return add_hotkey(', '.join(src + ' '), lambda: write('\b'*len(src) + dst))
  File "keyboard\generic.py", line 31, in wrapper
    return func(*args, **kwds)
  File "keyboard\keyboard.py", line 141, in write
    os_keyboard.type_unicode(letter)
  File "keyboard\winkeyboard.py", line 237, in type_unicode
    structure = KEYBDINPUT(0, (lower << 8) + higher, KEYEVENTF_UNICODE, 0, None)
TypeError: unsupported operand type(s) for <<: 'str' and 'int'

The Win7/Py33 setup crashed a lot, but when it didn't, it worked with all special characters I threw at it (including circumflex accents). However I was suprised to find that I needed to u-prefix some strings here too... (I had never tried Py3 before, but had heard that it was supposed to be natively Unicode ?)

One that doesn't work on either platform is the "AltGr" key, which replaces the right-Alt on many non-QWERTY keyboards and serves to access a third value on the top row keys (and very useful ones :-). Each keydown while hotkeys are in effect raises the following exception :

Traceback (most recent call last):
  File "_ctypes/callbacks.c", line 260, in 'calling callback function'
  File ".\keyboard\winkeyboard.py", line 186, in low_level_keyboard_handler
    names, is_keypad = from_scan_code[scan_code]
KeyError: 541

Side note : the "Windows key" doesn't do anything, good or bad. But it's mostly used for shortcuts and hotkeys, so it would be good to have here, too.

boppreh commented 8 years ago

Wow. I'm not on a Windows machine right now, but I'll get to that later today. Ok, let's go in parts, in no particular order.

TypeError: unsupported operand type(s) for <<: 'str' and 'int'

Typical 2<->3 difference. I've patched the code with an explicit conversion that should work on both.

However I was suprised to find that I needed to u-prefix some strings here too...

That's strange. The u prefix is supposed to be a nop in Python3.

I suspect that the ctypes interface to the underlying Windows DLL is quite brittle, and the slightest mismatch causes a crash.

That's why I placed all those type declarations on winkeyboard.py. Unfortunately it didn't seem to work.

At first the package wouldn't import because winkeyboard.py had accented characters in some comments, and no encoding declaration.

I've added encoding declaration to all files now.

Worse, it tends to blow up the python interpreter on a whim, on both platforms.

That's awful, sorry. And this is going to be a hard one to fix. Are you on 32 or 64 bits?

One that doesn't work on either platform is the "AltGr" key, Side note : the "Windows key" doesn't do anything, good or bad.

No idea what happened to the Windows key. Hopefully it's a canonization problem. AltGr is different from other keys, but it should work. I'll take a look into those later today or tomorrow.

Again, sorry for the troubles, but thank you very much for the honest and detailed feedback. This project is getting much better thanks to your contributions.

fpp-gh commented 8 years ago

No need to apologize for anything, quite the opposite ! That's a neat project you have here, with a lot of potential. And we'll get it in shape yet, although the crazy constraints on the target machines I have to work with certainly don't help :-) Actually I think I'll dump them for the time being, and test only on personal, "normal" machines until we have a known stable base to benchmark with.

And I do like a good challenge, especially when it clearly fills a need... I don't understand the low-level stuff well enough to dig in, but tinkering, breaking things and reporting are right up my alley :-)

Results of the day's experiments to follow soon...

fpp-gh commented 8 years ago

Here's the mostly very good news of today's tests on the W10/Py27 platform (my laptop):

Not 100% sure how or why (more on that later), but I finally have a setup on that machine that so far has proved quite reliable ! (as in : every time, durably, no crashes... :-)

Obviously testing was much easier with those conditions, and my test bed of abbreviations/hotkeys behaved rather well.

In particular I can confirm that the circumflex accent issue is indeed fixed, which is great news as it was my main usability issue yesterday.

Emboldened, I took my entire list out of the AutoHotkey config file, and reformatted them as a list of pythons tuples, all 42 of them. The script loaded everything just fine. The speed of text replacement was on par with AutoHotkey's, very snappy.

With that larger and more real-life sample came new discoveries :

On the whole, a very encouraging session, leading to high hopes and new ideas to implement once the remaining issues are ironed out :-)

fpp-gh commented 8 years ago

Now for the very mixed bag of today's tests on the W7/Py33 platform (office workstation)...

Clearly these machines are a pain to work with : almost stock Windows, with heavy-handed GPO policies disabling any user setting changes (ever seen an empty Control Panel ? :-). Of course there isn't a regular Python install on those : it's just that they come standard with LibreOffice, and someone forgot that Python comes bundled with that, deep down... There is some kind of "DLL Hell" going on in there, with various versions of executables and DLLs scattered around. Most don't play well together.

After a lot of messy mix 'n match experiments, I stumbled upon a combination that would actually start, not crash immediately, even load a script and give me exceptions instead of crashes, so I could debug my stupid Py3 newbie mistakes (in Notepad :-).

And for a while there, I really, really believed I had found the magic formula, like on my laptop : I had a session that worked, did not crash, and expanded abbreviations like mad ! Then I happily stopped it to add something to the script, and could never restart it again... even after reboots etc.

I'm so fed up with that, I'm going to leave them alone for a while and concentrate on "normal" machines where you can tell what works and what doesn't...

But in the short while where it worked (with the same full list of hotkeys), I had time to see two notable differences in how the script behaves under Py3 :

I couldn't repeat the experiment so I'm not quite sure about the second one ; but if so, it's the exact reverse of what happens in Py2 :-)

fpp-gh commented 8 years ago

Not 100% sure how or why (more on that later), but I finally have a setup on that machine that so far has proved quite reliable

I wanted to add this part because it also leads to a question at the end...

The reason I'm not sure is that this is the result of much trial by error, hit-or-miss tinkering, and often afterwards you don"t really recall which one did it exactly :-)

Certainly a lot of it was related to the encoding of the python script, which contains the strings for the hotkeys. I tried a number of variants, recoding the non-ascii chars as I went, from utf-8 right down to good old cp1252, the standard Windows encoding hereabouts (with the DOS console itself is stuck at the even older cp850). I don't claim to understand all this stuff, but clearly cp1252 was the easiest to get working, and the only one to correctly print all non non-ascii chars I threw at it.

Next was the testing procedure itself (and the question that comes with it) : the hotkeys are only active while the script is running, so testing it (and, some day, using it :-) requires keeping the code alive somewhere.

On the first batch of tests I did this by typing or pasting lines of code in the python REPL (or ipython, on my machines), which led to many crashes.

On the second batch I created a proper python script with the hotkeys and 'raw_input' or 'input' at the end. Then I could just launch it, test in some other window while it sat there in its DOS console, then type Enter in it to end. Coincidence or not, that was when things started being usable.

As a side note, on the cursed machine where it only worked just once, the DOS console did not close after Enter and sat there cursor blinking... meaning it has silently crashed without an exception output.

Finally, here comes the question-for-the-future : when this package gets to the point where we'll want it active 100% of the time (like I do for AHK), how will we go about that ? Daemonize it ? Give it a tk/qt/wx/etc. UI so it can have an icon in the tray ? Some other way ?...

boppreh commented 8 years ago

add_hotkey is caseless by design. There should be no difference between "ctrl+F" and "ctrl+f". However the keys "/" and "?" should be separate. I think this is caused by the same bug that makes numpad keys move the cursor: the layout is being registered wrong on load. I'll take a look.

Are those machines 32 or 64 bit? That makes a difference in ctypes.

I'll have access to Windows 8 and Windows 10 machines in the weekend. I'll report any fixes.

Again, thanks for doing this.

fpp-gh commented 8 years ago

add_hotkey is caseless by design. There should be no difference between "ctrl+F" and "ctrl+f".

That makes sense, yes. I guess that's why in AHK the semantics for declaring hotkeys and abbreviations are quite different.

Are those machines 32 or 64 bit? That makes a difference in ctypes.

Yes, sorry, forgot to answer that one : all Windows versions are 64, all Python versions are 32.

Edit: there doesn't seem to be much difference between Windows 7 and 10 : the script behaves much the same on my W10 laptop and my W7 PC at home.

boppreh commented 8 years ago

From https://msdn.microsoft.com/en-us/library/windows/desktop/ms644967(v=vs.85).aspx :

The hook procedure should process a message in less time than the data entry specified in the LowLevelHooksTimeout value in the following registry key: HKEY_CURRENT_USER\Control Panel\Desktop The value is in milliseconds. If the hook procedure times out, the system passes the message to the next hook. However, on Windows 7 and later, the hook is silently removed without being called. There is no way for the application to know whether the hook is removed.

(I don't have this value in my registry, so I don't know what kind of magnitude it is)

This may explain why it stops working after a while. When you couple the slowness of Python, a large number of hotkeys to be checked for matches, and high usage from user applications, it's easy to see the timeout being exceeded.

The alternative is to process hotkey matches in yet another thread, but that removes the ability to block keys (add_hotkey(..., blocking=True)). This parameter has given me a lot of trouble before, so it might be for the best.

Edit: I tried a sleep(1) inside the handler. It delayed every key press as expected, and still didn't trigger the Grim Reaper of callbacks. Still a plausible explanation for the problem.

Edit2: More information: https://blogs.msdn.microsoft.com/alejacma/2010/10/14/global-hooks-getting-lost-on-windows-7/ . So using RAW INPUT may be an alternative.

fpp-gh commented 8 years ago

Brave new world... Meantime I'd found this tidbit on a Intel support page :

Resuming Operation Works with a Long Delay On some systems, "resume" attempts may take about five seconds. This issue occurs only if the analyzing application uses a keyboard hook procedure of the type WH_KEYBOARD_LL, and the registry key LowLevelHooksTimeout is set to a larger value than the default (300 ms). If you regularly use Pause/Resume and resuming takes longer than you are comfortable with, use a registry editor such as RegEdit to modify the value of this registry key back to the default value of 300 ms.

The "5 seconds" part sounds like a cheap shot at MS : it seems that installing Visual Studio silently bumps the default value to 5000 :-)

Indeed this sounds plausible as it explains many things : the office workstation is by far the slowest of the three (old CPU, short on RAM, ravenous AV etc.) ; the console hanging on exit without error ; and the fact that it did run fine once, probably in a rare moment when the system was idle :-)

Maybe Raw Input might work better, but it's probably another rat's nest :-)

fpp-gh commented 8 years ago

Brave new world... Meantime I'd found this tidbit on a Intel support page :

Resuming Operation Works with a Long Delay On some systems, "resume" attempts may take about five seconds. This issue occurs only if the analyzing application uses a keyboard hook procedure of the type WH_KEYBOARD_LL, and the registry key LowLevelHooksTimeout is set to a larger value than the default (300 ms). If you regularly use Pause/Resume and resuming takes longer than you are comfortable with, use a registry editor such as RegEdit to modify the value of this registry key back to the default value of 300 ms.

The "5 seconds" part sounds like a cheap shot at MS : it seems that installing Visual Studio silently bumps the default value to 5000 :-)

Indeed this sounds plausible as it explains many things : the office workstation is by far the slowest of the three (old CPU, short on RAM, ravenous AV etc.) ; the console hanging on exit without error ; and the fact that it did run fine once, probably in a rare moment when the system was idle :-)

Maybe Raw Input might work better, but it's probably another rat's nest :-)

boppreh commented 8 years ago

It's been a while since I had access to a Windows machine, so I couldn't check the crashes and stability issues.

I made a lot of improvements on the Linux side of things (it now knows exactly what character is typed for each combination of key and modifiers), but I think that doesn't interest you.

I did however rewrite the add_abbreviation function. It doesn't depend on add_hotkey anymore, so It is now case-sensitive, works better with modifier keys, and is generally saner.

Finally, while I couldn't test on Windows (I'll release a new version once I can), I noticed a bug in the way abbreviations were handled. Previously the replacement would start being typed as soon as the space key was detected. In Windows this means before the rest of the system had a chance to process it. Therefore in Linux we had to backspace the space, while in Windows it was typed after the replacement. Aside from being an inconsistency, maybe this is why the crashes were happening, having nested events like that.

I should have access to a Windows machine in the next days. Before that, feel free to check the code on the master branch, I've made a lot of changes.

fpp-gh commented 8 years ago

Thanks a lot for the update !

I'll try the new code ASAP (which may not be very soon :-), starting on a "sane" Windows machine first, then on the shackled one if indeed it works better. And, as an aside, I'm definitely interested in Linux machines too, they just bug me less than the other :-)

fpp-gh commented 8 years ago

Hi,

Don't have time but couldn't resist a quick check :-) Seems there is a typo in init.py, line 282 :

if name in triggeres and matched:

(triggeres instead of triggers)

After this is corrected I can define an abbreviation but when I try to expand it I get this :

Exception in thread Thread-12:
Traceback (most recent call last):
  File "C:\Python\lib\threading.py", line 801, in __bootstrap_inner
    self.run()
  File "C:\Python\lib\threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "keyboard\__init__.py", line 92, in <lambda>
    _Thread(target=lambda: _time.sleep(delay) or fn(*args)).start()
  File "keyboard\__init__.py", line 326, in <lambda>
    callback = lambda: write(replacement, restore_state_after=False)
  File "keyboard\__init__.py", line 382, in write
    for modifier in modifiers:
TypeError: 'bool' object is not iterable

This is on the W10/Py2.7.11 machine, if it matters.

boppreh commented 8 years ago

That's weird, the test were passing. Probably I did something stupid in the coding spree of yeserteday.

I'll fix it as soon as I get home, in about four hours.

fpp-gh commented 8 years ago

Same here, no hurry :-)

boppreh commented 8 years ago

Ok, now the main module (OS independent part) has 100% test coverage. I think this rules out any typos or stupid typing mistakes on that part. Please update to the latest version on the master branch.

Unfortunately I've made a lot of changes recently without being able to test on Windows. As soon as I get my hands on one I'll publish a new version on PyPI.

Your feedback is as always extremely appreciated.

fpp-gh commented 8 years ago

I'm glad you're not mad at me for bringing mostly bad news :-)

Right now, importing the module fails on Windows:

D:\python\kb_test>python
Python 2.7.11 (v2.7.11:6d1b6a68f775, Dec  5 2015, 20:32:19) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import keyboard
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "keyboard\__init__.py", line 11, in <module>
    from. import _winkeyboard as _os_keyboard
  File "keyboard\_winkeyboard.py", line 167, in <module>
    setup_tables()
  File "keyboard\_winkeyboard.py", line 151, in setup_tables
    name = normalize_name(name.replace('Right ', '').replace('Left ', ''))
  File "keyboard\_keyboard_event.py", line 173, in normalize_name
    raise ValueError('Can only normalize string names. Unexpected '+ repr(name))
ValueError: Can only normalize string names. Unexpected u'\x1b'
>>>

I guess we really need to wait until you get your hands on that Windows machine :-)

NicoPy commented 8 years ago

This a Python2 problem (it works with Python3) related to unicode/str management differences between the 2 Python versions.

_Winkeyboard.py, line 134 : name_buffer = ctypes.create_unicode_buffer(32)

name_buffer is passed to normalize_name()

_keyboard_event.py, line 171 : normalize_name() is waiting for a str string.

boppreh commented 8 years ago

Hey NicoPy, thanks for the tip! I won't risk changing the file without testing, but if you want to create a Pull Request I'll gladly accept it.

fpp-gh commented 8 years ago

Yes, I should have mentioned that, sorry : I briefly tried this version on the darned "target machine" (the one that blows up :-) with its Py3.3 interpreter, and the import didn't fail. It did blow up when I tried to call add_abbreviation though. but that's just its bad temper I think :-)

NicoPy commented 8 years ago

@boppreh Unfortunately, I don't have time to work on it now. In case I find some time to spend on it, how do you usualy manage python2/python3 str differences ?

boppreh commented 8 years ago

Got my Windows computer, made lots of internal changes, tests coverage is now 100% on the OS-independent parts for both mouse and keyboard (is anybody using the mouse part yet?), and I manually tested the Windows part on Python3.

Please give another go and tell me what you think.

fpp-gh commented 8 years ago

Thanks a lot, but so far not much more success on my side...

On W10+Py2: First I had to add from __future__ import print_function at the top of _winkeyboard.py, else I had a syntax error in the keyboard.hook(print) at the end. After that, the module imports but when I try to add an abbreviation I get the same exception as in my message from oct. 18th above (the one with Unexpected u'\x1b').

On W7 + Py3: Module imports OK. Using add_abbreviation blows up like this:

>>> keyboard.add_abbreviation('qq','quelque')
Unhandled exception in thread started by
Traceback (most recent call last):
  File "C:\Program Files (x86)\LibreOffice 5\program\python-core-3.3.3\lib\threading.py", line 890, in _bootstrap_inner
    self._started.set()
AttributeError: 'Thread' object has no attribute '_started'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "C:\Program Files (x86)\LibreOffice 5\program\python-core-3.3.3\lib\threading.py", line 878, in _bootstrap
    self._bootstrap_inner()
  File "C:\Program Files (x86)\LibreOffice 5\program\python-core-3.3.3\lib\threading.py", line 944, in _bootstrap_inner
    self._stop()
  File "C:\Program Files (x86)\LibreOffice 5\program\python-core-3.3.3\lib\threading.py", line 953, in _stop
    self._block.acquire()
AttributeError: 'Thread' object has no attribute '_block'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "C:\Program Files (x86)\LibreOffice 5\program\python-core-3.3.3\lib\threading.py", line 880, in _bootstrap
    if self._daemonic and _sys is None:
AttributeError: 'Thread' object has no attribute '_daemonic'

Looks like my Python 3.3.3 is too old ? :-)

boppreh commented 8 years ago

I just fixed a source of segfaults on Windows (ToUnicode accepts the length of the buffer in characters, not bytes). I'm not sure if this was the only one, but I haven't seen any more segfaults.

The Python3 error is just bizarre. Those properties exist on Threads since at least 2.7 and it just makes no sense. I'll investigate further.

And the print is totally a stupid mistake. I just don't understand how the tests pass on Python2 with a syntax error on a file (even if it's not executed). It's fixed.

Thanks again for your patience.

boppreh commented 8 years ago

By the way, adding a Unicode character as abbreviation should work in Python2 now. You still have to prefix your strings with u in Python2, though.

fpp-gh commented 8 years ago

Heh, you need a lot more patience than I do ! And you know what ? This time I have good news :-)

Just tested this at home on W7/Py2 and... everything... just... WORKS !

No crashes, no hiccups, it just spits out abbreviations like there's no tomorrow, and with very good performance too (at least as fast as Autohotkey). All the accented chars print fine, even the pesky circumflexed vowels and corner cases like "ÿ". Bravo, and thanks for not giving up ! :-)

In my entire keyboard test suite there are only ten characters left that don't print properly (or at all): still the same ones as in my second Aug. 5 report above.

I'll test on the W10/Py2 laptop later on but I don't expect any different results. I won't have access to the much-hated target W7/Py3 machine until next Wednesday.

How was it again ? Ah yes, Thanks again for your patience ! :-)

PS: I confirm that abbreviations containing unicode chars work just fine.

boppreh commented 8 years ago

Currently I'm on Win10 and manually testing with both Py2 and Py3.

The ten digits problem is because the event sending function doesn't respect the current NumLock state. I'm investigating.

Meanwhile I've made a quick workaround. When you ask it to press a key that is available on both numpad and regular keyboard, it'll prefer the regular key. I couldn't reproduce the / problem, but maybe it was related (the numpad has a / key) and got fixed too.

Currently known issues:

fpp-gh commented 8 years ago

Again, passed with flying colours !

The workaround solves both the digits and the slash issues (I thought they were related too, although the plus, minus and star keys are all on the keypad, and never had a problem...).

Haven't seen any error in testing since yesterday either.

I understand the importance of modifiers such as NumLock, ALtGr etc. for macro record/play, but right now, with all characters working, I have all I need for the abbreviation part :-)

BTW, on the hotkey side, have you tried do anything with the "Windows Key" ?

boppreh commented 8 years ago

That's great to hear!

Unfortunately your last remark brings me bad news. The Windows key has been supported since the very first version, but trying it now I see there is a problem.

The first step when creating a hotkey is to canonicalize the combination received, which transforms key descriptions like win into known scan codes like 92, then groups them in parts and steps. But there are two win keys on the keyboard! So when it transforms into a scan code it has to choose one of them, and the other is ignored. Usually this is not a problem because it chooses the left ctrl, left alt, etc, which are the more commonly used, and why I didn't notice this problem before. But in this case it selects the right win key (my keyboard doesn't even have one!).

The correct behavior would be to match both keys if the user passed a name, but only one if the user passed a scan code. Therefore the canonicalize function must respect the user input type.

This will take a while to think through, but thankfully it's on the OS-independent part and easier to test. And will make the library saner, which is always a plus.

boppreh commented 8 years ago

The deed is done. https://github.com/boppreh/keyboard/commit/deb8b3810c9e175d50d8d290d0a1a2cc56b43a7a

Now wait('win') and wait(92) behave differently: win matches both left and right Windows keys, while 92 matches only the right one (on my test USB keyboard). I think it makes more sense, but this is a fairly important change and I'll keep my eyes open for any problems. Interestingly the number of lines affected was tiny and actually reduced line count (7 lines became 3)

fpp-gh commented 8 years ago

Aha, so probably it's why I thought it wasn't supported at all, early on (end of my long post on Aug. 4). Good news if it can be made to work, as I use it extensively for hotkeys :-)

Speaking of which, Autohotkey does have a very useful feature for hotkeys : a conditional call (it is actually a programming engine, just a very ugly Basic-like one :-) that only fires a hotkey if the active window has specific string in its name. Any chance something like this could be added here ?

boppreh commented 8 years ago

Hmm. That is a whole 'nother can of worms. Adding support for e.g. gamepads would be ok, but I draw the line at screen information. Remember I want to support even a Raspberry Pi running without graphical interface.

I do want to replicate this kind of feature, but not now and not in this library. Until then you I'm afraid you'll have to write your own code for this type of logic. To help you start here's what I did in another project to get the current window name: https://github.com/boppreh/activity/blob/master/watcher_daemon.pyw#L58 (requires the pywin32 library).

Again, thank you very much for your in-depth feedback. You have made this library much better.

fpp-gh commented 8 years ago

tests coverage is now 100% on the OS-independent parts for both mouse and keyboard (is anybody using the mouse part yet?) Huh, mouse part, what mouse part ? It's not even mentioned in the front page :-) Just took a look, sounds fun, must try that too :-)

About the window name stuff, you're right of course, it belongs in the hotkey callback, importing from other libs as needed. Is there no pure-python way to do it ?

Re: feedback : well I have a use for this, and it's actually fun, so don't think you'll get rid of me that easily :-)

boppreh commented 8 years ago

The mouse part is complete and stable. I want to put it front and center, but because the library is named keyboard I'm still deciding on how to do it (maybe rename to mouse_and_keyboard?). But yeah, it works exactly like the keyboard part, even with some identical functions (hook(callback), is_pressed(LEFT), play(record()), etc).

There is probably a way to translate the pywin32 calls into pure ctypes, like I did for the mouse and keyboard, but it takes work. Or maybe some other library already did, I'm not sure. The activity project is quite old.

fpp-gh commented 8 years ago

Yup, saw that in the mouse API... gave me some ideas :-) Make you could rename the lib to something like "user_input" ?

And I also had in mind your work with ctypes here in the second question. I'll fish around to see if anyone has done that, because I certainly won't be able to install pywin32 everywhere I need it :-)

boppreh commented 8 years ago

There's already inputs (very similar, but has only low level listeners) and PyUserInput (incomplete, lots of dependencies). Also, mouse is also already taken but not in use. And I want to make it clear it's not just reading events, but also writing, and input may give the wrong impression. I'm stumped.

Not wanting to install pywin32 is the reason I started this whole thing in the first place...

fpp-gh commented 8 years ago

I'm afraid there is a regression in this latest version, at least with Py2.7 (tried with both W7 and 10): digits and slash don't print any more, and hotkeys containing them produce huge volumes of traceback... I'll copy one here if you can't reproduce it on your side. Edit: also, expanded strings with '\n' in them, and '>' (but not '<').

boppreh commented 8 years ago

Not that easy to reproduce:

screenshot

Edit: the problem was in the send function, which was not expecting the canonicalized combinations to contain names. Fixed in https://github.com/boppreh/keyboard/commit/9be00595733e047fcd5061592b68c0b4d1b171d1 . I don't know how this escaped the tests, but I'll be adding one test specifically for this case to avoid further regressions in the future.

Edit2: we now have a to_scan_code function, which may be helpful for end users too. Also, the problem fixed involved only send. I couldn't reproduce any error on add_hotkey.

fpp-gh commented 8 years ago

Yup, all back to normal now, thanks :-)

fpp-gh commented 8 years ago

Re: find active window name without pywin32... Found this example, seems to work quite well:

import ctypes, time
GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
GetWindowText = ctypes.windll.user32.GetWindowTextW
while True:
    act_id = GetForegroundWindow()
    length = GetWindowTextLength(act_id)
    buff = ctypes.create_unicode_buffer(length + 1)
    GetWindowText(act_id, buff, length + 1)
    print(buff.value.encode('utf-8'))
    time.sleep(5)

source

boppreh commented 8 years ago

Thanks, if I can get a similar snippet for Linux maybe I'll create a utils submodule. Getting screen size would be useful for mouse movement, and screenshots would be nice too. But that's for the future.

NicoPy commented 8 years ago

@fpp-gh I've been able to use ctypes.windll.user32.SetWinEventHook() to get events when the foreground window changes. This avoids polling for changes. However, it seems to be working only when the calling app has a window.

@boppreh You want to change the name of Keyboard ? What about GUIOT : Great User Input Output Tool ? 😉

fpp-gh commented 8 years ago

Thanks Nico. I just copied the example I found and made sure that it works. If I were to use ctypes API for conditional macros/hotkeys, there would be no loop/polling, just one call at the start of the callback, so no real need for a hook...

NicoPy commented 8 years ago

I don't know if GetForegroundWindow() is a "heavy" function but calling it in the callback can lower the performance of your app. However, if your callback is triggered at a low rate, this is ok.