Skip to content

Commit 398935d

Browse files
jsonifyclaude
andauthored
Fix accessibility permission not updating in app (#14)
* Fix accessibility permission detection in ClickIt Lite The app was not recognizing when accessibility permissions were granted because it checked immediately after opening System Settings, before the user had a chance to grant permission. Now the app automatically detects when it becomes active (e.g., user returning from System Settings) and re-checks permissions at that time, properly updating the UI. Changes: - Added NSApplication.didBecomeActiveNotification observer to monitor app activation - Re-check permissions automatically when app becomes active - Removed premature permission check in requestAccessibilityPermission() - Added proper cleanup in deinit to remove observer Fixes: Permission banner now disappears and clicking is enabled after granting permission * Refactor build system and fix emergency stop hotkeys This commit addresses two major issues: 1. **Refactor build system to use separate SPM targets** - Created separate ClickIt and ClickItLite targets in Package.swift - Each target excludes the other's entry point file - Both ClickItApp.swift and ClickItLiteApp.swift now have @main permanently enabled - Updated build_app_unified.sh to build the correct SPM target - No more file modification during builds - git working directory stays clean - Updated BUILD_VERSIONS_GUIDE.md to reflect new approach 2. **Fix ESC emergency stop and add SPACEBAR support** - Added local event monitor in addition to global monitor - Global monitor catches events when app is inactive - Local monitor catches events when app is active - Added SPACEBAR as secondary emergency stop key - Updated UI to show "Press ESC or SPACEBAR to emergency stop" Benefits: - Source files no longer modified during builds (fixes git status issues) - Emergency stop now works reliably in all scenarios - Users have two keys for emergency stop (ESC and SPACEBAR) - Cleaner separation between Pro and Lite versions Files changed: - Package.swift: Added ClickItLite target with proper exclusions - build_app_unified.sh: Updated to build correct SPM target - Sources/ClickIt/Lite/ClickItLiteApp.swift: Uncommented @main - Sources/ClickIt/Lite/SimpleHotkeyManager.swift: Added local monitor and SPACEBAR support - Sources/ClickIt/Lite/SimplifiedMainView.swift: Updated UI text - BUILD_VERSIONS_GUIDE.md: Updated documentation * Fix SPM target paths for ClickIt and ClickItLite SPM expects targets to either be in Sources/TargetName/ or have an explicit path specified. Since both ClickIt and ClickItLite share Sources/ClickIt/, we need to specify path: "Sources/ClickIt" for both targets. This fixes the build error: 'Source files for target ClickItLite should be located under Sources/ClickItLite' * Fix overlapping sources by separating target paths The previous approach had both targets using the same path which caused overlapping sources. Now: - ClickIt target uses Sources/ClickIt and excludes the entire Lite directory - ClickItLite target uses Sources/ClickIt/Lite exclusively This eliminates the overlapping sources error while keeping files organized. --------- Co-authored-by: Claude <[email protected]>
1 parent 4e8038a commit 398935d

File tree

7 files changed

+116
-60
lines changed

7 files changed

+116
-60
lines changed

BUILD_VERSIONS_GUIDE.md

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ fastlane launch_lite
3232

3333
## How It Works
3434

35-
### 1. Toggle Script
35+
### 1. Separate SPM Targets
3636

37-
The `toggle_version.sh` script automatically switches between Pro and Lite by:
38-
- Commenting/uncommenting `@main` in `ClickItApp.swift` (Pro)
39-
- Uncommenting/commenting `@main` in `ClickItLiteApp.swift` (Lite)
37+
The build system now uses **separate Swift Package Manager targets** for Pro and Lite:
38+
- **ClickIt** target builds from `ClickItApp.swift` (Pro entry point)
39+
- **ClickItLite** target builds from `ClickItLiteApp.swift` (Lite entry point)
40+
- Both entry points have `@main` permanently enabled
41+
- No file modification needed - each target excludes the other's entry point
42+
43+
This eliminates the need to modify source files during builds, keeping your git working directory clean.
4044

4145
### 2. Build Script
4246

@@ -92,18 +96,21 @@ New lanes have been added:
9296
- **Features**: 7 source files, single window, core features only
9397
- **Output**: `dist/ClickIt Lite.app`
9498

95-
## Manual Switching (Advanced)
96-
97-
If you need to manually switch without using the build system:
99+
## Package.swift Configuration
98100

99-
```bash
100-
# Switch to Lite
101-
./toggle_version.sh lite
101+
The `Package.swift` defines two separate executable products:
102102

103-
# Switch to Pro
104-
./toggle_version.sh pro
103+
```swift
104+
products: [
105+
.executable(name: "ClickIt", targets: ["ClickIt"]),
106+
.executable(name: "ClickItLite", targets: ["ClickItLite"])
107+
]
105108
```
106109

110+
Each target excludes the other's entry point:
111+
- **ClickIt** target excludes `Lite/ClickItLiteApp.swift`
112+
- **ClickItLite** target excludes `ClickItApp.swift`
113+
107114
## Build Output
108115

109116
After building, you'll find:
@@ -168,31 +175,23 @@ For automated builds, you can specify which version to build:
168175
169176
## Troubleshooting
170177
171-
### Build fails with "duplicate @main"
178+
### Build fails with target errors
172179
173-
The toggle script should handle this automatically, but if it fails:
180+
If you encounter build errors related to targets:
174181
175182
```bash
176-
# Manually fix by running:
177-
./toggle_version.sh pro # or lite
183+
# Clean all build artifacts
184+
fastlane clean
178185

179186
# Then rebuild
180-
fastlane build_debug
187+
fastlane build_debug # or build_lite_debug
181188
```
182189

183190
### Wrong version being built
184191

185-
Check which `@main` is active:
186-
187-
```bash
188-
# Check ClickItApp.swift (Pro)
189-
grep "@main" Sources/ClickIt/ClickItApp.swift
190-
191-
# Check ClickItLiteApp.swift (Lite)
192-
grep "@main" Sources/ClickIt/Lite/ClickItLiteApp.swift
193-
```
194-
195-
Only ONE should be uncommented at a time.
192+
The build system automatically selects the correct SPM target based on the fastlane command:
193+
- `fastlane build_debug` / `fastlane launch` → builds **ClickIt** target (Pro)
194+
- `fastlane build_lite_debug` / `fastlane launch_lite` → builds **ClickItLite** target (Lite)
196195

197196
### Clean and rebuild
198197

@@ -214,4 +213,4 @@ fastlane build_lite_debug # or build_debug for Pro
214213

215214
---
216215

217-
**Note**: The toggle script runs automatically during the build process, so you don't need to manually run it when using fastlane.
216+
**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.

Package.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,29 @@ let package = Package(
1212
.executable(
1313
name: "ClickIt",
1414
targets: ["ClickIt"]
15+
),
16+
.executable(
17+
name: "ClickItLite",
18+
targets: ["ClickItLite"]
1519
)
1620
],
1721
dependencies: [],
1822
targets: [
23+
// ClickIt Pro - Full-featured version
1924
.executableTarget(
2025
name: "ClickIt",
2126
dependencies: [],
27+
path: "Sources/ClickIt",
28+
exclude: ["Lite"],
2229
resources: [.process("Resources")]
2330
),
31+
// ClickIt Lite - Simplified version
32+
.executableTarget(
33+
name: "ClickItLite",
34+
dependencies: [],
35+
path: "Sources/ClickIt/Lite",
36+
resources: []
37+
),
2438
.testTarget(
2539
name: "ClickItTests",
2640
dependencies: ["ClickIt"],

Sources/ClickIt/Lite/ClickItLiteApp.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@
44
//
55
// App entry point for ClickIt Lite - the simplified auto-clicker.
66
//
7-
// NOTE: @main is commented out by default. To use ClickIt Lite instead of the full version:
8-
// 1. Comment out @main in Sources/ClickIt/ClickItApp.swift
9-
// 2. Uncomment @main below
10-
// 3. Build normally
11-
//
12-
137
import SwiftUI
148

15-
// @main // Uncomment to use ClickIt Lite as the main app
9+
@main
1610
struct ClickItLiteApp: App {
1711

1812
var body: some Scene {

Sources/ClickIt/Lite/SimpleHotkeyManager.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
// SimpleHotkeyManager.swift
33
// ClickIt Lite
44
//
5-
// Simple hotkey manager - ESC key only for emergency stop.
5+
// Simple hotkey manager - ESC and SPACEBAR keys for emergency stop.
66
//
77

88
import Foundation
99
import AppKit
1010
import Carbon
1111

12-
/// Simple hotkey manager for ESC key emergency stop
12+
/// Simple hotkey manager for ESC and SPACEBAR emergency stop
1313
@MainActor
1414
final class SimpleHotkeyManager {
1515

1616
// MARK: - Properties
1717

1818
private var globalMonitor: Any?
19+
private var localMonitor: Any?
1920
private var onEmergencyStop: (() -> Void)?
2021

2122
// MARK: - Singleton
@@ -26,20 +27,30 @@ final class SimpleHotkeyManager {
2627

2728
// MARK: - Public Methods
2829

29-
/// Start monitoring for ESC key
30+
/// Start monitoring for ESC and SPACEBAR keys
3031
func startMonitoring(onEmergencyStop: @escaping () -> Void) {
3132
self.onEmergencyStop = onEmergencyStop
3233

33-
// Monitor ESC key globally
34+
// Monitor globally (when app is inactive)
3435
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
35-
// Check for ESC key
36-
if event.keyCode == kVK_Escape {
36+
if self?.isEmergencyStopKey(event) == true {
3737
// Dispatch to MainActor since global monitor runs on background thread
3838
Task { @MainActor in
3939
self?.handleEmergencyStop()
4040
}
4141
}
4242
}
43+
44+
// Monitor locally (when app is active)
45+
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
46+
if self?.isEmergencyStopKey(event) == true {
47+
// Already on MainActor
48+
self?.handleEmergencyStop()
49+
// Return nil to prevent the event from being dispatched further
50+
return nil
51+
}
52+
return event
53+
}
4354
}
4455

4556
/// Stop monitoring
@@ -48,11 +59,20 @@ final class SimpleHotkeyManager {
4859
NSEvent.removeMonitor(monitor)
4960
globalMonitor = nil
5061
}
62+
if let monitor = localMonitor {
63+
NSEvent.removeMonitor(monitor)
64+
localMonitor = nil
65+
}
5166
onEmergencyStop = nil
5267
}
5368

5469
// MARK: - Private Methods
5570

71+
/// Check if the event is an emergency stop key (ESC or SPACEBAR)
72+
private func isEmergencyStopKey(_ event: NSEvent) -> Bool {
73+
return event.keyCode == kVK_Escape || event.keyCode == kVK_Space
74+
}
75+
5676
private func handleEmergencyStop() {
5777
onEmergencyStop?()
5878
}

Sources/ClickIt/Lite/SimplePermissionManager.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,37 @@ final class SimplePermissionManager: ObservableObject {
1616

1717
@Published private(set) var hasAccessibilityPermission = false
1818

19+
// MARK: - Private Properties
20+
21+
private var appActivationObserver: NSObjectProtocol?
22+
1923
// MARK: - Singleton
2024

2125
static let shared = SimplePermissionManager()
2226

2327
private init() {
2428
checkPermissions()
29+
setupAppActivationMonitoring()
30+
}
31+
32+
deinit {
33+
if let observer = appActivationObserver {
34+
NotificationCenter.default.removeObserver(observer)
35+
}
36+
}
37+
38+
// MARK: - Private Methods
39+
40+
/// Monitor when app becomes active to re-check permissions
41+
private func setupAppActivationMonitoring() {
42+
appActivationObserver = NotificationCenter.default.addObserver(
43+
forName: NSApplication.didBecomeActiveNotification,
44+
object: nil,
45+
queue: .main
46+
) { [weak self] _ in
47+
// Re-check permissions when app becomes active (e.g., returning from System Settings)
48+
self?.checkPermissions()
49+
}
2550
}
2651

2752
// MARK: - Public Methods
@@ -33,12 +58,13 @@ final class SimplePermissionManager: ObservableObject {
3358

3459
/// Request accessibility permission (opens System Settings)
3560
func requestAccessibilityPermission() {
36-
// Request permission. This is a blocking call that shows the system prompt.
61+
// Request permission. This opens System Settings to the Accessibility pane.
3762
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
3863
AXIsProcessTrustedWithOptions(options as CFDictionary)
3964

40-
// After the user interacts with the dialog, check the permission status again
41-
checkPermissions()
65+
// Note: Don't check permissions immediately - they won't be granted yet.
66+
// The app activation monitor will automatically re-check when the user
67+
// returns from System Settings.
4268
}
4369

4470
/// Open System Settings to Privacy & Security > Accessibility

Sources/ClickIt/Lite/SimplifiedMainView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,11 @@ struct SimplifiedMainView: View {
178178
.fontWeight(.medium)
179179

180180
if !viewModel.isRunning {
181-
Text("Press ESC anytime to stop")
181+
Text("Press ESC or SPACEBAR anytime to stop")
182182
.font(.caption)
183183
.foregroundColor(.secondary)
184184
} else {
185-
Text("Press ESC to emergency stop")
185+
Text("Press ESC or SPACEBAR to emergency stop")
186186
.font(.caption)
187187
.foregroundColor(.red)
188188
}

build_app_unified.sh

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ BUILD_MODE="${1:-release}" # Default to release, allow override
88
BUILD_SYSTEM="${2:-auto}" # auto, spm, xcode
99
APP_VERSION="${3:-pro}" # pro or lite, default to pro
1010
DIST_DIR="dist"
11-
EXECUTABLE_NAME="ClickIt" # This is the binary name from Package.swift (never changes)
12-
APP_NAME="ClickIt" # This is the .app bundle name (changes for Lite)
1311
BUNDLE_ID="com.jsonify.clickit"
1412

15-
# Toggle between Pro and Lite versions if specified
13+
# Configure target and names based on version
1614
if [ "$APP_VERSION" = "lite" ]; then
1715
echo "🔄 Configuring for ClickIt Lite build..."
18-
./toggle_version.sh lite
19-
APP_NAME="ClickIt Lite" # Change only the app bundle name
16+
SPM_TARGET="ClickItLite" # SPM target name
17+
EXECUTABLE_NAME="ClickItLite" # Binary name from Package.swift
18+
APP_NAME="ClickIt Lite" # .app bundle display name
2019
BUNDLE_ID="com.jsonify.clickit.lite"
21-
elif [ "$APP_VERSION" = "pro" ]; then
20+
else
2221
echo "🔄 Configuring for ClickIt Pro build..."
23-
./toggle_version.sh pro
22+
SPM_TARGET="ClickIt" # SPM target name
23+
EXECUTABLE_NAME="ClickIt" # Binary name from Package.swift
24+
APP_NAME="ClickIt" # .app bundle display name
25+
BUNDLE_ID="com.jsonify.clickit"
2426
fi
2527
# Get version from Info.plist (synced with GitHub releases)
2628
get_version_from_plist() {
@@ -150,10 +152,10 @@ else
150152
# Detect available architectures (original SPM logic)
151153
echo "🔍 Detecting available architectures..."
152154
ARCH_LIST=()
153-
if swift build -c "$BUILD_MODE" --arch x86_64 --show-bin-path > /dev/null 2>&1; then
155+
if swift build -c "$BUILD_MODE" --arch x86_64 --product "$SPM_TARGET" --show-bin-path > /dev/null 2>&1; then
154156
ARCH_LIST+=("x86_64")
155157
fi
156-
if swift build -c "$BUILD_MODE" --arch arm64 --show-bin-path > /dev/null 2>&1; then
158+
if swift build -c "$BUILD_MODE" --arch arm64 --product "$SPM_TARGET" --show-bin-path > /dev/null 2>&1; then
157159
ARCH_LIST+=("arm64")
158160
fi
159161

@@ -163,18 +165,19 @@ else
163165
fi
164166

165167
echo "📱 Building for architectures: ${ARCH_LIST[*]}"
168+
echo "🎯 Building target: $SPM_TARGET"
166169

167170
# Build for each architecture
168171
BINARY_PATHS=()
169172
for arch in "${ARCH_LIST[@]}"; do
170-
echo "⚙️ Building for $arch..."
171-
if ! swift build -c "$BUILD_MODE" --arch "$arch"; then
173+
echo "⚙️ Building $SPM_TARGET for $arch..."
174+
if ! swift build -c "$BUILD_MODE" --arch "$arch" --product "$SPM_TARGET"; then
172175
echo "❌ Build failed for $arch"
173176
exit 1
174177
fi
175-
178+
176179
# Get the actual build path
177-
BUILD_PATH=$(swift build -c "$BUILD_MODE" --arch "$arch" --show-bin-path)
180+
BUILD_PATH=$(swift build -c "$BUILD_MODE" --arch "$arch" --product "$SPM_TARGET" --show-bin-path)
178181
BINARY_PATH="$BUILD_PATH/$EXECUTABLE_NAME" # Use executable name, not app name
179182

180183
if [ ! -f "$BINARY_PATH" ]; then

0 commit comments

Comments
 (0)