1.0.0 Release

This commit is contained in:
Jerico Thomas
2025-07-25 15:31:22 -04:00
parent 6c5f8c399e
commit 805524b78c
19 changed files with 1323 additions and 95 deletions

10
MANIFEST.in Normal file
View File

@@ -0,0 +1,10 @@
include README.md
include LICENSE
recursive-include assets *.icns *.png
recursive-include src *.py
recursive-exclude tests *
recursive-exclude logs *
recursive-exclude dist *
recursive-exclude build *
recursive-exclude __pycache__ *
recursive-exclude *.egg-info *

View File

@@ -95,9 +95,16 @@ DEBUG = False # Enable debug logging (default: False)
```
### Logging Configuration
- **Log Location**: `logs/hereiam.log`
- **Log Location**: `~/.hereiam/logs/hereiam.log` (same directory as config file)
- **Log Persistence**: Logs are persistent across reboots
- **Log Rotation**: Automatic (when DEBUG mode is enabled)
- **Log Levels**: INFO (default) or DEBUG (when DEBUG=True)
- **Log Levels**: INFO (default) or DEBUG (when DEBUG=True in options)
### Startup Configuration
- **Launch at Startup**: Automatically launch HereIAm when you log in to macOS
- **Implementation**: Uses macOS Launch Agents (`~/Library/LaunchAgents/`)
- **Requirements**: Only available when using the built .app bundle
- **Configuration**: Can be enabled/disabled through the Options dialog
## Dependencies
@@ -115,19 +122,30 @@ DEBUG = False # Enable debug logging (default: False)
## Project Structure
```
HereIAm/
├── src/
├── src/ # Source code
│ ├── main.py # Application entry point and main GUI logic
│ ├── mouse_mover.py # Mouse monitoring and movement logic
│ ├── config.py # Configuration and logging setup
│ ├── config_manager.py # Configuration management
│ ├── logging_config.py # Logging configuration
│ ├── options_dialog.py # Options dialog UI
│ └── utils.py # Utility functions and safety features
├── assets/
├── assets/ # Application assets
│ ├── Enabled.icns # Icon for enabled state
│ ├── Disabled-Light.icns # Icon for disabled state (light theme)
│ └── Disabled-Dark.icns # Icon for disabled state (dark theme)
├── logs/ # Application logs (created at runtime)
├── tests/ # Test scripts and test documentation
│ ├── test_config.py # Configuration tests
│ ├── test_dialog.py # Dialog functionality tests
│ ├── test_logging.py # Logging tests
│ ├── test_restart.py # Restart functionality tests
│ ├── test_app.sh # App bundle integration tests
│ └── README.md # Test documentation
├── build_app.sh # Build script for macOS app bundle
├── create_dmg.sh # DMG creation script
├── requirements.txt # Python dependencies
├── setup.py # Package installation configuration
── README.md # This file
├── requirements-build.txt # Build dependencies
── setup.py # Package installation configuration
└── README.md # This file
```
## Technical Details
@@ -150,6 +168,36 @@ HereIAm/
- **Graceful Shutdown**: Clean thread termination and resource cleanup
- **Fail-Safe Integration**: PyAutoGUI safety features configured appropriately
## Testing
The project includes comprehensive test scripts located in the `tests/` directory.
### Running Tests
**Python Unit Tests:**
```bash
cd tests
python3 test_config.py # Test configuration management
python3 test_dialog.py # Test options dialog
python3 test_logging.py # Test logging functionality
python3 test_restart.py # Test restart functionality
```
**Application Bundle Test:**
```bash
cd tests
./test_app.sh # Test built macOS app bundle
```
**Note:** Make sure to build the application first with `./build_app.sh` before running app bundle tests.
4. **Testing Startup Functionality**:
```bash
./test_startup.py
```
For detailed testing information, see [tests/README.md](tests/README.md).
## Troubleshooting
### Common Issues
@@ -162,7 +210,7 @@ HereIAm/
**Mouse movement not working:**
- Check that accessibility permissions are granted to Terminal/Python
- Verify pyautogui installation: `python -c "import pyautogui; print('OK')"`
- Review logs in `logs/hereiam.log` for error details
- Review logs in `~/.hereiam/logs/hereiam.log` for error details
**Icons not displaying:**
- Verify all `.icns` files are present in the `assets/` directory
@@ -170,9 +218,9 @@ HereIAm/
- Enable DEBUG mode in config.py for detailed icon loading logs
### Debugging
1. Enable debug mode in `src/config.py`: `DEBUG = True`
1. Enable debug mode in the Options dialog: set Log Level to "DEBUG"
2. Run from terminal to see console output
3. Check `logs/hereiam.log` for detailed operation logs
3. Check `~/.hereiam/logs/hereiam.log` for detailed operation logs
4. Use Activity Monitor to verify the application is running
## Development

98
pyproject.toml Normal file
View File

@@ -0,0 +1,98 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "HereIAm"
dynamic = ["version"]
description = "A macOS application that moves the mouse cursor to prevent inactivity."
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Jerico Thomas", email = "jerico@tekop.net"}
]
maintainers = [
{name = "Jerico Thomas", email = "jerico@tekop.net"}
]
keywords = ["macos", "mouse", "activity", "prevention", "system-tray"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"Operating System :: MacOS",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
"Environment :: MacOS X",
"Environment :: MacOS X :: Cocoa",
]
requires-python = ">=3.8"
dependencies = [
"pyautogui>=0.9.54",
"PyQt5>=5.15.0",
"pyobjc-framework-Cocoa>=9.0",
"pyobjc-framework-Quartz>=9.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=22.0.0",
"flake8>=4.0.0",
]
build = [
"pyinstaller>=5.0.0",
"dmgbuild>=1.6.0",
]
[project.urls]
Homepage = "https://github.com/your-username/hereiam"
Repository = "https://github.com/your-username/hereiam.git"
Issues = "https://github.com/your-username/hereiam/issues"
[project.scripts]
hereiam = "src.main:main"
[project.gui-scripts]
hereiam-gui = "src.main:main"
[tool.setuptools]
package-dir = {"" = "."}
packages = ["src"]
[tool.setuptools.package-data]
"*" = ["assets/*.icns", "assets/*.png"]
[tool.setuptools.dynamic]
version = {attr = "src.__init__.__version__"}
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| venv
| _build
| buck-out
| build
| dist
| tests
)/
'''
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"

View File

@@ -1,23 +1,40 @@
from setuptools import setup, find_packages
import os
import re
# Read version and other metadata from __init__.py
def get_metadata():
"""Extract metadata from src/__init__.py"""
init_file = os.path.join(os.path.dirname(__file__), 'src', '__init__.py')
with open(init_file, 'r', encoding='utf-8') as f:
content = f.read()
version = re.search(r'__version__ = ["\']([^"\']+)["\']', content).group(1)
author = re.search(r'__author__ = ["\']([^"\']+)["\']', content).group(1)
email = re.search(r'__email__ = ["\']([^"\']+)["\']', content).group(1)
return version, author, email
# Read README for long description
def read_readme():
with open("README.md", "r", encoding="utf-8") as fh:
return fh.read()
version, author, email = get_metadata()
setup(
name='HereIAm',
version='1.0.0',
version=version,
description='A macOS application that moves the mouse cursor to prevent inactivity.',
long_description=read_readme(),
long_description_content_type="text/markdown",
author='Jerico Thomas',
author_email='jerico@tekop.net',
author=author,
author_email=email,
url='https://github.com/your-username/hereiam', # Update with your repo URL
packages=find_packages(),
packages=['src'], # Only include src package
package_dir={'src': 'src'},
package_data={
'assets': ['*.icns', '*.png'],
'': ['assets/*.icns', 'assets/*.png'], # Include assets at root level
},
include_package_data=True,
install_requires=[
@@ -38,13 +55,16 @@ setup(
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
"Environment :: MacOS X",
"Environment :: MacOS X :: Cocoa",
],
license="MIT",
)

View File

@@ -1,43 +0,0 @@
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.

71
src/config_manager.py Normal file
View File

@@ -0,0 +1,71 @@
import json
import os
import logging
from logging_config import get_logger
class ConfigManager:
"""Manage application configuration settings"""
def __init__(self):
self.logger = get_logger(__name__)
self.config_dir = os.path.join(os.path.expanduser("~"), ".hereiam")
self.config_file = os.path.join(self.config_dir, "config.json")
# Default settings
self.default_config = {
'wait_time': 240,
'start_enabled': True,
'log_level': 'INFO',
'move_px': 10
}
# Ensure config directory exists
os.makedirs(self.config_dir, exist_ok=True)
def load_config(self):
"""Load configuration from file"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
config = json.load(f)
# Ensure all default keys exist
for key, value in self.default_config.items():
if key not in config:
config[key] = value
self.logger.debug(f"Configuration loaded: {config}")
return config
else:
self.logger.info("No config file found, using defaults")
return self.default_config.copy()
except Exception as e:
self.logger.error(f"Error loading config: {e}")
return self.default_config.copy()
def save_config(self, config):
"""Save configuration to file"""
try:
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=2)
self.logger.info(f"Configuration saved: {config}")
return True
except Exception as e:
self.logger.error(f"Error saving config: {e}")
return False
def get_config_value(self, key, default=None):
"""Get a specific configuration value"""
config = self.load_config()
return config.get(key, default)
def set_config_value(self, key, value):
"""Set a specific configuration value"""
config = self.load_config()
config[key] = value
return self.save_config(config)
def update_config(self, updates):
"""Update multiple configuration values"""
config = self.load_config()
config.update(updates)
return self.save_config(config)

164
src/launch_agent.py Normal file
View File

@@ -0,0 +1,164 @@
"""
macOS Launch Agent management for HereIAm application.
Handles creating, installing, and removing Launch Agents for startup functionality.
"""
import os
import sys
import plistlib
from pathlib import Path
from logging_config import get_logger
logger = get_logger(__name__)
class LaunchAgentManager:
"""Manages macOS Launch Agents for application startup"""
def __init__(self, app_name="HereIAm"):
self.app_name = app_name
self.bundle_id = f"com.hereiam.{app_name.lower()}"
self.launch_agents_dir = Path.home() / "Library" / "LaunchAgents"
self.plist_filename = f"{self.bundle_id}.plist"
self.plist_path = self.launch_agents_dir / self.plist_filename
# Ensure LaunchAgents directory exists
self.launch_agents_dir.mkdir(parents=True, exist_ok=True)
def get_app_path(self):
"""Get the path to the current application executable"""
if getattr(sys, 'frozen', False):
# Running as PyInstaller bundle
# Get the .app bundle path from the executable path
executable_path = sys.executable
# Navigate up from Contents/MacOS/HereIAm to get HereIAm.app
app_bundle = Path(executable_path).parent.parent.parent
return str(app_bundle)
else:
# Running in development mode - not suitable for launch agent
# Return the Python script path for development testing
return sys.argv[0]
def create_launch_agent_plist(self):
"""Create the Launch Agent plist configuration"""
app_path = self.get_app_path()
# For .app bundles, use 'open' command to launch properly
if app_path.endswith('.app'):
program_arguments = ['/usr/bin/open', '-a', app_path]
else:
# For development mode
program_arguments = [sys.executable, app_path]
plist_data = {
'Label': self.bundle_id,
'ProgramArguments': program_arguments,
'RunAtLoad': True,
'KeepAlive': False,
'StandardOutPath': str(Path.home() / '.hereiam' / 'logs' / 'launchd.log'),
'StandardErrorPath': str(Path.home() / '.hereiam' / 'logs' / 'launchd-error.log'),
}
return plist_data
def is_startup_enabled(self):
"""Check if startup is currently enabled"""
return self.plist_path.exists()
def enable_startup(self):
"""Enable application startup at login"""
try:
logger.info("Enabling startup at login...")
# Create the plist data
plist_data = self.create_launch_agent_plist()
# Write the plist file
with open(self.plist_path, 'wb') as f:
plistlib.dump(plist_data, f)
logger.info(f"Launch Agent created at: {self.plist_path}")
# Load the launch agent immediately
self._load_launch_agent()
return True
except Exception as e:
logger.error(f"Failed to enable startup: {e}")
return False
def disable_startup(self):
"""Disable application startup at login"""
try:
logger.info("Disabling startup at login...")
# Unload the launch agent if it's loaded
self._unload_launch_agent()
# Remove the plist file
if self.plist_path.exists():
self.plist_path.unlink()
logger.info(f"Launch Agent removed: {self.plist_path}")
return True
except Exception as e:
logger.error(f"Failed to disable startup: {e}")
return False
def _load_launch_agent(self):
"""Load the launch agent using launchctl"""
try:
import subprocess
cmd = ['launchctl', 'load', str(self.plist_path)]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
logger.debug("Launch Agent loaded successfully")
else:
logger.warning(f"Failed to load Launch Agent: {result.stderr}")
except Exception as e:
logger.error(f"Error loading Launch Agent: {e}")
def _unload_launch_agent(self):
"""Unload the launch agent using launchctl"""
try:
import subprocess
cmd = ['launchctl', 'unload', str(self.plist_path)]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
logger.debug("Launch Agent unloaded successfully")
else:
# It's okay if unload fails (agent might not be loaded)
logger.debug(f"Launch Agent unload result: {result.stderr}")
except Exception as e:
logger.error(f"Error unloading Launch Agent: {e}")
def get_launch_agent_info(self):
"""Get information about the current launch agent configuration"""
if not self.is_startup_enabled():
return None
try:
with open(self.plist_path, 'rb') as f:
plist_data = plistlib.load(f)
return {
'enabled': True,
'path': str(self.plist_path),
'program_arguments': plist_data.get('ProgramArguments', []),
'label': plist_data.get('Label', ''),
'run_at_load': plist_data.get('RunAtLoad', False)
}
except Exception as e:
logger.error(f"Error reading launch agent info: {e}")
return None
def get_launch_agent_manager():
"""Factory function to get a LaunchAgentManager instance"""
return LaunchAgentManager()

71
src/logging_config.py Normal file
View File

@@ -0,0 +1,71 @@
import logging
import os
import sys
from datetime import datetime
# Logging configuration - use same directory as config file
def get_log_directory():
"""Get the appropriate log directory based on runtime environment"""
# Use the same directory structure as config manager
config_dir = os.path.join(os.path.expanduser("~"), ".hereiam")
log_dir = os.path.join(config_dir, "logs")
return log_dir
LOG_DIR = get_log_directory()
LOG_FILE = os.path.join(LOG_DIR, 'hereiam.log')
def setup_logging(debug=False, force_reconfigure=False):
"""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'
# Get the root logger
root_logger = logging.getLogger()
# If we need to reconfigure, remove existing handlers
if force_reconfigure and root_logger.handlers:
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
# Only configure if no handlers exist or we're forcing reconfiguration
if not root_logger.handlers or force_reconfigure:
# Create formatter
formatter = logging.Formatter(log_format)
# Create file handler
file_handler = logging.FileHandler(LOG_FILE)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Add console handler if debug mode
if debug:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# Set the root logger level
root_logger.setLevel(log_level)
# Log startup with config directory info
logger = logging.getLogger(__name__)
logger.info(f"HereIAm logging configured - Level: {'DEBUG' if debug else 'INFO'}")
logger.info(f"Log directory: {LOG_DIR}")
logger.debug(f"Log file: {LOG_FILE}")
def get_logger(name):
"""Get a logger instance"""
return logging.getLogger(name)
def get_log_file_path():
"""Get the current log file path"""
return LOG_FILE
def get_log_directory_path():
"""Get the current log directory path"""
return LOG_DIR

View File

@@ -1,29 +1,61 @@
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
import os
import logging
import subprocess
from mouse_mover import MouseMover
from config import StartEnabled, setup_logging, get_logger
from logging_config import setup_logging, get_logger
from config_manager import ConfigManager
from options_dialog import OptionsDialog
from launch_agent import get_launch_agent_manager
from __init__ import __version__, __author__, __email__, __app_name__
# Setup logging before anything else
# We'll setup with default first, then update after loading config
setup_logging()
class HereIAmApp:
def __init__(self):
# Initialize configuration manager first
self.config_manager = ConfigManager()
self.config = self.config_manager.load_config()
# Update logging based on config with force reconfigure
debug_mode = self.config.get('log_level', 'INFO').upper() == 'DEBUG'
setup_logging(debug=debug_mode, force_reconfigure=True)
# Now get logger after proper configuration
self.logger = get_logger(__name__)
self.logger.info("Initializing HereIAm application")
self.logger.info(f"Config loaded: {self.config}")
self.app = QtWidgets.QApplication(sys.argv)
self.app.setQuitOnLastWindowClosed(False) # Keep running when windows close
# Initialize MouseMover instance
self.mouse_mover = MouseMover()
# Initialize MouseMover instance with configured wait time and move distance
self.mouse_mover = MouseMover(
wait_time=self.config.get('wait_time', 240),
move_px=self.config.get('move_px', 10)
)
# Initialize state from config
self.mouse_mover_enabled = StartEnabled
# Initialize state from config (prioritize saved config over default)
self.mouse_mover_enabled = self.config.get('start_enabled', True)
# Initialize launch agent manager
self.launch_agent_manager = get_launch_agent_manager()
# Dialog management
self.options_dialog = None
# Initialize icon paths (use absolute paths)
self.base_path = os.path.dirname(os.path.dirname(__file__))
# Handle PyInstaller bundle paths
if getattr(sys, 'frozen', False):
# Running in a PyInstaller bundle
self.base_path = sys._MEIPASS
else:
# Running in development mode
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")
@@ -50,17 +82,20 @@ class HereIAmApp:
self.tray_icon.show()
# Start mouse movement if enabled in config
if StartEnabled:
if self.mouse_mover_enabled:
self.mouse_mover.start()
self.logger.info("HereIAm application initialized successfully")
def _verify_icons(self):
"""Verify that all required icon files exist"""
self.logger.debug(f"Base path for icons: {self.base_path}")
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}")
else:
self.logger.debug(f"Icon file found: {icon_path}")
def _create_menu(self):
"""Create the context menu"""
@@ -77,8 +112,8 @@ class HereIAmApp:
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:
# Set initial menu state based on current config
if self.mouse_mover_enabled:
self.enable_action.setEnabled(False)
self.disable_action.setEnabled(True)
else:
@@ -88,6 +123,7 @@ class HereIAmApp:
self.menu.addSeparator()
# Additional options
self.options_action = self.menu.addAction("Options...")
self.about_action = self.menu.addAction("About HereIAm")
self.quit_action = self.menu.addAction("Quit")
@@ -101,7 +137,8 @@ class HereIAmApp:
# 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.options_action.triggered.connect(self.show_options)
self.about_action.triggered.connect(lambda: self._handle_action(self.show_about))
self.quit_action.triggered.connect(self.quit)
# Listen for system theme changes
@@ -126,10 +163,34 @@ class HereIAmApp:
else:
icon_path = self.disabled_dark_icon if self.is_dark_mode() else self.disabled_light_icon
self.logger.debug(f"Attempting to set icon: {icon_path}")
if os.path.exists(icon_path):
self.tray_icon.setIcon(QtGui.QIcon(icon_path))
try:
icon = QtGui.QIcon(icon_path)
if not icon.isNull():
self.tray_icon.setIcon(icon)
self.logger.debug(f"Successfully set icon: {icon_path}")
else:
self.logger.warning(f"Icon file exists but is invalid: {icon_path}")
self._set_fallback_icon()
except Exception as e:
self.logger.error(f"Error setting icon {icon_path}: {e}")
self._set_fallback_icon()
else:
self.logger.warning(f"Icon file not found: {icon_path}")
self._set_fallback_icon()
def _set_fallback_icon(self):
"""Set a fallback system icon if custom icons fail"""
try:
# Use a standard system icon as fallback
style = self.app.style()
icon = style.standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
self.tray_icon.setIcon(icon)
self.logger.info("Using fallback system icon")
except Exception as e:
self.logger.error(f"Failed to set fallback icon: {e}")
def _update_status_display(self):
"""Update the status display in the menu"""
@@ -174,22 +235,318 @@ class HereIAmApp:
self.mouse_mover.stop()
self.logger.debug("Mouse movement disabled successfully")
def show_options(self):
"""Show options dialog"""
self.logger.debug("Opening options dialog")
# Prevent multiple dialog instances
if self.options_dialog is not None:
try:
# Check if dialog is still valid and visible
if hasattr(self.options_dialog, 'isVisible') and self.options_dialog.isVisible():
self.logger.debug("Options dialog already open, bringing to front")
self.options_dialog.raise_()
self.options_dialog.activateWindow()
return
else:
# Dialog exists but not visible, clean it up
self._cleanup_options_dialog()
except RuntimeError:
# Dialog object is invalid, clean up reference
self.options_dialog = None
# Get current values
current_wait_time = self.config.get('wait_time', 240)
current_start_enabled = self.config.get('start_enabled', True)
current_log_level = self.config.get('log_level', 'INFO')
current_move_px = self.config.get('move_px', 10)
current_launch_at_startup = self.launch_agent_manager.is_startup_enabled()
# Create a new dialog instance
try:
self.options_dialog = OptionsDialog(
current_wait_time=current_wait_time,
current_start_enabled=current_start_enabled,
current_log_level=current_log_level,
current_move_px=current_move_px,
current_launch_at_startup=current_launch_at_startup,
parent=None # Use None as parent for system tray apps
)
# Connect finished signal to cleanup (but handle cleanup manually)
self.options_dialog.finished.connect(self._on_options_dialog_finished)
# Show dialog and get result
result = self.options_dialog.exec_()
# Get values immediately after exec_() while dialog is still valid
new_values = None
if result == QtWidgets.QDialog.Accepted:
try:
new_values = self.options_dialog.get_values()
if new_values is not None:
self.logger.info(f"Options updated: {new_values}")
else:
self.logger.error("Failed to get values from options dialog")
except (RuntimeError, AttributeError) as e:
self.logger.error(f"Error getting dialog values: {e}")
new_values = None
# Process the saved values if dialog was accepted and values are valid
if result == QtWidgets.QDialog.Accepted and new_values is not None:
# Check if restart is needed (start_enabled changed)
restart_needed = new_values['start_enabled'] != current_start_enabled
if restart_needed:
self.logger.info(f"Start enabled changed from {current_start_enabled} to {new_values['start_enabled']}, restart required")
# Handle launch at startup setting
if 'launch_at_startup' in new_values:
self._handle_startup_setting(new_values['launch_at_startup'])
# Save new configuration (excluding launch_at_startup as it's not a config setting)
config_values = {k: v for k, v in new_values.items() if k != 'launch_at_startup'}
self.config.update(config_values)
self.config_manager.save_config(self.config)
# Apply changes immediately where possible
if new_values['wait_time'] != current_wait_time:
self.mouse_mover.update_wait_time(new_values['wait_time'])
self.logger.debug(f"Wait time updated to {new_values['wait_time']} seconds")
if new_values['move_px'] != current_move_px:
self.mouse_mover.update_move_px(new_values['move_px'])
self.logger.debug(f"Move distance updated to {new_values['move_px']} pixels")
# Update log level
if new_values['log_level'] != current_log_level:
self._update_log_level(new_values['log_level'])
# Restart application if needed
if restart_needed:
self.logger.info("Start enabled setting changed, restarting application...")
self.restart_application()
else:
self.logger.debug("Settings applied immediately, no restart needed")
else:
self.logger.debug("Options dialog cancelled")
except Exception as e:
self.logger.error(f"Error showing options dialog: {e}")
QtWidgets.QMessageBox.critical(
None,
"Error",
f"Failed to open options dialog: {str(e)}"
)
finally:
# Always ensure cleanup happens
self._cleanup_options_dialog()
def _on_options_dialog_finished(self, result):
"""Called when options dialog is finished"""
self.logger.debug(f"Options dialog finished with result: {result}")
# Note: Cleanup is handled in show_options() method to avoid timing issues
def _cleanup_options_dialog(self):
"""Clean up options dialog reference"""
if self.options_dialog is not None:
try:
# Disconnect signals to prevent issues
self.options_dialog.finished.disconnect()
except (TypeError, RuntimeError, AttributeError):
# Signal already disconnected, object deleted, or doesn't exist
pass
try:
# Only call deleteLater if the object is still valid
self.options_dialog.deleteLater()
except RuntimeError:
# Object already deleted by Qt
pass
# Always clear the reference regardless of errors
self.options_dialog = None
def _update_log_level(self, log_level_str):
"""Update the current log level"""
try:
log_level = getattr(logging, log_level_str.upper())
# Update all loggers
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# If switching to DEBUG, add console handler if not present
# If switching away from DEBUG, remove console handler
debug_mode = log_level_str.upper() == 'DEBUG'
# Force reconfigure logging to handle console output properly
setup_logging(debug=debug_mode, force_reconfigure=True)
self.logger.info(f"Log level updated to {log_level_str}")
except AttributeError:
self.logger.error(f"Invalid log level: {log_level_str}")
def _handle_startup_setting(self, enable_startup):
"""Handle enabling or disabling startup at login"""
try:
current_startup_enabled = self.launch_agent_manager.is_startup_enabled()
if enable_startup and not current_startup_enabled:
# Enable startup
success = self.launch_agent_manager.enable_startup()
if success:
self.logger.info("Successfully enabled startup at login")
else:
self.logger.error("Failed to enable startup at login")
# Show error message to user
QtWidgets.QMessageBox.warning(
None,
"Startup Configuration",
"Failed to enable startup at login. Please check the logs for details."
)
elif not enable_startup and current_startup_enabled:
# Disable startup
success = self.launch_agent_manager.disable_startup()
if success:
self.logger.info("Successfully disabled startup at login")
else:
self.logger.error("Failed to disable startup at login")
# Show error message to user
QtWidgets.QMessageBox.warning(
None,
"Startup Configuration",
"Failed to disable startup at login. Please check the logs for details."
)
else:
self.logger.debug(f"Startup setting unchanged: {enable_startup}")
except Exception as e:
self.logger.error(f"Error handling startup setting: {e}")
QtWidgets.QMessageBox.critical(
None,
"Startup Configuration Error",
f"An error occurred while configuring startup settings:\n{str(e)}"
)
def restart_application(self):
"""Restart the application"""
self.logger.info("Restarting application...")
try:
# Stop the mouse mover
self.mouse_mover.stop()
# Hide the tray icon
self.tray_icon.hide()
# Get the current script path
script_path = os.path.abspath(__file__)
# If running as a module, try to restart via module
if __name__ != "__main__":
# Try to restart via python -m or the original command
args = [sys.executable] + sys.argv
else:
# Direct script execution
args = [sys.executable, script_path]
self.logger.debug(f"Restarting with command: {' '.join(args)}")
# Start a new instance of the application
subprocess.Popen(args, cwd=os.getcwd())
# Exit current instance
self.logger.info("New instance started, exiting current instance")
QtWidgets.QApplication.instance().quit()
sys.exit(0)
except Exception as e:
self.logger.error(f"Error restarting application: {e}")
# Fallback to regular quit if restart fails
self.quit()
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)
self.logger.debug("Opening about dialog")
try:
from logging_config import get_log_file_path
log_path = get_log_file_path()
# Get startup status
startup_status = "Enabled" if self.launch_agent_manager.is_startup_enabled() else "Disabled"
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><b>Launch at Startup:</b> {startup_status}</p>
<p><b>Log File:</b> {log_path}</p>
<p><i>Logs are stored in ~/.hereiam/logs/ directory</i></p>
<br>
<p><i>Built with PyQt5 and Python</i></p>
"""
self.logger.debug(f"About dialog text prepared: {about_text[:100]}...")
# Create and show the about dialog with explicit parent handling
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle(f"About {__app_name__}")
msg_box.setText(about_text)
msg_box.setTextFormat(QtCore.Qt.RichText)
msg_box.setIcon(QtWidgets.QMessageBox.Information)
# Ensure dialog appears on top and is properly managed
msg_box.setWindowFlags(
QtCore.Qt.Dialog |
QtCore.Qt.WindowStaysOnTopHint |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint
)
# Center the dialog on screen
screen = QtWidgets.QApplication.primaryScreen()
if screen:
screen_geometry = screen.geometry()
msg_box.resize(450, 350) # Slightly larger to accommodate log path
dialog_geometry = msg_box.geometry()
x = (screen_geometry.width() - dialog_geometry.width()) // 2
y = (screen_geometry.height() - dialog_geometry.height()) // 2
msg_box.move(x, y)
self.logger.debug("Showing about dialog")
msg_box.exec_()
self.logger.debug("About dialog closed")
except Exception as e:
self.logger.error(f"Error showing about dialog: {e}")
# Fallback to simple message
try:
QtWidgets.QMessageBox.information(
None,
f"About {__app_name__}",
f"{__app_name__} - Version {__version__}\nBy {__author__}"
)
except Exception as fallback_error:
self.logger.error(f"Fallback about dialog also failed: {fallback_error}")
# Last resort - show a notification
self.tray_icon.showMessage(
f"About {__app_name__}",
f"Version {__version__} by {__author__}",
QtWidgets.QSystemTrayIcon.Information,
5000
)
def quit(self):
self.logger.info("Application quit requested")
try:
# Clean up options dialog if open
if self.options_dialog is not None:
self.options_dialog.close()
self._cleanup_options_dialog()
self.mouse_mover.stop()
except Exception as e:
self.logger.error(f"Error stopping mouse mover: {e}")

View File

View File

@@ -3,25 +3,27 @@
import time
import threading
import pyautogui
from config import WAITTIME, MovePx, get_logger
from logging_config import get_logger
class MouseMover:
def __init__(self):
def __init__(self, wait_time=240, move_px=10):
self.logger = get_logger(__name__)
self.countdown = WAITTIME
self.wait_time = wait_time
self.move_px = move_px
self.countdown = self.wait_time
self.mouse_movement_enabled = False
self.xold = 0
self.yold = 0
self.thread = None
self.running = False
self.logger.debug("MouseMover initialized")
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:
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")
pyautogui.moveRel(0, self.move_px, duration=0.5) # Faster movement
pyautogui.moveRel(0, -self.move_px, duration=0.5)
self.logger.debug(f"Mouse moved {self.move_px}px vertically and back")
except Exception as e:
self.logger.error(f"Failed to move mouse: {e}")
@@ -32,7 +34,7 @@ class MouseMover:
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")
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")
@@ -61,8 +63,8 @@ class MouseMover:
# 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
self.logger.info(f"Timer expired ({self.wait_time}s), mouse moved automatically")
self.countdown = self.wait_time
continue
# Check if mouse has moved
@@ -74,7 +76,7 @@ class MouseMover:
else:
# Mouse moved, reset timer
self.logger.debug(f"Mouse moved to ({x}, {y}), timer reset")
self.countdown = WAITTIME
self.countdown = self.wait_time
self.xold = x
self.yold = y
@@ -84,11 +86,26 @@ class MouseMover:
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
}

153
src/options_dialog.py Normal file
View File

@@ -0,0 +1,153 @@
from PyQt5 import QtWidgets, QtCore, QtGui
import logging
from logging_config import get_logger
from launch_agent import get_launch_agent_manager
class OptionsDialog(QtWidgets.QDialog):
"""Options dialog for configuring HereIAm settings"""
def __init__(self, current_wait_time=240, current_start_enabled=True, current_log_level="INFO", current_move_px=10, current_launch_at_startup=False, parent=None):
super().__init__(parent)
self.logger = get_logger(__name__)
# Store current values
self.wait_time = current_wait_time
self.start_enabled = current_start_enabled
self.log_level = current_log_level
self.move_px = current_move_px
self.launch_at_startup = current_launch_at_startup
# Initialize launch agent manager
self.launch_agent_manager = get_launch_agent_manager()
self.setupUI()
self.load_current_values()
def setupUI(self):
"""Setup the dialog UI"""
self.setWindowTitle("HereIAm Options")
self.setModal(True)
self.setFixedSize(400, 220) # Increased height to accommodate new option
# Ensure dialog appears on top and is properly managed
self.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.WindowStaysOnTopHint)
# Remove WA_DeleteOnClose to prevent automatic deletion conflicts
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(10) # Reduce spacing between elements
# Create form layout
form_layout = QtWidgets.QFormLayout()
form_layout.setVerticalSpacing(8) # Reduce vertical spacing between form rows
form_layout.setHorizontalSpacing(10) # Set horizontal spacing
# Wait Time setting
self.wait_time_spinbox = QtWidgets.QSpinBox()
self.wait_time_spinbox.setRange(10, 3600) # 10 seconds to 1 hour
self.wait_time_spinbox.setSuffix(" seconds")
self.wait_time_spinbox.setToolTip("Time to wait before moving mouse when idle (Range: 10-3600 seconds)")
self.wait_time_spinbox.setKeyboardTracking(True) # Enable keyboard input
self.wait_time_spinbox.setButtonSymbols(QtWidgets.QAbstractSpinBox.UpDownArrows) # Ensure arrows are visible
self.wait_time_spinbox.lineEdit().setReadOnly(False) # Allow typing in the field
form_layout.addRow("Wait Time (10-3600s):", self.wait_time_spinbox)
# Mouse Movement Distance setting
self.move_px_spinbox = QtWidgets.QSpinBox()
self.move_px_spinbox.setRange(1, 100) # 1 to 100 pixels
self.move_px_spinbox.setSuffix(" pixels")
self.move_px_spinbox.setToolTip("Distance to move mouse in pixels (Range: 1-100 pixels)")
self.move_px_spinbox.setKeyboardTracking(True) # Enable keyboard input
self.move_px_spinbox.setButtonSymbols(QtWidgets.QAbstractSpinBox.UpDownArrows) # Ensure arrows are visible
self.move_px_spinbox.lineEdit().setReadOnly(False) # Allow typing in the field
form_layout.addRow("Movement Distance (1-100px):", self.move_px_spinbox)
# Start Enabled setting
self.start_enabled_checkbox = QtWidgets.QCheckBox()
self.start_enabled_checkbox.setToolTip("Start HereIAm enabled when application launches")
form_layout.addRow("Start Enabled:", self.start_enabled_checkbox)
# Launch at Startup setting
self.launch_at_startup_checkbox = QtWidgets.QCheckBox()
self.launch_at_startup_checkbox.setToolTip("Automatically launch HereIAm when you log in to macOS")
form_layout.addRow("Launch at Startup:", self.launch_at_startup_checkbox)
# Log Level setting
self.log_level_combo = QtWidgets.QComboBox()
self.log_level_combo.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
self.log_level_combo.setToolTip("Set the logging level for the application")
form_layout.addRow("Log Level:", self.log_level_combo)
layout.addLayout(form_layout)
# Button box
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
# Center the dialog on screen
self.center_on_screen()
def center_on_screen(self):
"""Center the dialog on the screen"""
screen = QtWidgets.QApplication.primaryScreen()
if screen:
screen_geometry = screen.geometry()
dialog_geometry = self.geometry()
x = (screen_geometry.width() - dialog_geometry.width()) // 2
y = (screen_geometry.height() - dialog_geometry.height()) // 2
self.move(x, y)
def showEvent(self, event):
"""Called when dialog is shown"""
super().showEvent(event)
self.raise_()
self.activateWindow()
def closeEvent(self, event):
"""Called when dialog is closed"""
self.logger.debug("Options dialog closing")
super().closeEvent(event)
def load_current_values(self):
"""Load current values into the form"""
self.wait_time_spinbox.setValue(self.wait_time)
self.move_px_spinbox.setValue(self.move_px)
self.start_enabled_checkbox.setChecked(self.start_enabled)
# Check current startup status from the system
current_startup_enabled = self.launch_agent_manager.is_startup_enabled()
self.launch_at_startup_checkbox.setChecked(current_startup_enabled)
# Set log level
index = self.log_level_combo.findText(self.log_level)
if index >= 0:
self.log_level_combo.setCurrentIndex(index)
def get_values(self):
"""Get the values from the form"""
try:
return {
'wait_time': self.wait_time_spinbox.value(),
'move_px': self.move_px_spinbox.value(),
'start_enabled': self.start_enabled_checkbox.isChecked(),
'launch_at_startup': self.launch_at_startup_checkbox.isChecked(),
'log_level': self.log_level_combo.currentText()
}
except AttributeError as e:
# If widgets are already destroyed, return None
self.logger.error(f"Error getting dialog values: {e}")
return None
@staticmethod
def get_options(current_wait_time=240, current_start_enabled=True, current_log_level="INFO", current_move_px=10, current_launch_at_startup=False, parent=None):
"""Static method to show the dialog and return values"""
dialog = OptionsDialog(current_wait_time, current_start_enabled, current_log_level, current_move_px, current_launch_at_startup, parent)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
return dialog.get_values(), True
return None, False

54
test_startup.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Test script for Launch Agent functionality.
"""
import sys
import os
# Add src directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from launch_agent import get_launch_agent_manager
from logging_config import setup_logging, get_logger
def test_launch_agent():
"""Test the launch agent functionality"""
setup_logging(debug=True)
logger = get_logger(__name__)
print("🧪 Testing Launch Agent Functionality")
print("=" * 40)
# Get launch agent manager
manager = get_launch_agent_manager()
# Check current status
is_enabled = manager.is_startup_enabled()
print(f"📋 Current startup status: {'Enabled' if is_enabled else 'Disabled'}")
if is_enabled:
info = manager.get_launch_agent_info()
if info:
print(f"📂 Plist path: {info['path']}")
print(f"🚀 Program: {' '.join(info['program_arguments'])}")
# Test app path detection
app_path = manager.get_app_path()
print(f"📱 Detected app path: {app_path}")
# Test plist creation (don't install it)
plist_data = manager.create_launch_agent_plist()
print(f"📄 Generated plist data:")
for key, value in plist_data.items():
print(f" {key}: {value}")
print("\n✅ Launch Agent test completed!")
print("\nTo test startup functionality:")
print("1. Build the app: ./build_app.sh")
print("2. Run the app and go to Options")
print("3. Enable 'Launch at Startup'")
print("4. Log out and back in to test")
if __name__ == "__main__":
test_launch_agent()

42
tests/README.md Normal file
View File

@@ -0,0 +1,42 @@
# HereIAm Tests
This directory contains all test scripts for the HereIAm application.
## Test Scripts
### Python Unit Tests
- `test_config.py` - Tests configuration management
- `test_dialog.py` - Tests options dialog functionality
- `test_logging.py` - Tests logging configuration
- `test_restart.py` - Tests application restart functionality
### Shell Scripts
- `test_app.sh` - Tests the built macOS application bundle
## Running Tests
### Python Tests
Run individual test files:
```bash
cd tests
python3 test_config.py
python3 test_dialog.py
python3 test_logging.py
python3 test_restart.py
```
### App Bundle Test
Test the built application:
```bash
cd tests
./test_app.sh
```
**Note:** Make sure to build the application first by running `../build_app.sh` from the tests directory.
## Test Requirements
- Python 3.x
- PyQt5
- Built HereIAm.app (for app bundle tests)
- macOS (for app bundle tests)

View File

@@ -15,12 +15,12 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
APP_PATH="dist/HereIAm.app"
APP_PATH="../dist/HereIAm.app"
# Check if app exists
if [ ! -d "$APP_PATH" ]; then
echo -e "${RED}❌ Error: $APP_PATH not found!${NC}"
echo "Please run './build_app.sh' first."
echo "Please run '../build_app.sh' first."
exit 1
fi
@@ -133,6 +133,6 @@ echo -e "${GREEN}📱 HereIAm.app is ready for distribution${NC}"
echo ""
echo -e "${YELLOW}📝 Next steps:${NC}"
echo "1. Test manually: open $APP_PATH"
echo "2. Create DMG: ./create_dmg.sh"
echo "2. Create DMG: ../create_dmg.sh"
echo "3. For distribution: code sign and notarize"
echo ""

47
tests/test_config.py Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Test script to verify the configuration system works correctly
"""
import sys
import os
sys.path.insert(0, '/Users/JeThomas/scripts/HereIAm/src')
from config_manager import ConfigManager
def test_config_manager():
print("Testing ConfigManager...")
# Create config manager
cm = ConfigManager()
# Test loading default config
config = cm.load_config()
print(f"Default config: {config}")
# Test updating config with new move_px
test_config = {
'wait_time': 300,
'start_enabled': False,
'log_level': 'DEBUG',
'move_px': 15
}
success = cm.save_config(test_config)
print(f"Save config success: {success}")
# Test loading updated config
loaded_config = cm.load_config()
print(f"Loaded config: {loaded_config}")
# Test individual value operations
cm.set_config_value('wait_time', 180)
cm.set_config_value('move_px', 20)
wait_time = cm.get_config_value('wait_time')
move_px = cm.get_config_value('move_px')
print(f"Individual wait_time: {wait_time}")
print(f"Individual move_px: {move_px}")
print("ConfigManager tests completed!")
if __name__ == "__main__":
test_config_manager()

47
tests/test_dialog.py Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Test script to verify options dialog works correctly
"""
import sys
import os
sys.path.insert(0, '/Users/JeThomas/scripts/HereIAm/src')
from PyQt5 import QtWidgets
from options_dialog import OptionsDialog
def test_dialog_multiple_opens():
print("Testing multiple dialog opens...")
app = QtWidgets.QApplication(sys.argv)
# Test opening dialog multiple times
for i in range(3):
print(f"Opening dialog #{i+1}")
dialog = OptionsDialog(
current_wait_time=240 + i*10,
current_start_enabled=True,
current_log_level="INFO",
current_move_px=10 + i
)
# Show dialog briefly (simulate quick open/close)
dialog.show()
app.processEvents() # Process pending events
# Get values to ensure dialog is functional
values = dialog.get_values()
print(f"Dialog #{i+1} values: {values}")
# Close dialog
dialog.close()
dialog.deleteLater()
app.processEvents() # Process cleanup events
print(f"Dialog #{i+1} closed successfully")
print("Multiple dialog test completed successfully!")
app.quit()
if __name__ == "__main__":
test_dialog_multiple_opens()

42
tests/test_logging.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
Test script to verify logging is working correctly
"""
import sys
import os
import time
sys.path.insert(0, '/Users/JeThomas/scripts/HereIAm/src')
from logging_config import setup_logging, get_logger
from config_manager import ConfigManager
def test_logging():
print("Testing logging system...")
# Test 1: Default logging setup
setup_logging()
logger = get_logger("test_logger")
logger.info("Test message 1 - Default setup")
logger.debug("This debug message should NOT appear in default setup")
# Test 2: Debug mode
print("\nSwitching to DEBUG mode...")
setup_logging(debug=True, force_reconfigure=True)
logger.info("Test message 2 - Debug mode")
logger.debug("This debug message SHOULD appear in debug mode")
# Test 3: Load config and apply logging
print("\nTesting with config manager...")
cm = ConfigManager()
config = cm.load_config()
debug_mode = config.get('log_level', 'INFO').upper() == 'DEBUG'
setup_logging(debug=debug_mode, force_reconfigure=True)
logger.info(f"Test message 3 - Config mode (debug={debug_mode})")
logger.debug(f"Debug message based on config: log_level={config.get('log_level', 'INFO')}")
print(f"\nCheck the log file at: ~/logs/hereiam.log")
print("Also check console output above for debug messages")
if __name__ == "__main__":
test_logging()

30
tests/test_restart.py Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""
Test script to verify restart logic
"""
import sys
import os
import subprocess
def test_restart_logic():
print("Testing restart logic...")
# Simulate the restart logic
script_path = os.path.abspath(__file__)
print(f"Current script path: {script_path}")
print(f"sys.argv: {sys.argv}")
print(f"__name__: {__name__}")
print(f"sys.executable: {sys.executable}")
print(f"Current working directory: {os.getcwd()}")
# Show what command would be used for restart
if __name__ != "__main__":
args = [sys.executable] + sys.argv
else:
args = [sys.executable, script_path]
print(f"Restart command would be: {' '.join(args)}")
print("Test completed - no actual restart performed")
if __name__ == "__main__":
test_restart_logic()