settings(qemu): separate custom arguments from the generated command line

The QEMU settings page listed UTM's generated arguments next to the
editable custom ones in a single list, which made users think the
generated arguments could be changed and invited pasting options found
online that do not apply to UTM's build of QEMU.

Move custom arguments to their own page, edited through a popover on
macOS and a pushed page on iOS like port forwarding, instead of inline
text fields that needed SwiftUI workarounds. On macOS the page is hidden
behind a checkbox on the QEMU page that is only on while the settings
sheet is open unless the VM already has custom arguments; a short
disclaimer sits under the checkbox and under the iOS row. The complete
command line is shown as read-only selectable text, so the export
command feature is removed.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-22 23:21:22 -05:00
parent 100701eb8b
commit 3de02b50ee
9 changed files with 371 additions and 280 deletions
@@ -0,0 +1,65 @@
//
// Copyright © 2026 Turing Software, LLC. 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 SwiftUI
/// Read-only view of the complete command line UTM runs.
struct VMConfigQEMUCommandLineView: View {
@ObservedObject var config: UTMQemuConfiguration
/// One option per line, in the form of a shell command with line continuations.
private var commandLine: String {
var lines = ["qemu-system-\(config.system.architecture.rawValue)"]
for argument in config.allArguments {
let string = argument.string.contains(" ") ? "\"\(argument.string)\"" : argument.string
if string.hasPrefix("-") {
lines.append(string)
} else {
lines[lines.count - 1] += " \(string)"
}
}
return lines.joined(separator: " \\\n ")
}
var body: some View {
#if os(macOS)
VStack(alignment: .leading) {
Text("Command Line")
.font(.headline)
List {
Text(commandLine)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
}.listStyle(.bordered)
}
#else
Section(header: Text("Command Line")) {
Text(commandLine)
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
}
#endif
}
}
struct VMConfigQEMUCommandLineView_Previews: PreviewProvider {
@State static private var config = UTMQemuConfiguration()
static var previews: some View {
VMConfigQEMUCommandLineView(config: config)
.padding()
}
}
+55 -134
View File
@@ -17,30 +17,25 @@
import SwiftUI
struct VMConfigQEMUView: View {
private struct Argument: Identifiable {
let id: Int
let string: String
}
@Binding var config: UTMQemuConfigurationQEMU
@Binding var system: UTMQemuConfigurationSystem
let fetchFixedArguments: () -> [QEMUArgument]
@ObservedObject var config: UTMQemuConfiguration
#if os(macOS)
@Binding var isCustomArgumentsEnabled: Bool
#endif
@State private var showExportLog: Bool = false
@State private var showExportArgs: Bool = false
private var logExists: Bool {
guard let debugLogURL = config.debugLogURL else {
guard let debugLogURL = config.qemu.debugLogURL else {
return false
}
return FileManager.default.fileExists(atPath: debugLogURL.path)
}
private var supportsUefi: Bool {
UTMQemuConfigurationQEMU.uefiImagePrefix(forArchitecture: system.architecture) != nil
UTMQemuConfigurationQEMU.uefiImagePrefix(forArchitecture: config.system.architecture) != nil
}
private var supportsPs2: Bool {
if system.target.rawValue.starts(with: "pc") || system.target.rawValue.starts(with: "q35") {
if config.system.target.rawValue.starts(with: "pc") || config.system.target.rawValue.starts(with: "q35") {
return true
} else {
return false
@@ -51,7 +46,7 @@ struct VMConfigQEMUView: View {
VStack {
Form {
Section(header: Text("Logging")) {
Toggle(isOn: $config.hasDebugLog, label: {
Toggle(isOn: $config.qemu.hasDebugLog, label: {
Text("Debug Logging")
})
Button("Export Debug Log") {
@@ -60,167 +55,93 @@ struct VMConfigQEMUView: View {
.disabled(!logExists)
}
DetailedSection("Tweaks", description: "These are advanced settings affecting QEMU which should be kept default unless you are running into issues.") {
Toggle("UEFI Boot", isOn: $config.hasUefiBoot)
Toggle("UEFI Boot", isOn: $config.qemu.hasUefiBoot)
.disabled(!supportsUefi)
.help("Should be off for older operating systems such as Windows 7 or lower.")
Toggle("RNG Device", isOn: $config.hasRNGDevice)
Toggle("RNG Device", isOn: $config.qemu.hasRNGDevice)
.help("Should be on always unless the guest cannot boot because of this.")
Toggle("Balloon Device", isOn: $config.hasBalloonDevice)
Toggle("Balloon Device", isOn: $config.qemu.hasBalloonDevice)
.help("Should be on always unless the guest cannot boot because of this.")
Toggle("TPM 2.0 Device", isOn: $config.hasTPMDevice)
Toggle("TPM 2.0 Device", isOn: $config.qemu.hasTPMDevice)
.help("TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided.")
.onChange(of: config.hasTPMDevice) { newValue in
.onChange(of: config.qemu.hasTPMDevice) { newValue in
if newValue {
config.isUefiVariableResetRequested = true
config.hasPreloadedSecureBootKeys = true
config.qemu.isUefiVariableResetRequested = true
config.qemu.hasPreloadedSecureBootKeys = true
} else {
config.hasPreloadedSecureBootKeys = false
config.qemu.hasPreloadedSecureBootKeys = false
}
}
Toggle("Use Hypervisor", isOn: $config.hasHypervisor)
Toggle("Use Hypervisor", isOn: $config.qemu.hasHypervisor)
.help("Only available if host architecture matches the target. Otherwise, TCG emulation is used.")
.disabled(!system.architecture.hasHypervisorSupport)
if config.hasHypervisor {
Toggle("Use TSO", isOn: $config.hasTSO)
.disabled(!config.system.architecture.hasHypervisorSupport)
if config.qemu.hasHypervisor {
Toggle("Use TSO", isOn: $config.qemu.hasTSO)
.help("Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general.")
.disabled(!system.architecture.hasTSOSupport)
.disabled(!config.system.architecture.hasTSOSupport)
}
Toggle("Use local time for base clock", isOn: $config.hasRTCLocalTime)
Toggle("Use local time for base clock", isOn: $config.qemu.hasRTCLocalTime)
.help("If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock.")
Toggle("Force PS/2 controller", isOn: $config.hasPS2Controller)
Toggle("Force PS/2 controller", isOn: $config.qemu.hasPS2Controller)
.disabled(!supportsPs2)
.help("Instantiate PS/2 controller even when USB input is supported. Required for older Windows.")
}
DetailedSection("Maintenance", description: "Options here only apply on next boot and are not saved.") {
Toggle("Reset UEFI Variables", isOn: $config.isUefiVariableResetRequested)
Toggle("Reset UEFI Variables", isOn: $config.qemu.isUefiVariableResetRequested)
.help("You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot.")
.disabled(!config.hasUefiBoot)
Toggle("Preload Secure Boot Keys", isOn: $config.hasPreloadedSecureBootKeys)
.disabled(!config.qemu.hasUefiBoot)
Toggle("Preload Secure Boot Keys", isOn: $config.qemu.hasPreloadedSecureBootKeys)
.help("Enable Secure Boot with Microsoft UEFI keys. This is required to Secure Boot Windows.")
.disabled(!config.isUefiVariableResetRequested || !config.hasTPMDevice)
.onChange(of: config.isUefiVariableResetRequested) { newValue in
.disabled(!config.qemu.isUefiVariableResetRequested || !config.qemu.hasTPMDevice)
.onChange(of: config.qemu.isUefiVariableResetRequested) { newValue in
if !newValue {
config.hasPreloadedSecureBootKeys = false
config.qemu.hasPreloadedSecureBootKeys = false
}
}
}
DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
DefaultTextField("", text: $config.qemu.machinePropertyOverride.bound, prompt: "Default")
}
#if os(macOS)
DetailedSection("Custom Arguments", description: "Custom arguments are for debugging only. Compatibility with future versions is not guaranteed.") {
Toggle("Use Custom Arguments", isOn: $isCustomArgumentsEnabled)
.onChange(of: isCustomArgumentsEnabled) { newValue in
if !newValue {
config.qemu.additionalArguments.removeAll()
}
}
}
#else
Section(footer: Text("Custom arguments are for debugging only. Compatibility with future versions is not guaranteed.")) {
NavigationLink("Arguments") {
VMConfigQEMUArgumentsView(config: config)
.navigationTitle("Arguments")
}
}
// macOS uses the new VMConfigQEMUArgumentsView
#if !os(macOS)
additionalArguments
#endif
}.navigationBarItems(trailing: EditButton())
}
.disableAutocorrection(true)
}
}
@ViewBuilder
var additionalArguments: some View {
Section(header: Text("QEMU Arguments")) {
let fixedArgs = fetchFixedArguments()
Button("Export QEMU Command…") {
showExportArgs.toggle()
}.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportArgs(fixedArgs)))
List {
ForEach(fixedArgs) { arg in
Text(arg.string)
}.foregroundColor(.secondary)
CustomArguments(config: $config)
NewArgumentTextField(config: $config)
}
}
}
private func exportDebugLog() -> VMShareItemModifier.ShareItem? {
guard let srcLogPath = config.debugLogURL else {
guard let srcLogPath = config.qemu.debugLogURL else {
return nil
}
return .debugLog(srcLogPath)
}
private func exportArgs(_ args: [QEMUArgument]) -> VMShareItemModifier.ShareItem {
var argString = "qemu-system-\(system.architecture.rawValue)"
for arg in args {
if arg.string.contains(" ") {
argString += " \"\(arg.string)\""
} else {
argString += " \(arg.string)"
}
}
for arg in config.additionalArguments {
argString += " \(arg.string)"
}
return .qemuCommand(argString)
}
}
struct CustomArguments: View {
@Binding var config: UTMQemuConfigurationQEMU
var body: some View {
ForEach($config.additionalArguments) { $arg in
let i = config.additionalArguments.firstIndex(of: arg) ?? 0
HStack {
DefaultTextField("", text: $arg.string, prompt: "(Delete)", onEditingChanged: { editing in
if !editing && arg.string == "" {
DispatchQueue.main.async { // SwiftUI doesn't like removing in a ForEach binding
config.additionalArguments.remove(at: i)
}
}
})
#if os(macOS)
Spacer()
if i != 0 {
Button(action: {
config.additionalArguments.move(fromOffsets: IndexSet(integer: i), toOffset: i-1)
}, label: {
Label("Move Up", systemImage: "arrow.up").labelStyle(.iconOnly)
})
}
#endif
}
}.onDelete { offsets in
config.additionalArguments.remove(atOffsets: offsets)
}
.onMove { offsets, index in
config.additionalArguments.move(fromOffsets: offsets, toOffset: index)
}
}
}
struct NewArgumentTextField: View {
@Binding var config: UTMQemuConfigurationQEMU
@State private var newArg: String = ""
var body: some View {
Group {
DefaultTextField("", text: $newArg, prompt: "New…", onEditingChanged: addArg)
}.onDisappear {
if newArg != "" {
addArg(editing: false)
}
}
}
private func addArg(editing: Bool) {
guard !editing else {
return
}
if newArg != "" {
config.additionalArguments.append(QEMUArgument(newArg))
}
newArg = ""
}
}
struct VMConfigQEMUView_Previews: PreviewProvider {
@State static private var config = UTMQemuConfigurationQEMU()
@State static private var system = UTMQemuConfigurationSystem()
@State static private var config = UTMQemuConfiguration()
static var previews: some View {
VMConfigQEMUView(config: $config, system: $system, fetchFixedArguments: { [] })
#if os(macOS)
VMConfigQEMUView(config: config, isCustomArgumentsEnabled: .constant(false))
.frame(minHeight: 500)
#else
VMConfigQEMUView(config: config)
.frame(minHeight: 500)
#endif
}
}
@@ -42,7 +42,6 @@ struct VMShareItemModifier: ViewModifier {
case debugLog(URL)
case utmCopy(VMData)
case utmMove(VMData)
case qemuCommand(String)
@MainActor func toActivityItem() -> Any {
switch self {
@@ -50,8 +49,6 @@ struct VMShareItemModifier: ViewModifier {
return url
case .utmCopy(let vm), .utmMove(let vm):
return vm.pathUrl
case .qemuCommand(let command):
return command
}
}
}
@@ -0,0 +1,102 @@
//
// Copyright © 2026 Turing Software, LLC. 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 SwiftUI
struct VMConfigQEMUArgumentsView: View {
@ObservedObject var config: UTMQemuConfiguration
var body: some View {
Form {
Section(header: Text("Custom Arguments")) {
ForEach($config.qemu.additionalArguments) { $argument in
NavigationLink {
QEMUArgumentEdit(argument: argument,
onSave: { argument = $0 },
onDelete: { config.qemu.additionalArguments.removeAll(where: { $0.id == argument.id }) })
} label: {
Text(argument.string)
.font(.system(.body, design: .monospaced))
}
}.onDelete { offsets in
config.qemu.additionalArguments.remove(atOffsets: offsets)
}.onMove { offsets, destination in
config.qemu.additionalArguments.move(fromOffsets: offsets, toOffset: destination)
}
NavigationLink("New") {
QEMUArgumentEdit(onSave: { config.qemu.additionalArguments.append($0) })
}
}
VMConfigQEMUCommandLineView(config: config)
}.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
struct QEMUArgumentEdit: View {
@State var argument: QEMUArgument = QEMUArgument("")
let onSave: (QEMUArgument) -> Void
var onDelete: (() -> Void)? = nil
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
Form {
Section(header: Text("Argument")) {
TextField("-option value", text: $argument.string)
.font(.system(.body, design: .monospaced))
.disableAutocorrection(true)
.textInputAutocapitalization(.never)
}
}
.navigationTitle(onDelete == nil ? "New Argument" : "Edit Argument")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if let onDelete = onDelete {
Button("Delete", role: .destructive) {
closePopup(after: onDelete)
}
}
Button("Save") {
closePopup(after: { onSave(argument) })
}.disabled(argument.string.isEmpty)
}
}
}
private func closePopup(after action: () -> Void) {
action()
presentationMode.wrappedValue.dismiss()
}
}
struct VMConfigQEMUArgumentsView_Previews: PreviewProvider {
@State static private var config = UTMQemuConfiguration()
static var previews: some View {
NavigationView {
VMConfigQEMUArgumentsView(config: config)
.navigationTitle("Arguments")
}.onAppear {
if config.qemu.additionalArguments.isEmpty {
config.qemu.additionalArguments.append(QEMUArgument("-append"))
config.qemu.additionalArguments.append(QEMUArgument("\"root=/dev/vda1 console=ttyS0\""))
}
}
}
}
+1 -3
View File
@@ -51,9 +51,7 @@ struct VMSettingsView: View {
}
}
NavigationLink(
destination: VMConfigQEMUView(config: $config.qemu, system: $config.system, fetchFixedArguments: {
config.generatedArguments
}).navigationTitle("QEMU"),
destination: VMConfigQEMUView(config: config).navigationTitle("QEMU"),
label: {
Label("QEMU", systemImage: "shippingbox")
.labelStyle(.roundRectIcon)
-15
View File
@@ -50,10 +50,6 @@ struct SavePanel: NSViewRepresentable {
savePanel.title = NSLocalizedString("Select where to save UTM Virtual Machine:", comment: "SavePanel")
savePanel.nameFieldStringValue = vm.pathUrl.lastPathComponent
savePanel.allowedContentTypes = [.UTM]
case .qemuCommand:
savePanel.title = NSLocalizedString("Select where to export QEMU command:", comment: "SavePanel")
savePanel.nameFieldStringValue = "command"
savePanel.allowedContentTypes = [.plainText]
}
// Calling savePanel.begin with the appropriate completion handlers
@@ -95,17 +91,6 @@ struct SavePanel: NSViewRepresentable {
}
isPresented = false
}
case .qemuCommand(let command):
savePanel.beginSheetModal(for: window) { result in
if result == .OK {
if let destUrl = savePanel.url {
data.busyWork {
try command.write(to: destUrl, atomically: true, encoding: .utf8)
}
}
}
isPresented = false
}
}
}
}
+112 -114
View File
@@ -17,130 +17,128 @@
import SwiftUI
struct VMConfigQEMUArgumentsView: View {
@ObservedObject var config: UTMQemuConfiguration
@State private var selectedId: UUID?
@State private var isEditingNewArgument = false
@State private var editArgument: QEMUArgument?
private var selectedIndex: Int? {
config.qemu.additionalArguments.firstIndex(where: { $0.id == selectedId })
}
var body: some View {
// GeometryReader keeps the ideal size small so the sheet does not grow to fit the whole command line
GeometryReader { _ in
VStack(alignment: .leading) {
Table(config.qemu.additionalArguments, selection: $selectedId) {
TableColumn("Custom Arguments") { argument in
Text(argument.string)
.font(.system(.body, design: .monospaced))
}
}
.frame(height: 150)
HStack {
Spacer()
Button("Move Up") {
move(by: -1)
}.disabled(selectedIndex == nil || selectedIndex == 0)
Button("Move Down") {
move(by: 1)
}.disabled(selectedIndex == nil || selectedIndex == config.qemu.additionalArguments.count - 1)
Button("Delete") {
if let index = selectedIndex {
config.qemu.additionalArguments.remove(at: index)
selectedId = nil
}
}.disabled(selectedIndex == nil)
Button("Edit…", action: editSelected)
.disabled(selectedIndex == nil)
.popover(item: $editArgument, arrowEdge: .top) { argument in
QEMUArgumentEdit(config: $config.qemu, argument: argument).padding()
.frame(width: 400)
}
Button("New…") {
isEditingNewArgument.toggle()
}.popover(isPresented: $isEditingNewArgument, arrowEdge: .top) {
QEMUArgumentEdit(config: $config.qemu, argument: QEMUArgument("")).padding()
.frame(width: 400)
}
}
VMConfigQEMUCommandLineView(config: config)
}.padding()
}
}
private func editSelected() {
editArgument = config.qemu.additionalArguments.first(where: { $0.id == selectedId })
}
private func move(by offset: Int) {
guard let index = selectedIndex else {
return
}
let destination = offset < 0 ? index - 1 : index + 2
config.qemu.additionalArguments.move(fromOffsets: IndexSet(integer: index), toOffset: destination)
}
}
struct QEMUArgumentEdit: View {
@Binding var config: UTMQemuConfigurationQEMU
let architecture: QEMUArchitecture
let fixedArguments: [QEMUArgument]
@State var argument: QEMUArgument
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let fixedUuids: Set<UUID>
@State private var selected: Set<UUID>
@State private var selectedArgument = QEMUArgument("")
@FocusState private var focused: UUID?
@State private var showExportArgs: Bool = false
private var customUuids: Set<UUID> {
Set(config.additionalArguments.map({ $0.id }))
}
private var exportShareItem: VMShareItemModifier.ShareItem {
var argString = "qemu-system-\(architecture.rawValue)"
for arg in fixedArguments {
if arg.string.contains(" ") {
argString += " \"\(arg.string)\""
} else {
argString += " \(arg.string)"
}
}
for arg in config.additionalArguments {
argString += " \(arg.string)"
}
return .qemuCommand(argString)
}
init(config: Binding<UTMQemuConfigurationQEMU>, architecture: QEMUArchitecture, fixedArguments: [QEMUArgument]) {
self._config = config
self.architecture = architecture
self.fixedArguments = fixedArguments
self.fixedUuids = Set(fixedArguments.map({ $0.id }))
self._selected = State<Set<UUID>>(initialValue: .init())
private var index: Int? {
config.additionalArguments.firstIndex(where: { $0.id == argument.id })
}
var body: some View {
VStack {
Table(of: QEMUArgument.self, selection: $selected) {
TableColumn("Arguments") { arg in
let customSelected = selected.intersection(customUuids)
if fixedUuids.contains(arg.id) || customSelected.count > 1 || !customSelected.contains(arg.id) {
Text(arg.string)
.foregroundColor(fixedUuids.contains(arg.id) ? .secondary : .primary)
.textSelection(.enabled)
} else {
TextField("", text: $selectedArgument.string)
.focused($focused, equals: arg.id)
.onSubmit(of: .text) {
if let index = config.additionalArguments.firstIndex(of: arg) {
config.additionalArguments[index] = selectedArgument
}
}
}
}
} rows: {
ForEach(fixedArguments) { arg in
TableRow(arg)
}
ForEach(config.additionalArguments) { arg in
TableRow(arg)
}
}.onChange(of: selected) { newValue in
// save changes to last selected argument
if let index = config.additionalArguments.firstIndex(where: { $0.id == selectedArgument.id }) {
config.additionalArguments[index] = selectedArgument
selectedArgument = .init("")
}
// get new selected argument
if let selectedId = selected.intersection(customUuids).first {
if let arg = config.additionalArguments.first(where: { $0.id == selectedId }) {
selectedArgument = arg
}
}
}
Spacer()
TextField("Argument", text: $argument.string, prompt: Text("-option value"))
.font(.system(.body, design: .monospaced))
.disableAutocorrection(true)
.onSubmit(save)
HStack {
Button {
showExportArgs.toggle()
} label: {
Text("Export QEMU Command…")
}.help("Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments.")
Spacer()
let customSelected = selected.intersection(customUuids)
if !customSelected.isEmpty {
if customSelected.count > 1 || customSelected.first != config.additionalArguments.first?.id {
Button {
for i in 1..<config.additionalArguments.count {
if customSelected.contains(config.additionalArguments[i].id) {
config.additionalArguments.move(fromOffsets: .init(integer: i), toOffset: i - 1)
}
}
} label: {
Text("Move Up")
}
}
if customSelected.count > 1 || customSelected.first != config.additionalArguments.last?.id {
Button {
for i in (0..<config.additionalArguments.count-1).reversed() {
if customSelected.contains(config.additionalArguments[i].id) {
config.additionalArguments.move(fromOffsets: .init(integer: i), toOffset: i + 2)
}
}
} label: {
Text("Move Down")
}
}
Button(role: .destructive) {
config.additionalArguments.removeAll(where: { customSelected.contains($0.id) })
} label: {
Text("Delete")
if let index = index {
Button("Delete") {
config.additionalArguments.remove(at: index)
closePopup()
}
}
Button {
let new = QEMUArgument("")
config.additionalArguments.append(new)
selected.removeAll()
selected.insert(new.id)
focused = new.id
} label: {
Text("New…")
Button("Save", action: save)
.disabled(argument.string.isEmpty)
}
}
}
private func save() {
guard !argument.string.isEmpty else {
return
}
if let index = index {
config.additionalArguments[index] = argument
} else {
config.additionalArguments.append(argument)
}
closePopup()
}
private func closePopup() {
presentationMode.wrappedValue.dismiss()
}
}
struct VMConfigQEMUArgumentsView_Previews: PreviewProvider {
@State static private var config = UTMQemuConfiguration()
static var previews: some View {
VMConfigQEMUArgumentsView(config: config)
.frame(width: 600, height: 500)
.onAppear {
if config.qemu.additionalArguments.isEmpty {
config.qemu.additionalArguments.append(QEMUArgument("-append"))
config.qemu.additionalArguments.append(QEMUArgument("\"root=/dev/vda1 console=ttyS0\""))
}
}.padding([.bottom, .leading, .trailing])
}.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportShareItem))
}
}
}
+18 -11
View File
@@ -23,6 +23,12 @@ struct VMQEMUSettingsView: View {
@State private var infoActive: Bool = true
@State private var isResetConfig: Bool = false
@State private var isNewDriveShown: Bool = false
@State private var isCustomArgumentsEnabled: Bool
init(config: UTMQemuConfiguration) {
self.config = config
self._isCustomArgumentsEnabled = State(initialValue: !config.qemu.additionalArguments.isEmpty)
}
var body: some View {
NavigationLink(destination: VMConfigInfoView(config: $config.information).scrollable().settingsToolbar(), isActive: $infoActive) {
@@ -38,23 +44,24 @@ struct VMQEMUSettingsView: View {
if newValue {
config.reset(forArchitecture: config.system.architecture, target: config.system.target)
isResetConfig = false
isCustomArgumentsEnabled = false
}
}
NavigationLink {
VMConfigQEMUView(config: $config.qemu, system: $config.system, fetchFixedArguments: {
config.generatedArguments
})
.scrollable()
.settingsToolbar()
VMConfigQEMUView(config: config, isCustomArgumentsEnabled: $isCustomArgumentsEnabled)
.scrollable()
.settingsToolbar()
} label: {
Label("QEMU", systemImage: "shippingbox")
}
NavigationLink {
VMConfigQEMUArgumentsView(config: $config.qemu, architecture: config.system.architecture, fixedArguments: config.generatedArguments)
.settingsToolbar()
} label: {
Label("Arguments", systemImage: "character.textbox")
.padding(.leading)
if isCustomArgumentsEnabled {
NavigationLink {
VMConfigQEMUArgumentsView(config: config)
.settingsToolbar()
} label: {
Label("Arguments", systemImage: "character.textbox")
.padding(.leading)
}
}
NavigationLink {
VMConfigInputView(config: $config.input, hasUsbSupport: config.system.architecture.hasUsbSupport)
+18
View File
@@ -1322,6 +1322,13 @@
FFB02A90266CB09C006CD71A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A8E266CB09C006CD71A /* InfoPlist.strings */; };
FFB02A93266CB09C006CD71A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A91266CB09C006CD71A /* InfoPlist.strings */; };
FFB02A96266CB09C006CD71A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A94266CB09C006CD71A /* Localizable.strings */; };
7EFF134EA0581C7A4CBBBAF5 /* VMConfigQEMUArgumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2D14863C5CDE332BC8698C /* VMConfigQEMUArgumentsView.swift */; };
B56B01FE103C463CC3F84753 /* VMConfigQEMUArgumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2D14863C5CDE332BC8698C /* VMConfigQEMUArgumentsView.swift */; };
E75442DAFC2ED7AF55CC87CA /* VMConfigQEMUArgumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2D14863C5CDE332BC8698C /* VMConfigQEMUArgumentsView.swift */; };
7591FABAADBC2544E9F0F37B /* VMConfigQEMUCommandLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */; };
947DDF020475E2433DDC2A21 /* VMConfigQEMUCommandLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */; };
31D9CB9DD8370CF0CCCDE85C /* VMConfigQEMUCommandLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */; };
5324DE20FD9339463499B17C /* VMConfigQEMUCommandLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -2258,6 +2265,8 @@
FFB02A8F266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
FFB02A92266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
FFB02A95266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
FF2D14863C5CDE332BC8698C /* VMConfigQEMUArgumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigQEMUArgumentsView.swift; sourceTree = "<group>"; };
03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigQEMUCommandLineView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -2866,6 +2875,7 @@
CE2D954A24AD4F980059923A /* iOS */ = {
isa = PBXGroup;
children = (
FF2D14863C5CDE332BC8698C /* VMConfigQEMUArgumentsView.swift */,
CE7BED4D22600F5000A1E1B6 /* Display */,
CE8813D224CD230300532628 /* ActivityView.swift */,
CED814EE24C7EB760042F0F1 /* ImagePicker.swift */,
@@ -3133,6 +3143,7 @@
CEB63A9624F47C1200CAF323 /* Shared */ = {
isa = PBXGroup;
children = (
03AEFABA6F4DF195E96AD2D5 /* VMConfigQEMUCommandLineView.swift */,
CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */,
84B36D2827B790BE00C22685 /* DestructiveButton.swift */,
8471772727CD3CAB00D3A50B /* DetailedSection.swift */,
@@ -3834,6 +3845,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7591FABAADBC2544E9F0F37B /* VMConfigQEMUCommandLineView.swift in Sources */,
7EFF134EA0581C7A4CBBBAF5 /* VMConfigQEMUArgumentsView.swift in Sources */,
CE2D926A24AD46670059923A /* VMDisplayMetalViewController+Pointer.h in Sources */,
CE231D462BDDFD03006D6DC3 /* UTMDonateStore.swift in Sources */,
84C4D9022880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */,
@@ -4016,6 +4029,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
947DDF020475E2433DDC2A21 /* VMConfigQEMUCommandLineView.swift in Sources */,
A1C4F00230C1200000CA9501 /* CapsLockRemapper.swift in Sources */,
CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */,
CEB63A7724F4654400CAF323 /* Main.swift in Sources */,
@@ -4247,6 +4261,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
31D9CB9DD8370CF0CCCDE85C /* VMConfigQEMUCommandLineView.swift in Sources */,
B56B01FE103C463CC3F84753 /* VMConfigQEMUArgumentsView.swift in Sources */,
CEA45E25263519B5002FA97D /* VMDisplayMetalViewController+Pointer.h in Sources */,
84C505AD28C588EC007CE8FF /* SizeTextField.swift in Sources */,
CEA45E27263519B5002FA97D /* VMRemovableDrivesView.swift in Sources */,
@@ -4436,6 +4452,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5324DE20FD9339463499B17C /* VMConfigQEMUCommandLineView.swift in Sources */,
E75442DAFC2ED7AF55CC87CA /* VMConfigQEMUArgumentsView.swift in Sources */,
CEF7F5972AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.h in Sources */,
CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */,
CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */,