From 1eb9f627c3bc91a722042a02ad77b0296458d710 Mon Sep 17 00:00:00 2001 From: Luka <97032624+Hardstylist@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:30:01 +0100 Subject: [PATCH] Fix: Replace emoji and non-ASCII characters (#177) --- research/VPhoneVirtualMachineRefactored.swift | 424 ++++++++++++++++++ sources/vphone-cli/VPhoneAppDelegate.swift | 2 +- sources/vphone-cli/VPhoneControl.swift | 2 +- sources/vphone-cli/VPhoneKeyHelper.swift | 2 +- .../VPhoneKeychainBrowserModel.swift | 2 +- .../VPhoneKeychainBrowserView.swift | 8 +- sources/vphone-cli/VPhoneKeychainItem.swift | 4 +- .../vphone-cli/VPhoneLocationProvider.swift | 4 +- sources/vphone-cli/VPhoneRemoteFile.swift | 2 +- sources/vphone-cli/VPhoneScreenRecorder.swift | 8 +- sources/vphone-cli/VPhoneVirtualMachine.swift | 14 +- .../vphone-cli/VPhoneWindowController.swift | 4 +- 12 files changed, 450 insertions(+), 26 deletions(-) create mode 100644 research/VPhoneVirtualMachineRefactored.swift diff --git a/research/VPhoneVirtualMachineRefactored.swift b/research/VPhoneVirtualMachineRefactored.swift new file mode 100644 index 0000000..f85f69c --- /dev/null +++ b/research/VPhoneVirtualMachineRefactored.swift @@ -0,0 +1,424 @@ +import Dynamic +import Foundation +import Virtualization + +/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode. +@MainActor +class VPhoneVirtualMachineRefactored: NSObject, VZVirtualMachineDelegate { + let virtualMachine: VZVirtualMachine + /// ECID hex string resolved from machineIdentifier (e.g. "0x0012345678ABCDEF"). + let ecidHex: String? + /// Read handle for VM serial output. + private var serialOutputReadHandle: FileHandle? + /// Synthetic battery source for runtime charge/connectivity updates. + private var batterySource: AnyObject? + + struct Configuration { + var romURL: URL + var nvramURL: URL + var machineIDURL: URL + var diskURL: URL + var cpuCount: Int = 8 + var memorySize: UInt64 = 8 * 1024 * 1024 * 1024 + var sepStorageURL: URL + var sepRomURL: URL + var screenConfiguration: ScreenConfiguration = .default + var kernelDebugPort: Int? + } + + struct ScreenConfiguration { + let width: Int + let height: Int + let pixelsPerInch: Int + let scale: Double + + static let `default` = ScreenConfiguration( + width: 1290, + height: 2796, + pixelsPerInch: 460, + scale: 3.0 + ) + } + + private struct DeviceIdentity { + let cpidHex: String + let ecidHex: String + let udid: String + } + + // MARK: - Battery Connectivity States + + private enum BatteryConnectivity { + static let charging = 1 + static let disconnected = 2 + } + + init(options: Configuration) throws { + // Create hardware model + let hardwareModel = try VPhoneHardware.createModel() + print("[vphone] PV=3 hardware model: isSupported = true") + + // Configure platform + let platform = try configurePlatform( + machineIDURL: options.machineIDURL, + nvramURL: options.nvramURL, + hardwareModel: hardwareModel + ) + + // Resolve device identity + if let machineIdentifier = platform.machineIdentifier { + ecidHex = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier)?.ecidHex + } else { + ecidHex = nil + } + + // Create bootloader + let bootloader = createBootloader(romURL: options.romURL) + + // Build VM configuration + let config = buildConfiguration( + options: options, + hardwareModel: hardwareModel, + platform: platform, + bootloader: bootloader + ) + + // Validate configuration + try config.validate() + print("[vphone] Configuration validated") + + virtualMachine = VZVirtualMachine(configuration: config) + super.init() + virtualMachine.delegate = self + + // Setup serial output forwarding + if let readHandle = serialOutputReadHandle { + readHandle.readabilityHandler = { handle in + let data = handle.availableData + guard !data.isEmpty else { return } + FileHandle.standardOutput.write(data) + } + } + } + + // MARK: - Platform Configuration + + private func configurePlatform( + machineIDURL: URL, + nvramURL: URL, + hardwareModel: VZMacHardwareModel + ) throws -> VZMacPlatformConfiguration { + let platform = VZMacPlatformConfiguration() + platform.hardwareModel = hardwareModel + + // Load or create machine identifier + let machineIdentifier = loadOrCreateMachineIdentifier(at: machineIDURL) + platform.machineIdentifier = machineIdentifier + + // Create auxiliary storage (NVRAM) + let auxiliaryStorage = try VZMacAuxiliaryStorage( + creatingStorageAt: nvramURL, + hardwareModel: hardwareModel, + options: .allowOverwrite + ) + platform.auxiliaryStorage = auxiliaryStorage + + // Configure boot args for serial output + setBootArgsSerialOutput(auxiliaryStorage) + + return platform + } + + private func loadOrCreateMachineIdentifier(at url: URL) -> VZMacMachineIdentifier { + if let savedData = try? Data(contentsOf: url), + let savedID = VZMacMachineIdentifier(dataRepresentation: savedData) + { + print("[vphone] Loaded machineIdentifier (ECID stable)") + return savedID + } + + let newID = VZMacMachineIdentifier() + try? newID.dataRepresentation.write(to: url) + print("[vphone] Created new machineIdentifier -> \(url.lastPathComponent)") + return newID + } + + private func setBootArgsSerialOutput(_ auxiliaryStorage: VZMacAuxiliaryStorage) { + let bootArgs = "serial=3 debug=0x104c04" + guard let bootArgsData = bootArgs.data(using: .utf8) else { return } + + let success = Dynamic(auxiliaryStorage) + ._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil) + .asBool ?? false + + if success { + print("[vphone] NVRAM boot-args: \(bootArgs)") + } + } + + // MARK: - Bootloader + + private func createBootloader(romURL: URL) -> VZMacOSBootLoader { + let bootloader = VZMacOSBootLoader() + Dynamic(bootloader)._setROMURL(romURL) + return bootloader + } + + // MARK: - Configuration Builder + + private func buildConfiguration( + options: Configuration, + hardwareModel _: VZMacHardwareModel, + platform: VZMacPlatformConfiguration, + bootloader: VZMacOSBootLoader + ) -> VZVirtualMachineConfiguration { + let config = VZVirtualMachineConfiguration() + config.bootLoader = bootloader + config.platform = platform + config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) + config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize) + + // Configure each subsystem + configureDisplay(&config, screen: options.screenConfiguration) + configureAudio(&config) + configureStorage(&config, diskURL: options.diskURL) + configureNetwork(&config) + configureSerialPort(&config) + configureInputDevices(&config) + configureSocketDevice(&config) + configureBattery(&config) + configureDebugStub(&config, port: options.kernelDebugPort) + configureSEP(&config, options: options) + + return config + } + + private func configureDisplay(_ config: inout VZVirtualMachineConfiguration, screen: ScreenConfiguration) { + let graphicsConfiguration = VZMacGraphicsDeviceConfiguration() + let displayConfiguration = VZMacGraphicsDisplayConfiguration( + widthInPixels: screen.width, + heightInPixels: screen.height, + pixelsPerInch: screen.pixelsPerInch + ) + graphicsConfiguration.displays = [displayConfiguration] + config.graphicsDevices = [graphicsConfiguration] + } + + private func configureAudio(_ config: inout VZVirtualMachineConfiguration) { + let soundDevice = VZVirtioSoundDeviceConfiguration() + let inputStream = VZVirtioSoundDeviceInputStreamConfiguration() + inputStream.source = VZHostAudioInputStreamSource() + + let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration() + outputStream.sink = VZHostAudioOutputStreamSink() + + soundDevice.streams = [inputStream, outputStream] + config.audioDevices = [soundDevice] + } + + private func configureStorage(_ config: inout VZVirtualMachineConfiguration, diskURL: URL) { + guard FileManager.default.fileExists(atPath: diskURL.path) else { + print("[vphone] Warning: Disk image not found at \(diskURL.path)") + return + } + + let attachment = try? VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false) + let storageDevice = VZVirtioBlockDeviceConfiguration(attachment: attachment!) + config.storageDevices = [storageDevice] + } + + private func configureNetwork(_ config: inout VZVirtualMachineConfiguration) { + let networkDevice = VZVirtioNetworkDeviceConfiguration() + networkDevice.attachment = VZNATNetworkDeviceAttachment() + config.networkDevices = [networkDevice] + } + + private func configureSerialPort(_ config: inout VZVirtualMachineConfiguration) { + guard let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration else { + return + } + + let inputPipe = Pipe() + let outputPipe = Pipe() + + serialPort.attachment = VZFileHandleSerialPortAttachment( + fileHandleForReading: inputPipe.fileHandleForReading, + fileHandleForWriting: outputPipe.fileHandleForWriting + ) + + // Forward host stdin → VM serial input + forwardStandardInput(to: inputPipe.fileHandleForWriting) + serialOutputReadHandle = outputPipe.fileHandleForReading + + config.serialPorts = [serialPort] + print("[vphone] PL011 serial port attached (interactive)") + } + + private func forwardStandardInput(to writeHandle: FileHandle) { + let stdinFD = FileHandle.standardInput.fileDescriptor + DispatchQueue.global(qos: .userInteractive).async { + var buffer = [UInt8](repeating: 0, count: 4096) + while true { + let bytesRead = read(stdinFD, &buffer, buffer.count) + guard bytesRead > 0 else { break } + writeHandle.write(Data(buffer[.. DeviceIdentity? { + let ecidValue = extractECID(from: machineIdentifier) + guard let ecidValue else { return nil } + + let cpidHex = String(format: "%08X", VPhoneHardware.udidChipID) + let ecidHex = String(format: "%016llX", ecidValue) + let udid = "\(cpidHex)-\(ecidHex)" + + return DeviceIdentity(cpidHex: cpidHex, ecidHex: ecidHex, udid: udid) + } + + private static func extractECID(from machineIdentifier: VZMacMachineIdentifier) -> UInt64? { + if let ecid = Dynamic(machineIdentifier)._ECID.asUInt64 { + return ecid + } else if let ecidNumber = Dynamic(machineIdentifier)._ECID.asObject as? NSNumber { + return ecidNumber.uint64Value + } + return nil + } + + // MARK: - Battery + + /// Update the synthetic battery charge and connectivity at runtime. + func updateBattery(charge: Double, isCharging: Bool) { + guard let source = batterySource else { return } + Dynamic(source).setCharge(charge) + Dynamic(source).setConnectivity(isCharging ? BatteryConnectivity.charging : BatteryConnectivity.disconnected) + } + + // MARK: - Start + + @MainActor + func start(forceDFU: Bool) async throws { + let startOptions = VZMacOSVirtualMachineStartOptions() + Dynamic(startOptions)._setForceDFU(forceDFU) + Dynamic(startOptions)._setStopInIBootStage1(false) + Dynamic(startOptions)._setStopInIBootStage2(false) + + print("[vphone] Starting\(forceDFU ? " DFU" : "")...") + + nonisolated(unsafe) let vm = virtualMachine + try await vm.start(options: startOptions) + + if forceDFU { + print("[vphone] VM started in DFU mode — connect with irecovery") + } else { + print("[vphone] VM started — booting normally") + } + + logDebugStubPortIfNeeded(vm) + } + + private func logDebugStubPortIfNeeded(_ vm: VZVirtualMachine) { + guard ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 26 else { + print("[vphone] Kernel GDB debug stub port query requires macOS 26+, skipped") + return + } + + guard let debugStub = Dynamic(vm)._configuration._debugStub.asAnyObject, + let port = Dynamic(debugStub).port.asInt, + port > 0 + else { + return + } + + print("[vphone] Kernel GDB debug stub listening on tcp://127.0.0.1:\(port)") + } + + // MARK: - Delegate + + nonisolated func guestDidStop(_: VZVirtualMachine) { + print("[vphone] Guest stopped") + exit(EXIT_SUCCESS) + } + + nonisolated func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) { + print("[vphone] Stopped with error: \(error)") + exit(EXIT_FAILURE) + } + + nonisolated func virtualMachine( + _: VZVirtualMachine, + networkDevice _: VZNetworkDevice, + attachmentWasDisconnectedWithError error: Error + ) { + print("[vphone] Network error: \(error)") + } +} diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index bc40420..7622ca9 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -24,7 +24,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { signal(SIGINT, SIG_IGN) let src = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main) src.setEventHandler { - print("\n[vphone] SIGINT — shutting down") + print("\n[vphone] SIGINT - shutting down") NSApp.terminate(nil) } src.activate() diff --git a/sources/vphone-cli/VPhoneControl.swift b/sources/vphone-cli/VPhoneControl.swift index 4b7c0b7..c806bf2 100644 --- a/sources/vphone-cli/VPhoneControl.swift +++ b/sources/vphone-cli/VPhoneControl.swift @@ -607,7 +607,7 @@ class VPhoneControl { continue } - // No pending request — handle as before (fire-and-forget) + // No pending request - handle as before (fire-and-forget) switch type { case "ok": let detail = msg["msg"] as? String ?? "" diff --git a/sources/vphone-cli/VPhoneKeyHelper.swift b/sources/vphone-cli/VPhoneKeyHelper.swift index 9b67bf5..8e1c0ca 100644 --- a/sources/vphone-cli/VPhoneKeyHelper.swift +++ b/sources/vphone-cli/VPhoneKeyHelper.swift @@ -123,7 +123,7 @@ class VPhoneKeyHelper { } } - // MARK: - ASCII → Apple VK Code (US Layout) + // MARK: - ASCII -> Apple VK Code (US Layout) private func asciiToVK(_ char: Character) -> (UInt16, Bool)? { switch char { diff --git a/sources/vphone-cli/VPhoneKeychainBrowserModel.swift b/sources/vphone-cli/VPhoneKeychainBrowserModel.swift index 29b572a..d5c57a9 100644 --- a/sources/vphone-cli/VPhoneKeychainBrowserModel.swift +++ b/sources/vphone-cli/VPhoneKeychainBrowserModel.swift @@ -81,7 +81,7 @@ class VPhoneKeychainBrowserModel { func refresh() async { guard control.isConnected else { - error = "Waiting for vphoned connection…" + error = "Waiting for vphoned connection..." return } isLoading = true diff --git a/sources/vphone-cli/VPhoneKeychainBrowserView.swift b/sources/vphone-cli/VPhoneKeychainBrowserView.swift index c8fcaf6..64ccf27 100644 --- a/sources/vphone-cli/VPhoneKeychainBrowserView.swift +++ b/sources/vphone-cli/VPhoneKeychainBrowserView.swift @@ -57,21 +57,21 @@ struct VPhoneKeychainBrowserView: View { .width(min: 60, ideal: 80, max: 100) TableColumn("Account", value: \.account) { item in - Text(item.account.isEmpty ? "—" : item.account) + Text(item.account.isEmpty ? "-" : item.account) .lineLimit(1) .help(item.account) } .width(min: 80, ideal: 150, max: .infinity) TableColumn("Service", value: \.service) { item in - Text(item.service.isEmpty ? "—" : item.service) + Text(item.service.isEmpty ? "-" : item.service) .lineLimit(1) .help(item.service) } .width(min: 80, ideal: 150, max: .infinity) TableColumn("Access Group", value: \.accessGroup) { item in - Text(item.accessGroup.isEmpty ? "—" : item.accessGroup) + Text(item.accessGroup.isEmpty ? "-" : item.accessGroup) .font(.system(.body, design: .monospaced)) .lineLimit(1) .help(item.accessGroup) @@ -79,7 +79,7 @@ struct VPhoneKeychainBrowserView: View { .width(min: 80, ideal: 160, max: .infinity) TableColumn("Protection", value: \.protection) { item in - Text(item.protection.isEmpty ? "—" : item.protection) + Text(item.protection.isEmpty ? "-" : item.protection) .font(.system(.body, design: .monospaced)) .lineLimit(1) .help(item.protectionDescription) diff --git a/sources/vphone-cli/VPhoneKeychainItem.swift b/sources/vphone-cli/VPhoneKeychainItem.swift index 4a2a3f0..5508102 100644 --- a/sources/vphone-cli/VPhoneKeychainItem.swift +++ b/sources/vphone-cli/VPhoneKeychainItem.swift @@ -38,7 +38,7 @@ struct VPhoneKeychainItem: Identifiable, Hashable { } var displayValue: String { - if value.isEmpty { return "—" } + if value.isEmpty { return "-" } if valueEncoding == "base64" { return "[\(ByteCountFormatter.string(fromByteCount: Int64(valueSize), countStyle: .file)) binary]" } @@ -73,7 +73,7 @@ struct VPhoneKeychainItem: Identifiable, Hashable { if let created { return Self.dateFormatter.string(from: created) } - return "—" + return "-" } private static let dateFormatter: DateFormatter = { diff --git a/sources/vphone-cli/VPhoneLocationProvider.swift b/sources/vphone-cli/VPhoneLocationProvider.swift index 042666c..5edcb17 100644 --- a/sources/vphone-cli/VPhoneLocationProvider.swift +++ b/sources/vphone-cli/VPhoneLocationProvider.swift @@ -6,7 +6,7 @@ import Foundation /// Uses macOS CoreLocation to track the Mac's real location and forwards /// every update to the guest. Call `startForwarding()` when the guest /// reports "location" capability. Safe to call multiple times (e.g. -/// after vphoned reconnects) — re-sends the last known position. +/// after vphoned reconnects) - re-sends the last known position. @MainActor class VPhoneLocationProvider: NSObject { struct ReplayPoint { @@ -244,7 +244,7 @@ private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate { guard let location = locations.last else { return } let c = location.coordinate print( - "[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude)) (±\(String(format: "%.0f", location.horizontalAccuracy))m)" + "[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude)) (+/-\(String(format: "%.0f", location.horizontalAccuracy))m)" ) handler(location) } diff --git a/sources/vphone-cli/VPhoneRemoteFile.swift b/sources/vphone-cli/VPhoneRemoteFile.swift index e6843cc..fb5dfe1 100644 --- a/sources/vphone-cli/VPhoneRemoteFile.swift +++ b/sources/vphone-cli/VPhoneRemoteFile.swift @@ -30,7 +30,7 @@ struct VPhoneRemoteFile: Identifiable, Hashable { } var displaySize: String { - if isDirectory || isSymbolicLink { return "—" } + if isDirectory || isSymbolicLink { return "-" } return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file) } diff --git a/sources/vphone-cli/VPhoneScreenRecorder.swift b/sources/vphone-cli/VPhoneScreenRecorder.swift index 35042d3..7ed03ba 100644 --- a/sources/vphone-cli/VPhoneScreenRecorder.swift +++ b/sources/vphone-cli/VPhoneScreenRecorder.swift @@ -100,7 +100,7 @@ class VPhoneScreenRecorder { } print( - "[record] started — \(url.lastPathComponent) (\(width)x\(height), source: \(captureModeDescription))" + "[record] started - \(url.lastPathComponent) (\(width)x\(height), source: \(captureModeDescription))" ) } @@ -123,7 +123,7 @@ class VPhoneScreenRecorder { didLogCaptureFailure = false if let url { - print("[record] saved — \(url.path)") + print("[record] saved - \(url.path)") } return url } @@ -153,7 +153,7 @@ class VPhoneScreenRecorder { let url = screenshotOutputURL() try pngData.write(to: url, options: .atomic) - print("[record] screenshot saved — \(url.path)") + print("[record] screenshot saved - \(url.path)") return url } @@ -288,7 +288,7 @@ class VPhoneScreenRecorder { let cfObject = imageObject as CFTypeRef if CFGetTypeID(cfObject) == CGImage.typeID { - // CGImage is a CF type, not a Swift class — unsafeDowncast cannot be used here. + // CGImage is a CF type, not a Swift class - unsafeDowncast cannot be used here. return (cfObject as! CGImage) // swiftlint:disable:this force_cast } diff --git a/sources/vphone-cli/VPhoneVirtualMachine.swift b/sources/vphone-cli/VPhoneVirtualMachine.swift index ff3540e..b6d6086 100644 --- a/sources/vphone-cli/VPhoneVirtualMachine.swift +++ b/sources/vphone-cli/VPhoneVirtualMachine.swift @@ -176,7 +176,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { net.attachment = VZNATNetworkDeviceAttachment() config.networkDevices = [net] - // Serial port (PL011 UART — pipes for input/output with boot detection) + // Serial port (PL011 UART - pipes for input/output with boot detection) if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration { @@ -188,7 +188,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { fileHandleForWriting: outputPipe.fileHandleForWriting ) - // Forward host stdin → VM serial input + // Forward host stdin -> VM serial input let writeHandle = inputPipe.fileHandleForWriting let stdinFD = FileHandle.standardInput.fileDescriptor DispatchQueue.global(qos: .userInteractive).async { @@ -214,10 +214,10 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { config.keyboards = [VZUSBKeyboardConfiguration()] - // Vsock (host ↔ guest control channel, no IP/TCP involved) + // Vsock (host <-> guest control channel, no IP/TCP involved) config.socketDevices = [VZVirtioSocketDeviceConfiguration()] - // Power source (synthetic battery — guest sees full charge, charging) + // Power source (synthetic battery - guest sees full charge, charging) let source = Dynamic._VZMacSyntheticBatterySource() source.setCharge(100.0) source.setConnectivity(1) // 1=charging, 2=disconnected @@ -265,7 +265,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { super.init() virtualMachine.delegate = self - // Forward VM serial output → host stdout + // Forward VM serial output -> host stdout if let readHandle = serialOutputReadHandle { readHandle.readabilityHandler = { handle in let data = handle.availableData @@ -328,9 +328,9 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { nonisolated(unsafe) let vm = virtualMachine try await vm.start(options: opts) if forceDFU { - print("[vphone] VM started in DFU mode — connect with irecovery") + print("[vphone] VM started in DFU mode - connect with irecovery") } else { - print("[vphone] VM started — booting normally") + print("[vphone] VM started - booting normally") } // Print auto-assigned debug stub port after VM starts (private API, macOS 26+ only) diff --git a/sources/vphone-cli/VPhoneWindowController.swift b/sources/vphone-cli/VPhoneWindowController.swift index c2fb6de..2c8d573 100644 --- a/sources/vphone-cli/VPhoneWindowController.swift +++ b/sources/vphone-cli/VPhoneWindowController.swift @@ -43,7 +43,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { window.isReleasedWhenClosed = false window.contentAspectRatio = windowSize - window.title = "VPHONE ⏳" + window.title = "VPHONE [loading]" window.subtitle = ecid ?? "" window.contentView = vmView if let ecid { @@ -75,7 +75,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { [weak self, weak window] _ in Task { @MainActor in guard let self, let window, let control = self.control else { return } - window.title = control.isConnected ? "VPHONE 🔗" : "VPHONE ⛓️‍💥" + window.title = control.isConnected ? "VPHONE [connected]" : "VPHONE [disconnected]" } } }