Files
wisper-key/Sources/WhisperKeyInput/main.swift
T

173 lines
5.6 KiB
Swift

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) {}