pynput keyboard listener causes delays - python

I am making a program that toggles on and off by a certain key on the keyboard (using pynput). I placed the keyboard listener loop in the first thread, and the action loop in the second.
The problem is that after I start the code, it doesn't listen to the keyboard immediately, only after 9-10 seconds have passed. And sometimes it refuses to react to Esc button, and sometimes it works. How to fix the lag? Is the code ok?
from threading import Thread
from pynput import keyboard
import time
flag = False
kill = False
def on_press(key):
global flag
global kill
if key == keyboard.KeyCode.from_char('a'):
print('pressed A')
flag = not flag
if key == keyboard.Key.esc:
kill = True
return False
def thr2():
print('joining...')
with keyboard.Listener(on_press=on_press) as listen:
listen.join()
def thr1():
while True:
if kill:
break
if flag:
print('looping....')
time.sleep(0.4)
if __name__ == "__main__":
thread1 = Thread(target=thr1)
thread2 = Thread(target=thr2)
thread1.start()
thread2.start()

It looks like the actual delay is coming from the pynput keyboard.Listener context handler itself. I can't tell you whats happening under the hood but the delay is not coming from the way you are managing your threads.
# pynput library creating keyboard.Listener thread causes the delay
with keyboard.Listener(on_press=on_press) as listen:
print('listen thread created') # This does not happen until after the delay
listen.join()
You may want to rephrase the question so that it is specific to pynput keyboard.Listener

Here is a solution that works nicely with multiprocessing:
import sys
from pynput import keyboard
from time import sleep
from multiprocessing import Process, Event
from functools import partial
def thr2(kill_event, flag_event):
def on_press(kill_event, flag_event, key):
if key == keyboard.KeyCode.from_char('a'):
print('pressed A')
if flag_event.is_set():
flag_event.clear()
else:
flag_event.set()
if key == keyboard.Key.esc:
print('esc')
kill_event.set()
sys.exit(0)
with keyboard.Listener(on_press=partial(on_press, kill_event, flag_event)) as listen:
listen.join()
def thr1(kill_event, flag_event):
while True:
if kill_event.is_set():
print('kill')
sys.exit(0)
if flag_event.is_set():
print('looping....')
sleep(0.4)
if __name__ == "__main__":
kill_event = Event()
flag_event = Event()
thread1 = Process(target=thr1, args=(kill_event, flag_event))
thread2 = Process(target=thr2, args=(kill_event, flag_event))
thread1.start()
thread2.start()
thread1.join() # Join processes here to avoid main process exit
thread2.join()

Related

Exit loop with pynput input

The purpose of my code is to continuously type a letter very fast. It starts when a certain key is pressed (in this case f3) and stops when another key is pressed (f4). My code looks like this currently.
from pynput.keyboard import Controller, Listener
import time
keyboard = Controller()
confirm = False
def on_press(key):
if "f3" in str(key):
global confirm
confirm = True
while confirm:
keyboard.press('e')
keyboard.release('e')
time.sleep(0.10)
elif "Key." in str(key):
pass
def exit_loop(key):
if "f4" in str(key):
global confirm
confirm = False
elif "Key." in str(key):
pass
with Listener(on_press=on_press) as ListenerStart:
ListenerStart.join()
with Listener(on_press=exit_loop) as ListenerEnd:
ListenerEnd.join()
My problem is that, while starting the program with the f3 key works, I am unable to stop the program with f4. Also, the program is supposed to be paused, not exited from. Any help would be appreciated. Thanks.
If you have long-running code like while-loop then you have to run it in separated thread - because it blocks current code and it can't check if you press f4.
If you want to pause code then you should use some variable - ie. paused = True - to control if code inside while-loop should be executed or skiped.
And then you need only one Listener to check keys and check if paused is True or False
from pynput.keyboard import Controller, Listener
import time
import threading
def function():
keyboard = Controller()
while True:
if not paused:
keyboard.press('e')
keyboard.release('e')
time.sleep(0.1)
def on_press(key):
global paused
if paused:
if "f3" in str(key):
paused = False
else:
if "f4" in str(key):
paused = True
# global variables with default values at star
paused = True
# run long-running `function` in separated thread
thread = threading.Thread(target=function) # function's name without `()`
thread.start()
with Listener(on_press=on_press) as listener:
listener.join()
In corrent code you could use the same f3 to start and pause loop.
from pynput.keyboard import Controller, Listener
import time
import threading
def function():
keyboard = Controller()
while True:
if not paused:
keyboard.press('e')
keyboard.release('e')
time.sleep(0.1)
def on_press(key):
global paused
if "f3" in str(key):
paused = not paused
# global variables with default values at star
paused = True
# run long-running `function` in separated thread
thread = threading.Thread(target=function)
thread.start()
with Listener(on_press=on_press) as listener:
listener.join()
This code could be more complex - f3 could check if thread already exists and create thread when it doesn't exist.
from pynput.keyboard import Controller, Listener
import time
import threading
def function():
keyboard = Controller()
while True:
if not paused:
keyboard.press('e')
keyboard.release('e')
time.sleep(0.1)
def on_press(key):
global paused
global thread
if "f3" in str(key):
paused = not paused
if thread is None:
# run long-running `function` in separated thread
thread = threading.Thread(target=function)
thread.start()
# global variables with default values at star
paused = True
thread = None
with Listener(on_press=on_press) as listener:
listener.join()

