Search code examples
pythonmultithreadingpython-multithreadingpynput

Trying to create a mouse recorder, but it keeps looping endlessly?


I'm trying my hand at Pynput, and I'm starting off with creating a simple program to record the movements of a mouse, and then replay those movements once a button is clicked.

However, every time I click the mouse, it just starts to freak out and endlessly loop. I think it's going through the movements at a super high speed, but I eventually have to Alt-F4 the shell to stop it.

Any help would be appreciated.

import pynput

arr = []

from pynput import mouse

mou = pynput.mouse.Controller()

def on_move(x,y):
    Pos = mou.position
    arr.append(Pos)

def on_click(x, y, button, pressed):
    listener.stop()
    for i in arr:
        mou.position = i
    print("Done")

listener = mouse.Listener(on_move = on_move, on_click=on_click)
listener.start()

Solution

  • You have to be careful when using multiple threads (which is the case here, since mouse.Listener runs in its own thread). Apparently, as long as you are in the callback function, all events are still processed, even after you have called listener.stop(). So when replaying, for each mouse position you set, the on_move callback function is called, so that mouse position is added to your list again, which causes the endless loop.

    In general, it's bad practice to implement too much functionality (in this case the "replaying") in a callback function. A better solution would be to use an event to signal another thread that the mouse button has been clicked. See the following example code. A few remarks:

    • I've added a few print statements to see what's happening.
    • I've added a small delay between the mouse positions to really see the playback. (NB: This also might make breaking out of the application a bit easier in case it hangs!)
    • I've changed a few variable names to make more sense. Calling an array "arr" is not a good idea. Try to use names that really describe the variable. In this case it is a list of positions, so I choose to call it positions.
    • I'm using return False to stop the mouse controller. The documentation states "Call pynput.mouse.Listener.stop from anywhere, raise StopException or return False from a callback to stop the listener.", but personally, I think returning False is the cleanest and safest solution.
    import threading
    import time
    
    import pynput
    
    positions = []
    clicked = threading.Event()
    controller = pynput.mouse.Controller()
    
    
    def on_move(x, y):
        print(f'on_move({x}, {y})')
        positions.append((x, y))
    
    
    def on_click(x, y, button, pressed):
        print(f'on_move({x}, {y}, {button}, {pressed})')
        # Tell the main thread that the mouse is clicked
        clicked.set()
        return False
    
    
    listener = pynput.mouse.Listener(on_move=on_move, on_click=on_click)
    listener.start()
    try:
        listener.wait()
        # Wait for the signal from the listener thread
        clicked.wait()
    finally:
        listener.stop()
    
    
    print('*REPLAYING*')
    for position in positions:
        controller.position = position
        time.sleep(0.01)
    

    Note that when you run this in a Windows command prompt, the application might hang because you have pressed the mouse button and are then starting to send mouse positions. This causes a "drag" movement, which pauses the terminal. If this happens, you can just press Escape and the program will continue to run.