// // KeyManager.swift // Voidex // // Created by True砖家 on 2024/7/23. // Copyright © 2024 True砖家 on Bilibili. All rights reserved. // import Foundation import UIKit // Define the RemoteCommand class @objc public class RemoteCommand: NSObject, NSSecureCoding { // MARK: - NSSecureCoding public static var supportsSecureCoding: Bool { return true } // MARK: - Properties @objc var cmdString: String @objc var alias: String // MARK: - Initialization init(cmdString: String, alias: String) { self.cmdString = cmdString self.alias = alias } // MARK: - NSSecureCoding required public init?(coder: NSCoder) { guard let cmdString = coder.decodeObject(of: NSString.self, forKey: "keyboardCmdString") as String?, let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else { return nil } self.cmdString = cmdString self.alias = alias } public func encode(with coder: NSCoder) { coder.encode(cmdString, forKey: "keyboardCmdString") coder.encode(alias, forKey: "alias") } } // Define the CommandManager class @objc public class CommandManager: NSObject { @objc public static let shared = CommandManager() @objc public static let mouseButtonMappings: [String: Int32] = [ "M_LEFT" : BUTTON_LEFT, "MLEFT" : BUTTON_LEFT, "M_MIDDLE" : BUTTON_MIDDLE, "MMIDDLE" : BUTTON_MIDDLE, "M_RIGHT" : BUTTON_RIGHT, "MRIGHT" : BUTTON_RIGHT, "M_X1" : BUTTON_X1, "MX1" : BUTTON_X1, "M_X2" : BUTTON_X2, "MX2" : BUTTON_X2 ] @objc public static let oscButtonMappings: [String: Int32] = [ "OSCA" : A_FLAG, "OSCB" : B_FLAG, "OSCX" : X_FLAG, "OSCY" : Y_FLAG, "OSCL1" : LB_FLAG, "L1" : LB_FLAG, "LB" : LB_FLAG, "OSCR1" : RB_FLAG, "R1" : RB_FLAG, "RB" : RB_FLAG, "OSCL3" : LS_CLK_FLAG, "L3" : LS_CLK_FLAG, "LS" : LS_CLK_FLAG, "OSCR3" : RS_CLK_FLAG, "R3" : RS_CLK_FLAG, "RS" : RS_CLK_FLAG, "OSCSTART" : PLAY_FLAG, "OSCPLAY" : PLAY_FLAG, "OSCSELECT" : BACK_FLAG, "OSCBACK" : BACK_FLAG, "OSCUP" : UP_FLAG, "OSCDOWN" : DOWN_FLAG, "OSCLEFT" : LEFT_FLAG, "OSCRIGHT" : RIGHT_FLAG, "OSCL2" : 0, "L2" : 0, "LT" : 0, "OSCR2" : 0, "R2" : 0, "RT" : 0 ] @objc public static let oscRectangleButtonCmds: [String] = [ "OSCUP", "OSCDOWN", "OSCLEFT", "OSCRIGHT", "OSCSTART", "OSCPLAY", "OSCSELECT", "OSCBACK" ] @objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "MOUSEPAD", "DPAD", "WASDPAD", "ARROWPAD"] @objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"] @objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"] @objc public static let specialOverlayButtonCmds: [String] = ["SETTINGS"] // @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"] static let keyboardButtonMappings: [String: Int16] = [ // Windows Key Codes "NULL": 0xFF, "CTRL": 0x11, // VK_CONTROL "SHIFT": 0x10, // VK_SHIFT "ALT": 0x12, // VK_MENU "F1": 0x70, // VK_F1 "F2": 0x71, // VK_F2 "F3": 0x72, // VK_F3 "F4": 0x73, // VK_F4 "F5": 0x74, // VK_F5 "F6": 0x75, // VK_F6 "F7": 0x76, // VK_F7 "F8": 0x77, // VK_F8 "F9": 0x78, // VK_F9 "F10": 0x79, // VK_F10 "F11": 0x7A, // VK_F11 "F12": 0x7B, // VK_F12 "A": 0x41, // 'A' key "B": 0x42, // 'B' key "C": 0x43, // 'C' key "D": 0x44, // 'D' key "E": 0x45, // 'E' key "F": 0x46, // 'F' key "G": 0x47, // 'G' key "H": 0x48, // 'H' key "I": 0x49, // 'I' key "J": 0x4A, // 'J' key "K": 0x4B, // 'K' key "L": 0x4C, // 'L' key "M": 0x4D, // 'M' key "N": 0x4E, // 'N' key "O": 0x4F, // 'O' key "P": 0x50, // 'P' key "Q": 0x51, // 'Q' key "R": 0x52, // 'R' key "S": 0x53, // 'S' key "T": 0x54, // 'T' key "U": 0x55, // 'U' key "V": 0x56, // 'V' key "W": 0x57, // 'W' key "X": 0x58, // 'X' key "Y": 0x59, // 'Y' key "Z": 0x5A, // 'Z' key "0": 0x30, // '0' key "1": 0x31, // '1' key "2": 0x32, // '2' key "3": 0x33, // '3' key "4": 0x34, // '4' key "5": 0x35, // '5' key "6": 0x36, // '6' key "7": 0x37, // '7' key "8": 0x38, // '8' key "9": 0x39, // '9' key "ESC": 0x1B, // VK_ESCAPE "SPACE": 0x20, // VK_SPACE "ENTER": 0x0D, // VK_RETURN "TAB": 0x09, // VK_TAB "BACKSPACE": 0x08, // VK_BACK "INSERT": 0x2D, // VK_INSERT "DEL": 0x2E, // VK_DELETE "HOME": 0x24, // VK_HOME "END": 0x23, // VK_END "PG_UP": 0x21, // VK_PRIOR "PGUP": 0x21, // VK_PRIOR "PG_DOWN": 0x22, // VK_NEXT "PGDOWN": 0x22, // VK_NEXT "PGDN": 0x22, // VK_NEXT "UP_ARROW": 0x26, // VK_UP "UPARR": 0x26, // VK_UP "DOWN_ARROW": 0x28, // VK_DOWN "DOWNARR": 0x28, // VK_DOWN "LEFT_ARROW": 0x25, // VK_LEFT "LEFTARR": 0x25, // VK_LEFT "RIGHT_ARROW": 0x27, // VK_RIGHT "RIGHTARR": 0x27, // VK_RIGHT "NUM_LCK": 0x90, // VK_NUMLOCK "NUMLCK": 0x90, // VK_NUMLOCK "SCR_LCK": 0x91, // VK_SCROLL "SCRLCK": 0x91, // VK_SCROLL "CAPS_LOCK": 0x14, // VK_CAPITAL "CAPSLOCK": 0x14, // VK_CAPITAL "PAUSE": 0x13, // VK_PAUSE "PR_SCR": 0x2C, // VK_SNAPSHOT "PRSCR": 0x2C, // VK_SNAPSHOT "NUMPAD0": 0x60, // VK_NUMPAD0 "NUMPAD1": 0x61, // VK_NUMPAD1 "NUMPAD2": 0x62, // VK_NUMPAD2 "NUMPAD3": 0x63, // VK_NUMPAD3 "NUMPAD4": 0x64, // VK_NUMPAD4 "NUMPAD5": 0x65, // VK_NUMPAD5 "NUMPAD6": 0x66, // VK_NUMPAD6 "NUMPAD7": 0x67, // VK_NUMPAD7 "NUMPAD8": 0x68, // VK_NUMPAD8 "NUMPAD9": 0x69, // VK_NUMPAD9 "MULTIPLY": 0x6A, // VK_MULTIPLY "ADD": 0x6B, // VK_ADD "SUBTRACT": 0x6D, // VK_SUBTRACT "DECIMAL": 0x6E, // VK_DECIMAL "DIVIDE": 0x6F, // VK_DIVIDE "SEMI_COLON": 0xBA, // VK_OEM_1 "SEMICOLON": 0xBA, // VK_OEM_1 "EQUALS": 0xBB, // VK_OEM_PLUS "COMMA": 0xBC, // VK_OEM_COMMA "MINUS": 0xBD, // VK_OEM_MINUS "PERIOD": 0xBE, // VK_OEM_PERIOD "FORWARD_SLASH": 0xBF, // VK_OEM_2 "FORWARDSLASH": 0xBF, // VK_OEM_2 "GRAVE_ACCENT": 0xC0, // VK_OEM_3 "GRAVEACCENT": 0xC0, // VK_OEM_3 "OPEN_BRACKET": 0xDB, // VK_OEM_4 "OPENBRACKET": 0xDB, // VK_OEM_4 "BACKSLASH": 0xDC, // VK_OEM_5 "CLOSE_BRACKET": 0xDD, // VK_OEM_6 "CLOSEBRACKET": 0xDD, // VK_OEM_6 "SINGLE_QUOTE": 0xDE, // VK_OEM_7 "SINGLEQUOTE": 0xDE, // VK_OEM_7 "VOLUME_MUTE": 0xAD, // VK_VOLUME_MUTE "VOLMUTE": 0xAD, // VK_VOLUME_MUTE "VOLUME_DOWN": 0xAE, // VK_VOLUME_DOWN "VOLDOWN": 0xAE, // VK_VOLUME_DOWN "VOLUME_UP": 0xAF, // VK_VOLUME_UP "VOLUP": 0xAF, // VK_VOLUME_UP "MEDIA_NEXT": 0xB0, // VK_MEDIA_NEXT_TRACK "MEDIANEXT": 0xB0, // VK_MEDIA_NEXT_TRACK "MEDIA_PREV": 0xB1, // VK_MEDIA_PREV_TRACK "MEDIAPREV": 0xB1, // VK_MEDIA_PREV_TRACK "MEDIA_STOP": 0xB2, // VK_MEDIA_STOP "MEDIASTOP": 0xB2, // VK_MEDIA_STOP "MEDIA_PLAY_PAUSE": 0xB3, // VK_MEDIA_PLAY_PAUSE "PLAYPAUSE": 0xB3, // VK_MEDIA_PLAY_PAUSE "LAUNCH_MAIL": 0xB4, // VK_LAUNCH_MAIL "LAUNCHMAIL": 0xB4, // VK_LAUNCH_MAIL "LAUNCH_MEDIA_SELECT": 0xB5, // VK_LAUNCH_MEDIA_SELECT "MEDIA_SELECT": 0xB5, // VK_LAUNCH_MEDIA_SELECT "LAUNCH_APP1": 0xB6, // VK_LAUNCH_APP1 "LAUNCHAPP1": 0xB6, // VK_LAUNCH_APP1 "LAUNCH_APP2": 0xB7, // VK_LAUNCH_APP2 "LAUNCHAPP2": 0xB7, // VK_LAUNCH_APP2 "WIN": 0x5B, "LEFT_WIN": 0x5B, // VK_LWIN "RIGHT_WIN": 0x5C, // VK_RWIN "RIGHTWIN": 0x5C, // VK_RWIN "APPS": 0x5D, // VK_APPS // macOS Key Codes "CMD": 0x37, // ⌘ Command "OPT": 0x3A, // ⌥ Option "CONTROL": 0x3B, // ⌃ Control "FUNCTION": 0x3F, // fn "SHIFTMAC": 0x38, // ⇧ Shift "DELETEMAC": 0x75, // Forward Delete "RETURNMAC": 0x24, // Return "ENTERMAC": 0x4C, // Enter "ESCAPEMAC": 0x35, // Escape "TABMAC": 0x30, // Tab "SPACEMAC": 0x31, // Space "UPARRMAC": 0x7E, // Up Arrow "DOWNARRMAC": 0x7D, // Down Arrow "LEFTARRMAC": 0x7B, // Left Arrow "RIGHTARRMAC": 0x7C, // Right Arrow "F1MAC": 0x7A, // F1 "F2MAC": 0x78, // F2 "F3MAC": 0x63, // F3 "F4MAC": 0x76, // F4 "F5MAC": 0x60, // F5 "F6MAC": 0x61, // F6 "F7MAC": 0x62, // F7 "F8MAC": 0x64, // F8 "F9MAC": 0x65, // F9 "F10MAC": 0x6D, // F10 "F11MAC": 0x67, // F11 "F12MAC": 0x6F, // F12 "0MAC": 0x52, // 0 "1MAC": 0x53, // 1 "2MAC": 0x54, // 2 "3MAC": 0x55, // 3 "4MAC": 0x56, // 4 "5MAC": 0x57, // 5 "6MAC": 0x58, // 6 "7MAC": 0x59, // 7 "8MAC": 0x5A, // 8 "9MAC": 0x5B, // 9 "NUMPAD0MAC": 0x4F, // Numpad 0 "NUMPAD1MAC": 0x50, // Numpad 1 "NUMPAD2MAC": 0x51, // Numpad 2 "NUMPAD3MAC": 0x52, // Numpad 3 "NUMPAD4MAC": 0x53, // Numpad 4 "NUMPAD5MAC": 0x54, // Numpad 5 "NUMPAD6MAC": 0x55, // Numpad 6 "NUMPAD7MAC": 0x56, // Numpad 7 "NUMPAD8MAC": 0x57, // Numpad 8 "NUMPAD9MAC": 0x58, // Numpad 9 "NUMPADADDMAC": 0x45, // Numpad Add "NUMPADSUBTRACTMAC": 0x4A, // Numpad Subtract "NUMPADMULTIPLYMAC": 0x43, // Numpad Multiply "NUMPADDIVIDEMAC": 0x4B, // Numpad Divide "NUMPADDECIMALMAC": 0x41, // Numpad Decimal "SHIFT_MAC": 0x38, // ⇧ Shift "DELETE_MAC": 0x75, // Forward Delete "RETURN_MAC": 0x24, // Return "ENTER_MAC": 0x4C, // Enter "ESCAPE_MAC": 0x35, // Escape "TAB_MAC": 0x30, // Tab "SPACE_MAC": 0x31, // Space "UP_ARROW_MAC": 0x7E, // Up Arrow "DOWN_ARROW_MAC": 0x7D, // Down Arrow "LEFT_ARROW_MAC": 0x7B, // Left Arrow "RIGHT_ARROW_MAC": 0x7C, // Right Arrow "F1_MAC": 0x7A, // F1 "F2_MAC": 0x78, // F2 "F3_MAC": 0x63, // F3 "F4_MAC": 0x76, // F4 "F5_MAC": 0x60, // F5 "F6_MAC": 0x61, // F6 "F7_MAC": 0x62, // F7 "F8_MAC": 0x64, // F8 "F9_MAC": 0x65, // F9 "F10_MAC": 0x6D, // F10 "F11_MAC": 0x67, // F11 "F12_MAC": 0x6F, // F12 "0_MAC": 0x52, // 0 "1_MAC": 0x53, // 1 "2_MAC": 0x54, // 2 "3_MAC": 0x55, // 3 "4_MAC": 0x56, // 4 "5_MAC": 0x57, // 5 "6_MAC": 0x58, // 6 "7_MAC": 0x59, // 7 "8_MAC": 0x5A, // 8 "9_MAC": 0x5B, // 9 "NUMPAD0_MAC": 0x4F, // Numpad 0 "NUMPAD1_MAC": 0x50, // Numpad 1 "NUMPAD2_MAC": 0x51, // Numpad 2 "NUMPAD3_MAC": 0x52, // Numpad 3 "NUMPAD4_MAC": 0x53, // Numpad 4 "NUMPAD5_MAC": 0x54, // Numpad 5 "NUMPAD6_MAC": 0x55, // Numpad 6 "NUMPAD7_MAC": 0x56, // Numpad 7 "NUMPAD8_MAC": 0x57, // Numpad 8 "NUMPAD9_MAC": 0x58, // Numpad 9 "NUMPAD_ADD_MAC": 0x45, // Numpad Add "NUMPAD_SUBTRACT_MAC": 0x4A, // Numpad Subtract "NUMPAD_MULTIPLY_MAC": 0x43, // Numpad Multiply "NUMPAD_DIVIDE_MAC": 0x4B, // Numpad Divide "NUMPAD_DECIMAL_MAC": 0x41, // Numpad Decimal ] private var commands: [RemoteCommand] = [] public weak var viewController: CommandManagerViewController? private override init() { super.init() loadCommands() } @objc static func presetDefaultCommands() { let defaults = UserDefaults.standard //if true { // save default entries if the data is empty. if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty. var defaultCommands: [RemoteCommand] = [ RemoteCommand(cmdString: "WIN", alias: "WIN"), RemoteCommand(cmdString: "F11", alias: "F11"), RemoteCommand(cmdString: "ESC", alias: "ESC"), RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "任务管理器(Task Manager)"), RemoteCommand(cmdString: "ALT+F1", alias: "N卡截图(Nvidia Screenshot)"), RemoteCommand(cmdString: "ALT+F9", alias: "N卡录屏(Nvidia Screen Recording)"), RemoteCommand(cmdString: "ALT+F4", alias: "关闭窗口(ALT+F4)"), RemoteCommand(cmdString: "CTRL+A", alias: "全选(Select All)"), RemoteCommand(cmdString: "CTRL+C", alias: "复制(Copy)"), RemoteCommand(cmdString: "CTRL+V", alias: "粘贴(Paste)"), RemoteCommand(cmdString: "WIN+D", alias: "切换桌面(Switch to Desktop)"), RemoteCommand(cmdString: "WIN+P", alias: "多显模式(Project)"), RemoteCommand(cmdString: "WIN+G", alias: "Xbox Game Bar"), RemoteCommand(cmdString: "SHIFT+TAB", alias: "Steam Overlay"), ] let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false) defaults.set(data, forKey: "savedCommands") } } @objc public func createTestKeyMappings() -> [String: Int16] { return CommandManager.keyboardButtonMappings } // extractKeyStrings from keyboardCMDString @objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? { let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|") let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$" guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { print("Failed to create regex") return nil } let range = NSRange(location: 0, length: input.utf16.count) guard let match = regex.firstMatch(in: input, options: [], range: range) else { print("No match found for input: \(input)") return nil } print("Regex matched for input: \(input)") let matchedString = (input as NSString).substring(with: match.range(at: 0)) let keyStrings = matchedString.split(separator: "+").map { String($0) } guard !keyStrings.isEmpty else { print("No key strings found in the matched string") return nil } var validKeyStrings: [String] = [] for key in keyStrings { if CommandManager.keyboardButtonMappings.keys.contains(key) { validKeyStrings.append(key) } else { print(" '\(key)' is not defined in key mappings") return nil //treat any illegal string as a whole } } if validKeyStrings.isEmpty { print("No valid key strings found in the matched string") return nil } for (index, key) in validKeyStrings.enumerated() { print("Valid Key \(index): \(key)") } return validKeyStrings } //super combo key button strings @objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? { let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String }, CommandManager.oscButtonMappings.keys.map { $0 as String }, CommandManager.mouseButtonMappings.keys.map { $0 as String }, CommandManager.touchPadCmds.map { $0 as String } ] .lazy // 零内存开销 .flatMap { $0 } // 三维展开 .map(String.init(describing:)) // 安全类型转换 let keys = combinedStrings.joined(separator: "|") print(keys) let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$" guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { print("Failed to create regex") return nil } let range = NSRange(location: 0, length: input.utf16.count) guard let match = regex.firstMatch(in: input, options: [], range: range) else { print("No match found for input: \(input)") return nil } print("Regex matched for input: \(input)") let matchedString = (input as NSString).substring(with: match.range(at: 0)) let cmdStrings = matchedString.split(separator: "-").map { String($0) } guard !cmdStrings.isEmpty else { print("No key strings found in the matched string") return nil } var validCmdStrings: [String] = [] for key in cmdStrings { validCmdStrings.append(key) } if validCmdStrings.isEmpty { print("No valid key strings found in the matched string") return nil } for (index, key) in validCmdStrings.enumerated() { print("Valid Key \(index): \(key)") } return validCmdStrings } @objc public func addCommand(_ command: RemoteCommand) -> Bool { command.cmdString = command.cmdString.uppercased() // convert all letters to upper case if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString) if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false commands.append(command) saveCommands() viewController?.reloadTableView() // don't know why but this reload has to be called from the CommandManager, doesn't work by calling it in the viewcontroller, probably related with the dialog box. return true } @objc public func deleteCommand(at index: Int) { guard index >= 0 && index < commands.count else { return } commands.remove(at: index) saveCommands() } @objc public func getAllCommands() -> [RemoteCommand] { return commands } private func loadCommands() { if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") { do { // Attempt to unarchive the data into an array of RemoteCommand if let savedCommands = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, RemoteCommand.self], from: savedCommandsData) as? [RemoteCommand] { // Assign the unarchived commands to your property print(" Assign the unarchived commands to your property ") commands = savedCommands } else { // Handle the case where the data could not be unarchived into the expected type print("Data could not be unarchived into [RemoteCommand]") } } catch { // Handle any errors that occur during unarchiving print("Failed to unarchive savedCommands with error: \(error)") } } } private func saveCommands() { if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) { UserDefaults.standard.set(data, forKey: "savedCommands") } } @objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming // 如果已处理完所有按键,则开始释放按键 guard index < keyboardCmdStrings.count else { // 释放按键 for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键 if let keyCode = CommandManager.keyboardButtonMappings[keyStr] { LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键 } } return } // 获取当前按键的映射值 if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] { // 发送当前按键的按下事件 LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0) // 延迟后递归处理下一个按键 DispatchQueue.main.asyncAfter(deadline: .now() + delay) { self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1) } } else { print("No mapping found for \(keyboardCmdStrings[index])") // 如果当前按键没有映射,跳过当前按键并继续下一个 self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1) } } @objc public func sendKeyComboDown(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming for keyStr in keyboardCmdStrings { if let keyCode = CommandManager.keyboardButtonMappings[keyStr] { LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0) // 释放按键 } } } @objc public func sendKeyComboUp(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming for keyStr in keyboardCmdStrings { if let keyCode = CommandManager.keyboardButtonMappings[keyStr] { LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键 } } return } }