CutBox.app

Coverage Report

Created: 2024-03-12 03:40

.../Source/Utilities/FakeKey.swift
Line
Count
Source (jump to first uncovered line)
1
//
2
//  FakeKeyEvents.swift
3
//  CutBox
4
//
5
//  Created by Jason Milkins on 26/3/18.
6
//  Copyright © 2018-2023 ocodo. All rights reserved.
7
//
8
9
import Foundation
10
import Carbon
11
12
class FakeKey {
13
    static var testing = false
14
    static var testResult: [CGEvent?] = []
15
16
144
    private let keyCodeRange = 0x00...0x7E
17
18
5
    func send(fakeKey key: String, useCommandFlag: Bool) {
19
5
        let keyCode = stringToKeyCode(key)
20
5
        send(fakeKey: numericCast(keyCode), useCommandFlag: useCommandFlag)
21
5
    }
22
23
508
    private func transformKeyCode(_ keyCode: Int) -> String {
24
508
        guard keyCodeRange.contains(keyCode)
25
508
            else { fatalError("Cannot transform \(keyCode) - out of range") }
26
508
27
508
        let inputSource = unsafeBitCast(
28
508
            TISGetInputSourceProperty(
29
508
                TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue(),
30
508
                kTISPropertyUnicodeKeyLayoutData
31
508
        ), to: CFData.self)
32
508
33
508
        var deadKeyState: UInt32 = 0
34
508
        var char = UniChar()
35
508
        var length = 0
36
508
37
508
        CoreServices.UCKeyTranslate(unsafeBitCast(CFDataGetBytePtr(inputSource),
38
508
                                                  to: UnsafePointer<CoreServices.UCKeyboardLayout>.self),
39
508
                                    UInt16(keyCode),
40
508
                                    UInt16(CoreServices.kUCKeyActionDisplay),
41
508
                                    UInt32(0),
42
508
                                    UInt32(LMGetKbdType()),
43
508
                                    OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit),
44
508
45
508
                                    &deadKeyState,
46
508
                                    1,
47
508
                                    &length,
48
508
                                    &char)
49
508
50
508
        return NSString(characters: &char, length: length).uppercased
51
508
    }
52
53
4
    private func keyCodeTranslationTable() -> [String: Int] {
54
4
        var table = [String: Int]()
55
508
        for code in keyCodeRange {
56
508
            table[transformKeyCode(code)] = code
57
508
        }
58
4
        return table
59
4
    }
60
61
5
    private func stringToKeyCode(_ key: String) -> Int {
62
5
        guard key.count == 1 else { fatalError("Only single char strings can be used") }
63
5
64
5
        let translationTable = keyCodeTranslationTable()
65
5
66
5
        guard let keyCode = translationTable[key]
67
5
            else { fatalError("Unable to get keyCode for: \(key)") }
68
5
69
5
        return keyCode
70
5
    }
71
72
4
    private func send(fakeKey keyCode: CGKeyCode, useCommandFlag: Bool) {
73
4
        let sourceRef = CGEventSource(stateID: .combinedSessionState)
74
4
75
4
        if sourceRef == nil {
76
0
            NSLog("No event source")
77
0
            return
78
4
        }
79
4
80
4
        let keyDownEvent = CGEvent(
81
4
            keyboardEventSource: sourceRef,
82
4
            virtualKey: keyCode,
83
4
            keyDown: true
84
4
        )
85
4
86
4
        if useCommandFlag {
87
3
            keyDownEvent?.flags = .maskCommand
88
4
        }
89
4
90
4
        let keyUpEvent = CGEvent(
91
4
            keyboardEventSource: sourceRef,
92
4
            virtualKey: keyCode,
93
4
            keyDown: false
94
4
        )
95
4
96
4
        if !Self.testing {
97
0
            keyDownEvent?.post(tap: .cghidEventTap)
98
0
            keyUpEvent?.post(tap: .cghidEventTap)
99
4
        } else {
100
4
            Self.testResult = [keyDownEvent, keyUpEvent]
101
4
        }
102
4
    }
103
}