Files
OrionToolbar/patches/oriontoolbar_reorder_icons_final.py
T
2026-10-23 10:16:05 +01:00

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.')