189 lines
6.1 KiB
Swift
189 lines
6.1 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 = ""
|
|
private static let wakeModifiers: NSEvent.ModifierFlags = [
|
|
.command,
|
|
.option,
|
|
.control,
|
|
.shift,
|
|
]
|
|
|
|
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()
|
|
}
|
|
|
|
override func handle(_ event: NSEvent!, client sender: Any!) -> Bool {
|
|
let isWakeEvent = event.type == .keyDown
|
|
&& event.keyCode == UInt16(kVK_F20)
|
|
&& event.modifierFlags.contains(Self.wakeModifiers)
|
|
if isWakeEvent {
|
|
inputLog.notice("wake event consumed")
|
|
}
|
|
return isWakeEvent
|
|
}
|
|
|
|
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) {}
|