Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created February 2, 2025 08:35
Show Gist options
  • Save Hammer2900/0bb43a9b977a8af34352801314365c24 to your computer and use it in GitHub Desktop.
Save Hammer2900/0bb43a9b977a8af34352801314365c24 to your computer and use it in GitHub Desktop.
fast copy paste with pynput for key ctrl alt and mouse
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