├── .github └── FUNDING.yml ├── .gitignore ├── DeepLinkSecurity ├── DeepLinkSecurity.h └── Source │ ├── Base │ └── DeepLinkSecurityDefines.swift │ ├── DeepLinkSentinel.swift │ ├── Models │ ├── DeepLinkClient.swift │ ├── DeepLinkClientDescriptor.swift │ ├── Extensions │ │ ├── DeepLinkClient+Crypto.swift │ │ └── DeepLinkClientDescriptor+.swift │ └── OpenDeepLinkRequest.swift │ ├── Storage │ ├── DeepLinkAuthStore.swift │ ├── DeepLinkManagementStore.swift │ ├── KeychainDeepLinkAuthStore.swift │ ├── MemoryDeepLinkAuthStore.swift │ └── UserDefaultsDeepLinkManagementStore.swift │ └── UI │ └── DeepLinkAuthUI.swift ├── LICENSE ├── README.md ├── ReleaseNotes ├── VirtualBuddy 1.2 Release Nodes.md ├── VirtualBuddy 1.2.1 Release Notes.md └── VirtualBuddy 1.2.2 Release Notes.md ├── VirtualBuddy.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── DeepLinkSecurity.xcscheme │ ├── VirtualBuddy (Dev Release).xcscheme │ ├── VirtualBuddy (Managed - Beta).xcscheme │ ├── VirtualBuddy (Managed).xcscheme │ ├── VirtualBuddy.xcscheme │ ├── VirtualBuddyGuest.xcscheme │ ├── VirtualCore.xcscheme │ ├── VirtualUI.xcscheme │ └── VirtualWormhole.xcscheme ├── VirtualBuddy ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon-Default.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── AppIcon-Dev.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── AppIcon-zBeta.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ └── Contents.json ├── Automation │ ├── DeepLinkHandler.swift │ ├── Support │ │ └── DeepLinkAuthDialog.swift │ └── VirtualBuddyDeepLinks.swift ├── Bootstrap │ ├── SoftwareUpdateController.swift │ ├── VirtualBuddyApp.swift │ └── VirtualBuddyAppDelegate.swift ├── Config │ ├── AppTarget.xcconfig │ ├── Entitlements │ │ ├── VirtualBuddy.entitlements │ │ └── VirtualBuddy_Managed.entitlements │ ├── Features.xcconfig │ ├── InfoPlist.xcconfig │ ├── Main.xcconfig │ ├── Paths.xcconfig │ ├── Signing.xcconfig │ └── Versions.xcconfig ├── Info.plist └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── VirtualBuddyGuest ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── Contents.json │ └── StatusItem.imageset │ │ ├── Contents.json │ │ ├── menubar.png │ │ └── menubar@2x.png ├── Dashboard │ ├── GuestDashboard.swift │ ├── GuestDefaultsImportView.swift │ ├── GuestSharedFoldersManager.swift │ ├── HostConnectionStateProvider.swift │ └── Support │ │ ├── GuestLaunchAtLoginManager.swift │ │ └── VirtualBuddyGuest-Bridging-Header.h ├── GuestAppDelegate.swift ├── GuestAppInstaller.swift ├── Main.storyboard ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── VirtualBuddyGuest.entitlements ├── VirtualBuddyGuestHelper ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── GuestHelperAppDelegate.swift └── VirtualBuddyGuestHelper.entitlements ├── VirtualCore ├── Source │ ├── Definitions │ │ ├── Logging.swift │ │ ├── PreviewSupport.swift │ │ └── VirtualCoreConstants.swift │ ├── GuestSupport │ │ ├── CreateGuestImage.sh │ │ └── GuestAdditionsDiskImage.swift │ ├── Headers │ │ └── VirtualizationPrivate.h │ ├── Models │ │ ├── Configuration │ │ │ ├── ConfigurationModels+Summary.swift │ │ │ ├── ConfigurationModels+Validation.swift │ │ │ ├── ConfigurationModels.swift │ │ │ ├── DecodableDefault.swift │ │ │ └── VBMacDevice+Storage.swift │ │ ├── SavedState │ │ │ ├── VBSavedStateMetadata+Clone.swift │ │ │ ├── VBSavedStateMetadata.swift │ │ │ ├── VBSavedStatePackage+VM.swift │ │ │ ├── VBSavedStatePackage.swift │ │ │ └── VMLibraryController+SavedState.swift │ │ ├── VBError.swift │ │ ├── VBNVRAMVariable.swift │ │ ├── VBStorageDeviceContainer.swift │ │ ├── VBVirtualMachine+Metadata.swift │ │ ├── VBVirtualMachine+Screenshot.swift │ │ └── VBVirtualMachine.swift │ ├── ReleaseTrains │ │ ├── AppUpdateChannel.swift │ │ └── VBBuildType.swift │ ├── Resources │ │ ├── Preview │ │ │ ├── Preview-Linux.vbvm │ │ │ │ └── .vbdata │ │ │ │ │ ├── Config.plist │ │ │ │ │ └── Metadata.plist │ │ │ ├── Preview.vbvm │ │ │ │ ├── .vbdata │ │ │ │ │ ├── Config.plist │ │ │ │ │ ├── Install.plist │ │ │ │ │ ├── Metadata.plist │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ └── Thumbnail.heic │ │ │ │ ├── Disk.img │ │ │ │ ├── Fake Custom Path Disk.dmg │ │ │ │ └── Fake Managed Disk.img │ │ │ └── PreviewSavedStates │ │ │ │ ├── Save-2024-03-27_16;06;24.vbst │ │ │ │ ├── Info.plist │ │ │ │ ├── Screenshot.heic │ │ │ │ └── Thumbnail.heic │ │ │ │ ├── Save-2024-03-27_16;07;04.vbst │ │ │ │ ├── Info.plist │ │ │ │ ├── Screenshot.heic │ │ │ │ └── Thumbnail.heic │ │ │ │ ├── Save-2024-03-27_16;08;06.vbst │ │ │ │ ├── Info.plist │ │ │ │ ├── Screenshot.heic │ │ │ │ └── Thumbnail.heic │ │ │ │ ├── Save-2024-03-27_16;08;28.vbst │ │ │ │ ├── Info.plist │ │ │ │ ├── Screenshot.heic │ │ │ │ └── Thumbnail.heic │ │ │ │ └── Save-2024-03-27_16;08;51.vbst │ │ │ │ ├── Info.plist │ │ │ │ ├── Screenshot.heic │ │ │ │ └── Thumbnail.heic │ │ └── VirtualCore.xcassets │ │ │ ├── Adjectives.dataset │ │ │ ├── Adjectives.txt │ │ │ └── Contents.json │ │ │ ├── Animals.dataset │ │ │ ├── Animals.txt │ │ │ └── Contents.json │ │ │ └── Contents.json │ ├── Restore Images │ │ ├── Models │ │ │ ├── SoftwareVersion.swift │ │ │ ├── VBRestoreImageInfo.swift │ │ │ └── VBRestoreImagesResponse.swift │ │ ├── VBAPIClient.swift │ │ └── VBDownloader.swift │ ├── Settings │ │ ├── VBSettings.swift │ │ └── VBSettingsContainer.swift │ ├── Utilities │ │ ├── Bundle+Version.swift │ │ ├── LogStreamer.swift │ │ ├── ProcessInfo+ECID.swift │ │ ├── VBMemoryLeakDebugAssertions.swift │ │ ├── VolumeUtils.swift │ │ └── WeakReference.swift │ └── Virtualization │ │ ├── Helpers │ │ ├── DirectoryObserver.swift │ │ ├── DiskImageGenerator.swift │ │ ├── LinuxVirtualMachineConfigurationHelper.swift │ │ ├── MacOSVirtualMachineConfigurationHelper.swift │ │ ├── RandomNameGenerator.swift │ │ ├── VBDebugUtil.h │ │ ├── VBDebugUtil.m │ │ ├── VZVirtualMachineConfiguration+NVRAM.swift │ │ └── VirtualMachineConfigurationHelper.swift │ │ ├── Screenshot │ │ ├── NSImage+HEIC.swift │ │ ├── NSImage+VMScreenshot.swift │ │ ├── VMScreenshotTimingController.swift │ │ ├── VZGraphicsDisplay+Screenshot.h │ │ └── VZGraphicsDisplay+Screenshot.m │ │ ├── VBVirtualMachine+Virtualization.swift │ │ ├── VMController.swift │ │ ├── VMInstance.swift │ │ ├── VMLibraryController.swift │ │ └── VMSavedStatesController.swift └── VirtualCore.h ├── VirtualUI ├── Resources │ └── VirtualUI.xcassets │ │ ├── Contents.json │ │ ├── StatusItemPanelChromeBorder.colorset │ │ └── Contents.json │ │ ├── ThumbnailPlaceholder.imageset │ │ ├── Contents.json │ │ ├── ThumbnailPlaceholder.png │ │ └── ThumbnailPlaceholder@2x.png │ │ └── VBGuestType │ │ ├── Contents.json │ │ ├── linux.imageset │ │ ├── Contents.json │ │ └── icon-linux.pdf │ │ └── mac.imageset │ │ ├── Contents.json │ │ └── icon-mac.pdf ├── Source │ ├── Building Blocks │ │ ├── CGFloat+OnePixel.swift │ │ ├── Configuration Controls │ │ │ ├── EphemeralTextField.swift │ │ │ ├── NumericPropertyControl.swift │ │ │ ├── NumericValueField.swift │ │ │ ├── PropertyControl.swift │ │ │ └── SharedFocusEnvironment.swift │ │ ├── ControlGroupChrome.swift │ │ ├── FixSwiftUIMaterialInPreviews.m │ │ └── SliderConversion.swift │ ├── Components │ │ ├── DecentFormView.swift │ │ ├── HostingWindowController │ │ │ ├── HostingWindowController.swift │ │ │ ├── OpenCocoaWindowAction.swift │ │ │ ├── VBRestorableWindow+Resizing.swift │ │ │ ├── VBRestorableWindow.swift │ │ │ └── WindowEnvironment.swift │ │ ├── LogConsole.swift │ │ ├── MaterialView.swift │ │ ├── NSAlert+Confirmation.swift │ │ ├── OnAppearOnce.swift │ │ ├── OpenSavePanelUtils.swift │ │ ├── SelfSizingGroupedForm.swift │ │ └── SwiftUI Status Item │ │ │ ├── Components │ │ │ ├── Cocoa │ │ │ │ ├── StatusBarContentPanel.swift │ │ │ │ ├── StatusBarHighlightView.swift │ │ │ │ ├── StatusItemMenuBarExtraView.swift │ │ │ │ ├── StatusItemPanelContentController.swift │ │ │ │ └── VUIAppKitViewControllerHost.swift │ │ │ ├── ObjC │ │ │ │ ├── NSApplication+MenuBar.h │ │ │ │ ├── NSApplication+MenuBar.m │ │ │ │ ├── NSStatusBarPrivate.h │ │ │ │ ├── NSStatusItem+.h │ │ │ │ └── NSStatusItem+.m │ │ │ ├── ScreenChangeModifier.swift │ │ │ ├── StatusBarPanelChrome.swift │ │ │ ├── StatusItemButton.swift │ │ │ └── StatusItemProviderProtocol.swift │ │ │ └── StatusItemManager.swift │ ├── Definitions │ │ ├── PreviewSupport-VirtualUI.swift │ │ └── VirtualUIConstants.swift │ ├── Installer │ │ ├── Components │ │ │ ├── AuthenticatingWebView.swift │ │ │ ├── InstallationConsole.swift │ │ │ ├── InstallationWizardTitle.swift │ │ │ └── VirtualMachineNameField.swift │ │ ├── Steps │ │ │ ├── GuestTypePicker.swift │ │ │ ├── InstallConfigurationStepView.swift │ │ │ ├── InstallMethod.swift │ │ │ ├── InstallMethodPicker.swift │ │ │ ├── InstallProgressStepView.swift │ │ │ ├── RestoreImageDownloadView.swift │ │ │ └── RestoreImagePicker.swift │ │ ├── VMInstallationViewModel.swift │ │ └── VMInstallationWizard.swift │ ├── Library │ │ ├── Components │ │ │ └── LibraryItemView.swift │ │ └── LibraryView.swift │ ├── Session │ │ ├── Components │ │ │ ├── ContinuousProgressIndicator.swift │ │ │ ├── MaskProgressView.swift │ │ │ ├── NumberDisplayMode.swift │ │ │ ├── SavedStatePicker.swift │ │ │ ├── SwiftUIVMView.swift │ │ │ ├── VMProgressOverlay.swift │ │ │ ├── VMScreenshotter.swift │ │ │ └── VirtualMachineControls.swift │ │ ├── Configuration │ │ │ └── VMSessionConfigurationView.swift │ │ ├── VirtualMachineSessionUI.swift │ │ ├── VirtualMachineSessionUIManager.swift │ │ └── VirtualMachineSessionView.swift │ ├── Settings │ │ ├── DeepLinkAuthManagementUI.swift │ │ └── PreferencesView.swift │ └── VM Configuration │ │ ├── Components │ │ ├── ConfigurationSection.swift │ │ └── GroupedList.swift │ │ ├── Sections │ │ ├── DisplayConfigurationView.swift │ │ ├── HardwareConfigurationView.swift │ │ ├── KeyboardDeviceConfigurationView.swift │ │ ├── NetworkConfigurationView.swift │ │ ├── PointingDeviceConfigurationView.swift │ │ ├── Sharing │ │ │ ├── SharedFolderListItem.swift │ │ │ ├── SharedFoldersManagementView.swift │ │ │ └── SharingConfigurationView.swift │ │ ├── SoundConfigurationView.swift │ │ └── Storage │ │ │ ├── ManagedDiskImageEditor.swift │ │ │ ├── StorageConfigurationView.swift │ │ │ └── StorageDeviceDetailView.swift │ │ ├── VMConfigurationSheet.swift │ │ ├── VMConfigurationView.swift │ │ └── VMConfigurationViewModel.swift └── VirtualUI.h ├── VirtualWormhole ├── Source │ ├── Definitions │ │ └── VirtualWormholeConstants.swift │ ├── Services │ │ ├── Base │ │ │ ├── WormholeServiceClient.swift │ │ │ └── WormholeServiceProtocol.swift │ │ ├── DarwinNotifications │ │ │ ├── SystemNotification.swift │ │ │ └── WHDarwinNotificationsService.swift │ │ ├── DefaultsImport │ │ │ ├── Implementation │ │ │ │ ├── DefaultsDomain+ExportImport.swift │ │ │ │ ├── DefaultsDomainDescriptor.swift │ │ │ │ └── DefaultsImportController.swift │ │ │ ├── Resources │ │ │ │ └── DefaultsDomains.plist │ │ │ ├── WHDefaultsImportClient.swift │ │ │ └── WHDefaultsImportService.swift │ │ └── WHSharedClipboardService.swift │ ├── WireProtocol │ │ ├── WHPayload.swift │ │ ├── WHPing.swift │ │ └── WormholePacket.swift │ └── WormholeManager.swift └── VirtualWormhole.h ├── VirtualWormholeTests ├── Resources │ └── TestStream.bin └── WormholePacketTests.swift ├── assets ├── DeviceSupport.png ├── GuestApp.jpg ├── Showcase.jpg └── VirtualBuddyIcon.png └── data ├── ipsws_v1.json └── linux_v1.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: insidegui 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __MACOSX 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | xcuserdata 12 | profile 13 | *.moved-aside 14 | DerivedData 15 | .idea/ 16 | Crashlytics.sh 17 | generatechangelog.sh 18 | Pods/ 19 | Carthage 20 | Provisioning 21 | Crashlytics.sh 22 | Sharing.h 23 | Tests/Private 24 | Design/Icon 25 | Build/ 26 | MockServer/ -------------------------------------------------------------------------------- /DeepLinkSecurity/DeepLinkSecurity.h: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkSecurity.h 3 | // DeepLinkSecurity 4 | // 5 | // Created by Guilherme Rambo on 12/10/23. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for DeepLinkSecurity. 11 | FOUNDATION_EXPORT double DeepLinkSecurityVersionNumber; 12 | 13 | //! Project version string for DeepLinkSecurity. 14 | FOUNDATION_EXPORT const unsigned char DeepLinkSecurityVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Base/DeepLinkSecurityDefines.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | struct DeepLinkSecurityDefines { 5 | static let subsystem = "codes.rambo.DeepLinkSecurity" 6 | } 7 | 8 | struct DeepLinkError: LocalizedError { 9 | var errorDescription: String? 10 | init(_ errorDescription: String) { 11 | self.errorDescription = errorDescription 12 | } 13 | } 14 | 15 | extension Logger { 16 | static func deepLinkLogger(for type: T.Type) -> Logger { 17 | Logger(subsystem: DeepLinkSecurityDefines.subsystem, category: String(describing: type)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Models/DeepLinkClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CryptoKit 3 | 4 | /// Represents an app/process that's attempting to open a deep link in the app. 5 | public struct DeepLinkClient: Identifiable { 6 | /// The client ID is a SHA256 hash of its designated CS requirement. 7 | public var id: String 8 | /// The bundle URL for the app or executable URL for the process. 9 | public var url: URL 10 | /// The designated code signing requirement for the client. 11 | /// This is hashed and used as a key to store the user's decision, 12 | /// and it's also used in order to verify that the client's code signature is valid. 13 | public var designatedRequirement: String 14 | 15 | public init(url: URL, designatedRequirement: String) { 16 | self.url = url 17 | self.designatedRequirement = designatedRequirement 18 | self.id = SHA256.hash(data: Data(designatedRequirement.utf8)) 19 | .map { String(format: "%02X", $0) } 20 | .joined() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Models/DeepLinkClientDescriptor.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | /// Describes metadata for a client that's previously requested deep link authorization. 4 | /// See ``DeepLinkManagementStore``. 5 | public struct DeepLinkClientDescriptor: Identifiable, Hashable, Codable { 6 | public struct Icon: Hashable, Codable { 7 | public var image: NSImage 8 | } 9 | 10 | /// Unique identifier for the client. 11 | public var id: String 12 | /// The client's main bundle or executable URL. 13 | public var url: URL 14 | /// The client's bundle identifier, if available. 15 | public var bundleIdentifier: String? 16 | /// A user-friendly name for the client. 17 | public var displayName: String 18 | /// Icon image representing the client app or executable. 19 | public var icon: Icon 20 | /// The current authorization state for the client. 21 | public var authorization: DeepLinkClientAuthorization 22 | /// Will be `false` if the descriptor's client could no longer be found on the filesystem. 23 | public var isValid: Bool 24 | } 25 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Models/OpenDeepLinkRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a client's request for opening a deep link in the app. 4 | public struct OpenDeepLinkRequest { 5 | /// The URL for the deep link the client is trying to open. 6 | public var url: URL 7 | /// The client model used for authentication. 8 | public var client: DeepLinkClientDescriptor 9 | 10 | public init(url: URL, client: DeepLinkClientDescriptor) { 11 | self.url = url 12 | self.client = client 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Storage/DeepLinkAuthStore.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | /// Describes a user's decision about a client opening deep links in the app. 4 | public enum DeepLinkClientAuthorization: Int, Codable, CustomStringConvertible { 5 | /// The user has not granted/rejected the client yet. 6 | /// Also used as a fallback when something goes wrong in the process of 7 | /// authenticating a previously authorized/denied client. 8 | case undetermined 9 | /// The user has granted authorization to the client. 10 | case authorized 11 | /// The user has denied authorization for the client. 12 | case denied 13 | } 14 | 15 | public extension DeepLinkClientAuthorization { 16 | var description: String { 17 | switch self { 18 | case .undetermined: 19 | return "undetermined" 20 | case .authorized: 21 | return "authorized" 22 | case .denied: 23 | return "denied" 24 | } 25 | } 26 | } 27 | 28 | /// Implemented by types that can provide persistence for user's decision regarding the opening of deep links from other apps. 29 | /// See ``MemoryDeepLinkAuthStore`` and ``KeychainDeepLinkAuthStore``. 30 | public protocol DeepLinkAuthStore { 31 | func authorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization 32 | func setAuthorization(_ authorization: DeepLinkClientAuthorization, for client: DeepLinkClient) async throws 33 | } 34 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Storage/DeepLinkManagementStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Implemented by types that can provide persistence for a list of authorized/denied deep link clients, 4 | /// so that a UI can be assembled showing the user their previous decisions and allowing users to change their mind. 5 | public protocol DeepLinkManagementStore { 6 | /// Returns all deep link client descriptors previously added using ``insert(_:)``. 7 | nonisolated func clientDescriptors() -> AsyncStream<[DeepLinkClientDescriptor]> 8 | 9 | /// Whether the store currently has a descriptor with the specified identifier. 10 | func hasDescriptor(with id: DeepLinkClientDescriptor.ID) async -> Bool 11 | 12 | /// Upserts a client descriptor. 13 | func insert(_ descriptor: DeepLinkClientDescriptor) async throws 14 | 15 | /// Deletes an existing descriptor. 16 | func delete(_ descriptor: DeepLinkClientDescriptor) async throws 17 | } 18 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/Storage/MemoryDeepLinkAuthStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | /// A very basic store that uses an in-memory dictionary and is destroyed when the app terminates. Useful for testing. 5 | public final actor MemoryDeepLinkAuthStore: DeepLinkAuthStore { 6 | 7 | private lazy var logger = Logger.deepLinkLogger(for: Self.self) 8 | 9 | private var authorizationByClientRequirement = [String: DeepLinkClientAuthorization]() 10 | 11 | public init() { } 12 | 13 | public func authorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization { 14 | if let result = authorizationByClientRequirement[client.designatedRequirement] { 15 | logger.debug("Found existing authorization \(result) for \(client.designatedRequirement)") 16 | return result 17 | } else { 18 | logger.debug("No authorization in store for \(client.designatedRequirement), returning undetermined") 19 | return .undetermined 20 | } 21 | } 22 | 23 | public func setAuthorization(_ authorization: DeepLinkClientAuthorization, for client: DeepLinkClient) async throws { 24 | logger.debug("Setting authorization \(authorization) for \(client.designatedRequirement)") 25 | 26 | authorizationByClientRequirement[client.designatedRequirement] = authorization 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /DeepLinkSecurity/Source/UI/DeepLinkAuthUI.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public protocol DeepLinkAuthUI: AnyObject { 4 | /// Return ``DeepLinkClientAuthorization/authorized`` if the user has allowed the client to open deep links in the app. 5 | /// 6 | /// If this method throws, then the auth store is not modified and the user will be prompted again the next time the same client 7 | /// attempts to open a deep link in the app. 8 | /// 9 | /// If this method returns ``DeepLinkClientAuthorization/denied``, then the auth store will be modified and future 10 | /// requests from the same client will be rejected without a prompt. 11 | @MainActor 12 | func presentDeepLinkAuth(for request: OpenDeepLinkRequest) async throws -> DeepLinkClientAuthorization 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Guilherme Rambo 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /ReleaseNotes/VirtualBuddy 1.2 Release Nodes.md: -------------------------------------------------------------------------------- 1 | # What's new in VirtualBuddy 1.2 2 | 3 | - Managing virtual machines can now be done entirely within the library view, the contextual menu offers options for renaming, deleting, duplicating, and showing the VM in Finder 4 | - The library now sorts virtual machines by creation date, in reverse chronological order 5 | - Virtual machines can now be configured with custom CPU, RAM, storage devices, network devices, displays, and many other options 6 | - The option to capture system keyboard shortcuts is now persisted for each virtual machine in the library 7 | - Adds support for shared folders to share specific directories from your Mac with the virtual machine 8 | - Adds support for bridged networking, allowing a physical network interface from your Mac to be exposed to the virtual machine 9 | - Additional storage can now be added to virtual machines by creating new disk images from within VirtualBuddy 10 | - A new debug console showing logs related to the installation process is now available while installing macOS in a new virtual machine 11 | - The default library directory for new installs is now ~/Library/Application Support/VirtualBuddy (this is where VirtualBuddy stores virtual machines and downloads) 12 | - Clicking a virtual machine that's already open in the library will now correctly focus the existing window for that virtual machine -------------------------------------------------------------------------------- /ReleaseNotes/VirtualBuddy 1.2.1 Release Notes.md: -------------------------------------------------------------------------------- 1 | # New in VirtualBuddy 1.2.1 2 | 3 | ### General improvements to the installer user interface: 4 | 5 | - Addresses an issue that caused the installer to clip the configuration user interface, hiding the "continue" button 6 | - It is now possible to navigate using the arrow keys when selecting the installation method 7 | - Text fields now use the same consistent style 8 | - The virtual machine name button is automatically focused as expected 9 | - Command + R can be used to generate a new random name while editing the virtual machine's name during installation -------------------------------------------------------------------------------- /ReleaseNotes/VirtualBuddy 1.2.2 Release Notes.md: -------------------------------------------------------------------------------- 1 | # Fixed in VirtualBuddy 1.2.2 2 | 3 | - Makes custom IPSW URL validation less strict, allowing downloads from plain HTTP URLs and URLs that don't end in .ipsw 4 | - Addresses an issue that caused audio input to not work in virtual machines; VirtualBuddy will ask for microphone access the first time audio input is used within a virtual machine 5 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "5aabd6ca94084da6e618f041d74475ec3f152216f0ee77b4939a37c8bef3ee17", 3 | "pins" : [ 4 | { 5 | "identity" : "sparkle", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sparkle-project/Sparkle", 8 | "state" : { 9 | "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", 10 | "version" : "2.7.0" 11 | } 12 | }, 13 | { 14 | "identity" : "swiftui-introspect", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/insidegui/swiftui-introspect.git", 17 | "state" : { 18 | "branch" : "macOS15", 19 | "revision" : "65046b6a298c469bfbf49fbb4531bd09d9b53428" 20 | } 21 | }, 22 | { 23 | "identity" : "urlqueryitemcoder", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/kylehughes/URLQueryItemCoder.git", 26 | "state" : { 27 | "revision" : "a828b3bb5b273adf85a3ca95d9705987cb2de7c7", 28 | "version" : "1.0.0" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/xcshareddata/xcschemes/DeepLinkSecurity.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualCore.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.551", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddy/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualBuddy/Automation/VirtualBuddyDeepLinks.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import URLQueryItemCoder 3 | import DeepLinkSecurity 4 | import OSLog 5 | import VirtualCore 6 | 7 | struct OpenVMParameters: Codable { 8 | var name: String 9 | } 10 | 11 | struct BootVMParameters: Codable { 12 | var name: String 13 | var options: VMSessionOptions? 14 | } 15 | 16 | struct StopVMParameters: Codable { 17 | var name: String 18 | } 19 | 20 | enum DeepLinkAction { 21 | case open(OpenVMParameters) 22 | case boot(BootVMParameters) 23 | case stop(StopVMParameters) 24 | } 25 | 26 | extension DeepLinkAction { 27 | init(_ url: URL) throws { 28 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { 29 | throw Failure("Invalid URL: failed to construct URLComponents") 30 | } 31 | guard let host = components.host else { 32 | throw Failure("Invalid URL: missing host") 33 | } 34 | 35 | switch host { 36 | case "open": 37 | let params = try Self.decodeParameters(OpenVMParameters.self, from: components) 38 | self = .open(params) 39 | case "boot": 40 | let params = try Self.decodeParameters(BootVMParameters.self, from: components) 41 | self = .boot(params) 42 | case "stop": 43 | let params = try Self.decodeParameters(StopVMParameters.self, from: components) 44 | self = .stop(params) 45 | default: 46 | throw Failure("Unrecognized URL action \"\(host)\"") 47 | } 48 | } 49 | 50 | private static func decodeParameters(_ type: T.Type, from components: URLComponents) throws -> T where T: Decodable { 51 | let items = components.queryItems ?? [] 52 | return try URLQueryItemDecoder.deepLink.decode(type, from: items) 53 | } 54 | } 55 | 56 | private extension URLQueryItemDecoder { 57 | static let deepLink = URLQueryItemDecoder() 58 | } 59 | -------------------------------------------------------------------------------- /VirtualBuddy/Bootstrap/SoftwareUpdateController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoftwareUpdateController.swift 3 | // VirtualBuddy 4 | // 5 | // Created by Guilherme Rambo on 25/06/22. 6 | // 7 | 8 | import Foundation 9 | import VirtualCore 10 | 11 | #if ENABLE_SPARKLE 12 | import Sparkle 13 | #endif 14 | 15 | final class SoftwareUpdateController: NSObject, ObservableObject { 16 | 17 | static let shared = SoftwareUpdateController() 18 | 19 | private var settings: VBSettings { VBSettingsContainer.current.settings } 20 | 21 | @Published var automaticUpdatesEnabled = true { 22 | didSet { 23 | #if ENABLE_SPARKLE 24 | guard automaticUpdatesEnabled != oldValue else { return } 25 | 26 | updateController.updater.automaticallyChecksForUpdates = automaticUpdatesEnabled 27 | #endif 28 | } 29 | } 30 | 31 | #if ENABLE_SPARKLE 32 | private lazy var updateController: SPUStandardUpdaterController = { 33 | SPUStandardUpdaterController( 34 | startingUpdater: false, 35 | updaterDelegate: self, 36 | userDriverDelegate: self 37 | ) 38 | }() 39 | #endif 40 | 41 | func activate() { 42 | #if ENABLE_SPARKLE 43 | updateController.startUpdater() 44 | automaticUpdatesEnabled = updateController.updater.automaticallyChecksForUpdates 45 | registerForUpdateChannelChanges() 46 | #endif 47 | } 48 | 49 | @objc func checkForUpdates(_ sender: Any?) { 50 | #if ENABLE_SPARKLE 51 | updateController.checkForUpdates(sender) 52 | #else 53 | let alert = NSAlert() 54 | alert.messageText = "Updating Disabled" 55 | alert.informativeText = "This build doesn't include Sparkle updates." 56 | alert.runModal() 57 | #endif 58 | } 59 | 60 | private func registerForUpdateChannelChanges() { 61 | NotificationCenter.default.addObserver( 62 | self, 63 | selector: #selector(handleUpdateChannelChanged), 64 | name: VBSettings.updateChannelDidChangeNotification, 65 | object: nil 66 | ) 67 | } 68 | 69 | /// Check for updates when switching from release to beta channel. 70 | @objc private func handleUpdateChannelChanged(_ note: Notification) { 71 | guard let channel = note.object as? AppUpdateChannel else { return } 72 | guard channel != .release else { return } 73 | 74 | checkForUpdates(nil) 75 | } 76 | 77 | } 78 | 79 | #if ENABLE_SPARKLE 80 | extension SoftwareUpdateController: SPUUpdaterDelegate, SPUStandardUserDriverDelegate { 81 | 82 | func feedURLString(for updater: SPUUpdater) -> String? { 83 | settings.updateChannel.appCastURL.absoluteString 84 | } 85 | 86 | func allowedChannels(for updater: SPUUpdater) -> Set { 87 | [settings.updateChannel.id] 88 | } 89 | 90 | } 91 | #endif 92 | -------------------------------------------------------------------------------- /VirtualBuddy/Bootstrap/VirtualBuddyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualBuddyApp.swift 3 | // VirtualBuddy 4 | // 5 | // Created by Guilherme Rambo on 07/04/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | import VirtualUI 11 | 12 | let kShellAppSubsystem = "codes.rambo.VirtualBuddy" 13 | 14 | @main 15 | struct VirtualBuddyApp: App { 16 | @NSApplicationDelegateAdaptor 17 | var appDelegate: VirtualBuddyAppDelegate 18 | 19 | private var settingsContainer: VBSettingsContainer { appDelegate.settingsContainer } 20 | private var updateController: SoftwareUpdateController { appDelegate.updateController } 21 | private var library: VMLibraryController { appDelegate.library } 22 | private var sessionManager: VirtualMachineSessionUIManager { appDelegate.sessionManager } 23 | 24 | @Environment(\.openWindow) 25 | private var openWindow 26 | 27 | @StateObject private var updatesController = SoftwareUpdateController.shared 28 | 29 | private let mainWindowTitle: String = Bundle.main.vbFullVersionDescription 30 | 31 | var body: some Scene { 32 | Window(Text(mainWindowTitle), id: .vb_libraryWindowID) { 33 | LibraryView() 34 | .onAppearOnce(perform: updateController.activate) 35 | .environmentObject(library) 36 | .environmentObject(sessionManager) 37 | } 38 | .windowToolbarStyle(.unified) 39 | .commands { 40 | #if ENABLE_SPARKLE 41 | CommandGroup(after: .appInfo) { 42 | Button("Check for Updates…") { 43 | updateController.checkForUpdates(nil) 44 | } 45 | } 46 | #endif 47 | 48 | CommandGroup(before: .windowSize) { 49 | VirtualMachineWindowCommands() 50 | .environmentObject(sessionManager) 51 | } 52 | 53 | CommandGroup(after: .windowArrangement) { 54 | Button("Library") { 55 | openWindow(id: .vb_libraryWindowID) 56 | } 57 | .keyboardShortcut(KeyEquivalent("0"), modifiers: .command) 58 | } 59 | } 60 | 61 | Settings { 62 | PreferencesView(deepLinkSentinel: DeepLinkHandler.shared.sentinel, enableAutomaticUpdates: $updatesController.automaticUpdatesEnabled) 63 | .environmentObject(settingsContainer) 64 | .frame(minWidth: 420, maxWidth: .infinity, minHeight: 370, maxHeight: .infinity) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Entitlements/VirtualBuddy.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.audio-input 6 | 7 | com.apple.security.virtualization 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Entitlements/VirtualBuddy_Managed.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.audio-input 6 | 7 | com.apple.security.virtualization 8 | 9 | com.apple.vm.networking 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Features.xcconfig: -------------------------------------------------------------------------------- 1 | // ENABLE_SPARKLE = Enables building with Sparkle for automatic updates 2 | // ENABLE_USERDEFAULTS_SYNC = Enables the user defaults sync feature 3 | OTHER_SWIFT_FLAGS = -D ENABLE_SPARKLE $(inherited) 4 | 5 | // The BETA flag must be present in all targets, hence why these are here instead of in AppTarget.xcconfig 6 | OTHER_SWIFT_FLAGS[config=Beta_Debug][sdk=*][arch=*] = -D BETA $(inherited) 7 | OTHER_SWIFT_FLAGS[config=Beta_Release][sdk=*][arch=*] = -D BETA $(inherited) 8 | 9 | VB_SPARKLE_PUBLIC_ED_KEY=dj8ljUPnwoLj/dLs6HyJg5Ayw+t8zWtgjQUfQsH56ww= 10 | VB_SPARKLE_CHECK_INTERVAL=86400 11 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/InfoPlist.xcconfig: -------------------------------------------------------------------------------- 1 | INFOPLIST_KEY_NSHumanReadableCopyright = © 2024 Buddy Software LTD 2 | INFOPLIST_KEY_NSMicrophoneUsageDescription = Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio. 3 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Main.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Paths.xcconfig" 2 | #include "Versions.xcconfig" 3 | #include "Signing.xcconfig" 4 | #include "Features.xcconfig" 5 | #include "InfoPlist.xcconfig" 6 | 7 | ARCHS=arm64 8 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Paths.xcconfig: -------------------------------------------------------------------------------- 1 | ENTITLEMENTS_DIR=VirtualBuddy/Config/Entitlements 2 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Signing.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = Apple Development 2 | VB_BUNDLE_ID_PREFIX = 3 | 4 | GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID = $(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuestHelper 5 | GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID_STR=@"$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)" 6 | 7 | GCC_PREPROCESSOR_DEFINITIONS=$(inherited) GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID='$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID_STR)' 8 | -------------------------------------------------------------------------------- /VirtualBuddy/Config/Versions.xcconfig: -------------------------------------------------------------------------------- 1 | MARKETING_VERSION = 1.7.1 2 | CURRENT_PROJECT_VERSION = 133 3 | DYLIB_CURRENT_VERSION = $(CURRENT_PROJECT_VERSION) 4 | VERSIONING_SYSTEM = apple-generic 5 | MACOSX_DEPLOYMENT_TARGET = 13.0 6 | -------------------------------------------------------------------------------- /VirtualBuddy/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.551", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menubar.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "menubar@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | }, 18 | "properties" : { 19 | "template-rendering-intent" : "template" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/menubar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/menubar.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/menubar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/menubar@2x.png -------------------------------------------------------------------------------- /VirtualBuddyGuest/Dashboard/HostConnectionStateProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import VirtualWormhole 3 | 4 | protocol HostConnectionStateProvider: ObservableObject { 5 | var isConnected: Bool { get } 6 | } 7 | 8 | extension WormholeManager: HostConnectionStateProvider { } 9 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Dashboard/Support/GuestLaunchAtLoginManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceManagement 3 | import OSLog 4 | 5 | final class GuestLaunchAtLoginManager: ObservableObject { 6 | 7 | private lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: Self.self)) 8 | 9 | var isLaunchAtLoginEnabled: Bool { SMAppService.guestHelper.status == .enabled } 10 | 11 | func setLaunchAtLoginEnabled(_ enabled: Bool) async throws { 12 | logger.debug("Set launch at login enabled: \(enabled, privacy: .public)") 13 | 14 | if enabled { 15 | try SMAppService.guestHelper.register() 16 | } else { 17 | try await SMAppService.guestHelper.unregister() 18 | } 19 | 20 | await MainActor.run { objectWillChange.send() } 21 | } 22 | 23 | private var hasAutoEnabled: Bool { 24 | get { UserDefaults.standard.bool(forKey: #function) } 25 | set { 26 | UserDefaults.standard.set(newValue, forKey: #function) 27 | UserDefaults.standard.synchronize() 28 | } 29 | } 30 | 31 | func autoEnableIfNeeded() { 32 | guard !hasAutoEnabled else { return } 33 | hasAutoEnabled = true 34 | 35 | logger.debug("Attempting to auto-enable launch at login") 36 | 37 | Task { 38 | do { 39 | try await setLaunchAtLoginEnabled(true) 40 | 41 | logger.debug("Successfully auto-enabled launch at login") 42 | } catch { 43 | logger.error("Failed to auto-enable launch at login: \(error, privacy: .public)") 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | @available(macOS 13.0, *) 51 | private extension SMAppService { 52 | static let guestHelper = SMAppService.loginItem(identifier: kGuestAppLaunchAtLoginHelperBundleID) 53 | } 54 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #define kGuestAppLaunchAtLoginHelperBundleID GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID 2 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/GuestAppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import VirtualUI 4 | import VirtualWormhole 5 | 6 | @NSApplicationMain 7 | final class GuestAppDelegate: NSObject, NSApplicationDelegate { 8 | 9 | private lazy var launchAtLoginManager = GuestLaunchAtLoginManager() 10 | 11 | private lazy var sharedFolders = GuestSharedFoldersManager() 12 | 13 | private lazy var dashboardItem: StatusItemManager = { 14 | StatusItemManager( 15 | configuration: .default.id("dashboard"), 16 | statusItem: .button(label: { Image("StatusItem") }), 17 | content: GuestDashboard() 18 | .environmentObject(self.launchAtLoginManager) 19 | .environmentObject(WormholeManager.sharedGuest) 20 | .environmentObject(self.sharedFolders) 21 | ) 22 | }() 23 | 24 | private var shouldShowPanelAfterLaunching: Bool { 25 | get { !UserDefaults.standard.bool(forKey: "shownPanelOnFirstLauch") || UserDefaults.standard.bool(forKey: "ShowPanelOnLaunch") } 26 | set { 27 | UserDefaults.standard.set(!newValue, forKey: "shownPanelOnFirstLauch") 28 | UserDefaults.standard.synchronize() 29 | } 30 | } 31 | 32 | private let installer = GuestAppInstaller() 33 | 34 | func applicationWillFinishLaunching(_ notification: Notification) { 35 | do { 36 | try installer.installIfNeeded() 37 | } catch { 38 | let alert = NSAlert(error: error) 39 | alert.runModal() 40 | } 41 | } 42 | 43 | func applicationDidFinishLaunching(_ notification: Notification) { 44 | /// Skip regular app activation if installation is needed (i.e. running from disk image). 45 | guard !installer.needsInstall else { return } 46 | 47 | launchAtLoginManager.autoEnableIfNeeded() 48 | 49 | WormholeManager.sharedGuest.activate() 50 | 51 | Task { 52 | try? await sharedFolders.mount() 53 | } 54 | 55 | dashboardItem.install() 56 | 57 | perform(#selector(showPanelForFirstLaunchIfNeeded), with: nil, afterDelay: 0.5) 58 | } 59 | 60 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 61 | showPanel() 62 | 63 | return true 64 | } 65 | 66 | @objc private func showPanelForFirstLaunchIfNeeded() { 67 | guard shouldShowPanelAfterLaunching else { return } 68 | shouldShowPanelAfterLaunching = false 69 | 70 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(showPanelForFirstLaunchIfNeeded), object: nil) 71 | 72 | showPanel() 73 | } 74 | 75 | @objc private func showPanel() { 76 | dashboardItem.showPanel() 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualBuddyGuest/VirtualBuddyGuest.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /VirtualBuddyGuestHelper/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /VirtualBuddyGuestHelper/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /VirtualBuddyGuestHelper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualBuddyGuestHelper/GuestHelperAppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import os.log 3 | 4 | @main 5 | final class GuestHelperAppDelegate: NSObject, NSApplicationDelegate { 6 | 7 | private let log = OSLog(subsystem: "codes.rambo.VirtualBuddyGuestHelper", category: String(describing: GuestHelperAppDelegate.self)) 8 | 9 | func applicationDidFinishLaunching(_ aNotification: Notification) { 10 | let config = NSWorkspace.OpenConfiguration() 11 | config.activates = false 12 | config.addsToRecentItems = false 13 | config.promptsUserIfNeeded = false 14 | 15 | NSWorkspace.shared.openApplication( 16 | at: Bundle.main.mainAppBundleURL, 17 | configuration: config) { _, error in 18 | if let error = error { 19 | os_log("Failed to launch main app: %{public}@", log: self.log, type: .fault, String(describing: error)) 20 | } else { 21 | os_log("Main app launched successfully", log: self.log, type: .info) 22 | } 23 | 24 | DispatchQueue.main.async { NSApp?.terminate(nil) } 25 | } 26 | } 27 | 28 | } 29 | 30 | extension Bundle { 31 | 32 | var mainAppBundleURL: URL { 33 | bundleURL 34 | .deletingLastPathComponent() // VirtualBuddyGuestHelper.app 35 | .deletingLastPathComponent() // LoginItems 36 | .deletingLastPathComponent() // Library 37 | .deletingLastPathComponent() // Contents 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VirtualCore/Source/Definitions/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 05/06/22. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | extension Error { 12 | var log: String { String(describing: self) } 13 | } 14 | 15 | public extension Logger { 16 | init(for type: T.Type, label: String? = nil) { 17 | let suffix = label.flatMap { "(\($0))" } ?? "" 18 | self.init(subsystem: VirtualCoreConstants.subsystemName, category: "\(String(describing: type))\(suffix)") 19 | } 20 | } 21 | 22 | extension Logger { 23 | func assert(_ message: String) { 24 | fault("\(message, privacy: .public)") 25 | assertionFailure(message) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /VirtualCore/Source/Definitions/VirtualCoreConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualCoreConstants.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 02/06/22. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | struct VirtualCoreConstants { 12 | static let subsystemName = "codes.rambo.VirtualCore" 13 | } 14 | 15 | private final class _VirtualCoreStub { } 16 | 17 | public extension Bundle { 18 | static let virtualCore = Bundle(for: _VirtualCoreStub.self) 19 | } 20 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/Configuration/ConfigurationModels+Summary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationModels+Summary.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 19/07/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension VBMacConfiguration { 11 | 12 | var generalSummary: String { 13 | "\(hardware.cpuCount) CPUs / \(hardware.memorySize / 1024 / 1024 / 1024) GB" 14 | } 15 | 16 | var storageSummary: String { 17 | if hardware.storageDevices.count > 1 { 18 | return "\(hardware.storageDevices.count) Devices" 19 | } else { 20 | return "Boot Only" 21 | } 22 | } 23 | 24 | var displaySummary: String { 25 | guard let display = hardware.displayDevices.first else { return "No Displays" } 26 | return "\(display.width)x\(display.height)x\(display.pixelsPerInch)" 27 | } 28 | 29 | var soundSummary: String { 30 | guard let sound = hardware.soundDevices.first else { return "No Sound" } 31 | return sound.enableInput ? "Input / Output" : "Output Only" 32 | } 33 | 34 | var sharingSummary: String { 35 | let foldersSum: String 36 | if sharedFolders.count > 1 { 37 | foldersSum = "\(sharedFolders.count) Folders" 38 | } else if sharedFolders.isEmpty { 39 | foldersSum = "" 40 | } else { 41 | foldersSum = "One Folder" 42 | } 43 | 44 | return foldersSum.isEmpty ? "None" : foldersSum 45 | } 46 | 47 | var networkSummary: String { 48 | guard let network = hardware.networkDevices.first else { return "No Network" } 49 | return network.kind.name 50 | } 51 | 52 | var pointingDeviceSummary: String { hardware.pointingDevice.kind.name } 53 | 54 | var keyboardDeviceSummary: String { hardware.keyboardDevice.kind.name } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/Configuration/VBMacDevice+Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBMacDevice+Storage.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension VBMacDevice { 11 | 12 | mutating func addOrUpdate(_ storage: VBStorageDevice) { 13 | if let idx = storageDevices.firstIndex(where: { $0.id == storage.id }) { 14 | storageDevices[idx] = storage 15 | } else { 16 | storageDevices.append(storage) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/SavedState/VBSavedStateMetadata+Clone.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension VBSavedStateMetadata { 4 | static func createStorageDeviceClones(packageURL: URL, model: VBVirtualMachine) async throws -> [VBStorageDevice] { 5 | let inputDevices = model.configuration.hardware.storageDevices 6 | var outputDevices = [VBStorageDevice]() 7 | 8 | for var device in inputDevices { 9 | guard case .managedImage = device.backing else { 10 | /// Custom images are arbirary, may be anywhere on disk or external storage. 11 | /// Such images are managed by the user, not the app, and thus are not cloned when saving state. 12 | outputDevices.append(device) 13 | continue 14 | } 15 | 16 | let inputURL: URL = model.diskImageURL(for: device) 17 | let cloneURL: URL = packageURL.appending(path: inputURL.lastPathComponent) 18 | 19 | try FileManager.default.copyItem(at: inputURL, to: cloneURL) 20 | 21 | device.isSavedStateClone = true 22 | 23 | outputDevices.append(device) 24 | } 25 | 26 | return outputDevices 27 | } 28 | 29 | mutating func createStorageDeviceClones(packageURL: URL, model: VBVirtualMachine) async throws { 30 | storageDevices = try await Self.createStorageDeviceClones(packageURL: packageURL, model: model) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/SavedState/VBSavedStateMetadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct VBSavedStateMetadata: Identifiable, Hashable, Codable { 4 | public var id: UUID 5 | public var vmUUID: UUID 6 | public var date: Date 7 | public var appVersion: SoftwareVersion 8 | public var appBuild: Int 9 | public var hostECID: UInt64? 10 | 11 | /// Copy of ``VBMacDevice/storageDevices`` as those existed at the time the snapshot was taken. 12 | /// The ``VBStorageDevice/isSavedStateClone`` property is set to `true` once the state has been saved. 13 | /// Only managed disk images are cloned alongside saved states, custom user-provided images are referenced from their original locations. 14 | @DecodableDefault.EmptyList 15 | public var storageDevices: [VBStorageDevice] 16 | 17 | init(id: UUID, vmUUID: UUID, date: Date, appVersion: SoftwareVersion, appBuild: Int, hostECID: UInt64? = nil, storageDevices: [VBStorageDevice]) { 18 | self.id = id 19 | self.vmUUID = vmUUID 20 | self.date = date 21 | self.appVersion = appVersion 22 | self.appBuild = appBuild 23 | self.hostECID = hostECID 24 | self.storageDevices = storageDevices 25 | } 26 | } 27 | 28 | // MARK: - Saved State Metadata Creation 29 | 30 | public extension VBSavedStateMetadata { 31 | init(model: VBVirtualMachine) { 32 | let ecid = ProcessInfo.processInfo.machineECID 33 | 34 | assert(ecid != nil, "Failed to get host machine ECID") 35 | 36 | self.init( 37 | id: UUID(), 38 | vmUUID: model.metadata.uuid, 39 | date: .now, 40 | appVersion: Bundle.main.vbVersion, 41 | appBuild: Bundle.main.vbBuild, 42 | hostECID: ecid, 43 | storageDevices: model.configuration.hardware.storageDevices // will be modified once package is saved 44 | ) 45 | } 46 | } 47 | 48 | // MARK: - Directory Helpers 49 | 50 | @MainActor 51 | public extension VBVirtualMachine { 52 | func savedStatesDirectoryURL(in library: VMLibraryController) -> URL { 53 | library.savedStateDirectoryURL(for: self) 54 | } 55 | 56 | func savedStatesDirectoryURLCreatingIfNeeded(in library: VMLibraryController) throws -> URL { 57 | try library.savedStateDirectoryURLCreatingIfNeeded(for: self) 58 | } 59 | 60 | /// Convenience for ``VMLibraryController/createSavedStatePackage(for:)``. 61 | func createSavedStatePackage(in library: VMLibraryController, snapshotName name: String) throws -> VBSavedStatePackage { 62 | try library.createSavedStatePackage(for: self, snapshotName: name) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/SavedState/VBSavedStatePackage+VM.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension VBSavedStatePackage: VBStorageDeviceContainer { 4 | public var bundleURL: URL { url } 5 | public var storageDevices: [VBStorageDevice] { metadata.storageDevices } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/SavedState/VMLibraryController+SavedState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @MainActor 4 | public extension VMLibraryController { 5 | 6 | nonisolated static let savedStateDirectoryName = "_SavedState" 7 | 8 | var savedStatesDirectoryURL: URL { 9 | self.libraryURL.appending(path: Self.savedStateDirectoryName, directoryHint: .isDirectory) 10 | } 11 | 12 | func savedStatesLibraryURLCreatingIfNeeded() throws -> URL { 13 | try savedStatesDirectoryURL.creatingDirectoryIfNeeded() 14 | } 15 | 16 | func savedStateDirectoryURL(for model: VBVirtualMachine) -> URL { 17 | savedStatesDirectoryURL 18 | .appending(path: model.metadata.uuid.uuidString, directoryHint: .isDirectory) 19 | } 20 | 21 | func savedStateDirectoryURLCreatingIfNeeded(for model: VBVirtualMachine) throws -> URL { 22 | try savedStateDirectoryURL(for: model) 23 | .creatingDirectoryIfNeeded() 24 | } 25 | 26 | func createSavedStatePackage(for model: VBVirtualMachine, snapshotName name: String) throws -> VBSavedStatePackage { 27 | let baseURL = try model.savedStatesDirectoryURLCreatingIfNeeded(in: self) 28 | 29 | return try VBSavedStatePackage(creatingPackageInDirectoryAt: baseURL, model: model, snapshotName: name) 30 | } 31 | 32 | func virtualMachine(with uuid: UUID) throws -> VBVirtualMachine { 33 | guard let model = virtualMachines.first(where: { $0.metadata.uuid == uuid }) else { 34 | throw Failure("Virtual machine not found with UUID \(uuid)") 35 | } 36 | return model 37 | } 38 | 39 | func virtualMachineURL(forSavedStatePackageURL url: URL) throws -> URL { 40 | try virtualMachine(forSavedStatePackageURL: url).bundleURL 41 | } 42 | 43 | func virtualMachine(forSavedStatePackageURL url: URL) throws -> VBVirtualMachine { 44 | let metadata = try VBSavedStateMetadata(packageAt: url) 45 | let model = try virtualMachine(forSavedStateMetadata: metadata) 46 | return model 47 | } 48 | 49 | func virtualMachine(forSavedStateMetadata metadata: VBSavedStateMetadata) throws -> VBVirtualMachine { 50 | try virtualMachine(with: metadata.vmUUID) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/VBError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBError.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 10/04/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VBError: LocalizedError { 11 | 12 | public var errorDescription: String? 13 | 14 | init(_ desc: String) { self.errorDescription = desc } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/VBNVRAMVariable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBNVRAMVariable.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 11/04/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VBNVRAMVariable: Identifiable, Hashable, Codable { 11 | public var id: String { name } 12 | public let name: String 13 | public var value: String? 14 | 15 | public init(name: String, value: String?) { 16 | self.name = name 17 | self.value = value 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/VBStorageDeviceContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Adopted by ``VMVirtualMachine`` and ``VBSavedStatePackage`` 4 | /// in order to help resolve storage devices when bootstrapping a VM. 5 | public protocol VBStorageDeviceContainer { 6 | var bundleURL: URL { get } 7 | var storageDevices: [VBStorageDevice] { get } 8 | var bootDevice: VBStorageDevice { get throws } 9 | var bootDiskImage: VBManagedDiskImage { get throws } 10 | var allowDiskImageCreation: Bool { get } 11 | func diskImageURL(for image: VBManagedDiskImage) -> URL 12 | func diskImageURL(for device: VBStorageDevice) -> URL 13 | } 14 | 15 | // MARK: - Default Implementations 16 | 17 | /// These default implementations take care of resolving disk images for both ``VBVirtualMachine`` and ``VBSavedStatePackage``. 18 | /// Which one is used will be determine in `VirtualMachineConfigurationHelper` when bootstrapping the VM. 19 | public extension VBStorageDeviceContainer { 20 | var allowDiskImageCreation: Bool { false } 21 | 22 | var bootDevice: VBStorageDevice { 23 | get throws { 24 | guard let device = storageDevices.first(where: { $0.isBootVolume }) else { 25 | throw Failure("The virtual machine lacks a bootable storage device.") 26 | } 27 | 28 | return device 29 | } 30 | } 31 | 32 | var bootDiskImage: VBManagedDiskImage { 33 | get throws { 34 | let device = try bootDevice 35 | 36 | guard case .managedImage(let image) = device.backing else { 37 | throw Failure("The boot device must use a disk image managed by VirtualBuddy") 38 | } 39 | 40 | return image 41 | } 42 | } 43 | 44 | func diskImageURL(for image: VBManagedDiskImage) -> URL { 45 | bundleURL 46 | .appendingPathComponent(image.filename) 47 | .appendingPathExtension(image.format.fileExtension) 48 | } 49 | 50 | func diskImageURL(for device: VBStorageDevice) -> URL { 51 | switch device.backing { 52 | case .managedImage(let image): 53 | return diskImageURL(for: image) 54 | case .customImage(let customURL): 55 | return customURL 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/VBVirtualMachine+Metadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBVirtualMachine+Metadata.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 24/06/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | public extension VBVirtualMachine { 11 | 12 | func metadataDirectoryCreatingIfNeeded() throws -> URL { 13 | try metadataDirectoryURL.creatingDirectoryIfNeeded() 14 | } 15 | 16 | func write(_ data: Data, forMetadataFileNamed name: String) throws { 17 | let baseURL = try metadataDirectoryCreatingIfNeeded() 18 | 19 | let fileURL = baseURL.appendingPathComponent(name) 20 | 21 | try data.write(to: fileURL, options: .atomic) 22 | } 23 | 24 | func deleteMetadataFile(named name: String) throws { 25 | let baseURL = try metadataDirectoryCreatingIfNeeded() 26 | 27 | let fileURL = baseURL.appendingPathComponent(name) 28 | 29 | guard FileManager.default.fileExists(atPath: fileURL.path) else { return } 30 | 31 | try FileManager.default.removeItem(at: fileURL) 32 | } 33 | 34 | func metadataFileURL(_ fileName: String) throws -> URL { 35 | let baseURL = try metadataDirectoryCreatingIfNeeded() 36 | 37 | let fileURL = baseURL.appendingPathComponent(fileName) 38 | 39 | return fileURL 40 | } 41 | 42 | func metadataContents(_ fileName: String) -> Data? { 43 | guard let fileURL = try? metadataFileURL(fileName) else { return nil } 44 | 45 | guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil } 46 | 47 | return try? Data(contentsOf: fileURL) 48 | } 49 | 50 | } 51 | 52 | extension URL { 53 | func creatingDirectoryIfNeeded() throws -> Self { 54 | if !FileManager.default.fileExists(atPath: path) { 55 | try FileManager.default.createDirectory(at: self, withIntermediateDirectories: true) 56 | } 57 | return self 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /VirtualCore/Source/Models/VBVirtualMachine+Screenshot.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | public extension VBVirtualMachine { 4 | 5 | var screenshot: NSImage? { 6 | guard let imageData = metadataContents(VBVirtualMachine.screenshotFileName) ?? metadataContents(VBVirtualMachine._legacyScreenshotFileName) else { return nil } 7 | return NSImage(data: imageData) 8 | } 9 | 10 | func thumbnailImage() -> NSImage? { 11 | guard let thumbnailURL = try? metadataFileURL(Self.thumbnailFileName) else { return nil } 12 | 13 | if let existingImage = NSImage(contentsOf: thumbnailURL) { 14 | return existingImage 15 | } 16 | 17 | return try? screenshot?.vb_createThumbnail(at: thumbnailURL) 18 | } 19 | 20 | func invalidateThumbnail() throws { 21 | try deleteMetadataFile(named: Self.thumbnailFileName) 22 | 23 | DispatchQueue.main.async { 24 | self.didInvalidateThumbnail.send() 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /VirtualCore/Source/ReleaseTrains/AppUpdateChannel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUpdateChannel.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 25/06/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AppUpdateChannel: Identifiable, Hashable, CustomStringConvertible { 11 | public var id: String { name.lowercased() } 12 | public let name: String 13 | public let appCastURL: URL 14 | } 15 | 16 | public extension AppUpdateChannel { 17 | static let release = AppUpdateChannel( 18 | name: "Release", 19 | appCastURL: URL(string: "https://su.virtualbuddy.app/appcast.xml?channel=release")! 20 | ) 21 | 22 | static let beta = AppUpdateChannel( 23 | name: "Beta", 24 | appCastURL: URL(string: "https://su.virtualbuddy.app/appcast.xml?channel=beta")! 25 | ) 26 | 27 | static let channelsByID: [AppUpdateChannel.ID: AppUpdateChannel] = [ 28 | AppUpdateChannel.release.id: AppUpdateChannel.release, 29 | AppUpdateChannel.beta.id: AppUpdateChannel.beta 30 | ] 31 | 32 | static func defaultChannel(for buildType: VBBuildType) -> AppUpdateChannel { 33 | switch buildType { 34 | case .debug, .devRelease, .release: 35 | return .release 36 | case .betaDebug, .betaRelease: 37 | return .beta 38 | } 39 | } 40 | } 41 | 42 | public extension AppUpdateChannel { 43 | var description: String { id } 44 | } 45 | -------------------------------------------------------------------------------- /VirtualCore/Source/ReleaseTrains/VBBuildType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents the type of app build, such as beta vs. release. 4 | public enum VBBuildType: String, CaseIterable, CustomStringConvertible { 5 | case debug 6 | case betaDebug 7 | case release 8 | case betaRelease 9 | case devRelease 10 | } 11 | 12 | public extension VBBuildType { 13 | /// The current build type according to compile-time flags. 14 | static let current: VBBuildType = { 15 | #if BUILDING_DEV_RELEASE 16 | return .devRelease 17 | #elseif BETA && DEBUG 18 | return .betaDebug 19 | #elseif BETA 20 | return .betaRelease 21 | #elseif DEBUG 22 | return .debug 23 | #else 24 | return .release 25 | #endif 26 | }() 27 | 28 | /// A user-facing name for the build type, or `nil` if it's a regular release build. 29 | var name: String? { 30 | switch self { 31 | case .debug: 32 | return "Debug" 33 | case .betaDebug: 34 | return "Beta Debug" 35 | case .release: 36 | return nil 37 | case .betaRelease: 38 | return "Beta" 39 | case .devRelease: 40 | return "Dev" 41 | } 42 | } 43 | 44 | var description: String { name ?? "Release" } 45 | } 46 | 47 | public extension Bundle { 48 | var vbBuildType: VBBuildType { .current } 49 | 50 | /// The build description, including build type, version, and build number. 51 | /// Example: "Beta 2.0 - 123" 52 | var vbBuildDescription: String { 53 | if let typeDescription = vbBuildType.name { 54 | return "\(typeDescription) \(vbShortVersionString) - \(vbBuild)" 55 | } else { 56 | return "\(vbShortVersionString) - \(vbBuild)" 57 | } 58 | } 59 | 60 | /// The full version description such as "VirtualBuddy (Beta 2.0 - 123)", or just "VirtualBuddy" for release builds. 61 | var vbFullVersionDescription: String { 62 | let appName = "VirtualBuddy" 63 | if vbBuildType.name != nil { 64 | return "\(appName) (\(vbBuildDescription))" 65 | } else { 66 | return appName 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview-Linux.vbvm/.vbdata/Config.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview-Linux.vbvm/.vbdata/Config.plist -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview-Linux.vbvm/.vbdata/Metadata.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview-Linux.vbvm/.vbdata/Metadata.plist -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Config.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Config.plist -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Install.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Install.plist -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Metadata.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Metadata.plist -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/Preview.vbvm/.vbdata/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/Disk.img: -------------------------------------------------------------------------------- 1 | Fake 2 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/Fake Custom Path Disk.dmg: -------------------------------------------------------------------------------- 1 | Fake 2 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/Preview.vbvm/Fake Managed Disk.img: -------------------------------------------------------------------------------- 1 | Fake 2 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appBuild 6 | 110 7 | appVersion 8 | 1.4.1 9 | date 10 | 2024-03-27T19:06:24Z 11 | hostECID 12 | 2939957236449310 13 | id 14 | 65D33ECA-ACF2-4241-9F45-00417FAFF473 15 | vmUUID 16 | 96830330-D195-4211-BB4B-7D898177B12C 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appBuild 6 | 110 7 | appVersion 8 | 1.4.1 9 | date 10 | 2024-03-27T19:07:04Z 11 | hostECID 12 | 2939957236449310 13 | id 14 | 85CCCE19-1B97-45B5-A2E0-DF8A1F2DFA07 15 | vmUUID 16 | 96830330-D195-4211-BB4B-7D898177B12C 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appBuild 6 | 110 7 | appVersion 8 | 1.4.1 9 | date 10 | 2024-03-27T19:08:06Z 11 | hostECID 12 | 2939957236449310 13 | id 14 | 3C248913-3420-4688-96FB-5B32E91026E4 15 | vmUUID 16 | 96830330-D195-4211-BB4B-7D898177B12C 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appBuild 6 | 110 7 | appVersion 8 | 1.4.1 9 | date 10 | 2024-03-27T19:08:28Z 11 | hostECID 12 | 2939957236449310 13 | id 14 | FC0182EC-BC5C-4817-ABFF-8DD1B9E49735 15 | vmUUID 16 | 96830330-D195-4211-BB4B-7D898177B12C 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appBuild 6 | 110 7 | appVersion 8 | 1.4.1 9 | date 10 | 2024-03-27T19:08:51Z 11 | hostECID 12 | 2939957236449310 13 | id 14 | F29733F1-0056-4C90-A9E5-04F42C127086 15 | vmUUID 16 | 96830330-D195-4211-BB4B-7D898177B12C 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Screenshot.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Screenshot.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Thumbnail.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Thumbnail.heic -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/VirtualCore.xcassets/Adjectives.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "Adjectives.txt", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/VirtualCore.xcassets/Animals.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "Animals.txt", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /VirtualCore/Source/Resources/VirtualCore.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualCore/Source/Restore Images/Models/VBRestoreImageInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBRestoreImageInfo.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 07/06/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VBGuestReleaseChannel: Hashable, Identifiable, Codable { 11 | public struct Authentication: Hashable, Identifiable, Codable { 12 | public var id: String { name } 13 | public var name: String 14 | public var url: URL 15 | public var note: String 16 | } 17 | 18 | public var id: String 19 | public var name: String 20 | public var note: String 21 | public var icon: String 22 | public var authentication: Authentication? 23 | } 24 | 25 | public struct VBGuestReleaseGroup: Hashable, Identifiable, Codable { 26 | public var id: String 27 | public var name: String 28 | public var majorVersion: SoftwareVersion 29 | public var minHostVersion: SoftwareVersion 30 | } 31 | 32 | public struct VBRestoreImageInfo: Hashable, Identifiable, Codable { 33 | public var id: String { build } 34 | public var group: VBGuestReleaseGroup 35 | public var channel: VBGuestReleaseChannel 36 | public var name: String 37 | public var build: String 38 | public var url: URL 39 | @DecodableDefault.False 40 | public var needsCookie: Bool 41 | } 42 | 43 | public extension VBRestoreImageInfo { 44 | 45 | var authenticationRequirement: VBGuestReleaseChannel.Authentication? { 46 | guard needsCookie else { return nil } 47 | 48 | return channel.authentication 49 | } 50 | 51 | } 52 | 53 | public extension VBGuestReleaseChannel.Authentication { 54 | 55 | func satisfiedCookieHeaderValue(with cookies: [HTTPCookie]) -> String? { 56 | let targetCookieNames = Set(["myacinfo", "aidshd", "DSESSIONID", "PHPSESSID", "dawsp", "aasp"]) 57 | guard Set(cookies.map(\.name)).intersection(targetCookieNames) == targetCookieNames else { return nil } 58 | 59 | let formattedCookies = cookies.map({ "\($0.name)=\($0.value)" }).joined(separator: "; ") 60 | 61 | return formattedCookies 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /VirtualCore/Source/Restore Images/Models/VBRestoreImagesResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBRestoreImagesResponse.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 07/06/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct VBRestoreImagesResponse: Decodable { 11 | let success: Bool 12 | let error: String? 13 | let channels: [VBGuestReleaseChannel] 14 | let groups: [VBGuestReleaseGroup] 15 | let images: [VBRestoreImageInfo] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /VirtualCore/Source/Settings/VBSettingsContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBSettingsContainer.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 05/06/22. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import OSLog 11 | 12 | public final class VBSettingsContainer: ObservableObject { 13 | 14 | private lazy var logger = Logger(for: Self.self) 15 | 16 | public static let current = VBSettingsContainer() 17 | 18 | @Published public var settings = VBSettings() 19 | 20 | public let defaults: UserDefaults 21 | 22 | public init(with defaults: UserDefaults = .standard) { 23 | self.defaults = defaults 24 | 25 | read() 26 | bind() 27 | } 28 | 29 | private lazy var cancellables = Set() 30 | 31 | } 32 | 33 | // MARK: - Internal Extensions 34 | 35 | private extension VBSettingsContainer { 36 | 37 | func bind() { 38 | $settings 39 | .removeDuplicates() 40 | .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) 41 | .sink { [weak self] settings in 42 | self?.write(settings) 43 | } 44 | .store(in: &cancellables) 45 | } 46 | 47 | func read() { 48 | logger.debug(#function) 49 | 50 | do { 51 | self.settings = try VBSettings(with: defaults) 52 | } catch { 53 | logger.assert("Failed to read settings from defaults: \(error.log)") 54 | } 55 | } 56 | 57 | func write(_ newSettings: VBSettings) { 58 | logger.debug(#function) 59 | 60 | do { 61 | try newSettings.write(to: defaults) 62 | } catch { 63 | logger.assert("Failed to write settings into defaults: \(error.log)") 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /VirtualCore/Source/Utilities/Bundle+Version.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Bundle { 4 | var vbShortVersionString: String { 5 | infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0" 6 | } 7 | 8 | var vbVersion: SoftwareVersion { 9 | SoftwareVersion(string: vbShortVersionString) ?? SoftwareVersion.empty 10 | } 11 | 12 | var vbBuild: Int { 13 | let str = infoDictionary?["CFBundleVersion"] as? String ?? "0" 14 | return Int(str) ?? 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /VirtualCore/Source/Utilities/ProcessInfo+ECID.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import IOKit 3 | 4 | extension ProcessInfo { 5 | 6 | var machineECID: UInt64? { 7 | let entry = IORegistryEntryFromPath(kIOMainPortDefault, "IODeviceTree:/chosen") 8 | guard entry != MACH_PORT_NULL else { return nil } 9 | 10 | guard let ecidData = IORegistryEntrySearchCFProperty(entry, "IODeviceTree:/chosen", "unique-chip-id" as CFString, kCFAllocatorDefault, 0) as? Data else { return nil } 11 | 12 | guard ecidData.count > 0 else { return nil } 13 | 14 | return ecidData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> UInt64? in 15 | guard let base = buffer.baseAddress else { return nil } 16 | return UnsafeRawPointer(base).load(as: UInt64.self) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /VirtualCore/Source/Utilities/VBMemoryLeakDebugAssertions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class VBMemoryLeakDebugAssertions { 4 | @inlinable 5 | public static func vb_objectShouldBeReleasedSoon(_ object: AnyObject, after interval: TimeInterval = 0.5) { 6 | #if DEBUG 7 | _vb_objectShouldBeReleasedSoon(object, after: interval) 8 | #endif 9 | } 10 | 11 | @inlinable 12 | public static func vb_objectIsBeingReleased(_ object: AnyObject) { 13 | #if DEBUG 14 | _vb_objectIsBeingReleased(object) 15 | #endif 16 | } 17 | 18 | #if DEBUG 19 | public static let _disableFlag = "VBDisableMemoryLeakAssertions" 20 | 21 | public static var _vb_debugAssertionsEnabled: Bool { !UserDefaults.standard.bool(forKey: _disableFlag) } 22 | 23 | public static var _releasedObjects = Set() 24 | 25 | @inlinable 26 | public static func _objectID(_ object: AnyObject) -> String { 27 | String(describing: Unmanaged.passUnretained(object).toOpaque()) 28 | } 29 | 30 | @inlinable 31 | public static func _vb_objectShouldBeReleasedSoon(_ object: AnyObject, after interval: TimeInterval) { 32 | guard _vb_debugAssertionsEnabled else { return } 33 | 34 | let id = _objectID(object) 35 | let className = String(describing: type(of: object)) 36 | 37 | let description: String 38 | 39 | if let window = object as? NSWindow { 40 | let title = window.title 41 | description = "#\(id) (\(className) - \"\(title)\")" 42 | } else { 43 | description = "#\(id) (\(className) - \"\(String(describing: object))\")" 44 | } 45 | 46 | DispatchQueue.main.asyncAfter(deadline: .now() + interval) { 47 | assert(_releasedObjects.contains(id), "💦 POSSIBLE LEAK: \(description) was not released when expected (set \(_disableFlag) defaults flag to disable this)") 48 | } 49 | } 50 | 51 | @inlinable 52 | public static func _vb_objectIsBeingReleased(_ object: AnyObject) { 53 | guard _vb_debugAssertionsEnabled else { return } 54 | 55 | _releasedObjects.insert(_objectID(object)) 56 | } 57 | #endif 58 | } 59 | -------------------------------------------------------------------------------- /VirtualCore/Source/Utilities/WeakReference.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Wraps a reference type as a weak reference. 4 | /// Useful as value type in collections when holding a strong reference to the objects is undesirable. 5 | public struct WeakReference { 6 | public private(set) weak var object: Object? 7 | 8 | public init(_ object: Object) { 9 | self.object = object 10 | } 11 | } 12 | 13 | extension WeakReference: Equatable where Object: Equatable { } 14 | extension WeakReference: Hashable where Object: Hashable { } 15 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/DirectoryObserver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import OSLog 4 | 5 | final class DirectoryObserver: NSObject, NSFilePresenter { 6 | 7 | private let logger: Logger 8 | 9 | var presentedItemURL: URL? 10 | 11 | var presentedItemOperationQueue: OperationQueue = .main 12 | 13 | let signal: PassthroughSubject 14 | let fileExtensions: Set 15 | 16 | init(presentedItemURL: URL?, fileExtensions: Set, label: String, signal: PassthroughSubject) { 17 | self.logger = Logger(for: DirectoryObserver.self, label: label) 18 | self.presentedItemURL = presentedItemURL 19 | self.fileExtensions = fileExtensions 20 | self.signal = signal 21 | 22 | super.init() 23 | 24 | NSFileCoordinator.addFilePresenter(self) 25 | } 26 | 27 | private func sendSignalIfNeeded(for url: URL) { 28 | guard fileExtensions.contains(url.pathExtension) else { return } 29 | 30 | signal.send(url) 31 | } 32 | 33 | func presentedSubitemDidAppear(at url: URL) { 34 | logger.debug("Added: \(url.path)") 35 | 36 | sendSignalIfNeeded(for: url) 37 | } 38 | 39 | func presentedSubitemDidChange(at url: URL) { 40 | sendSignalIfNeeded(for: url) 41 | } 42 | 43 | func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) { 44 | logger.debug("Moved: \(oldURL.path) -> \(newURL.path)") 45 | 46 | sendSignalIfNeeded(for: newURL) 47 | } 48 | 49 | func accommodatePresentedSubitemDeletion(at url: URL) async throws { 50 | logger.debug("Deleted: \(url.path)") 51 | 52 | sendSignalIfNeeded(for: url) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/LinuxVirtualMachineConfigurationHelper.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`. 6 | */ 7 | 8 | import Foundation 9 | import Virtualization 10 | 11 | @available(macOS 13.0, *) 12 | struct LinuxVirtualMachineConfigurationHelper: VirtualMachineConfigurationHelper { 13 | let vm: VBVirtualMachine 14 | let savedState: VBSavedStatePackage? 15 | 16 | init(vm: VBVirtualMachine) { 17 | self.vm = vm 18 | self.savedState = nil 19 | } 20 | 21 | func createInstallDevice(installImageURL: URL) throws -> VZStorageDeviceConfiguration { 22 | let attachment = try VZDiskImageStorageDeviceAttachment(url: installImageURL, readOnly: true, cachingMode: .cached, synchronizationMode: .fsync) 23 | let usbDeviceConfiguration = VZUSBMassStorageDeviceConfiguration(attachment: attachment) 24 | return usbDeviceConfiguration 25 | } 26 | 27 | func createBootLoader() throws -> VZBootLoader { 28 | let efi = VZEFIBootLoader() 29 | let storeURL = vm.metadataDirectoryURL.appendingPathComponent("nvram") 30 | if FileManager.default.fileExists(atPath: storeURL.path) { 31 | efi.variableStore = VZEFIVariableStore(url: storeURL) 32 | } else { 33 | efi.variableStore = try VZEFIVariableStore(creatingVariableStoreAt: storeURL, options: []) 34 | } 35 | return efi 36 | } 37 | 38 | func createGraphicsDevices() -> [VZGraphicsDeviceConfiguration] { 39 | let graphicsConfiguration = VZVirtioGraphicsDeviceConfiguration() 40 | 41 | graphicsConfiguration.scanouts = vm.configuration.hardware.displayDevices.map(\.vzScanout) 42 | 43 | return [graphicsConfiguration] 44 | } 45 | 46 | @available(macOS 13.0, *) 47 | func createSpiceAgentConsoleDeviceConfiguration() -> VZVirtioConsoleDeviceConfiguration? { 48 | let consoleDevice = VZVirtioConsoleDeviceConfiguration() 49 | 50 | let spiceAgentPort = VZVirtioConsolePortConfiguration() 51 | spiceAgentPort.name = VZSpiceAgentPortAttachment.spiceAgentPortName 52 | spiceAgentPort.attachment = VZSpiceAgentPortAttachment() 53 | consoleDevice.ports[0] = spiceAgentPort 54 | 55 | return consoleDevice 56 | } 57 | } 58 | 59 | // MARK: - Configuration Models -> Virtualization 60 | 61 | @available(macOS 13.0, *) 62 | extension VBDisplayDevice { 63 | 64 | var vzScanout: VZVirtioGraphicsScanoutConfiguration { 65 | VZVirtioGraphicsScanoutConfiguration(widthInPixels: width, heightInPixels: height) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/MacOSVirtualMachineConfigurationHelper.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`. 6 | */ 7 | 8 | import Foundation 9 | import Virtualization 10 | 11 | struct MacOSVirtualMachineConfigurationHelper: VirtualMachineConfigurationHelper { 12 | let vm: VBVirtualMachine 13 | let savedState: VBSavedStatePackage? 14 | 15 | func createInstallDevice(installImageURL: URL) throws -> VZStorageDeviceConfiguration { 16 | fatalError() 17 | } 18 | 19 | func createBootLoader() -> VZBootLoader { 20 | return VZMacOSBootLoader() 21 | } 22 | 23 | func createGraphicsDevices() -> [VZGraphicsDeviceConfiguration] { 24 | let graphicsConfiguration = VZMacGraphicsDeviceConfiguration() 25 | 26 | graphicsConfiguration.displays = vm.configuration.hardware.displayDevices.map(\.vzDisplay) 27 | 28 | return [graphicsConfiguration] 29 | } 30 | 31 | func createAdditionalBlockDevices() async throws -> [VZVirtioBlockDeviceConfiguration] { 32 | var devices = try storageDeviceContainer.additionalBlockDevices(guestType: vm.configuration.systemType) 33 | 34 | if vm.configuration.guestAdditionsEnabled, let disk = try? VZVirtioBlockDeviceConfiguration.guestAdditionsDisk { 35 | devices.append(disk) 36 | } 37 | 38 | return devices 39 | } 40 | 41 | func createKeyboardConfiguration() -> VZKeyboardConfiguration { 42 | if #available(macOS 14.0, *) { 43 | switch vm.configuration.hardware.keyboardDevice.kind { 44 | case .generic: 45 | return VZUSBKeyboardConfiguration() 46 | case .mac: 47 | return VZMacKeyboardConfiguration() 48 | } 49 | } else { 50 | return VZUSBKeyboardConfiguration() 51 | } 52 | } 53 | 54 | func createEntropyDevices() -> [VZEntropyDeviceConfiguration] { 55 | [VZVirtioEntropyDeviceConfiguration()] 56 | } 57 | 58 | @available(macOS 15.0, *) 59 | func createUSBControllers() -> [VZUSBControllerConfiguration] { 60 | let xhci = VZXHCIControllerConfiguration() 61 | return [xhci] 62 | } 63 | } 64 | 65 | // MARK: - Configuration Models -> Virtualization 66 | 67 | extension VBDisplayDevice { 68 | 69 | var vzDisplay: VZMacGraphicsDisplayConfiguration { 70 | VZMacGraphicsDisplayConfiguration(widthInPixels: width, heightInPixels: height, pixelsPerInch: pixelsPerInch) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/RandomNameGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RandomNameGenerator.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 19/07/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class RandomNameGenerator { 11 | 12 | public static let shared = RandomNameGenerator() 13 | 14 | private var adjectives = [String]() 15 | private var animals = [String]() 16 | 17 | private init() { 18 | guard let animalsData = NSDataAsset(name: "Animals", bundle: .virtualCore)?.data, 19 | let adjectivesData = NSDataAsset(name: "Adjectives", bundle: .virtualCore)?.data 20 | else { 21 | assertionFailure("Couldn't load random name generator asssets") 22 | return 23 | } 24 | 25 | adjectives = String(decoding: adjectivesData, as: UTF8.self).components(separatedBy: .newlines) 26 | animals = String(decoding: animalsData, as: UTF8.self).components(separatedBy: .newlines) 27 | } 28 | 29 | public func newName() -> String { 30 | guard let adjective = adjectives.randomElement(), 31 | let animal = animals.randomElement() 32 | else { 33 | return UUID().uuidString 34 | } 35 | 36 | return adjective + " " + animal 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/VBDebugUtil.h: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | 3 | #import 4 | 5 | @class VZVirtualMachine; 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | /// This is used to stop VirtualBuddy in the debugger at specific points in a VM's lifecycle, 10 | /// so that exploring Virtualization internals can be done in Objective-C. 11 | @interface VBDebugUtil : NSObject 12 | 13 | + (void)debugVirtualMachineBeforeStart:(VZVirtualMachine *_Nonnull)vm; 14 | + (void)debugVirtualMachineAfterStart:(VZVirtualMachine *_Nonnull)vm; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/VBDebugUtil.m: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | 3 | #import "VBDebugUtil.h" 4 | 5 | @import Virtualization; 6 | 7 | #import 8 | 9 | @implementation VBDebugUtil 10 | 11 | /// Runs in debug builds when VM is about to be started. 12 | + (void)debugVirtualMachineBeforeStart:(VZVirtualMachine *_Nonnull)vm 13 | { 14 | NSLog(@"Debug virtual machine before start: %@", vm); 15 | return; 16 | } 17 | 18 | /// Runs in debug builds right after the VM has been started. 19 | + (void)debugVirtualMachineAfterStart:(VZVirtualMachine *_Nonnull)vm 20 | { 21 | NSLog(@"Debug virtual machine after start: %@", vm); 22 | 23 | __weak typeof(vm) weakVM = vm; 24 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 25 | if (!weakVM) return; 26 | 27 | [self debugVirtualMachineAfterStartWithDelay:weakVM]; 28 | }); 29 | } 30 | 31 | /// Runs in debug builds several seconds after the VM has been started (likely after it's finished booting up). 32 | + (void)debugVirtualMachineAfterStartWithDelay:(VZVirtualMachine *_Nonnull)vm 33 | { 34 | NSLog(@"Debug virtual machine after start with delay: %@", vm); 35 | return; 36 | } 37 | 38 | @end 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Helpers/VZVirtualMachineConfiguration+NVRAM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VZVirtualMachineConfiguration+NVRAM.swift 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 11/04/22. 6 | // 7 | 8 | import Foundation 9 | import Virtualization 10 | 11 | public struct Failure: LocalizedError { 12 | public var errorDescription: String? 13 | 14 | public init(_ msg: String) { self.errorDescription = msg } 15 | } 16 | 17 | public extension VZMacAuxiliaryStorage { 18 | 19 | func fetchNVRAMVariables() throws -> [VBNVRAMVariable] { 20 | var error: NSError? 21 | let variables = _allNVRAMVariablesWithError(&error) 22 | 23 | if let error = error { throw error } 24 | 25 | let vars = variables.map { VBNVRAMVariable(name: $0.key, value: $0.value as? String) } 26 | return vars 27 | } 28 | 29 | func updateNVRAM(_ variable: VBNVRAMVariable) throws { 30 | if let value = variable.value { 31 | try _setValue(value, forNVRAMVariableNamed: variable.name) 32 | } else { 33 | try _removeNVRAMVariableNamed(variable.name) 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Screenshot/NSImage+VMScreenshot.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Virtualization 3 | 4 | public extension UserDefaults { 5 | var virtualMachineScreenshotsDisabled: Bool { bool(forKey: #function) } 6 | } 7 | 8 | @available(macOS 14.0, *) 9 | public extension NSImage { 10 | @MainActor 11 | static func screenshot(from virtualMachine: VZVirtualMachine) async throws -> NSImage { 12 | guard !UserDefaults.standard.virtualMachineScreenshotsDisabled else { 13 | throw Failure("Screenshots disabled by defaults flag.") 14 | } 15 | 16 | guard #unavailable(macOS 15.4) else { 17 | throw Failure("Feature disabled on macOS 15.4+") 18 | } 19 | guard let device = virtualMachine.graphicsDevices.first else { 20 | throw Failure("Can't screenshot a virtual machine without a graphics device.") 21 | } 22 | guard let display = device.displays.first else { 23 | throw Failure("Can't screenshot a virtual machine without a display.") 24 | } 25 | 26 | return try await screenshot(from: display) 27 | } 28 | 29 | @MainActor 30 | static func screenshot(from display: VZGraphicsDisplay) async throws -> NSImage { 31 | try await display.vb_takeScreenshot() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Screenshot/VMScreenshotTimingController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Provides scheduling for periodic VM screenshots. 4 | /// This is used by `VMScreenshotter` in `VirtualUI`. 5 | public final class VMScreenshotTimingController { 6 | private var timer: Timer? 7 | 8 | public let interval: TimeInterval 9 | private let onTimerFired: () async throws -> Void 10 | 11 | public init(interval: TimeInterval, onTimerFired: @escaping () async throws -> Void) { 12 | assert(interval > 1, "The minimum interval is 1 second") 13 | 14 | self.interval = max(1, interval) 15 | self.onTimerFired = onTimerFired 16 | } 17 | 18 | public func activate() { 19 | timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in 20 | self?.timerFired() 21 | }) 22 | } 23 | 24 | public func invalidate() { 25 | pendingTask?.cancel() 26 | pendingTask = nil 27 | 28 | timer?.invalidate() 29 | timer = nil 30 | } 31 | 32 | private var pendingTask: Task<(), Error>? 33 | 34 | private func timerFired() { 35 | pendingTask?.cancel() 36 | 37 | pendingTask = Task.detached(priority: .utility) { [weak self] in 38 | guard let self else { return } 39 | try await self.onTimerFired() 40 | } 41 | } 42 | 43 | deinit { 44 | #if DEBUG 45 | print("\(String(describing: self)) 👋🏻") 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Screenshot/VZGraphicsDisplay+Screenshot.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface VZGraphicsDisplay (Screenshot) 6 | 7 | /// Wraps Virtualization SPI to make it safer to call from Swift. 8 | /// Checks for SPI availability and that the completion block types match the ones we expect. 9 | - (void)vb_takeScreenshotWithCompletionHandler:(void(^_Nonnull)(NSImage *_Nullable image, NSError *_Nullable error))completion NS_SWIFT_UI_ACTOR API_AVAILABLE(macos(14.0)); 10 | 11 | @end 12 | 13 | NS_ASSUME_NONNULL_END 14 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/Screenshot/VZGraphicsDisplay+Screenshot.m: -------------------------------------------------------------------------------- 1 | #import "VZGraphicsDisplay+Screenshot.h" 2 | 3 | #import 4 | 5 | @implementation VZGraphicsDisplay (Screenshot) 6 | 7 | - (BOOL)vb_supportsScreenshotSPI 8 | { 9 | static dispatch_once_t onceToken; 10 | static BOOL supports; 11 | dispatch_once(&onceToken, ^{ 12 | supports = [self respondsToSelector:NSSelectorFromString(@"_takeScreenshotWithCompletionHandler:")]; 13 | }); 14 | return supports; 15 | } 16 | 17 | - (void)vb_takeScreenshotWithCompletionHandler:(void(^_Nonnull)(NSImage *_Nullable image, NSError *_Nullable error))completion 18 | { 19 | #define failure( msg ) [NSError errorWithDomain:@"screenshot" code:1 userInfo:@{NSLocalizedDescriptionKey: msg}] 20 | 21 | if (![self vb_supportsScreenshotSPI]) { 22 | completion(nil, failure(@"VZGraphicsDisplay doesn't have the _takeScreenshotWithCompletionHandler: method")); 23 | return; 24 | } 25 | 26 | [self _takeScreenshotWithCompletionHandler:^(id _Nullable image, id _Nullable error) { 27 | if (image) { 28 | if (![image isKindOfClass:[NSImage class]]) { 29 | completion(nil, failure(@"Unexpected image type")); 30 | } else { 31 | completion(image, nil); 32 | } 33 | } else { 34 | if ([error isKindOfClass:[NSError class]]) { 35 | completion(nil, error); 36 | } else { 37 | completion(nil, failure(@"Unexpected result: no image nor error")); 38 | } 39 | } 40 | }]; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /VirtualCore/Source/Virtualization/VMSavedStatesController.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | import OSLog 4 | 5 | @MainActor 6 | public final class VMSavedStatesController: ObservableObject { 7 | 8 | @Published 9 | public private(set) var states = [VBSavedStatePackage]() 10 | 11 | private let logger = Logger(for: VMSavedStatesController.self) 12 | private let filePresenter: DirectoryObserver 13 | private let updateSignal = PassthroughSubject() 14 | private let directoryURL: URL 15 | private let virtualMachine: VBVirtualMachine 16 | 17 | public init(library: VMLibraryController, virtualMachine: VBVirtualMachine) { 18 | self.virtualMachine = virtualMachine 19 | self.directoryURL = library.savedStateDirectoryURL(for: virtualMachine) 20 | self.filePresenter = DirectoryObserver( 21 | presentedItemURL: directoryURL, 22 | fileExtensions: [VBSavedStatePackage.fileExtension], 23 | label: "SavedStates", 24 | signal: updateSignal 25 | ) 26 | 27 | loadStates() 28 | bind() 29 | } 30 | 31 | private lazy var cancellables = Set() 32 | 33 | private lazy var fileManager = FileManager() 34 | 35 | private func bind() { 36 | updateSignal 37 | .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) 38 | .sink { [weak self] _ in 39 | self?.loadStates() 40 | } 41 | .store(in: &cancellables) 42 | } 43 | 44 | public func loadStates() { 45 | guard let enumerator = fileManager.enumerator(at: directoryURL, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants], errorHandler: nil) else { 46 | logger.error("Failed to open directory at \(self.directoryURL.path, privacy: .public)") 47 | return 48 | } 49 | 50 | var loadedStates = [VBSavedStatePackage]() 51 | 52 | while let url = enumerator.nextObject() as? URL { 53 | guard url.pathExtension == VBSavedStatePackage.fileExtension else { continue } 54 | 55 | do { 56 | let package = try VBSavedStatePackage(url: url) 57 | 58 | loadedStates.append(package) 59 | } catch { 60 | assertionFailure("Failed to construct saved state package: \(error)") 61 | } 62 | } 63 | 64 | loadedStates.sort(by: { $0.metadata.date > $1.metadata.date }) 65 | 66 | self.states = loadedStates 67 | } 68 | 69 | public func reload(animated: Bool = true) { 70 | if animated { 71 | withAnimation(.spring()) { 72 | loadStates() 73 | } 74 | } else { 75 | loadStates() 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /VirtualCore/VirtualCore.h: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualCore.h 3 | // VirtualCore 4 | // 5 | // Created by Guilherme Rambo on 07/04/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for VirtualCore. 11 | FOUNDATION_EXPORT double VirtualCoreVersionNumber; 12 | 13 | //! Project version string for VirtualCore. 14 | FOUNDATION_EXPORT const unsigned char VirtualCoreVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | #import 19 | #import 20 | #import 21 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/StatusItemPanelChromeBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.686", 9 | "green" : "0.686", 10 | "red" : "0.686" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.251", 27 | "green" : "0.251", 28 | "red" : "0.251" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ThumbnailPlaceholder.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ThumbnailPlaceholder@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/ThumbnailPlaceholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/ThumbnailPlaceholder.png -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/ThumbnailPlaceholder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/ThumbnailPlaceholder@2x.png -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/linux.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-linux.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/linux.imageset/icon-linux.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/linux.imageset/icon-linux.pdf -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/mac.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-mac.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/mac.imageset/icon-mac.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/mac.imageset/icon-mac.pdf -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/CGFloat+OnePixel.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | public extension CGFloat { 4 | 5 | static func onePixel(in view: NSView?) -> CGFloat { 6 | guard let screen = view?.window?.screen ?? NSScreen.main else { return 1 } 7 | 8 | return 1 / screen.backingScaleFactor 9 | } 10 | 11 | static var onePixel: CGFloat { 12 | let scale = NSScreen.main?.backingScaleFactor ?? 2 13 | return 1 / scale 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/Configuration Controls/NumericPropertyControl.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | import VirtualCore 4 | 5 | struct NumericPropertyControl: View { 6 | @Binding var value: Value 7 | var range: ClosedRange 8 | var step: Value? = nil 9 | var hideSlider = false 10 | var label: String 11 | var formatter: F 12 | var unfocus = VoidSubject() 13 | var spacing: CGFloat = 2 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: spacing) { 17 | HStack { 18 | PropertyControlLabel(label) 19 | 20 | Spacer() 21 | 22 | NumericValueField( 23 | label: label, 24 | value: $value, 25 | range: range, 26 | formatter: formatter 27 | ) 28 | .textFieldStyle(.plain) 29 | .multilineTextAlignment(.trailing) 30 | .monospacedDigit() 31 | } 32 | 33 | if !hideSlider { 34 | Group { 35 | if let step { 36 | Slider(value: $value.sliderValue, in: range.sliderRange, step: Double(step), onEditingChanged: sliderEditingChanged) 37 | } else { 38 | Slider(value: $value.sliderValue, in: range.sliderRange, onEditingChanged: sliderEditingChanged) 39 | } 40 | } 41 | .controlSize(.mini) 42 | } 43 | } 44 | .transition(.asymmetric(insertion: .offset(x: 0, y: -40), removal: .offset(x: 0, y: 40)).combined(with: .opacity)) 45 | } 46 | 47 | private func sliderEditingChanged(_ isEditing: Bool) { 48 | guard isEditing else { return } 49 | unfocus.send() 50 | } 51 | } 52 | 53 | extension NumberFormatter { 54 | static let numericPropertyControlDefault: NumberFormatter = { 55 | let f = NumberFormatter() 56 | f.numberStyle = .decimal 57 | f.maximumFractionDigits = 0 58 | f.minimumFractionDigits = 0 59 | f.hasThousandSeparators = false 60 | return f 61 | }() 62 | } 63 | 64 | #if DEBUG 65 | struct PropertySlider_Previews: PreviewProvider { 66 | static var previews: some View { 67 | _Template() 68 | } 69 | 70 | struct _Template: View { 71 | @State private var value = 1 72 | 73 | var body: some View { 74 | NumericPropertyControl(value: $value, range: 0...10, step: 1, hideSlider: false, label: "Preview", formatter: NumberFormatter.numericPropertyControlDefault) 75 | .padding() 76 | .frame(maxWidth: 200) 77 | } 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/Configuration Controls/NumericValueField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | import VirtualCore 4 | 5 | struct NumericValueField: View { 6 | 7 | var label: String 8 | @Binding var value: Value 9 | var range: ClosedRange 10 | var formatter: F 11 | 12 | @Environment(\.unfocusActiveField) 13 | private var unfocus 14 | 15 | init(label: String, value: Binding, range: ClosedRange, formatter: F) { 16 | self.label = label 17 | self._value = value 18 | self.range = range 19 | self.formatter = formatter 20 | } 21 | 22 | @FocusState 23 | private var isFocused: Bool 24 | 25 | @State private var isInEditMode = false 26 | 27 | var body: some View { 28 | EphemeralTextField($value) { currentValue in 29 | Text(formatter.string(for: currentValue) ?? "") 30 | } editableContent: { binding in 31 | TextField(label, value: binding, formatter: formatter) 32 | } clamp: { $0.limited(to: range) } 33 | } 34 | } 35 | 36 | extension BinaryInteger { 37 | func limited(to range: ClosedRange) -> Self { 38 | Swift.min(range.upperBound, Swift.max(range.lowerBound, self)) 39 | } 40 | } 41 | 42 | #if DEBUG 43 | 44 | struct NumericValueField_Previews: PreviewProvider { 45 | static var previews: some View { 46 | _Template() 47 | } 48 | 49 | struct _Template: View { 50 | @State var value = 1 51 | 52 | @Environment(\.unfocusActiveField) 53 | private var unfocusActiveField 54 | 55 | private let formatter: NumberFormatter = { 56 | let f = NumberFormatter() 57 | f.numberStyle = .decimal 58 | f.maximumFractionDigits = 0 59 | f.minimumFractionDigits = 0 60 | f.hasThousandSeparators = false 61 | return f 62 | }() 63 | 64 | var body: some View { 65 | VStack { 66 | NumericValueField( 67 | label: "Test", 68 | value: $value, 69 | range: 1...10, 70 | formatter: formatter 71 | ) 72 | 73 | Button("Unfocus") { 74 | unfocusActiveField.send(.cancel) 75 | } 76 | .controlSize(.small) 77 | } 78 | .padding() 79 | } 80 | } 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/Configuration Controls/PropertyControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyControl.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 18/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PropertyControl: View { 11 | 12 | var label: String 13 | var spacing: CGFloat 14 | var content: () -> Content 15 | 16 | init(_ label: String, spacing: CGFloat = 4, @ViewBuilder content: @escaping () -> Content) { 17 | self.label = label 18 | self.spacing = spacing 19 | self.content = content 20 | } 21 | 22 | var body: some View { 23 | VStack(alignment: .leading, spacing: spacing) { 24 | PropertyControlLabel(label) 25 | content().labelsHidden() 26 | } 27 | } 28 | 29 | } 30 | 31 | struct PropertyControlLabel: View { 32 | init(_ title: String) { 33 | self.title = title 34 | } 35 | 36 | var title: String 37 | 38 | var body: some View { 39 | Text(title) 40 | .foregroundColor(.white.opacity(0.7)) 41 | .blendMode(.plusLighter) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/Configuration Controls/SharedFocusEnvironment.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | enum UnfocusFieldAction { 5 | case commit 6 | case cancel 7 | } 8 | 9 | typealias UnfocusFieldSubject = PassthroughSubject 10 | 11 | extension EnvironmentValues { 12 | var unfocusActiveField: UnfocusFieldSubject { 13 | get { self[UnfocusActiveFieldEnvironmentKey.self] } 14 | set { self[UnfocusActiveFieldEnvironmentKey.self] = newValue } 15 | } 16 | } 17 | 18 | extension View { 19 | func unfocusOnTap() -> some View { 20 | modifier(UnfocusOnTap()) 21 | } 22 | } 23 | 24 | private struct UnfocusOnTap: ViewModifier { 25 | 26 | @Environment(\.unfocusActiveField) 27 | private var unfocus 28 | 29 | func body(content: Content) -> some View { 30 | content 31 | .onTapGesture { unfocus.send(.commit) } 32 | } 33 | 34 | } 35 | 36 | private struct UnfocusActiveFieldEnvironmentKey: EnvironmentKey { 37 | static var defaultValue = UnfocusFieldSubject() 38 | } 39 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/FixSwiftUIMaterialInPreviews.m: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | 3 | /* 4 | This fixes SwiftUI previews not rendering translucent materials correctly by 5 | swizzling a couple of properties on NSWindow. 6 | 7 | Just drop into your project and add to the target being previewed (or something it links against). 8 | 9 | Notice the #if DEBUG, so this code won't end up in release builds. It also checks for the 10 | XCODE_RUNNING_FOR_PREVIEWS environment variable so that it won't affect regular debug builds of the app. 11 | */ 12 | 13 | @import Cocoa; 14 | @import ObjectiveC.runtime; 15 | 16 | @import VirtualCore; 17 | 18 | @interface FixSwiftUIMaterialInPreviews : NSObject 19 | 20 | @end 21 | 22 | @implementation FixSwiftUIMaterialInPreviews 23 | 24 | + (void)load 25 | { 26 | if (![NSProcessInfo isSwiftUIPreview]) return; 27 | 28 | // VirtualBuddy is always in dark mode, make previews dark mode as well 29 | NSApp.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; 30 | 31 | NSLog(@"😇 Installing fix for translucency in SwiftUI previews"); 32 | 33 | Method hasKeyAppearance = class_getInstanceMethod([NSWindow class], NSSelectorFromString(@"hasKeyAppearance")); 34 | Method hasMainAppearance = class_getInstanceMethod([NSWindow class], NSSelectorFromString(@"hasMainAppearance")); 35 | Method override = class_getClassMethod([self class], @selector(__hasKeyAppearanceOverride)); 36 | 37 | if (hasKeyAppearance) method_exchangeImplementations(hasKeyAppearance, override); 38 | if (hasMainAppearance) method_exchangeImplementations(hasMainAppearance, override); 39 | } 40 | 41 | + (BOOL)__hasKeyAppearanceOverride 42 | { 43 | return YES; 44 | } 45 | 46 | @end 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /VirtualUI/Source/Building Blocks/SliderConversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderConversion.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 17/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Binding where Value: BinaryInteger { 11 | var sliderValue: Binding { 12 | .init { 13 | Double(wrappedValue) 14 | } set: { wrappedValue = Value($0) } 15 | } 16 | } 17 | 18 | extension ClosedRange where Bound: BinaryInteger { 19 | var sliderRange: ClosedRange { 20 | Double(lowerBound)...Double(upperBound) 21 | } 22 | } 23 | 24 | extension Binding where Value == UInt64 { 25 | var gbValue: Binding { 26 | .init { 27 | Int(wrappedValue / 1024 / 1024 / 1024) 28 | } set: { wrappedValue = Value($0 * 1024 * 1024 * 1024) } 29 | } 30 | 31 | var gbStorageValue: Binding { 32 | .init { 33 | Int(wrappedValue / .storageGigabyte) 34 | } set: { wrappedValue = Value(UInt64($0) * .storageGigabyte) } 35 | } 36 | } 37 | 38 | extension UInt64 { 39 | var gbStorageValue: Int { 40 | Int(self / .storageGigabyte) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/NSAlert+Confirmation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAlert+Confirmation.swift 3 | // VirtualBuddy 4 | // 5 | // Created by Guilherme Rambo on 29/06/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | public extension NSAlert { 11 | 12 | static func runConfirmationAlert(title: String, 13 | message: String, 14 | continueButtonTitle: String = "Continue", 15 | cancelButtonTitle: String = "Cancel") async -> Bool 16 | { 17 | let alert = NSAlert() 18 | 19 | alert.messageText = title 20 | alert.informativeText = message 21 | 22 | alert.addButton(withTitle: cancelButtonTitle) 23 | alert.addButton(withTitle: continueButtonTitle) 24 | 25 | let response: NSApplication.ModalResponse 26 | 27 | if let window = NSApp?.keyWindow { 28 | response = await alert.beginSheetModal(for: window) 29 | } else { 30 | response = alert.runModal() 31 | } 32 | 33 | return response == .alertSecondButtonReturn 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/OnAppearOnce.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnAppearOnce.swift 3 | // Uploader 4 | // 5 | // Created by Guilherme Rambo on 22/04/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /// Performs the specified code block the first time the view this modifier is attached to appears. 13 | /// - Parameter block: The callback to be performed only the first time the view appears. 14 | func onAppearOnce(perform block: @escaping () -> Void) -> some View { 15 | modifier(OnAppearOnce(callback: block)) 16 | } 17 | } 18 | 19 | private struct OnAppearOnce: ViewModifier { 20 | 21 | @State private var performed = false 22 | let callback: () -> Void 23 | 24 | func body(content: Content) -> some View { 25 | content 26 | .onAppear { 27 | guard !performed else { return } 28 | performed = true 29 | callback() 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/OpenSavePanelUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import OSLog 4 | 5 | private let logger = Logger(subsystem: VirtualUIConstants.subsystemName, category: "OpenSavePanelUtils") 6 | 7 | public extension NSOpenPanel { 8 | 9 | static func run(accepting contentTypes: Set, directoryURL: URL? = nil, defaultDirectoryKey: String? = nil) -> URL? { 10 | let panel = NSOpenPanel() 11 | 12 | if contentTypes == [.folder] || contentTypes == [.directory] { 13 | panel.canChooseFiles = false 14 | panel.canChooseDirectories = true 15 | } else { 16 | panel.allowedContentTypes = Array(contentTypes) 17 | } 18 | 19 | panel.treatsFilePackagesAsDirectories = true 20 | 21 | let defaultsKey = defaultDirectoryKey.flatMap { "defaultDirectory-\($0)" } 22 | 23 | if let defaultsKey, let defaultDirectoryPath = UserDefaults.standard.string(forKey: defaultsKey) { 24 | panel.directoryURL = URL(fileURLWithPath: defaultDirectoryPath) 25 | } else if let directoryURL { 26 | panel.directoryURL = directoryURL 27 | } 28 | 29 | guard panel.runModal() == .OK, let url = panel.url else { return nil } 30 | 31 | if let defaultsKey { 32 | /// If user is choosing a folder, then just store the path to the folder itself. 33 | /// If user is choosing files, then remove the last path component to save the path to the file's directory instead. 34 | let effectiveURL = contentTypes.contains(.folder) ? url : url.deletingLastPathComponent() 35 | UserDefaults.standard.set(effectiveURL.path, forKey: defaultsKey) 36 | } 37 | 38 | return url 39 | } 40 | 41 | } 42 | 43 | public extension NSSavePanel { 44 | 45 | static func run(for contentTypes: Set, directoryURL: URL? = nil) -> URL? { 46 | let panel = NSSavePanel() 47 | 48 | panel.allowedContentTypes = Array(contentTypes) 49 | 50 | panel.treatsFilePackagesAsDirectories = true 51 | panel.directoryURL = directoryURL 52 | 53 | guard panel.runModal() == .OK, let url = panel.url else { return nil } 54 | 55 | return url 56 | } 57 | 58 | static func run(saving data: Data, as contentType: UTType, directoryURL: URL? = nil) { 59 | guard let url = run(for: [contentType], directoryURL: directoryURL) else { return } 60 | 61 | do { 62 | try data.write(to: url, options: .atomic) 63 | } catch { 64 | logger.error("Save failed: \(error.localizedDescription, privacy: .public)") 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SelfSizingGroupedForm.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftUIIntrospect 3 | 4 | /// A `Form` with the `.grouped` style that automatically resizes itself so that it 5 | /// perfectly fits its contents in the vertical axis. 6 | struct SelfSizingGroupedForm: View { 7 | var minHeight: CGFloat 8 | @ViewBuilder var content: () -> Content 9 | 10 | @State private var contentHeight: CGFloat = 0 11 | 12 | private let disabled = UserDefaults.standard.bool(forKey: "VBDisableSelfSizingGroupedForm") 13 | 14 | var body: some View { 15 | ZStack { 16 | Form { 17 | content() 18 | } 19 | .formStyle(.grouped) 20 | .introspect(.scrollView, on: .macOS(.v13, .v14, .v15)) { scrollView in 21 | guard !disabled else { return } 22 | guard let frame = scrollView.documentView?.frame else { return } 23 | guard frame.height != contentHeight else { return } 24 | guard frame.height > 0, frame.height.isFinite, !frame.height.isNaN else { return } 25 | /// Ugly, I know, but I reaaaaally wanted the form to look a specific way 🥺 26 | DispatchQueue.main.async { 27 | contentHeight = frame.height 28 | } 29 | } 30 | } 31 | .frame(height: max(minHeight, contentHeight)) 32 | } 33 | } 34 | 35 | #if DEBUG 36 | 37 | private struct _Preview: View { 38 | @State var someText = "Hello, World" 39 | @State var someBool = true 40 | 41 | var body: some View { 42 | SelfSizingGroupedForm(minHeight: 100) { 43 | TextField("This is a text field", text: $someText) 44 | Toggle("This is a toggle", isOn: $someBool) 45 | } 46 | } 47 | } 48 | 49 | #Preview("SelfSizingGroupedForm") { 50 | _Preview() 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusBarContentPanel.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | final class StatusBarContentPanel: NSPanel { 4 | 5 | override var canBecomeKey: Bool { true } 6 | 7 | @objc func hasKeyAppearance() -> Bool { 8 | return true 9 | } 10 | 11 | @objc func hasMainAppearance() -> Bool { 12 | return true 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusBarHighlightView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | 4 | struct StatusBarHighlightView: NSViewRepresentable { 5 | 6 | typealias NSViewType = NSView 7 | 8 | var isHighlighted: Bool 9 | 10 | func makeNSView(context: Context) -> NSViewType { 11 | let v = NSView() 12 | 13 | updateHighlight(in: v) 14 | 15 | return v 16 | } 17 | 18 | func updateNSView(_ nsView: NSViewType, context: Context) { 19 | updateHighlight(in: nsView) 20 | } 21 | 22 | private func updateHighlight(in view: NSViewType) { 23 | NSStatusItem.vui_drawMenuBarHighlight( 24 | in: view, 25 | highlighted: isHighlighted, 26 | inset: StatusItemButtonStyle.highlightCornerRadius * 0.5 27 | ) 28 | } 29 | 30 | } 31 | 32 | #if DEBUG 33 | 34 | struct StatusBarHighlightView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | StatusBarHighlightView(isHighlighted: true) 37 | .frame(width: 30, height: 30) 38 | .previewDisplayName("Highlighted") 39 | StatusBarHighlightView(isHighlighted: false) 40 | .frame(width: 30, height: 30) 41 | .previewDisplayName("Normal") 42 | } 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusItemMenuBarExtraView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | final class StatusItemMenuBarExtraView: NSView { 4 | 5 | override var isOpaque: Bool { false } 6 | 7 | override func viewDidMoveToSuperview() { 8 | super.viewDidMoveToSuperview() 9 | 10 | guard let superview else { return } 11 | 12 | wantsLayer = true 13 | layer?.masksToBounds = false 14 | 15 | superview.wantsLayer = true 16 | superview.layer?.masksToBounds = false 17 | 18 | superview.superview?.layer?.masksToBounds = false 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusItemPanelContentController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | final class StatusItemPanelContentController: NSViewController { 4 | 5 | let child: NSViewController 6 | var onContentSizeChange: ((CGSize) -> Void)? 7 | 8 | init(child: NSViewController, onContentSizeChange: ((CGSize) -> Void)? = nil) { 9 | self.child = child 10 | self.onContentSizeChange = onContentSizeChange 11 | 12 | super.init(nibName: nil, bundle: nil) 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | fatalError() 17 | } 18 | 19 | override func loadView() { 20 | view = NSView() 21 | view.wantsLayer = true 22 | 23 | addChild(child) 24 | child.view.translatesAutoresizingMaskIntoConstraints = false 25 | view.addSubview(child.view) 26 | 27 | NSLayoutConstraint.activate([ 28 | child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: StatusBarPanelChromeMetrics.shadowPadding), 29 | child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -StatusBarPanelChromeMetrics.shadowPadding), 30 | child.view.topAnchor.constraint(equalTo: view.topAnchor, constant: StatusBarPanelChromeMetrics.shadowPadding), 31 | child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -StatusBarPanelChromeMetrics.shadowPadding), 32 | ]) 33 | } 34 | 35 | var contentSize: CGSize { view.bounds.size } 36 | 37 | private var previouslyReportedSize: CGSize = .zero 38 | 39 | override func viewDidLayout() { 40 | super.viewDidLayout() 41 | 42 | let newSize = view.bounds.size 43 | 44 | guard newSize != previouslyReportedSize else { return } 45 | 46 | previouslyReportedSize = newSize 47 | 48 | self.onContentSizeChange?(newSize) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/VUIAppKitViewControllerHost.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | 4 | /// A SwiftUI view that hosts any AppKit view controller so that it can be presented in a 5 | /// context that requires the contents to be SwiftUI views, such as with ``StatusItemManager``. 6 | struct VUIAppKitViewControllerHost: NSViewControllerRepresentable where ChildController: NSViewController { 7 | 8 | typealias NSViewControllerType = _VUIViewControllerHostingController 9 | 10 | private let contentControllerBuilder: () -> ChildController 11 | 12 | init(with contentController: @escaping @autoclosure () -> ChildController) { 13 | self.contentControllerBuilder = contentController 14 | } 15 | 16 | func makeNSViewController(context: Context) -> NSViewControllerType { 17 | let host = _VUIViewControllerHostingController(with: contentControllerBuilder) 18 | return host 19 | } 20 | 21 | func updateNSViewController(_ nsViewController: NSViewControllerType, context: Context) { 22 | 23 | } 24 | 25 | } 26 | 27 | final class _VUIViewControllerHostingController: NSViewController where ChildController: NSViewController { 28 | 29 | private let contentControllerBuilder: () -> ChildController 30 | 31 | fileprivate init(with contentController: @escaping () -> ChildController) { 32 | self.contentControllerBuilder = contentController 33 | 34 | super.init(nibName: nil, bundle: nil) 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError() 39 | } 40 | 41 | override func loadView() { 42 | view = NSView() 43 | view.wantsLayer = true 44 | 45 | let contentController = contentControllerBuilder() 46 | 47 | addChild(contentController) 48 | contentController.view.translatesAutoresizingMaskIntoConstraints = false 49 | view.addSubview(contentController.view) 50 | 51 | NSLayoutConstraint.activate([ 52 | contentController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 53 | contentController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 54 | contentController.view.topAnchor.constraint(equalTo: view.topAnchor), 55 | contentController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) 56 | ]) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSApplication+MenuBar.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface NSApplication (MenuBar) 6 | 7 | - (void)__vui_setMenuBarVisible:(BOOL)visible; 8 | 9 | @end 10 | 11 | NS_ASSUME_NONNULL_END 12 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSApplication+MenuBar.m: -------------------------------------------------------------------------------- 1 | #import "NSApplication+MenuBar.h" 2 | 3 | @import os.log; 4 | 5 | #import 6 | 7 | #import 8 | 9 | Boolean __vui_softLinkHIMenuBarRequestVisibility(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)); 10 | 11 | @implementation NSApplication (MenuBar) 12 | 13 | + (os_log_t)__vui_menuBarLog 14 | { 15 | static os_log_t _log; 16 | static dispatch_once_t onceToken; 17 | dispatch_once(&onceToken, ^{ 18 | _log = os_log_create([[_VirtualUIConstantsObjC subsystemName] UTF8String], "NSApplication+VUIMenuBar"); 19 | }); 20 | return _log; 21 | } 22 | 23 | - (void)__vui_setMenuBarVisible:(BOOL)visible 24 | { 25 | os_log_t log = [NSApplication __vui_menuBarLog]; 26 | 27 | os_log_debug(log, "Set menu bar visibility to %{public}d", visible); 28 | 29 | Boolean result = false; 30 | Boolean alreadyInState = false; 31 | 32 | result = __vui_softLinkHIMenuBarRequestVisibility(visible, &alreadyInState, ^{ }); 33 | 34 | os_log_debug(log, "Set menu bar visibility result: %{public}d, already in state: %{public}d", visible, alreadyInState); 35 | } 36 | 37 | @end 38 | 39 | typedef Boolean (*_VUIHIMenuBarRequestVisibilityPtr)(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)); 40 | 41 | Boolean __vui_softLinkHIMenuBarRequestVisibility(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)) { 42 | static _VUIHIMenuBarRequestVisibilityPtr fnptr; 43 | 44 | static dispatch_once_t onceToken; 45 | dispatch_once(&onceToken, ^{ 46 | void *handle = dlopen("/System/Library/Frameworks/Carbon.framework/Versions/Current/Carbon", RTLD_NOW); 47 | if (!handle) { 48 | __assert("Couldn't load Carbon dylib", __FILE__, __LINE__); 49 | return; 50 | } 51 | 52 | void *fn = dlsym(handle, "_HIMenuBarRequestVisibility"); 53 | if (!fn) { 54 | __assert("Couldn't load _HIMenuBarRequestVisibility symbol", __FILE__, __LINE__); 55 | return; 56 | } 57 | 58 | fnptr = (_VUIHIMenuBarRequestVisibilityPtr)fn; 59 | }); 60 | 61 | if (!fnptr) return false; 62 | 63 | return fnptr(visibility, outAlreadyInState, completion); 64 | } 65 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusBarPrivate.h: -------------------------------------------------------------------------------- 1 | @import Cocoa; 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface NSStatusBar (Private) 6 | 7 | @property (nonatomic, readonly) CGFloat contentPadding; 8 | 9 | - (void)drawBackgroundInRect:(NSRect *)rect inView:(NSView *)view highlight:(BOOL)highlight; 10 | 11 | @end 12 | 13 | @interface NSStatusItem (Private) 14 | 15 | - (void)setAllowsVibrancy:(BOOL)flag; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusItem+.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface NSStatusItem (VUIAdditions) 6 | 7 | @property (nonatomic, strong) NSView *__nullable vui_contentView; 8 | 9 | - (void)vui_disableVibrancy; 10 | 11 | @property (class, nonatomic, readonly) CGFloat vui_idealPadding; 12 | 13 | + (void)vui_drawMenuBarHighlightInView:(NSView *)view 14 | highlighted:(BOOL)isHighlighted 15 | inset:(CGFloat)insetAmount; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusItem+.m: -------------------------------------------------------------------------------- 1 | #import "NSStatusItem+.h" 2 | 3 | #import "NSStatusBarPrivate.h" 4 | 5 | #pragma clang diagnostic push 6 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 7 | 8 | @implementation NSStatusItem (VUIAdditions) 9 | 10 | - (NSView *)vui_contentView 11 | { 12 | return self.view; 13 | } 14 | 15 | - (void)setVui_contentView:(NSView *)vui_contentView 16 | { 17 | self.view = vui_contentView; 18 | } 19 | 20 | - (void)vui_disableVibrancy 21 | { 22 | if ([self respondsToSelector:@selector(setAllowsVibrancy:)]) { 23 | [self setAllowsVibrancy:NO]; 24 | } 25 | } 26 | 27 | + (CGFloat)vui_idealPadding 28 | { 29 | if ([[NSStatusBar systemStatusBar] respondsToSelector:@selector(contentPadding)]) { 30 | return [[NSStatusBar systemStatusBar] contentPadding]; 31 | } else { 32 | return 16.0; 33 | } 34 | } 35 | 36 | + (void)vui_drawMenuBarHighlightInView:(NSView *)view highlighted:(BOOL)isHighlighted inset:(CGFloat)insetAmount 37 | { 38 | if (!view.window || !view.superview) return; 39 | if (!view.window.isVisible) return; 40 | if (view.bounds.size.width <= 0 || view.bounds.size.height <= 0) return; 41 | 42 | if (![[NSStatusBar systemStatusBar] respondsToSelector:@selector(drawBackgroundInRect:inView:highlight:)]) { 43 | return [self __vui_drawFallbackMenuBarHighlightInView:view]; 44 | } 45 | 46 | NSRect rect = NSInsetRect(view.bounds, insetAmount, 0); 47 | 48 | [[NSStatusBar systemStatusBar] drawBackgroundInRect:&rect inView:view highlight:isHighlighted]; 49 | } 50 | 51 | + (void)__vui_drawFallbackMenuBarHighlightInView:(NSView *)view 52 | { 53 | [view lockFocus]; 54 | [[[NSColor blackColor] colorWithAlphaComponent:0.1] setFill]; 55 | NSRectFill(view.bounds); 56 | [view unlockFocus]; 57 | } 58 | 59 | @end 60 | 61 | #pragma clang diagnostic pop 62 | -------------------------------------------------------------------------------- /VirtualUI/Source/Components/SwiftUI Status Item/Components/StatusItemProviderProtocol.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | 4 | public protocol StatusItemProvider: ObservableObject { 5 | /// `true` when the background of the status item's view should be highlighted. 6 | var isStatusItemHighlighted: Bool { get } 7 | 8 | var isStatusItemOccluded: Bool { get } 9 | 10 | /// Show/hide the status item's content panel. 11 | func togglePanelVisible() 12 | 13 | /// Show a pop up menu produced by running the builder closure. 14 | func showPopUpMenu(using builder: () -> NSMenu) 15 | } 16 | -------------------------------------------------------------------------------- /VirtualUI/Source/Definitions/PreviewSupport-VirtualUI.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import SwiftUI 3 | import VirtualCore 4 | 5 | @MainActor 6 | public extension VirtualMachineSessionUI { 7 | static let preview = VirtualMachineSessionUI(controller: .preview) 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /VirtualUI/Source/Definitions/VirtualUIConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualUIConstants.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | @_exported import VirtualCore 11 | 12 | struct VirtualUIConstants { 13 | static let subsystemName = "codes.rambo.VirtualUI" 14 | } 15 | 16 | @available(swift, obsoleted: 1.0, message: "Provided for Objective-C compatibility, don't use in Swift code.") 17 | @objcMembers 18 | public final class _VirtualUIConstantsObjC: NSObject { 19 | public class var subsystemName: String { VirtualUIConstants.subsystemName } 20 | } 21 | 22 | private final class _VirtualUIStub { } 23 | 24 | public extension Bundle { 25 | static let virtualUI = Bundle(for: _VirtualUIStub.self) 26 | } 27 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Components/InstallationConsole.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallationConsole.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct InstallationConsole: View { 12 | 13 | @Binding var isExpanded: Bool 14 | 15 | var overridePredicate: LogStreamer.Predicate? = nil 16 | 17 | private var predicate: LogStreamer.Predicate { 18 | overridePredicate ?? .process("com.apple.Virtualization.Installation") 19 | } 20 | 21 | var body: some View { 22 | ZStack { 23 | if isExpanded { 24 | LogConsole(predicate: predicate) 25 | .frame(minWidth: 200, maxWidth: .infinity, minHeight: 30, maxHeight: .infinity) 26 | } else { 27 | Button { 28 | withAnimation(.spring()) { 29 | isExpanded = true 30 | } 31 | } label: { 32 | Text("View Installation Logs") 33 | } 34 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 60) 35 | .buttonStyle(.link) 36 | } 37 | } 38 | .controlGroup(cornerRadius: 8, level: .secondary) 39 | } 40 | 41 | } 42 | 43 | #if DEBUG 44 | struct InstallationConsole_Previews: PreviewProvider { 45 | static var previews: some View { 46 | _Template(expanded: true) 47 | .previewDisplayName("Expanded") 48 | 49 | _Template(expanded: false) 50 | .previewDisplayName("Collapsed") 51 | } 52 | 53 | struct _Template: View { 54 | @State var isExpanded = false 55 | init(expanded: Bool) { 56 | self._isExpanded = .init(wrappedValue: expanded) 57 | } 58 | var body: some View { 59 | InstallationConsole(isExpanded: $isExpanded, overridePredicate: .process("Xcode")) 60 | .padding() 61 | .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity, alignment: .bottom) 62 | } 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Components/InstallationWizardTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallationWizardTitle.swift 3 | // VirtualBuddy 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InstallationWizardTitle: View { 11 | var text: String 12 | init(_ text: String) { self.text = text } 13 | 14 | var body: some View { 15 | Text(text) 16 | .font(.system(.title, design: .rounded).weight(.medium)) 17 | .padding(.vertical, 22) 18 | .multilineTextAlignment(.center) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Components/VirtualMachineNameField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualMachineNameField.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 01/08/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct VirtualMachineNameField: View { 12 | @Binding var name: String 13 | 14 | @FocusState private var isFocused: Bool 15 | 16 | var body: some View { 17 | HStack { 18 | TextField("Virtual Mac Name", text: $name) 19 | .textFieldStyle(.roundedBorder) 20 | .controlSize(.large) 21 | .focused($isFocused) 22 | 23 | Spacer() 24 | 25 | Button { 26 | name = RandomNameGenerator.shared.newName() 27 | } label: { 28 | Image(systemName: "arrow.clockwise") 29 | .help("Generate new name") 30 | } 31 | .buttonStyle(.borderless) 32 | .font(.system(size: 15, weight: .medium, design: .rounded)) 33 | .keyboardShortcut(.init("r", modifiers: .command)) 34 | } 35 | .onAppearOnce { 36 | DispatchQueue.main.async { 37 | self.isFocused = true 38 | } 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Steps/InstallConfigurationStepView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallConfigurationStepView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct InstallConfigurationStepView: View { 12 | @StateObject private var viewModel: VMConfigurationViewModel 13 | @State private var vm: VBVirtualMachine 14 | var onSave: (VBVirtualMachine) -> Void 15 | 16 | init(vm: VBVirtualMachine, onSave: @escaping (VBVirtualMachine) -> Void) { 17 | self._vm = .init(wrappedValue: vm) 18 | self._viewModel = .init(wrappedValue: VMConfigurationViewModel(vm, context: .preInstall)) 19 | self.onSave = onSave 20 | } 21 | 22 | var body: some View { 23 | VMConfigurationSheet(configuration: $vm.configuration, customConfirmationButtonAction: { configuration in 24 | var updatedVM = vm 25 | updatedVM.configuration = configuration 26 | self.vm = updatedVM 27 | onSave(updatedVM) 28 | }) 29 | .environmentObject(viewModel) 30 | } 31 | } 32 | 33 | #if DEBUG 34 | struct VMInstallerConfigurationStepView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | _Template() 37 | } 38 | 39 | struct _Template: View { 40 | @State private var vm = VBVirtualMachine.preview 41 | 42 | var body: some View { 43 | InstallConfigurationStepView(vm: vm, onSave: { _ in }) 44 | } 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Steps/InstallMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallMethod.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 06/03/23. 6 | // 7 | 8 | import Foundation 9 | import VirtualCore 10 | 11 | enum InstallMethod: String, Identifiable, CaseIterable, Codable { 12 | var id: RawValue { rawValue } 13 | 14 | case localFile 15 | case remoteOptions 16 | case remoteManual 17 | } 18 | 19 | extension InstallMethod { 20 | func description(for type: VBGuestType) -> String { 21 | switch self { 22 | case .localFile: 23 | switch type { 24 | case .mac: 25 | return "Open custom IPSW file from local storage" 26 | case .linux: 27 | return "Open custom ISO file from local storage" 28 | } 29 | case .remoteOptions: 30 | return "Download \(type.name) installer from a list of options" 31 | case .remoteManual: 32 | return "Download \(type.name) installer from a custom URL" 33 | } 34 | } 35 | 36 | var imageName: String { 37 | switch self { 38 | case .localFile: 39 | return "folder.fill" 40 | case .remoteOptions: 41 | return "square.and.arrow.down.fill" 42 | case .remoteManual: 43 | return "text.cursor" 44 | } 45 | } 46 | } 47 | 48 | extension VBGuestType { 49 | var customURLPrompt: String { 50 | switch self { 51 | case .mac: 52 | return "Enter the macOS IPSW URL" 53 | case .linux: 54 | return "Enter the Linux ISO URL" 55 | } 56 | } 57 | 58 | var restoreImagePickerPrompt: String { 59 | "Pick a \(name) Version to Download" 60 | } 61 | 62 | var installFinishedMessage: String { 63 | "Your \(name) Virtual Machine is Ready!" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallProgressStepView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct InstallProgressStepView: View { 12 | @EnvironmentObject var viewModel: VMInstallationViewModel 13 | 14 | @State private var consoleExpanded = false 15 | 16 | var body: some View { 17 | VStack { 18 | loadingView 19 | 20 | InstallationConsole(isExpanded: $consoleExpanded) 21 | } 22 | } 23 | 24 | @ViewBuilder 25 | private var loadingView: some View { 26 | switch viewModel.state { 27 | case .loading(let progress, let info): 28 | VStack { 29 | ProgressView(value: progress) { } 30 | .progressViewStyle(.linear) 31 | .labelsHidden() 32 | 33 | if let info = info { 34 | Text(info) 35 | .font(.system(size: 12, weight: .medium).monospacedDigit()) 36 | .foregroundColor(.secondary) 37 | } 38 | } 39 | case .error(let message): 40 | Text(message) 41 | case .idle: 42 | Text("Starting…") 43 | .foregroundColor(.secondary) 44 | } 45 | } 46 | } 47 | 48 | struct InstallProgressStepView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | InstallProgressStepView() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /VirtualUI/Source/Installer/Steps/RestoreImageDownloadView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreImageDownloadView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 20/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct RestoreImageDownloadView: View { 12 | @ObservedObject var downloader: VBDownloader 13 | 14 | var body: some View { 15 | VStack { 16 | switch downloader.state { 17 | case .idle: 18 | Text("Preparing Download…") 19 | case .downloading(let progress, let eta): 20 | progressBar(progress, eta: eta) 21 | case .done: 22 | Text("Done!") 23 | case .failed(let message): 24 | Text("The download failed: \(message)") 25 | .foregroundColor(.red) 26 | } 27 | } 28 | } 29 | 30 | @ViewBuilder 31 | private func progressBar(_ progress: Double?, eta: Double?) -> some View { 32 | VStack { 33 | ProgressView(value: progress) { } 34 | .progressViewStyle(.linear) 35 | .labelsHidden() 36 | 37 | if let eta { 38 | Text(formattedETA(from: eta)) 39 | .font(.system(size: 12, weight: .medium).monospacedDigit()) 40 | .foregroundColor(.secondary) 41 | } 42 | } 43 | } 44 | 45 | private func formattedETA(from eta: Double) -> String { 46 | let time = Int(eta) 47 | 48 | let seconds = time % 60 49 | let minutes = (time / 60) % 60 50 | let hours = (time / 3600) 51 | 52 | if hours >= 1 { 53 | return String(format: "%0.2d:%0.2d:%0.2d",hours,minutes,seconds) 54 | } else { 55 | return String(format: "%0.2d:%0.2d",minutes,seconds) 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Components/ContinuousProgressIndicator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContinuousProgressIndicator: View { 4 | var duration: TimeInterval 5 | @ViewBuilder var content: (Double) -> Content 6 | 7 | @State private var progress = Double(0) 8 | 9 | var body: some View { 10 | ZStack { 11 | content(progress) 12 | } 13 | .task { @MainActor in 14 | withAnimation(.easeIn(duration: duration)) { 15 | progress = 1 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Components/MaskProgressView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MaskProgressView: View { 4 | var progress: Double 5 | var background: Background 6 | var foreground: Foreground 7 | @ViewBuilder var mask: (Double) -> Mask 8 | 9 | var body: some View { 10 | ZStack { 11 | let content = mask(progress) 12 | 13 | content 14 | .foregroundStyle(background) 15 | 16 | content 17 | .foregroundStyle(foreground) 18 | .mask { 19 | GeometryReader { proxy in 20 | Rectangle().fill(.white) 21 | .frame(width: proxy.size.width * progress, alignment: .leading) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Components/NumberDisplayMode.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum NumberDisplayMode: Int, CaseIterable { 4 | case hex 5 | case decimal 6 | 7 | var title: String { 8 | switch self { 9 | case .hex: 10 | return "Hex" 11 | case .decimal: 12 | return "Decimal" 13 | } 14 | } 15 | } 16 | 17 | extension EnvironmentValues { 18 | @Entry var numberDisplayMode: NumberDisplayMode = .decimal 19 | } 20 | 21 | protocol FormattableNumber: CVarArg, FixedWidthInteger { 22 | func formatted(mode: NumberDisplayMode) -> String 23 | } 24 | 25 | extension FormattableNumber where Self: SignedInteger { 26 | func formatted(mode: NumberDisplayMode) -> String { 27 | switch mode { 28 | case .hex: 29 | return String(format: "0x%02llX", Int64(self)) 30 | case .decimal: 31 | return String(format: "%lld", Int64(self)) 32 | } 33 | } 34 | } 35 | 36 | extension FormattableNumber where Self: UnsignedInteger { 37 | func formatted(mode: NumberDisplayMode) -> String { 38 | switch mode { 39 | case .hex: 40 | return String(format: "0x%02llX", UInt64(self)) 41 | case .decimal: 42 | return String(format: "%llu", UInt64(self)) 43 | } 44 | } 45 | } 46 | 47 | extension Int64: FormattableNumber { } 48 | extension UInt64: FormattableNumber { } 49 | extension Int32: FormattableNumber { } 50 | extension UInt32: FormattableNumber { } 51 | extension Int16: FormattableNumber { } 52 | extension UInt16: FormattableNumber { } 53 | extension Int8: FormattableNumber { } 54 | extension UInt8: FormattableNumber { } 55 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Components/SavedStatePicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import VirtualCore 3 | 4 | struct SavedStatePicker: View { 5 | @EnvironmentObject private var controller: VMSavedStatesController 6 | 7 | @Binding var selectedStateURL: URL? 8 | 9 | var body: some View { 10 | Picker("State", selection: $selectedStateURL) { 11 | if controller.states.isEmpty { 12 | Text("No Saved States") 13 | .tag(Optional.none) 14 | } else { 15 | Text("Don’t Restore") 16 | .tag(Optional.none) 17 | 18 | Divider() 19 | } 20 | 21 | ForEach(controller.states) { state in 22 | Text(state.url.deletingPathExtension().lastPathComponent) 23 | .tag(Optional.some(state.url)) 24 | } 25 | } 26 | .disabled(controller.states.isEmpty) 27 | } 28 | } 29 | 30 | #if DEBUG 31 | private struct _Preview: View { 32 | @StateObject private var controller = VMSavedStatesController.preview 33 | @State private var selectedStateURL: URL? 34 | 35 | var body: some View { 36 | Form { 37 | SavedStatePicker(selectedStateURL: $selectedStateURL) 38 | .environmentObject(controller) 39 | } 40 | .formStyle(.grouped) 41 | } 42 | } 43 | 44 | #Preview("SavedStatePicker") { 45 | _Preview() 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Components/VMProgressOverlay.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct VMProgressOverlay: View { 4 | let message: String 5 | let duration: TimeInterval 6 | 7 | var body: some View { 8 | ContinuousProgressIndicator(duration: duration) { progress in 9 | MaskProgressView(progress: progress * 1.2, background: .tertiary, foreground: .primary) { _ in 10 | Text(message) 11 | .font(.system(.title, design: .rounded, weight: .semibold)) 12 | } 13 | .scaleEffect(0.85 + 0.15 * progress) 14 | } 15 | .id(message) 16 | .transition(.scale(scale: 0.2).combined(with: .opacity)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /VirtualUI/Source/Session/Configuration/VMSessionConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMSessionConfigurationView.swift 3 | // VirtualBuddy 4 | // 5 | // Created by Guilherme Rambo on 07/04/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct VMSessionConfigurationView: View { 12 | @EnvironmentObject var controller: VMController 13 | 14 | @State private var isShowingVMSettings = false 15 | 16 | private var vm: VBVirtualMachine { controller.virtualMachineModel } 17 | 18 | var body: some View { 19 | SelfSizingGroupedForm(minHeight: 100) { 20 | if showSavedStatePicker { 21 | SavedStatePicker(selectedStateURL: $controller.options.stateRestorationPackageURL) 22 | .environmentObject(controller.savedStatesController) 23 | } 24 | 25 | if showInstallDeviceOption { 26 | Toggle("Boot on install drive", isOn: $controller.options.bootOnInstallDevice) 27 | } 28 | 29 | if showRecoveryModeOption { 30 | Toggle("Boot in recovery mode", isOn: $controller.options.bootInRecoveryMode) 31 | .disabled(controller.options.bootInDFUMode) 32 | } 33 | 34 | if showDFUOption { 35 | Toggle("Boot in DFU mode", isOn: $controller.options.bootInDFUMode) 36 | .disabled(controller.options.bootInRecoveryMode) 37 | } 38 | 39 | Toggle("Capture system keyboard shortcuts", isOn: $controller.virtualMachineModel.configuration.captureSystemKeys) 40 | 41 | Button("Virtual Machine Settings…") { 42 | isShowingVMSettings.toggle() 43 | } 44 | .frame(maxWidth: .infinity, alignment: .trailing) 45 | } 46 | .sheet(isPresented: $isShowingVMSettings) { 47 | VMConfigurationSheet( 48 | configuration: $controller.virtualMachineModel.configuration 49 | ) 50 | .environmentObject(VMConfigurationViewModel(vm)) 51 | } 52 | } 53 | 54 | private var showInstallDeviceOption: Bool { vm.configuration.systemType == .linux && vm.metadata.installImageURL != nil } 55 | 56 | private var showRecoveryModeOption: Bool { vm.configuration.systemType == .mac } 57 | 58 | private var showDFUOption: Bool { VBMacConfiguration.appBuildAllowsDFUMode && vm.configuration.systemType == .mac } 59 | 60 | private var showSavedStatePicker: Bool { vm.configuration.systemType.supportsStateRestoration } 61 | } 62 | 63 | #if DEBUG 64 | #Preview { 65 | VirtualMachineSessionViewPreview() 66 | } 67 | #endif 68 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/HardwareConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HardwareConfigurationView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 18/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct HardwareConfigurationView: View { 12 | 13 | @Binding var device: VBMacDevice 14 | 15 | var body: some View { 16 | NumericPropertyControl( 17 | value: $device.cpuCount, 18 | range: VBMacDevice.virtualCPUCountRange, 19 | label: "Virtual CPUs", 20 | formatter: NumberFormatter.numericPropertyControlDefault, 21 | spacing: VMConfigurationView.labelSpacing 22 | ) 23 | 24 | NumericPropertyControl( 25 | value: $device.memorySize.gbValue, 26 | range: VBMacDevice.memorySizeRangeInGigabytes, 27 | label: "Memory (GB)", 28 | formatter: NumberFormatter.numericPropertyControlDefault, 29 | spacing: VMConfigurationView.labelSpacing 30 | ) 31 | } 32 | 33 | } 34 | 35 | #if DEBUG 36 | struct HardwareConfigurationView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | _ConfigurationSectionPreview { HardwareConfigurationView(device: $0.hardware) } 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/KeyboardDeviceConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardDeviceConfigurationView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 02/10/23. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct KeyboardDeviceConfigurationView: View { 12 | @Binding var hardware: VBMacDevice 13 | 14 | var body: some View { 15 | PropertyControl("Device Type", spacing: 8) { 16 | VStack(alignment: .leading) { 17 | Picker("Device Type", selection: $hardware.keyboardDevice.kind) { 18 | ForEach(VBKeyboardDevice.Kind.allCases) { kind in 19 | Text(kind.name) 20 | .tag(kind) 21 | } 22 | } 23 | 24 | if let error = hardware.keyboardDevice.kind.error { 25 | Text(error) 26 | .foregroundColor(.red) 27 | } else if let warning = hardware.keyboardDevice.kind.warning { 28 | Text(warning) 29 | .foregroundColor(.yellow) 30 | } 31 | } 32 | } 33 | 34 | } 35 | } 36 | 37 | #if DEBUG 38 | struct KeyboardDeviceConfigurationView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | _ConfigurationSectionPreview { KeyboardDeviceConfigurationView(hardware: $0.hardware) } 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/PointingDeviceConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointingDeviceConfigurationView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 19/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct PointingDeviceConfigurationView: View { 12 | @Binding var hardware: VBMacDevice 13 | 14 | var body: some View { 15 | PropertyControl("Device Type", spacing: 8) { 16 | VStack(alignment: .leading) { 17 | Picker("Device Type", selection: $hardware.pointingDevice.kind) { 18 | ForEach(VBPointingDevice.Kind.allCases) { kind in 19 | Text(kind.name) 20 | .tag(kind) 21 | } 22 | } 23 | 24 | if let error = hardware.pointingDevice.kind.error { 25 | Text(error) 26 | .foregroundColor(.red) 27 | } else if let warning = hardware.pointingDevice.kind.warning { 28 | Text(warning) 29 | .foregroundColor(.yellow) 30 | } 31 | } 32 | } 33 | 34 | } 35 | } 36 | 37 | #if DEBUG 38 | struct PointingDeviceConfigurationView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | _ConfigurationSectionPreview { PointingDeviceConfigurationView(hardware: $0.hardware) } 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/Sharing/SharedFolderListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedFolderListItem.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 19/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct SharedFolderListItem: View { 12 | @Binding var folder: VBSharedFolder 13 | 14 | var body: some View { 15 | HStack(spacing: 2) { 16 | Toggle(folder.shortName, isOn: $folder.isEnabled) 17 | label 18 | } 19 | .lineLimit(1) 20 | .truncationMode(.middle) 21 | .controlSize(.mini) 22 | .disabled(folder.errorMessage != nil) 23 | .labelsHidden() 24 | .padding(.vertical, 4) 25 | /// Easier to hit trailing edge buttons without hovering floating scroll bar. 26 | .padding(.trailing, 4) 27 | } 28 | 29 | @ViewBuilder 30 | private var label: some View { 31 | HStack(spacing: 4) { 32 | folder.icon(maxHeight: 14) 33 | 34 | Text(folder.shortName) 35 | .help(folder.url.path) 36 | 37 | Spacer() 38 | 39 | if let errorMessage = folder.errorMessage { 40 | Image(systemName: "exclamationmark.triangle.fill") 41 | .symbolRenderingMode(.multicolor) 42 | .help(errorMessage) 43 | } else { 44 | Button { 45 | folder.isReadOnly.toggle() 46 | } label: { 47 | Image(systemName: folder.isReadOnly ? "pencil.slash" : "pencil") 48 | } 49 | .help(folder.isReadOnly ? "Make writable" : "Make read only") 50 | .buttonStyle(.plain) 51 | .disabled(!folder.isEnabled) 52 | } 53 | } 54 | .padding(.leading, 6) 55 | .opacity(folder.isEnabled ? 1 : 0.8) 56 | .opacity(folder.errorMessage != nil ? 0.3 : 1) 57 | .font(.system(size: 11)) 58 | } 59 | } 60 | 61 | extension VBSharedFolder { 62 | func icon(maxHeight: CGFloat) -> some View { 63 | let image: NSImage 64 | if let externalVolumeURL { 65 | image = NSWorkspace.shared.icon(forFile: externalVolumeURL.path) 66 | } else { 67 | image = NSWorkspace.shared.icon(forFile: url.path) 68 | } 69 | let dimension = min(image.size.width, image.size.height) 70 | let scale = (maxHeight) / dimension 71 | image.size = NSSize(width: image.size.width * scale, height: image.size.height * scale) 72 | return Image(nsImage: image) 73 | .resizable() 74 | .aspectRatio(contentMode: .fit) 75 | .frame(maxHeight: maxHeight) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/Sharing/SharingConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharingConfigurationView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 18/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct SharingConfigurationView: View { 12 | @Binding var configuration: VBMacConfiguration 13 | 14 | var body: some View { 15 | SharedFoldersManagementView(configuration: $configuration) 16 | } 17 | } 18 | 19 | #if DEBUG 20 | struct _ConfigurationSectionPreview: View { 21 | 22 | @State private var config: VBMacConfiguration 23 | var content: (Binding) -> C 24 | var ungrouped: Bool 25 | 26 | init(_ config: VBMacConfiguration = .preview, ungrouped: Bool = false, @ViewBuilder _ content: @escaping (Binding) -> C) { 27 | self._config = .init(wrappedValue: config) 28 | self.ungrouped = ungrouped 29 | self.content = content 30 | } 31 | 32 | var body: some View { 33 | Group { 34 | if ungrouped { 35 | content($config) 36 | } else { 37 | ConfigurationSection(.constant(false), { 38 | content($config) 39 | }, header: { 40 | Label("SwiftUI Preview", systemImage: "eye") 41 | }) 42 | } 43 | } 44 | .frame(maxWidth: 320, maxHeight: .infinity, alignment: .top) 45 | .padding() 46 | .controlGroup() 47 | .padding(30) 48 | .frame(maxWidth: .infinity, maxHeight: .infinity) 49 | } 50 | 51 | } 52 | 53 | struct SharingConfigurationView_Previews: PreviewProvider { 54 | static var previews: some View { 55 | _ConfigurationSectionPreview { SharingConfigurationView(configuration: $0) } 56 | 57 | _ConfigurationSectionPreview(.preview.linuxVirtualMachine) { 58 | SharingConfigurationView(configuration: $0) } 59 | .previewDisplayName("Linux Sharing Configuration View") 60 | 61 | _ConfigurationSectionPreview(.preview.removingSharedFolders) { SharingConfigurationView(configuration: $0) } 62 | .previewDisplayName("Empty") 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/Sections/SoundConfigurationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoundConfigurationView.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 18/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | struct SoundConfigurationView: View { 12 | @Binding var hardware: VBMacDevice 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 16) { 16 | Toggle("Enable Sound", isOn: soundEnabled) 17 | 18 | if hardware.soundDevices.isEmpty { 19 | Toggle("Enable Sound Input", isOn: .constant(false)) 20 | .disabled(true) 21 | } else { 22 | Toggle("Enable Sound Input", isOn: $hardware.soundDevices[0].enableInput) 23 | } 24 | } 25 | } 26 | 27 | private var soundEnabled: Binding { 28 | .init(get: { 29 | !hardware.soundDevices.isEmpty 30 | }, set: { newValue in 31 | if newValue, hardware.soundDevices.isEmpty { 32 | hardware.soundDevices = [.default] 33 | } else { 34 | hardware.soundDevices.removeAll() 35 | } 36 | }) 37 | } 38 | } 39 | 40 | #if DEBUG 41 | struct SoundConfigurationView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | _ConfigurationSectionPreview { SoundConfigurationView(hardware: $0.hardware) } 44 | } 45 | 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /VirtualUI/Source/VM Configuration/VMConfigurationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMConfigurationViewModel.swift 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 18/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import VirtualCore 10 | 11 | public enum VMConfigurationContext: Int { 12 | case preInstall 13 | case postInstall 14 | } 15 | 16 | public final class VMConfigurationViewModel: ObservableObject { 17 | 18 | @Published var config: VBMacConfiguration { 19 | didSet { 20 | /// Reset display preset when changing display settings. 21 | /// This is so the warning goes away, if any warning is being shown. 22 | if config.hardware.displayDevices != oldValue.hardware.displayDevices, 23 | config.hardware.displayDevices.first != selectedDisplayPreset?.device 24 | { 25 | selectedDisplayPreset = nil 26 | } 27 | } 28 | } 29 | 30 | @Published public internal(set) var supportState: VBMacConfiguration.SupportState = .supported 31 | 32 | @Published var selectedDisplayPreset: VBDisplayPreset? 33 | 34 | @Published private(set) var vm: VBVirtualMachine 35 | 36 | public let context: VMConfigurationContext 37 | 38 | public init(_ vm: VBVirtualMachine, context: VMConfigurationContext = .postInstall) { 39 | self.config = vm.configuration 40 | self.vm = vm 41 | self.context = context 42 | 43 | Task { await validate() } 44 | } 45 | 46 | @discardableResult 47 | public func validate() async -> VBMacConfiguration.SupportState { 48 | let updatedState = await config.validate(for: vm, skipVirtualizationConfig: context == .preInstall) 49 | 50 | await MainActor.run { 51 | supportState = updatedState 52 | } 53 | 54 | return updatedState 55 | } 56 | 57 | public func createImage(for device: VBStorageDevice) async throws { 58 | guard let image = device.managedImage else { 59 | throw Failure("Only managed disk images can be created.") 60 | } 61 | 62 | let settings = DiskImageGenerator.ImageSettings(for: image, in: vm) 63 | 64 | try await DiskImageGenerator.generateImage(with: settings) 65 | } 66 | 67 | public func updateBootStorageDevice(with image: VBManagedDiskImage) { 68 | guard let idx = config.hardware.storageDevices.firstIndex(where: { $0.isBootVolume }) else { 69 | fatalError("Missing boot device in VM configuration") 70 | } 71 | 72 | var device = config.hardware.storageDevices[idx] 73 | device.backing = .managedImage(image) 74 | config.hardware.addOrUpdate(device) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /VirtualUI/VirtualUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualUI.h 3 | // VirtualUI 4 | // 5 | // Created by Guilherme Rambo on 17/07/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for VirtualUI. 11 | FOUNDATION_EXPORT double VirtualUIVersionNumber; 12 | 13 | //! Project version string for VirtualUI. 14 | FOUNDATION_EXPORT const unsigned char VirtualUIVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | #import 19 | #import 20 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Definitions/VirtualWormholeConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualWormholeConstants.swift 3 | // VirtualWormholeConstants 4 | // 5 | // Created by Guilherme Rambo on 02/06/22. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | struct VirtualWormholeConstants { 12 | static let subsystemName = "codes.rambo.VirtualWormhole" 13 | 14 | static let verboseLoggingEnabled: Bool = { 15 | #if DEBUG 16 | return UserDefaults.standard.bool(forKey: "WHVerbosePacketLogging") 17 | #else 18 | return false 19 | #endif 20 | }() 21 | 22 | static let payloadPropagationEnabled: Bool = { 23 | !UserDefaults.standard.bool(forKey: "WHDisablePayloadPropagation") 24 | }() 25 | 26 | static let connectionTimeoutInNanoseconds: UInt64 = 15 * NSEC_PER_SEC 27 | 28 | static let pingIntervalInSeconds: TimeInterval = 5.0 29 | } 30 | 31 | extension Logger { 32 | init(for type: T.Type) { 33 | self.init(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: type)) 34 | } 35 | } 36 | 37 | private final class _VirtualWormholeStub { } 38 | 39 | public extension Bundle { 40 | static let virtualWormhole = Bundle(for: _VirtualWormholeStub.self) 41 | } 42 | 43 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/Base/WormholeServiceClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WormholeServiceClient.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 10/03/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol WormholeServiceClient { 11 | associatedtype ServiceType: WormholeService 12 | 13 | init(with service: ServiceType) 14 | } 15 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/Base/WormholeServiceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WormholeServiceProtocol.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 02/06/22. 6 | // 7 | 8 | import Foundation 9 | import Virtualization 10 | 11 | public protocol WormholeMultiplexer: AnyObject { 12 | 13 | var side: WHConnectionSide { get } 14 | 15 | func send(_ payload: T, to peerID: WHPeerID?) async 16 | 17 | func stream(for payloadType: T.Type) -> AsyncThrowingStream<(senderID: WHPeerID, payload: T), Error> 18 | 19 | } 20 | 21 | public protocol WormholeService: AnyObject { 22 | 23 | static var id: String { get } 24 | 25 | init(with connection: WormholeMultiplexer) 26 | 27 | func activate() 28 | 29 | } 30 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/DefaultsImport/Implementation/DefaultsDomainDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsDomainDescriptor.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 09/03/23. 6 | // 7 | 8 | import Cocoa 9 | import UniformTypeIdentifiers 10 | 11 | public struct DefaultsDomainDescriptor: Identifiable, Codable { 12 | public struct Target: Identifiable, Codable { 13 | public var id: String { bundleIdentifier } 14 | public var bundleIdentifier: String 15 | public var name: String 16 | public var isSystemService: Bool 17 | } 18 | 19 | public struct Restart: Codable { 20 | public var command: String 21 | public var needsConfirmation = true 22 | public var shouldRelaunch = true 23 | } 24 | 25 | public var id: Target.ID { target.id } 26 | public var target: Target 27 | public var ignoredKeyPaths: [String] = [] 28 | public var restart: Restart? 29 | } 30 | 31 | public extension DefaultsDomainDescriptor.Target { 32 | var isRunning: Bool { 33 | /// System services are not included in `NSRunningApplication.runningApplications`, 34 | /// so just assume they're always running (which will be the case most of the time). 35 | guard !isSystemService else { return true } 36 | return NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).contains(where: { !$0.isTerminated }) 37 | } 38 | 39 | var bundleURL: URL? { 40 | NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) 41 | } 42 | 43 | func iconImage(with size: CGSize = CGSize(width: 128, height: 128)) -> NSImage { 44 | func getIcon() -> NSImage { 45 | guard let url = bundleURL else { 46 | return NSWorkspace.shared.icon(for: .application) 47 | } 48 | 49 | return NSWorkspace.shared.icon(forFile: url.path) 50 | } 51 | 52 | let image = getIcon() 53 | image.size = size 54 | return image 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/DefaultsImport/Implementation/DefaultsImportController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsImportController.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 09/03/23. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import Combine 11 | 12 | public typealias DefaultsDomainCollection = [DefaultsDomainDescriptor.ID: DefaultsDomainDescriptor] 13 | 14 | public final class DefaultsImportController: ObservableObject { 15 | 16 | private lazy var logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: Self.self)) 17 | 18 | @Published public private(set) var sortedDomains = [DefaultsDomainDescriptor]() 19 | 20 | @Published public private(set) var descriptors = DefaultsDomainCollection() 21 | 22 | private lazy var cancellables = Set() 23 | 24 | public init() { 25 | $descriptors 26 | .map { $0.values.sorted(by: { $0.target.name.localizedStandardCompare($1.target.name) == .orderedAscending }) } 27 | .assign(to: &$sortedDomains) 28 | 29 | loadDomains() 30 | } 31 | 32 | private func loadDomains() { 33 | do { 34 | guard let url = Bundle.virtualWormhole.url(forResource: "DefaultsDomains", withExtension: "plist") else { 35 | throw CocoaError(.fileNoSuchFile, userInfo: [NSLocalizedDescriptionKey: "DefaultsDomains.plist missing from VirtualWormhole bundle"]) 36 | } 37 | 38 | let data = try Data(contentsOf: url) 39 | 40 | let loadedDescriptors = try PropertyListDecoder().decode(DefaultsDomainCollection.self, from: data) 41 | 42 | self.descriptors = loadedDescriptors 43 | } catch { 44 | logger.fault("Failed to load descriptors: \(error, privacy: .public)") 45 | assertionFailure("Failed to load descriptors: \(error)") 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/DefaultsImport/Resources/DefaultsDomains.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.Terminal 6 | 7 | target 8 | 9 | bundleIdentifier 10 | com.apple.Terminal 11 | name 12 | Terminal 13 | isSystemService 14 | 15 | 16 | ignoredKeyPaths 17 | 18 | restart 19 | 20 | command 21 | killall Terminal && sleep 1 22 | needsConfirmation 23 | 24 | shouldRelaunch 25 | 26 | 27 | 28 | com.apple.Dock 29 | 30 | target 31 | 32 | bundleIdentifier 33 | com.apple.Dock 34 | name 35 | Dock 36 | isSystemService 37 | 38 | 39 | ignoredKeyPaths 40 | 41 | persistent-apps 42 | persistent-others 43 | recent-apps 44 | 45 | restart 46 | 47 | command 48 | killall Dock 49 | needsConfirmation 50 | 51 | shouldRelaunch 52 | 53 | 54 | 55 | com.apple.Finder 56 | 57 | target 58 | 59 | bundleIdentifier 60 | com.apple.Finder 61 | name 62 | Finder 63 | isSystemService 64 | 65 | 66 | ignoredKeyPaths 67 | 68 | NSNavLastRootDirectory 69 | RecentMoveAndCopyDestinations 70 | NewWindowTargetPath 71 | FXConnectToLastURL 72 | GoToField 73 | GoToFieldHistory 74 | 75 | restart 76 | 77 | command 78 | killall Finder 79 | needsConfirmation 80 | 81 | shouldRelaunch 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/Services/DefaultsImport/WHDefaultsImportClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WHDefaultsImportClient.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 10/03/23. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | public final class WHDefaultsImportClient: WormholeServiceClient { 12 | 13 | private lazy var logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: Self.self)) 14 | 15 | public typealias ServiceType = WHDefaultsImportService 16 | 17 | let service: WHDefaultsImportService 18 | 19 | public init(with service: WHDefaultsImportService) { 20 | self.service = service 21 | } 22 | 23 | public func importDomain(with id: DefaultsDomainDescriptor.ID) async throws { 24 | logger.debug("Requesting export for \(id, privacy: .public)") 25 | 26 | let response = Task { 27 | let stream = service.onDomainResponseReceived.filter({ $0.domainID == id }).values 28 | 29 | for await response in stream { 30 | switch response { 31 | case .failure(_, let message): 32 | logger.error("Export request for \(id, privacy: .public) resolved with error: \(message, privacy: .public)") 33 | 34 | throw CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: message]) 35 | case .success(let id, let data): 36 | logger.debug("Export request for \(id, privacy: .public) resolved successfully") 37 | 38 | try await performImport(for: id, with: data) 39 | default: 40 | continue 41 | } 42 | break 43 | } 44 | } 45 | 46 | await service.sendExportRequest(for: id) 47 | 48 | logger.debug("Export request for \(id, privacy: .public) sent, waiting for response") 49 | 50 | try await response.value 51 | } 52 | 53 | private func performImport(for domainID: String, with data: Data) async throws { 54 | let domain = try service.fetchDescriptor(for: domainID) 55 | 56 | let tempURL = service.temporaryURL(for: domainID) 57 | 58 | try data.write(to: tempURL) 59 | 60 | try await domain.importDefaults(from: tempURL) 61 | 62 | try? FileManager.default.removeItem(at: tempURL) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/WireProtocol/WHPayload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WHPayload.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 25/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Protocol adopted by types that can be sent over the guest <> host connection. 11 | public protocol WHPayload: Codable { 12 | /// When `true`, the payload will be sent again if connection gets interrupted and re-established. 13 | static var resendOnReconnect: Bool { get } 14 | 15 | /// When `true`, the host will distribute the payload to all booted guests 16 | /// upon receiving the payload from one of the guests. 17 | static var propagateBetweenGuests: Bool { get } 18 | } 19 | 20 | public extension WHPayload { 21 | static var resendOnReconnect: Bool { false } 22 | static var propagateBetweenGuests: Bool { false } 23 | } 24 | -------------------------------------------------------------------------------- /VirtualWormhole/Source/WireProtocol/WHPing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WHPing.swift 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 08/03/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WHPing: WHPayload { 11 | var date = Date.now 12 | } 13 | 14 | struct WHPong: WHPayload { 15 | var date = Date.now 16 | } 17 | 18 | extension WormholePacket { 19 | var isPing: Bool { payloadType == String(describing: WHPing.self) } 20 | var isPong: Bool { payloadType == String(describing: WHPong.self) } 21 | } 22 | 23 | extension WormholePacket { 24 | static var ping: WormholePacket { 25 | get throws { try WormholePacket(WHPing()) } 26 | } 27 | static var pong: WormholePacket { 28 | get throws { try WormholePacket(WHPong()) } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /VirtualWormhole/VirtualWormhole.h: -------------------------------------------------------------------------------- 1 | // 2 | // VirtualWormhole.h 3 | // VirtualWormhole 4 | // 5 | // Created by Guilherme Rambo on 02/06/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for VirtualWormhole. 11 | FOUNDATION_EXPORT double VirtualWormholeVersionNumber; 12 | 13 | //! Project version string for VirtualWormhole. 14 | FOUNDATION_EXPORT const unsigned char VirtualWormholeVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /VirtualWormholeTests/Resources/TestStream.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/VirtualWormholeTests/Resources/TestStream.bin -------------------------------------------------------------------------------- /assets/DeviceSupport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/assets/DeviceSupport.png -------------------------------------------------------------------------------- /assets/GuestApp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/assets/GuestApp.jpg -------------------------------------------------------------------------------- /assets/Showcase.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/assets/Showcase.jpg -------------------------------------------------------------------------------- /assets/VirtualBuddyIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/VirtualBuddy/14c7947fff30137fa062697bb37cff461c3549b4/assets/VirtualBuddyIcon.png -------------------------------------------------------------------------------- /data/linux_v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": 1, 3 | "channels": [ 4 | { 5 | "id": "stable", 6 | "name": "Stable", 7 | "note": "Public, stable releases.", 8 | "icon": "checkmark.seal" 9 | }, 10 | { 11 | "id": "daily", 12 | "name": "Daily", 13 | "note": "Daily builds meant for testing.", 14 | "icon": "wrench.and.screwdriver" 15 | } 16 | ], 17 | "groups": [ 18 | { 19 | "id": "ubuntu", 20 | "name": "Ubuntu", 21 | "majorVersion": "22.0", 22 | "minHostVersion": "13.0" 23 | } 24 | ], 25 | "restoreImages": [ 26 | { 27 | "group": "ubuntu", 28 | "name": "Jammy Jellyfish Daily Build", 29 | "build": "22.04.3 LTS", 30 | "url": "https://cdimage.ubuntu.com/jammy/daily-live/current/jammy-desktop-arm64.iso", 31 | "channel": "daily" 32 | } 33 | ] 34 | } --------------------------------------------------------------------------------