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

160 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""OrionToolbar: fix Parallels VM Home parsing, icon discovery, and drag reordering."""
from pathlib import Path
from datetime import datetime
import re, shutil
root=Path.cwd()
paths={k:root/'OrionToolbar'/k for k in ('OrionVMController.swift','SettingsView.swift','OrionMenuView.swift')}
for p in paths.values():
if not p.is_file(): raise SystemExit(f'Missing {p}. Run from project root.')
c=paths['OrionVMController.swift'].read_text()
s=paths['SettingsView.swift'].read_text()
m=paths['OrionMenuView.swift'].read_text()
if 'func reorderServer(draggedID:' in c:
raise SystemExit('This patch has already been applied.')
# Home ends in a slash on this installation. Existing regex expects .pvm followed by whitespace,
# so it fails to capture /Volumes/Parallels/Ubuntu Linux Server.pvm/.
start=c.find(' let pattern = #"(?m)^')
if start<0: raise SystemExit('Cannot locate existing Home path regex. No changes made.')
end=c.find('\n }',start)
if end<0: raise SystemExit('Cannot locate end of Home parser. No changes made.')
old=c[start:end]
if 'Home:' not in old or 'bundlePath' not in old:
raise SystemExit('Unexpected Home parser; no changes made.')
new=''' // prlctl prints `Home: /Volumes/Parallels/VM.pvm/` (trailing slash).
// Match the full line, then normalize the bundle path.
if let homeLine = detail.split(separator: "\\n").first(where: {
$0.trimmingCharacters(in: .whitespaces).hasPrefix("Home:")
}) {
let raw = homeLine.trimmingCharacters(in: .whitespaces)
.replacingOccurrences(of: "Home:", with: "", range: rawHomePrefixRange(homeLine))
.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: CharacterSet(charactersIn: "\\\"'"))
let path = (raw as NSString).expandingTildeInPath
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if FileManager.default.fileExists(atPath: "/" + path) {
discoveredVMs[i].bundlePath = "/" + path
}
}'''
# Simpler and safe without helper: strip Home: prefix using dropFirst(5).
new=''' // `Home:` ends with .pvm/ on this Parallels installation.
if let homeLine = detail.split(separator: "\\n").first(where: {
$0.trimmingCharacters(in: .whitespaces).hasPrefix("Home:")
}) {
let trimmed = homeLine.trimmingCharacters(in: .whitespaces)
let raw = String(trimmed.dropFirst("Home:".count))
.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: CharacterSet(charactersIn: "\\\"'"))
let expanded = (raw as NSString).expandingTildeInPath
let normalized = (expanded as NSString).standardizingPath
if FileManager.default.fileExists(atPath: normalized) {
discoveredVMs[i].bundlePath = normalized
}
}'''
c=c[:start]+new+c[end:]
# Real VM icon files are sometimes inside the .pvm package. Fall back to macOS package icon.
a=c.find(' func icon(for machine: ParallelsVM) -> NSImage? {')
b=c.find(' func status(for server: ServerDefinition)',a)
if a<0 or b<0: raise SystemExit('Cannot locate VM icon function. No changes made.')
c=c[:a]+''' 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)
// Prefer a VM-specific icon if the Parallels bundle contains one.
for name in ["icon.png", "Icon.png", "icon.icns", "Icon.icns",
".VolumeIcon.icns", "icon.tiff", "Icon.tiff"] {
if let image = NSImage(contentsOf: bundle.appendingPathComponent(name)) {
return image
}
}
// This is the same package icon Finder shows for the Parallels VM.
return NSWorkspace.shared.icon(forFile: path)
}
'''+c[b:]
anchor=' func icon(for server: ServerDefinition) -> NSImage? {'
if anchor not in c: raise SystemExit('Cannot locate server icon function. No changes made.')
c=c.replace(anchor,''' func reorderServer(draggedID: UUID, onto targetID: UUID) {
guard draggedID != targetID,
let from = servers.firstIndex(where: { $0.id == draggedID }),
let to = servers.firstIndex(where: { $0.id == targetID }) else { return }
let moved = servers.remove(at: from)
servers.insert(moved, at: to) // @Published didSet persists the order.
}
'''+anchor,1)
# Make Settings rows draggable. Avoid editing window presentation.
pattern=r'(\s*Text\(s\.name\)\n\s*\}\n\s*\.contentShape\(Rectangle\(\)\)\n\s*\.onTapGesture \{ selection = s\.id \}\n)(\s*\.tag\(s\.id\))'
match=re.search(pattern,s)
if not match: raise SystemExit('Cannot locate Settings server list row. No changes made.')
replacement=match.group(1)+''' .draggable(s.id.uuidString)
.dropDestination(for: String.self) { items, _ in
guard let value = items.first,
let id = UUID(uuidString: value) else { return false }
vm.reorderServer(draggedID: id, onto: s.id)
return true
}
'''+match.group(2)
s=s[:match.start()]+replacement+s[match.end():]
# Add visible drag affordance next to the name.
s=s.replace(''' Text(s.name)
}''',''' Text(s.name)
Spacer(minLength: 0)
Image(systemName: "line.3.horizontal")
.foregroundStyle(.secondary)
.help("Drag to reorder servers")
}''',1)
# Show custom server icons in dashboard, and enable drag/drop integrated rows.
old=''' if let machine { icon(machine) }
else { Image(systemName: "server.rack").frame(width: 36, height: 36).foregroundStyle(.cyan) }'''
if old not in m: raise SystemExit('Cannot locate integrated dashboard icon. No changes made.')
m=m.replace(old,''' Group {
if let image = vm.icon(for: server) {
Image(nsImage: image).resizable().scaledToFit()
} else if let machine {
icon(machine)
} else {
Image(systemName: "server.rack").foregroundStyle(.cyan)
}
}.frame(width: 36, height: 36)''',1)
name=''' Text(server.name).font(.system(size: 12, weight: .semibold)).lineLimit(1).frame(width: 185, alignment: .leading)'''
if name not in m: raise SystemExit('Cannot locate integrated server name. No changes made.')
m=m.replace(name,''' HStack(spacing: 5) {
Image(systemName: "line.3.horizontal").foregroundStyle(OrionStyle.muted)
Text(server.name).lineLimit(1)
}
.font(.system(size: 12, weight: .semibold))
.frame(width: 185, alignment: .leading)
.draggable(server.id.uuidString)''',1)
needle=''' .background(OrionStyle.card.opacity(0.75), in: RoundedRectangle(cornerRadius: 10))
}
private func action(_ title: String'''
if needle not in m: raise SystemExit('Cannot locate end of integrated row. No changes made.')
m=m.replace(needle,''' .background(OrionStyle.card.opacity(0.75), in: RoundedRectangle(cornerRadius: 10))
.dropDestination(for: String.self) { items, _ in
guard let value = items.first,
let id = UUID(uuidString: value) else { return false }
vm.reorderServer(draggedID: id, onto: server.id)
return true
}
}
private func action(_ title: String''',1)
backup=root/'backups'/datetime.now().strftime('%Y%m%d-%H%M%S-%f')
for key,path in paths.items():
dest=backup/path.relative_to(root)
dest.parent.mkdir(parents=True,exist_ok=True)
shutil.copy2(path,dest)
for key,contents in [('OrionVMController.swift',c),('SettingsView.swift',s),('OrionMenuView.swift',m)]:
paths[key].write_text(contents)
print('Updated controller, Settings and integrated dashboard. Backups:',backup)
print('Run: /bin/zsh scripts/build.sh --launch')