Initial commit: HereIAm mouse movement monitor

- Complete macOS application with PyQt5 GUI
- Smart mouse monitoring with configurable timing
- System menu bar integration with adaptive theming
- Comprehensive build system with PyInstaller
- Professional DMG creation for distribution
- Full documentation and testing scripts
This commit is contained in:
Jerico Thomas
2025-07-25 13:38:33 -04:00
commit c726cc0716
20 changed files with 1646 additions and 0 deletions

10
src/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""
HereIAm - Mouse Movement Monitor for macOS
A sophisticated application that prevents system sleep by intelligently monitoring
and managing mouse activity.
"""
__version__ = "1.0.0"
__author__ = "Jerico Thomas"
__email__ = "jerico@tekop.net"
__app_name__ = "HereIAm"

43
src/config.py Normal file
View File

@@ -0,0 +1,43 @@
import logging
import os
from datetime import datetime
# Configuration
WAITTIME = 240
MovePx = 10
StartEnabled = True
DEBUG = False
# Logging configuration
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
LOG_FILE = os.path.join(LOG_DIR, 'hereiam.log')
def setup_logging():
"""Setup logging configuration"""
# Create logs directory if it doesn't exist
os.makedirs(LOG_DIR, exist_ok=True)
# Configure logging
log_level = logging.DEBUG if DEBUG else logging.INFO
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers = [logging.FileHandler(LOG_FILE)]
if DEBUG:
handlers.append(logging.StreamHandler()) # Also log to console in debug mode
logging.basicConfig(
level=log_level,
format=log_format,
handlers=handlers
)
# Log startup
logger = logging.getLogger(__name__)
logger.info("HereIAm application starting...")
logger.info(f"Config: WAITTIME={WAITTIME}, MovePx={MovePx}, StartEnabled={StartEnabled}")
def get_logger(name):
"""Get a logger instance"""
return logging.getLogger(name)
# Add any additional configuration settings or constants here as needed.

216
src/main.py Normal file
View File

@@ -0,0 +1,216 @@
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
import os
from mouse_mover import MouseMover
from config import StartEnabled, setup_logging, get_logger
from __init__ import __version__, __author__, __email__, __app_name__
# Setup logging before anything else
setup_logging()
class HereIAmApp:
def __init__(self):
self.logger = get_logger(__name__)
self.logger.info("Initializing HereIAm application")
self.app = QtWidgets.QApplication(sys.argv)
self.app.setQuitOnLastWindowClosed(False) # Keep running when windows close
# Initialize MouseMover instance
self.mouse_mover = MouseMover()
# Initialize state from config
self.mouse_mover_enabled = StartEnabled
# Initialize icon paths (use absolute paths)
self.base_path = os.path.dirname(os.path.dirname(__file__))
self.enabled_icon = os.path.join(self.base_path, "assets", "Enabled.icns")
self.disabled_light_icon = os.path.join(self.base_path, "assets", "Disabled-Light.icns")
self.disabled_dark_icon = os.path.join(self.base_path, "assets", "Disabled-Dark.icns")
# Verify icon files exist
self._verify_icons()
# Create tray icon
self.tray_icon = QtWidgets.QSystemTrayIcon(self.app)
self.tray_icon.setToolTip("HereIAm - Mouse Movement Monitor")
# Check if system tray is available
if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
self.logger.critical("System tray is not available on this system")
QtWidgets.QMessageBox.critical(None, "HereIAm",
"System tray is not available on this system.")
sys.exit(1)
self.update_icon() # Set initial icon based on theme
self._create_menu()
self._setup_connections()
self.tray_icon.show()
# Start mouse movement if enabled in config
if StartEnabled:
self.mouse_mover.start()
self.logger.info("HereIAm application initialized successfully")
def _verify_icons(self):
"""Verify that all required icon files exist"""
icons = [self.enabled_icon, self.disabled_light_icon, self.disabled_dark_icon]
for icon_path in icons:
if not os.path.exists(icon_path):
self.logger.warning(f"Icon file not found: {icon_path}")
def _create_menu(self):
"""Create the context menu"""
self.menu = QtWidgets.QMenu()
# Add status item (non-clickable)
status_text = "Enabled" if self.mouse_mover_enabled else "Disabled"
self.status_action = self.menu.addAction(f"Status: {status_text}")
self.status_action.setEnabled(False)
self.menu.addSeparator()
# Main actions
self.enable_action = self.menu.addAction("Enable HereIAm")
self.disable_action = self.menu.addAction("Disable HereIAm")
# Set initial menu state based on StartEnabled config
if StartEnabled:
self.enable_action.setEnabled(False)
self.disable_action.setEnabled(True)
else:
self.enable_action.setEnabled(True)
self.disable_action.setEnabled(False)
self.menu.addSeparator()
# Additional options
self.about_action = self.menu.addAction("About HereIAm")
self.quit_action = self.menu.addAction("Quit")
# Set the context menu - this provides the most reliable behavior on macOS
self.tray_icon.setContextMenu(self.menu)
def _setup_connections(self):
"""Setup all signal connections"""
self.tray_icon.activated.connect(self.on_tray_icon_activated)
# Use lambda to add immediate feedback
self.enable_action.triggered.connect(lambda: self._handle_action(self.enable_mouse_movement))
self.disable_action.triggered.connect(lambda: self._handle_action(self.disable_mouse_movement))
self.about_action.triggered.connect(self.show_about)
self.quit_action.triggered.connect(self.quit)
# Listen for system theme changes
self.app.paletteChanged.connect(self.on_theme_changed)
def _handle_action(self, action_func):
"""Handle menu actions with immediate feedback"""
self.logger.debug(f"Menu action triggered: {action_func.__name__}")
action_func()
def is_dark_mode(self):
"""Detect if the system is in dark mode"""
palette = self.app.palette()
window_color = palette.color(QtGui.QPalette.Window)
# If the window background is dark, we're in dark mode
return window_color.lightness() < 128
def update_icon(self):
"""Update the tray icon based on current state and theme"""
if self.mouse_mover_enabled:
icon_path = self.enabled_icon
else:
icon_path = self.disabled_dark_icon if self.is_dark_mode() else self.disabled_light_icon
if os.path.exists(icon_path):
self.tray_icon.setIcon(QtGui.QIcon(icon_path))
else:
self.logger.warning(f"Icon file not found: {icon_path}")
def _update_status_display(self):
"""Update the status display in the menu"""
status_text = "Enabled" if self.mouse_mover_enabled else "Disabled"
self.status_action.setText(f"Status: {status_text}")
def on_theme_changed(self):
"""Called when system theme changes"""
self.logger.debug("System theme changed")
self.update_icon()
def on_tray_icon_activated(self, reason):
"""Handle tray icon activation"""
self.logger.debug(f"Tray icon activated with reason: {reason}")
# On macOS, single click usually shows the context menu automatically
# We only need to handle double-click for quick toggle
if reason == QtWidgets.QSystemTrayIcon.DoubleClick:
self.logger.debug("Double click detected - toggling state")
if self.mouse_mover_enabled:
self.disable_mouse_movement()
else:
self.enable_mouse_movement()
def enable_mouse_movement(self):
self.logger.info("Enabling mouse movement via user request")
self.mouse_mover_enabled = True
self.enable_action.setEnabled(False)
self.disable_action.setEnabled(True)
self._update_status_display()
self.update_icon()
self.mouse_mover.start()
self.logger.debug("Mouse movement enabled successfully")
def disable_mouse_movement(self):
self.logger.info("Disabling mouse movement via user request")
self.mouse_mover_enabled = False
self.disable_action.setEnabled(False)
self.enable_action.setEnabled(True)
self._update_status_display()
self.update_icon()
self.mouse_mover.stop()
self.logger.debug("Mouse movement disabled successfully")
def show_about(self):
"""Show about dialog"""
about_text = f"""
<h3>{__app_name__}</h3>
<p>Version {__version__}</p>
<p>A macOS application that prevents system sleep by moving the mouse cursor periodically.</p>
<p><b>Author:</b> {__author__}</p>
<p><b>Email:</b> {__email__}</p>
<br>
<p><i>Built with PyQt5 and Python</i></p>
"""
QtWidgets.QMessageBox.about(None, f"About {__app_name__}", about_text)
def quit(self):
self.logger.info("Application quit requested")
try:
self.mouse_mover.stop()
except Exception as e:
self.logger.error(f"Error stopping mouse mover: {e}")
self.tray_icon.hide()
QtWidgets.QApplication.instance().quit()
self.logger.info("Application exited")
sys.exit(0)
def main():
"""Main entry point"""
try:
app_instance = HereIAmApp()
sys.exit(app_instance.app.exec_())
except KeyboardInterrupt:
print("Application interrupted")
sys.exit(0)
except Exception as e:
logger = get_logger(__name__)
logger.critical(f"Fatal application error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()

0
src/menu_bar.py Normal file
View File

94
src/mouse_mover.py Normal file
View File

@@ -0,0 +1,94 @@
# filepath: src/mouse_mover.py
import time
import threading
import pyautogui
from config import WAITTIME, MovePx, get_logger
class MouseMover:
def __init__(self):
self.logger = get_logger(__name__)
self.countdown = WAITTIME
self.mouse_movement_enabled = False
self.xold = 0
self.yold = 0
self.thread = None
self.running = False
self.logger.debug("MouseMover initialized")
def _move_mouse(self):
"""Move mouse slightly and return to original position"""
try:
pyautogui.moveRel(0, MovePx, duration=0.5) # Faster movement
pyautogui.moveRel(0, -MovePx, duration=0.5)
self.logger.debug(f"Mouse moved {MovePx}px vertically and back")
except Exception as e:
self.logger.error(f"Failed to move mouse: {e}")
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("Mouse movement monitoring started")
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")
while self.running and self.mouse_movement_enabled:
try:
x, y = pyautogui.position()
# Check if timer has expired
if self.countdown <= 0:
self._move_mouse()
self.logger.info(f"Timer expired ({WAITTIME}s), mouse moved automatically")
self.countdown = WAITTIME
continue
# Check if mouse has moved
if x == self.xold and y == self.yold:
# Mouse hasn't moved, count down
self.logger.debug(f"Mouse idle, countdown: {self.countdown}s")
time.sleep(1)
self.countdown -= 1
else:
# Mouse moved, reset timer
self.logger.debug(f"Mouse moved to ({x}, {y}), timer reset")
self.countdown = WAITTIME
self.xold = x
self.yold = y
except Exception as e:
self.logger.error(f"Error in mouse monitoring: {e}")
break
self.logger.debug("Mouse monitoring thread ended")
def get_status(self):
"""Get current status information"""
return {
'running': self.running,
'enabled': self.mouse_movement_enabled,
'countdown': self.countdown,
'thread_alive': self.thread.is_alive() if self.thread else False
}

40
src/utils.py Normal file
View File

@@ -0,0 +1,40 @@
# Security and error handling utilities
import pyautogui
import logging
def setup_pyautogui_safety():
"""Setup PyAutoGUI safety features"""
# Prevent pyautogui from crashing when mouse is moved to corner
pyautogui.FAILSAFE = False
# Set reasonable pause between actions
pyautogui.PAUSE = 0.1
logger = logging.getLogger(__name__)
logger.debug("PyAutoGUI safety features configured")
def safe_move_mouse(x_offset, y_offset, duration=0.5):
"""Safely move mouse with error handling"""
logger = logging.getLogger(__name__)
try:
# Get current position first
current_x, current_y = pyautogui.position()
# Calculate new position
new_x = current_x + x_offset
new_y = current_y + y_offset
# Get screen size for bounds checking
screen_width, screen_height = pyautogui.size()
# Ensure we stay within screen bounds
new_x = max(0, min(new_x, screen_width - 1))
new_y = max(0, min(new_y, screen_height - 1))
# Move mouse
pyautogui.moveTo(new_x, new_y, duration=duration)
return True
except Exception as e:
logger.error(f"Failed to move mouse: {e}")
return False