diff --git a/BUILD_VERSIONS_GUIDE.md b/BUILD_VERSIONS_GUIDE.md index 4cf304d..4efc42c 100644 --- a/BUILD_VERSIONS_GUIDE.md +++ b/BUILD_VERSIONS_GUIDE.md @@ -32,11 +32,15 @@ fastlane launch_lite ## How It Works -### 1. Toggle Script +### 1. Separate SPM Targets -The `toggle_version.sh` script automatically switches between Pro and Lite by: -- Commenting/uncommenting `@main` in `ClickItApp.swift` (Pro) -- Uncommenting/commenting `@main` in `ClickItLiteApp.swift` (Lite) +The build system now uses **separate Swift Package Manager targets** for Pro and Lite: +- **ClickIt** target builds from `ClickItApp.swift` (Pro entry point) +- **ClickItLite** target builds from `ClickItLiteApp.swift` (Lite entry point) +- Both entry points have `@main` permanently enabled +- No file modification needed - each target excludes the other's entry point + +This eliminates the need to modify source files during builds, keeping your git working directory clean. ### 2. Build Script @@ -92,18 +96,21 @@ New lanes have been added: - **Features**: 7 source files, single window, core features only - **Output**: `dist/ClickIt Lite.app` -## Manual Switching (Advanced) - -If you need to manually switch without using the build system: +## Package.swift Configuration -```bash -# Switch to Lite -./toggle_version.sh lite +The `Package.swift` defines two separate executable products: -# Switch to Pro -./toggle_version.sh pro +```swift +products: [ + .executable(name: "ClickIt", targets: ["ClickIt"]), + .executable(name: "ClickItLite", targets: ["ClickItLite"]) +] ``` +Each target excludes the other's entry point: +- **ClickIt** target excludes `Lite/ClickItLiteApp.swift` +- **ClickItLite** target excludes `ClickItApp.swift` + ## Build Output After building, you'll find: @@ -168,31 +175,23 @@ For automated builds, you can specify which version to build: ## Troubleshooting -### Build fails with "duplicate @main" +### Build fails with target errors -The toggle script should handle this automatically, but if it fails: +If you encounter build errors related to targets: ```bash -# Manually fix by running: -./toggle_version.sh pro # or lite +# Clean all build artifacts +fastlane clean # Then rebuild -fastlane build_debug +fastlane build_debug # or build_lite_debug ``` ### Wrong version being built -Check which `@main` is active: - -```bash -# Check ClickItApp.swift (Pro) -grep "@main" Sources/ClickIt/ClickItApp.swift - -# Check ClickItLiteApp.swift (Lite) -grep "@main" Sources/ClickIt/Lite/ClickItLiteApp.swift -``` - -Only ONE should be uncommented at a time. +The build system automatically selects the correct SPM target based on the fastlane command: +- `fastlane build_debug` / `fastlane launch` → builds **ClickIt** target (Pro) +- `fastlane build_lite_debug` / `fastlane launch_lite` → builds **ClickItLite** target (Lite) ### Clean and rebuild @@ -214,4 +213,4 @@ fastlane build_lite_debug # or build_debug for Pro --- -**Note**: The toggle script runs automatically during the build process, so you don't need to manually run it when using fastlane. +**Note**: The build system automatically selects the correct SPM target based on which fastlane command you use. No manual file modification is required - source files remain unchanged during builds. diff --git a/Package.swift b/Package.swift index 9ba543a..1074575 100644 --- a/Package.swift +++ b/Package.swift @@ -12,15 +12,29 @@ let package = Package( .executable( name: "ClickIt", targets: ["ClickIt"] + ), + .executable( + name: "ClickItLite", + targets: ["ClickItLite"] ) ], dependencies: [], targets: [ + // ClickIt Pro - Full-featured version .executableTarget( name: "ClickIt", dependencies: [], + path: "Sources/ClickIt", + exclude: ["Lite"], resources: [.process("Resources")] ), + // ClickIt Lite - Simplified version + .executableTarget( + name: "ClickItLite", + dependencies: [], + path: "Sources/ClickIt/Lite", + resources: [] + ), .testTarget( name: "ClickItTests", dependencies: ["ClickIt"], diff --git a/Sources/ClickIt/Lite/ClickItLiteApp.swift b/Sources/ClickIt/Lite/ClickItLiteApp.swift index 782972a..feb32d4 100644 --- a/Sources/ClickIt/Lite/ClickItLiteApp.swift +++ b/Sources/ClickIt/Lite/ClickItLiteApp.swift @@ -4,15 +4,9 @@ // // App entry point for ClickIt Lite - the simplified auto-clicker. // -// NOTE: @main is commented out by default. To use ClickIt Lite instead of the full version: -// 1. Comment out @main in Sources/ClickIt/ClickItApp.swift -// 2. Uncomment @main below -// 3. Build normally -// - import SwiftUI -// @main // Uncomment to use ClickIt Lite as the main app +@main struct ClickItLiteApp: App { var body: some Scene { diff --git a/Sources/ClickIt/Lite/SimpleHotkeyManager.swift b/Sources/ClickIt/Lite/SimpleHotkeyManager.swift index f8f8403..0241290 100644 --- a/Sources/ClickIt/Lite/SimpleHotkeyManager.swift +++ b/Sources/ClickIt/Lite/SimpleHotkeyManager.swift @@ -2,20 +2,21 @@ // SimpleHotkeyManager.swift // ClickIt Lite // -// Simple hotkey manager - ESC key only for emergency stop. +// Simple hotkey manager - ESC and SPACEBAR keys for emergency stop. // import Foundation import AppKit import Carbon -/// Simple hotkey manager for ESC key emergency stop +/// Simple hotkey manager for ESC and SPACEBAR emergency stop @MainActor final class SimpleHotkeyManager { // MARK: - Properties private var globalMonitor: Any? + private var localMonitor: Any? private var onEmergencyStop: (() -> Void)? // MARK: - Singleton @@ -26,20 +27,30 @@ final class SimpleHotkeyManager { // MARK: - Public Methods - /// Start monitoring for ESC key + /// Start monitoring for ESC and SPACEBAR keys func startMonitoring(onEmergencyStop: @escaping () -> Void) { self.onEmergencyStop = onEmergencyStop - // Monitor ESC key globally + // Monitor globally (when app is inactive) globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in - // Check for ESC key - if event.keyCode == kVK_Escape { + if self?.isEmergencyStopKey(event) == true { // Dispatch to MainActor since global monitor runs on background thread Task { @MainActor in self?.handleEmergencyStop() } } } + + // Monitor locally (when app is active) + localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + if self?.isEmergencyStopKey(event) == true { + // Already on MainActor + self?.handleEmergencyStop() + // Return nil to prevent the event from being dispatched further + return nil + } + return event + } } /// Stop monitoring @@ -48,11 +59,20 @@ final class SimpleHotkeyManager { NSEvent.removeMonitor(monitor) globalMonitor = nil } + if let monitor = localMonitor { + NSEvent.removeMonitor(monitor) + localMonitor = nil + } onEmergencyStop = nil } // MARK: - Private Methods + /// Check if the event is an emergency stop key (ESC or SPACEBAR) + private func isEmergencyStopKey(_ event: NSEvent) -> Bool { + return event.keyCode == kVK_Escape || event.keyCode == kVK_Space + } + private func handleEmergencyStop() { onEmergencyStop?() } diff --git a/Sources/ClickIt/Lite/SimplePermissionManager.swift b/Sources/ClickIt/Lite/SimplePermissionManager.swift index f3180ba..fce20f3 100644 --- a/Sources/ClickIt/Lite/SimplePermissionManager.swift +++ b/Sources/ClickIt/Lite/SimplePermissionManager.swift @@ -16,12 +16,37 @@ final class SimplePermissionManager: ObservableObject { @Published private(set) var hasAccessibilityPermission = false + // MARK: - Private Properties + + private var appActivationObserver: NSObjectProtocol? + // MARK: - Singleton static let shared = SimplePermissionManager() private init() { checkPermissions() + setupAppActivationMonitoring() + } + + deinit { + if let observer = appActivationObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + // MARK: - Private Methods + + /// Monitor when app becomes active to re-check permissions + private func setupAppActivationMonitoring() { + appActivationObserver = NotificationCenter.default.addObserver( + forName: NSApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + // Re-check permissions when app becomes active (e.g., returning from System Settings) + self?.checkPermissions() + } } // MARK: - Public Methods @@ -33,12 +58,13 @@ final class SimplePermissionManager: ObservableObject { /// Request accessibility permission (opens System Settings) func requestAccessibilityPermission() { - // Request permission. This is a blocking call that shows the system prompt. + // Request permission. This opens System Settings to the Accessibility pane. let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] AXIsProcessTrustedWithOptions(options as CFDictionary) - // After the user interacts with the dialog, check the permission status again - checkPermissions() + // Note: Don't check permissions immediately - they won't be granted yet. + // The app activation monitor will automatically re-check when the user + // returns from System Settings. } /// Open System Settings to Privacy & Security > Accessibility diff --git a/Sources/ClickIt/Lite/SimplifiedMainView.swift b/Sources/ClickIt/Lite/SimplifiedMainView.swift index b960fbd..646281a 100644 --- a/Sources/ClickIt/Lite/SimplifiedMainView.swift +++ b/Sources/ClickIt/Lite/SimplifiedMainView.swift @@ -178,11 +178,11 @@ struct SimplifiedMainView: View { .fontWeight(.medium) if !viewModel.isRunning { - Text("Press ESC anytime to stop") + Text("Press ESC or SPACEBAR anytime to stop") .font(.caption) .foregroundColor(.secondary) } else { - Text("Press ESC to emergency stop") + Text("Press ESC or SPACEBAR to emergency stop") .font(.caption) .foregroundColor(.red) } diff --git a/build_app_unified.sh b/build_app_unified.sh index ffb788d..b11fb04 100755 --- a/build_app_unified.sh +++ b/build_app_unified.sh @@ -8,19 +8,21 @@ BUILD_MODE="${1:-release}" # Default to release, allow override BUILD_SYSTEM="${2:-auto}" # auto, spm, xcode APP_VERSION="${3:-pro}" # pro or lite, default to pro DIST_DIR="dist" -EXECUTABLE_NAME="ClickIt" # This is the binary name from Package.swift (never changes) -APP_NAME="ClickIt" # This is the .app bundle name (changes for Lite) BUNDLE_ID="com.jsonify.clickit" -# Toggle between Pro and Lite versions if specified +# Configure target and names based on version if [ "$APP_VERSION" = "lite" ]; then echo "🔄 Configuring for ClickIt Lite build..." - ./toggle_version.sh lite - APP_NAME="ClickIt Lite" # Change only the app bundle name + SPM_TARGET="ClickItLite" # SPM target name + EXECUTABLE_NAME="ClickItLite" # Binary name from Package.swift + APP_NAME="ClickIt Lite" # .app bundle display name BUNDLE_ID="com.jsonify.clickit.lite" -elif [ "$APP_VERSION" = "pro" ]; then +else echo "🔄 Configuring for ClickIt Pro build..." - ./toggle_version.sh pro + SPM_TARGET="ClickIt" # SPM target name + EXECUTABLE_NAME="ClickIt" # Binary name from Package.swift + APP_NAME="ClickIt" # .app bundle display name + BUNDLE_ID="com.jsonify.clickit" fi # Get version from Info.plist (synced with GitHub releases) get_version_from_plist() { @@ -150,10 +152,10 @@ else # Detect available architectures (original SPM logic) echo "🔍 Detecting available architectures..." ARCH_LIST=() - if swift build -c "$BUILD_MODE" --arch x86_64 --show-bin-path > /dev/null 2>&1; then + if swift build -c "$BUILD_MODE" --arch x86_64 --product "$SPM_TARGET" --show-bin-path > /dev/null 2>&1; then ARCH_LIST+=("x86_64") fi - if swift build -c "$BUILD_MODE" --arch arm64 --show-bin-path > /dev/null 2>&1; then + if swift build -c "$BUILD_MODE" --arch arm64 --product "$SPM_TARGET" --show-bin-path > /dev/null 2>&1; then ARCH_LIST+=("arm64") fi @@ -163,18 +165,19 @@ else fi echo "📱 Building for architectures: ${ARCH_LIST[*]}" + echo "🎯 Building target: $SPM_TARGET" # Build for each architecture BINARY_PATHS=() for arch in "${ARCH_LIST[@]}"; do - echo "⚙️ Building for $arch..." - if ! swift build -c "$BUILD_MODE" --arch "$arch"; then + echo "⚙️ Building $SPM_TARGET for $arch..." + if ! swift build -c "$BUILD_MODE" --arch "$arch" --product "$SPM_TARGET"; then echo "❌ Build failed for $arch" exit 1 fi - + # Get the actual build path - BUILD_PATH=$(swift build -c "$BUILD_MODE" --arch "$arch" --show-bin-path) + BUILD_PATH=$(swift build -c "$BUILD_MODE" --arch "$arch" --product "$SPM_TARGET" --show-bin-path) BINARY_PATH="$BUILD_PATH/$EXECUTABLE_NAME" # Use executable name, not app name if [ ! -f "$BINARY_PATH" ]; then