Files
UTM/Platform/macOS/UTMMenuBarExtraScene.swift
osy 3b0da37c46 apple: run without saving changes on macOS 27
"Run without saving changes" has only been available for QEMU, where it
maps to `-snapshot`. Apple Virtualization has no such option, but
DiskImageKit on macOS 27 can stack a copy-on-write layer on top of an
image and Virtualization accepts the stacked image as a storage
attachment.

When started with the disposable option, the Apple backend now creates a
temporary directory in the sandbox and gives every writable drive an
ephemeral ASIF overlay there. The base image is opened read-only and is
never touched. The EFI variable store and the macOS auxiliary storage are
cloned into the same directory so firmware settings are not persisted
either. The directory is deleted when the guest stops or when start
fails, any leftover from a previous start is discarded before the next
start, and directories orphaned by a crash are swept at app launch.
Removable drives attached at start or hot-plugged during the run are
covered as well.

A disposable run boots fresh and leaves any saved suspend state and the
modification date alone, and reports the save/restore path as
unsupported so closing the window offers to kill the VM instead of
suspending it, matching the QEMU behaviour. The stop paths that delete a
saved suspend state skip that while running disposably.

The context menu, the Start intent and the menu bar extra now check the
backend capability instead of assuming QEMU, and the Apple display window
shows the same "Disposable Mode" subtitle as QEMU.

Assisted-by: Claude:claude-fable-5-1
2026-09-19 10:51:06 -05:00

81 lines
2.6 KiB
Swift

//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
@available(macOS 13, *)
struct UTMMenuBarExtraScene: Scene {
@ObservedObject var data: UTMData
@AppStorage("ShowMenuIcon") private var isMenuIconShown: Bool = false
@AppStorage("HideDockIcon") private var isDockIconHidden: Bool = false
@Environment(\.openWindow) private var openWindow
var body: some Scene {
MenuBarExtra(isInserted: $isMenuIconShown) {
Button("Show UTM") {
openWindow(id: "home")
}.keyboardShortcut("0")
.help("Show the main window.")
Toggle("Hide dock icon on next launch", isOn: $isDockIconHidden)
.help("Requires restarting UTM to take affect.")
Divider()
if data.virtualMachines.isEmpty {
Text("No virtual machines found.")
} else {
ForEach(data.virtualMachines) { vm in
VMMenuItem(vm: vm).environmentObject(data)
}
}
Divider()
Button("Quit") {
NSApp.terminate(self)
}.keyboardShortcut("Q")
.help("Terminate UTM and stop all running VMs.")
} label: {
Image("MenuBarExtra")
}
}
}
private struct VMMenuItem: View {
@ObservedObject var vm: VMData
@EnvironmentObject private var data: UTMData
var body: some View {
Menu(vm.detailsTitleLabel) {
if vm.isStopped {
Button("Start") {
data.run(vm: vm)
}
} else if !vm.isBusy {
Button("Stop") {
data.stop(vm: vm)
}
Button("Suspend") {
let isSnapshot = vm.wrapped?.isRunningAsDisposible ?? false
vm.wrapped!.requestVmPause(save: !isSnapshot)
}
Button("Reset") {
vm.wrapped!.requestVmReset()
}
} else {
Text("Busy…")
}
}
}
}