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
16 changes: 14 additions & 2 deletions MiddleDrag/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Application Lifecycle

func applicationDidFinishLaunching(_ notification: Notification) {
//Initialize Analytics
// Initialize Analytics first (sets up Sentry for crash reporting)
AnalyticsManager.shared.initialize()

Log.info("MiddleDrag starting...", category: .app)

// Hide dock icon (menu bar app only)
NSApp.setActivationPolicy(.accessory)

Expand All @@ -33,16 +35,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {

// Load preferences
preferences = PreferencesManager.shared.loadPreferences()
Log.info("Preferences loaded", category: .app)

// Configure and start multitouch manager
multitouchManager.updateConfiguration(preferences.gestureConfig)
multitouchManager.start()
Log.info("Multitouch manager started", category: .app)

// Set up menu bar UI
menuBarController = MenuBarController(
multitouchManager: multitouchManager,
preferences: preferences
)
Log.info("Menu bar controller initialized", category: .app)

// Set up notification observers
setupNotifications()
Expand All @@ -55,14 +60,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Check Accessibility permission AFTER UI is set up
// This way the menu bar icon appears even if permission is missing
if !AXIsProcessTrusted() {
Log.warning("Accessibility permission not granted", category: .app)
AnalyticsManager.shared.trackAccessibilityPermission(granted: false)
showAccessibilityAlert()
} else {
Log.info("Accessibility permission granted", category: .app)
AnalyticsManager.shared.trackAccessibilityPermission(granted: true)
}

// Final cleanup of any stray windows
closeAllWindows()

Log.info("MiddleDrag initialization complete", category: .app)
}

private func showAccessibilityAlert() {
Expand Down Expand Up @@ -103,7 +112,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func applicationWillTerminate(_ notification: Notification) {
//Track app termination
Log.info("MiddleDrag terminating", category: .app)

// Track app termination
AnalyticsManager.shared.trackTermination()

multitouchManager.stop()
Expand Down Expand Up @@ -142,6 +153,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
preferences = newPreferences
PreferencesManager.shared.savePreferences(preferences)
multitouchManager.updateConfiguration(preferences.gestureConfig)
Log.info("Preferences updated", category: .app)
}
}

Expand Down
67 changes: 32 additions & 35 deletions MiddleDrag/Managers/DeviceMonitor.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import Foundation
import CoreFoundation

// MARK: - Debug Logging (Debug builds only)
// MARK: - Debug Touch Counter (Debug builds only)

#if DEBUG
private let debugLogPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("middledrag_touch.log")
private var touchCount = 0

private func logToFile(_ message: String) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let line = "[\(timestamp)] \(message)\n"
if let data = line.data(using: .utf8) {
if FileManager.default.fileExists(atPath: debugLogPath.path) {
if let handle = try? FileHandle(forWritingTo: debugLogPath) {
handle.seekToEndOfFile()
handle.write(data)
handle.closeFile()
}
} else {
try? data.write(to: debugLogPath)
}
}
}
#else
@inline(__always) private func logToFile(_ message: String) {}
#endif

