133 lines
6.5 KiB
Swift
133 lines
6.5 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
/// The registration boundary for session commands, runtime inspection, and shell pane identity.
|
|
nonisolated protocol RemoteMultiplexerDialect: Sendable {
|
|
var id: SessionPersistenceBackendID { get }
|
|
var capabilities: RemoteSessionCapabilities { get }
|
|
var versionArgument: String { get }
|
|
var paneVariable: String? { get }
|
|
func validateVersion(_ output: String) throws
|
|
func validPane(_ value: String) -> Bool
|
|
func backend(execute: @escaping RemoteSessionCommandExecutor) -> any RemoteSessionBackend
|
|
func runtime(
|
|
reference: RemoteSessionReference?, execute: @escaping RemoteSessionCommandExecutor, charset: RemoteCharset
|
|
) throws -> any RemoteMultiplexerRuntime
|
|
func helper(_ reference: RemoteSessionReference) throws -> String?
|
|
}
|
|
|
|
nonisolated protocol RemoteMultiplexerRuntime: TerminalRuntimeAdapter {
|
|
func draftTarget(
|
|
context: TerminalShellContext, terminal: String, replacing: Bool
|
|
) async throws -> RemoteMultiplexerDraftTarget
|
|
}
|
|
|
|
nonisolated enum RemoteMultiplexerRegistry {
|
|
static let dialects: [any RemoteMultiplexerDialect] = [
|
|
RemoteTmuxDialect(), RemoteZellijDialect(), RemoteZmxDialect(), RemoteHerdrDialect(),
|
|
]
|
|
private static let registrations = Dictionary(uniqueKeysWithValues: dialects.map { ($0.id, $0) })
|
|
|
|
static func dialect(_ id: SessionPersistenceBackendID) -> (any RemoteMultiplexerDialect)? {
|
|
registrations[id]
|
|
}
|
|
|
|
static func socketDialect(_ id: SessionPersistenceBackendID) -> (any RemoteSocketMultiplexerDialect)? {
|
|
dialect(id) as? any RemoteSocketMultiplexerDialect
|
|
}
|
|
}
|
|
|
|
extension RemoteMultiplexerDialect {
|
|
nonisolated func probe(execute: RemoteSessionCommandExecutor) async throws -> RemoteSessionCapabilities {
|
|
let result = try await execute(RemoteMultiplexerCommands.discovery(id) + "\n\"$exe\" \(versionArgument)")
|
|
try RemoteMultiplexerBackend.check(result)
|
|
try validateVersion(result.stdout)
|
|
return capabilities
|
|
}
|
|
}
|
|
|
|
/// Only native differences live here; the socket backend retains framing and ownership orchestration.
|
|
nonisolated protocol RemoteSocketMultiplexerDialect: RemoteMultiplexerDialect {
|
|
var list: String { get }
|
|
func parseList(_ text: String) throws -> [RemoteMultiplexerCandidate]
|
|
// Creation receives a validated managed name. Attach/stop/runtimeCommands/writeDraft receive quoted arguments.
|
|
func create(name: String, token: UUID) -> String
|
|
func attach(name: String) -> String
|
|
func stop(name: String) -> String
|
|
func routing(_ identity: RemoteMultiplexerIdentity) throws -> String
|
|
func validRoute(_ identity: RemoteMultiplexerIdentity, name: String) -> Bool
|
|
func validateIdentity(_ identity: RemoteMultiplexerIdentity) throws
|
|
func identityGuard(_ identity: RemoteMultiplexerIdentity, name: String) throws -> String
|
|
func ownershipGuard(_ identity: RemoteMultiplexerIdentity, name: String, token: UUID) throws -> String
|
|
func metadataOwnershipGuard(_ candidate: RemoteMultiplexerCandidate) throws -> String
|
|
func metadataFields(_ candidate: RemoteMultiplexerCandidate) throws -> String
|
|
func prepareAttachment() -> String
|
|
func validateTermination(_ reference: RemoteSessionReference, identity: RemoteMultiplexerIdentity) throws
|
|
func terminationCleanup(_ reference: RemoteSessionReference, identity: RemoteMultiplexerIdentity) throws -> String
|
|
func runtimeCommands(name: String) -> (String, String)
|
|
func activePane(native: String, focus: String, reference: RemoteSessionReference, clientBound: Bool) throws
|
|
-> String?
|
|
func writeDraft(name: String, pane: String, key: String) -> String
|
|
var restoreShellEnvironment: String { get }
|
|
}
|
|
|
|
extension RemoteSocketMultiplexerDialect {
|
|
nonisolated var versionArgument: String { "--version" }
|
|
nonisolated var capabilities: RemoteSessionCapabilities { .init(shellIntegration: true) }
|
|
nonisolated func backend(execute: @escaping RemoteSessionCommandExecutor) -> any RemoteSessionBackend {
|
|
RemoteMultiplexerBackend(dialect: self, execute: execute)
|
|
}
|
|
nonisolated func runtime(
|
|
reference: RemoteSessionReference?, execute: @escaping RemoteSessionCommandExecutor, charset: RemoteCharset
|
|
) throws -> any RemoteMultiplexerRuntime {
|
|
guard let reference else { throw TerminalRuntimeError.unsupported }
|
|
return RemoteSocketMultiplexerRuntime(reference: reference, execute: execute, charset: charset)
|
|
}
|
|
nonisolated func helper(_ reference: RemoteSessionReference) throws -> String? {
|
|
try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: id).helper
|
|
}
|
|
nonisolated func validateIdentity(_ identity: RemoteMultiplexerIdentity) throws {
|
|
guard !identity.isolated else { throw RemoteSessionFailure.invalidResponse }
|
|
}
|
|
nonisolated func identityGuard(_ identity: RemoteMultiplexerIdentity, name: String) throws -> String { "" }
|
|
nonisolated func ownershipGuard(_ identity: RemoteMultiplexerIdentity, name: String, token: UUID) throws -> String {
|
|
""
|
|
}
|
|
nonisolated func metadataOwnershipGuard(_ candidate: RemoteMultiplexerCandidate) throws -> String { "" }
|
|
nonisolated func metadataFields(_ candidate: RemoteMultiplexerCandidate) throws -> String { "" }
|
|
nonisolated func prepareAttachment() -> String { "" }
|
|
nonisolated func validateTermination(_ reference: RemoteSessionReference, identity: RemoteMultiplexerIdentity)
|
|
throws
|
|
{}
|
|
nonisolated func terminationCleanup(_ reference: RemoteSessionReference, identity: RemoteMultiplexerIdentity) throws
|
|
-> String
|
|
{
|
|
""
|
|
}
|
|
nonisolated var restoreShellEnvironment: String { "" }
|
|
}
|
|
|
|
nonisolated struct RemoteMultiplexerVersion {
|
|
let major: Int
|
|
let minor: Int
|
|
let patch: Int
|
|
|
|
init(_ output: String, backend: SessionPersistenceBackendID) throws {
|
|
let fields = output.split(whereSeparator: \.isWhitespace)
|
|
guard fields.count >= 2, fields[0].lowercased() == backend.rawValue else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
let components = fields[1].split(separator: ".")
|
|
guard components.count == 3, let major = Int(components[0]), let minor = Int(components[1]),
|
|
let patch = Int(components[2]), major >= 0, minor >= 0, patch >= 0
|
|
else { throw RemoteSessionFailure.unsupported }
|
|
self.major = major
|
|
self.minor = minor
|
|
self.patch = patch
|
|
}
|
|
}
|