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:
148
.gitignore
vendored
Normal file
148
.gitignore
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
# HereIAm .gitignore
|
||||
# =================
|
||||
|
||||
# Build and Distribution
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec.bak
|
||||
temp_*.dmg
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon?
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
*.log.*
|
||||
|
||||
# IDE and Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.so
|
||||
.Python
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
htmlcov/
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# DMG temporary files
|
||||
dmg_temp/
|
||||
*.tmp.dmg
|
||||
|
||||
# Code signing artifacts
|
||||
*.p12
|
||||
*.mobileprovision
|
||||
*.certSigningRequest
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
*.old
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# Application specific
|
||||
# Keep the actual app files but ignore temporary builds
|
||||
# dist/*.app # Uncomment if you don't want to track the built app
|
||||
# dist/*.dmg # Uncomment if you don't want to track the DMG
|
||||
|
||||
# Security - Never commit these!
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p8
|
||||
*.mobileprovision
|
||||
keychain_profiles.txt
|
||||
notarization_passwords.txt
|
||||
|
||||
# Local configuration files that might contain secrets
|
||||
local_config.py
|
||||
secrets.py
|
||||
.secrets
|
||||
|
||||
# Xcode (if you ever need to modify the app bundle)
|
||||
*.xcodeproj/
|
||||
*.xcworkspace/
|
||||
DerivedData/
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# CocoaPods
|
||||
Pods/
|
||||
Podfile.lock
|
||||
|
||||
# Development tools
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
245
DISTRIBUTION.md
Normal file
245
DISTRIBUTION.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# HereIAm Distribution Guide
|
||||
|
||||
## Building for macOS Distribution
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
# 1. Build the .app
|
||||
./build_app.sh
|
||||
|
||||
# 2. Create distributable DMG
|
||||
./create_dmg.sh
|
||||
|
||||
# 3. Your app is ready at: dist/HereIAm-1.0.0.dmg
|
||||
```
|
||||
|
||||
### Detailed Build Process
|
||||
|
||||
#### Prerequisites
|
||||
- macOS 10.14+ (for building)
|
||||
- Python 3.8+
|
||||
- Xcode Command Line Tools: `xcode-select --install`
|
||||
|
||||
#### Step 1: Environment Setup
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone <your-repo>
|
||||
cd HereIAm
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-build.txt
|
||||
```
|
||||
|
||||
#### Step 2: Build the Application
|
||||
```bash
|
||||
# Build .app bundle
|
||||
./build_app.sh
|
||||
```
|
||||
|
||||
This creates `dist/HereIAm.app` with:
|
||||
- All dependencies bundled
|
||||
- Icons and assets included
|
||||
- Menu bar-only app (no dock icon)
|
||||
- Ready to run on any macOS 10.14+ system
|
||||
|
||||
#### Step 3: Create Distribution Package
|
||||
```bash
|
||||
# Create DMG for distribution
|
||||
./create_dmg.sh
|
||||
```
|
||||
|
||||
This creates `dist/HereIAm-1.0.0.dmg` with:
|
||||
- Drag-to-Applications installer
|
||||
- Professional DMG layout
|
||||
- README instructions
|
||||
- Compressed for smaller download
|
||||
|
||||
### Code Signing (Recommended for Distribution)
|
||||
|
||||
#### For Developer ID Distribution:
|
||||
```bash
|
||||
# Sign the app
|
||||
codesign --deep --force --verify --verbose \
|
||||
--sign "Developer ID Application: Your Name (TEAM_ID)" \
|
||||
dist/HereIAm.app
|
||||
|
||||
# Verify signing
|
||||
codesign --verify --verbose dist/HereIAm.app
|
||||
spctl --assess --verbose dist/HereIAm.app
|
||||
```
|
||||
|
||||
#### For Mac App Store:
|
||||
```bash
|
||||
# Sign for App Store
|
||||
codesign --deep --force --verify --verbose \
|
||||
--sign "3rd Party Mac Developer Application: Your Name (TEAM_ID)" \
|
||||
dist/HereIAm.app
|
||||
|
||||
# Create installer package
|
||||
productbuild --component dist/HereIAm.app /Applications \
|
||||
--sign "3rd Party Mac Developer Installer: Your Name (TEAM_ID)" \
|
||||
HereIAm.pkg
|
||||
```
|
||||
|
||||
### Notarization (Required for Gatekeeper)
|
||||
|
||||
#### Setup (one-time):
|
||||
```bash
|
||||
# Store credentials
|
||||
xcrun notarytool store-credentials "AC_PASSWORD" \
|
||||
--apple-id "your-email@example.com" \
|
||||
--team-id "YOUR_TEAM_ID" \
|
||||
--password "app-specific-password"
|
||||
```
|
||||
|
||||
#### Notarize DMG:
|
||||
```bash
|
||||
# Submit for notarization
|
||||
xcrun notarytool submit dist/HereIAm-1.0.0.dmg \
|
||||
--keychain-profile "AC_PASSWORD" --wait
|
||||
|
||||
# Staple the notarization
|
||||
xcrun stapler staple dist/HereIAm-1.0.0.dmg
|
||||
```
|
||||
|
||||
### Distribution Options
|
||||
|
||||
#### 1. Direct Download
|
||||
- Upload DMG to your website
|
||||
- Users download and install manually
|
||||
- Requires code signing for smooth installation
|
||||
|
||||
#### 2. Mac App Store
|
||||
- Most secure distribution
|
||||
- Apple handles updates
|
||||
- Requires App Store review process
|
||||
- Need to modify app for sandboxing requirements
|
||||
|
||||
#### 3. Package Managers
|
||||
```bash
|
||||
# Homebrew Cask (example)
|
||||
brew install --cask hereiam
|
||||
```
|
||||
|
||||
### Testing the Build
|
||||
|
||||
#### Local Testing:
|
||||
```bash
|
||||
# Test the app bundle
|
||||
open dist/HereIAm.app
|
||||
|
||||
# Test the DMG
|
||||
open dist/HereIAm-1.0.0.dmg
|
||||
```
|
||||
|
||||
#### Distribution Testing:
|
||||
1. Test on clean macOS system
|
||||
2. Verify all dependencies are bundled
|
||||
3. Check accessibility permissions prompt
|
||||
4. Test menu bar functionality
|
||||
5. Verify icon changes with system theme
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Common Build Issues:
|
||||
|
||||
**PyInstaller import errors:**
|
||||
```bash
|
||||
# Add missing imports to HereIAm.spec hiddenimports
|
||||
hiddenimports=[
|
||||
'PyQt5.QtCore',
|
||||
'PyQt5.QtGui',
|
||||
'PyQt5.QtWidgets',
|
||||
'pyautogui',
|
||||
'missing_module_name',
|
||||
]
|
||||
```
|
||||
|
||||
**Missing assets:**
|
||||
- Verify all `.icns` files are in `assets/`
|
||||
- Check file permissions
|
||||
- Ensure assets are copied in spec file
|
||||
|
||||
**Large bundle size:**
|
||||
- Review excludes in spec file
|
||||
- Remove unused dependencies
|
||||
- Use UPX compression (enabled by default)
|
||||
|
||||
**Gatekeeper issues:**
|
||||
- Code sign the application
|
||||
- Notarize with Apple
|
||||
- Test with: `spctl --assess --verbose dist/HereIAm.app`
|
||||
|
||||
#### Performance Optimization:
|
||||
|
||||
**Reduce bundle size:**
|
||||
```python
|
||||
# In HereIAm.spec, add to excludes:
|
||||
excludes=[
|
||||
'tkinter', 'unittest', 'email', 'http', 'urllib', 'xml',
|
||||
'pydoc', 'doctest', 'argparse', 'difflib', 'inspect',
|
||||
'calendar', 'pprint', 'pdb', 'bdb', 'cmd', 'pstats',
|
||||
]
|
||||
```
|
||||
|
||||
**Faster startup:**
|
||||
- Minimize imports in main.py
|
||||
- Use lazy imports where possible
|
||||
- Optimize icon loading
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
#### GitHub Actions Example:
|
||||
```yaml
|
||||
name: Build macOS App
|
||||
|
||||
on: [push, release]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-build.txt
|
||||
- name: Build app
|
||||
run: ./build_app.sh
|
||||
- name: Create DMG
|
||||
run: ./create_dmg.sh
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: HereIAm-macOS
|
||||
path: dist/HereIAm-*.dmg
|
||||
```
|
||||
|
||||
### Version Management
|
||||
|
||||
Update version in `src/__init__.py`:
|
||||
```python
|
||||
__version__ = "1.1.0" # Update this for new releases
|
||||
```
|
||||
|
||||
### Support and Maintenance
|
||||
|
||||
#### User Support:
|
||||
- Provide clear installation instructions
|
||||
- Document accessibility permission requirements
|
||||
- Include troubleshooting in README
|
||||
- Offer support email (jerico@tekop.net)
|
||||
|
||||
#### Updates:
|
||||
- Use semantic versioning (1.0.0 → 1.0.1 → 1.1.0)
|
||||
- Test on multiple macOS versions
|
||||
- Consider auto-update mechanism for future versions
|
||||
129
HereIAm.spec
Normal file
129
HereIAm.spec
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Get base directory
|
||||
base_dir = Path.cwd()
|
||||
|
||||
# Add src directory to path for imports
|
||||
src_path = base_dir / 'src'
|
||||
sys.path.insert(0, str(src_path))
|
||||
|
||||
# Import version info
|
||||
try:
|
||||
from __init__ import __version__, __app_name__
|
||||
except ImportError:
|
||||
__version__ = "1.0.0"
|
||||
__app_name__ = "HereIAm"
|
||||
|
||||
# Define paths
|
||||
src_dir = base_dir / "src"
|
||||
assets_dir = base_dir / "assets"
|
||||
dist_dir = base_dir / "dist"
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
[str(src_dir / 'main.py')],
|
||||
pathex=[str(src_dir)],
|
||||
binaries=[],
|
||||
datas=[
|
||||
(str(assets_dir / '*.icns'), 'assets'),
|
||||
(str(assets_dir), 'assets'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'PyQt5.QtCore',
|
||||
'PyQt5.QtGui',
|
||||
'PyQt5.QtWidgets',
|
||||
'pyautogui',
|
||||
'objc',
|
||||
'objc._objc',
|
||||
'Foundation',
|
||||
'Cocoa',
|
||||
'AppKit',
|
||||
'Quartz',
|
||||
'CoreFoundation',
|
||||
'xml',
|
||||
'xml.etree',
|
||||
'xml.etree.ElementTree',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[
|
||||
'tkinter',
|
||||
'unittest',
|
||||
'email',
|
||||
'http',
|
||||
'pydoc',
|
||||
],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
# Remove unnecessary modules to reduce size
|
||||
a.binaries = [x for x in a.binaries if not x[0].startswith('tk')]
|
||||
a.binaries = [x for x in a.binaries if not x[0].startswith('tcl')]
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name=__app_name__,
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, # No console window
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=str(assets_dir / 'Enabled.icns'),
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name=__app_name__,
|
||||
)
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name=f'{__app_name__}.app',
|
||||
icon=str(assets_dir / 'Enabled.icns'),
|
||||
bundle_identifier=f'net.tekop.{__app_name__.lower()}',
|
||||
version=__version__,
|
||||
info_plist={
|
||||
'CFBundleName': __app_name__,
|
||||
'CFBundleDisplayName': __app_name__,
|
||||
'CFBundleGetInfoString': f'{__app_name__} - Mouse Movement Monitor',
|
||||
'CFBundleIdentifier': f'net.tekop.{__app_name__.lower()}',
|
||||
'CFBundleVersion': __version__,
|
||||
'CFBundleShortVersionString': __version__,
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'NSAppleScriptEnabled': False,
|
||||
'NSHighResolutionCapable': True,
|
||||
'LSUIElement': True, # This makes it a menu bar only app (no dock icon)
|
||||
'LSMinimumSystemVersion': '10.14.0',
|
||||
'NSHumanReadableCopyright': f'Copyright © 2025 Jerico Thomas. All rights reserved.',
|
||||
'CFBundleDocumentTypes': [],
|
||||
'NSRequiresAquaSystemAppearance': False, # Support dark mode
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
# Accessibility permissions
|
||||
'NSAppleEventsUsageDescription': 'HereIAm needs to move the mouse cursor to prevent system sleep.',
|
||||
'NSSystemAdministrationUsageDescription': 'HereIAm needs accessibility access to monitor and move the mouse cursor.',
|
||||
},
|
||||
)
|
||||
67
Makefile
Normal file
67
Makefile
Normal file
@@ -0,0 +1,67 @@
|
||||
# HereIAm Makefile
|
||||
# Simplified build commands
|
||||
|
||||
.PHONY: help setup build dmg test clean dev install
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "HereIAm Build System"
|
||||
@echo "===================="
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " setup - Install dependencies"
|
||||
@echo " dev - Run in development mode"
|
||||
@echo " build - Build .app bundle"
|
||||
@echo " dmg - Create distribution DMG"
|
||||
@echo " test - Test the built app"
|
||||
@echo " install - Install build dependencies"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " all - Build and create DMG"
|
||||
@echo ""
|
||||
|
||||
# Setup development environment
|
||||
setup:
|
||||
@echo "🔧 Setting up development environment..."
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install --upgrade pip
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
@echo "✅ Setup complete! Run 'source .venv/bin/activate' to activate."
|
||||
|
||||
# Install build dependencies
|
||||
install:
|
||||
@echo "📦 Installing build dependencies..."
|
||||
pip install -r requirements-build.txt
|
||||
|
||||
# Run in development mode
|
||||
dev:
|
||||
@echo "🚀 Running HereIAm in development mode..."
|
||||
python src/main.py
|
||||
|
||||
# Build the app
|
||||
build:
|
||||
@echo "🔨 Building HereIAm.app..."
|
||||
./build_app.sh
|
||||
|
||||
# Create DMG
|
||||
dmg: build
|
||||
@echo "📦 Creating distribution DMG..."
|
||||
./create_dmg.sh
|
||||
|
||||
# Test the built app
|
||||
test:
|
||||
@echo "🧪 Testing HereIAm.app..."
|
||||
./test_app.sh
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "🧹 Cleaning build artifacts..."
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf __pycache__/
|
||||
find . -name "*.pyc" -delete
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
@echo "✅ Clean complete!"
|
||||
|
||||
# Build everything
|
||||
all: clean build dmg test
|
||||
@echo "🎉 Build complete! Check dist/ for your application."
|
||||
240
README.md
Normal file
240
README.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# HereIAm - Mouse Movement Monitor
|
||||
|
||||
## Overview
|
||||
HereIAm is a sophisticated macOS application that runs discretely in the system menu bar. It intelligently monitors mouse activity and automatically moves the cursor to prevent system inactivity when needed. The application features a clean PyQt5-based interface with adaptive theming and comprehensive logging.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
- **Smart Mouse Monitoring**: Tracks mouse position and automatically moves cursor after a configurable period of inactivity
|
||||
- **System Menu Bar Integration**: Runs as a native macOS menu bar application with context menu controls
|
||||
- **Adaptive Icon Theming**: Automatically adjusts icons based on system dark/light mode
|
||||
- **Thread-Safe Operation**: Mouse monitoring runs in a separate thread for responsive UI
|
||||
|
||||
### User Interface
|
||||
- **Right-click Context Menu**: Easy enable/disable controls with visual status indicators
|
||||
- **Double-click Toggle**: Quick enable/disable by double-clicking the menu bar icon
|
||||
- **About Dialog**: Displays application information and version details
|
||||
- **Status Display**: Real-time status indication in the context menu
|
||||
|
||||
### Advanced Features
|
||||
- **Comprehensive Logging**: Detailed logging with configurable levels (DEBUG/INFO)
|
||||
- **Error Handling**: Robust error handling and recovery mechanisms
|
||||
- **Configuration Management**: Centralized configuration with easy customization
|
||||
- **System Integration**: Proper macOS system tray integration with fail-safes
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- macOS (tested on macOS 10.14+)
|
||||
- Python 3.8 or higher
|
||||
- System with menu bar/system tray support
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd HereIAm
|
||||
```
|
||||
|
||||
2. **Create a virtual environment (recommended):**
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **Install as package (optional):**
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Application
|
||||
|
||||
**From source:**
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
**If installed as package:**
|
||||
```bash
|
||||
hereiam
|
||||
```
|
||||
|
||||
### Using the Application
|
||||
|
||||
1. **Launch**: The application will appear in your menu bar with an icon
|
||||
2. **Enable/Disable**: Right-click the icon to access the context menu
|
||||
3. **Quick Toggle**: Double-click the icon for instant enable/disable
|
||||
4. **Status Monitoring**: The icon changes to indicate current state:
|
||||
- ✅ **Enabled**: Green icon when mouse monitoring is active
|
||||
- ❌ **Disabled**: Gray icon (theme-aware) when monitoring is off
|
||||
|
||||
### Menu Options
|
||||
- **Status**: Shows current monitoring state
|
||||
- **Enable HereIAm**: Starts mouse movement monitoring
|
||||
- **Disable HereIAm**: Stops mouse movement monitoring
|
||||
- **About HereIAm**: Application information and version
|
||||
- **Quit**: Cleanly exits the application
|
||||
|
||||
## Configuration
|
||||
|
||||
### Main Settings (`src/config.py`)
|
||||
```python
|
||||
WAITTIME = 240 # Wait time in seconds before moving mouse (default: 4 minutes)
|
||||
MovePx = 10 # Pixels to move mouse (default: 10px)
|
||||
StartEnabled = True # Start with monitoring enabled (default: True)
|
||||
DEBUG = False # Enable debug logging (default: False)
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
- **Log Location**: `logs/hereiam.log`
|
||||
- **Log Rotation**: Automatic (when DEBUG mode is enabled)
|
||||
- **Log Levels**: INFO (default) or DEBUG (when DEBUG=True)
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Runtime Dependencies
|
||||
- **PyQt5** (≥5.15.0): GUI framework and system tray integration
|
||||
- **pyautogui** (≥0.9.54): Mouse movement and position detection
|
||||
- **pyobjc-framework-Cocoa** (≥9.0): macOS system integration
|
||||
- **pyobjc-framework-Quartz** (≥9.0): macOS graphics and display APIs
|
||||
|
||||
### Development Dependencies (Optional)
|
||||
- **pytest** (≥7.0.0): Testing framework
|
||||
- **black** (≥22.0.0): Code formatting
|
||||
- **flake8** (≥4.0.0): Code linting
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
HereIAm/
|
||||
├── src/
|
||||
│ ├── main.py # Application entry point and main GUI logic
|
||||
│ ├── mouse_mover.py # Mouse monitoring and movement logic
|
||||
│ ├── config.py # Configuration and logging setup
|
||||
│ └── utils.py # Utility functions and safety features
|
||||
├── 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)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── setup.py # Package installation configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Architecture
|
||||
- **Main Application** (`main.py`): PyQt5-based GUI with system tray integration
|
||||
- **Mouse Monitor** (`mouse_mover.py`): Threaded mouse position tracking and automatic movement
|
||||
- **Configuration** (`config.py`): Centralized settings and logging configuration
|
||||
- **Utilities** (`utils.py`): Safety features and error handling for mouse operations
|
||||
|
||||
### How It Works
|
||||
1. **Monitoring**: The application continuously tracks mouse position in a background thread
|
||||
2. **Timer Management**: A countdown timer resets whenever mouse movement is detected
|
||||
3. **Automatic Movement**: When the timer expires, the mouse is moved slightly and returned to position
|
||||
4. **User Feedback**: The menu bar icon and status reflect the current monitoring state
|
||||
|
||||
### Safety Features
|
||||
- **Bounds Checking**: Mouse movements are constrained to screen boundaries
|
||||
- **Error Recovery**: Comprehensive exception handling prevents crashes
|
||||
- **Graceful Shutdown**: Clean thread termination and resource cleanup
|
||||
- **Fail-Safe Integration**: PyAutoGUI safety features configured appropriately
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Application doesn't appear in menu bar:**
|
||||
- Ensure system tray is available on your system
|
||||
- Check that PyQt5 is properly installed
|
||||
- Verify you have necessary permissions for GUI applications
|
||||
|
||||
**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
|
||||
|
||||
**Icons not displaying:**
|
||||
- Verify all `.icns` files are present in the `assets/` directory
|
||||
- Check file permissions on the assets folder
|
||||
- Enable DEBUG mode in config.py for detailed icon loading logs
|
||||
|
||||
### Debugging
|
||||
1. Enable debug mode in `src/config.py`: `DEBUG = True`
|
||||
2. Run from terminal to see console output
|
||||
3. Check `logs/hereiam.log` for detailed operation logs
|
||||
4. Use Activity Monitor to verify the application is running
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
```bash
|
||||
# Setup development environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run in development mode
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### Building for Distribution
|
||||
|
||||
#### Create macOS .app Bundle
|
||||
```bash
|
||||
# Install build dependencies
|
||||
pip install -r requirements-build.txt
|
||||
|
||||
# Build the application
|
||||
./build_app.sh
|
||||
```
|
||||
|
||||
This creates `dist/HereIAm.app` - a standalone application bundle.
|
||||
|
||||
#### Create Distribution DMG
|
||||
```bash
|
||||
# Create installer DMG
|
||||
./create_dmg.sh
|
||||
```
|
||||
|
||||
This creates `dist/HereIAm-1.0.0.dmg` ready for distribution.
|
||||
|
||||
#### Full Distribution Workflow
|
||||
```bash
|
||||
# Complete build and package process
|
||||
./build_app.sh && ./create_dmg.sh
|
||||
```
|
||||
|
||||
See [DISTRIBUTION.md](DISTRIBUTION.md) for detailed building, code signing, and distribution instructions.
|
||||
|
||||
### Creating a Standalone App (macOS)
|
||||
```bash
|
||||
# Install PyInstaller
|
||||
pip install pyinstaller
|
||||
|
||||
# Create app bundle
|
||||
pyinstaller --windowed --onefile --add-data "assets:assets" --icon="assets/Enabled.icns" src/main.py
|
||||
```
|
||||
|
||||
## Author & Support
|
||||
|
||||
**Author:** Jerico Thomas
|
||||
**Email:** jerico@tekop.net
|
||||
**Version:** 1.0.0
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||
|
||||
---
|
||||
|
||||
*HereIAm - Keeping your Mac awake, one mouse movement at a time.*
|
||||
BIN
assets/Disabled-Dark.icns
Normal file
BIN
assets/Disabled-Dark.icns
Normal file
Binary file not shown.
BIN
assets/Disabled-Light.icns
Normal file
BIN
assets/Disabled-Light.icns
Normal file
Binary file not shown.
BIN
assets/Enabled.icns
Normal file
BIN
assets/Enabled.icns
Normal file
Binary file not shown.
115
build_app.sh
Executable file
115
build_app.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
|
||||
# HereIAm Build Script
|
||||
# This script builds the macOS .app bundle for distribution
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🚀 Building HereIAm.app for macOS..."
|
||||
echo "======================================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Check if virtual environment exists and activate it
|
||||
if [ -d ".venv" ]; then
|
||||
echo -e "${BLUE}📦 Activating virtual environment...${NC}"
|
||||
source .venv/bin/activate
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ No virtual environment found. Using system Python.${NC}"
|
||||
fi
|
||||
|
||||
# Check Python version
|
||||
echo -e "${BLUE}🐍 Checking Python version...${NC}"
|
||||
python_version=$(python3 --version 2>&1)
|
||||
echo "Using: $python_version"
|
||||
|
||||
# Install/upgrade build dependencies
|
||||
echo -e "${BLUE}📚 Installing build dependencies...${NC}"
|
||||
pip install --upgrade pip
|
||||
pip install pyinstaller
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Clean previous builds
|
||||
echo -e "${BLUE}🧹 Cleaning previous builds...${NC}"
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf __pycache__/
|
||||
find . -name "*.pyc" -delete
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# Verify assets exist
|
||||
echo -e "${BLUE}🎨 Verifying assets...${NC}"
|
||||
if [ ! -f "assets/Enabled.icns" ]; then
|
||||
echo -e "${RED}❌ Error: assets/Enabled.icns not found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "assets/Disabled-Light.icns" ]; then
|
||||
echo -e "${RED}❌ Error: assets/Disabled-Light.icns not found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "assets/Disabled-Dark.icns" ]; then
|
||||
echo -e "${RED}❌ Error: assets/Disabled-Dark.icns not found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ All assets found${NC}"
|
||||
|
||||
# Build the application
|
||||
echo -e "${BLUE}🔨 Building application with PyInstaller...${NC}"
|
||||
pyinstaller --clean HereIAm.spec
|
||||
|
||||
# Check if build was successful
|
||||
if [ -d "dist/HereIAm.app" ]; then
|
||||
echo -e "${GREEN}✅ Build successful!${NC}"
|
||||
|
||||
# Get app size
|
||||
app_size=$(du -sh "dist/HereIAm.app" | cut -f1)
|
||||
echo -e "${GREEN}📦 App size: $app_size${NC}"
|
||||
|
||||
# Verify the app structure
|
||||
echo -e "${BLUE}🔍 Verifying app structure...${NC}"
|
||||
if [ -f "dist/HereIAm.app/Contents/MacOS/HereIAm" ]; then
|
||||
echo -e "${GREEN}✅ Executable found${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Executable not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "dist/HereIAm.app/Contents/Resources/assets" ]; then
|
||||
echo -e "${GREEN}✅ Assets bundled${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Assets not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Optional: Test if the app can be launched (basic check)
|
||||
echo -e "${BLUE}🧪 Testing app launch (will quit immediately)...${NC}"
|
||||
timeout 5s dist/HereIAm.app/Contents/MacOS/HereIAm || true
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 Build completed successfully!${NC}"
|
||||
echo -e "${GREEN}📱 Your app is ready at: dist/HereIAm.app${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📝 Next steps:${NC}"
|
||||
echo "1. Test the app: open dist/HereIAm.app"
|
||||
echo "2. For distribution, consider code signing:"
|
||||
echo " codesign --deep --force --verify --verbose --sign \"Developer ID Application: Your Name\" dist/HereIAm.app"
|
||||
echo "3. Create a DMG for distribution:"
|
||||
echo " ./create_dmg.sh"
|
||||
echo ""
|
||||
|
||||
else
|
||||
echo -e "${RED}❌ Build failed! Check the output above for errors.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
100
create_dmg.sh
Executable file
100
create_dmg.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
# HereIAm DMG Creation Script
|
||||
# Creates a distributable DMG file with the app
|
||||
|
||||
set -e
|
||||
|
||||
echo "📦 Creating DMG for HereIAm..."
|
||||
echo "=============================="
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Configuration
|
||||
APP_NAME="HereIAm"
|
||||
VERSION="1.0.0"
|
||||
DMG_NAME="${APP_NAME}-${VERSION}"
|
||||
APP_PATH="dist/${APP_NAME}.app"
|
||||
DMG_DIR="dmg_temp"
|
||||
FINAL_DMG="dist/${DMG_NAME}.dmg"
|
||||
|
||||
# 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 to build the app."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up any existing DMG files
|
||||
echo -e "${BLUE}🧹 Cleaning up previous DMG files...${NC}"
|
||||
rm -f "$FINAL_DMG"
|
||||
rm -rf "$DMG_DIR"
|
||||
|
||||
# Create temporary DMG directory
|
||||
echo -e "${BLUE}📁 Creating DMG structure...${NC}"
|
||||
mkdir -p "$DMG_DIR"
|
||||
|
||||
# Copy app to DMG directory
|
||||
echo -e "${BLUE}📋 Copying app to DMG...${NC}"
|
||||
cp -R "$APP_PATH" "$DMG_DIR/"
|
||||
|
||||
# Create Applications symlink for easy installation
|
||||
echo -e "${BLUE}🔗 Creating Applications symlink...${NC}"
|
||||
ln -sf /Applications "$DMG_DIR/Applications"
|
||||
|
||||
# Create a README for the DMG
|
||||
cat > "$DMG_DIR/README.txt" << EOF
|
||||
HereIAm - Mouse Movement Monitor for macOS
|
||||
Version $VERSION
|
||||
|
||||
Installation:
|
||||
1. Drag HereIAm.app to the Applications folder
|
||||
2. Launch HereIAm from Applications or Spotlight
|
||||
3. Grant accessibility permissions when prompted
|
||||
|
||||
The app will appear in your menu bar. Right-click to access controls.
|
||||
|
||||
For support: jerico@tekop.net
|
||||
EOF
|
||||
|
||||
# Calculate size needed for DMG
|
||||
echo -e "${BLUE}📏 Calculating DMG size...${NC}"
|
||||
SIZE=$(du -sk "$DMG_DIR" | cut -f1)
|
||||
SIZE=$((SIZE + 1000)) # Add some padding
|
||||
|
||||
# Create the DMG
|
||||
echo -e "${BLUE}💿 Creating DMG file...${NC}"
|
||||
hdiutil create -srcfolder "$DMG_DIR" -volname "$APP_NAME" -fs HFS+ \
|
||||
-fsargs "-c c=64,a=16,e=16" -format UDZO -imagekey zlib-level=9 "$FINAL_DMG"
|
||||
|
||||
# Clean up
|
||||
echo -e "${BLUE}🧹 Cleaning up temporary files...${NC}"
|
||||
rm -rf "$DMG_DIR"
|
||||
|
||||
# Verify final DMG
|
||||
if [ -f "$FINAL_DMG" ]; then
|
||||
dmg_size=$(du -sh "$FINAL_DMG" | cut -f1)
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 DMG created successfully!${NC}"
|
||||
echo -e "${GREEN}📦 File: $FINAL_DMG${NC}"
|
||||
echo -e "${GREEN}📏 Size: $dmg_size${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📝 Next steps for distribution:${NC}"
|
||||
echo "1. Test the DMG: open \"$FINAL_DMG\""
|
||||
echo "2. For wider distribution, consider notarization:"
|
||||
echo " xcrun notarytool submit \"$FINAL_DMG\" --keychain-profile \"AC_PASSWORD\""
|
||||
echo "3. Upload to your website or distribution platform"
|
||||
echo ""
|
||||
else
|
||||
echo -e "${RED}❌ DMG creation failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
4
requirements-build.txt
Normal file
4
requirements-build.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Build requirements - additional packages needed for creating the .app bundle
|
||||
pyinstaller>=5.13.0
|
||||
setuptools>=68.0.0
|
||||
wheel>=0.41.0
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Core dependencies
|
||||
pyautogui>=0.9.54
|
||||
PyQt5>=5.15.0
|
||||
|
||||
# macOS specific dependencies
|
||||
pyobjc-framework-Cocoa>=9.0
|
||||
pyobjc-framework-Quartz>=9.0
|
||||
50
setup.py
Normal file
50
setup.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
|
||||
# Read README for long description
|
||||
def read_readme():
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
setup(
|
||||
name='HereIAm',
|
||||
version='1.0.0',
|
||||
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',
|
||||
url='https://github.com/your-username/hereiam', # Update with your repo URL
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'assets': ['*.icns', '*.png'],
|
||||
},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'pyautogui>=0.9.54',
|
||||
'PyQt5>=5.15.0',
|
||||
'pyobjc-framework-Cocoa>=9.0',
|
||||
'pyobjc-framework-Quartz>=9.0',
|
||||
],
|
||||
python_requires='>=3.8',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'hereiam=src.main:main',
|
||||
],
|
||||
'gui_scripts': [
|
||||
'hereiam-gui=src.main:main',
|
||||
],
|
||||
},
|
||||
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",
|
||||
"Topic :: Utilities",
|
||||
],
|
||||
)
|
||||
10
src/__init__.py
Normal file
10
src/__init__.py
Normal 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
43
src/config.py
Normal 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
216
src/main.py
Normal 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
0
src/menu_bar.py
Normal file
94
src/mouse_mover.py
Normal file
94
src/mouse_mover.py
Normal 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
40
src/utils.py
Normal 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
|
||||
138
test_app.sh
Executable file
138
test_app.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for HereIAm.app
|
||||
# Verifies the built application works correctly
|
||||
|
||||
set -e
|
||||
|
||||
echo "🧪 Testing HereIAm.app..."
|
||||
echo "========================="
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📱 Testing app structure...${NC}"
|
||||
|
||||
# Test executable exists
|
||||
if [ -f "$APP_PATH/Contents/MacOS/HereIAm" ]; then
|
||||
echo -e "${GREEN}✅ Executable found${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Executable missing${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test Info.plist exists
|
||||
if [ -f "$APP_PATH/Contents/Info.plist" ]; then
|
||||
echo -e "${GREEN}✅ Info.plist found${NC}"
|
||||
|
||||
# Check bundle identifier
|
||||
bundle_id=$(plutil -extract CFBundleIdentifier raw "$APP_PATH/Contents/Info.plist" 2>/dev/null || echo "")
|
||||
if [ "$bundle_id" = "net.tekop.hereiam" ]; then
|
||||
echo -e "${GREEN}✅ Bundle identifier correct${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Bundle identifier: $bundle_id${NC}"
|
||||
fi
|
||||
|
||||
# Check version
|
||||
version=$(plutil -extract CFBundleVersion raw "$APP_PATH/Contents/Info.plist" 2>/dev/null || echo "")
|
||||
echo -e "${BLUE}📋 Version: $version${NC}"
|
||||
|
||||
else
|
||||
echo -e "${RED}❌ Info.plist missing${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test assets are bundled
|
||||
if [ -d "$APP_PATH/Contents/Resources/assets" ]; then
|
||||
echo -e "${GREEN}✅ Assets directory found${NC}"
|
||||
|
||||
# Check for required icons
|
||||
for icon in "Enabled.icns" "Disabled-Light.icns" "Disabled-Dark.icns"; do
|
||||
if [ -f "$APP_PATH/Contents/Resources/assets/$icon" ]; then
|
||||
echo -e "${GREEN}✅ $icon found${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ $icon missing${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo -e "${RED}❌ Assets directory missing${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test code signing (if signed)
|
||||
echo -e "${BLUE}🔍 Checking code signature...${NC}"
|
||||
if codesign -dv "$APP_PATH" 2>/dev/null; then
|
||||
echo -e "${GREEN}✅ App is code signed${NC}"
|
||||
|
||||
# Verify signature
|
||||
if codesign --verify --verbose "$APP_PATH" 2>/dev/null; then
|
||||
echo -e "${GREEN}✅ Signature is valid${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Signature verification failed${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ App is not code signed${NC}"
|
||||
echo -e "${YELLOW} (Users may see security warnings)${NC}"
|
||||
fi
|
||||
|
||||
# Test Gatekeeper assessment (if signed)
|
||||
echo -e "${BLUE}🛡️ Checking Gatekeeper assessment...${NC}"
|
||||
if spctl --assess --verbose "$APP_PATH" 2>/dev/null; then
|
||||
echo -e "${GREEN}✅ Gatekeeper will allow this app${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Gatekeeper may block this app${NC}"
|
||||
echo -e "${YELLOW} (Code signing and notarization recommended for distribution)${NC}"
|
||||
fi
|
||||
|
||||
# Get app size
|
||||
app_size=$(du -sh "$APP_PATH" | cut -f1)
|
||||
echo -e "${BLUE}📦 App bundle size: $app_size${NC}"
|
||||
|
||||
# Basic launch test (background)
|
||||
echo -e "${BLUE}🚀 Testing app launch...${NC}"
|
||||
"$APP_PATH/Contents/MacOS/HereIAm" &
|
||||
APP_PID=$!
|
||||
|
||||
# Wait a moment for launch
|
||||
sleep 3
|
||||
|
||||
# Check if process is running
|
||||
if kill -0 $APP_PID 2>/dev/null; then
|
||||
echo -e "${GREEN}✅ App launched successfully${NC}"
|
||||
|
||||
# Clean shutdown
|
||||
kill $APP_PID 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Force kill if still running
|
||||
kill -9 $APP_PID 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}✅ App shutdown cleanly${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ App failed to launch or crashed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 All tests passed!${NC}"
|
||||
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 "3. For distribution: code sign and notarize"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user