diff --git a/ShadowsocksX-NG2/App/HomeView+Cards.swift b/ShadowsocksX-NG2/App/HomeView+Cards.swift index 841b48ca..a57e15d7 100644 --- a/ShadowsocksX-NG2/App/HomeView+Cards.swift +++ b/ShadowsocksX-NG2/App/HomeView+Cards.swift @@ -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) diff --git a/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift b/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift index 8fa21ed4..a902677c 100644 --- a/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift +++ b/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift @@ -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。 diff --git a/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift b/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift index 49eda90d..3bd65623 100644 --- a/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift @@ -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() { diff --git a/ShadowsocksX-NG2/Tests/SystemInterfaceFactsProviderTests.swift b/ShadowsocksX-NG2/Tests/SystemInterfaceFactsProviderTests.swift index 89b81493..d2da5310 100644 --- a/ShadowsocksX-NG2/Tests/SystemInterfaceFactsProviderTests.swift +++ b/ShadowsocksX-NG2/Tests/SystemInterfaceFactsProviderTests.swift @@ -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 = ["autoconf secured", "autoconf", "dynamic", nil] XCTAssertTrue( knownAnnotations.contains(candidate.annotation), - "IPv6 候选注记只能来自 ifconfig 同源闭集:\(candidate.label)") + "IPv6 候选注记只能来自 ifconfig 同源闭集:\(candidate.address)") } } } diff --git a/docs/design/terminal-command-address.md b/docs/design/terminal-command-address.md index 446c4074..d6c585fa 100644 --- a/docs/design/terminal-command-address.md +++ b/docs/design/terminal-command-address.md @@ -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 相同也保留接口身份。