Add SSH-authenticated transports with independent settings, protocol-aware menus and snippets, and tmux session persistence. Restore eligible iOS workspaces from encrypted transport checkpoints without adding a macOS recovery entry. Include schema migrations and regression coverage.
112 lines
4.9 KiB
Swift
112 lines
4.9 KiB
Swift
import ActivityKit
|
|
import Foundation
|
|
|
|
/// One activity summarizes all mobile workspaces. Never contains credentials or terminal output.
|
|
nonisolated struct WorkspaceActivityAttributes: ActivityAttributes {
|
|
struct Recovery: Codable, Hashable {
|
|
var version = 1
|
|
var sourceWorkspaceID: UUID
|
|
var hostID: UUID
|
|
var kind: String
|
|
/// Opaque, versioned domain reference. Never included in deep links.
|
|
var remoteSession: Data?
|
|
}
|
|
|
|
struct Session: Codable, Hashable, Identifiable {
|
|
var id: UUID
|
|
var title: String
|
|
var kind: String
|
|
var connected: Bool
|
|
var iconAsset: String? = nil
|
|
var recovery: Recovery? = nil
|
|
|
|
var destinationURL: URL {
|
|
guard let recovery else { return WorkspaceActivityAttributes.destinationURL(for: id) }
|
|
var components = URLComponents(
|
|
url: WorkspaceActivityAttributes.destinationURL(for: id), resolvingAgainstBaseURL: false)!
|
|
components.queryItems = [
|
|
URLQueryItem(name: "v", value: "1"),
|
|
URLQueryItem(name: "host", value: recovery.hostID.uuidString),
|
|
URLQueryItem(name: "kind", value: recovery.kind),
|
|
]
|
|
return components.url!
|
|
}
|
|
}
|
|
|
|
struct ContentState: Codable, Hashable {
|
|
static let maximumPresentedSessionCount = 6
|
|
|
|
var workspaceCount: Int
|
|
var connectedCount: Int
|
|
/// Bounded to fit ActivityKit's payload and the lock-screen layout.
|
|
var sessions: [Session] = []
|
|
var focusedWorkspaceID: UUID?
|
|
|
|
static func snapshot(_ workspaces: [Session], focusedID: UUID?) -> Self {
|
|
let focused = workspaces.first(where: { $0.id == focusedID }) ?? workspaces.last
|
|
let ordered =
|
|
focused.map { selected in [selected] + workspaces.filter { $0.id != selected.id } } ?? workspaces
|
|
let visible = ordered.prefix(maximumPresentedSessionCount).map { session in
|
|
var session = session
|
|
session.title = String(decoding: String(session.title.prefix(32)).utf8.prefix(96), as: UTF8.self)
|
|
return session
|
|
}
|
|
var result = Self(
|
|
workspaceCount: workspaces.count, connectedCount: workspaces.filter(\.connected).count,
|
|
sessions: visible, focusedWorkspaceID: focused?.id)
|
|
// Leave room for Activity attributes and system encoding overhead. Drop whole entries,
|
|
// never part of an identity. The focused entry is first and therefore retained longest.
|
|
while !result.sessions.isEmpty, (try? JSONEncoder().encode(result).count) ?? Int.max > 3500 {
|
|
result.sessions.removeLast()
|
|
}
|
|
if !result.sessions.contains(where: { $0.id == result.focusedWorkspaceID }) {
|
|
result.focusedWorkspaceID = result.sessions.first?.id
|
|
}
|
|
return result
|
|
}
|
|
|
|
var destinationURL: URL {
|
|
sessions.first(where: { $0.id == focusedWorkspaceID })?.destinationURL
|
|
?? focusedWorkspaceID.map(WorkspaceActivityAttributes.destinationURL(for:))
|
|
?? WorkspaceActivityAttributes.destinationURL
|
|
}
|
|
}
|
|
|
|
var scope = "workspaces"
|
|
|
|
static var destinationURL: URL { URL(string: "termind://workspaces")! }
|
|
|
|
static func destinationURL(for workspaceID: UUID) -> URL {
|
|
URL(string: "termind://workspace/\(workspaceID.uuidString)")!
|
|
}
|
|
|
|
static func accepts(_ url: URL) -> Bool {
|
|
url.scheme == "termind" && url.host == "workspaces"
|
|
&& (url.path.isEmpty || url.path == "/") && url.query == nil && url.fragment == nil
|
|
}
|
|
|
|
static func workspaceID(from url: URL) -> UUID? {
|
|
guard url.scheme == "termind", url.host == "workspace",
|
|
url.user == nil, url.password == nil, url.port == nil,
|
|
url.fragment == nil, url.pathComponents.count == 2
|
|
else { return nil }
|
|
if url.query != nil, recovery(from: url) == nil { return nil }
|
|
return UUID(uuidString: url.lastPathComponent)
|
|
}
|
|
|
|
static func recovery(from url: URL) -> Recovery? {
|
|
guard url.scheme == "termind", url.host == "workspace",
|
|
url.user == nil, url.password == nil, url.port == nil, url.fragment == nil,
|
|
url.pathComponents.count == 2, let id = UUID(uuidString: url.lastPathComponent),
|
|
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
|
|
items.count == 3, Set(items.map(\.name)) == ["v", "host", "kind"],
|
|
items.first(where: { $0.name == "v" })?.value == "1",
|
|
let rawHost = items.first(where: { $0.name == "host" })?.value,
|
|
let host = UUID(uuidString: rawHost),
|
|
let kind = items.first(where: { $0.name == "kind" })?.value,
|
|
["ssh", "mosh", "eternalTerminal", "sftp", "telnet"].contains(kind)
|
|
else { return nil }
|
|
return Recovery(sourceWorkspaceID: id, hostID: host, kind: kind)
|
|
}
|
|
}
|