UTMScriptingInputImpl.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. //
  2. // Copyright © 2025 Turing Software, LLC. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. import CocoaSpice
  18. private let kDelayNs: UInt64 = 20000000
  19. @objc extension UTMScriptingVirtualMachineImpl {
  20. @nonobjc private var primaryInput: CSInput {
  21. get throws {
  22. guard vm.state == .started else {
  23. throw ScriptingError.notRunning
  24. }
  25. guard let ioService = (vm as? any UTMSpiceVirtualMachine)?.ioService else {
  26. throw ScriptingError.operationNotSupported
  27. }
  28. guard let input = ioService.primaryInput else {
  29. throw ScriptingError.operationNotAvailable
  30. }
  31. return input
  32. }
  33. }
  34. @objc func sendScanCode(_ command: NSScriptCommand) {
  35. let scanCodes = command.evaluatedArguments?["codes"] as? [Int]
  36. withScriptCommand(command) { [self] in
  37. guard let scanCodes = scanCodes else {
  38. throw ScriptingError.invalidParameter
  39. }
  40. let input = try self.primaryInput
  41. for scanCode in scanCodes {
  42. var _scanCode = scanCode
  43. if (_scanCode & 0xFF00) == 0xE000 {
  44. _scanCode = 0x100 | (_scanCode & 0xFF)
  45. }
  46. if (_scanCode & 0x80) == 0x80 {
  47. input.send(.release, code: Int32(_scanCode & 0x17F))
  48. } else {
  49. input.send(.press, code: Int32(_scanCode))
  50. }
  51. try await Task.sleep(nanoseconds: kDelayNs)
  52. }
  53. }
  54. }
  55. @objc func sendKeystroke(_ command: NSScriptCommand) {
  56. let keystrokes = command.evaluatedArguments?["keystrokes"] as? String
  57. let _modifiers = command.evaluatedArguments?["modifiers"] as? [AEKeyword] ?? []
  58. let modifiers = _modifiers.compactMap({ UTMScriptingModifierKey(rawValue: $0) })
  59. withScriptCommand(command) { [self] in
  60. func scanCodeToSpice(_ scanCode: Int) -> Int32 {
  61. var keyCode = scanCode
  62. if (keyCode & 0xFF00) == 0xE000 {
  63. keyCode = (keyCode & 0xFF) | 0x100
  64. }
  65. return Int32(keyCode)
  66. }
  67. guard let keystrokes = keystrokes else {
  68. throw ScriptingError.invalidParameter
  69. }
  70. let input = try self.primaryInput
  71. for modifier in modifiers {
  72. input.send(.press, code: modifier.toSpiceKeyCode())
  73. try await Task.sleep(nanoseconds: kDelayNs)
  74. }
  75. let keyboardMap = VMKeyboardMap()
  76. await keyboardMap.mapText(keystrokes) { scanCode in
  77. input.send(.release, code: scanCodeToSpice(scanCode))
  78. } keyDown: { scanCode in
  79. input.send(.press, code: scanCodeToSpice(scanCode))
  80. }
  81. try await Task.sleep(nanoseconds: kDelayNs)
  82. for modifier in modifiers {
  83. input.send(.release, code: modifier.toSpiceKeyCode())
  84. try await Task.sleep(nanoseconds: kDelayNs)
  85. }
  86. }
  87. }
  88. @objc func sendMouseClick(_ command: NSScriptCommand) {
  89. let coordinate = command.evaluatedArguments?["coordinate"] as? [Int]
  90. let _mouseButton = command.evaluatedArguments?["button"] as? AEKeyword ?? UTMScriptingMouseButton.left.rawValue
  91. let mouseButton = UTMScriptingMouseButton(rawValue: _mouseButton) ?? .left
  92. let monitorNumber = command.evaluatedArguments?["monitor"] as? Int ?? 1
  93. withScriptCommand(command) { [self] in
  94. guard let coordinate = coordinate, coordinate.count == 2 else {
  95. throw ScriptingError.invalidParameter
  96. }
  97. let xPosition = coordinate[0]
  98. let yPosition = coordinate[1]
  99. let input = try self.primaryInput
  100. try await (vm as! UTMQemuVirtualMachine).changeInputTablet(true)
  101. input.sendMousePosition(mouseButton.toSpiceButton(), absolutePoint: CGPoint(x: xPosition, y: yPosition), forMonitorID: monitorNumber-1)
  102. try await Task.sleep(nanoseconds: kDelayNs)
  103. input.sendMouseButton(mouseButton.toSpiceButton(), pressed: true)
  104. try await Task.sleep(nanoseconds: kDelayNs)
  105. input.sendMouseButton(mouseButton.toSpiceButton(), pressed: false)
  106. }
  107. }
  108. }
  109. private extension UTMScriptingModifierKey {
  110. func toSpiceKeyCode() -> Int32 {
  111. switch self {
  112. case .capsLock: return 0x3a
  113. case .shift: return 0x2a
  114. case .control: return 0x1d
  115. case .option: return 0x38
  116. case .command: return 0x15b
  117. case .escape: return 0x01
  118. }
  119. }
  120. }
  121. private extension UTMScriptingMouseButton {
  122. func toSpiceButton() -> CSInputButton {
  123. switch self {
  124. case .left: return .left
  125. case .right: return .right
  126. case .middle: return .middle
  127. }
  128. }
  129. }