Files
OrionToolbar/patches/oriontoolbar_server_icons_order_patch.py
2026-10-23 10:07:59 +01:00

109 lines
6.6 KiB
Python

#!/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)