// // Copyright © 2021 osy. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import Carbon.HIToolbox /// Based on https://stackoverflow.com/a/64344453/4236245 /// Translated to Swift by conath class KeyCodeMap { private static var keyMapDict: Dictionary>! private static var modFlagDict: Dictionary! private static var modFlags: [UInt]! /// Creates the internal key map if needed. Must be called on the main queue! static func createKeyMapIfNeeded() { if keyMapDict == nil { keyMapDict = makeKeyMap() } } static func characterToKeyCode(character: Character) -> Dictionary? { createKeyMapIfNeeded() /* The returned dictionary contains entries for the virtual key code and boolean flags for modifier keys used for the character. */ if let keyCodeDict = keyMapDict[String(character)] { return keyCodeDict } else { return tryHandleSpecialChar(character) } } private static func makeKeyMap() -> Dictionary> { var modifiers: UInt = 0 // create dictionary of modifier names and keys. if (modFlagDict == nil) { modFlagDict = ["option": NSEvent.ModifierFlags.option.rawValue, "shift": NSEvent.ModifierFlags.shift.rawValue, "function": NSEvent.ModifierFlags.function.rawValue, "control": NSEvent.ModifierFlags.control.rawValue, "command": NSEvent.ModifierFlags.command.rawValue] modFlags = Array(modFlagDict.values) } var keyMapDict = Dictionary>() // run through 128 base key codes to see what they produce for keyCode: UInt16 in 0..<128 { // create dummy NSEvent from a CGEvent for a keypress let coreEvent = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true)! let keyEvent = NSEvent(cgEvent: coreEvent)! if (keyEvent.type == .keyDown) { // this repeat/while loop through every permutation of modifier keys for a given key code repeat { var subDict = Dictionary() // cerate dictionary containing current modifier keys and virtual key code for key: String in modFlagDict.keys { let modKeyIsUsed = ((modFlagDict[key]! & modifiers) != 0) subDict[key] = NSNumber(booleanLiteral: modKeyIsUsed).intValue } subDict["virtKeyCode"] = (keyCode as NSNumber).intValue // manipulate the NSEvent to get character produce by virtual key code and modifiers var character: String if modifiers == 0 { character = keyEvent.characters! } else { character = keyEvent.characters(byApplyingModifiers: NSEvent.ModifierFlags(rawValue: modifiers))! } // add sub-dictionary to main dictionary using character as key if keyMapDict[character] == nil { keyMapDict[character] = subDict } // permutate the modifiers modifiers = permutatateMods(modFlags: modFlags) } while (modifiers != 0) } } return keyMapDict } private static let idxSet = NSMutableIndexSet() private static func permutatateMods(modFlags: [UInt]) -> UInt { var modifiers: UInt = 0 var idx: Int = 0 /* Starting at 0, if the index exists, remove it and move up; if the index doesn't exist, add it. Will cycle through a standard binary progression. Indexes are then applied to the passed array, and the selected elements are 'OR'ed together */ var done = false while !done { if idxSet.contains([idx]) { idxSet.remove([idx]) idx += 1 continue; } if idx < modFlags.count { idxSet.add([idx]) } else { idxSet.removeAllIndexes() } done = true } let modArray = (modFlags as NSArray).objects(at: idxSet as IndexSet) as NSArray for modObj in modArray { modifiers |= (modObj as! NSNumber).uintValue } return modifiers } /// Keyboard scan code for key down and up (which is usually `down + 0x80`) struct ScanCodes { let down: UInt16 let up: UInt8 /// Construct a `ScanCodes` from a tuple of `Int`s static func t(_ tuple: (down: UInt16, up: UInt8)) -> ScanCodes { return ScanCodes(down: tuple.down, up: tuple.up) } } // Key Scan Codes mapping from https://www.cs.yale.edu/flint/cs422/doc/art-of-asm/pdf/CH20.PDF // Page 1154, Table 72: PC Keyboard Scan Codes (in hex) /// Converts macOS key code to IBM scan code for key up and down /// The "up" scan codes are currently unused in UTM due to SPICE: /// we instead send keyUp with the "down" scan code. /// (See also `CSInput.sendKey:type:code`) static let keyCodeToScanCodes: [Int:ScanCodes] = [ kVK_Escape: .t((down: 0x01, up: 0x81)), kVK_ANSI_1: .t((down: 0x02, up: 0x82)), kVK_ANSI_2: .t((down: 0x03, up: 0x83)), kVK_ANSI_3: .t((down: 0x04, up: 0x84)), kVK_ANSI_4: .t((down: 0x05, up: 0x85)), kVK_ANSI_5: .t((down: 0x06, up: 0x86)), kVK_ANSI_6: .t((down: 0x07, up: 0x87)), kVK_ANSI_7: .t((down: 0x08, up: 0x88)), kVK_ANSI_8: .t((down: 0x09, up: 0x89)), kVK_ANSI_9: .t((down: 0x0a, up: 0x8a)), kVK_ANSI_0: .t((down: 0x0b, up: 0x8b)), kVK_ANSI_Minus: .t((down: 0x0c, up: 0x8c)), kVK_ANSI_Equal: .t((down: 0x0d, up: 0x8d)), kVK_Delete: .t((down: 0x0e, up: 0x8e)), /// IBM name is `backspace` kVK_Tab: .t((down: 0x0f, up: 0x8f)), kVK_ANSI_Q: .t((down: 0x10, up: 0x90)), kVK_ANSI_W: .t((down: 0x11, up: 0x91)), kVK_ANSI_E: .t((down: 0x12, up: 0x92)), kVK_ANSI_R: .t((down: 0x13, up: 0x93)), kVK_ANSI_T: .t((down: 0x14, up: 0x94)), kVK_ANSI_Y: .t((down: 0x15, up: 0x95)), kVK_ANSI_U: .t((down: 0x16, up: 0x96)), kVK_ANSI_I: .t((down: 0x17, up: 0x97)), kVK_ANSI_O: .t((down: 0x18, up: 0x98)), kVK_ANSI_P: .t((down: 0x19, up: 0x99)), kVK_ANSI_LeftBracket: .t((down: 0x1a, up: 0x9a)), kVK_ANSI_RightBracket: .t((down: 0x1b, up: 0x9b)), kVK_Return: .t((down: 0x1c, up: 0x9c)), /// IBM name is `enter` kVK_Control: .t((down: 0x1d, up: 0x9d)), kVK_ANSI_A: .t((down: 0x1e, up: 0x9e)), kVK_ANSI_S: .t((down: 0x1f, up: 0x9f)), kVK_ANSI_D: .t((down: 0x20, up: 0xa0)), kVK_ANSI_F: .t((down: 0x21, up: 0xa1)), kVK_ANSI_G: .t((down: 0x22, up: 0xa2)), kVK_ANSI_H: .t((down: 0x23, up: 0xa3)), kVK_ANSI_J: .t((down: 0x24, up: 0xa4)), kVK_ANSI_K: .t((down: 0x25, up: 0xa5)), kVK_ANSI_L: .t((down: 0x26, up: 0xa6)), kVK_ANSI_Semicolon: .t((down: 0x27, up: 0xa7)), kVK_ANSI_Quote: .t((down: 0x28, up: 0xa8)), kVK_ANSI_Grave: .t((down: 0x29, up: 0xa9)), kVK_Shift: .t((down: 0x2a, up: 0xaa)), kVK_ANSI_Backslash: .t((down: 0x2b, up: 0xab)), kVK_ANSI_Z: .t((down: 0x2c, up: 0xac)), kVK_ANSI_X: .t((down: 0x2d, up: 0xad)), kVK_ANSI_C: .t((down: 0x2e, up: 0xae)), kVK_ANSI_V: .t((down: 0x2f, up: 0xaf)), kVK_ANSI_B: .t((down: 0x30, up: 0xb0)), kVK_ANSI_N: .t((down: 0x31, up: 0xb1)), kVK_ANSI_M: .t((down: 0x32, up: 0xb2)), kVK_ANSI_Comma: .t((down: 0x33, up: 0xb3)), kVK_ANSI_Period: .t((down: 0x34, up: 0xb4)), kVK_ANSI_Slash: .t((down: 0x35, up: 0xb5)), kVK_RightShift: .t((down: 0x36, up: 0xb6)), // Print screen not available in Carbon kVK_Option: .t((down: 0x38, up: 0xb8)), /// IBM name is `alt` kVK_Space: .t((down: 0x39, up: 0xb9)), kVK_CapsLock: .t((down: 0x3a, up: 0xba)), kVK_F1: .t((down: 0x3b, up: 0xbb)), kVK_F2: .t((down: 0x3c, up: 0xbc)), kVK_F3: .t((down: 0x3d, up: 0xbd)), kVK_F4: .t((down: 0x3e, up: 0xbe)), kVK_F5: .t((down: 0x3f, up: 0xbf)), kVK_F6: .t((down: 0x40, up: 0xc0)), kVK_F7: .t((down: 0x41, up: 0xc1)), kVK_F8: .t((down: 0x42, up: 0xc2)), kVK_F9: .t((down: 0x43, up: 0xc3)), kVK_F10: .t((down: 0x44, up: 0xc4)), // Numlock not available in Carbon // Scroll lock not available in Carbon // Number pad Home, up, pgUp not available in Carbon kVK_ANSI_KeypadMinus: .t((down: 0x4a, up: 0xca)), // Number pad left, center, right not available in Carbon kVK_ANSI_KeypadPlus: .t((down: 0x4e, up: 0xce)), // Number pad end, down, pgDown, insert not available in Carbon kVK_ANSI_KeypadClear: .t((down: 0x45, up: 0xC5)), /// in IBM this is num lock, so we send that kVK_ANSI_KeypadDivide: .t((down: 0xe035, up: 0xb5)), kVK_ANSI_KeypadEnter: .t((down: 0xe01c, up: 0x9c)), kVK_ANSI_Keypad0: .t((down: 0x52, up: 0xD2)), kVK_ANSI_Keypad1: .t((down: 0x4F, up: 0xCF)), kVK_ANSI_Keypad2: .t((down: 0x50, up: 0xD0)), kVK_ANSI_Keypad3: .t((down: 0x51, up: 0xD1)), kVK_ANSI_Keypad4: .t((down: 0x4B, up: 0xCB)), kVK_ANSI_Keypad5: .t((down: 0x4C, up: 0xCC)), kVK_ANSI_Keypad6: .t((down: 0x4D, up: 0xCD)), kVK_ANSI_Keypad7: .t((down: 0x47, up: 0xC7)), kVK_ANSI_Keypad8: .t((down: 0x48, up: 0xC8)), kVK_ANSI_Keypad9: .t((down: 0x49, up: 0xC9)), kVK_ANSI_KeypadDecimal: .t((down: 0x53, up: 0xD3)), kVK_ANSI_KeypadEquals: .t((down: 0x00, up: 0x00)), /// Not found on IBM kVK_ANSI_KeypadMultiply:.t((down: 0x37, up: 0xB7)), kVK_F11: .t((down: 0x57, up: 0xd7)), kVK_F12: .t((down: 0x58, up: 0xd8)), // Insert not available in Carbon kVK_ForwardDelete: .t((down: 0xe053, up: 0xd3)), /// IBM name is `delete` kVK_Home: .t((down: 0xe047, up: 0xc7)), kVK_End: .t((down: 0xe04f, up: 0xcf)), kVK_PageUp: .t((down: 0xe049, up: 0xc9)), kVK_PageDown: .t((down: 0xe051, up: 0xd1)), kVK_LeftArrow: .t((down: 0xe04b, up: 0xcb)), kVK_RightArrow: .t((down: 0xe04d, up: 0xcd)), kVK_UpArrow: .t((down: 0xe048, up: 0xc8)), kVK_DownArrow: .t((down: 0xe050, up: 0xd0)), kVK_RightOption: .t((down: 0xe038, up: 0xb8)), /// IBM name is `right alt` kVK_RightControl: .t((down: 0xe01d, up: 0x9d)), // Pause not available in Carbon /* Additional non-IBM keys */ kVK_Command: .t((down: 0xe05b, up: 0xdb)), kVK_RightCommand: .t((down: 0xe05c, up: 0xdc)), kVK_ISO_Section: .t((down: 0x56, up: 0xD6)), kVK_VolumeUp: .t((down: 0xe030, up: 0xb0)), kVK_VolumeDown: .t((down: 0xe02e, up: 0xae)), kVK_Mute: .t((down: 0xE020, up: 0xa0)), kVK_F13: .t((down: 0x64, up: 0xe4)), kVK_F14: .t((down: 0x65, up: 0xe5)), kVK_F15: .t((down: 0x66, up: 0xe6)), kVK_F16: .t((down: 0x67, up: 0xe7)), kVK_F17: .t((down: 0x68, up: 0xe8)), kVK_F18: .t((down: 0x69, up: 0xe9)), kVK_F19: .t((down: 0x6a, up: 0xea)), kVK_F20: .t((down: 0x6b, up: 0xeb)), kVK_JIS_Yen: .t((down: 0x7d, up: 0xfd)), kVK_JIS_Underscore: .t((down: 0x73, up: 0xf3)), kVK_JIS_KeypadComma: .t((down: 0x00, up: 0x00)), /// Not found on IBM kVK_JIS_Eisu: .t((down: 0x7b, up: 0xfb)), /// Fallback to `muhenkan` kVK_JIS_Kana: .t((down: 0x79, up: 0xf9)), /// Fallback to `henkan` /* The Function and help keys doesn't have a scan code */ kVK_Function: .t((down: 0x00, up: 0x00)), kVK_Help: .t((down: 0x00, up: 0x00)) ] } extension KeyCodeMap { /// Support ASCII control characters /// https://jkorpela.fi/chars/c0.html fileprivate static func tryHandleSpecialChar(_ character: Character) -> Dictionary? { if let ascii = character.asciiValue { var virtKeyCode: Int? if ascii <= 31 { /// Control held switch ascii { case 1: virtKeyCode = kVK_ANSI_A case 2: virtKeyCode = kVK_ANSI_B case 3: virtKeyCode = kVK_ANSI_C case 4: virtKeyCode = kVK_ANSI_D case 5: virtKeyCode = kVK_ANSI_E case 6: virtKeyCode = kVK_ANSI_F case 7: virtKeyCode = kVK_ANSI_G case 8: virtKeyCode = kVK_ANSI_H case 9: virtKeyCode = kVK_ANSI_I case 10: virtKeyCode = kVK_ANSI_J case 11: virtKeyCode = kVK_ANSI_K case 12: virtKeyCode = kVK_ANSI_L case 13: virtKeyCode = kVK_ANSI_M case 14: virtKeyCode = kVK_ANSI_N case 15: virtKeyCode = kVK_ANSI_O case 16: virtKeyCode = kVK_ANSI_P case 17: virtKeyCode = kVK_ANSI_Q case 18: virtKeyCode = kVK_ANSI_R case 19: virtKeyCode = kVK_ANSI_S case 20: virtKeyCode = kVK_ANSI_T case 21: virtKeyCode = kVK_ANSI_U case 22: virtKeyCode = kVK_ANSI_V case 23: virtKeyCode = kVK_ANSI_W case 24: virtKeyCode = kVK_ANSI_Y case 25: virtKeyCode = kVK_ANSI_X case 26: virtKeyCode = kVK_ANSI_Z case 27: virtKeyCode = kVK_ANSI_LeftBracket case 28: virtKeyCode = kVK_ANSI_Backslash case 29: virtKeyCode = kVK_ANSI_RightBracket case 30: if var dict = characterToKeyCode(character: "^") { dict["control"] = 1 return dict } else { return nil } case 31: if var dict = characterToKeyCode(character: "_") { dict["control"] = 1 return dict } else { return nil } default: virtKeyCode = nil } if let virtKeyCode = virtKeyCode { return [ "option": 0, "shift": 0, "function": 0, "control": 1, "command": 0, "virtKeyCode": virtKeyCode ] } } else if ascii == 127 { /// Delete key return [ "option": 0, "shift": 0, "function": 0, "control": 1, "command": 0, "virtKeyCode": kVK_Delete ] } } return nil } }