Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 28 additions & 29 deletions BUILD_VERSIONS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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.
14 changes: 14 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
),
Comment on lines +31 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the addition of the ClickItLite target, it's important to also add a corresponding test target to ensure its functionality is covered by tests. Currently, only the ClickIt (Pro) target has a test target.

To maintain code quality and prevent regressions in the Lite version, please consider adding a new test target for ClickItLite. For example:

.testTarget(
    name: "ClickItLiteTests",
    dependencies: ["ClickItLite"],
    path: "Tests/ClickItLiteTests" // You may need to create this directory
)

.testTarget(
name: "ClickItTests",
dependencies: ["ClickIt"],
Expand Down
8 changes: 1 addition & 7 deletions Sources/ClickIt/Lite/ClickItLiteApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 26 additions & 6 deletions Sources/ClickIt/Lite/SimpleHotkeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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?()
}
Expand Down
32 changes: 29 additions & 3 deletions Sources/ClickIt/Lite/SimplePermissionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/ClickIt/Lite/SimplifiedMainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 16 additions & 13 deletions build_app_unified.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
Loading