Files
Termind/Frameworks/TermindPresentation/PortForwardingFields.swift
T

129 lines
5.2 KiB
Swift

import SwiftUI
import TermindDomain
import struct TermindDomain.Host
public struct PortForwardingFields: View {
@Binding private var rule: PortForwardingRule
private let hosts: [Host]
public init(rule: Binding<PortForwardingRule>, hosts: [Host]) {
_rule = rule
self.hosts = hosts
}
private var namePrompt: Text {
guard let host = selectedHost else { return Text(verbatim: "") }
let name = SavedName.resolve(host.name, default: host.defaultName)
return Text(verbatim: rule.defaultName(hostName: name))
}
public var body: some View {
Section {
VaultNameField(text: $rule.label, prompt: namePrompt)
VaultColorField(selection: $rule.colorTag)
}
Section("Port Forwarding") {
Picker("Host", selection: hostSelection) {
Text("Select Host").tag(UUID?.none)
ForEach(hosts) { host in
Text(PortForwardingFieldPresentation.hostTitle(for: host)).tag(Optional(host.id))
}
}
if hosts.isEmpty {
Text("Add a host before creating a forwarding rule.").font(.caption).foregroundStyle(.secondary)
}
PortForwardingDirectionPicker(kind: $rule.kind)
Text(LocalizedStringKey(PortForwardingFieldPresentation.directionDescription(for: rule.kind)))
.font(.caption).foregroundStyle(.secondary)
LabeledContent("Bind Address") {
TextField("Bind Address", text: $rule.bindHost.singleLineText, axis: .horizontal).singleLinePaste()
.lineLimit(1)
.labelsHidden().multilineTextAlignment(.trailing)
}
let portLabel = LocalizedStringKey(PortForwardingFieldPresentation.portLabel(for: rule.kind))
LabeledContent(portLabel) {
TextField(portLabel, value: $rule.bindPort, format: .number.grouping(.never)).lineLimit(1)
.labelsHidden()
.multilineTextAlignment(.trailing)
#if os(iOS)
.keyboardType(.numberPad)
#endif
}
if PortForwardingFieldPresentation.showsDestination(for: rule.kind) {
let destinationHostLabel = LocalizedStringKey("Destination Host (Port Forwarding)")
LabeledContent(destinationHostLabel) {
TextField(
destinationHostLabel,
text: Binding(get: { rule.targetHost ?? "localhost" }, set: { rule.targetHost = $0 })
.singleLineText, axis: .horizontal
).singleLinePaste().lineLimit(1).labelsHidden().multilineTextAlignment(.trailing)
}
LabeledContent("Destination Port") {
TextField(
"Destination Port",
value: Binding(get: { rule.targetPort ?? 80 }, set: { rule.targetPort = $0 }),
format: .number.grouping(.never)
).lineLimit(1).labelsHidden().multilineTextAlignment(.trailing)
#if os(iOS)
.keyboardType(.numberPad)
#endif
}
}
}
#if os(iOS)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
#endif
}
private var selectedHost: Host? { hosts.first { $0.id == rule.hostID } }
private var hostSelection: Binding<UUID?> {
Binding(
get: { selectedHost?.id },
set: { rule.hostID = $0 ?? UUID() }
)
}
}
nonisolated enum PortForwardingFieldPresentation {
static func hostTitle(for host: Host) -> String { host.name }
static func portLabel(for kind: PortForwardingRule.Kind) -> String {
kind == .remote ? "Remote Port" : "Local Port"
}
static func directionDescription(for kind: PortForwardingRule.Kind) -> String {
#if os(macOS)
switch kind {
case .local: "This Mac → SSH Host → Destination"
case .remote: "SSH Host → This Mac → Destination"
case .dynamic: "SOCKS5 on This Mac → SSH Host → Requested Destination"
}
#else
switch kind {
case .local: "This Device → SSH Host → Destination"
case .remote: "SSH Host → This Device → Destination"
case .dynamic: "SOCKS5 on This Device → SSH Host → Requested Destination"
}
#endif
}
static func showsDestination(for kind: PortForwardingRule.Kind) -> Bool { kind != .dynamic }
}
private struct PortForwardingDirectionPicker: View {
@Binding var kind: PortForwardingRule.Kind
var body: some View {
Picker("Direction", selection: $kind) {
Text("Local").tag(PortForwardingRule.Kind.local).accessibilityLabel("Local Forward")
Text("Remote").tag(PortForwardingRule.Kind.remote).accessibilityLabel("Remote Forward")
Text("Dynamic").tag(PortForwardingRule.Kind.dynamic).accessibilityLabel("Dynamic SOCKS5 Proxy")
}
.labelsHidden().multilineTextAlignment(.trailing)
.pickerStyle(.segmented)
.frame(maxWidth: .infinity)
}
}