Files
luma/Sources/LumaCore/Engine+UIState.swift
Ole André Vadla Ravnås 0e933206fb Boot virtual machines from the target picker
Add a machine abstraction to LumaCore, with QEMU as its first
backend: templates mirroring frida-core's test guests, a QMP client,
and a display that maps QEMU's surface over D-Bus rather than
copying frames.

Leave readiness to the user: drive the guest, mark it ready, and come
back to that snapshot later. Keep the machine in the project, so
booting it again resumes where it was marked. Hand the stub to
addBareboneDevice, so it joins the picker beside the other devices.

Show the display while booting, and afterwards in a Machines panel,
which the side panel now models as an enum to share its slot.
2026-08-20 14:52:52 +02:00

195 lines
7.8 KiB
Swift

import Foundation
extension Engine {
public var pharoSnippets: [PharoPlaygroundSnippet] {
guard let json = projectUIState.pharoSnippetsJSON,
let data = json.data(using: .utf8),
let snippets = try? JSONDecoder().decode([PharoPlaygroundSnippet].self, from: data)
else { return [] }
return snippets
}
public func setPharoSnippets(_ snippets: [PharoPlaygroundSnippet]) {
let json = (try? JSONEncoder().encode(snippets)).flatMap { String(data: $0, encoding: .utf8) }
guard projectUIState.pharoSnippetsJSON != json else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.pharoSnippetsJSON != json else { return }
self.projectUIState.pharoSnippetsJSON = json
try? self.store.save(self.projectUIState)
}
}
public var pharoPageWidth: Double? {
projectUIState.pharoPageWidth
}
public func setPharoPageWidth(_ width: Double) {
guard projectUIState.pharoPageWidth != width else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.pharoPageWidth != width else { return }
self.projectUIState.pharoPageWidth = width
try? self.store.save(self.projectUIState)
}
}
public var pharoPageMaximized: Bool {
projectUIState.pharoPageMaximized
}
public func setPharoPageMaximized(_ maximized: Bool) {
guard projectUIState.pharoPageMaximized != maximized else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.pharoPageMaximized != maximized else { return }
self.projectUIState.pharoPageMaximized = maximized
try? self.store.save(self.projectUIState)
}
}
public func setEventStreamCollapsed(_ collapsed: Bool) {
guard projectUIState.isEventStreamCollapsed != collapsed else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.isEventStreamCollapsed != collapsed else { return }
self.projectUIState.isEventStreamCollapsed = collapsed
try? self.store.save(self.projectUIState)
}
}
public func setEventStreamBottomHeight(_ height: Double) {
guard projectUIState.eventStreamBottomHeight != height else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.eventStreamBottomHeight != height else { return }
self.projectUIState.eventStreamBottomHeight = height
try? self.store.save(self.projectUIState)
}
}
public func setSidePanel(_ panel: SidePanel?) {
guard projectUIState.sidePanel != panel else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.sidePanel != panel else { return }
self.projectUIState.sidePanel = panel
try? self.store.save(self.projectUIState)
}
}
public func toggleSidePanel(_ panel: SidePanel) {
setSidePanel((projectUIState.sidePanel == panel) ? nil : panel)
}
public func setSelectedItemJSON(_ json: String?) {
guard projectUIState.selectedItemJSON != json else { return }
Task { @MainActor [weak self] in
guard let self, self.projectUIState.selectedItemJSON != json else { return }
self.projectUIState.selectedItemJSON = json
try? self.store.save(self.projectUIState)
}
}
public func setSidebarExpansion(sessionID: UUID, _ expansion: SidebarExpansion) {
mutateSessionUIState(sessionID: sessionID) { $0.sidebarExpansion = expansion }
}
public func setSidebarExpansion(customInstrumentDefID: UUID, _ expansion: SidebarExpansion) {
Task { @MainActor [weak self] in
guard let self else { return }
var state = self.customInstrumentDefUIStates[customInstrumentDefID]
?? CustomInstrumentDefUIState(defID: customInstrumentDefID)
guard state.sidebarExpansion != expansion else { return }
state.sidebarExpansion = expansion
self.customInstrumentDefUIStates[customInstrumentDefID] = state
try? self.store.save(state)
}
}
public func setSidebarExpansion(sessionID: UUID, group: SessionSidebarGroup, _ expansion: SidebarExpansion) {
mutateSessionUIState(sessionID: sessionID) { state in
switch group {
case .modules: state.modulesExpansion = expansion
case .threads: state.threadsExpansion = expansion
}
}
}
public func setHooksExpansion(sessionID: UUID, instrumentID: UUID, _ expansion: SidebarExpansion) {
mutateSessionUIState(sessionID: sessionID) { state in
switch expansion {
case .expanded: state.collapsedHookInstruments.remove(instrumentID)
case .collapsed: state.collapsedHookInstruments.insert(instrumentID)
}
}
}
public func setLastSelectedModuleID(sessionID: UUID, moduleID: String?) {
mutateSessionUIState(sessionID: sessionID) { $0.lastSelectedModuleID = moduleID }
}
public func setLastSelectedThreadID(sessionID: UUID, threadID: UInt?) {
mutateSessionUIState(sessionID: sessionID) { $0.lastSelectedThreadID = threadID }
}
public func setREPLLanguage(sessionID: UUID, _ language: REPLLanguage) {
mutateSessionUIState(sessionID: sessionID) { $0.replLanguage = language }
}
public func setREPLDraft(sessionID: UUID, _ draft: String?) {
mutateSessionUIState(sessionID: sessionID) { $0.replDraft = draft }
}
public func setREPLSeekAnchor(sessionID: UUID, _ anchor: AddressAnchor?) {
mutateSessionUIState(sessionID: sessionID) { $0.replSeekAnchor = anchor }
}
private func mutateSessionUIState(sessionID: UUID, _ mutate: (inout SessionUIState) -> Void) {
var state = sessionUIStates[sessionID] ?? SessionUIState(sessionID: sessionID)
mutate(&state)
guard state != sessionUIStates[sessionID] else { return }
sessionUIStates[sessionID] = state
let saved = state
Task { @MainActor [weak self] in
try? self?.store.save(saved)
}
}
public func sidebarExpansion(forSessionID sessionID: UUID) -> SidebarExpansion {
sessionUIStates[sessionID]?.sidebarExpansion ?? .expanded
}
public func sidebarExpansion(forCustomInstrumentDefID defID: UUID) -> SidebarExpansion {
customInstrumentDefUIStates[defID]?.sidebarExpansion ?? .expanded
}
public func sidebarExpansion(forSessionID sessionID: UUID, group: SessionSidebarGroup) -> SidebarExpansion {
let state = sessionUIStates[sessionID]
switch group {
case .modules: return state?.modulesExpansion ?? .collapsed
case .threads: return state?.threadsExpansion ?? .collapsed
}
}
public func hooksExpansion(forSessionID sessionID: UUID, instrumentID: UUID) -> SidebarExpansion {
let collapsed = sessionUIStates[sessionID]?.collapsedHookInstruments.contains(instrumentID) ?? false
return collapsed ? .collapsed : .expanded
}
public func lastSelectedModuleID(forSessionID sessionID: UUID) -> String? {
sessionUIStates[sessionID]?.lastSelectedModuleID
}
public func lastSelectedThreadID(forSessionID sessionID: UUID) -> UInt? {
sessionUIStates[sessionID]?.lastSelectedThreadID
}
public func replLanguage(forSessionID sessionID: UUID) -> REPLLanguage {
sessionUIStates[sessionID]?.replLanguage ?? .javascript
}
public func replDraft(forSessionID sessionID: UUID) -> String? {
sessionUIStates[sessionID]?.replDraft
}
public func replSeekAnchor(forSessionID sessionID: UUID) -> AddressAnchor? {
sessionUIStates[sessionID]?.replSeekAnchor
}
}