serve-sim 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ import Foundation
2
+
3
+ /// Environment policy for the optional Xcode 27 Device Hub keyboard route.
4
+ public enum DeviceHubKeyboardConfiguration {
5
+ /// Only the documented value `1` opts out. Values such as `0` and `false`
6
+ /// leave the route enabled.
7
+ public static func isDisabled(environmentValue: String?) -> Bool {
8
+ environmentValue == "1"
9
+ }
10
+ }
11
+
12
+ public struct DeviceHubWindow: Equatable, Sendable {
13
+ public let processIdentifier: Int32
14
+ public let windowNumber: UInt32
15
+ public let name: String
16
+ public let layer: Int
17
+ public let isOnScreen: Bool
18
+
19
+ public init(
20
+ processIdentifier: Int32,
21
+ windowNumber: UInt32,
22
+ name: String,
23
+ layer: Int = 0,
24
+ isOnScreen: Bool = true
25
+ ) {
26
+ self.processIdentifier = processIdentifier
27
+ self.windowNumber = windowNumber
28
+ self.name = name
29
+ self.layer = layer
30
+ self.isOnScreen = isOnScreen
31
+ }
32
+ }
33
+
34
+ public enum DeviceHubWindowRoutingFailure: Error, Equatable, Sendable, CustomStringConvertible {
35
+ case noEligibleWindows
36
+ case targetNotFound
37
+ case ambiguousTarget(count: Int)
38
+ case targetNotFocused(focusedName: String)
39
+
40
+ public var description: String {
41
+ switch self {
42
+ case .noEligibleWindows:
43
+ return "Device Hub has no visible simulator window"
44
+ case .targetNotFound:
45
+ return "the target simulator window is not visible"
46
+ case .ambiguousTarget(let count):
47
+ return "the target simulator name matches \(count) windows"
48
+ case .targetNotFocused(let focusedName):
49
+ return "another Device Hub window has keyboard focus (\(focusedName))"
50
+ }
51
+ }
52
+ }
53
+
54
+ /// Chooses a Device Hub simulator window from a key-window-first snapshot. A
55
+ /// simulator name is not globally unique, so duplicate matches are deliberately
56
+ /// rejected instead of risking keyboard input in the wrong guest.
57
+ public enum DeviceHubWindowRouter {
58
+ public static func route(
59
+ windows: [DeviceHubWindow],
60
+ processIdentifier: Int32,
61
+ targetWindowTitle: String
62
+ ) -> Result<DeviceHubWindow, DeviceHubWindowRoutingFailure> {
63
+ let eligible = windows.filter {
64
+ $0.processIdentifier == processIdentifier
65
+ && $0.layer == 0
66
+ && $0.isOnScreen
67
+ && !$0.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
68
+ }
69
+ guard let focused = eligible.first else { return .failure(.noEligibleWindows) }
70
+
71
+ // Device Hub's AX title is canonical (`<device> – <runtime>`). Match
72
+ // it exactly: simulator names are user-controlled and prefix matching
73
+ // could route input to a different device with a longer name.
74
+ let matches = eligible.filter { $0.name == targetWindowTitle }
75
+ guard !matches.isEmpty else { return .failure(.targetNotFound) }
76
+ guard matches.count == 1 else { return .failure(.ambiguousTarget(count: matches.count)) }
77
+ guard matches[0].windowNumber == focused.windowNumber else {
78
+ return .failure(.targetNotFocused(focusedName: focused.name))
79
+ }
80
+ return .success(matches[0])
81
+ }
82
+ }
83
+
84
+ public enum HIDModifier: Equatable, Sendable {
85
+ case control
86
+ case shift
87
+ case option
88
+ case command
89
+ }
90
+
91
+ /// USB HID Keyboard/Keypad usage (page 0x07) to macOS virtual key code.
92
+ public enum HIDKeyboardMapping {
93
+ public static func macVirtualKeyCode(for usage: UInt32) -> UInt16? {
94
+ keyCodes[usage]
95
+ }
96
+
97
+ public static func modifier(for usage: UInt32) -> HIDModifier? {
98
+ switch usage {
99
+ case 0xE0, 0xE4: return .control
100
+ case 0xE1, 0xE5: return .shift
101
+ case 0xE2, 0xE6: return .option
102
+ case 0xE3, 0xE7: return .command
103
+ default: return nil
104
+ }
105
+ }
106
+
107
+ private static let keyCodes: [UInt32: UInt16] = [
108
+ // A-Z
109
+ 0x04: 0, 0x05: 11, 0x06: 8, 0x07: 2, 0x08: 14, 0x09: 3,
110
+ 0x0A: 5, 0x0B: 4, 0x0C: 34, 0x0D: 38, 0x0E: 40, 0x0F: 37,
111
+ 0x10: 46, 0x11: 45, 0x12: 31, 0x13: 35, 0x14: 12, 0x15: 15,
112
+ 0x16: 1, 0x17: 17, 0x18: 32, 0x19: 9, 0x1A: 13, 0x1B: 7,
113
+ 0x1C: 16, 0x1D: 6,
114
+
115
+ // Number row
116
+ 0x1E: 18, 0x1F: 19, 0x20: 20, 0x21: 21, 0x22: 23,
117
+ 0x23: 22, 0x24: 26, 0x25: 28, 0x26: 25, 0x27: 29,
118
+
119
+ // Editing and punctuation
120
+ 0x28: 36, 0x29: 53, 0x2A: 51, 0x2B: 48, 0x2C: 49,
121
+ 0x2D: 27, 0x2E: 24, 0x2F: 33, 0x30: 30, 0x31: 42,
122
+ 0x32: 42, 0x33: 41, 0x34: 39, 0x35: 50, 0x36: 43,
123
+ 0x37: 47, 0x38: 44, 0x39: 57,
124
+
125
+ // Function keys
126
+ 0x3A: 122, 0x3B: 120, 0x3C: 99, 0x3D: 118, 0x3E: 96, 0x3F: 97,
127
+ 0x40: 98, 0x41: 100, 0x42: 101, 0x43: 109, 0x44: 103, 0x45: 111,
128
+ 0x46: 105, 0x47: 107, 0x48: 113,
129
+
130
+ // Navigation
131
+ 0x49: 114, 0x4A: 115, 0x4B: 116, 0x4C: 117, 0x4D: 119,
132
+ 0x4E: 121, 0x4F: 124, 0x50: 123, 0x51: 125, 0x52: 126,
133
+
134
+ // Numeric keypad
135
+ 0x53: 71, 0x54: 75, 0x55: 67, 0x56: 78, 0x57: 69, 0x58: 76,
136
+ 0x59: 83, 0x5A: 84, 0x5B: 85, 0x5C: 86, 0x5D: 87, 0x5E: 88,
137
+ 0x5F: 89, 0x60: 91, 0x61: 92, 0x62: 82, 0x63: 65,
138
+ 0x64: 10, 0x67: 81,
139
+
140
+ // Media keys commonly emitted by browsers
141
+ 0x7F: 74, 0x80: 72, 0x81: 73,
142
+
143
+ // Modifiers
144
+ 0xE0: 59, 0xE1: 56, 0xE2: 58, 0xE3: 55,
145
+ 0xE4: 62, 0xE5: 60, 0xE6: 61, 0xE7: 54,
146
+ ]
147
+ }
@@ -0,0 +1,55 @@
1
+ public struct FramebufferSurfaceSize: Equatable, Sendable {
2
+ public let width: Int
3
+ public let height: Int
4
+
5
+ public init(width: Int, height: Int) {
6
+ self.width = width
7
+ self.height = height
8
+ }
9
+
10
+ public var isLive: Bool { width > 0 && height > 0 }
11
+
12
+ fileprivate func matches(_ other: FramebufferSurfaceSize) -> Bool {
13
+ (width == other.width && height == other.height)
14
+ || (width == other.height && height == other.width)
15
+ }
16
+ }
17
+
18
+ public struct FramebufferSurfaceSelection: Equatable, Sendable {
19
+ public let index: Int
20
+ public let matchedExpectedSize: Bool
21
+
22
+ public init(index: Int, matchedExpectedSize: Bool) {
23
+ self.index = index
24
+ self.matchedExpectedSize = matchedExpectedSize
25
+ }
26
+ }
27
+
28
+ /// Selects the simulator's main display without relying on a maximum-size
29
+ /// heuristic. Newer Device Hub versions may publish an additional presentation
30
+ /// surface (for example 7680x4320 while resizing); the device type's native
31
+ /// screen size identifies the real display. When that private metadata is not
32
+ /// available, the historical largest-live-surface behavior is preserved.
33
+ public enum FramebufferSurfaceSelector {
34
+ public static func select(
35
+ from candidates: [FramebufferSurfaceSize],
36
+ expectedSize: FramebufferSurfaceSize?
37
+ ) -> FramebufferSurfaceSelection? {
38
+ let live = candidates.indices.filter { candidates[$0].isLive }
39
+ guard !live.isEmpty else { return nil }
40
+
41
+ if let expectedSize, expectedSize.isLive,
42
+ let exact = live.first(where: { candidates[$0].matches(expectedSize) }) {
43
+ return FramebufferSurfaceSelection(index: exact, matchedExpectedSize: true)
44
+ }
45
+
46
+ let largest = live.max { lhs, rhs in
47
+ area(of: candidates[lhs]) < area(of: candidates[rhs])
48
+ }!
49
+ return FramebufferSurfaceSelection(index: largest, matchedExpectedSize: false)
50
+ }
51
+
52
+ private static func area(of size: FramebufferSurfaceSize) -> Int64 {
53
+ Int64(size.width) * Int64(size.height)
54
+ }
55
+ }
@@ -0,0 +1,123 @@
1
+ import XCTest
2
+ @testable import SimNativeSupport
3
+
4
+ final class DeviceHubKeyboardRoutingTests: XCTestCase {
5
+ private let pid: Int32 = 42
6
+
7
+ func testKeyboardOptOutRequiresDocumentedValue() {
8
+ XCTAssertTrue(DeviceHubKeyboardConfiguration.isDisabled(environmentValue: "1"))
9
+ for value: String? in [nil, "", "0", "false", "true"] {
10
+ XCTAssertFalse(DeviceHubKeyboardConfiguration.isDisabled(environmentValue: value))
11
+ }
12
+ }
13
+
14
+ func testRoutesOnlyVisibleTargetWindow() throws {
15
+ let target = window(number: 10, name: "iPhone 17 Pro – iOS 27.0")
16
+ let routed = try DeviceHubWindowRouter.route(
17
+ windows: [target],
18
+ processIdentifier: pid,
19
+ targetWindowTitle: "iPhone 17 Pro – iOS 27.0"
20
+ ).get()
21
+
22
+ XCTAssertEqual(routed, target)
23
+ }
24
+
25
+ func testRoutesFocusedTargetAmongMultipleSimulatorWindows() throws {
26
+ let target = window(number: 10, name: "iPhone 17 Pro – iOS 27.0")
27
+ let other = window(number: 11, name: "iPad Air – iOS 27.0")
28
+ let routed = try DeviceHubWindowRouter.route(
29
+ windows: [target, other],
30
+ processIdentifier: pid,
31
+ targetWindowTitle: "iPhone 17 Pro – iOS 27.0"
32
+ ).get()
33
+
34
+ XCTAssertEqual(routed, target)
35
+ }
36
+
37
+ func testRejectsNonCanonicalAndLongerDeviceTitles() {
38
+ let result = DeviceHubWindowRouter.route(
39
+ windows: [
40
+ window(number: 10, name: "iPhone 17 Pro"),
41
+ window(number: 11, name: "iPhone 17 Pro Max – iOS 27.0"),
42
+ ],
43
+ processIdentifier: pid,
44
+ targetWindowTitle: "iPhone 17 Pro – iOS 27.0"
45
+ )
46
+
47
+ XCTAssertEqual(result.failure, .targetNotFound)
48
+ }
49
+
50
+ func testRejectsTargetThatDoesNotHaveDeviceHubFocus() {
51
+ let result = DeviceHubWindowRouter.route(
52
+ windows: [
53
+ window(number: 11, name: "iPad Air"),
54
+ window(number: 10, name: "iPhone 17 Pro – iOS 27.0"),
55
+ ],
56
+ processIdentifier: pid,
57
+ targetWindowTitle: "iPhone 17 Pro – iOS 27.0"
58
+ )
59
+
60
+ XCTAssertEqual(result.failure, .targetNotFocused(focusedName: "iPad Air"))
61
+ }
62
+
63
+ func testRejectsDuplicateDeviceNames() {
64
+ let result = DeviceHubWindowRouter.route(
65
+ windows: [
66
+ window(number: 10, name: "iPhone 17 Pro"),
67
+ window(number: 11, name: "iPhone 17 Pro"),
68
+ ],
69
+ processIdentifier: pid,
70
+ targetWindowTitle: "iPhone 17 Pro"
71
+ )
72
+
73
+ XCTAssertEqual(result.failure, .ambiguousTarget(count: 2))
74
+ }
75
+
76
+ func testIgnoresHiddenUtilityAndOtherProcessWindows() {
77
+ let result = DeviceHubWindowRouter.route(
78
+ windows: [
79
+ window(number: 1, name: "iPhone 17 Pro", layer: 1),
80
+ window(number: 2, name: "iPhone 17 Pro", isOnScreen: false),
81
+ DeviceHubWindow(
82
+ processIdentifier: 99,
83
+ windowNumber: 3,
84
+ name: "iPhone 17 Pro"
85
+ ),
86
+ ],
87
+ processIdentifier: pid,
88
+ targetWindowTitle: "iPhone 17 Pro – iOS 27.0"
89
+ )
90
+
91
+ XCTAssertEqual(result.failure, .noEligibleWindows)
92
+ }
93
+
94
+ func testKeyboardUsageMappingIncludesModifiersAndRejectsUnknownUsage() {
95
+ XCTAssertEqual(HIDKeyboardMapping.macVirtualKeyCode(for: 0x04), 0)
96
+ XCTAssertEqual(HIDKeyboardMapping.macVirtualKeyCode(for: 0x2A), 51)
97
+ XCTAssertEqual(HIDKeyboardMapping.modifier(for: 0xE1), .shift)
98
+ XCTAssertNil(HIDKeyboardMapping.macVirtualKeyCode(for: 0xFFFF))
99
+ XCTAssertNil(HIDKeyboardMapping.modifier(for: 0x04))
100
+ }
101
+
102
+ private func window(
103
+ number: UInt32,
104
+ name: String,
105
+ layer: Int = 0,
106
+ isOnScreen: Bool = true
107
+ ) -> DeviceHubWindow {
108
+ DeviceHubWindow(
109
+ processIdentifier: pid,
110
+ windowNumber: number,
111
+ name: name,
112
+ layer: layer,
113
+ isOnScreen: isOnScreen
114
+ )
115
+ }
116
+ }
117
+
118
+ private extension Result {
119
+ var failure: Failure? {
120
+ guard case .failure(let error) = self else { return nil }
121
+ return error
122
+ }
123
+ }
@@ -0,0 +1,68 @@
1
+ import XCTest
2
+ @testable import SimNativeSupport
3
+
4
+ final class FramebufferSurfaceSelectorTests: XCTestCase {
5
+ func testExpectedDeviceSurfaceBeatsLargerPresentationSurface() {
6
+ let candidates = [
7
+ FramebufferSurfaceSize(width: 7680, height: 4320),
8
+ FramebufferSurfaceSize(width: 0, height: 0),
9
+ FramebufferSurfaceSize(width: 1206, height: 2622),
10
+ ]
11
+
12
+ XCTAssertEqual(
13
+ FramebufferSurfaceSelector.select(
14
+ from: candidates,
15
+ expectedSize: FramebufferSurfaceSize(width: 1206, height: 2622)
16
+ ),
17
+ FramebufferSurfaceSelection(index: 2, matchedExpectedSize: true)
18
+ )
19
+ }
20
+
21
+ func testExpectedSizeMatchesRotatedSurface() {
22
+ XCTAssertEqual(
23
+ FramebufferSurfaceSelector.select(
24
+ from: [FramebufferSurfaceSize(width: 2622, height: 1206)],
25
+ expectedSize: FramebufferSurfaceSize(width: 1206, height: 2622)
26
+ ),
27
+ FramebufferSurfaceSelection(index: 0, matchedExpectedSize: true)
28
+ )
29
+ }
30
+
31
+ func testZeroSizedSurfacesAreIgnored() {
32
+ XCTAssertNil(
33
+ FramebufferSurfaceSelector.select(
34
+ from: [
35
+ FramebufferSurfaceSize(width: 0, height: 0),
36
+ FramebufferSurfaceSize(width: 1206, height: 0),
37
+ ],
38
+ expectedSize: FramebufferSurfaceSize(width: 1206, height: 2622)
39
+ )
40
+ )
41
+ }
42
+
43
+ func testMissingMetadataPreservesLargestSurfaceFallback() {
44
+ XCTAssertEqual(
45
+ FramebufferSurfaceSelector.select(
46
+ from: [
47
+ FramebufferSurfaceSize(width: 320, height: 240),
48
+ FramebufferSurfaceSize(width: 1179, height: 2556),
49
+ ],
50
+ expectedSize: nil
51
+ ),
52
+ FramebufferSurfaceSelection(index: 1, matchedExpectedSize: false)
53
+ )
54
+ }
55
+
56
+ func testNoExactMatchPreservesLargestSurfaceFallback() {
57
+ XCTAssertEqual(
58
+ FramebufferSurfaceSelector.select(
59
+ from: [
60
+ FramebufferSurfaceSize(width: 1000, height: 2000),
61
+ FramebufferSurfaceSize(width: 1100, height: 2200),
62
+ ],
63
+ expectedSize: FramebufferSurfaceSize(width: 1206, height: 2622)
64
+ ),
65
+ FramebufferSurfaceSelection(index: 1, matchedExpectedSize: false)
66
+ )
67
+ }
68
+ }