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.
195 lines
7.8 KiB
Swift
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
|
|
}
|
|
}
|