From 90fb1bfbccf9ff4d8f0de50311595de29a30cf85 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Tue, 22 Sep 2026 01:08:11 +0800 Subject: [PATCH] Change Telnet's icon --- .../Connections/ConnectionHistoryEntry.swift | 2 +- .../ConnectionJourneyRouteView.swift | 9 +----- .../ConnectionJourneySections.swift | 31 ++++++++++++------- TFI/Features/Vault/MobileVaultView.swift | 6 ++-- .../MobileHomeScreenQuickActionTests.swift | 14 ++++++--- .../Commands/AppCommandRegistry.swift | 2 +- TFM/DesignSystem/MenuSymbol.swift | 3 +- TFM/Features/Hosts/HostActions.swift | 4 +-- TFM/Features/Workspaces/Pane.swift | 2 +- TFMTests/ConnectionHistoryTests.swift | 8 +++++ TFMTests/ConnectionJourneyTests.swift | 12 ++++++- TFMTests/QuickOpenTests.swift | 23 ++++++++++++++ TFMTests/WindowStateTests.swift | 11 +++++++ 13 files changed, 94 insertions(+), 33 deletions(-) diff --git a/Frameworks/TermindDomain/Connections/ConnectionHistoryEntry.swift b/Frameworks/TermindDomain/Connections/ConnectionHistoryEntry.swift index 4a7eb42e..31924419 100644 --- a/Frameworks/TermindDomain/Connections/ConnectionHistoryEntry.swift +++ b/Frameworks/TermindDomain/Connections/ConnectionHistoryEntry.swift @@ -21,7 +21,7 @@ public nonisolated struct ConnectionHistoryEntry: Identifiable, Hashable, Sendab public var symbol: String { switch self { case .ssh: "terminal" - case .telnet: "terminal" + case .telnet: "text.rectangle" case .sftp: "folder" } } diff --git a/Frameworks/TermindPresentation/ConnectionJourneyRouteView.swift b/Frameworks/TermindPresentation/ConnectionJourneyRouteView.swift index 6d7e113b..b0eea737 100644 --- a/Frameworks/TermindPresentation/ConnectionJourneyRouteView.swift +++ b/Frameworks/TermindPresentation/ConnectionJourneyRouteView.swift @@ -233,14 +233,7 @@ private struct JourneyEndpointView: View { #endif case .jump: return "server.rack" case .destination: - return switch purpose { - case .sshSession: "apple.terminal.fill" - case .telnetSession: "terminal.fill" - case .sftpChannel: "folder.fill" - case .metrics: "waveform.path.ecg" - case .portForwarding: "arrow.left.arrow.right" - case .serialSession: "cable.connector" - } + return purpose.symbol(for: .routeDestination) } } diff --git a/Frameworks/TermindPresentation/ConnectionJourneySections.swift b/Frameworks/TermindPresentation/ConnectionJourneySections.swift index f4d0eb63..0f7a92f0 100644 --- a/Frameworks/TermindPresentation/ConnectionJourneySections.swift +++ b/Frameworks/TermindPresentation/ConnectionJourneySections.swift @@ -1,6 +1,25 @@ import SwiftUI import TermindApplication +nonisolated enum ConnectionJourneySymbolPlacement { + case header + case routeDestination +} + +extension ConnectionPurpose { + nonisolated func symbol(for placement: ConnectionJourneySymbolPlacement) -> String { + switch (self, placement) { + case (.sshSession, _): "apple.terminal.fill" + case (.telnetSession, _): "text.rectangle.fill" + case (.sftpChannel, .header): "folder.fill.badge.gearshape" + case (.sftpChannel, .routeDestination): "folder.fill" + case (.serialSession, _): "cable.connector" + case (.metrics, _): "waveform.path.ecg" + case (.portForwarding, _): "arrow.left.arrow.right" + } + } +} + public struct ConnectionJourneyHeader: View { let journey: ConnectionJourney let selectedForeground: Color @@ -29,7 +48,7 @@ public struct ConnectionJourneyHeader: View { .fill(Color.accentColor.gradient) .frame(width: 52, height: 52) .overlay { - Image(systemName: symbol) + Image(systemName: journey.purpose.symbol(for: .header)) .font(.system(size: 22, weight: .semibold)) .foregroundStyle(selectedForeground) } @@ -82,16 +101,6 @@ public struct ConnectionJourneyHeader: View { return "\(protocolName) \(address)" } - private var symbol: String { - switch journey.purpose { - case .sshSession: "apple.terminal.fill" - case .telnetSession: "terminal.fill" - case .sftpChannel: "folder.fill.badge.gearshape" - case .serialSession: "cable.connector" - case .metrics: "waveform.path.ecg" - case .portForwarding: "arrow.left.arrow.right" - } - } } public struct ConnectionJourneyStatus: View { diff --git a/TFI/Features/Vault/MobileVaultView.swift b/TFI/Features/Vault/MobileVaultView.swift index e4829421..6010aed6 100644 --- a/TFI/Features/Vault/MobileVaultView.swift +++ b/TFI/Features/Vault/MobileVaultView.swift @@ -609,13 +609,13 @@ struct MobileVaultView: View { private func hostMenuButton(_ action: MobileHostMenuAction, host: Host) -> some View { switch action { case .ssh: - Button("SSH", systemImage: "terminal") { open(host.id, .ssh) } + Button("SSH", systemImage: ConnectionHistoryEntry.Kind.ssh.symbol) { open(host.id, .ssh) } .disabled(!host.sshEnabled) case .telnet: - Button("Telnet", systemImage: "terminal") { open(host.id, .telnet) } + Button("Telnet", systemImage: ConnectionHistoryEntry.Kind.telnet.symbol) { open(host.id, .telnet) } .disabled(!host.telnetEnabled) case .sftp: - Button("SFTP", systemImage: "folder") { open(host.id, .sftp) } + Button("SFTP", systemImage: ConnectionHistoryEntry.Kind.sftp.symbol) { open(host.id, .sftp) } .disabled(!host.sshEnabled) case .edit: Button("Edit", systemImage: "pencil") { editingHost = host } diff --git a/TFITests/MobileHomeScreenQuickActionTests.swift b/TFITests/MobileHomeScreenQuickActionTests.swift index a7041dff..42b28de3 100644 --- a/TFITests/MobileHomeScreenQuickActionTests.swift +++ b/TFITests/MobileHomeScreenQuickActionTests.swift @@ -11,7 +11,9 @@ import struct TermindDomain.Host struct MobileHomeScreenQuickActionTests { @Test func descriptorsUseTheNewestThreeUniqueValidHistoryRows() throws { let app = try MobileAppModel(inMemory: true) - let repeated = Host(name: "Production", host: "production.example.invalid") + let repeated = Host( + name: "Production", host: "production.example.invalid", + telnetEnabled: true) let others = (0..<3).map { Host(name: "Host \($0)", host: "host-\($0).example.invalid") } app.library.upsert(repeated) for host in others { app.library.upsert(host) } @@ -20,6 +22,8 @@ struct MobileHomeScreenQuickActionTests { kind: .ssh, for: repeated.id, at: Date(timeIntervalSince1970: 60)) _ = app.connectionHistory.record( kind: .ssh, for: repeated.id, at: Date(timeIntervalSince1970: 50)) + _ = app.connectionHistory.record( + kind: .telnet, for: repeated.id, at: Date(timeIntervalSince1970: 45)) _ = app.connectionHistory.record( kind: .sftp, for: repeated.id, at: Date(timeIntervalSince1970: 40)) _ = app.connectionHistory.record( @@ -43,11 +47,13 @@ struct MobileHomeScreenQuickActionTests { descriptors.map(\.action) == [ .history(hostID: repeated.id, kind: .ssh), + .history(hostID: repeated.id, kind: .telnet), .history(hostID: repeated.id, kind: .sftp), - .history(hostID: others[0].id, kind: .ssh), ]) - #expect(descriptors.map(\.title) == ["Production", "Production", "Host 0"]) - #expect(descriptors.map(\.symbol) == ["terminal", "folder", "terminal"]) + #expect(descriptors.map(\.title) == ["Production", "Production", "Production"]) + #expect(descriptors.map(\.symbol) == ["terminal", "text.rectangle", "folder"]) + #expect(UIImage(systemName: descriptors[1].symbol) != nil) + #expect(descriptors[1].shortcutItem.icon != nil) } @Test func historyShortcutPayloadRoundTripsAndMalformedItemsAreRejected() throws { diff --git a/TFM/Application/Commands/AppCommandRegistry.swift b/TFM/Application/Commands/AppCommandRegistry.swift index 7f1ac6f9..08d9867c 100644 --- a/TFM/Application/Commands/AppCommandRegistry.swift +++ b/TFM/Application/Commands/AppCommandRegistry.swift @@ -219,7 +219,7 @@ final class AppCommandRegistry { AppCommandDescriptor( id: "host.telnet.\(host.id.uuidString)", title: host.name, subtitle: String(localized: "Connect with Telnet ยท \(host.displayAddress(for: .telnet))"), - symbol: "terminal", section: .hosts, keywords: keywords + ["telnet"], + symbol: MenuSymbol.telnet, section: .hosts, keywords: keywords + ["telnet"], target: .connectHost(host.id, .telnetSession))) } return commands diff --git a/TFM/DesignSystem/MenuSymbol.swift b/TFM/DesignSystem/MenuSymbol.swift index 624d535c..db7922c6 100644 --- a/TFM/DesignSystem/MenuSymbol.swift +++ b/TFM/DesignSystem/MenuSymbol.swift @@ -33,6 +33,7 @@ nonisolated enum MenuSymbol { static let connect = "network" static let batchStartup = "wind" static let ssh = "apple.terminal" + static let telnet = "text.rectangle" static let sftp = "folder" // MARK: Creating @@ -121,7 +122,7 @@ nonisolated enum MenuSymbol { /// is exactly the kind of mistake no one notices in review. static let all: [String] = [ settings, quickOpen, find, actions, support, showAssistant, - quickConnect, connect, batchStartup, ssh, sftp, + quickConnect, connect, batchStartup, ssh, telnet, sftp, newChat, newHost, newGroup, newPackage, newFolder, newSnippet, saveAsSnippet, newIdentity, generateKey, generateCommand, importItem, download, export, upload, diff --git a/TFM/Features/Hosts/HostActions.swift b/TFM/Features/Hosts/HostActions.swift index 3387eafc..665c978f 100644 --- a/TFM/Features/Hosts/HostActions.swift +++ b/TFM/Features/Hosts/HostActions.swift @@ -190,7 +190,7 @@ enum HostMenuEntries { app.open([host], as: .sshSession, in: window) }, NativeMenuEntry( - title: String(localized: "with Telnet"), symbol: "terminal", + title: String(localized: "with Telnet"), symbol: MenuSymbol.telnet, isEnabled: host.telnetEnabled ) { app.open([host], as: .telnetSession, in: window) @@ -231,7 +231,7 @@ struct HostConnectMenu: View { app.open(hosts, as: .sshSession, in: window) } .disabled(!hosts.allSatisfy(\.sshEnabled)) - Button("with Telnet", systemImage: "terminal") { + Button("with Telnet", systemImage: MenuSymbol.telnet) { app.open(hosts, as: .telnetSession, in: window) } .disabled(!hosts.allSatisfy(\.telnetEnabled)) diff --git a/TFM/Features/Workspaces/Pane.swift b/TFM/Features/Workspaces/Pane.swift index a3bb26df..f2d04b93 100644 --- a/TFM/Features/Workspaces/Pane.swift +++ b/TFM/Features/Workspaces/Pane.swift @@ -162,7 +162,7 @@ final class Pane: Identifiable { var symbol: String { switch content { case .sshSession: "apple.terminal" - case .telnetSession: "terminal" + case .telnetSession: MenuSymbol.telnet case .sftpChannel: "folder" case .serialSession: "cable.connector" case .empty: "rectangle.dashed" diff --git a/TFMTests/ConnectionHistoryTests.swift b/TFMTests/ConnectionHistoryTests.swift index 1faf292b..52d1f2f5 100644 --- a/TFMTests/ConnectionHistoryTests.swift +++ b/TFMTests/ConnectionHistoryTests.swift @@ -15,6 +15,14 @@ import struct TermindDomain.Host @Suite @MainActor struct ConnectionHistoryTests { + @Test + func connectionProtocolSymbolsAreDistinct() { + #expect(ConnectionHistoryEntry.Kind.ssh.symbol == "terminal") + #expect(ConnectionHistoryEntry.Kind.telnet.symbol == "text.rectangle") + #expect(ConnectionHistoryEntry.Kind.sftp.symbol == "folder") + #expect(Set(ConnectionHistoryEntry.Kind.allCases.map(\.symbol)).count == 3) + } + @Test func schemaV5MigratesWithAnEmptyConnectionTimeline() throws { let directory = FileManager.default.temporaryDirectory diff --git a/TFMTests/ConnectionJourneyTests.swift b/TFMTests/ConnectionJourneyTests.swift index 022ac26c..ff2d1679 100644 --- a/TFMTests/ConnectionJourneyTests.swift +++ b/TFMTests/ConnectionJourneyTests.swift @@ -3,17 +3,27 @@ import TermindApplication import TermindDomain import TermindInfrastructure import TermindPersistence -import TermindPresentation import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host @testable import Termind +@testable import TermindPresentation @Suite @MainActor struct ConnectionJourneyTests { + @Test + func telnetUsesItsOwnSymbolThroughoutTheConnectionJourney() { + #expect(ConnectionPurpose.telnetSession.symbol(for: .header) == "text.rectangle.fill") + #expect(ConnectionPurpose.telnetSession.symbol(for: .routeDestination) == "text.rectangle.fill") + #expect(ConnectionPurpose.sshSession.symbol(for: .header) == "apple.terminal.fill") + #expect(ConnectionPurpose.sshSession.symbol(for: .routeDestination) == "apple.terminal.fill") + #expect(ConnectionPurpose.sftpChannel.symbol(for: .header) == "folder.fill.badge.gearshape") + #expect(ConnectionPurpose.sftpChannel.symbol(for: .routeDestination) == "folder.fill") + } + @Test func routeStartsAtBlueLocalEndpointAndKeepsHostsPending() { let jump = Host(name: "Bastion", host: "jump.example") diff --git a/TFMTests/QuickOpenTests.swift b/TFMTests/QuickOpenTests.swift index a578e015..0d7605a4 100644 --- a/TFMTests/QuickOpenTests.swift +++ b/TFMTests/QuickOpenTests.swift @@ -15,6 +15,29 @@ import struct TermindDomain.Host @Suite @MainActor struct QuickOpenTests { + @Test + func telnetUsesItsOwnSymbolInQuickOpenAndNativeHostMenus() throws { + let host = Host( + name: "Legacy Console", host: "console.example.invalid", + telnetEnabled: true) + let store = HostLibraryModel(hosts: [host]) + let app = AppModel(hosts: store) + let window = WindowModel() + + let commands = app.commands.commands(app: app, window: window) + let ssh = try #require(commands.first { $0.target == .connectHost(host.id, .sshSession) }) + let telnet = try #require(commands.first { $0.target == .connectHost(host.id, .telnetSession) }) + #expect(ssh.symbol == MenuSymbol.ssh) + #expect(telnet.symbol == MenuSymbol.telnet) + #expect(ssh.symbol != telnet.symbol) + + let entries = HostMenuEntries.entries(for: host, app: app, window: window) + let connect = try #require(entries.first { $0.title == String(localized: "Connect") }) + let telnetEntry = try #require( + connect.children.first { $0.title == String(localized: "with Telnet") }) + #expect(telnetEntry.symbol == MenuSymbol.telnet) + } + @Test func registryCombinesManagementWorkspacesHostsAndActions() { let host = Host(name: "Production", host: "prod.example", tags: ["critical"]) diff --git a/TFMTests/WindowStateTests.swift b/TFMTests/WindowStateTests.swift index 771b5697..850ef4d7 100644 --- a/TFMTests/WindowStateTests.swift +++ b/TFMTests/WindowStateTests.swift @@ -15,6 +15,17 @@ import struct TermindDomain.Host @Suite @MainActor struct WindowStateTests { + @Test + func sshAndTelnetPanesUseDistinctProtocolSymbols() { + let host = Host( + name: "Console", host: "console.example.invalid", + telnetEnabled: true) + + #expect(Pane.sshSession(host: host).symbol == MenuSymbol.ssh) + #expect(Pane.telnetSession(host: host).symbol == MenuSymbol.telnet) + #expect(MenuSymbol.ssh != MenuSymbol.telnet) + } + @Test func assistantManualOrderMovesAcrossPinnedBoundaryAndStaysStableAfterActivity() throws { let first = ConversationModel(updatedAt: Date(timeIntervalSinceReferenceDate: 10))