import Frida import LumaCore import SwiftUI #if canImport(AppKit) struct VirtualMachinePanel: View { let engine: Engine @State private var miniaturized: Set = [] @State private var failure: String? @State private var isShowingBootSheet = false var body: some View { VStack(alignment: .leading, spacing: 8) { header if !records.isEmpty { machineList } else { emptyState } if let failure { Text(failure) .font(.footnote) .foregroundStyle(.red) } } .padding(12) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private var machineList: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { ForEach(records) { record in VirtualMachineControls( engine: engine, record: record, isMiniature: miniature(record), failure: $failure ) if record.id != records.last?.id { Divider() } } } } } private func miniature(_ record: VirtualMachineRecord) -> Binding { Binding( get: { miniaturized.contains(record.id) }, set: { wanted in if wanted { miniaturized.insert(record.id) } else { miniaturized.remove(record.id) } } ) } private var emptyState: some View { VStack(spacing: 24) { EmptyStateHeading( title: "Machines", systemImage: "desktopcomputer", subtitle: "Boot a guest and instrument anything running in it." ) Button { isShowingBootSheet = true } label: { Label("Boot Machine", systemImage: "play.fill") } .buttonStyle(.borderedProminent) .controlSize(.large) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, maxHeight: .infinity) .sheet(isPresented: $isShowingBootSheet) { BootVirtualMachineSheet(engine: engine) { _ in } } } private var header: some View { HStack { if !records.isEmpty { Text("Machines") .font(.headline) } Spacer() Button { engine.setSidePanel(nil) } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 14)) } .buttonStyle(.plain) } } private var records: [VirtualMachineRecord] { engine.virtualMachines.records } } struct VirtualMachineControls: View { let engine: Engine let record: VirtualMachineRecord @Binding var isMiniature: Bool @Binding var failure: String? @State private var isConfirmingForget = false var body: some View { VStack(alignment: .leading, spacing: 8) { summary if !isMiniature, let display = machine?.display { VirtualMachineDisplayView(display: display) .frame(maxWidth: .infinity) .containerRelativeFrame(.vertical) { height, _ in height * 0.85 } } } .confirmationDialog("Forget Machine?", isPresented: $isConfirmingForget, titleVisibility: .visible) { Button("Forget Machine", role: .destructive) { Task { await engine.virtualMachines.forget(record) } } Button("Cancel", role: .cancel) {} } message: { Text("This will stop \"\(record.name)\" and delete its snapshot. Its disk image is left alone.") } } private var summary: some View { HStack(alignment: .center, spacing: 10) { Button { isMiniature.toggle() } label: { Image(systemName: isMiniature ? "chevron.forward" : "chevron.down") .font(.caption.weight(.semibold)) .frame(width: 12) } .buttonStyle(.plain) .help(isMiniature ? "Show the screen at full size" : "Collapse the screen into a miniature") HStack(spacing: 10) { if let icon = engine.virtualMachines.template(for: record)?.icon { icon.swiftUIImage .resizable() .interpolation(.high) .frame(width: 28, height: 28) } VStack(alignment: .leading, spacing: 2) { Text(record.name) .font(.subheadline.weight(.medium)) if let template = engine.virtualMachines.template(for: record), case let detail = template.detailLabel(for: record.parameters), detail != record.name { Text(detail) .font(.caption) .foregroundStyle(.secondary) } } .lineLimit(1) .truncationMode(.tail) } .contentShape(Rectangle()) .onTapGesture { isMiniature.toggle() } Spacer(minLength: 8) if isMiniature, let display = machine?.display { VirtualMachineDisplayView(display: display) .frame(height: Self.miniatureHeight) .allowsHitTesting(false) .contentShape(Rectangle()) .onTapGesture { isMiniature = false } } if let passingState { Text(passingState) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.tail) } primaryAction Menu { lessTravelledActions } label: { Image(systemName: "ellipsis.circle") } .menuStyle(.borderlessButton) .menuIndicator(.hidden) .frame(width: 24) } } @ViewBuilder private var primaryAction: some View { if let machine { action("Stop", systemImage: "stop.fill") { Task { await engine.virtualMachines.stop(record) } } .disabled(machine.state == .starting || engine.virtualMachines.isStopping(record)) } else { action(record.hasReadySnapshot ? "Resume where it was marked ready" : "Boot", systemImage: "play.fill") { perform { _ = try await engine.virtualMachines.boot(record) } } } } @ViewBuilder private var lessTravelledActions: some View { if let display = machine?.display { Button("Full Screen") { VirtualMachineFullScreen.show(display, named: record.name) } Divider() } if let machine { Button("Mark Ready") { perform { try await engine.virtualMachines.markReady(machine) } } .disabled(!machine.capabilities.contains(.snapshot)) Button("Back to Ready") { perform { try await machine.restoreReadySnapshot() } } .disabled(!machine.capabilities.contains(.snapshot) || !record.hasReadySnapshot) } else if record.hasReadySnapshot { Button("Boot Fresh, Leaving the Snapshot Behind") { perform { _ = try await engine.virtualMachines.boot(record, resumingFromReadySnapshot: false) } } } if record.hasReadySnapshot { Button("Forget the Ready Snapshot") { perform { try await engine.virtualMachines.discardReadySnapshot(record) } } } Divider() Button("Forget Machine…", role: .destructive) { isConfirmingForget = true } } private func action(_ name: String, systemImage: String, perform: @escaping () -> Void) -> some View { Button(action: perform) { Image(systemName: systemImage) .frame(width: 18, height: 18) } .buttonStyle(.borderless) .help(name) .accessibilityLabel(name) } private static let miniatureHeight: CGFloat = 40 /// Running and stopped are what the button beside this already says; the /// states worth a word are the ones on the way to somewhere. private var passingState: String? { if engine.virtualMachines.isStopping(record) { return "Stopping…" } switch machine?.state { case .running, .stopped, nil: return nil case .some(let state): return state.summary } } private var machine: (any VirtualMachine)? { engine.virtualMachines.machine(for: record) } private func perform(_ work: @escaping () async throws -> Void) { failure = nil Task { do { try await work() } catch { failure = error.localizedDescription } } } } extension VirtualMachineState { var summary: String { switch self { case .starting: return "Starting…" case .installing(let fraction): return "Installing… \(Int(fraction * 100))%" case .running: return "Running" case .capturingSnapshot: return "Taking a snapshot…" case .restoringSnapshot: return "Going back to the snapshot…" case .stopped: return "Stopped" case .failed(let reason): return reason } } } #endif