import AppKit import SwiftUI struct OrionMenuView: View { @EnvironmentObject var vm: OrionVMController @Environment(\.openSettings) private var openSettings @State private var expandedGroups: Set = [] @State private var expandedServers: Set = [] private var groups: [String] { let preferred = [ "Windows", "Ubuntu", "Debian", "Fedora", "CentOS", "Alpine", "Oracle Linux", "Rocky Linux", "AlmaLinux", "Arch Linux", "Gentoo", "FreeBSD", "Other" ] return preferred.filter { os in vm.discoveredVMs.contains { $0.operatingSystem == os } } } var body: some View { VStack(alignment: .leading, spacing: 0) { header Divider() ScrollView { VStack(alignment: .leading, spacing: 4) { discoverySection Divider() .padding(.vertical, 10) integratedSection } .padding(.horizontal, 14) .padding(.vertical, 10) } .frame(maxHeight: 560) Divider() footer } .frame(width: 410) .task { await vm.discoverVMs() } .alert( "OrionToolbar", isPresented: Binding( get: { vm.lastError != nil }, set: { if !$0 { vm.lastError = nil } } ) ) { Button("OK") { vm.lastError = nil } } message: { Text(vm.lastError ?? "") } } private var header: some View { HStack { Image(systemName: "server.rack") .foregroundStyle(.blue) Text("OrionToolbar") .font(.headline) Spacer() if vm.discovering { ProgressView() .controlSize(.small) } Button { Task { await vm.discoverVMs() } } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.borderless) .disabled(vm.discovering) .help("Refresh Parallels virtual machines") } .padding(16) } private var discoverySection: some View { VStack(alignment: .leading, spacing: 8) { HStack { Text("PARALLELS VIRTUAL MACHINES") .font(.caption) .fontWeight(.semibold) .foregroundStyle(.secondary) Spacer() Text("\(vm.discoveredVMs.count)") .font(.caption) .foregroundStyle(.secondary) } if vm.discoveredVMs.isEmpty { ContentUnavailableView( "No Virtual Machines", systemImage: "desktopcomputer", description: Text( "Refresh to discover Parallels virtual machines." ) ) .frame(height: 120) } ForEach(groups, id: \.self) { group in operatingSystemGroup(group) } if !vm.discoveredVMs.isEmpty { Divider() .padding(.vertical, 6) HStack { Text("\(vm.selectedVMIDs.count) selected") .font(.caption) .foregroundStyle(.secondary) Spacer() Button("Select All") { vm.selectAllVMs() } Button("Clear") { vm.clearVMSelection() } .disabled(vm.selectedVMIDs.isEmpty) } Button { vm.integrateSelectedVMs() } label: { Label( "Integrate Selected", systemImage: "plus.circle.fill" ) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(vm.selectedVMIDs.isEmpty) } } } private func operatingSystemGroup( _ group: String ) -> some View { let machines = vm.discoveredVMs.filter { $0.operatingSystem == group } return VStack(alignment: .leading, spacing: 3) { Button { if expandedGroups.contains(group) { expandedGroups.remove(group) } else { expandedGroups.insert(group) } } label: { HStack(spacing: 10) { Image(systemName: expandedGroups.contains(group) ? "chevron.down" : "chevron.right" ) .font(.caption) .frame(width: 12) Image(systemName: machines.first?.symbol ?? "desktopcomputer" ) .frame(width: 22) Text(group) .fontWeight(.medium) Spacer() Text("\(machines.count)") .foregroundStyle(.secondary) } .contentShape(Rectangle()) .padding(.vertical, 9) } .buttonStyle(.plain) if expandedGroups.contains(group) { ForEach(machines) { machine in machineRow(machine) } } } } private func machineRow( _ machine: ParallelsVM ) -> some View { HStack(alignment: .center, spacing: 10) { Toggle( "", isOn: Binding( get: { vm.selectedVMIDs.contains(machine.id) }, set: { selected in if selected { vm.selectedVMIDs.insert(machine.id) } else { vm.selectedVMIDs.remove(machine.id) } } ) ) .labelsHidden() .toggleStyle(.checkbox) if let icon = vm.icon(for: machine) { Image(nsImage: icon) .resizable() .scaledToFit() .frame(width: 30, height: 30) } else { Image(systemName: machine.symbol) .font(.title3) .frame(width: 30, height: 30) .foregroundStyle(.secondary) } VStack(alignment: .leading, spacing: 3) { Text(machine.name) .font(.system(size: 13, weight: .medium)) HStack(spacing: 5) { Circle() .fill( machine.status.lowercased() == "running" ? Color.green : Color.gray ) .frame(width: 7, height: 7) Text(machine.status.capitalized) if vm.isIntegrated(machine) { Text("· Integrated") .foregroundStyle(.green) } } .font(.caption) .foregroundStyle(.secondary) } Spacer(minLength: 0) } .padding(.leading, 22) .padding(.vertical, 6) } private var integratedSection: some View { VStack(alignment: .leading, spacing: 8) { Text("INTEGRATED SERVERS") .font(.caption) .fontWeight(.semibold) .foregroundStyle(.secondary) ForEach(vm.servers) { server in integratedServerRow(server) } } } private func integratedServerRow( _ server: ServerDefinition ) -> some View { let status = vm.status(for: server) let expanded = expandedServers.contains(server.id) return VStack(alignment: .leading, spacing: 5) { Button { if expanded { expandedServers.remove(server.id) } else { expandedServers.insert(server.id) } } label: { HStack(spacing: 10) { Image(systemName: expanded ? "chevron.down" : "chevron.right" ) .font(.caption) Circle() .fill(status == .running ? Color.green : Color.gray) .frame(width: 9, height: 9) VStack(alignment: .leading, spacing: 2) { Text(server.name) .fontWeight(.medium) Text(status.title) .font(.caption) .foregroundStyle(.secondary) } Spacer() } .contentShape(Rectangle()) .padding(.vertical, 8) } .buttonStyle(.plain) if expanded { VStack(alignment: .leading, spacing: 5) { if status == .stopped { action("Start", "play.fill") { vm.start(server) } } else { action("Stop", "stop.fill") { vm.stop(server) } .disabled(vm.isBusy(server)) action("Restart", "arrow.clockwise") { vm.restart(server) } .disabled( vm.isBusy(server) || status != .running ) action( "Force Stop", "exclamationmark.triangle" ) { vm.forceStop(server) } .disabled(vm.isBusy(server)) } action("Open SSH in Terminal", "terminal") { vm.openSSH(server) } .disabled( status != .running || server.host.isEmpty ) action("Open Mailpit", "envelope") { vm.openMailpit(server) } .disabled( status != .running || server.mailpitPort == nil || server.host.isEmpty ) } .padding(.leading, 26) .padding(.bottom, 8) } } } private func action( _ title: String, _ symbol: String, perform: @escaping () -> Void ) -> some View { Button(action: perform) { Label(title, systemImage: symbol) .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 5) } .buttonStyle(.plain) } private var footer: some View { VStack(spacing: 2) { action("Open Parallels Desktop", "macwindow") { vm.openParallels() } action("Settings…", "gearshape") { openSettings() NSApplication.shared.activate( ignoringOtherApps: true ) } Divider() action("Quit OrionToolbar", "xmark") { NSApplication.shared.terminate(nil) } } .padding(.horizontal, 16) .padding(.vertical, 10) } }