124 lines
3.7 KiB
Swift
124 lines
3.7 KiB
Swift
import Carbon
|
|
import Foundation
|
|
|
|
final class InputSourceManager {
|
|
private struct Source {
|
|
let inputSource: TISInputSource
|
|
let id: String
|
|
}
|
|
|
|
private var orderedSources: [Source] = []
|
|
|
|
var currentDisplayName: String {
|
|
guard let source = currentSource() else {
|
|
return "-"
|
|
}
|
|
return property(source, key: kTISPropertyLocalizedName)
|
|
?? property(source, key: kTISPropertyInputSourceID)
|
|
?? "-"
|
|
}
|
|
|
|
var currentShortName: String {
|
|
let name = currentDisplayName
|
|
if let first = name.split(whereSeparator: { $0 == " " || $0 == "-" }).first,
|
|
!first.isEmpty {
|
|
return String(first).uppercased()
|
|
}
|
|
return name
|
|
}
|
|
|
|
func refresh() {
|
|
orderedSources = enabledKeyboardSourcesInUserOrder()
|
|
}
|
|
|
|
@discardableResult
|
|
func selectNextInputSource() -> Bool {
|
|
let sources = orderedSources
|
|
guard sources.count > 1 else {
|
|
return false
|
|
}
|
|
|
|
let currentID = currentSource().flatMap {
|
|
property($0, key: kTISPropertyInputSourceID)
|
|
}
|
|
|
|
let currentIndex = currentID.flatMap { id in
|
|
sources.firstIndex { $0.id == id }
|
|
}
|
|
let nextIndex = ((currentIndex ?? -1) + 1) % sources.count
|
|
return TISSelectInputSource(sources[nextIndex].inputSource) == noErr
|
|
}
|
|
|
|
private func enabledKeyboardSourcesInUserOrder() -> [Source] {
|
|
let allSources = TISCreateInputSourceList(nil, false)
|
|
.takeRetainedValue() as NSArray as! [TISInputSource]
|
|
|
|
let sources = allSources.compactMap { inputSource -> Source? in
|
|
guard isEnabled(inputSource),
|
|
property(inputSource, key: kTISPropertyInputSourceCategory)
|
|
== (kTISCategoryKeyboardInputSource as String),
|
|
let id = property(inputSource, key: kTISPropertyInputSourceID) else {
|
|
return nil
|
|
}
|
|
|
|
return Source(inputSource: inputSource, id: id)
|
|
}
|
|
|
|
var byID: [String: Source] = [:]
|
|
for source in sources {
|
|
byID[source.id] = source
|
|
}
|
|
|
|
let configuredIDs = configuredInputSourceIDs()
|
|
var usedIDs: Set<String> = []
|
|
var ordered: [Source] = []
|
|
|
|
for id in configuredIDs {
|
|
if let source = byID[id], usedIDs.insert(id).inserted {
|
|
ordered.append(source)
|
|
}
|
|
}
|
|
|
|
ordered.append(contentsOf: sources.filter {
|
|
usedIDs.insert($0.id).inserted
|
|
})
|
|
return ordered
|
|
}
|
|
|
|
private func currentSource() -> TISInputSource? {
|
|
TISCopyCurrentKeyboardInputSource()?.takeRetainedValue()
|
|
}
|
|
|
|
private func configuredInputSourceIDs() -> [String] {
|
|
guard let entries = UserDefaults(suiteName: "com.apple.HIToolbox")?
|
|
.array(forKey: "AppleEnabledInputSources") as? [[String: Any]] else {
|
|
return []
|
|
}
|
|
|
|
return entries.compactMap { $0["InputSourceID"] as? String }
|
|
}
|
|
|
|
private func isEnabled(_ inputSource: TISInputSource) -> Bool {
|
|
guard let pointer = TISGetInputSourceProperty(
|
|
inputSource,
|
|
kTISPropertyInputSourceIsEnabled
|
|
) else {
|
|
return false
|
|
}
|
|
|
|
let value = Unmanaged<CFBoolean>.fromOpaque(pointer).takeUnretainedValue()
|
|
return CFBooleanGetValue(value)
|
|
}
|
|
|
|
private func property(
|
|
_ inputSource: TISInputSource,
|
|
key: CFString
|
|
) -> String? {
|
|
guard let pointer = TISGetInputSourceProperty(inputSource, key) else {
|
|
return nil
|
|
}
|
|
|
|
return Unmanaged<CFString>.fromOpaque(pointer)
|
|
.takeUnretainedValue() as String
|
|
}
|
|
}
|