How to clear keyboard event buffer pynput.keyboard

I have a function that is called on_press. However, if the user constantly hits the key the keyboard event buffer queue gets really large and my function (which takes a few hundreds of ms) gets called even after the user has stopped pressing the key. How can I solve this issue?
from pynput import keyboard
def f1():
print("starting f1()..")
# f1 takes time to finish
def on_press(key):
print("pressed some key")
f1()
with keyboard.Listener(on_press=on_press) as listener:
listener.join()
You could create a thread or process to achieve that,If you want to use thread, like this below:
from pynput import keyboard
import threading, time
def task():
time.sleep(3) # Simulate the time of cost
print("task finish")
def f1():
print("starting task..")
# f1 takes time to finish
# create a thread to execute the task.
threading.Thread(target=task).start()
def on_press(key):
print("pressed some key")
f1()
with keyboard.Listener(on_press=on_press) as listener:
listener.join()
For sure, you could reduce the amount of functions,just for easy to understand, so I define 3 functions.
An answer is given here: https://pythonadventures.wordpress.com/2019/11/16/flush-the-stdin/
The solution is to flush the buffer with:
import sys
import termios
termios.tcflush(sys.stdin, termios.TCIOFLUSH)
You can create a queue to store the incoming keyboard keys and only add to it when it is empty like so:
from pynput import keyboard
from queue import Queue
queue = Queue()
def on_press(key):
if queue.empty():
queue.put(key)
listener = keyboard.Listener(on_press=on_press)
listener.start()
while True:
key = queue.get()
f1()

threading Timer - use timer to 'sys.exit' main thread

I would like to use Timer to exit a script where an infinite loop is running.
Here is a simple stub that I created to test that logic:
import threading
import sys
import time
def exit():
sys.exit()
if __name__ == '__main__':
t = threading.Timer(3, exit)
t.start()
while True:
time.sleep(3)
This doesn't make the script to sys.exit though.
Any pointer why, and how can i achieve the Timer to sys.exit the main thread?

I need to stop multi thread code from running

