macos_handler.swift

  1import Cocoa
  2
  3class AppDelegate: NSObject, NSApplicationDelegate {
  4    var handled = false
  5    
  6    func applicationDidFinishLaunching(_ notification: Notification) {
  7        log("MatchaMail handler started")
  8        
  9        // Register for legacy Apple Events (GURL = 1196711500)
 10        NSAppleEventManager.shared().setEventHandler(
 11            self,
 12            andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
 13            forEventClass: AEEventClass(1196711500),
 14            andEventID: AEEventID(1196711500)
 15        )
 16        
 17        // Timeout
 18        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
 19            if !self.handled {
 20                self.log("No URL event received within 2s, terminating.")
 21                NSApp.terminate(nil)
 22            }
 23        }
 24    }
 25    
 26    // Modern URL handling
 27    func application(_ application: NSApplication, open urls: [URL]) {
 28        if let url = urls.first {
 29            log("Modern API received URL: \(url.absoluteString)")
 30            launchMatcha(with: url.absoluteString)
 31        }
 32    }
 33    
 34    // Legacy Apple Event handling
 35    @objc func handleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {
 36        if let urlString = event.paramDescriptor(forKeyword: AEKeyword(757935405))?.stringValue {
 37            log("Legacy API received URL: \(urlString)")
 38            launchMatcha(with: urlString)
 39        }
 40    }
 41    
 42    func launchMatcha(with url: String) {
 43        guard !handled else { return }
 44        handled = true
 45        
 46        let matchaPath = "{{MATCHA_PATH}}"
 47        log("Launching Matcha via .command file at \(matchaPath) with URL \(url)")
 48        
 49        // Use a .command file to open in the DEFAULT terminal
 50        let tempDir = NSTemporaryDirectory()
 51        let commandFileName = "matcha-mailto-\(UUID().uuidString).command"
 52        let commandFileUrl = URL(fileURLWithPath: tempDir).appendingPathComponent(commandFileName)
 53        
 54        // We use a bash script that opens matcha and then removes itself
 55        let scriptContent = """
 56        #!/bin/bash
 57        '\(matchaPath)' '\(url)'
 58        # Clean up this temporary script
 59        rm -- "$0"
 60        exit
 61        """
 62        
 63        do {
 64            try scriptContent.write(to: commandFileUrl, atomically: true, encoding: .utf8)
 65            
 66            // Make the file executable
 67            let attributes = [FileAttributeKey.posixPermissions: 0o755]
 68            try FileManager.default.setAttributes(attributes, ofItemAtPath: commandFileUrl.path)
 69            
 70            // Open the file with NSWorkspace. 
 71            // Since it's a .command file, macOS will open it in the default terminal.
 72            NSWorkspace.shared.open(commandFileUrl)
 73            log("Successfully requested macOS to open .command file")
 74            
 75        } catch {
 76            log("Failed to create/open .command file: \(error.localizedDescription)")
 77        }
 78        
 79        // Small delay to ensure launch
 80        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
 81            NSApp.terminate(nil)
 82        }
 83    }
 84    
 85    func log(_ message: String) {
 86        let logPath = "/tmp/matcha-handler.log"
 87        let timestamp = Date().description
 88        let line = "[\(timestamp)] \(message)\n"
 89        if let data = line.data(using: .utf8) {
 90            if let fileHandle = FileHandle(forWritingAtPath: logPath) {
 91                fileHandle.seekToEndOfFile()
 92                fileHandle.write(data)
 93                fileHandle.closeFile()
 94            } else {
 95                try? data.write(to: URL(fileURLWithPath: logPath))
 96            }
 97        }
 98        NSLog(message)
 99    }
100}
101
102let app = NSApplication.shared
103let delegate = AppDelegate()
104app.delegate = delegate
105app.run()