|
@@ -0,0 +1,138 @@
|
|
|
|
+//
|
|
|
|
+// Copyright © 2025 Turing Software, LLC. 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 CocoaSpice
|
|
|
|
+
|
|
|
|
+private let kDelayNs: UInt64 = 20000000
|
|
|
|
+
|
|
|
|
+@objc extension UTMScriptingVirtualMachineImpl {
|
|
|
|
+ @nonobjc private var primaryInput: CSInput {
|
|
|
|
+ get throws {
|
|
|
|
+ guard vm.state == .started else {
|
|
|
|
+ throw ScriptingError.notRunning
|
|
|
|
+ }
|
|
|
|
+ guard let ioService = (vm as? any UTMSpiceVirtualMachine)?.ioService else {
|
|
|
|
+ throw ScriptingError.operationNotSupported
|
|
|
|
+ }
|
|
|
|
+ guard let input = ioService.primaryInput else {
|
|
|
|
+ throw ScriptingError.operationNotAvailable
|
|
|
|
+ }
|
|
|
|
+ return input
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @objc func sendScanCode(_ command: NSScriptCommand) {
|
|
|
|
+ let scanCodes = command.evaluatedArguments?["codes"] as? [Int]
|
|
|
|
+ withScriptCommand(command) { [self] in
|
|
|
|
+ guard let scanCodes = scanCodes else {
|
|
|
|
+ throw ScriptingError.invalidParameter
|
|
|
|
+ }
|
|
|
|
+ let input = try self.primaryInput
|
|
|
|
+ for scanCode in scanCodes {
|
|
|
|
+ var _scanCode = scanCode
|
|
|
|
+ if (_scanCode & 0xFF00) == 0xE000 {
|
|
|
|
+ _scanCode = 0x100 | (_scanCode & 0xFF)
|
|
|
|
+ }
|
|
|
|
+ if (_scanCode & 0x80) == 0x80 {
|
|
|
|
+ input.send(.release, code: Int32(_scanCode & 0x17F))
|
|
|
|
+ } else {
|
|
|
|
+ input.send(.press, code: Int32(_scanCode))
|
|
|
|
+ }
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @objc func sendKeystroke(_ command: NSScriptCommand) {
|
|
|
|
+ let keystrokes = command.evaluatedArguments?["keystrokes"] as? String
|
|
|
|
+ let _modifiers = command.evaluatedArguments?["modifiers"] as? [AEKeyword] ?? []
|
|
|
|
+ let modifiers = _modifiers.map({ UTMScriptingModifierKey(rawValue: $0)! })
|
|
|
|
+ withScriptCommand(command) { [self] in
|
|
|
|
+ func scanCodeToSpice(_ scanCode: Int) -> Int32 {
|
|
|
|
+ var keyCode = scanCode
|
|
|
|
+ if (keyCode & 0xFF00) == 0xE000 {
|
|
|
|
+ keyCode = (keyCode & 0xFF) | 0x100
|
|
|
|
+ }
|
|
|
|
+ return Int32(keyCode)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ guard let keystrokes = keystrokes else {
|
|
|
|
+ throw ScriptingError.invalidParameter
|
|
|
|
+ }
|
|
|
|
+ let input = try self.primaryInput
|
|
|
|
+ for modifier in modifiers {
|
|
|
|
+ input.send(.press, code: modifier.toSpiceKeyCode())
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ }
|
|
|
|
+ let keyboardMap = VMKeyboardMap()
|
|
|
|
+ await keyboardMap.mapText(keystrokes) { scanCode in
|
|
|
|
+ input.send(.release, code: scanCodeToSpice(scanCode))
|
|
|
|
+ } keyDown: { scanCode in
|
|
|
|
+ input.send(.press, code: scanCodeToSpice(scanCode))
|
|
|
|
+ }
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ for modifier in modifiers {
|
|
|
|
+ input.send(.release, code: modifier.toSpiceKeyCode())
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @objc func sendMouseClick(_ command: NSScriptCommand) {
|
|
|
|
+ let coordinate = command.evaluatedArguments?["coordinate"] as? [Int]
|
|
|
|
+ let _mouseButton = command.evaluatedArguments?["button"] as? AEKeyword ?? UTMScriptingMouseButton.left.rawValue
|
|
|
|
+ let mouseButton = UTMScriptingMouseButton(rawValue: _mouseButton)!
|
|
|
|
+ let monitorNumber = command.evaluatedArguments?["monitor"] as? Int ?? 1
|
|
|
|
+ withScriptCommand(command) { [self] in
|
|
|
|
+ guard let coordinate = coordinate, coordinate.count == 2 else {
|
|
|
|
+ throw ScriptingError.invalidParameter
|
|
|
|
+ }
|
|
|
|
+ let xPosition = coordinate[0]
|
|
|
|
+ let yPosition = coordinate[1]
|
|
|
|
+ let input = try self.primaryInput
|
|
|
|
+ try await (vm as! UTMQemuVirtualMachine).changeInputTablet(true)
|
|
|
|
+ input.sendMousePosition(mouseButton.toSpiceButton(), absolutePoint: CGPoint(x: xPosition, y: yPosition), forMonitorID: monitorNumber-1)
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ input.sendMouseButton(mouseButton.toSpiceButton(), pressed: true)
|
|
|
|
+ try await Task.sleep(nanoseconds: kDelayNs)
|
|
|
|
+ input.sendMouseButton(mouseButton.toSpiceButton(), pressed: false)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+private extension UTMScriptingModifierKey {
|
|
|
|
+ func toSpiceKeyCode() -> Int32 {
|
|
|
|
+ switch self {
|
|
|
|
+ case .capsLock: return 0x3a
|
|
|
|
+ case .shift: return 0x2a
|
|
|
|
+ case .control: return 0x1d
|
|
|
|
+ case .option: return 0x38
|
|
|
|
+ case .command: return 0x15b
|
|
|
|
+ case .escape: return 0x01
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+private extension UTMScriptingMouseButton {
|
|
|
|
+ func toSpiceButton() -> CSInputButton {
|
|
|
|
+ switch self {
|
|
|
|
+ case .left: return .left
|
|
|
|
+ case .right: return .right
|
|
|
|
+ case .middle: return .middle
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|