ui(app): show selected address details as caption lines for #72

收起状态按钮只显示当前选中 IP;接口名称(接口:…)与地址类型
(地址类型:…)以说明行展示在按钮下方,与菜单项的 title/subtitle
结构对应。模型移除不再被呈现层使用的 label 组合字段。
This commit is contained in:
Qiu Yuzhou
2026-10-01 16:06:53 +08:00
parent 24666df4ac
commit a7465154d2
5 changed files with 25 additions and 20 deletions
+13 -4
View File
@@ -215,17 +215,26 @@ struct TerminalProxyEnvironmentCard: View {
}
}
} label: {
Text(control.snapshot.commandAddressPicker.selected.label)
Text(control.snapshot.commandAddressPicker.selected.address)
.lineLimit(1)
}
.menuStyle(.borderedButton)
.frame(maxWidth: .infinity, alignment: .leading)
if let annotation = control.snapshot.commandAddressPicker.selected.annotation {
Text("地址类型:\(annotation)")
// 收起状态详情:接口名称与地址类型以说明行展示在按钮下方。
HStack(spacing: 4) {
Text("接口:\(control.snapshot.commandAddressPicker.selected.displayName)")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
if let annotation = control.snapshot.commandAddressPicker.selected.annotation {
Text("·")
.font(.caption)
.foregroundStyle(.secondary)
Text("地址类型:\(annotation)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
commandButton(for: .zshBash)
commandButton(for: .fish)
@@ -4,9 +4,9 @@ import SystemConfiguration
// MARK: - 命令地址投影(issue #72)
/// 首页命令地址候选(CONTEXT.md「Terminal command address」)。身份 = BSD
/// 接口身份 + 规范化 IP;显示名与类型注记仅用于呈现,不参与身份。`Hashable`
/// 供 SwiftUI Picker 标签匹配使用,选择校验一律走 `identity`。
struct TerminalCommandAddress: Equatable, Hashable, Sendable {
/// 接口身份 + 规范化 IP;显示名与类型注记仅用于呈现,不参与身份,由呈现层
/// 自行组合(菜单项主行 IP、副标题「注记 - 名称」与收起状态说明行)。
struct TerminalCommandAddress: Equatable, Sendable {
/// BSD 接口名;回环固定为 lo0。
let bsdName: String
/// 规范化 IP 文本。
@@ -28,10 +28,6 @@ struct TerminalCommandAddress: Equatable, Hashable, Sendable {
var identity: TerminalCommandAddressIdentity {
TerminalCommandAddressIdentity(bsdName: bsdName, address: address)
}
/// 候选主行标签:`{IP} - {name}`;类型注记由呈现层作为菜单项副标题与
/// 说明行单独渲染,不进主行。
var label: String { "\(address) - \(displayName)" }
}
/// 命令地址身份:BSD 接口名 + 规范化 IP。
@@ -103,7 +103,7 @@ final class ProxyCommandAddressWorkflowTests: XCTestCase {
["127.0.0.1", "::1", "240e::17", "240e::8150"],
"RFC 4941 隐私临时地址被过滤(会轮换);其余按地址稳定排序")
let secured = picker.candidates.first { $0.address == "240e::8150" }
XCTAssertEqual(secured?.label, "240e::8150 - Wi-Fi", "主行标签不含注记")
XCTAssertEqual(secured?.displayName, "Wi-Fi")
XCTAssertEqual(secured?.annotation, "autoconf secured", "注记作为独立呈现事实")
XCTAssertEqual(
picker.candidates.first { $0.address == "240e::17" }?.annotation,
@@ -125,7 +125,7 @@ final class ProxyCommandAddressWorkflowTests: XCTestCase {
XCTAssertEqual(picker.selected.address, "::1")
}
func testLabelsUseLocalizedNameWithBSDNameFallback() {
func testDisplayNamesUseLocalizedNameWithBSDNameFallback() {
setListenerMode(.allIPv4Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(localizedName: "Wi-Fi", addresses: [.ipv4("192.168.1.10")]),
@@ -134,9 +134,9 @@ final class ProxyCommandAddressWorkflowTests: XCTestCase {
interfaceFacts.emitChange()
XCTAssertEqual(
workflow.snapshot.commandAddressPicker.candidates.map(\.label),
["127.0.0.1 - lo0", "10.0.0.5 - en1", "192.168.1.10 - Wi-Fi"],
"标签为 {IP} - {name};名称缺失回退 BSD 名,回环显示 lo0")
workflow.snapshot.commandAddressPicker.candidates.map(\.displayName),
["lo0", "en1", "Wi-Fi"],
"名称缺失回退 BSD 名,回环显示 lo0")
}
func testSameInterfaceDuplicateIPMergesAcrossInterfacesKept() {
@@ -88,15 +88,15 @@ final class SystemInterfaceFactsProviderTests: XCTestCase {
XCTAssertTrue(
type == kSCNetworkInterfaceTypeIEEE80211 as String
|| type == kSCNetworkInterfaceTypeEthernet as String,
"候选 \(candidate.label) 必须来自允许类型接口(实际类型 \(type ?? "nil"))")
"候选 \(candidate.address) 必须来自允许类型接口(实际类型 \(type ?? "nil"))")
XCTAssertFalse(candidate.address.contains("%"), "候选地址已规范化")
XCTAssertFalse(
fact?.v6Flags?.contains(.temporary) ?? false,
"临时地址(会轮换)不进候选:\(candidate.label)")
"临时地址(会轮换)不进候选:\(candidate.address)")
let knownAnnotations: Set<String?> = ["autoconf secured", "autoconf", "dynamic", nil]
XCTAssertTrue(
knownAnnotations.contains(candidate.annotation),
"IPv6 候选注记只能来自 ifconfig 同源闭集:\(candidate.label)")
"IPv6 候选注记只能来自 ifconfig 同源闭集:\(candidate.address)")
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@
| 所有 IPv4 与 IPv6 接口 | IPv4、IPv6,包含两个回环地址 | 127.0.0.1 |
| 仅所有 IPv6 接口 | IPv6 | ::1 |
下拉菜单项主行显示 `{IP}`,副标题行显示「注记 - 接口名称」(无注记时仅接口名称;菜单项经 adaptive-controls 机制把两个 `Text` 桥接为 `NSMenuItem` 的原生 title/subtitle,注记与 ifconfig 同源,如 `autoconf secured`、`dynamic`),收起状态显示当前选中项的 `{IP} - {name}`,其下另有类型说明行,便于识别地址稳定性。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
下拉菜单项主行显示 `{IP}`,副标题行显示「注记 - 接口名称」(无注记时仅接口名称;菜单项经 adaptive-controls 机制把两个 `Text` 桥接为 `NSMenuItem` 的原生 title/subtitle,注记与 ifconfig 同源,如 `autoconf secured`、`dynamic`)。收起状态只显示当前选中项的 `{IP}`,其下以说明行展示「接口:{接口名称}」与「地址类型:{注记}」(有注记时),便于识别地址稳定性。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
回环项置顶,双栈时 127.0.0.1 在 ::1 前。其余按接口名称排序,同一接口先 IPv4 后 IPv6,再按地址稳定排序。同一接口的重复 IP 合并;不同接口即使 IP 相同也保留接口身份。