Created
February 2, 2025 08:35
-
-
Save Hammer2900/0bb43a9b977a8af34352801314365c24 to your computer and use it in GitHub Desktop.
fast copy paste with pynput for key ctrl alt and mouse
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import time | |
from dataclasses import dataclass | |
from typing import Callable, Optional, Dict | |
import logging | |
import multiprocessing | |
import fire | |
from pynput import keyboard, mouse | |
import sh | |
logging.basicConfig(level=logging.DEBUG) | |
@dataclass | |
class Action: | |
"""Base class for tracking time and handling event-based actions.""" | |
threshold: float | |
action: Optional[Callable] = None | |
_last_time: float = 0 | |
_count: int = 0 | |
def check(self): | |
"""Checks if action should be triggered based on time between events.""" | |
current_time = time.time() | |
if current_time - self._last_time < self.threshold: | |
self._count += 1 | |
else: | |
self._count = 1 | |
self._last_time = current_time | |
if self._count == 2 and self.action: | |
self.action() | |
self._count = 0 | |
class InputBindings: | |
"""Base class for handling input bindings.""" | |
def __init__(self): | |
self.bindings: Dict[keyboard.Key, Action] = {} | |
def run(self): | |
"""Runs the input listener with error handling and automatic restart.""" | |
while True: | |
try: | |
self._start_listener() | |
except Exception as e: | |
logging.debug(f'Error: {e}') | |
continue | |
def _start_listener(self): | |
"""Abstract method for starting and managing the listener. | |
Must be implemented in subclasses. | |
""" | |
raise NotImplementedError | |
def _process_event(self, event_type: str, event_data): | |
"""Processes input event and triggers corresponding actions.""" | |
if event_type not in ('keyboard', 'mouse'): | |
raise ValueError(f'Unknown event type: {event_type}') | |
if event_type == 'keyboard': | |
key = event_data | |
if action := self.bindings.get(key): | |
logging.debug(f'Key: {key}') | |
action.check() | |
elif event_type == 'mouse': | |
x, y, button, pressed = event_data | |
if button == mouse.Button.left: | |
self.left_pressed = pressed | |
elif button == mouse.Button.right and getattr(self, 'left_pressed', False): | |
if action := self.bindings.get(mouse.Button.right): | |
action.check() | |
logging.debug(f'Mouse: x:{x} y:{y} button:{button} pressed:{pressed}') | |
class KeyboardBindings(InputBindings): | |
"""Handles keyboard key combinations.""" | |
def __init__(self): | |
super().__init__() | |
self.ctrl_pressed = False | |
self.bindings = { | |
keyboard.Key.ctrl: Action(threshold=0.5, action=lambda: sh.xdotool.key('ctrl+c')), | |
keyboard.Key.alt_l: Action( | |
threshold=0.5, action=lambda: sh.xdotool.key('--clearmodifiers', 'Shift+Insert') | |
), | |
'ctrl_alt': Action(threshold=0.5, action=lambda: sh.xdotool.key('Return')), | |
} | |
def _start_listener(self): | |
with keyboard.Listener(on_press=self._on_press, on_release=self._on_release) as listener: | |
listener.join() | |
def _on_press(self, key): | |
logging.debug(f'Key pressed: {key}') | |
if key == keyboard.Key.ctrl: | |
self.ctrl_pressed = True | |
if action := self.bindings.get(key): | |
action.check() | |
elif key == keyboard.Key.alt_l: | |
if self.ctrl_pressed: | |
if action := self.bindings.get('ctrl_alt'): | |
action.check() | |
else: | |
if action := self.bindings.get(key): | |
action.check() | |
def _on_release(self, key): | |
if key == keyboard.Key.ctrl: | |
self.ctrl_pressed = False | |
class MouseBindings(InputBindings): | |
"""Handles mouse button combinations.""" | |
def __init__(self): | |
super().__init__() | |
self.left_pressed = False | |
self.bindings = { | |
mouse.Button.right: Action( | |
threshold=0.55, # 550ms threshold for double click | |
action=lambda: sh.xdotool.key('Return'), | |
) | |
} | |
def _start_listener(self): | |
with mouse.Listener(on_click=self._on_click) as listener: | |
listener.join() | |
def _on_click(self, x, y, button, pressed): | |
if button == mouse.Button.left: | |
self.left_pressed = pressed | |
elif button == mouse.Button.right and pressed and self.left_pressed: | |
if action := self.bindings.get(button): | |
action.check() | |
logging.debug(f'Mouse: x:{x} y:{y} button:{button} pressed:{pressed}') | |
def run_keyboard(): | |
"""Starts keyboard bindings listener.""" | |
kb = KeyboardBindings() | |
kb.run() | |
def run_mouse(): | |
"""Starts mouse bindings listener.""" | |
mb = MouseBindings() | |
mb.run() | |
def run_all(): | |
"""Runs both keyboard and mouse bindings in separate processes.""" | |
keyboard_process = multiprocessing.Process(target=run_keyboard) | |
mouse_process = multiprocessing.Process(target=run_mouse) | |
keyboard_process.start() | |
mouse_process.start() | |
keyboard_process.join() | |
mouse_process.join() | |
def main(): | |
"""Entry point with Fire CLI integration.""" | |
debug_env = os.environ.get('DEBUG_MODE') | |
if debug_env and debug_env.lower() in ('true', '1', 'yes'): | |
logging.getLogger().setLevel(logging.DEBUG) | |
logging.debug('Debug mode enabled via DEBUG_MODE environment variable') | |
else: | |
logging.getLogger().setLevel(logging.INFO) | |
fire.Fire({'keyboard': KeyboardBindings, 'mouse': MouseBindings, 'all': run_all}) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment