Skip to content

Commit 60acec9

Browse files
authored
Merge pull request #20 : Build with SwiftPM and avoid code signing
This PR makes the build reproducible which in turns enable integration in the homebrew repository. Merge pull request #20 from ratkins/master
2 parents 54e322f + aea1c1e commit 60acec9

17 files changed

+2772
-638
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

Package.resolved

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"object": {
3+
"pins": [
4+
{
5+
"package": "SwiftPM",
6+
"repositoryURL": "https://github.com/apple/swift-package-manager",
7+
"state": {
8+
"branch": null,
9+
"revision": "235aacc514cb81a6881364b0fedcb3dd083228f3",
10+
"version": "0.3.0"
11+
}
12+
}
13+
]
14+
},
15+
"version": 1
16+
}

Package.swift

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version:4.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "autokbisw",
8+
dependencies: [
9+
// Dependencies declare other packages that this package depends on.
10+
// .package(url: /* package url */, from: "1.0.0"),
11+
.package(url: "https://github.com/apple/swift-package-manager", from: "0.3.0"),
12+
],
13+
targets: [
14+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
15+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
16+
.target(
17+
name: "autokbisw",
18+
dependencies: ["Utility"]
19+
),
20+
.testTarget(
21+
name: "autokbiswTests",
22+
dependencies: ["autokbisw"]
23+
),
24+
]
25+
)
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright [2016] Jean Helou
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Carbon
16+
import Foundation
17+
import IOKit
18+
import IOKit.hid
19+
import IOKit.usb
20+
21+
internal final class IOKeyEventMonitor {
22+
private let hidManager: IOHIDManager
23+
fileprivate let MAPPINGS_DEFAULTS_KEY = "keyboardISMapping"
24+
fileprivate let notificationCenter: CFNotificationCenter
25+
fileprivate var lastActiveKeyboard: String = ""
26+
fileprivate var kb2is: [String: TISInputSource] = [String: TISInputSource]()
27+
fileprivate var defaults: UserDefaults = UserDefaults.standard
28+
fileprivate var useLocation: Bool
29+
fileprivate var verbosity: Int
30+
31+
init? (usagePage: Int, usage: Int, useLocation: Bool, verbosity: Int) {
32+
self.useLocation = useLocation
33+
self.verbosity = verbosity
34+
hidManager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
35+
notificationCenter = CFNotificationCenterGetDistributedCenter()
36+
let deviceMatch: CFMutableDictionary = [kIOHIDDeviceUsageKey: usage, kIOHIDDeviceUsagePageKey: usagePage] as NSMutableDictionary
37+
IOHIDManagerSetDeviceMatching(hidManager, deviceMatch)
38+
loadMappings()
39+
}
40+
41+
deinit {
42+
self.saveMappings()
43+
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
44+
IOHIDManagerRegisterInputValueCallback(hidManager, Optional.none, context)
45+
CFNotificationCenterRemoveObserver(notificationCenter, context, CFNotificationName(kTISNotifySelectedKeyboardInputSourceChanged), nil)
46+
}
47+
48+
func start() {
49+
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
50+
51+
observeIputSourceChangedNotification(context: context)
52+
registerHIDKeyboardCallback(context: context)
53+
54+
IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode!.rawValue)
55+
IOHIDManagerOpen(hidManager, IOOptionBits(kIOHIDOptionsTypeNone))
56+
}
57+
58+
private func observeIputSourceChangedNotification(context: UnsafeMutableRawPointer) {
59+
let inputSourceChanged: CFNotificationCallback = {
60+
_, observer, _, _, _ in
61+
let selfPtr = Unmanaged<IOKeyEventMonitor>.fromOpaque(observer!).takeUnretainedValue()
62+
selfPtr.onInputSourceChanged()
63+
}
64+
65+
CFNotificationCenterAddObserver(notificationCenter,
66+
context, inputSourceChanged,
67+
kTISNotifySelectedKeyboardInputSourceChanged, nil,
68+
CFNotificationSuspensionBehavior.deliverImmediately)
69+
}
70+
71+
private func registerHIDKeyboardCallback(context: UnsafeMutableRawPointer) {
72+
let myHIDKeyboardCallback: IOHIDValueCallback = {
73+
context, _, sender, _ in
74+
let selfPtr = Unmanaged<IOKeyEventMonitor>.fromOpaque(context!).takeUnretainedValue()
75+
let senderDevice = Unmanaged<IOHIDDevice>.fromOpaque(sender!).takeUnretainedValue()
76+
77+
let vendorId = IOHIDDeviceGetProperty(senderDevice, kIOHIDVendorIDKey as CFString) ??? "unknown"
78+
let productId = IOHIDDeviceGetProperty(senderDevice, kIOHIDProductIDKey as CFString) ??? "unknown"
79+
let product = IOHIDDeviceGetProperty(senderDevice, kIOHIDProductKey as CFString) ??? "unknown"
80+
let manufacturer = IOHIDDeviceGetProperty(senderDevice, kIOHIDManufacturerKey as CFString) ??? "unknown"
81+
let serialNumber = IOHIDDeviceGetProperty(senderDevice, kIOHIDSerialNumberKey as CFString) ??? "unknown"
82+
let locationId = IOHIDDeviceGetProperty(senderDevice, kIOHIDLocationIDKey as CFString) ??? "unknown"
83+
let uniqueId = IOHIDDeviceGetProperty(senderDevice, kIOHIDUniqueIDKey as CFString) ??? "unknown"
84+
85+
let keyboard = selfPtr.useLocation
86+
? "\(product)-[\(vendorId)-\(productId)-\(manufacturer)-\(serialNumber)-\(locationId)]"
87+
: "\(product)-[\(vendorId)-\(productId)-\(manufacturer)-\(serialNumber)]"
88+
89+
if selfPtr.verbosity >= TRACE {
90+
print("received event from keyboard \(keyboard) - \(locationId) - \(uniqueId)")
91+
}
92+
selfPtr.onKeyboardEvent(keyboard: keyboard)
93+
}
94+
95+
IOHIDManagerRegisterInputValueCallback(hidManager, myHIDKeyboardCallback, context)
96+
}
97+
}
98+
99+
extension IOKeyEventMonitor {
100+
func restoreInputSource(keyboard: String) {
101+
if let targetIs = kb2is[keyboard] {
102+
if verbosity >= DEBUG {
103+
print("set input source to \(targetIs) for keyboard \(keyboard)")
104+
}
105+
TISSelectInputSource(targetIs)
106+
} else {
107+
storeInputSource(keyboard: keyboard)
108+
}
109+
}
110+
111+
func storeInputSource(keyboard: String) {
112+
let currentSource: TISInputSource = TISCopyCurrentKeyboardInputSource().takeUnretainedValue()
113+
kb2is[keyboard] = currentSource
114+
saveMappings()
115+
}
116+
117+
func onInputSourceChanged() {
118+
storeInputSource(keyboard: lastActiveKeyboard)
119+
}
120+
121+
func onKeyboardEvent(keyboard: String) {
122+
guard lastActiveKeyboard != keyboard else { return }
123+
124+
restoreInputSource(keyboard: keyboard)
125+
lastActiveKeyboard = keyboard
126+
}
127+
128+
func loadMappings() {
129+
let selectableIsProperties = [
130+
kTISPropertyInputSourceIsEnableCapable: true,
131+
kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource,
132+
] as CFDictionary
133+
let inputSources = TISCreateInputSourceList(selectableIsProperties, false).takeUnretainedValue() as! Array<TISInputSource>
134+
135+
let inputSourcesById = inputSources.reduce([String: TISInputSource]()) {
136+
(dict, inputSource) -> [String: TISInputSource] in
137+
var dict = dict
138+
if let id = unmanagedStringToString(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)) {
139+
dict[id] = inputSource
140+
}
141+
return dict
142+
}
143+
144+
if let mappings = self.defaults.dictionary(forKey: MAPPINGS_DEFAULTS_KEY) {
145+
for (keyboardId, inputSourceId) in mappings {
146+
kb2is[keyboardId] = inputSourcesById[String(describing: inputSourceId)]
147+
}
148+
}
149+
}
150+
151+
func saveMappings() {
152+
let mappings = kb2is.mapValues(is2Id)
153+
defaults.set(mappings, forKey: MAPPINGS_DEFAULTS_KEY)
154+
}
155+
156+
private func is2Id(_ inputSource: TISInputSource) -> String? {
157+
return unmanagedStringToString(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID))!
158+
}
159+
160+
func unmanagedStringToString(_ p: UnsafeMutableRawPointer?) -> String? {
161+
if let cfValue = p {
162+
let value = Unmanaged.fromOpaque(cfValue).takeUnretainedValue() as CFString
163+
if CFGetTypeID(value) == CFStringGetTypeID() {
164+
return value as String
165+
} else {
166+
return nil
167+
}
168+
} else {
169+
return nil
170+
}
171+
}
172+
}
173+
174+
// Nicer string interpolation of optional strings, see: https://oleb.net/blog/2016/12/optionals-string-interpolation/
175+
176+
infix operator ???: NilCoalescingPrecedence
177+
178+
public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String {
179+
return optional.map { String(describing: $0) } ?? defaultValue()
180+
}

Sources/autokbisw/main.swift

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright [2016] Jean Helou
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Basic
16+
import Foundation
17+
import Utility
18+
19+
let DEBUG = 1
20+
let TRACE = 2
21+
22+
let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
23+
let parser = ArgumentParser(usage: "<options>", overview: "Automatic keyboard/input source switching for macOS")
24+
25+
let locationUsage = """
26+
Use locationId to identify keyboards (defaults to false.)
27+
Note that the locationId changes when you plug a keyboard in a different port. Therefore using the locationId in the keyboards identifiers means the configured language will be associated to a keyboard on a specific port.
28+
29+
"""
30+
let locationOption = parser.add(option: "--location", shortName: "-l", kind: Bool.self, usage: locationUsage, completion: .none)
31+
let verboseOption = parser.add(option: "--verbose", shortName: "-v", kind: Int.self, usage: "Print verbose output (1 = DEBUG, 2 = TRACE)", completion: .none)
32+
33+
do {
34+
let parsedArguments = try parser.parse(arguments)
35+
let useLocation = parsedArguments.get(locationOption) ?? false
36+
let verbosity = parsedArguments.get(verboseOption) ?? 0
37+
38+
if verbosity > 0 {
39+
print("Starting with useLocation: \(useLocation) - verbosity: \(verbosity)");
40+
}
41+
let monitor = IOKeyEventMonitor(usagePage: 0x01, usage: 6, useLocation: useLocation, verbosity: verbosity)
42+
monitor?.start()
43+
CFRunLoopRun()
44+
} catch let e as ArgumentParserError {
45+
print(e.description)
46+
} catch let e {
47+
print(e.localizedDescription)
48+
}

Tests/LinuxMain.swift

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import autokbiswTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += autokbiswTests.allTests()
7+
XCTMain(tests)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
3+
#if !os(macOS)
4+
public func allTests() -> [XCTestCaseEntry] {
5+
return [
6+
testCase(autokbiswTests.allTests),
7+
]
8+
}
9+
#endif
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import class Foundation.Bundle
2+
import XCTest
3+
4+
final class autokbiswTests: XCTestCase {
5+
func testExample() throws {
6+
// This is an example of a functional test case.
7+
// Use XCTAssert and related functions to verify your tests produce the correct
8+
// results.
9+
10+
// Some of the APIs that we use below are available in macOS 10.13 and above.
11+
guard #available(macOS 10.13, *) else {
12+
return
13+
}
14+
15+
let fooBinary = productsDirectory.appendingPathComponent("autokbisw")
16+
17+
let process = Process()
18+
process.executableURL = fooBinary
19+
20+
let pipe = Pipe()
21+
process.standardOutput = pipe
22+
23+
try process.run()
24+
process.waitUntilExit()
25+
26+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
27+
let output = String(data: data, encoding: .utf8)
28+
29+
XCTAssertEqual(output, "Hello, world!\n")
30+
}
31+
32+
/// Returns path to the built products directory.
33+
var productsDirectory: URL {
34+
#if os(macOS)
35+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
36+
return bundle.bundleURL.deletingLastPathComponent()
37+
}
38+
fatalError("couldn't find the products directory")
39+
#else
40+
return Bundle.main.bundleURL
41+
#endif
42+
}
43+
44+
static var allTests = [
45+
("testExample", testExample),
46+
]
47+
}

0 commit comments

Comments
 (0)