Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Apihplays/93a9893b3b64f131ffe44628fff0789c to your computer and use it in GitHub Desktop.

Select an option

Save Apihplays/93a9893b3b64f131ffe44628fff0789c to your computer and use it in GitHub Desktop.

This guide provides a Python script that creates a virtual mouse for an auto-clicker functionality, specifically configured to run on Linux without requiring sudo after initial setup. It addresses common evdev permission issues by utilizing a udev rule.


These files are required

automation.py

import asyncio
import threading
import time
from evdev import InputDevice, UInput, ecodes as e, list_devices
from pynput.mouse import Button, Controller

# --- Configuration ---
# evdev code for the button. BTN_SIDE is the generic code for the first side button (often button 8).
TRIGGER_BUTTON_CODE = e.BTN_SIDE
CLICK_INTERVAL = 0.1  # Time in seconds between clicks
# ---------------------

clicking_enabled = False
pynput_mouse = Controller()

def find_mouse_device():
    """Finds the event device for a physical mouse."""
    devices = [InputDevice(path) for path in list_devices()]
    for device in devices:
        # Look for a device that has mouse-like capabilities
        if 'mouse' in device.name.lower() and e.EV_KEY in device.capabilities() and e.BTN_LEFT in device.capabilities()[e.EV_KEY]:
            print(f"Found mouse device: {device.path} ({device.name})")
            return device
    return None

def clicker():
    """This thread performs the rapid left-clicks."""
    while True:
        if clicking_enabled:
            pynput_mouse.click(Button.left, 1)
        time.sleep(CLICK_INTERVAL)

async def handle_events(dev, ui):
    """Reads events from the real mouse and processes them."""
    global clicking_enabled
    try:
        async for event in dev.async_read_loop():
            # If it's our trigger button, we handle it ourselves
            if event.type == e.EV_KEY and event.code == TRIGGER_BUTTON_CODE:
                if event.value == 1:  # 1 is pressed
                    print("Auto-clicker enabled.")
                    clicking_enabled = True
                elif event.value == 0:  # 0 is released
                    print("Auto-clicker disabled.")
                    clicking_enabled = False
                # Do NOT write this event to the virtual device, effectively swallowing it.
            else:
                # For all other events, pass them through to the virtual device
                ui.write_event(event)
    except (OSError, asyncio.CancelledError) as err:
        print(f"Event loop error: {err}")
    finally:
        print("Closing devices.")
        dev.ungrab() # Release the physical device
        ui.close()

def main():
    dev = find_mouse_device()
    if not dev:
        print("Error: Could not find a mouse device. Make sure the script has permissions to read /dev/input/.")
        return

    # Create a virtual device (uinput) with the same capabilities as the real one
    with UInput.from_device(dev, name='virtual-mouse') as ui:
        print("Virtual mouse created. Grabbing physical mouse...")
        try:
            # Grab the physical device for exclusive access
            dev.grab()
            print("Mouse grabbed successfully. Auto-clicker is active.")
            print("Hold down the side mouse button to click. Press Ctrl+C to exit.")

            # Start the auto-clicker thread
            click_thread = threading.Thread(target=clicker, daemon=True)
            click_thread.start()

            # Run the event handling loop
            asyncio.run(handle_events(dev, ui))

        except OSError as e:
            print(f"Error: {e}. This script requires administrator privileges.")
            print("Please run it with 'sudo'.")
        except Exception as e:
            print(f"An unexpected error occurred: {e}")

if __name__ == "__main__":
    main()

run.sh

#!/bin/bash
venc/bin/python automation.py

99-input.rules

KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"

Instructions for Users:

Step 1: Set up your Project

  1. Create a project directory:
    > mkdir my_autoclicker
    > cd my_autoclicker
  2. Create a Python virtual environment:
    > python3 -m venv venv
  3. Activate the virtual environment:
    > source venv/bin/activate
  4. Install necessary Python packages:
    > pip install evdev pynput

Step 2: Create the Script Files

  1. Create automation.py: Copy the content from the automation.py section above and save it as automation.py in your my_autoclicker directory.
  2. Create run.sh: Copy the content from the run.sh section above and save it as run.sh in your my_autoclicker directory.
  3. Make run.sh executable:
    > chmod +x run.sh

Step 3: Configure Udev Rule (Crucial for No-Sudo Operation)

This step allows your user to create virtual input devices without needing sudo every time.

  1. Add your user to the input group:
    > sudo usermod -aG input $USER
    You must log out and log back in for this change to take effect.
  2. Create or edit the udev rule file:
    > sudo nano /etc/udev/rules.d/99-input.rules
  3. Paste the content from the 99-input.rules section above into the editor.
    • If the file already exists, ensure its content matches exactly.
    • Save the file and exit the editor (Ctrl+O, Enter, Ctrl+X in nano).
  4. Reload udev rules:
    > sudo udevadm control --reload-rules && sudo udevadm trigger

Step 4: Run the Auto-Clicker

  1. Navigate to your project directory:
    > cd /path/to/my_autoclicker
  2. Run the script:
    > ./run.sh
    You should now be able to run the script without being prompted for a password.

How it Works:

  • The automation.py script uses evdev to create a virtual mouse device and pynput to simulate clicks. It also grabs your physical mouse to prevent duplicate input.
  • The udev rule (99-input.rules) modifies the permissions of /dev/uinput (the kernel interface for virtual input devices) to allow users in the input group to write to it.
  • By adding your user to the input group and applying this rule, your script gains the necessary permissions without needing sudo for every execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment