Files
2026-05-31 17:48:08 +08:00

126 lines
4.2 KiB
Swift

#if canImport(GhosttyTerminal) && os(iOS)
import Combine
import Foundation
import Library
import SwiftUI
@MainActor
final class TerminalSessionManager: ObservableObject {
struct ManagedSession: Identifiable {
let id: UUID
let presentedSession: TailscaleSSHPresentedSession
let viewModel: TerminalWrapperViewModel
}
@Published private(set) var sessions: [ManagedSession] = []
@Published var activeSessionID: UUID?
var onDismissAll: (() -> Void)?
private var phaseCancellables: [UUID: AnyCancellable] = [:]
private var viewModelCancellables: [UUID: AnyCancellable] = [:]
var activeSession: ManagedSession? {
sessions.first { $0.id == activeSessionID }
}
var activeDisplayTitle: String {
guard let active = activeSession else { return "" }
return TerminalSessionContentView.displayTitle(
phase: active.viewModel.phase,
extrasTitle: active.viewModel.extras.title,
peerHostName: active.presentedSession.peerHostName
)
}
func displayName(for session: ManagedSession) -> String {
TerminalSessionContentView.displayTitle(
phase: session.viewModel.phase,
extrasTitle: session.viewModel.extras.title,
peerHostName: session.presentedSession.peerHostName
)
}
func addSession(from presented: TailscaleSSHPresentedSession) {
let vm = TerminalWrapperViewModel()
let managed = ManagedSession(
id: presented.id,
presentedSession: presented,
viewModel: vm
)
sessions.append(managed)
activeSessionID = presented.id
viewModelCancellables[presented.id] = vm.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
phaseCancellables[presented.id] = vm.$phase
.dropFirst()
.sink { [weak self] phase in
if case .finished(.cleanExit) = phase {
let sessionID = presented.id
Task { @MainActor [weak self] in
try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
self?.closeSession(id: sessionID)
}
}
}
vm.start(presented)
}
func closeSession(id: UUID) {
guard let index = sessions.firstIndex(where: { $0.id == id }) else { return }
let session = sessions[index]
session.viewModel.disconnect()
phaseCancellables.removeValue(forKey: id)
viewModelCancellables.removeValue(forKey: id)
sessions.remove(at: index)
if activeSessionID == id {
activeSessionID = sessions.last?.id
}
if sessions.isEmpty {
onDismissAll?()
}
}
func disconnectAll() {
for session in sessions {
session.viewModel.disconnect()
}
phaseCancellables.removeAll()
viewModelCancellables.removeAll()
sessions.removeAll()
}
func switchTo(id: UUID) {
guard sessions.contains(where: { $0.id == id }) else { return }
activeSessionID = id
}
func createDuplicateSession() {
guard let active = activeSession else { return }
let src = active.presentedSession
let newSession = TailscaleSSHPresentedSession(
endpointTag: src.endpointTag,
peerHostName: src.peerHostName,
peerAddress: src.peerAddress,
username: src.username,
terminalType: src.terminalType,
hostKeys: src.hostKeys,
forwardAgent: src.forwardAgent
)
addSession(from: newSession)
}
func addSessionFromPeer(_ peer: TailscaleSSHPeerEntry) {
Task {
let session = await peer.createSession()
addSession(from: session)
}
}
}
#endif