Initial commit: HereIAm mouse movement monitor

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

148
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
assets/Disabled-Light.icns Normal file

Binary file not shown.

BIN
assets/Enabled.icns Normal file

Binary file not shown.

115
build_app.sh Executable file
View 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
View 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
View 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
View 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
View 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
View File

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

43
src/config.py Normal file
View File

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

216
src/main.py Normal file
View File

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

0
src/menu_bar.py Normal file
View File

94
src/mouse_mover.py Normal file
View File

@@ -0,0 +1,94 @@
# filepath: src/mouse_mover.py
import time
import threading
import pyautogui
from config import WAITTIME, MovePx, get_logger
class MouseMover:
def __init__(self):
self.logger = get_logger(__name__)
self.countdown = WAITTIME
self.mouse_movement_enabled = False
self.xold = 0
self.yold = 0
self.thread = None
self.running = False
self.logger.debug("MouseMover initialized")
def _move_mouse(self):
"""Move mouse slightly and return to original position"""
try:
pyautogui.moveRel(0, MovePx, duration=0.5) # Faster movement
pyautogui.moveRel(0, -MovePx, duration=0.5)
self.logger.debug(f"Mouse moved {MovePx}px vertically and back")
except Exception as e:
self.logger.error(f"Failed to move mouse: {e}")
def start(self):
"""Start the mouse movement monitoring in a separate thread"""
if not self.running:
self.running = True
self.mouse_movement_enabled = True
self.thread = threading.Thread(target=self._monitor_mouse, daemon=True)
self.thread.start()
self.logger.info("Mouse movement monitoring started")
else:
self.logger.warning("Mouse movement monitoring already running")
def stop(self):
"""Stop the mouse movement monitoring"""
if self.running:
self.logger.info("Stopping mouse movement monitoring...")
self.running = False
self.mouse_movement_enabled = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2) # Increased timeout
if self.thread.is_alive():
self.logger.warning("Mouse monitoring thread did not stop gracefully")
else:
self.logger.debug("Mouse monitoring thread stopped successfully")
self.logger.info("Mouse movement monitoring stopped")
def _monitor_mouse(self):
"""Main monitoring loop that runs in separate thread"""
self.logger.debug("Mouse monitoring thread started")
while self.running and self.mouse_movement_enabled:
try:
x, y = pyautogui.position()
# Check if timer has expired
if self.countdown <= 0:
self._move_mouse()
self.logger.info(f"Timer expired ({WAITTIME}s), mouse moved automatically")
self.countdown = WAITTIME
continue
# Check if mouse has moved
if x == self.xold and y == self.yold:
# Mouse hasn't moved, count down
self.logger.debug(f"Mouse idle, countdown: {self.countdown}s")
time.sleep(1)
self.countdown -= 1
else:
# Mouse moved, reset timer
self.logger.debug(f"Mouse moved to ({x}, {y}), timer reset")
self.countdown = WAITTIME
self.xold = x
self.yold = y
except Exception as e:
self.logger.error(f"Error in mouse monitoring: {e}")
break
self.logger.debug("Mouse monitoring thread ended")
def get_status(self):
"""Get current status information"""
return {
'running': self.running,
'enabled': self.mouse_movement_enabled,
'countdown': self.countdown,
'thread_alive': self.thread.is_alive() if self.thread else False
}

40
src/utils.py Normal file
View File

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

138
test_app.sh Executable file
View 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 ""