Browse Source

connect: show model of Mac

osy 1 năm trước cách đây
mục cha
commit
fd4f17312c

+ 3 - 2
Platform/Shared/ContentView.swift

@@ -20,9 +20,10 @@ import UniformTypeIdentifiers
 import IQKeyboardManagerSwift
 #endif
 
-#if WITH_QEMU_TCI
+// on visionOS, there is no text to show more than UTM
+#if WITH_QEMU_TCI && !os(visionOS)
 let productName = "UTM SE"
-#elseif WITH_REMOTE
+#elseif WITH_REMOTE && !os(visionOS)
 let productName = "UTM Remote"
 #else
 let productName = "UTM"

+ 111 - 0
Platform/Shared/MacDeviceLabel.swift

@@ -0,0 +1,111 @@
+//
+// Copyright © 2024 osy. 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 SwiftUI
+import UniformTypeIdentifiers
+
+struct MacDeviceLabel<Title>: View where Title : StringProtocol {
+    let title: Title
+    let device: MacDevice
+
+    init(_ title: Title, device macDevice: MacDevice) {
+        self.title = title
+        self.device = macDevice
+    }
+
+    var body: some View {
+        Label(title, systemImage: device.symbolName)
+    }
+}
+
+// credits: https://adamdemasi.com/2023/04/15/mac-device-icon-by-device-class.html
+
+private extension UTTagClass {
+    static let deviceModelCode = UTTagClass(rawValue: "com.apple.device-model-code")
+}
+
+private extension UTType {
+    static let macBook          = UTType("com.apple.mac.laptop")
+    static let macBookWithNotch = UTType("com.apple.mac.notched-laptop")
+    static let macMini          = UTType("com.apple.macmini")
+    static let macStudio        = UTType("com.apple.macstudio")
+    static let iMac             = UTType("com.apple.imac")
+    static let macPro           = UTType("com.apple.macpro")
+    static let macPro2013       = UTType("com.apple.macpro-cylinder")
+    static let macPro2019       = UTType("com.apple.macpro-2019")
+}
+
+struct MacDevice {
+    let model: String
+    let symbolName: String
+
+    #if os(macOS)
+    static let current: Self = {
+        let key = "hw.model"
+        var size = size_t()
+        sysctlbyname(key, nil, &size, nil, 0)
+        let value = malloc(size)
+        defer {
+            value?.deallocate()
+        }
+        sysctlbyname(key, value, &size, nil, 0)
+        guard let cChar = value?.bindMemory(to: CChar.self, capacity: size) else {
+            return Self(model: "Unknown")
+        }
+        return Self(model: String(cString: cChar))
+    }()
+    #endif
+
+    init(model: String?) {
+        self.model = model ?? "Unknown"
+        self.symbolName = Self.symbolName(from: self.model)
+    }
+
+    private static func checkModel(_ model: String, conformsTo type: UTType?) -> Bool {
+        guard let type else {
+            return false
+        }
+        return UTType(tag: model, tagClass: .deviceModelCode, conformingTo: nil)?.conforms(to: type) ?? false
+    }
+
+    private static func symbolName(from model: String) -> String {
+        if checkModel(model, conformsTo: .macBookWithNotch),
+            #available(macOS 14, iOS 17, macCatalyst 17, tvOS 17, watchOS 10, *) {
+            // macbook.gen2 was added with SF Symbols 5.0 (macOS Sonoma, 2023), but MacBooks with a notch
+            // were released in 2021!
+            return "macbook.gen2"
+        } else if checkModel(model, conformsTo: .macBook) {
+            return "laptopcomputer"
+        } else if checkModel(model, conformsTo: .macMini) {
+            return "macmini"
+        } else if checkModel(model, conformsTo: .macStudio) {
+            return "macstudio"
+        } else if checkModel(model, conformsTo: .iMac) {
+            return "desktopcomputer"
+        } else if checkModel(model, conformsTo: .macPro2019) {
+            return "macpro.gen3"
+        } else if checkModel(model, conformsTo: .macPro2013) {
+            return "macpro.gen2"
+        } else if checkModel(model, conformsTo: .macPro) {
+            return "macpro"
+        }
+        return "display"
+    }
+}
+
+#Preview {
+    MacDeviceLabel("MacBook", device: MacDevice(model: "Mac14,6"))
+}

+ 25 - 24
Platform/iOS/UTMRemoteConnectView.swift

@@ -23,10 +23,6 @@ struct UTMRemoteConnectView: View {
     @State private var selectedServer: UTMRemoteClient.State.Server?
     @State private var isAutoConnect: Bool = false
 
-    private var idiom: UIUserInterfaceIdiom {
-        UIDevice.current.userInterfaceIdiom
-    }
-
     private var remoteClient: UTMRemoteClient {
         data.remoteClient
     }
@@ -36,6 +32,9 @@ struct UTMRemoteConnectView: View {
             HStack {
                 ProgressView().progressViewStyle(.circular)
                 Spacer()
+                Text("Select a UTM Server")
+                    .font(.headline)
+                Spacer()
                 Button {
                     openURL(URL(string: "https://docs.getutm.app/remote/")!)
                 } label: {
@@ -52,41 +51,43 @@ struct UTMRemoteConnectView: View {
                 }
             }.padding()
             List {
-                Section(header: Text("Saved")) {
-                    ForEach(remoteClientState.savedServers) { server in
-                        Button {
-                            isAutoConnect = true
-                            selectedServer = server
-                        } label: {
-                            Text(server.name)
-                        }.contextMenu {
+                if remoteClientState.savedServers.count > 0 {
+                    Section(header: Text("Saved")) {
+                        ForEach(remoteClientState.savedServers) { server in
                             Button {
-                                isAutoConnect = false
+                                isAutoConnect = true
                                 selectedServer = server
                             } label: {
-                                Label("Edit…", systemImage: "slider.horizontal.3")
+                                MacDeviceLabel(server.name, device: .init(model: server.model))
+                            }.foregroundColor(.primary)
+                            .contextMenu {
+                                Button {
+                                    isAutoConnect = false
+                                    selectedServer = server
+                                } label: {
+                                    Label("Edit…", systemImage: "slider.horizontal.3")
+                                }
+                                DestructiveButton("Delete") {
+
+                                }
                             }
-                            DestructiveButton("Delete") {
+                        }.onDelete { indexSet in
 
-                            }
                         }
-                    }.onDelete { indexSet in
-
                     }
                 }
-                Section(header: Text("Found")) {
+                Section(header: Text("Discovered")) {
                     ForEach(remoteClientState.foundServers) { server in
                         Button {
                             isAutoConnect = true
                             selectedServer = server
                         } label: {
-                            Text(server.name)
-                        }
+                            MacDeviceLabel(server.name, device: .init(model: server.model))
+                        }.foregroundColor(.primary)
                     }
                 }
-            }.listStyle(.plain)
-        }.frame(maxWidth: idiom == .pad ? 600 : nil)
-        .alert(item: $remoteClientState.alertMessage) { item in
+            }.listStyle(.insetGrouped)
+        }.alert(item: $remoteClientState.alertMessage) { item in
             Alert(title: Text(item.message))
         }
         .sheet(item: $selectedServer) { server in

+ 18 - 10
Remote/UTMRemoteClient.swift

@@ -47,8 +47,8 @@ actor UTMRemoteClient {
     func startScanning() {
         scanTask = Task {
             await withErrorAlert {
-                for try await endpoints in Connection.endpoints(forServiceType: service) {
-                    await self.didFindEndpoints(endpoints)
+                for try await results in Connection.browse(forServiceType: service) {
+                    await self.didFindResults(results)
                 }
             }
         }
@@ -59,16 +59,23 @@ actor UTMRemoteClient {
         scanTask = nil
     }
 
-    func didFindEndpoints(_ endpoints: [NWEndpoint]) async {
-        self.endpoints = endpoints.reduce(into: [String: NWEndpoint]()) { map, endpoint in
-            map[endpoint.debugDescription] = endpoint
-        }
-        let servers = endpoints.compactMap { endpoint in
-            switch endpoint {
+    func didFindResults(_ results: Set<NWBrowser.Result>) async {
+        self.endpoints = results.reduce(into: [String: NWEndpoint]()) { map, result in
+            map[result.endpoint.debugDescription] = result.endpoint
+        }
+        let servers = results.compactMap { result in
+            let model: String?
+            if case .bonjour(let txtRecord) = result.metadata,
+                case .string(let value) = txtRecord.getEntry(for: "Model") {
+                model = value
+            } else {
+                model = nil
+            }
+            switch result.endpoint {
             case .hostPort(let host, _):
-                return State.Server(hostname: host.debugDescription, name: host.debugDescription, lastSeen: Date())
+                return State.Server(hostname: result.endpoint.hostname!, model: model, name: host.debugDescription, lastSeen: Date())
             case .service(let name, _, _, _):
-                return State.Server(hostname: endpoint.debugDescription, name: name, lastSeen: Date())
+                return State.Server(hostname: result.endpoint.debugDescription, model: model, name: name, lastSeen: Date())
             default:
                 return nil
             }
@@ -107,6 +114,7 @@ extension UTMRemoteClient {
         typealias ServerFingerprint = String
         struct Server: Codable, Identifiable, Hashable {
             let hostname: String
+            var model: String?
             var fingerprint: ServerFingerprint?
             var name: String
             var lastSeen: Date

+ 1 - 0
Remote/UTMRemoteMessage.swift

@@ -57,6 +57,7 @@ extension UTMRemoteMessageServer {
         struct Reply: Serializable, Codable {
             let version: Int
             let capabilities: UTMCapabilities
+            let model: String
         }
     }
 

+ 6 - 2
Remote/UTMRemoteServer.swift

@@ -98,6 +98,10 @@ actor UTMRemoteServer {
         }
     }
 
+    private var metadata: NWTXTRecord {
+        NWTXTRecord(["Model": MacDevice.current.model])
+    }
+
     func start() async {
         do {
             try await center.requestAuthorization(options: .alert)
@@ -112,7 +116,7 @@ actor UTMRemoteServer {
             registerNotifications()
             listener = Task {
                 await withErrorNotification {
-                    for try await connection in Connection.advertise(forServiceType: service, identity: keyManager.identity) {
+                    for try await connection in Connection.advertise(forServiceType: service, txtRecord: metadata, identity: keyManager.identity) {
                         if let connection = try? await Connection(connection: connection) {
                             await newRemoteConnection(connection)
                         }
@@ -579,7 +583,7 @@ extension UTMRemoteServer {
         }
 
         private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
-            return .init(version: UTMRemoteMessageServer.version, capabilities: .current)
+            return .init(version: UTMRemoteMessageServer.version, capabilities: .current, model: MacDevice.current.model)
         }
 
         private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply {

+ 6 - 0
UTM.xcodeproj/project.pbxproj

@@ -424,6 +424,8 @@
 		CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
 		CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
 		CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
+		CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
+		CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
 		CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; };
 		CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
 		CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
@@ -1765,6 +1767,7 @@
 		CE0DF17125A80B6300A51894 /* Bootstrap.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Bootstrap.c; sourceTree = "<group>"; };
 		CE0E9B86252FD06B0026E02B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
 		CE19392526DCB093005CEC17 /* RAMSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RAMSlider.swift; sourceTree = "<group>"; };
+		CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = "<group>"; };
 		CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = "<group>"; };
 		CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = "<group>"; };
 		CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = "<group>"; };
@@ -2876,6 +2879,7 @@
 				CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
 				8471770527CC974F00D3A50B /* DefaultTextField.swift */,
 				8432329328C2ED9000CFBC97 /* FileBrowseField.swift */,
+				CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */,
 				84F909FE289488F90008DBE2 /* MenuLabel.swift */,
 				CED234EC254796E500ED0A57 /* NumberTextField.swift */,
 				CE19392526DCB093005CEC17 /* RAMSlider.swift */,
@@ -3603,6 +3607,7 @@
 				2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
 				CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,
 				85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */,
+				CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
 				CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */,
 				845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */,
 				CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */,
@@ -4025,6 +4030,7 @@
 				CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */,
 				CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */,
 				CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */,
+				CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
 				CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */,
 				CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */,
 				CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */,

+ 1 - 1
UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -78,7 +78,7 @@
       "location" : "https://github.com/utmapp/SwiftConnect",
       "state" : {
         "branch" : "main",
-        "revision" : "c8c5584be464065688b6674f04510f38d4f4adb0"
+        "revision" : "c6e84abcc1563a1ec6521d6649b5b918494539bc"
       }
     },
     {