// MARK: - Global Callback Storage
Expand All @@ -36,8 +17,9 @@ private var gDeviceMonitor: DeviceMonitor?
private let deviceContactCallback: MTContactCallbackFunction = { device, touches, numTouches, timestamp, frame in
#if DEBUG
touchCount += 1
if touchCount <= 5 || touchCount % 100 == 0 {
logToFile("Touch callback #\(touchCount): \(numTouches) touches")
// Log sparingly to avoid performance impact
if touchCount <= 5 || touchCount % 500 == 0 {
Log.debug("Touch callback #\(touchCount): \(numTouches) touches", category: .device)
}
#endif

Expand Down Expand Up @@ -85,44 +67,57 @@ class DeviceMonitor {
func start() {
guard !isRunning else { return }

logToFile("DeviceMonitor.start() called")
Log.info("DeviceMonitor starting...", category: .device)

var deviceCount = 0
var registeredDevices: Set<UnsafeMutableRawPointer> = []

// Try to get all devices
if let deviceList = MTDeviceCreateList() {
let count = CFArrayGetCount(deviceList)
logToFile("Found \(count) multitouch device(s)")
Log.info("Found \(count) multitouch device(s)", category: .device)

for i in 0..<count {
let devicePtr = CFArrayGetValueAtIndex(deviceList, i)
if let dev = devicePtr {
let deviceRef = UnsafeMutableRawPointer(mutating: dev)
logToFile("Registering callback for device \(i): \(deviceRef)")
MTRegisterContactFrameCallback(deviceRef, deviceContactCallback)
MTDeviceStart(deviceRef, 0)
registeredDevices.insert(deviceRef)
deviceCount += 1

if device == nil {
device = deviceRef
}
}
}
} else {
logToFile("MTDeviceCreateList returned nil, trying default")
Log.warning("MTDeviceCreateList returned nil, trying default device", category: .device)
}

// Also try the default device
// Also try the default device if not already registered
if let defaultDevice = MultitouchFramework.shared.getDefaultDevice() {
logToFile("Also registering default device: \(defaultDevice)")
MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback)
MTDeviceStart(defaultDevice, 0)

if device == nil {
device = defaultDevice
if !registeredDevices.contains(defaultDevice) {
MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback)
MTDeviceStart(defaultDevice, 0)
registeredDevices.insert(defaultDevice)
deviceCount += 1

if device == nil {
device = defaultDevice
}
} else {
Log.debug("Default device already registered from device list", category: .device)
}
}

logToFile("Device registration complete")
if device == nil {
Log.error("No multitouch device found!", category: .device)
} else {
Log.info("DeviceMonitor started with \(deviceCount) device(s)", category: .device)
}

isRunning = true
logToFile("DeviceMonitor started successfully")
}

/// Stop monitoring
Expand All @@ -134,6 +129,8 @@ class DeviceMonitor {

self.device = nil
isRunning = false

Log.info("DeviceMonitor stopped", category: .device)
}

// MARK: - Internal
Expand Down
2 changes: 1 addition & 1 deletion MiddleDrag/Managers/MultitouchManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class MultitouchManager {
},
userInfo: refcon
) else {
print("⚠️ Could not create event tap")
Log.warning("Could not create event tap", category: .device)
return
}

Expand Down
99 changes: 94 additions & 5 deletions MiddleDrag/Utilities/AnalyticsManager.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,95 @@
import Foundation
import Cocoa
import Sentry
import os.log

// MARK: - Sentry Logger
/// A unified logger that writes to both os_log and Sentry breadcrumbs
/// Usage: Log.debug("message"), Log.info("message"), Log.warning("message"), Log.error("message"), Log.fatal("message")
enum Log {
private static let subsystem = Bundle.main.bundleIdentifier ?? "com.middledrag"

// OS Log categories
private static let gestureLog = OSLog(subsystem: subsystem, category: "gesture")
private static let deviceLog = OSLog(subsystem: subsystem, category: "device")
private static let analyticsLog = OSLog(subsystem: subsystem, category: "analytics")
private static let appLog = OSLog(subsystem: subsystem, category: "app")

enum Category: String {
case gesture
case device
case analytics
case app

var osLog: OSLog {
switch self {
case .gesture: return Log.gestureLog
case .device: return Log.deviceLog
case .analytics: return Log.analyticsLog
case .app: return Log.appLog
}
}
}

/// Debug level - only in debug builds, not sent to Sentry
static func debug(_ message: String, category: Category = .app) {
#if DEBUG
os_log(.debug, log: category.osLog, "%{public}@", message)
#endif
}

/// Info level - logged locally and as Sentry breadcrumb
static func info(_ message: String, category: Category = .app) {
os_log(.info, log: category.osLog, "%{public}@", message)
addBreadcrumb(message: message, category: category, level: .info)
}

/// Warning level - logged locally and as Sentry breadcrumb
static func warning(_ message: String, category: Category = .app) {
os_log(.error, log: category.osLog, "⚠️ %{public}@", message)
addBreadcrumb(message: message, category: category, level: .warning)
}

/// Error level - logged locally, sent as Sentry breadcrumb AND captured as event
static func error(_ message: String, category: Category = .app, error: Error? = nil) {
os_log(.fault, log: category.osLog, "❌ %{public}@", message)
addBreadcrumb(message: message, category: category, level: .error)

// Also capture as Sentry event for errors
if let error = error {
SentrySDK.capture(error: error) { scope in
scope.setContext(value: ["message": message], key: "log_context")
}
} else {
SentrySDK.capture(message: message) { scope in
scope.setLevel(.error)
scope.setTag(value: category.rawValue, key: "log_category")
}
}
}

/// Fatal level - for unrecoverable errors, always captured
static func fatal(_ message: String, category: Category = .app, error: Error? = nil) {
os_log(.fault, log: category.osLog, "💀 FATAL: %{public}@", message)

SentrySDK.capture(message: "FATAL: \(message)") { scope in
scope.setLevel(.fatal)
scope.setTag(value: category.rawValue, key: "log_category")
if let error = error {
scope.setContext(value: ["error": error.localizedDescription], key: "error_info")
}
}
}

private static func addBreadcrumb(message: String, category: Category, level: SentryLevel) {
let breadcrumb = Breadcrumb()
breadcrumb.category = category.rawValue
breadcrumb.message = message
breadcrumb.level = level
breadcrumb.timestamp = Date()
SentrySDK.addBreadcrumb(breadcrumb)
}
}

// MARK: - Analytics Manager
/// Centralized analytics for MiddleDrag using Sentry (crash reporting) and Simple Analytics (usage)
Expand Down Expand Up @@ -55,7 +144,7 @@ final class AnalyticsManager {
trackEvent(.appLaunched)

#if DEBUG
print("[Analytics] Initialized (Sentry + Simple Analytics)")
Log.debug("Initialized (Sentry + Simple Analytics)", category: .analytics)
#endif
}

Expand All @@ -69,7 +158,7 @@ final class AnalyticsManager {
// Skip if DSN not configured
guard isSentryConfigured else {
#if DEBUG
print("[Analytics] Sentry DSN not configured - skipping initialization")
Log.debug("Sentry DSN not configured - skipping initialization", category: .analytics)
#endif
return
}
Expand Down Expand Up @@ -146,7 +235,7 @@ final class AnalyticsManager {
URLSession.shared.dataTask(with: request) { _, _, error in
#if DEBUG
if let error = error {
print("[Analytics] Simple Analytics error: \(error.localizedDescription)")
Log.warning("Simple Analytics error: \(error.localizedDescription)", category: .analytics)
}
#endif
}.resume()
Expand All @@ -172,7 +261,7 @@ final class AnalyticsManager {
sendSimpleAnalyticsEvent(path: event.category, event: event.rawValue)

#if DEBUG
print("[Analytics] Event: \(event.rawValue) \(parameters)")
Log.debug("Event: \(event.rawValue) \(parameters)", category: .analytics)
#endif
}

Expand All @@ -199,7 +288,7 @@ final class AnalyticsManager {
}

#if DEBUG
print("[Analytics] Error: \(error.localizedDescription)")
Log.debug("Error tracked: \(error.localizedDescription)", category: .analytics)
#endif
}

Expand Down
7 changes: 4 additions & 3 deletions MiddleDrag/Utilities/LaunchAtLoginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@ class LaunchAtLoginManager {
do {
if enabled {
try SMAppService.mainApp.register()
Log.info("Launch at login enabled", category: .app)
} else {
try SMAppService.mainApp.unregister()
Log.info("Launch at login disabled", category: .app)
}
} catch {
print("Failed to configure launch at login: \(error)")
Log.error("Failed to configure launch at login: \(error.localizedDescription)", category: .app, error: error)
}
}

private func configureLaunchAtLoginLegacy(_ enabled: Bool) {
// For older macOS versions, we would use LSSharedFileList
// or SMLoginItemSetEnabled, but these are deprecated
// For simplicity, we'll just log a message
print("Launch at login configuration not available on macOS < 13.0")
Log.warning("Launch at login not available on macOS < 13.0", category: .app)
}

/// Check if launch at login is enabled
Expand Down
Loading