187 lines
8.9 KiB
Python
187 lines
8.9 KiB
Python
# filepath: src/mouse_mover.py
|
|
|
|
import time
|
|
import threading
|
|
import pyautogui
|
|
from logging_config import get_logger
|
|
|
|
class MouseMover:
|
|
def __init__(self, wait_time=240, move_px=10):
|
|
self.logger = get_logger(__name__)
|
|
self.wait_time = wait_time
|
|
self.move_px = move_px
|
|
self.countdown = self.wait_time
|
|
self.mouse_movement_enabled = False
|
|
self.xold = None # Start with None to force initial update
|
|
self.yold = None
|
|
self.thread = None
|
|
self.running = False
|
|
self.last_activity_time = time.time()
|
|
self.logger.debug(f"MouseMover initialized with wait_time={self.wait_time}, move_px={self.move_px}")
|
|
|
|
def _move_mouse(self):
|
|
"""Move mouse slightly and return to original position"""
|
|
try:
|
|
# Check if PyAutoGUI is working first
|
|
current_pos = pyautogui.position()
|
|
self.logger.debug(f"Current mouse position before move: {current_pos}")
|
|
|
|
# Use larger movement that's more likely to prevent sleep
|
|
move_distance = max(self.move_px, 10) # Ensure at least 10px movement
|
|
|
|
# Move in a more noticeable pattern
|
|
pyautogui.moveRel(0, move_distance, duration=1)
|
|
time.sleep(0.1) # Small pause
|
|
pyautogui.moveRel(0, -move_distance, duration=1)
|
|
|
|
# Optional: Add a tiny wiggle for good measure
|
|
pyautogui.moveRel(1, 0, duration=0.1)
|
|
pyautogui.moveRel(-1, 0, duration=0.1)
|
|
|
|
# Verify movement worked
|
|
new_pos = pyautogui.position()
|
|
self.logger.debug(f"Mouse position after move: {new_pos}")
|
|
|
|
# Every few movements, also press a "safe" key that won't interfere
|
|
# F15 is a function key that typically doesn't do anything
|
|
try:
|
|
pyautogui.press('f15')
|
|
self.logger.debug("Sent F15 key press for additional wake signal")
|
|
except Exception as key_error:
|
|
self.logger.warning(f"Could not send key press (this may be normal in packaged app): {key_error}")
|
|
|
|
self.logger.debug(f"Mouse moved {move_distance}px vertically and back with wiggle")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to move mouse - this may indicate accessibility permission issues: {e}")
|
|
# Try alternative approach if PyAutoGUI fails
|
|
try:
|
|
import subprocess
|
|
# Use AppleScript as fallback for mouse movement
|
|
script = f"""
|
|
tell application "System Events"
|
|
set currentPos to (get position of mouse)
|
|
set x to item 1 of currentPos
|
|
set y to item 2 of currentPos
|
|
set mouse position {{x, y + {max(self.move_px, 10)}}}
|
|
delay 0.1
|
|
set mouse position {{x, y}}
|
|
end tell
|
|
"""
|
|
result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5)
|
|
if result.returncode == 0:
|
|
self.logger.info("Successfully moved mouse using AppleScript fallback")
|
|
else:
|
|
self.logger.error(f"AppleScript fallback failed: {result.stderr}")
|
|
except Exception as fallback_error:
|
|
self.logger.error(f"Both PyAutoGUI and AppleScript fallback failed: {fallback_error}")
|
|
self.logger.error("This app may need to be granted Accessibility permissions in System Preferences")
|
|
|
|
def start(self):
|
|
"""Start the mouse movement monitoring in a separate thread"""
|
|
if not self.running:
|
|
self.running = True
|
|
self.mouse_movement_enabled = True
|
|
self.thread = threading.Thread(target=self._monitor_mouse, daemon=True)
|
|
self.thread.start()
|
|
self.logger.info(f"Mouse movement monitoring started (wait_time={self.wait_time}s, move_px={self.move_px}px)")
|
|
else:
|
|
self.logger.warning("Mouse movement monitoring already running")
|
|
|
|
def stop(self):
|
|
"""Stop the mouse movement monitoring"""
|
|
if self.running:
|
|
self.logger.info("Stopping mouse movement monitoring...")
|
|
self.running = False
|
|
self.mouse_movement_enabled = False
|
|
if self.thread and self.thread.is_alive():
|
|
self.thread.join(timeout=2) # Increased timeout
|
|
if self.thread.is_alive():
|
|
self.logger.warning("Mouse monitoring thread did not stop gracefully")
|
|
else:
|
|
self.logger.debug("Mouse monitoring thread stopped successfully")
|
|
self.logger.info("Mouse movement monitoring stopped")
|
|
|
|
def _monitor_mouse(self):
|
|
"""Main monitoring loop that runs in separate thread"""
|
|
self.logger.debug("Mouse monitoring thread started")
|
|
|
|
# Initialize with 0,0 like original script
|
|
self.xold, self.yold = 0, 0
|
|
self.last_activity_time = time.time()
|
|
self.logger.debug(f"Initial mouse position tracking: ({self.xold}, {self.yold})")
|
|
|
|
while self.running and self.mouse_movement_enabled:
|
|
try:
|
|
# Try to get mouse position with error handling
|
|
try:
|
|
x, y = pyautogui.position()
|
|
except Exception as pos_error:
|
|
self.logger.error(f"Failed to get mouse position: {pos_error}")
|
|
self.logger.error("This indicates PyAutoGUI accessibility issues in packaged app")
|
|
# Sleep and continue to avoid tight error loop
|
|
time.sleep(5)
|
|
continue
|
|
|
|
current_time = time.time()
|
|
|
|
# Check if timer has expired first (like original script)
|
|
if self.countdown <= 0:
|
|
self._move_mouse()
|
|
self.logger.info(f"Timer expired ({self.wait_time}s), mouse moved automatically")
|
|
self.countdown = self.wait_time
|
|
self.last_activity_time = current_time
|
|
# After our movement, continue to next iteration immediately
|
|
continue
|
|
|
|
# Use the original script's exact logic: if (x == xold or y == yold)
|
|
if x == self.xold or y == self.yold:
|
|
# Mouse is considered idle - count down by 1 and sleep 1 second (like original)
|
|
self.logger.debug(f"Mouse idle (x={x} vs xold={self.xold}, y={y} vs yold={self.yold}), countdown: {self.countdown}s")
|
|
|
|
# Sleep for 1 second and decrement by 1 (exactly like original script)
|
|
time.sleep(1)
|
|
self.countdown -= 1
|
|
self.logger.debug(f"Countdown: {self.countdown}")
|
|
|
|
else:
|
|
# Mouse moved to a new position, reset timer and update yold (exactly like original)
|
|
self.logger.info(f"Mouse moved to ({x}, {y}) from ({self.xold}, {self.yold}), timer reset")
|
|
self.countdown = self.wait_time
|
|
self.last_activity_time = current_time
|
|
# Only update yold, keep xold at 0 (matches original script behavior exactly)
|
|
self.yold = y
|
|
# Note: intentionally NOT updating self.xold to match original behavior
|
|
|
|
# Check if we should stop (more frequently than the 10-second chunks)
|
|
if not self.running or not self.mouse_movement_enabled:
|
|
break
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in mouse monitoring: {e}")
|
|
break
|
|
|
|
self.logger.debug("Mouse monitoring thread ended")
|
|
|
|
def update_wait_time(self, new_wait_time):
|
|
"""Update the wait time setting"""
|
|
self.wait_time = new_wait_time
|
|
# Reset countdown to new wait time if currently counting down
|
|
if self.running and self.countdown > self.wait_time:
|
|
self.countdown = self.wait_time
|
|
self.logger.info(f"Wait time updated to {self.wait_time} seconds")
|
|
|
|
def update_move_px(self, new_move_px):
|
|
"""Update the mouse movement distance setting"""
|
|
self.move_px = new_move_px
|
|
self.logger.info(f"Mouse movement distance updated to {self.move_px} pixels")
|
|
|
|
def get_status(self):
|
|
"""Get current status information"""
|
|
return {
|
|
'running': self.running,
|
|
'enabled': self.mouse_movement_enabled,
|
|
'countdown': self.countdown,
|
|
'wait_time': self.wait_time,
|
|
'move_px': self.move_px,
|
|
'thread_alive': self.thread.is_alive() if self.thread else False
|
|
} |