109 lines
6.6 KiB
Python
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)
|