moses-palmer / pynput

Sends virtual input commands
GNU Lesser General Public License v3.0
1.73k stars 243 forks source link

Script Continues Rapid Clicking Despite Mouse Button Release (is_pressed Functionality Issue) #588

Open NSC9 opened 4 months ago

NSC9 commented 4 months ago

THE SCRIPT: Essentially, the functionality is to rapidly click WHILE (AND ONLY WHILE) the physical left mouse button is held DOWN and stops clicking immediately when the physical left mouse button enters the UP position.

#!/usr/bin/env python3

from pynput.mouse import Listener, Controller, Button
import time

# Constants
CLICK_DELAY = 0.05

# Global variable to track the state of the left mouse button
is_pressed = False

# Function to perform rapid clicking
def rapid_clicking():
    mouse = Controller()
    while is_pressed:
        mouse.click(Button.left)
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(x, y, button, pressed):
    global is_pressed
    if button == Button.left:
        is_pressed = pressed
        if pressed:
            rapid_clicking()

# Start listening to mouse events
with Listener(on_click=on_click) as listener:
    listener.join()

Description This Python script utilizes the pynput library to listen for mouse events, specifically the left mouse button clicks. Here's a breakdown of what the script does:

  1. Import necessary modules:

    • Listener: from pynput.mouse, for listening to mouse events.
    • Controller: from pynput.mouse, for controlling the mouse.
    • Button: from pynput.mouse, for identifying mouse buttons.
    • time: for adding delays.
  2. Define constants:

    • CLICK_DELAY: Specifies the delay between each click.
  3. Define a global variable:

    • is_pressed: Tracks the state of the left mouse button (whether it's pressed or released).
  4. Define a function rapid_clicking():

    • This function performs rapid clicking while the left mouse button is pressed.
    • It continuously clicks the left mouse button with a delay specified by CLICK_DELAY.
  5. Define a callback function on_click():

    • This function is called whenever a mouse button is clicked (pressed or released).
    • It updates the global variable is_pressed based on the state of the left mouse button.
    • If the left mouse button is pressed, it starts the rapid_clicking() function.
  6. Start listening to mouse events using Listener:

    • It listens for mouse events and calls the on_click() function whenever a mouse button is clicked.
    • The with statement ensures that the Listener is properly closed after execution.

Overall, this script should allow for rapid clicking with the left mouse button while it is held down and stops clicking when the button is released.

Platform and pynput version Linux Mint 21.3 Cinnamon v6.0.4 Kernel: 6.5.0-21-generic display-server: X11 pynput-Version: 1.7.6

This script is my attempt at converting the following Autohotkey script to work on Linux (so far, of all the linux-ahk like macroing distros, the above script is as close to the desired functionality of the following):

*lbutton::
    ; main
    Sendinput {Shift Up}
    Sendinput {Click}
    Sleep, 0
    KeyWait, lButton, T0.105
    If ErrorLevel
    {
        While (GetKeyState("lButton", "P"))
        {
           Sendinput {Click}
           Sleep, 66
        }
        KeyWait, lButton
    }
    return

ISSUE: The mouse successfully and continuously sends clicks when the left mouse button is DOWN. The problem is, the script fails to temporarily pause when the left mouse button is in the UP position. The script does not stop clicking after the first initial click. I need the script to be able to start and temporarily stop easily without the script inside the terminal being stopped completely.

NSC9 commented 4 months ago

UPDATE I have verified that xdotool does send clicks when isolated. Attached is a link of several hours of debugging with Chatgpt. https://chat.openai.com/share/96402fab-772f-4ca1-8b95-c1646522d51d

The terminal reads the events correctly. I am having trouble integrating the isolated xdotool clicks with Listener. I suspect the problem might be with Listener.

The current code below does not send any left clicks. Sometimes Chatgpt makes it where it keeps clicking after the first initial click but it does not stop after the loop begins.

from pynput.mouse import Listener, Button
import subprocess

# Constants
CLICK_DELAY = 0.1

# Global variable to track the state of the left mouse button
is_pressed = False

# Function to perform clicking using xdotool
def perform_click():
    subprocess.run(['xdotool', 'click', '1'])  # 1 corresponds to the left mouse button
    print("sending a left click...")
# Callback function for mouse events
def on_click(x, y, button, pressed):
    global is_pressed
    if button == Button.left:
        is_pressed = pressed
        if pressed:
            print("Mouse button pressed")
        else:
            print("Mouse button released")

# Start listening to mouse events
with Listener(on_click=on_click) as listener:
    while listener.running:
        if is_pressed:
            perform_click()
NSC9 commented 3 months ago

Made a post here seeking help. https://www.reddit.com/r/learnpython/comments/1bk5pqw/pynput_script_so_close_to_working_please_help_btc/

moses-palmer commented 3 months ago

Thank you for your report.

I think the problem stems from this simple fact: every click you send with a virtual pointing device---either pynput.mouse.Controller or xdotool---will also generate a new click event. I have on-going work on the branch feature-injected, which adds a new optional argument to the callbacks. This branch has not yet been merged to master, as I have not yet had time to properly test it.

If you install pynput from this branch, you might be able to modify you code thus:

#!/usr/bin/env python3

from pynput.mouse import Listener, Controller, Button
import time

# Constants
CLICK_DELAY = 0.05

# Global variable to track the state of the left mouse button
is_pressed = False

# Function to perform rapid clicking
def rapid_clicking():
    # Please note that this will also need to be changed: as documented
    # (https://pynput.readthedocs.io/en/latest/mouse.html#the-mouse-listener-thread), the callback must
    # not block, so this must be delegated to a separate thread
    mouse = Controller()
    while is_pressed:
        mouse.click(Button.left)
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(x, y, button, pressed, injected):
    global is_pressed
    if button == Button.left and not injected:
        is_pressed = pressed
        if pressed:
            rapid_clicking()

# Start listening to mouse events
with Listener(on_click=on_click) as listener:
    listener.join()

Please give it a try! If you report back success, I will merge the feature branch.

NSC9 commented 3 months ago

Here are some of my attempts to do this. First, pip install git+https://github.com/moses-palmer/pynput.git@feature-injected Second, pip show -f pynput After doing this, I do not see the feature-injected branch so I am not sure I installed the custom branch correctly.

Name: pynput
Version: 1.7.6
Summary: Monitor and control user input devices
Home-page: https://github.com/moses-palmer/pynput
Author: Moses Palmér
Author-email: moses.palmer@gmail.com
License: LGPLv3
Location: /home/professor/.local/lib/python3.10/site-packages
Requires: evdev, python-xlib, six
Required-by: 
Files:
  pynput-1.7.6.dist-info/COPYING.LGPL
  pynput-1.7.6.dist-info/INSTALLER
  pynput-1.7.6.dist-info/METADATA
  pynput-1.7.6.dist-info/RECORD
  pynput-1.7.6.dist-info/REQUESTED
  pynput-1.7.6.dist-info/WHEEL
  pynput-1.7.6.dist-info/direct_url.json
  pynput-1.7.6.dist-info/top_level.txt
  pynput-1.7.6.dist-info/zip-safe
  pynput/__init__.py
  pynput/__pycache__/__init__.cpython-310.pyc
  pynput/__pycache__/_info.cpython-310.pyc
  pynput/_info.py
  pynput/_util/__init__.py
  pynput/_util/__pycache__/__init__.cpython-310.pyc
  pynput/_util/__pycache__/darwin.cpython-310.pyc
  pynput/_util/__pycache__/darwin_vks.cpython-310.pyc
  pynput/_util/__pycache__/uinput.cpython-310.pyc
  pynput/_util/__pycache__/win32.cpython-310.pyc
  pynput/_util/__pycache__/win32_vks.cpython-310.pyc
  pynput/_util/__pycache__/xorg.cpython-310.pyc
  pynput/_util/__pycache__/xorg_keysyms.cpython-310.pyc
  pynput/_util/darwin.py
  pynput/_util/darwin_vks.py
  pynput/_util/uinput.py
  pynput/_util/win32.py
  pynput/_util/win32_vks.py
  pynput/_util/xorg.py
  pynput/_util/xorg_keysyms.py
  pynput/keyboard/__init__.py
  pynput/keyboard/__pycache__/__init__.cpython-310.pyc
  pynput/keyboard/__pycache__/_base.cpython-310.pyc
  pynput/keyboard/__pycache__/_darwin.cpython-310.pyc
  pynput/keyboard/__pycache__/_dummy.cpython-310.pyc
  pynput/keyboard/__pycache__/_uinput.cpython-310.pyc
  pynput/keyboard/__pycache__/_win32.cpython-310.pyc
  pynput/keyboard/__pycache__/_xorg.cpython-310.pyc
  pynput/keyboard/_base.py
  pynput/keyboard/_darwin.py
  pynput/keyboard/_dummy.py
  pynput/keyboard/_uinput.py
  pynput/keyboard/_win32.py
  pynput/keyboard/_xorg.py
  pynput/mouse/__init__.py
  pynput/mouse/__pycache__/__init__.cpython-310.pyc
  pynput/mouse/__pycache__/_base.cpython-310.pyc
  pynput/mouse/__pycache__/_darwin.cpython-310.pyc
  pynput/mouse/__pycache__/_dummy.cpython-310.pyc
  pynput/mouse/__pycache__/_win32.cpython-310.pyc
  pynput/mouse/__pycache__/_xorg.cpython-310.pyc
  pynput/mouse/_base.py
  pynput/mouse/_darwin.py
  pynput/mouse/_dummy.py
  pynput/mouse/_win32.py
  pynput/mouse/_xorg.py

I tested this code and the terminal read correctly but no clicks were sent.

#!/usr/bin/env python3

from pynput.mouse import Listener, Controller, Button
import time
import threading

# Constants
CLICK_DELAY = 0.04

# Global variable to track the state of the left mouse button
is_pressed = False

# Function to perform rapid clicking
def rapid_clicking():
    mouse = Controller()
    while is_pressed:
        mouse.click(Button.left)
        print("Sending a left click...")
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(x, y, button, pressed, injected):
    global is_pressed
    if button == Button.left:
        is_pressed = pressed
        if pressed:
            print("Mouse button pressed")
            thread = threading.Thread(target=rapid_clicking)
            thread.start()
        else:
            print("Mouse button released")

# Start listening to mouse events
with Listener(on_click=on_click) as listener:
    listener.join()

I tried implementing your comment about the callback being delegated to a separate thread. No clicks where sent when the code below was ran.

#!/usr/bin/env python3

from pynput.mouse import Listener, Controller, Button
import time
import threading

# Constants
CLICK_DELAY = 0.04

# Global variable to track the state of the left mouse button
is_pressed = False

# Function to perform rapid clicking
def rapid_clicking():
    mouse = Controller()
    while is_pressed:
        mouse.click(Button.left)
        print("Sending a left click...")
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(x, y, button, pressed, injected):
    global is_pressed
    try:
        if button == Button.left:
            is_pressed = pressed
            if pressed:
                print("Mouse button pressed")
                thread = threading.Thread(target=rapid_clicking)
                thread.start()
            else:
                print("Mouse button released")
    except Exception as e:
        print(f"Error occurred in callback function: {e}")

# Start listening to mouse events
with Listener(on_click=on_click) as listener:
    try:
        listener.join()
    except KeyboardInterrupt:
        print("Program terminated by user")
davisvkz commented 1 week ago

you could try the evdev library, the same library that pynput uses but you could select an physic devices to listen instead of listen global events from any devices

could make something like that:

#!/usr/bin/env python3

from pynput.mouse import Listener, Controller, Button
import time
import threading
import evdev

# Constants
CLICK_DELAY = 0.04

# Global variable to track the state of the left mouse button
is_pressed = False

dev = evdev.InputDevice('/dev/input/event6')
# Function to perform rapid clicking
def rapid_clicking():
    mouse = Controller()
    while is_pressed:
        mouse.click(Button.left)
        print("Sending a left click...")
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(pressed):
    global is_pressed
    is_pressed = pressed
    try:
        thread = threading.Thread(target=rapid_clicking)
        thread.start()
    except Exception as e:
        print(f"Error occurred in callback function: {e}")

for event in dev.read_loop():
    event:evdev.InputEvent
    if event.type==1 and event.value==1 and event.code==272:
        on_click(True)
    elif event.type==1 and event.value==0 and event.code==272:
        on_click(False)

the unique problem is that when you pressing the button, the click can't not be overwritten

davisvkz commented 1 week ago

or even better you could uses evdev event llistener and using dev.grab() to block all event en intercep en resend the call of event something like this

#!/usr/bin/env python3

import time
import threading
import evdev

# Constants
CLICK_DELAY = 0.04

# Global variable to track the state of the left mouse button
is_pressed = False

dev = evdev.InputDevice('/dev/input/event6')
ui = evdev.UInput.from_device(dev)
# Function to perform rapid clicking
def rapid_clicking():
    while is_pressed:
        ui.write(1, 272, 1)
        ui.syn()
        ui.write(1, 272, 0)
        ui.syn()
        print("Sending a left click...")
        time.sleep(CLICK_DELAY)

# Callback function for mouse events
def on_click(pressed):
    global is_pressed
    is_pressed = pressed
    try:
        thread = threading.Thread(target=rapid_clicking)
        thread.start()
    except Exception as e:
        print(f"Error occurred in callback function: {e}")

dev.grab()
for event in dev.read_loop():
    event:evdev.InputEvent
    if event.type==1 and event.value==1 and event.code==272:
        on_click(True)
    elif event.type==1 and event.value==0 and event.code==272:
        on_click(False)
    else:
        ui.write_event(event)