diff --git a/README.md b/README.md index 82ebf9ebd..559899f22 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ pnpm clawdis gateway --force ### macOS Companion (Clawdis.app) - A menu bar app that can start/stop the Gateway, show health/presence, and provide a local ops UI. +- Instances UI shows friendly hardware model names (from the vendored MIT dataset under `apps/macos/Sources/Clawdis/Resources/DeviceModels/`). - **Voice Wake** (on-device speech recognition) and Push-to-talk overlay. - **WebChat** embed + debug tooling (logs, status, heartbeats, sessions). - Hosts **PeekabooBridge** for UI automation brokering (for clawd workflows). diff --git a/apps/macos/Package.swift b/apps/macos/Package.swift index 9c91d0a9a..c70fb0ab2 100644 --- a/apps/macos/Package.swift +++ b/apps/macos/Package.swift @@ -50,8 +50,9 @@ let package = Package( ], resources: [ .copy("Resources/Clawdis.icns"), - .copy("Resources/WebChat"), .copy("Resources/CanvasA2UI"), + .copy("Resources/WebChat"), + .copy("Resources/DeviceModels"), ], swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), diff --git a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift index 86d2a3860..d563a194a 100644 --- a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift +++ b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift @@ -6,15 +6,18 @@ struct DevicePresentation: Sendable { } enum DeviceModelCatalog { + private static let modelIdentifierToName: [String: String] = loadModelIdentifierToName() + static func presentation(deviceFamily: String?, modelIdentifier: String?) -> DevicePresentation? { let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let modelEntry = model.isEmpty ? nil : modelIdentifierTable[model] - let symbol = modelEntry?.symbol ?? fallbackSymbol(for: family, modelIdentifier: model) + let friendlyName = model.isEmpty ? nil : modelIdentifierToName[model] + let symbol = symbolFor(modelIdentifier: model, friendlyName: friendlyName) + ?? fallbackSymbol(for: family, modelIdentifier: model) - let title = if let name = modelEntry?.name, !name.isEmpty { - name + let title = if let friendlyName, !friendlyName.isEmpty { + friendlyName } else if !family.isEmpty, !model.isEmpty { "\(family) (\(model))" } else if !family.isEmpty { @@ -29,6 +32,36 @@ enum DeviceModelCatalog { return DevicePresentation(title: title, symbol: symbol) } + private static func symbolFor(modelIdentifier rawModelIdentifier: String, friendlyName: String?) -> String? { + let modelIdentifier = rawModelIdentifier.trimmingCharacters(in: .whitespacesAndNewlines) + guard !modelIdentifier.isEmpty else { return nil } + + let lower = modelIdentifier.lowercased() + if lower.hasPrefix("ipad") { return "ipad" } + if lower.hasPrefix("iphone") { return "iphone" } + if lower.hasPrefix("ipod") { return "iphone" } + if lower.hasPrefix("watch") { return "applewatch" } + if lower.hasPrefix("appletv") { return "appletv" } + if lower.hasPrefix("audio") || lower.hasPrefix("homepod") { return "speaker" } + + if lower.hasPrefix("macbook") || lower.hasPrefix("macbookpro") || lower.hasPrefix("macbookair") { + return "laptopcomputer" + } + if lower.hasPrefix("imac") || lower.hasPrefix("macmini") || lower.hasPrefix("macpro") || lower.hasPrefix("macstudio") { + return "desktopcomputer" + } + + if lower.hasPrefix("mac"), let friendlyNameLower = friendlyName?.lowercased() { + if friendlyNameLower.contains("macbook") { return "laptopcomputer" } + if friendlyNameLower.contains("imac") { return "desktopcomputer" } + if friendlyNameLower.contains("mac mini") { return "desktopcomputer" } + if friendlyNameLower.contains("mac studio") { return "desktopcomputer" } + if friendlyNameLower.contains("mac pro") { return "desktopcomputer" } + } + + return nil + } + private static func fallbackSymbol(for familyRaw: String, modelIdentifier: String) -> String? { let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines) if family.isEmpty { return nil } @@ -49,21 +82,62 @@ enum DeviceModelCatalog { } } - private struct ModelEntry: Sendable { - let name: String - let symbol: String? + private static func loadModelIdentifierToName() -> [String: String] { + var combined: [String: String] = [:] + combined.merge(loadMapping(resourceName: "ios-device-identifiers"), uniquingKeysWith: { current, _ in current }) + combined.merge(loadMapping(resourceName: "mac-device-identifiers"), uniquingKeysWith: { current, _ in current }) + return combined } - // Friendly model names for a small set of known identifiers. - // Extend this table as needed; unknown identifiers fall back to the raw value. - private static let modelIdentifierTable: [String: ModelEntry] = [ - // iPad - "iPad16,5": .init(name: "iPad Pro 11-inch (M4)", symbol: "ipad"), - "iPad16,6": .init(name: "iPad Pro 13-inch (M4)", symbol: "ipad"), + private static func loadMapping(resourceName: String) -> [String: String] { + guard let url = Bundle.module.url( + forResource: resourceName, + withExtension: "json", + subdirectory: "DeviceModels") + else { + return [:] + } - // Mac - "Mac16,6": .init(name: "MacBook Pro (14-inch, 2024)", symbol: "laptopcomputer"), - "Mac16,8": .init(name: "MacBook Pro (16-inch, 2024)", symbol: "laptopcomputer"), - ] + do { + let data = try Data(contentsOf: url) + let decoded = try JSONDecoder().decode([String: NameValue].self, from: data) + return decoded.compactMapValues { $0.normalizedName } + } catch { + return [:] + } + } + + private enum NameValue: Decodable { + case string(String) + case stringArray([String]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let s = try? container.decode(String.self) { + self = .string(s) + return + } + if let arr = try? container.decode([String].self) { + self = .stringArray(arr) + return + } + throw DecodingError.typeMismatch( + String.self, + .init(codingPath: decoder.codingPath, debugDescription: "Expected string or string array")) + } + + var normalizedName: String? { + switch self { + case .string(let s): + let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + case .stringArray(let arr): + let values = arr + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !values.isEmpty else { return nil } + return values.joined(separator: " / ") + } + } + } } - diff --git a/apps/macos/Sources/Clawdis/InstanceIdentity.swift b/apps/macos/Sources/Clawdis/InstanceIdentity.swift index 35902ce00..58357854e 100644 --- a/apps/macos/Sources/Clawdis/InstanceIdentity.swift +++ b/apps/macos/Sources/Clawdis/InstanceIdentity.swift @@ -39,7 +39,8 @@ enum InstanceIdentity { var buffer = [CChar](repeating: 0, count: size) guard sysctlbyname("hw.model", &buffer, &size, nil, 0) == 0 else { return nil } - let s = String(cString: buffer).trimmingCharacters(in: .whitespacesAndNewlines) + let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) } + let s = String(decoding: bytes, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) return s.isEmpty ? nil : s }() } diff --git a/apps/macos/Sources/Clawdis/Resources/DeviceModels/LICENSE.apple-device-identifiers.txt b/apps/macos/Sources/Clawdis/Resources/DeviceModels/LICENSE.apple-device-identifiers.txt new file mode 100644 index 000000000..4592f65f5 --- /dev/null +++ b/apps/macos/Sources/Clawdis/Resources/DeviceModels/LICENSE.apple-device-identifiers.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Kyle Seongwoo Jun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/apps/macos/Sources/Clawdis/Resources/DeviceModels/NOTICE.md b/apps/macos/Sources/Clawdis/Resources/DeviceModels/NOTICE.md new file mode 100644 index 000000000..664e78d7b --- /dev/null +++ b/apps/macos/Sources/Clawdis/Resources/DeviceModels/NOTICE.md @@ -0,0 +1,9 @@ +# Apple device identifier mappings + +This directory includes model identifier → human-readable name mappings derived from the open-source project: + +- `kyle-seongwoo-jun/apple-device-identifiers` + - iOS mapping pinned to commit `8e7388b29da046183f5d976eb74dbb2f2acda955` + - macOS mapping pinned to commit `98ca75324f7a88c1649eb5edfc266ef47b7b8193` + +See `LICENSE.apple-device-identifiers.txt` for license terms. diff --git a/apps/macos/Sources/Clawdis/Resources/DeviceModels/ios-device-identifiers.json b/apps/macos/Sources/Clawdis/Resources/DeviceModels/ios-device-identifiers.json new file mode 100644 index 000000000..236ac2f0e --- /dev/null +++ b/apps/macos/Sources/Clawdis/Resources/DeviceModels/ios-device-identifiers.json @@ -0,0 +1,176 @@ +{ + "i386": "iPhone Simulator", + "x86_64": "iPhone Simulator", + "arm64": "iPhone Simulator", + "iPhone1,1": "iPhone", + "iPhone1,2": "iPhone 3G", + "iPhone2,1": "iPhone 3GS", + "iPhone3,1": "iPhone 4", + "iPhone3,2": "iPhone 4", + "iPhone3,3": "iPhone 4", + "iPhone4,1": "iPhone 4s", + "iPhone5,1": "iPhone 5", + "iPhone5,2": "iPhone 5", + "iPhone5,3": "iPhone 5c", + "iPhone5,4": "iPhone 5c", + "iPhone6,1": "iPhone 5s", + "iPhone6,2": "iPhone 5s", + "iPhone7,1": "iPhone 6 Plus", + "iPhone7,2": "iPhone 6", + "iPhone8,1": "iPhone 6s", + "iPhone8,2": "iPhone 6s Plus", + "iPhone8,4": "iPhone SE (1st generation)", + "iPhone9,1": "iPhone 7", + "iPhone9,2": "iPhone 7 Plus", + "iPhone9,3": "iPhone 7", + "iPhone9,4": "iPhone 7 Plus", + "iPhone10,1": "iPhone 8", + "iPhone10,2": "iPhone 8 Plus", + "iPhone10,3": "iPhone X", + "iPhone10,4": "iPhone 8", + "iPhone10,5": "iPhone 8 Plus", + "iPhone10,6": "iPhone X", + "iPhone11,2": "iPhone XS", + "iPhone11,4": "iPhone XS Max", + "iPhone11,6": "iPhone XS Max", + "iPhone11,8": "iPhone XR", + "iPhone12,1": "iPhone 11", + "iPhone12,3": "iPhone 11 Pro", + "iPhone12,5": "iPhone 11 Pro Max", + "iPhone12,8": "iPhone SE (2nd generation)", + "iPhone13,1": "iPhone 12 mini", + "iPhone13,2": "iPhone 12", + "iPhone13,3": "iPhone 12 Pro", + "iPhone13,4": "iPhone 12 Pro Max", + "iPhone14,2": "iPhone 13 Pro", + "iPhone14,3": "iPhone 13 Pro Max", + "iPhone14,4": "iPhone 13 mini", + "iPhone14,5": "iPhone 13", + "iPhone14,6": "iPhone SE (3rd generation)", + "iPhone14,7": "iPhone 14", + "iPhone14,8": "iPhone 14 Plus", + "iPhone15,2": "iPhone 14 Pro", + "iPhone15,3": "iPhone 14 Pro Max", + "iPhone15,4": "iPhone 15", + "iPhone15,5": "iPhone 15 Plus", + "iPhone16,1": "iPhone 15 Pro", + "iPhone16,2": "iPhone 15 Pro Max", + "iPhone17,1": "iPhone 16 Pro", + "iPhone17,2": "iPhone 16 Pro Max", + "iPhone17,3": "iPhone 16", + "iPhone17,4": "iPhone 16 Plus", + "iPhone17,5": "iPhone 16e", + "iPhone18,1": "iPhone 17 Pro", + "iPhone18,2": "iPhone 17 Pro Max", + "iPhone18,3": "iPhone 17", + "iPhone18,4": "iPhone Air", + "iPad1,1": "iPad", + "iPad1,2": "iPad", + "iPad2,1": "iPad 2", + "iPad2,2": "iPad 2", + "iPad2,3": "iPad 2", + "iPad2,4": "iPad 2", + "iPad2,5": "iPad mini", + "iPad2,6": "iPad mini", + "iPad2,7": "iPad mini", + "iPad3,1": "iPad (3rd generation)", + "iPad3,2": "iPad (3rd generation)", + "iPad3,3": "iPad (3rd generation)", + "iPad3,4": "iPad (4th generation)", + "iPad3,5": "iPad (4th generation)", + "iPad3,6": "iPad (4th generation)", + "iPad4,1": "iPad Air", + "iPad4,2": "iPad Air", + "iPad4,3": "iPad Air", + "iPad4,4": "iPad mini 2", + "iPad4,5": "iPad mini 2", + "iPad4,6": "iPad mini 2", + "iPad4,7": "iPad mini 3", + "iPad4,8": "iPad mini 3", + "iPad4,9": "iPad mini 3", + "iPad5,1": "iPad mini 4", + "iPad5,2": "iPad mini 4", + "iPad5,3": "iPad Air 2", + "iPad5,4": "iPad Air 2", + "iPad6,3": "iPad Pro (9.7-inch)", + "iPad6,4": "iPad Pro (9.7-inch)", + "iPad6,7": "iPad Pro (12.9-inch)", + "iPad6,8": "iPad Pro (12.9-inch)", + "iPad6,11": "iPad (5th generation)", + "iPad6,12": "iPad (5th generation)", + "iPad7,1": "iPad Pro (12.9-inch) (2nd generation)", + "iPad7,2": "iPad Pro (12.9-inch) (2nd generation)", + "iPad7,3": "iPad Pro (10.5-inch)", + "iPad7,4": "iPad Pro (10.5-inch)", + "iPad7,5": "iPad (6th generation)", + "iPad7,6": "iPad (6th generation)", + "iPad7,11": "iPad (7th generation)", + "iPad7,12": "iPad (7th generation)", + "iPad8,1": "iPad Pro (11-inch)", + "iPad8,2": "iPad Pro (11-inch)", + "iPad8,3": "iPad Pro (11-inch)", + "iPad8,4": "iPad Pro (11-inch)", + "iPad8,5": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,6": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,7": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,8": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,9": "iPad Pro (11-inch) (2nd generation)", + "iPad8,10": "iPad Pro (11-inch) (2nd generation)", + "iPad8,11": "iPad Pro (12.9-inch) (4th generation)", + "iPad8,12": "iPad Pro (12.9-inch) (4th generation)", + "iPad11,1": "iPad mini (5th generation)", + "iPad11,2": "iPad mini (5th generation)", + "iPad11,3": "iPad Air (3rd generation)", + "iPad11,4": "iPad Air (3rd generation)", + "iPad11,6": "iPad (8th generation)", + "iPad11,7": "iPad (8th generation)", + "iPad12,1": "iPad (9th generation)", + "iPad12,2": "iPad (9th generation)", + "iPad13,1": "iPad Air (4th generation)", + "iPad13,2": "iPad Air (4th generation)", + "iPad13,4": "iPad Pro (11-inch) (3rd generation)", + "iPad13,5": "iPad Pro (11-inch) (3rd generation)", + "iPad13,6": "iPad Pro (11-inch) (3rd generation)", + "iPad13,7": "iPad Pro (11-inch) (3rd generation)", + "iPad13,8": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,9": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,10": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,11": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,16": "iPad Air (5th generation)", + "iPad13,17": "iPad Air (5th generation)", + "iPad13,18": "iPad (10th generation)", + "iPad13,19": "iPad (10th generation)", + "iPad14,1": "iPad mini (6th generation)", + "iPad14,2": "iPad mini (6th generation)", + "iPad14,3": "iPad Pro (11-inch) (4th generation)", + "iPad14,4": "iPad Pro (11-inch) (4th generation)", + "iPad14,5": "iPad Pro (12.9-inch) (6th generation)", + "iPad14,6": "iPad Pro (12.9-inch) (6th generation)", + "iPad14,8": "iPad Air 11-inch (M2)", + "iPad14,9": "iPad Air 11-inch (M2)", + "iPad14,10": "iPad Air 13-inch (M2)", + "iPad14,11": "iPad Air 13-inch (M2)", + "iPad15,3": "iPad Air 11-inch (M3)", + "iPad15,4": "iPad Air 11-inch (M3)", + "iPad15,5": "iPad Air 13-inch (M3)", + "iPad15,6": "iPad Air 13-inch (M3)", + "iPad15,7": "iPad (A16)", + "iPad15,8": "iPad (A16)", + "iPad16,1": "iPad mini (A17 Pro)", + "iPad16,2": "iPad mini (A17 Pro)", + "iPad16,3": "iPad Pro 11-inch (M4)", + "iPad16,4": "iPad Pro 11-inch (M4)", + "iPad16,5": "iPad Pro 13-inch (M4)", + "iPad16,6": "iPad Pro 13-inch (M4)", + "iPad17,1": "iPad Pro 11-inch (M5)", + "iPad17,2": "iPad Pro 11-inch (M5)", + "iPad17,3": "iPad Pro 13-inch (M5)", + "iPad17,4": "iPad Pro 13-inch (M5)", + "iPod1,1": "iPod touch", + "iPod2,1": "iPod touch (2nd generation)", + "iPod3,1": "iPod touch (3rd generation)", + "iPod4,1": "iPod touch (4th generation)", + "iPod5,1": "iPod touch (5th generation)", + "iPod7,1": "iPod touch (6th generation)", + "iPod9,1": "iPod touch (7th generation)" +} \ No newline at end of file diff --git a/apps/macos/Sources/Clawdis/Resources/DeviceModels/mac-device-identifiers.json b/apps/macos/Sources/Clawdis/Resources/DeviceModels/mac-device-identifiers.json new file mode 100644 index 000000000..2b7483581 --- /dev/null +++ b/apps/macos/Sources/Clawdis/Resources/DeviceModels/mac-device-identifiers.json @@ -0,0 +1,214 @@ +{ + "iMac9,1": [ + "iMac (20-inch, Early 2009)", + "iMac (24-inch, Early 2009)" + ], + "iMac10,1": [ + "iMac (21.5-inch, Late 2009)", + "iMac (27-inch, Late 2009)" + ], + "iMac11,2": "iMac (21.5-inch, Mid 2010)", + "iMac11,3": "iMac (27-inch, Mid 2010)", + "iMac12,1": "iMac (21.5-inch, Mid 2011)", + "iMac12,2": "iMac (27-inch, Mid 2011)", + "iMac13,1": "iMac (21.5-inch, Late 2012)", + "iMac13,2": "iMac (27-inch, Late 2012)", + "iMac14,1": "iMac (21.5-inch, Late 2013)", + "iMac14,2": "iMac (27-inch, Late 2013)", + "iMac14,4": "iMac (21.5-inch, Mid 2014)", + "iMac15,1": [ + "iMac (Retina 5K, 27-inch, Late 2014)", + "iMac (Retina 5K, 27-inch, Mid 2015)" + ], + "iMac16,1": "iMac (21.5-inch, Late 2015)", + "iMac16,2": "iMac (Retina 4K, 21.5-inch, Late 2015)", + "iMac17,1": "iMac (Retina 5K, 27-inch, Late 2015)", + "iMac18,1": "iMac (21.5-inch, 2017)", + "iMac18,2": "iMac (Retina 4K, 21.5-inch, 2017)", + "iMac18,3": "iMac (Retina 5K, 27-inch, 2017)", + "iMac19,1": "iMac (Retina 5K, 27-inch, 2019)", + "iMac19,2": "iMac (Retina 4K, 21.5-inch, 2019)", + "iMac20,1": "iMac (Retina 5K, 27-inch, 2020)", + "iMac20,2": "iMac (Retina 5K, 27-inch, 2020)", + "iMac21,1": "iMac (24-inch, M1, 2021)", + "iMac21,2": "iMac (24-inch, M1, 2021)", + "iMacPro1,1": "iMac Pro (2017)", + "Mac13,1": "Mac Studio (2022)", + "Mac13,2": "Mac Studio (2022)", + "Mac14,2": "MacBook Air (M2, 2022)", + "Mac14,3": "Mac mini (2023)", + "Mac14,5": "MacBook Pro (14-inch, 2023)", + "Mac14,6": "MacBook Pro (16-inch, 2023)", + "Mac14,7": "MacBook Pro (13-inch, M2, 2022)", + "Mac14,8": [ + "Mac Pro (2023)", + "Mac Pro (Rack, 2023)" + ], + "Mac14,9": "MacBook Pro (14-inch, 2023)", + "Mac14,10": "MacBook Pro (16-inch, 2023)", + "Mac14,12": "Mac mini (2023)", + "Mac14,13": "Mac Studio (2023)", + "Mac14,14": "Mac Studio (2023)", + "Mac14,15": "MacBook Air (15-inch, M2, 2023)", + "Mac15,3": "MacBook Pro (14-inch, Nov 2023)", + "Mac15,4": "iMac (24-inch, 2023, Two ports)", + "Mac15,5": "iMac (24-inch, 2023, Four ports)", + "Mac15,6": "MacBook Pro (14-inch, Nov 2023)", + "Mac15,7": "MacBook Pro (16-inch, Nov 2023)", + "Mac15,8": "MacBook Pro (14-inch, Nov 2023)", + "Mac15,9": "MacBook Pro (16-inch, Nov 2023)", + "Mac15,10": "MacBook Pro (14-inch, Nov 2023)", + "Mac15,11": "MacBook Pro (16-inch, Nov 2023)", + "Mac15,12": "MacBook Air (13-inch, M3, 2024)", + "Mac15,13": "MacBook Air (15-inch, M3, 2024)", + "Mac15,14": "Mac Studio (2025)", + "Mac16,1": "MacBook Pro (14-inch, 2024)", + "Mac16,2": "iMac (24-inch, 2024, Two ports)", + "Mac16,3": "iMac (24-inch, 2024, Four ports)", + "Mac16,5": "MacBook Pro (16-inch, 2024)", + "Mac16,6": "MacBook Pro (14-inch, 2024)", + "Mac16,7": "MacBook Pro (16-inch, 2024)", + "Mac16,8": "MacBook Pro (14-inch, 2024)", + "Mac16,9": "Mac Studio (2025)", + "Mac16,10": "Mac mini (2024)", + "Mac16,11": "Mac mini (2024)", + "Mac16,12": "MacBook Air (13-inch, M4, 2025)", + "Mac16,13": "MacBook Air (15-inch, M4, 2025)", + "Mac17,2": "MacBook Pro (14-inch, M5)", + "MacBook5,2": [ + "MacBook (13-inch, Early 2009)", + "MacBook (13-inch, Mid 2009)" + ], + "MacBook6,1": "MacBook (13-inch, Late 2009)", + "MacBook7,1": "MacBook (13-inch, Mid 2010)", + "MacBook8,1": "MacBook (Retina, 12-inch, Early 2015)", + "MacBook9,1": "MacBook (Retina, 12-inch, Early 2016)", + "MacBook10,1": "MacBook (Retina, 12-inch, 2017)", + "MacBookAir2,1": "MacBook Air (Mid 2009)", + "MacBookAir3,1": "MacBook Air (11-inch, Late 2010)", + "MacBookAir3,2": "MacBook Air (13-inch, Late 2010)", + "MacBookAir4,1": "MacBook Air (11-inch, Mid 2011)", + "MacBookAir4,2": "MacBook Air (13-inch, Mid 2011)", + "MacBookAir5,1": "MacBook Air (11-inch, Mid 2012)", + "MacBookAir5,2": "MacBook Air (13-inch, Mid 2012)", + "MacBookAir6,1": [ + "MacBook Air (11-inch, Early 2014)", + "MacBook Air (11-inch, Mid 2013)" + ], + "MacBookAir6,2": [ + "MacBook Air (13-inch, Early 2014)", + "MacBook Air (13-inch, Mid 2013)" + ], + "MacBookAir7,1": "MacBook Air (11-inch, Early 2015)", + "MacBookAir7,2": [ + "MacBook Air (13-inch, 2017)", + "MacBook Air (13-inch, Early 2015)" + ], + "MacBookAir8,1": "MacBook Air (Retina, 13-inch, 2018)", + "MacBookAir8,2": "MacBook Air (Retina, 13-inch, 2019)", + "MacBookAir9,1": "MacBook Air (Retina, 13-inch, 2020)", + "MacBookAir10,1": "MacBook Air (M1, 2020)", + "MacBookPro4,1": [ + "MacBook Pro (15-inch, Early 2008)", + "MacBook Pro (17-inch, Early 2008)" + ], + "MacBookPro5,1": "MacBook Pro (15-inch, Late 2008)", + "MacBookPro5,2": [ + "MacBook Pro (17-inch, Early 2009)", + "MacBook Pro (17-inch, Mid 2009)" + ], + "MacBookPro5,3": [ + "MacBook Pro (15-inch, 2.53GHz, Mid 2009)", + "MacBook Pro (15-inch, Mid 2009)" + ], + "MacBookPro5,5": "MacBook Pro (13-inch, Mid 2009)", + "MacBookPro6,1": "MacBook Pro (17-inch, Mid 2010)", + "MacBookPro6,2": "MacBook Pro (15-inch, Mid 2010)", + "MacBookPro7,1": "MacBook Pro (13-inch, Mid 2010)", + "MacBookPro8,1": [ + "MacBook Pro (13-inch, Early 2011)", + "MacBook Pro (13-inch, Late 2011)" + ], + "MacBookPro8,2": [ + "MacBook Pro (15-inch, Early 2011)", + "MacBook Pro (15-inch, Late 2011)" + ], + "MacBookPro8,3": [ + "MacBook Pro (17-inch, Early 2011)", + "MacBook Pro (17-inch, Late 2011)" + ], + "MacBookPro9,1": "MacBook Pro (15-inch, Mid 2012)", + "MacBookPro9,2": "MacBook Pro (13-inch, Mid 2012)", + "MacBookPro10,1": [ + "MacBook Pro (Retina, 15-inch, Early 2013)", + "MacBook Pro (Retina, 15-inch, Mid 2012)" + ], + "MacBookPro10,2": [ + "MacBook Pro (Retina, 13-inch, Early 2013)", + "MacBook Pro (Retina, 13-inch, Late 2012)" + ], + "MacBookPro11,1": [ + "MacBook Pro (Retina, 13-inch, Late 2013)", + "MacBook Pro (Retina, 13-inch, Mid 2014)" + ], + "MacBookPro11,2": [ + "MacBook Pro (Retina, 15-inch, Late 2013)", + "MacBook Pro (Retina, 15-inch, Mid 2014)" + ], + "MacBookPro11,3": [ + "MacBook Pro (Retina, 15-inch, Late 2013)", + "MacBook Pro (Retina, 15-inch, Mid 2014)" + ], + "MacBookPro11,4": "MacBook Pro (Retina, 15-inch, Mid 2015)", + "MacBookPro11,5": "MacBook Pro (Retina, 15-inch, Mid 2015)", + "MacBookPro12,1": "MacBook Pro (Retina, 13-inch, Early 2015)", + "MacBookPro13,1": "MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)", + "MacBookPro13,2": "MacBook Pro (13-inch, 2016, Four Thunderbolt 3 ports)", + "MacBookPro13,3": "MacBook Pro (15-inch, 2016)", + "MacBookPro14,1": "MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)", + "MacBookPro14,2": "MacBook Pro (13-inch, 2017, Four Thunderbolt 3 ports)", + "MacBookPro14,3": "MacBook Pro (15-inch, 2017)", + "MacBookPro15,1": [ + "MacBook Pro (15-inch, 2018)", + "MacBook Pro (15-inch, 2019)" + ], + "MacBookPro15,2": [ + "MacBook Pro (13-inch, 2018, Four Thunderbolt 3 ports)", + "MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports)" + ], + "MacBookPro15,3": "MacBook Pro (15-inch, 2019)", + "MacBookPro15,4": "MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)", + "MacBookPro16,1": "MacBook Pro (16-inch, 2019)", + "MacBookPro16,2": "MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)", + "MacBookPro16,3": "MacBook Pro (13-inch, 2020, Two Thunderbolt 3 ports)", + "MacBookPro16,4": "MacBook Pro (16-inch, 2019)", + "MacBookPro17,1": "MacBook Pro (13-inch, M1, 2020)", + "MacBookPro18,1": "MacBook Pro (16-inch, 2021)", + "MacBookPro18,2": "MacBook Pro (16-inch, 2021)", + "MacBookPro18,3": "MacBook Pro (14-inch, 2021)", + "MacBookPro18,4": "MacBook Pro (14-inch, 2021)", + "Macmini3,1": [ + "Mac mini (Early 2009)", + "Mac mini (Late 2009)" + ], + "Macmini4,1": "Mac mini (Mid 2010)", + "Macmini5,1": "Mac mini (Mid 2011)", + "Macmini5,2": "Mac mini (Mid 2011)", + "Macmini6,1": "Mac mini (Late 2012)", + "Macmini6,2": "Mac mini (Late 2012)", + "Macmini7,1": "Mac mini (Late 2014)", + "Macmini8,1": "Mac mini (2018)", + "Macmini9,1": "Mac mini (M1, 2020)", + "MacPro4,1": "Mac Pro (Early 2009)", + "MacPro5,1": [ + "Mac Pro (Mid 2010)", + "Mac Pro (Mid 2012)", + "Mac Pro Server (Mid 2010)", + "Mac Pro Server (Mid 2012)" + ], + "MacPro6,1": "Mac Pro (Late 2013)", + "MacPro7,1": [ + "Mac Pro (2019)", + "Mac Pro (Rack, 2019)" + ] +} \ No newline at end of file