Skip to content

Commit c7f4aea

Browse files
committed
connect: show model of Mac
1 parent cae50ce commit c7f4aea

File tree

8 files changed

+171
-39
lines changed

8 files changed

+171
-39
lines changed

Platform/Shared/ContentView.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import UniformTypeIdentifiers
2020
import IQKeyboardManagerSwift
2121
#endif
2222

23-
#if WITH_QEMU_TCI
23+
// on visionOS, there is no text to show more than UTM
24+
#if WITH_QEMU_TCI && !os(visionOS)
2425
let productName = "UTM SE"
25-
#elseif WITH_REMOTE
26+
#elseif WITH_REMOTE && !os(visionOS)
2627
let productName = "UTM Remote"
2728
#else
2829
let productName = "UTM"

Platform/Shared/MacDeviceLabel.swift

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// Copyright © 2024 osy. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import SwiftUI
18+
import UniformTypeIdentifiers
19+
20+
struct MacDeviceLabel<Title>: View where Title : StringProtocol {
21+
let title: Title
22+
let device: MacDevice
23+
24+
init(_ title: Title, device macDevice: MacDevice) {
25+
self.title = title
26+
self.device = macDevice
27+
}
28+
29+
var body: some View {
30+
Label(title, systemImage: device.symbolName)
31+
}
32+
}
33+
34+
// credits: https://adamdemasi.com/2023/04/15/mac-device-icon-by-device-class.html
35+
36+
private extension UTTagClass {
37+
static let deviceModelCode = UTTagClass(rawValue: "com.apple.device-model-code")
38+
}
39+
40+
private extension UTType {
41+
static let macBook = UTType("com.apple.mac.laptop")
42+
static let macBookWithNotch = UTType("com.apple.mac.notched-laptop")
43+
static let macMini = UTType("com.apple.macmini")
44+
static let macStudio = UTType("com.apple.macstudio")
45+
static let iMac = UTType("com.apple.imac")
46+
static let macPro = UTType("com.apple.macpro")
47+
static let macPro2013 = UTType("com.apple.macpro-cylinder")
48+
static let macPro2019 = UTType("com.apple.macpro-2019")
49+
}
50+
51+
struct MacDevice {
52+
let model: String
53+
let symbolName: String
54+
55+
#if os(macOS)
56+
static let current: Self = {
57+
let key = "hw.model"
58+
var size = size_t()
59+
sysctlbyname(key, nil, &size, nil, 0)
60+
let value = malloc(size)
61+
defer {
62+
value?.deallocate()
63+
}
64+
sysctlbyname(key, value, &size, nil, 0)
65+
guard let cChar = value?.bindMemory(to: CChar.self, capacity: size) else {
66+
return Self(model: "Unknown")
67+
}
68+
return Self(model: String(cString: cChar))
69+
}()
70+
#endif
71+
72+
init(model: String?) {
73+
self.model = model ?? "Unknown"
74+
self.symbolName = Self.symbolName(from: self.model)
75+
}
76+
77+
private static func checkModel(_ model: String, conformsTo type: UTType?) -> Bool {
78+
guard let type else {
79+
return false
80+
}
81+
return UTType(tag: model, tagClass: .deviceModelCode, conformingTo: nil)?.conforms(to: type) ?? false
82+
}
83+
84+
private static func symbolName(from model: String) -> String {
85+
if checkModel(model, conformsTo: .macBookWithNotch),
86+
#available(macOS 14, iOS 17, macCatalyst 17, tvOS 17, watchOS 10, *) {
87+
// macbook.gen2 was added with SF Symbols 5.0 (macOS Sonoma, 2023), but MacBooks with a notch
88+
// were released in 2021!
89+
return "macbook.gen2"
90+
} else if checkModel(model, conformsTo: .macBook) {
91+
return "laptopcomputer"
92+
} else if checkModel(model, conformsTo: .macMini) {
93+
return "macmini"
94+
} else if checkModel(model, conformsTo: .macStudio) {
95+
return "macstudio"
96+
} else if checkModel(model, conformsTo: .iMac) {
97+
return "desktopcomputer"
98+
} else if checkModel(model, conformsTo: .macPro2019) {
99+
return "macpro.gen3"
100+
} else if checkModel(model, conformsTo: .macPro2013) {
101+
return "macpro.gen2"
102+
} else if checkModel(model, conformsTo: .macPro) {
103+
return "macpro"
104+
}
105+
return "display"
106+
}
107+
}
108+
109+
#Preview {
110+
MacDeviceLabel("MacBook", device: MacDevice(model: "Mac14,6"))
111+
}

Platform/iOS/UTMRemoteConnectView.swift

+25-24
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ struct UTMRemoteConnectView: View {
2323
@State private var selectedServer: UTMRemoteClient.State.Server?
2424
@State private var isAutoConnect: Bool = false
2525

26-
private var idiom: UIUserInterfaceIdiom {
27-
UIDevice.current.userInterfaceIdiom
28-
}
29-
3026
private var remoteClient: UTMRemoteClient {
3127
data.remoteClient
3228
}
@@ -36,6 +32,9 @@ struct UTMRemoteConnectView: View {
3632
HStack {
3733
ProgressView().progressViewStyle(.circular)
3834
Spacer()
35+
Text("Select a UTM Server")
36+
.font(.headline)
37+
Spacer()
3938
Button {
4039
openURL(URL(string: "https://docs.getutm.app/remote/")!)
4140
} label: {
@@ -52,41 +51,43 @@ struct UTMRemoteConnectView: View {
5251
}
5352
}.padding()
5453
List {
55-
Section(header: Text("Saved")) {
56-
ForEach(remoteClientState.savedServers) { server in
57-
Button {
58-
isAutoConnect = true
59-
selectedServer = server
60-
} label: {
61-
Text(server.name)
62-
}.contextMenu {
54+
if remoteClientState.savedServers.count > 0 {
55+
Section(header: Text("Saved")) {
56+
ForEach(remoteClientState.savedServers) { server in
6357
Button {
64-
isAutoConnect = false
58+
isAutoConnect = true
6559
selectedServer = server
6660
} label: {
67-
Label("Edit…", systemImage: "slider.horizontal.3")
61+
MacDeviceLabel(server.name, device: .init(model: server.model))
62+
}.foregroundColor(.primary)
63+
.contextMenu {
64+
Button {
65+
isAutoConnect = false
66+
selectedServer = server
67+
} label: {
68+
Label("Edit…", systemImage: "slider.horizontal.3")
69+
}
70+
DestructiveButton("Delete") {
71+
72+
}
6873
}
69-
DestructiveButton("Delete") {
74+
}.onDelete { indexSet in
7075

71-
}
7276
}
73-
}.onDelete { indexSet in
74-
7577
}
7678
}
77-
Section(header: Text("Found")) {
79+
Section(header: Text("Discovered")) {
7880
ForEach(remoteClientState.foundServers) { server in
7981
Button {
8082
isAutoConnect = true
8183
selectedServer = server
8284
} label: {
83-
Text(server.name)
84-
}
85+
MacDeviceLabel(server.name, device: .init(model: server.model))
86+
}.foregroundColor(.primary)
8587
}
8688
}
87-
}.listStyle(.plain)
88-
}.frame(maxWidth: idiom == .pad ? 600 : nil)
89-
.alert(item: $remoteClientState.alertMessage) { item in
89+
}.listStyle(.insetGrouped)
90+
}.alert(item: $remoteClientState.alertMessage) { item in
9091
Alert(title: Text(item.message))
9192
}
9293
.sheet(item: $selectedServer) { server in

Remote/UTMRemoteClient.swift

+18-10
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ actor UTMRemoteClient {
4747
func startScanning() {
4848
scanTask = Task {
4949
await withErrorAlert {
50-
for try await endpoints in Connection.endpoints(forServiceType: service) {
51-
await self.didFindEndpoints(endpoints)
50+
for try await results in Connection.browse(forServiceType: service) {
51+
await self.didFindResults(results)
5252
}
5353
}
5454
}
@@ -59,16 +59,23 @@ actor UTMRemoteClient {
5959
scanTask = nil
6060
}
6161

62-
func didFindEndpoints(_ endpoints: [NWEndpoint]) async {
63-
self.endpoints = endpoints.reduce(into: [String: NWEndpoint]()) { map, endpoint in
64-
map[endpoint.debugDescription] = endpoint
65-
}
66-
let servers = endpoints.compactMap { endpoint in
67-
switch endpoint {
62+
func didFindResults(_ results: Set<NWBrowser.Result>) async {
63+
self.endpoints = results.reduce(into: [String: NWEndpoint]()) { map, result in
64+
map[result.endpoint.debugDescription] = result.endpoint
65+
}
66+
let servers = results.compactMap { result in
67+
let model: String?
68+
if case .bonjour(let txtRecord) = result.metadata,
69+
case .string(let value) = txtRecord.getEntry(for: "Model") {
70+
model = value
71+
} else {
72+
model = nil
73+
}
74+
switch result.endpoint {
6875
case .hostPort(let host, _):
69-
return State.Server(hostname: host.debugDescription, name: host.debugDescription, lastSeen: Date())
76+
return State.Server(hostname: result.endpoint.hostname!, model: model, name: host.debugDescription, lastSeen: Date())
7077
case .service(let name, _, _, _):
71-
return State.Server(hostname: endpoint.debugDescription, name: name, lastSeen: Date())
78+
return State.Server(hostname: result.endpoint.debugDescription, model: model, name: name, lastSeen: Date())
7279
default:
7380
return nil
7481
}
@@ -107,6 +114,7 @@ extension UTMRemoteClient {
107114
typealias ServerFingerprint = String
108115
struct Server: Codable, Identifiable, Hashable {
109116
let hostname: String
117+
var model: String?
110118
var fingerprint: ServerFingerprint?
111119
var name: String
112120
var lastSeen: Date

Remote/UTMRemoteMessage.swift

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ extension UTMRemoteMessageServer {
5757
struct Reply: Serializable, Codable {
5858
let version: Int
5959
let capabilities: UTMCapabilities
60+
let model: String
6061
}
6162
}
6263

Remote/UTMRemoteServer.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ actor UTMRemoteServer {
9898
}
9999
}
100100

101+
private var metadata: NWTXTRecord {
102+
NWTXTRecord(["Model": MacDevice.current.model])
103+
}
104+
101105
func start() async {
102106
do {
103107
try await center.requestAuthorization(options: .alert)
@@ -112,7 +116,7 @@ actor UTMRemoteServer {
112116
registerNotifications()
113117
listener = Task {
114118
await withErrorNotification {
115-
for try await connection in Connection.advertise(forServiceType: service, identity: keyManager.identity) {
119+
for try await connection in Connection.advertise(forServiceType: service, txtRecord: metadata, identity: keyManager.identity) {
116120
if let connection = try? await Connection(connection: connection) {
117121
await newRemoteConnection(connection)
118122
}
@@ -579,7 +583,7 @@ extension UTMRemoteServer {
579583
}
580584

581585
private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
582-
return .init(version: UTMRemoteMessageServer.version, capabilities: .current)
586+
return .init(version: UTMRemoteMessageServer.version, capabilities: .current, model: MacDevice.current.model)
583587
}
584588

585589
private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply {

UTM.xcodeproj/project.pbxproj

+6
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@
424424
CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
425425
CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
426426
CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
427+
CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
428+
CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
427429
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; };
428430
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
429431
CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
@@ -1765,6 +1767,7 @@
17651767
CE0DF17125A80B6300A51894 /* Bootstrap.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Bootstrap.c; sourceTree = "<group>"; };
17661768
CE0E9B86252FD06B0026E02B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
17671769
CE19392526DCB093005CEC17 /* RAMSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RAMSlider.swift; sourceTree = "<group>"; };
1770+
CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = "<group>"; };
17681771
CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = "<group>"; };
17691772
CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = "<group>"; };
17701773
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = "<group>"; };
@@ -2876,6 +2879,7 @@
28762879
CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
28772880
8471770527CC974F00D3A50B /* DefaultTextField.swift */,
28782881
8432329328C2ED9000CFBC97 /* FileBrowseField.swift */,
2882+
CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */,
28792883
84F909FE289488F90008DBE2 /* MenuLabel.swift */,
28802884
CED234EC254796E500ED0A57 /* NumberTextField.swift */,
28812885
CE19392526DCB093005CEC17 /* RAMSlider.swift */,
@@ -3603,6 +3607,7 @@
36033607
2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
36043608
CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,
36053609
85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */,
3610+
CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
36063611
CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */,
36073612
845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */,
36083613
CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */,
@@ -4025,6 +4030,7 @@
40254030
CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */,
40264031
CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */,
40274032
CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */,
4033+
CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
40284034
CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */,
40294035
CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */,
40304036
CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */,

UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"location" : "https://github.com/utmapp/SwiftConnect",
7979
"state" : {
8080
"branch" : "main",
81-
"revision" : "c8c5584be464065688b6674f04510f38d4f4adb0"
81+
"revision" : "c6e84abcc1563a1ec6521d6649b5b918494539bc"
8282
}
8383
},
8484
{

0 commit comments

Comments
 (0)