Keep mobile list snapshots and section identity stable during swipe closure. Move managed-session termination to app-owned background cleanup, transfer existing SSH connections, and allow one silent reconnect when needed. Preserve passive teardown behavior and prevent reopened workspaces from attaching to sessions being terminated. Cover connection ownership, cleanup cancellation, saved workspace closure, and list updates with regression tests.
61 lines
2.5 KiB
Swift
61 lines
2.5 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
/// Only saved-host references and presentation state survive an app restart.
|
|
struct ClosedWorkspaceRecord: Codable, Identifiable {
|
|
let id: UUID
|
|
let title: String
|
|
let symbol: String
|
|
let layout: WorkspaceTemplateLayout?
|
|
let paneNames: [String?]
|
|
let focusedPaneIndex: Int
|
|
var remoteSessions: [RemoteSessionReference?]? = nil
|
|
|
|
@MainActor
|
|
init(_ workspace: Workspace, hosts: HostLibraryModel) {
|
|
let layout =
|
|
workspace.panes.contains { $0.sshSession?.isExternalTemporaryConnection == true }
|
|
? nil : workspace.workspaceTemplateLayout(in: hosts)
|
|
self.remoteSessions = workspace.panes.map { $0.sshSession?.restorableRemoteSession }
|
|
self.id = workspace.id
|
|
self.title = workspace.title(in: hosts)
|
|
self.symbol = workspace.symbol
|
|
self.layout = layout
|
|
self.paneNames = workspace.panes.map(\.customTitle)
|
|
self.focusedPaneIndex = workspace.panes.firstIndex { $0.id == workspace.focusedPaneID } ?? 0
|
|
}
|
|
|
|
@MainActor
|
|
func canRestore(in hosts: HostLibraryModel) -> Bool {
|
|
guard let layout else { return false }
|
|
return layout.isValid
|
|
&& layout.panes.allSatisfy { pane in
|
|
switch pane {
|
|
case .empty: true
|
|
case .ssh(let id), .sftp(let id): hosts.host(id)?.sshEnabled == true
|
|
case .telnet(let id): hosts.host(id)?.telnetEnabled == true
|
|
case .mosh(let id): hosts.host(id)?.isEnabled(.mosh) == true
|
|
case .eternalTerminal(let id): hosts.host(id)?.isEnabled(.eternalTerminal) == true
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func restore(in hosts: HostLibraryModel) -> Workspace? {
|
|
guard canRestore(in: hosts), let layout else { return nil }
|
|
let node = layout.makeSplitNode(using: hosts).node
|
|
let workspace = Workspace(pane: node.firstPane)
|
|
workspace.layout = node
|
|
for (pane, name) in zip(workspace.panes, paneNames) { pane.customTitle = name }
|
|
for (pane, reference) in zip(workspace.panes, remoteSessions ?? []) {
|
|
pane.sshSession?.sessionPersistence.attachment = reference
|
|
}
|
|
if workspace.isSplit { workspace.customTitle = title } else { workspace.automaticTitle = title }
|
|
if workspace.panes.indices.contains(focusedPaneIndex) {
|
|
workspace.focusedPaneID = workspace.panes[focusedPaneIndex].id
|
|
}
|
|
return workspace
|
|
}
|
|
}
|