diff --git a/ClickIt/Info.plist b/ClickIt/Info.plist
index e6aeca3..55c61ac 100644
--- a/ClickIt/Info.plist
+++ b/ClickIt/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.5.3
+ 1.5.5
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
LSMinimumSystemVersion
diff --git a/Sources/ClickIt/Core/Click/ClickCoordinator.swift b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
index 1957426..1626258 100644
--- a/Sources/ClickIt/Core/Click/ClickCoordinator.swift
+++ b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
@@ -360,21 +360,32 @@ class ClickCoordinator: ObservableObject {
/// Simple automation step execution from working version
private func executeAutomationStep(configuration: AutomationConfiguration) async -> ClickResult {
print("ClickCoordinator: executeAutomationStep() - Simple working approach")
-
+
+ // Determine click location based on mode
+ let clickLocation: CGPoint
+ if configuration.useDynamicMouseTracking {
+ // Active target mode: get current mouse position
+ clickLocation = NSEvent.mouseLocation
+ print("ClickCoordinator: Active target mode - clicking at cursor position \(clickLocation)")
+ } else {
+ // Fixed location mode: use configured location
+ clickLocation = configuration.location
+ }
+
// Use the working performSingleClick method
let result = await performSingleClick(
configuration: ClickConfiguration(
type: configuration.clickType,
- location: configuration.location,
+ location: clickLocation,
targetPID: nil
)
)
-
+
// Update visual feedback if enabled
if configuration.showVisualFeedback {
- VisualFeedbackOverlay.shared.updateOverlay(at: configuration.location, isActive: true)
+ VisualFeedbackOverlay.shared.updateOverlay(at: clickLocation, isActive: true)
}
-
+
return result
}
diff --git a/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift b/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
index 487e118..f27c035 100644
--- a/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
+++ b/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
@@ -17,12 +17,21 @@ class HotkeyManager: ObservableObject {
@Published var emergencyStopActivated: Bool = false
// MARK: - Private Properties
-
+
private var globalEventMonitor: Any?
private var localEventMonitor: Any?
+ private var globalMouseMonitor: Any?
+ private var localMouseMonitor: Any?
private var lastHotkeyTime: TimeInterval = 0
private let hotkeyDebounceInterval: TimeInterval = 0.01 // 10ms debounce for ultra-fast emergency response
private var responseTimeTracker: EmergencyStopResponseTracker?
+
+ // MARK: - Mouse Click Handling
+
+ /// Callback invoked when left mouse button is clicked (for active target mode)
+ var onLeftMouseClick: (() -> Void)?
+ private var lastMouseClickTime: TimeInterval = 0
+ private let mouseClickDebounceInterval: TimeInterval = 0.1 // 100ms debounce for mouse clicks
// MARK: - Initialization
@@ -36,6 +45,7 @@ class HotkeyManager: ObservableObject {
func cleanup() {
unregisterGlobalHotkey()
+ unregisterMouseMonitor()
}
func registerGlobalHotkey(_ config: HotkeyConfiguration) -> Bool {
@@ -70,15 +80,49 @@ class HotkeyManager: ObservableObject {
NSEvent.removeMonitor(monitor)
globalEventMonitor = nil
}
-
+
if let monitor = localEventMonitor {
NSEvent.removeMonitor(monitor)
localEventMonitor = nil
}
-
+
isRegistered = false
print("HotkeyManager: Successfully unregistered hotkey monitoring")
}
+
+ /// Register global mouse click monitoring for active target mode
+ func registerMouseMonitor() {
+ // Unregister existing monitors first
+ unregisterMouseMonitor()
+
+ // Monitor left mouse clicks globally (when app is in background)
+ globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
+ self?.handleMouseClick(event)
+ }
+
+ // Monitor left mouse clicks locally (when app is in foreground)
+ localMouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
+ self?.handleMouseClick(event)
+ return event // Pass through the event
+ }
+
+ print("HotkeyManager: Successfully registered mouse click monitoring")
+ }
+
+ /// Unregister mouse click monitoring
+ func unregisterMouseMonitor() {
+ if let monitor = globalMouseMonitor {
+ NSEvent.removeMonitor(monitor)
+ globalMouseMonitor = nil
+ }
+
+ if let monitor = localMouseMonitor {
+ NSEvent.removeMonitor(monitor)
+ localMouseMonitor = nil
+ }
+
+ print("HotkeyManager: Successfully unregistered mouse click monitoring")
+ }
// MARK: - Private Methods
@@ -91,12 +135,12 @@ class HotkeyManager: ObservableObject {
private func handleMultiKeyEvent(_ event: NSEvent) {
let currentTime = CFAbsoluteTimeGetCurrent()
-
+
// Debounce emergency stop to prevent rapid fire (50ms for emergency response)
if currentTime - lastHotkeyTime < hotkeyDebounceInterval {
return
}
-
+
// Check if this event matches any emergency stop key
if let matchedConfig = matchEmergencyStopKey(event) {
print("HotkeyManager: Emergency stop key activated - \(matchedConfig.description)")
@@ -104,6 +148,23 @@ class HotkeyManager: ObservableObject {
handleEmergencyStop(triggeredBy: matchedConfig)
}
}
+
+ private func handleMouseClick(_ event: NSEvent) {
+ let currentTime = CFAbsoluteTimeGetCurrent()
+
+ // Debounce mouse clicks to prevent accidental double-clicks
+ if currentTime - lastMouseClickTime < mouseClickDebounceInterval {
+ return
+ }
+
+ lastMouseClickTime = currentTime
+ print("HotkeyManager: Left mouse click detected for active target mode")
+
+ // Invoke callback on main thread
+ Task { @MainActor in
+ self.onLeftMouseClick?()
+ }
+ }
private func matchEmergencyStopKey(_ event: NSEvent) -> HotkeyConfiguration? {
// Check all available emergency stop configurations
diff --git a/Sources/ClickIt/Core/Models/ClickSettings.swift b/Sources/ClickIt/Core/Models/ClickSettings.swift
index b6052a4..139ebe3 100644
--- a/Sources/ClickIt/Core/Models/ClickSettings.swift
+++ b/Sources/ClickIt/Core/Models/ClickSettings.swift
@@ -105,7 +105,14 @@ class ClickSettings: ObservableObject {
saveSettings()
}
}
-
+
+ /// Active target mode - clicks follow cursor position
+ @Published var isActiveTargetMode: Bool = false {
+ didSet {
+ saveSettings()
+ }
+ }
+
// MARK: - CPS Randomization Properties
/// Whether to enable CPS timing randomization
@@ -231,6 +238,7 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
showVisualFeedback: showVisualFeedback,
playSoundFeedback: playSoundFeedback,
+ isActiveTargetMode: isActiveTargetMode,
randomizeTiming: randomizeTiming,
timingVariancePercentage: timingVariancePercentage,
distributionPattern: distributionPattern,
@@ -268,6 +276,7 @@ class ClickSettings: ObservableObject {
stopOnError = settings.stopOnError
showVisualFeedback = settings.showVisualFeedback
playSoundFeedback = settings.playSoundFeedback
+ isActiveTargetMode = settings.isActiveTargetMode ?? false // Default for backward compatibility
randomizeTiming = settings.randomizeTiming
timingVariancePercentage = settings.timingVariancePercentage
distributionPattern = settings.distributionPattern
@@ -293,6 +302,7 @@ class ClickSettings: ObservableObject {
stopOnError = false
showVisualFeedback = true
playSoundFeedback = false
+ isActiveTargetMode = false
randomizeTiming = false
timingVariancePercentage = 0.1
distributionPattern = .normal
@@ -318,8 +328,8 @@ class ClickSettings: ObservableObject {
func createAutomationConfiguration() -> AutomationConfiguration {
let maxClicksValue = durationMode == .clickCount ? maxClicks : nil
let maxDurationValue = durationMode == .timeLimit ? durationSeconds : nil
-
- print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback)")
+
+ print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback), activeTargetMode: \(isActiveTargetMode)")
return AutomationConfiguration(
location: clickLocation,
@@ -331,6 +341,8 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(locationVariance),
+ useDynamicMouseTracking: isActiveTargetMode,
+ showVisualFeedback: showVisualFeedback,
cpsRandomizerConfig: createCPSRandomizerConfiguration()
)
}
@@ -353,13 +365,14 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
showVisualFeedback: showVisualFeedback,
playSoundFeedback: playSoundFeedback,
+ isActiveTargetMode: isActiveTargetMode,
randomizeTiming: randomizeTiming,
timingVariancePercentage: timingVariancePercentage,
distributionPattern: distributionPattern,
humannessLevel: humannessLevel,
schedulingMode: schedulingMode,
scheduledDateTime: scheduledDateTime,
- exportVersion: "1.1",
+ exportVersion: "1.2",
exportDate: Date(),
appVersion: AppConstants.appVersion
)
@@ -403,6 +416,7 @@ class ClickSettings: ObservableObject {
stopOnError = importData.stopOnError
showVisualFeedback = importData.showVisualFeedback
playSoundFeedback = importData.playSoundFeedback
+ isActiveTargetMode = importData.isActiveTargetMode ?? false // Default for backward compatibility
randomizeTiming = importData.randomizeTiming
timingVariancePercentage = importData.timingVariancePercentage
distributionPattern = importData.distributionPattern
@@ -527,6 +541,7 @@ private struct SettingsData: Codable {
let stopOnError: Bool
let showVisualFeedback: Bool
let playSoundFeedback: Bool
+ let isActiveTargetMode: Bool? // Optional for backward compatibility
let randomizeTiming: Bool
let timingVariancePercentage: Double
let distributionPattern: CPSRandomizer.DistributionPattern
@@ -550,6 +565,7 @@ struct SettingsExportData: Codable {
let stopOnError: Bool
let showVisualFeedback: Bool
let playSoundFeedback: Bool
+ let isActiveTargetMode: Bool? // Optional for backward compatibility
let randomizeTiming: Bool
let timingVariancePercentage: Double
let distributionPattern: CPSRandomizer.DistributionPattern
diff --git a/Sources/ClickIt/Services/CursorManager.swift b/Sources/ClickIt/Services/CursorManager.swift
new file mode 100644
index 0000000..d98b614
--- /dev/null
+++ b/Sources/ClickIt/Services/CursorManager.swift
@@ -0,0 +1,77 @@
+//
+// CursorManager.swift
+// ClickIt
+//
+// Manages custom cursor states for active target mode
+//
+
+import Cocoa
+import os.log
+
+/// Manages cursor appearance for different automation modes
+final class CursorManager {
+ static let shared = CursorManager()
+
+ private let logger = Logger(subsystem: "com.clickit.app", category: "CursorManager")
+ private var isTargetCursorActive = false
+ private var cursorUpdateTimer: Timer?
+
+ private init() {}
+
+ /// Shows the target/crosshair cursor for active target mode
+ func showTargetCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ if !self.isTargetCursorActive {
+ self.isTargetCursorActive = true
+
+ // Set the cursor immediately
+ NSCursor.crosshair.set()
+
+ // Keep re-setting the cursor on a timer to ensure it stays active
+ // This is needed because system can reset cursor on window focus changes
+ self.cursorUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
+ guard let self = self, self.isTargetCursorActive else { return }
+ NSCursor.crosshair.set()
+ }
+
+ self.logger.info("Target cursor activated with timer refresh")
+ }
+ }
+ }
+
+ /// Restores the normal system cursor
+ func restoreNormalCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ if self.isTargetCursorActive {
+ self.isTargetCursorActive = false
+
+ // Stop the cursor update timer
+ self.cursorUpdateTimer?.invalidate()
+ self.cursorUpdateTimer = nil
+
+ // Restore arrow cursor
+ NSCursor.arrow.set()
+
+ self.logger.info("Normal cursor restored")
+ }
+ }
+ }
+
+ /// Force restore cursor (useful for cleanup on app termination)
+ func forceRestoreNormalCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ self.isTargetCursorActive = false
+ self.cursorUpdateTimer?.invalidate()
+ self.cursorUpdateTimer = nil
+ NSCursor.arrow.set()
+
+ self.logger.info("Cursor forcefully restored")
+ }
+ }
+}
diff --git a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
index 035e845..cebb151 100644
--- a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
+++ b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
@@ -21,7 +21,12 @@ class ClickItViewModel: ObservableObject {
// Settings
@Published var clickSettings = ClickSettings()
-
+
+ // Expose settings for UI binding
+ var settings: ClickSettings {
+ clickSettings
+ }
+
// Configuration Properties
@Published var intervalHours = 0 {
didSet {
@@ -112,6 +117,9 @@ class ClickItViewModel: ObservableObject {
// MARK: - Dependencies
private let clickCoordinator = ClickCoordinator.shared
private let schedulingManager = SchedulingManager.shared
+
+ // MARK: - Active Target Mode State
+ private var isProcessingActiveTargetClick = false
// MARK: - Initialization
init() {
@@ -453,11 +461,11 @@ class ClickItViewModel: ObservableObject {
self?.updateStatistics()
}
.store(in: &cancellables)
-
+
// Monitor automation active state to sync UI state
clickCoordinator.$isActive.sink { [weak self] isActive in
guard let self = self else { return }
-
+
// Sync ViewModel state with ClickCoordinator state
if !isActive && (self.isRunning || self.isPaused) {
print("ClickItViewModel: Automation stopped externally (e.g., DELETE key), updating UI state")
@@ -469,13 +477,100 @@ class ClickItViewModel: ObservableObject {
}
}
.store(in: &cancellables)
+
+ // Monitor active target mode changes
+ clickSettings.$isActiveTargetMode.sink { [weak self] isEnabled in
+ guard let self = self else { return }
+ self.handleActiveTargetModeChange(isEnabled)
+ }
+ .store(in: &cancellables)
}
private func updateStatistics() {
// SIMPLE WORKING APPROACH: Use ClickCoordinator statistics directly
statistics = clickCoordinator.getSessionStatistics()
}
-
+
+ // MARK: - Active Target Mode Management
+
+ private func handleActiveTargetModeChange(_ isEnabled: Bool) {
+ print("ClickItViewModel: Active target mode changed to \(isEnabled)")
+
+ if isEnabled {
+ // Enable active target mode
+ CursorManager.shared.showTargetCursor()
+ setupMouseClickHandler()
+ } else {
+ // Disable active target mode
+ CursorManager.shared.restoreNormalCursor()
+ removeMouseClickHandler()
+ }
+ }
+
+ private func setupMouseClickHandler() {
+ // Set up the click handler for active target mode
+ HotkeyManager.shared.onLeftMouseClick = { [weak self] in
+ Task { @MainActor in
+ self?.handleActiveTargetClick()
+ }
+ }
+
+ // Register mouse monitoring
+ HotkeyManager.shared.registerMouseMonitor()
+ print("ClickItViewModel: Mouse click handler registered for active target mode")
+ }
+
+ private func removeMouseClickHandler() {
+ // Remove the click handler
+ HotkeyManager.shared.onLeftMouseClick = nil
+
+ // Unregister mouse monitoring
+ HotkeyManager.shared.unregisterMouseMonitor()
+ print("ClickItViewModel: Mouse click handler removed")
+ }
+
+ private func handleActiveTargetClick() {
+ // Prevent re-entrancy from rapid clicks or automated clicks
+ guard !isProcessingActiveTargetClick else {
+ print("ClickItViewModel: Ignoring click - already processing")
+ return
+ }
+
+ isProcessingActiveTargetClick = true
+ defer {
+ // Reset the flag after a short delay to allow next click
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
+ self?.isProcessingActiveTargetClick = false
+ }
+ }
+
+ print("ClickItViewModel: Active target click detected, isRunning: \(isRunning)")
+
+ // Toggle automation on/off with left click
+ if isRunning {
+ // Stop automation
+ stopAutomation()
+ print("ClickItViewModel: Stopped automation via active target click")
+ } else {
+ // Start automation - but only if other prerequisites are met
+ if canStartAutomation || clickSettings.isActiveTargetMode {
+ // In active target mode, capture the current mouse position
+ // This is just for validation - the actual clicking will use live position
+ if clickSettings.isActiveTargetMode {
+ let currentMousePosition = NSEvent.mouseLocation
+ targetPoint = currentMousePosition
+ clickSettings.clickLocation = currentMousePosition
+ print("ClickItViewModel: Captured mouse position for active target mode: \(currentMousePosition)")
+ }
+
+ startAutomation()
+ print("ClickItViewModel: Started automation via active target click")
+ } else {
+ print("ClickItViewModel: Cannot start automation - prerequisites not met")
+ }
+ }
+ }
+
// MARK: - Emergency Stop
/// Performs emergency stop using ClickCoordinator directly
diff --git a/Sources/ClickIt/UI/Views/QuickStartTab.swift b/Sources/ClickIt/UI/Views/QuickStartTab.swift
index 6bd1285..3380570 100644
--- a/Sources/ClickIt/UI/Views/QuickStartTab.swift
+++ b/Sources/ClickIt/UI/Views/QuickStartTab.swift
@@ -31,6 +31,9 @@ struct QuickStartTab: View {
// Target Point Selector (with Timer Support)
TargetPointSelectionCard(viewModel: viewModel)
+ // Active Target Mode Toggle
+ ActiveTargetModeCard()
+
// Inline Timing Controls
InlineTimingControls()
@@ -258,19 +261,19 @@ private struct CompactStatisticView: View {
let title: String
let value: String
let icon: String
-
+
var body: some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(.blue)
-
+
Text(value)
.font(.system(.caption, design: .monospaced))
.fontWeight(.semibold)
.lineLimit(1)
.minimumScaleFactor(0.8)
-
+
Text(title)
.font(.system(size: 9))
.foregroundColor(.secondary)
@@ -282,6 +285,47 @@ private struct CompactStatisticView: View {
}
}
+// Active Target Mode Card
+private struct ActiveTargetModeCard: View {
+ @EnvironmentObject private var viewModel: ClickItViewModel
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack {
+ Image(systemName: "scope")
+ .foregroundColor(.purple)
+ .font(.system(size: 14))
+
+ Toggle("Active Target Mode", isOn: $viewModel.clickSettings.isActiveTargetMode)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .toggleStyle(.switch)
+ }
+
+ if viewModel.clickSettings.isActiveTargetMode {
+ HStack(spacing: 6) {
+ Image(systemName: "info.circle")
+ .foregroundColor(.purple)
+ .font(.system(size: 10))
+
+ Text("Cursor becomes crosshair. Left-click to start/stop clicking at cursor position.")
+ .font(.system(size: 10))
+ .foregroundColor(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+
+ Spacer()
+ }
+ .padding(.top, 4)
+ }
+ }
+ .padding(12)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color(NSColor.textBackgroundColor))
+ )
+ }
+}
+
// MARK: - Preview
struct QuickStartTab_Previews: PreviewProvider {