I'm trying to stop the code from running if the user presses ctrl+shift+c. I use the code below. Unfortunately sys.exit() stops only "wait_for_ctrl_shift_c" function, but not "main_func". What should I use to stop them both?
Thanks.
def wait_for_ctrl_shift_c():
print ('wait_for_ctrl_shift_c is working')
keyboard.wait('ctrl+shift+c')
print('wait_for_ctrl_shift_c was pressed!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
sys.exit()
def main_func():
a=0
while True:
print ('Working2 ',a)
a=a+1
sleep(1)
if __name__ == '__main__':
Thread(target = wait_for_ctrl_shift_c).start()
Thread(target = main_func).start()
There are multiple ways to do it. First of all you have 3 threads, one main thread and the other 2 (infinite loop & keyboard one) you create.
You can register signals and handle it, also you can call interrupt_main to interrupt main thread (not the while loop thread). Interrupt will go to main exception handler. Also instead of True i changed the second thread to have an attribute to check if it should run for clean exit.
import os
import threading
import time
import sys
import _thread
def wait_for_ctrl_shift_c():
print ('wait_for_ctrl_shift_c is working')
keyboard.wait('ctrl+shift+c')
print ('exiting thread')
_thread.interrupt_main()
sys.exit()
def main_func():
a=0
t = threading.currentThread()
while getattr(t, "run", True):
print ('Working2 ',a)
a=a+1
time.sleep(1)
print ('exiting main_func')
if __name__ == '__main__':
try:
t1 = threading.Thread(target = wait_for_ctrl_shift_c)
t2 = threading.Thread(target = main_func)
t1.start()
t2.start()
t1.join()
t2.join()
except:
print ('main exiting')
t2.run = False
sys.exit()
Open shell in another window, type ps to list running processes, and kill the Python one (via kill 3145, if 3145 is its PID) to stop them both. This way we kill the process within which these threads run.

Python 3 Autoclicker On/Off Hotkey

I'm new to Python and I figured I'd make a simple autoclicker as a cool starter project.
I want a user to be able to specify a click interval and then turn the automatic clicking on and off with a hotkey.
I am aware of Ctrl-C, and you'll see that in my current code, but I want the program to work so that the hotkey doesn't have to be activated in the python window.
import pyautogui, sys
print("Press Ctrl + C to quit.")
interval = float(input("Please give me an interval for between clicks in seconds: "))
try:
while True:
pyautogui.click()
except KeyboardInterrupt:
print("\n")
Do I need to make a tkinter message box to make the switch or can I use a hotkey?
Thanks for the help.
Update
import multiprocessing
import time
import pyHook, pyautogui, pythoncom
import queue
click_interval = float(input("Please give an interval between clicks in seconds: "))
class AutoClicker(multiprocessing.Process):
def __init__(self, queue, interval):
multiprocessing.Process.__init__(self)
self.queue = queue
self.click_interval = click_interval
def run(self):
while True:
try:
task = self.queue.get(block=False)
if task == "Start":
print("Clicking...")
pyautogui.click(interval == click_interval)
except task == "Exit":
print("Exiting")
self.queue.task_done()
break
return
def OnKeyboardEvent(event):
key = event.Key
if key == "F3":
print("Starting auto clicker")
# Start consumers
queue.put("Start")
queue.join
elif key == "F4":
print("Stopping auto clicker")
# Add exit message to queue
queue.put("Exit")
# Wait for all of the tasks to finish
queue.join()
# return True to pass the event to other handlers
return True
if __name__ == '__main__':
# Establish communication queues
queue = multiprocessing.JoinableQueue()
# create a hook manager
hm = pyHook.HookManager()
# watch for all mouse events
hm.KeyDown = OnKeyboardEvent
# set the hook
hm.HookKeyboard()
# wait forever
pythoncom.PumpMessages()
AutoClicker.run(self)
First of all if you want to monitor global input outside the Python window you will need pyHook or something similar. It will allow you to monitor keyboard events. I chose to act upon the F3 and F4 key-presses, which are used for starting and stopping the autoclicker.
To accomplish what you've asked, the best way I'm aware of is to create a process which will do the clicking, and to communicate with it through the use of a Queue. When the F4 key is pressed, it will add an "Exit" string to the queue. The autoclicker will recognize this and then return.
Before the F4 key has been pressed, the queue will remain empty and the queue.Empty exception will continually occur. This will execute a single mouse click.
import multiprocessing
import time
import pyHook, pyautogui, pythoncom
import queue
class AutoClicker(multiprocessing.Process):
def __init__(self, queue, interval):
multiprocessing.Process.__init__(self)
self.queue = queue
self.click_interval = interval
def run(self):
while True:
try:
task = self.queue.get(block=False)
if task == "Exit":
print("Exiting")
self.queue.task_done()
break
except queue.Empty:
time.sleep(self.click_interval)
print("Clicking...")
pyautogui.click()
return
def OnKeyboardEvent(event):
key = event.Key
if key == "F3":
print("Starting auto clicker")
# Start consumers
clicker = AutoClicker(queue, 0.1)
clicker.start()
elif key == "F4":
print("Stopping auto clicker")
# Add exit message to queue
queue.put("Exit")
# Wait for all of the tasks to finish
queue.join()
# return True to pass the event to other handlers
return True
if __name__ == '__main__':
# Establish communication queues
queue = multiprocessing.JoinableQueue()
# create a hook manager
hm = pyHook.HookManager()
# watch for all mouse events
hm.KeyDown = OnKeyboardEvent
# set the hook
hm.HookKeyboard()
# wait forever
pythoncom.PumpMessages()
Keep in mind this implementation is far from perfect, but hopefully it's helpful as a starting point.
Why can't I use a simple while loop or if/else statement?
A while loop is blocking, which means when the loop is running it blocks all other code from executing.
So once the clicker loop starts it will be stuck in that loop indefinitely. You can't check for F4 key presses while this is happening because the loop will block any other code from executing (ie. the code that checks for key-presses). Since we want the auto-clicker clicks and the key-press checks to occur simultaneously, they need to be separate processes (so they don't block each other).
The main process which checks for key presses needs to be able to communicate somehow with the auto-clicker process. So when the F4 key is pressed we want the clicking process to exit. Using a queue is one way to communicate (there are others). The auto-clicker can continuously check the queue from inside the while loop. When we want to stop the clicking we can add an "Exit" string to the queue from the main process. The next time the clicker process reads the queue it will see this and break.

Categories

Resources