# 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 }