UTMInputIntent.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. //
  2. // Copyright © 2025 osy. 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 AppIntents
  17. private let kDelayNs: UInt64 = 20000000
  18. @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
  19. struct UTMSendScanCodeIntent: UTMIntent {
  20. static let title: LocalizedStringResource = "Send Scan Code"
  21. static let description = IntentDescription("Send a sequence of raw keyboard scan codes to the virtual machine. Only supported on QEMU backend.")
  22. static var parameterSummary: some ParameterSummary {
  23. Summary("Send scan code to \(\.$vmEntity)") {
  24. \.$scanCodes
  25. }
  26. }
  27. @Dependency
  28. var data: UTMData
  29. @Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
  30. var vmEntity: UTMVirtualMachineEntity
  31. @Parameter(title: "Scan Code", description: "List of PC AT scan codes in decimal (0-65535 inclusive).", controlStyle: .field, inclusiveRange: (0, 0xFFFF))
  32. var scanCodes: [Int]
  33. @MainActor
  34. func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
  35. guard let vm = vm as? any UTMSpiceVirtualMachine else {
  36. throw UTMIntentError.unsupportedBackend
  37. }
  38. guard let input = vm.ioService?.primaryInput else {
  39. throw UTMIntentError.inputHandlerNotAvailable
  40. }
  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. return .result()
  54. }
  55. }
  56. @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
  57. struct UTMSendKeystrokesIntent: UTMIntent {
  58. static let title: LocalizedStringResource = "Send Keystrokes"
  59. static let description = IntentDescription("Send text as a sequence of keystrokes to the virtual machine. Only supported on QEMU backend.")
  60. static var parameterSummary: some ParameterSummary {
  61. Summary("Send \(\.$keystrokes) to \(\.$vmEntity)") {
  62. \.$modifiers
  63. }
  64. }
  65. enum Modifier: Int, CaseIterable, AppEnum {
  66. case capsLock
  67. case shift
  68. case control
  69. case option
  70. case command
  71. case escape
  72. static let typeDisplayRepresentation: TypeDisplayRepresentation =
  73. TypeDisplayRepresentation(
  74. name: "Modifier Key"
  75. )
  76. static let caseDisplayRepresentations: [Modifier: DisplayRepresentation] = [
  77. .capsLock: DisplayRepresentation(title: "Caps Lock (⇪)"),
  78. .shift: DisplayRepresentation(title: "Shift (⇧)"),
  79. .control: DisplayRepresentation(title: "Control (⌃)"),
  80. .option: DisplayRepresentation(title: "Option (⌥)"),
  81. .command: DisplayRepresentation(title: "Command (⌘)"),
  82. .escape: DisplayRepresentation(title: "Escape (⎋)"),
  83. ]
  84. func toSpiceKeyCode() -> Int32 {
  85. switch self {
  86. case .capsLock: return 0x3a
  87. case .shift: return 0x2a
  88. case .control: return 0x1d
  89. case .option: return 0x38
  90. case .command: return 0x15b
  91. case .escape: return 0x01
  92. }
  93. }
  94. }
  95. @Dependency
  96. var data: UTMData
  97. @Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
  98. var vmEntity: UTMVirtualMachineEntity
  99. @Parameter(title: "Keystrokes", description: "Text will be converted to a sequence of keystrokes.")
  100. var keystrokes: String
  101. @Parameter(title: "Modifiers", description: "The modifier keys will be held down while the keystroke sequence is sent.", default: [])
  102. var modifiers: [Modifier]
  103. @MainActor
  104. func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
  105. guard let vm = vm as? any UTMSpiceVirtualMachine else {
  106. throw UTMIntentError.unsupportedBackend
  107. }
  108. guard let input = vm.ioService?.primaryInput else {
  109. throw UTMIntentError.inputHandlerNotAvailable
  110. }
  111. for modifier in modifiers {
  112. input.send(.press, code: modifier.toSpiceKeyCode())
  113. try await Task.sleep(nanoseconds: kDelayNs)
  114. }
  115. let keyboardMap = VMKeyboardMap()
  116. await keyboardMap.mapText(keystrokes) { scanCode in
  117. input.send(.release, code: scanCodeToSpice(scanCode))
  118. } keyDown: { scanCode in
  119. input.send(.press, code: scanCodeToSpice(scanCode))
  120. }
  121. try await Task.sleep(nanoseconds: kDelayNs)
  122. for modifier in modifiers {
  123. input.send(.release, code: modifier.toSpiceKeyCode())
  124. try await Task.sleep(nanoseconds: kDelayNs)
  125. }
  126. return .result()
  127. }
  128. private func scanCodeToSpice(_ scanCode: Int) -> Int32 {
  129. var keyCode = scanCode
  130. if (keyCode & 0xFF00) == 0xE000 {
  131. keyCode = (keyCode & 0xFF) | 0x100
  132. }
  133. return Int32(keyCode)
  134. }
  135. }
  136. @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
  137. struct UTMMouseClickIntent: UTMIntent {
  138. static let title: LocalizedStringResource = "Send Mouse Click"
  139. static let description = IntentDescription("Send a mouse position and click to the virtual machine. Only supported on QEMU backend.")
  140. static var parameterSummary: some ParameterSummary {
  141. Summary("Send mouse click at (\(\.$xPosition), \(\.$yPosition)) to \(\.$vmEntity)") {
  142. \.$mouseButton
  143. \.$monitorNumber
  144. }
  145. }
  146. enum MouseButton: Int, CaseIterable, AppEnum {
  147. case left
  148. case right
  149. case middle
  150. static let typeDisplayRepresentation: TypeDisplayRepresentation =
  151. TypeDisplayRepresentation(
  152. name: "Mouse Button"
  153. )
  154. static let caseDisplayRepresentations: [MouseButton: DisplayRepresentation] = [
  155. .left: DisplayRepresentation(title: "Left"),
  156. .right: DisplayRepresentation(title: "Right"),
  157. .middle: DisplayRepresentation(title: "Middle"),
  158. ]
  159. func toSpiceButton() -> CSInputButton {
  160. switch self {
  161. case .left: return .left
  162. case .right: return .right
  163. case .middle: return .middle
  164. }
  165. }
  166. }
  167. @Dependency
  168. var data: UTMData
  169. @Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
  170. var vmEntity: UTMVirtualMachineEntity
  171. @Parameter(title: "X Position", description: "X coordinate of the absolute position.", default: 0, controlStyle: .field)
  172. var xPosition: Int
  173. @Parameter(title: "Y Position", description: "Y coordinate of the absolute position.", default: 0, controlStyle: .field)
  174. var yPosition: Int
  175. @Parameter(title: "Mouse Button", description: "Mouse button to click.", default: .left)
  176. var mouseButton: MouseButton
  177. @Parameter(title: "Monitor Number", description: "Which monitor to target (starting at 1).", default: 1, controlStyle: .stepper)
  178. var monitorNumber: Int
  179. @MainActor
  180. func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
  181. guard let vm = vm as? UTMQemuVirtualMachine else {
  182. throw UTMIntentError.unsupportedBackend
  183. }
  184. guard let input = vm.ioService?.primaryInput else {
  185. throw UTMIntentError.inputHandlerNotAvailable
  186. }
  187. try await vm.changeInputTablet(true)
  188. input.sendMousePosition(mouseButton.toSpiceButton(), absolutePoint: CGPoint(x: xPosition, y: yPosition), forMonitorID: monitorNumber-1)
  189. try await Task.sleep(nanoseconds: kDelayNs)
  190. input.sendMouseButton(mouseButton.toSpiceButton(), mask: [], pressed: true)
  191. try await Task.sleep(nanoseconds: kDelayNs)
  192. input.sendMouseButton(mouseButton.toSpiceButton(), mask: [], pressed: false)
  193. return .result()
  194. }
  195. }