macOS: add leading device icons in Instances

This commit is contained in:
Peter Steinberger
2025-12-18 09:15:50 +01:00
parent 97ec5d52c3
commit 2f8b75d86e
3 changed files with 145 additions and 46 deletions

View File

@@ -13,8 +13,7 @@ enum DeviceModelCatalog {
let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let friendlyName = model.isEmpty ? nil : self.modelIdentifierToName[model]
let symbol = self.symbolFor(modelIdentifier: model, friendlyName: friendlyName)
?? self.fallbackSymbol(for: family, modelIdentifier: model)
let symbol = self.symbol(deviceFamily: family, modelIdentifier: model, friendlyName: friendlyName)
let title = if let friendlyName, !friendlyName.isEmpty {
friendlyName
@@ -32,6 +31,14 @@ enum DeviceModelCatalog {
return DevicePresentation(title: title, symbol: symbol)
}
static func symbol(deviceFamily familyRaw: String, modelIdentifier modelIdentifierRaw: String, friendlyName: String?) -> String? {
let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines)
let modelIdentifier = modelIdentifierRaw.trimmingCharacters(in: .whitespacesAndNewlines)
return self.symbolFor(modelIdentifier: modelIdentifier, friendlyName: friendlyName)
?? self.fallbackSymbol(for: family, modelIdentifier: modelIdentifier)
}
private static func symbolFor(modelIdentifier rawModelIdentifier: String, friendlyName: String?) -> String? {
let modelIdentifier = rawModelIdentifier.trimmingCharacters(in: .whitespacesAndNewlines)
guard !modelIdentifier.isEmpty else { return nil }
@@ -47,17 +54,15 @@ enum DeviceModelCatalog {
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("macstudio") { return "macstudio" }
if lower.hasPrefix("macmini") { return "macmini" }
if lower.hasPrefix("imac") || lower.hasPrefix("macpro") { 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 mini") { return "macmini" }
if friendlyNameLower.contains("mac studio") { return "macstudio" }
if friendlyNameLower.contains("mac pro") { return "desktopcomputer" }
}
@@ -75,8 +80,7 @@ enum DeviceModelCatalog {
case "mac":
return "laptopcomputer"
case "android":
// Prefer tablet glyph when we know it's an Android tablet. (No attempt to infer phone/tablet here.)
return "cpu"
return "logo.android"
case "linux":
return "cpu"
default:

View File

@@ -60,47 +60,67 @@ struct InstancesSettings: View {
@ViewBuilder
private func instanceRow(_ inst: InstanceInfo) -> some View {
let isGateway = (inst.mode ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "gateway"
let prettyPlatform = inst.platform.flatMap { self.prettyPlatform($0) }
let device = DeviceModelCatalog.presentation(
deviceFamily: inst.deviceFamily,
modelIdentifier: inst.modelIdentifier)
let leadingSymbol = self.leadingDeviceSymbol(inst, device: device)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text(inst.host ?? "unknown host").font(.subheadline.bold())
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
}
HStack(spacing: 8) {
if let version = inst.version {
self.label(icon: "shippingbox", text: version)
HStack(alignment: .top, spacing: 12) {
Image(systemName: leadingSymbol)
.font(.system(size: 26, weight: .regular))
.foregroundStyle(.secondary)
.frame(width: 28, height: 28, alignment: .center)
.padding(.top, 1)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text(inst.host ?? "unknown host").font(.subheadline.bold())
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
}
let prettyPlatform = inst.platform.flatMap { self.prettyPlatform($0) }
let device = DeviceModelCatalog.presentation(
deviceFamily: inst.deviceFamily,
modelIdentifier: inst.modelIdentifier)
if let device {
// Avoid showing generic "Mac"/"iPhone"/etc; prefer the concrete model name.
let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let isGeneric = !family.isEmpty && device.title == family
if !isGeneric {
if let prettyPlatform {
self.label(icon: device.symbol, text: "\(device.title) · \(prettyPlatform)")
} else {
self.label(icon: device.symbol, text: device.title)
HStack(alignment: .firstTextBaseline, spacing: 12) {
HStack(spacing: 8) {
if let version = inst.version {
self.label(icon: "shippingbox", text: version)
}
} else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
if let device {
// Avoid showing generic "Mac"/"iPhone"/etc; prefer the concrete model name.
let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let isGeneric = !family.isEmpty && device.title == family
if !isGeneric {
if let prettyPlatform {
self.label(icon: device.symbol, text: "\(device.title) · \(prettyPlatform)")
} else {
self.label(icon: device.symbol, text: device.title)
}
} else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
}
} else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
}
if let mode = inst.mode { self.label(icon: "network", text: mode) }
}
} else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
}
.layoutPriority(1)
// Last local input is helpful for interactive nodes, but noisy/meaningless for the gateway.
if !isGateway, let secs = inst.lastInputSeconds {
self.label(icon: "clock", text: "\(secs)s ago")
}
if let mode = inst.mode { self.label(icon: "network", text: mode) }
Spacer(minLength: 0)
if let update = self.updateSummaryText(inst, isGateway: isGateway) {
self.label(icon: "arrow.clockwise", text: update)
.help(self.presenceUpdateSourceHelp(inst.reason ?? ""))
HStack(spacing: 8) {
// Last local input is helpful for interactive nodes, but noisy/meaningless for the gateway.
if !isGateway, let secs = inst.lastInputSeconds {
self.label(icon: "clock", text: "\(secs)s ago")
}
if let update = self.updateSummaryText(inst, isGateway: isGateway) {
self.label(icon: "arrow.clockwise", text: update)
.help(self.presenceUpdateSourceHelp(inst.reason ?? ""))
}
}
.foregroundStyle(.secondary)
}
}
}
@@ -116,7 +136,7 @@ struct InstancesSettings: View {
private func label(icon: String?, text: String) -> some View {
HStack(spacing: 4) {
if let icon {
if let icon, self.isSystemSymbolAvailable(icon) {
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
}
Text(text)
@@ -124,6 +144,47 @@ struct InstancesSettings: View {
.font(.footnote)
}
private func leadingDeviceSymbol(_ inst: InstanceInfo, device: DevicePresentation?) -> String {
let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if family == "android" {
return self.safeSystemSymbol("logo.android", fallback: "cpu")
}
if let title = device?.title.lowercased() {
if title.contains("mac studio") {
return self.safeSystemSymbol("macstudio", fallback: "desktopcomputer")
}
if title.contains("macbook") {
return self.safeSystemSymbol("laptopcomputer", fallback: "laptopcomputer")
}
if title.contains("ipad") {
return self.safeSystemSymbol("ipad", fallback: "ipad")
}
if title.contains("iphone") {
return self.safeSystemSymbol("iphone", fallback: "iphone")
}
}
if let symbol = device?.symbol {
return self.safeSystemSymbol(symbol, fallback: "cpu")
}
if let platform = inst.platform {
return self.safeSystemSymbol(self.platformIcon(platform), fallback: "cpu")
}
return "cpu"
}
private func safeSystemSymbol(_ preferred: String, fallback: String) -> String {
if self.isSystemSymbolAvailable(preferred) { return preferred }
return fallback
}
private func isSystemSymbolAvailable(_ name: String) -> Bool {
NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil
}
private func platformIcon(_ raw: String) -> String {
let (prefix, _) = self.parsePlatform(raw)
switch prefix {

View File

@@ -0,0 +1,34 @@
import Testing
@testable import Clawdis
@Suite
struct DeviceModelCatalogTests {
@Test
func symbolPrefersModelIdentifierPrefixes() {
#expect(DeviceModelCatalog.symbol(deviceFamily: "iPad", modelIdentifier: "iPad16,6", friendlyName: nil) == "ipad")
#expect(DeviceModelCatalog.symbol(deviceFamily: "iPhone", modelIdentifier: "iPhone17,3", friendlyName: nil) == "iphone")
}
@Test
func symbolUsesFriendlyNameForMacVariants() {
#expect(DeviceModelCatalog.symbol(
deviceFamily: "Mac",
modelIdentifier: "Mac99,1",
friendlyName: "Mac Studio (2025)") == "macstudio")
#expect(DeviceModelCatalog.symbol(
deviceFamily: "Mac",
modelIdentifier: "Mac99,2",
friendlyName: "Mac mini (2024)") == "macmini")
#expect(DeviceModelCatalog.symbol(
deviceFamily: "Mac",
modelIdentifier: "Mac99,3",
friendlyName: "MacBook Pro (14-inch, 2024)") == "laptopcomputer")
}
@Test
func symbolFallsBackToDeviceFamily() {
#expect(DeviceModelCatalog.symbol(deviceFamily: "Android", modelIdentifier: "", friendlyName: nil) == "logo.android")
#expect(DeviceModelCatalog.symbol(deviceFamily: "Linux", modelIdentifier: "", friendlyName: nil) == "cpu")
}
}