import AppKit import Carbon import InputMethodKit import OSLog private let inputLog = Logger( subsystem: "com.romanv.inputmethod.WhisperKey", category: "composition" ) private enum CompositionMessage { static let ping = Notification.Name("com.romanv.WhisperKeyInput.ping") static let ready = Notification.Name("com.romanv.WhisperKeyInput.ready") static let draft = Notification.Name("com.romanv.WhisperKeyInput.draft") static let commit = Notification.Name("com.romanv.WhisperKeyInput.commit") static let cancel = Notification.Name("com.romanv.WhisperKeyInput.cancel") static let textKey = "text" static let activeKey = "active" } private final class CompositionCoordinator: @unchecked Sendable { static let shared = CompositionCoordinator() weak var activeController: WhisperKeyInputController? { didSet { announceReady() } } private init() { let center = DistributedNotificationCenter.default() center.addObserver( self, selector: #selector(handlePing), name: CompositionMessage.ping, object: nil ) center.addObserver( self, selector: #selector(handleDraft), name: CompositionMessage.draft, object: nil ) center.addObserver( self, selector: #selector(handleCommit), name: CompositionMessage.commit, object: nil ) center.addObserver( self, selector: #selector(handleCancel), name: CompositionMessage.cancel, object: nil ) } @objc private func handlePing() { NSLog("WhisperKeyInput: ping") announceReady() } @objc private func handleDraft(_ notification: Notification) { guard let text = notification.userInfo?[CompositionMessage.textKey] as? String else { return } NSLog("WhisperKeyInput: draft %ld", (text as NSString).length) activeController?.setDraft(text) } @objc private func handleCommit(_ notification: Notification) { guard let text = notification.userInfo?[CompositionMessage.textKey] as? String else { return } NSLog("WhisperKeyInput: commit %ld", (text as NSString).length) activeController?.commit(text) } @objc private func handleCancel() { NSLog("WhisperKeyInput: cancel") activeController?.cancelDraft() } private func announceReady() { DistributedNotificationCenter.default().post( name: CompositionMessage.ready, object: nil, userInfo: [CompositionMessage.activeKey: activeController != nil] ) } } @objc(WhisperKeyInputController) final class WhisperKeyInputController: IMKInputController { private var draft = "" override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { super.init(server: server, delegate: delegate, client: inputClient) _ = CompositionCoordinator.shared NSLog("WhisperKeyInput: controller initialized") } override func activateServer(_ sender: Any!) { super.activateServer(sender) inputLog.notice( "controller activated for \(self.client().bundleIdentifier(), privacy: .public)" ) CompositionCoordinator.shared.activeController = self client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout.Russian") } override func deactivateServer(_ sender: Any!) { cancelDraft() if CompositionCoordinator.shared.activeController === self { CompositionCoordinator.shared.activeController = nil } super.deactivateServer(sender) } override func inputControllerWillClose() { if CompositionCoordinator.shared.activeController === self { CompositionCoordinator.shared.activeController = nil } super.inputControllerWillClose() } func setDraft(_ text: String) { inputLog.notice( "setting marked text for \(self.client().bundleIdentifier(), privacy: .public)" ) draft = text let length = (text as NSString).length client().setMarkedText( text, selectionRange: NSRange(location: length, length: 0), replacementRange: activeReplacementRange() ) } func commit(_ text: String) { inputLog.notice( "inserting final text for \(self.client().bundleIdentifier(), privacy: .public)" ) client().insertText(text, replacementRange: activeReplacementRange()) draft = "" } func cancelDraft() { guard !draft.isEmpty else { return } client().insertText("", replacementRange: activeReplacementRange()) draft = "" } private func activeReplacementRange() -> NSRange { let marked = client().markedRange() return marked.location == NSNotFound ? NSRange(location: NSNotFound, length: 0) : marked } } let app = NSApplication.shared app.setActivationPolicy(.prohibited) let connectionName = Bundle.main.object(forInfoDictionaryKey: "InputMethodConnectionName") as? String ?? "com.romanv.inputmethod.WhisperKey_Connection" let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.romanv.inputmethod.WhisperKey" guard let inputServer = IMKServer( name: connectionName, bundleIdentifier: bundleIdentifier ) else { fatalError("Не удалось запустить WhisperKey Input Method.") } _ = CompositionCoordinator.shared NSLog("WhisperKeyInput: server started") app.run() withExtendedLifetime(inputServer) {}