#!/usr/bin/env python3 from pathlib import Path from datetime import datetime import shutil root=Path.cwd(); controller=root/'OrionToolbar/OrionVMController.swift'; settings=root/'OrionToolbar/SettingsView.swift' if not controller.exists() or not settings.exists(): raise SystemExit('Run from OrionToolbar project root') c=controller.read_text(); s=settings.read_text() checks=[('controller field',' var mailpitPort: Int?\n',c),('controller icon',' func icon(for server: ServerDefinition) -> NSImage? {',c),('settings list',' List(selection: $selection) { ForEach(vm.servers) { s in HStack {',s),('settings editor',' TextField("Display name", text: $server.name)',s)] for name,needle,source in checks: if needle not in source: raise SystemExit(f'Unexpected {name}; no files changed') if 'customIconPath' in c: raise SystemExit('Already patched') c=c.replace(' var mailpitPort: Int?\n',' var mailpitPort: Int?\n var customIconPath: String? = nil\n',1) c=c.replace(' func icon(for server: ServerDefinition) -> NSImage? {',''' func moveServer(from source: IndexSet, to destination: Int) { servers.move(fromOffsets: source, toOffset: destination) } func moveServer(_ id: UUID, by delta: Int) { guard let index = servers.firstIndex(where: { $0.id == id }) else { return } let target = index + delta guard servers.indices.contains(target) else { return } servers.swapAt(index, target) } func icon(for server: ServerDefinition) -> NSImage? { if let path = server.customIconPath, let image = NSImage(contentsOfFile: path) { return image } ''',1) # Add UI list move handler. Explicit list style keeps reorder controls available via buttons. s=s.replace(' .tag(s.id) } }',' .tag(s.id) }.onMove(perform: vm.moveServer) }',1) s=s.replace(' HStack { Button("Edit…") { editing = server }; Button("Force Stop", role: .destructive) { vm.forceStop(server) }.disabled(vm.status(for: server) == .stopped) }',''' HStack { Button("Edit…") { editing = server } Button("Move Up", systemImage: "arrow.up") { vm.moveServer(server.id, by: -1) } .disabled(vm.servers.first?.id == server.id) Button("Move Down", systemImage: "arrow.down") { vm.moveServer(server.id, by: 1) } .disabled(vm.servers.last?.id == server.id) Button("Force Stop", role: .destructive) { vm.forceStop(server) } .disabled(vm.status(for: server) == .stopped) }''',1) s=s.replace(' @State var server: ServerDefinition\n',' @State var server: ServerDefinition\n @State private var iconError: String?\n',1) s=s.replace(' TextField("Display name", text: $server.name)',''' TextField("Display name", text: $server.name) HStack(spacing: 12) { Group { if let path = server.customIconPath, let image = NSImage(contentsOfFile: path) { Image(nsImage: image).resizable().scaledToFit() } else { Image(systemName: "server.rack").resizable().scaledToFit() } } .frame(width: 38, height: 38) VStack(alignment: .leading) { Text(server.customIconPath == nil ? "Parallels VM icon (automatic)" : "Custom icon") .font(.caption) HStack { Button("Choose Icon…") { chooseIcon() } Button("Use Parallels Icon") { server.customIconPath = nil } } } } if let iconError { Text(iconError).font(.caption).foregroundStyle(.red) }''',1) # editor needs vm environment for showing actual Parallels icon s=s.replace('struct ServerEditor: View {\n','struct ServerEditor: View {\n @EnvironmentObject private var vm: OrionVMController\n',1) s=s.replace(''' } else { Image(systemName: "server.rack").resizable().scaledToFit() } ''',''' } else if let image = vm.icon(for: server) { Image(nsImage: image).resizable().scaledToFit() } else { Image(systemName: "server.rack").resizable().scaledToFit() } ''',1) s=s.replace(''' }.padding(20).frame(width: 470) } }''',''' }.padding(20).frame(width: 470) } private func chooseIcon() { let panel = NSOpenPanel() panel.allowedContentTypes = [.png, .jpeg, .tiff, .icns, .heic] panel.allowsMultipleSelection = false panel.canChooseDirectories = false guard panel.runModal() == .OK, let source = panel.url else { return } do { let folder = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/OrionToolbar/Icons", isDirectory: true) try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) let destination = folder.appendingPathComponent("\\(server.id.uuidString).\\(source.pathExtension)") if FileManager.default.fileExists(atPath: destination.path) { try FileManager.default.removeItem(at: destination) } try FileManager.default.copyItem(at: source, to: destination) server.customIconPath = destination.path iconError = nil } catch { iconError = error.localizedDescription } } }''',1) # SwiftUI sheet does not automatically pass environmentObject when manually constructed? inherited environment usually yes, explicit safe s=s.replace('''{ server in ServerEditor(server: server, isNew:''','''{ server in ServerEditor(server: server, isNew:''') # ensure icon imports s=s.replace('import SwiftUI\n','import SwiftUI\nimport UniformTypeIdentifiers\n',1) # old data decoder: optional property with default synthesized Codable decodeIfPresent? Swift synthesized decoder for optional yes backup=root/'backups'/datetime.now().strftime('%Y%m%d-%H%M%S-%f') for path in (controller,settings): dest=backup/path.relative_to(root);dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,dest) controller.write_text(c);settings.write_text(s) print('Updated:',controller,settings,'\nBackups:',backup)