mirror of
https://github.com/SagerNet/sing-box-for-apple.git
synced 2026-06-03 11:37:44 +00:00
126 lines
4.2 KiB
Swift
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
|