updates to launcer
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hide/re-add Parallels VMs in OrionToolbar only; never unregister/delete VMs."""
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
root=Path.cwd()
|
||||
cpath=root/'OrionToolbar/OrionVMController.swift'
|
||||
mpath=root/'OrionToolbar/OrionMenuView.swift'
|
||||
if not cpath.is_file() or not mpath.is_file(): raise SystemExit('Run from OrionToolbar project root')
|
||||
c=cpath.read_text(); m=mpath.read_text()
|
||||
def once(s,old,new,desc):
|
||||
n=s.count(old)
|
||||
if n!=1: raise SystemExit(f'Expected exactly one {desc}, found {n}. No changes made.')
|
||||
return s.replace(old,new,1)
|
||||
if 'orionHiddenVMIDs.v1' in c: raise SystemExit('Already installed; no changes made')
|
||||
c=once(c,' @Published var discoveredVMs: [ParallelsVM] = []',''' @Published var discoveredVMs: [ParallelsVM] = []
|
||||
@Published private(set) var hiddenVMIDs: Set<String> = Set(
|
||||
UserDefaults.standard.stringArray(forKey: "orionHiddenVMIDs.v1") ?? []
|
||||
)
|
||||
|
||||
func hideVM(_ machine: ParallelsVM) {
|
||||
hiddenVMIDs.insert(machine.id)
|
||||
UserDefaults.standard.set(hiddenVMIDs.sorted(), forKey: "orionHiddenVMIDs.v1")
|
||||
}
|
||||
|
||||
func restoreVM(_ machine: ParallelsVM) {
|
||||
hiddenVMIDs.remove(machine.id)
|
||||
UserDefaults.standard.set(hiddenVMIDs.sorted(), forKey: "orionHiddenVMIDs.v1")
|
||||
}
|
||||
|
||||
var visibleVMs: [ParallelsVM] {
|
||||
discoveredVMs.filter { !hiddenVMIDs.contains($0.id) }
|
||||
}
|
||||
|
||||
var hiddenVMs: [ParallelsVM] {
|
||||
discoveredVMs.filter { hiddenVMIDs.contains($0.id) }
|
||||
}
|
||||
''','published VM declaration')
|
||||
m=once(m,'vm.discoveredVMs.filter { $0.status.lowercased() == "running" }','vm.visibleVMs.filter { $0.status.lowercased() == "running" }','running list')
|
||||
m=once(m,'count: "\\(vm.discoveredVMs.count) VMs"','count: "\\(vm.visibleVMs.count) VMs"','VM count')
|
||||
m=once(m,'if vm.discoveredVMs.isEmpty {','if vm.visibleVMs.isEmpty {','VM empty state')
|
||||
m=once(m,'ForEach(vm.discoveredVMs) { machine in','ForEach(vm.visibleVMs) { machine in','All VM rows')
|
||||
# Hide an integrated server's row if its Parallels VM has been hidden.
|
||||
m=once(m,'ForEach(vm.servers) { server in\n integratedRow(server)','''ForEach(vm.servers.filter { server in
|
||||
!vm.hiddenVMIDs.contains(server.vmUUID ??
|
||||
vm.discoveredVMs.first(where: { $0.name == server.vmName })?.id ?? "")
|
||||
}) { server in
|
||||
integratedRow(server)''','Integrated rows')
|
||||
# Add a restore dropdown to the dashboard header; visible even if every VM is hidden.
|
||||
anchor=''' if vm.discovering { ProgressView().controlSize(.small) }'''
|
||||
m=once(m,anchor,''' Menu {
|
||||
if vm.hiddenVMs.isEmpty {
|
||||
Text("No removed virtual machines")
|
||||
} else {
|
||||
ForEach(vm.hiddenVMs) { machine in
|
||||
Button("Re-add " + machine.name) { vm.restoreVM(machine) }
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Re-add VM", systemImage: "plus.rectangle.on.folder")
|
||||
}
|
||||
.help("Restore virtual machines removed from OrionToolbar")
|
||||
'''+anchor,'header restore menu')
|
||||
# Add context menu to VM row; no changes to existing drag/drop modifiers.
|
||||
start=m.index(' private func machineRow('); end=m.index(' private func integratedRow(',start)
|
||||
block=m[start:end]
|
||||
needle=''' .background(OrionStyle.card.opacity(0.75), in: RoundedRectangle(cornerRadius: 10))'''
|
||||
block=once(block,needle,needle+'''
|
||||
.contextMenu {
|
||||
Button("Remove from OrionToolbar", systemImage: "minus.circle") {
|
||||
vm.hideVM(machine)
|
||||
}
|
||||
}''','All VM context menu')
|
||||
# Also add a clearly visible Remove button, retaining existing start/stop/integrate actions.
|
||||
needle=''' if vm.isIntegrated(machine) {'''
|
||||
block=once(block,needle,''' Button {
|
||||
vm.hideVM(machine)
|
||||
} label: {
|
||||
Image(systemName: "minus.circle")
|
||||
.foregroundStyle(OrionStyle.muted)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Remove from OrionToolbar (keeps the Parallels VM)")
|
||||
'''+needle,'VM remove button')
|
||||
m=m[:start]+block+m[end:]
|
||||
backup=root/'backups'/datetime.now().strftime('%Y%m%d-%H%M%S-%f')
|
||||
for path in (cpath,mpath):
|
||||
dest=backup/path.relative_to(root); dest.parent.mkdir(parents=True,exist_ok=True); shutil.copy2(path,dest)
|
||||
cpath.write_text(c); mpath.write_text(m)
|
||||
print('Updated:',cpath.relative_to(root),mpath.relative_to(root))
|
||||
print('Backups:',backup)
|
||||
print('Parallels VMs are never deleted or unregistered. Icons and drag ordering untouched.')
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drag reorder both dashboard lists; preserve icons and Settings."""
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import shutil, re
|
||||
root=Path.cwd()
|
||||
controller=root/'OrionToolbar/OrionVMController.swift'
|
||||
menu=root/'OrionToolbar/OrionMenuView.swift'
|
||||
for p in (controller,menu):
|
||||
if not p.is_file(): raise SystemExit(f'Missing {p}; run from OrionToolbar root')
|
||||
c=controller.read_text(); m=menu.read_text()
|
||||
if 'orionVMOrder.v1' in c: raise SystemExit('This patch has already been applied.')
|
||||
|
||||
def replace_once(src, old, new, label):
|
||||
if src.count(old)!=1: raise SystemExit(f'Cannot uniquely locate {label} (found {src.count(old)}). No changes made.')
|
||||
return src.replace(old,new,1)
|
||||
|
||||
# Maintain a stable, persisted order separate from server definitions.
|
||||
anchor=' private let defaultsKey = "serverDefinitions.v2"'
|
||||
c=replace_once(c,anchor,anchor+'''
|
||||
private let vmOrderKey = "orionVMOrder.v1"
|
||||
private var vmOrder: [String] {
|
||||
get { UserDefaults.standard.stringArray(forKey: vmOrderKey) ?? [] }
|
||||
set { UserDefaults.standard.set(newValue, forKey: vmOrderKey) }
|
||||
}
|
||||
|
||||
func reorderVM(draggedID: String, onto targetID: String) {
|
||||
guard draggedID != targetID,
|
||||
discoveredVMs.contains(where: { $0.id == draggedID }),
|
||||
discoveredVMs.contains(where: { $0.id == targetID }) else { return }
|
||||
var order = discoveredVMs.map(\\.id)
|
||||
guard let from = order.firstIndex(of: draggedID),
|
||||
let to = order.firstIndex(of: targetID) else { return }
|
||||
order.remove(at: from)
|
||||
order.insert(draggedID, at: to)
|
||||
vmOrder = order
|
||||
let ranks = Dictionary(uniqueKeysWithValues: order.enumerated().map { ($0.element, $0.offset) })
|
||||
discoveredVMs.sort { (ranks[$0.id] ?? Int.max) < (ranks[$1.id] ?? Int.max) }
|
||||
}
|
||||
''','controller ordering properties')
|
||||
# Discovery sorts alphabetically initially; then apply persisted order, preserving newly discovered VMs.
|
||||
anchor=''' // Resolve Parallels VM bundles without requiring the guest to run.'''
|
||||
c=replace_once(c,anchor,''' // Keep the user's VM order through every 15-second discovery refresh.
|
||||
// New VMs are appended in discovery order.
|
||||
let savedOrder = vmOrder
|
||||
let ranks = Dictionary(uniqueKeysWithValues: savedOrder.enumerated().map { ($0.element, $0.offset) })
|
||||
discoveredVMs.sort { left, right in
|
||||
let a = ranks[left.id] ?? Int.max
|
||||
let b = ranks[right.id] ?? Int.max
|
||||
if a != b { return a < b }
|
||||
return left.name.localizedStandardCompare(right.name) == .orderedAscending
|
||||
}
|
||||
|
||||
'''+anchor,'discovery sort persistence')
|
||||
# Use NSItemProvider for consistent macOS drag and drop. Drag handle only: buttons remain clickable.
|
||||
# The drag provider is explicitly NSString, and callback converts from NSItemProviderReading.
|
||||
vm_name=''' Text(machine.name).font(.system(size: 12, weight: .semibold)).lineLimit(1).frame(width: 230, alignment: .leading)'''
|
||||
vm_name_new=''' HStack(spacing: 6) {
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.foregroundStyle(OrionStyle.muted)
|
||||
Text(machine.name).lineLimit(1)
|
||||
}
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.frame(width: 230, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onDrag { NSItemProvider(object: ("vm:" + machine.id) as NSString) }'''
|
||||
m=replace_once(m,vm_name,vm_name_new,'All Virtual Machines drag handle')
|
||||
# Target only machineRow by isolating function boundaries.
|
||||
start=m.index(' private func machineRow(')
|
||||
end=m.index(' private func integratedRow(',start)
|
||||
block=m[start:end]
|
||||
background=''' .background(OrionStyle.card.opacity(0.75), in: RoundedRectangle(cornerRadius: 10))'''
|
||||
vm_drop=''' .onDrop(of: [.plainText], isTargeted: nil) { providers in
|
||||
guard let provider = providers.first else { return false }
|
||||
_ = provider.loadObject(ofClass: NSString.self) { item, _ in
|
||||
guard let string = item as? NSString else { return }
|
||||
let value = string as String
|
||||
guard value.hasPrefix("vm:") else { return }
|
||||
Task { @MainActor in
|
||||
vm.reorderVM(draggedID: String(value.dropFirst(3)), onto: machine.id)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}'''
|
||||
block=replace_once(block,background,background+'\n'+vm_drop,'All Virtual Machines drop target')
|
||||
m=m[:start]+block+m[end:]
|
||||
# Integrated server grip (may already be .onDrag or .draggable).
|
||||
start=m.index(' private func integratedRow(')
|
||||
end=m.index(' private func action(',start)
|
||||
block=m[start:end]
|
||||
block=block.replace('.draggable(server.id.uuidString)', '.onDrag { NSItemProvider(object: ("server:" + server.id.uuidString) as NSString) }')
|
||||
# Prior patch may have installed .onDrag without a namespace; update its payload.
|
||||
block=block.replace('NSItemProvider(object: server.id.uuidString as NSString)', 'NSItemProvider(object: ("server:" + server.id.uuidString) as NSString)')
|
||||
# Replace only integratedRow drop handler, whether original .dropDestination or last .onDrop.
|
||||
new_drop=''' .onDrop(of: [.plainText], isTargeted: nil) { providers in
|
||||
guard let provider = providers.first else { return false }
|
||||
_ = provider.loadObject(ofClass: NSString.self) { item, _ in
|
||||
guard let string = item as? NSString else { return }
|
||||
let value = string as String
|
||||
guard value.hasPrefix("server:"),
|
||||
let dragged = UUID(uuidString: String(value.dropFirst(7))) else { return }
|
||||
Task { @MainActor in
|
||||
vm.reorderServer(draggedID: dragged, onto: server.id)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}'''
|
||||
block,n=re.subn(r' \.onDrop\(of: \[\.plainText\], isTargeted: nil\) \{ providers in\n.*?\n \}',lambda _:new_drop,block,count=1,flags=re.S)
|
||||
if n==0:
|
||||
block,n=re.subn(r' \.dropDestination\(for: String\.self\) \{ items, _ in\n.*?\n \}',lambda _:new_drop,block,count=1,flags=re.S)
|
||||
if n!=1: raise SystemExit('Cannot locate integrated server drop handler; no changes made')
|
||||
if '.onDrag' not in block: raise SystemExit('Cannot locate integrated server drag handle; no changes made')
|
||||
m=m[:start]+block+m[end:]
|
||||
# Fix previous build error if it remains elsewhere, without modifying other behavior.
|
||||
m=m.replace('guard let text = value as String?, let dragged = UUID(uuidString: text) else { return }', 'guard let text = value as? NSString, let dragged = UUID(uuidString: text as String) else { return }')
|
||||
# Avoid duplicate right-hand chevron if prior patch was not applied.
|
||||
chevron=''' Menu {
|
||||
Button("Open in Parallels") { vm.openParallels() }
|
||||
if machine.status.lowercased() == "running" {
|
||||
Button("Restart") { vm.operate(machine, action: "restart") }
|
||||
Button("Force Stop", role: .destructive) { pendingForceStop = machine }
|
||||
}
|
||||
} label: { Image(systemName: "chevron.right").foregroundStyle(OrionStyle.muted) }
|
||||
.menuStyle(.borderlessButton).frame(width: 22)'''
|
||||
m=m.replace(chevron,'')
|
||||
backup=root/'backups'/datetime.now().strftime('%Y%m%d-%H%M%S-%f')
|
||||
for path,source in ((controller,c),(menu,m)):
|
||||
target=backup/path.relative_to(root)
|
||||
target.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(path,target)
|
||||
for path,source in ((controller,c),(menu,m)): path.write_text(source)
|
||||
print('Updated OrionVMController.swift and OrionMenuView.swift')
|
||||
print('Backups:',backup)
|
||||
print('Icons and SettingsView.swift unchanged.')
|
||||
Reference in New Issue
Block a user