Skip to content

Commit

Permalink
connect: show model of Mac
Browse files Browse the repository at this point in the history
  • Loading branch information
osy committed Feb 11, 2024
1 parent cae50ce commit c7f4aea
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 39 deletions.
5 changes: 3 additions & 2 deletions Platform/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
111 changes: 111 additions & 0 deletions Platform/Shared/MacDeviceLabel.swift
Original file line number Diff line number Diff line change
@@ -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"))
}
49 changes: 25 additions & 24 deletions Platform/iOS/UTMRemoteConnectView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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: {
Expand All @@ -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
Expand Down
28 changes: 18 additions & 10 deletions Remote/UTMRemoteClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Remote/UTMRemoteMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extension UTMRemoteMessageServer {
struct Reply: Serializable, Codable {
let version: Int
let capabilities: UTMCapabilities
let model: String
}
}

Expand Down
8 changes: 6 additions & 2 deletions Remote/UTMRemoteServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions UTM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"location" : "https://github.com/utmapp/SwiftConnect",
"state" : {
"branch" : "main",
"revision" : "c8c5584be464065688b6674f04510f38d4f4adb0"
"revision" : "c6e84abcc1563a1ec6521d6649b5b918494539bc"
}
},
{
Expand Down

0 comments on commit c7f4aea

Please sign in to comment.