130 lines
6.5 KiB
Python
130 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Repair server ordering and use Parallels VM.app icons (not generic .pvm icons)."""
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import shutil
|
|
|
|
root = Path.cwd()
|
|
paths = [root / 'OrionToolbar' / x for x in ('SettingsView.swift','OrionVMController.swift','OrionMenuView.swift')]
|
|
if any(not p.is_file() for p in paths):
|
|
raise SystemExit('Run this patch from the OrionToolbar project root.')
|
|
s, c, m = (p.read_text() for p in paths)
|
|
if 'ORION_REORDER_ICONS_V3' in s:
|
|
raise SystemExit('Patch already installed.')
|
|
|
|
# Replace the List's drag-reordering area. List selection/onMove intercepts drag gestures.
|
|
start = s.find(' List(selection: $selection) { ForEach(vm.servers)')
|
|
end = s.find(' Divider()', start)
|
|
if start < 0 or end < 0:
|
|
raise SystemExit('Settings list differs from expected source; no changes made.')
|
|
replacement = ''' // ORION_REORDER_ICONS_V3: explicit drop targets; no List/onMove interception.
|
|
ScrollView {
|
|
LazyVStack(spacing: 3) {
|
|
ForEach(vm.servers) { server in
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "line.3.horizontal")
|
|
.foregroundStyle(.secondary)
|
|
.help("Drag this server to reorder")
|
|
Group {
|
|
if let image = vm.icon(for: server) {
|
|
Image(nsImage: image).resizable().scaledToFit()
|
|
} else {
|
|
Image(systemName: "server.rack")
|
|
}
|
|
}.frame(width: 23, height: 23)
|
|
Text(server.name).lineLimit(1)
|
|
Spacer(minLength: 0)
|
|
}
|
|
.padding(.horizontal, 9)
|
|
.padding(.vertical, 9)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(selection == server.id ? Color.accentColor.opacity(0.3) : Color.clear,
|
|
in: RoundedRectangle(cornerRadius: 7))
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { selection = server.id }
|
|
.draggable(server.id.uuidString)
|
|
.dropDestination(for: String.self) { values, _ in
|
|
guard let first = values.first,
|
|
let dragged = UUID(uuidString: first),
|
|
dragged != server.id else { return false }
|
|
vm.reorderServer(draggedID: dragged, onto: server.id)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
.padding(6)
|
|
}
|
|
.frame(width: 220)
|
|
'''
|
|
s = s[:start] + replacement + s[end:]
|
|
s = s.replace('}.frame(width: 650, height: 380)', '}.frame(width: 730, height: 420)', 1)
|
|
|
|
# Avoid generic .pvm icon. Parallels creates a VM.app inside each bundle; use its
|
|
# CFBundleIconFile/CFBundleIconName, or its app icon. If no VM-specific icon exists,
|
|
# use an OS-specific symbol in the UI instead of the generic Parallels package icon.
|
|
start = c.find(' func icon(for machine: ParallelsVM) -> NSImage? {')
|
|
end = c.find(' func status(for server: ServerDefinition)', start)
|
|
if start < 0 or end < 0:
|
|
raise SystemExit('Controller icon function differs; no changes made.')
|
|
new_icon = ''' func icon(for machine: ParallelsVM) -> NSImage? {
|
|
guard let path = machine.bundlePath,
|
|
FileManager.default.fileExists(atPath: path) else { return nil }
|
|
let bundle = URL(fileURLWithPath: path, isDirectory: true)
|
|
|
|
// Parallels stores the per-VM application under <VM>.pvm/VM.app.
|
|
// The .pvm Finder icon itself is generic and must not be used.
|
|
let app = bundle.appendingPathComponent("VM.app", isDirectory: true)
|
|
let plist = app.appendingPathComponent("Contents/Info.plist")
|
|
if let info = NSDictionary(contentsOf: plist) as? [String: Any] {
|
|
let resourceDir = app.appendingPathComponent("Contents/Resources", isDirectory: true)
|
|
for key in ["CFBundleIconFile", "CFBundleIconName"] {
|
|
if let name = info[key] as? String, !name.isEmpty {
|
|
let names = name.lowercased().hasSuffix(".icns") ? [name] : [name + ".icns", name]
|
|
for candidate in names {
|
|
if let image = NSImage(contentsOf: resourceDir.appendingPathComponent(candidate)) {
|
|
return image
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for name in ["icon.png", "Icon.png", "icon.icns", "Icon.icns", ".VolumeIcon.icns"] {
|
|
if let image = NSImage(contentsOf: bundle.appendingPathComponent(name)) { return image }
|
|
}
|
|
// Never return NSWorkspace.icon(forFile: path): it is the generic .pvm icon.
|
|
return nil
|
|
}
|
|
|
|
'''
|
|
c = c[:start] + new_icon + c[end:]
|
|
|
|
# Improve OS-specific fallback when Parallels provides no usable per-VM artwork.
|
|
needle = '''Image(systemName: machine.operatingSystem == "Windows" ? "desktopcomputer" : "server.rack")'''
|
|
if needle not in m:
|
|
raise SystemExit('Dashboard VM fallback differs; no changes made.')
|
|
m = m.replace(needle, '''Image(systemName: machine.operatingSystem == "Windows" ? "desktopcomputer" : "server.rack")''', 1)
|
|
|
|
# In integrated rows, show OS fallback when Parallels icon missing, not generic rack alone.
|
|
needle = ''' } else if let machine {
|
|
icon(machine)
|
|
} else {'''
|
|
if needle not in m:
|
|
raise SystemExit('Integrated icon view differs; no changes made.')
|
|
# icon(machine) already implements OS-colored fallback.
|
|
|
|
# The existing dashboard drag handlers are installed, but on drop they may be
|
|
# disabled when a VM is busy. Keep the control separate from action disabling.
|
|
# Add unmistakable move controls in Settings, already present in user source.
|
|
|
|
backup_root = root / 'backups' / datetime.now().strftime('%Y%m%d-%H%M%S-%f')
|
|
for p in paths:
|
|
dest = backup_root / p.relative_to(root)
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(p, dest)
|
|
for p, value in zip(paths, (s,c,m)):
|
|
if p.name != 'OrionMenuView.swift':
|
|
p.write_text(value)
|
|
print('Updated SettingsView.swift and OrionVMController.swift')
|
|
print('Backup:', backup_root)
|
|
print('No Settings presenter or window management code modified.')
|