SIGN IN SIGN UP
utmapp / UTM UNCLAIMED

Virtual machines for iOS and macOS

33462 0 0 Swift
//
// 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.compactMap({ 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) ?? .left
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(), mask: [], pressed: true)
try await Task.sleep(nanoseconds: kDelayNs)
input.sendMouseButton(mouseButton.toSpiceButton(), mask: [], 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
}
}
}