diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index 07367a6a..02d10d2a 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -47438,6 +47438,47 @@ } } }, + "Open current directory with %@ — right-click for more options" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelles Verzeichnis mit %@ öffnen — Rechtsklick für weitere Optionen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir le répertoire actuel via %@ — clic droit pour plus d’options" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のディレクトリを%@で開く — 右クリックでその他の操作を表示" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@로 현재 디렉터리 열기 — 추가 옵션을 보려면 마우스 오른쪽 버튼 클릭" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用%@打开当前目录—右键查看更多操作" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用%@開啟目前目錄—右鍵檢視更多操作" + } + } + } + }, "Open current directory with SFTP — right-click for more options" : { "comment" : "A contextual help for the open button in the status bar.", "extractionState" : "manual", @@ -47481,47 +47522,6 @@ } } }, - "Open current directory with SSH — right-click for more options" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktuelles Verzeichnis mit SSH öffnen — Rechtsklick für weitere Optionen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ouvrir le répertoire actuel via SSH — clic droit pour plus d’options" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "現在のディレクトリをSSHで開く — 右クリックでその他の操作を表示" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH로 현재 디렉터리 열기 — 추가 옵션을 보려면 마우스 오른쪽 버튼 클릭" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用SSH打开当前目录—右键查看更多操作" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用SSH開啟目前目錄—右鍵檢視更多操作" - } - } - } - }, "Open login page" : { "extractionState" : "manual", "localizations" : { @@ -47645,6 +47645,47 @@ } } }, + "Open with %@" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit %@ öffnen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir via %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@で開く" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@로 열기" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用%@打开" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用%@開啟" + } + } + } + }, "Open with SFTP" : { "extractionState" : "manual", "localizations" : { @@ -47686,47 +47727,6 @@ } } }, - "Open with SSH" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit SSH öffnen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ouvrir via SSH" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSHで開く" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH로 열기" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用SSH打开" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用SSH開啟" - } - } - } - }, "OpenSSH commands are unavailable for routes using an HTTP or SOCKS5 proxy." : { "extractionState" : "manual", "localizations" : { diff --git a/Frameworks/TermindPresentation/Strings/Connections.swift b/Frameworks/TermindPresentation/Strings/Connections.swift index cf24ecdc..7b4a4459 100644 --- a/Frameworks/TermindPresentation/Strings/Connections.swift +++ b/Frameworks/TermindPresentation/Strings/Connections.swift @@ -143,8 +143,8 @@ extension AppStrings { public static var hostsYouConnectToAppearHere: LocalizedStringResource { "Hosts you connect to appear here." } - public static var openWithSsh: LocalizedStringResource { - "Open with SSH" + public static func openWithTransport(_ name: String) -> LocalizedStringResource { + "Open with \(name)" } public static var sshIsDisabledForThisHost: LocalizedStringResource { "SSH is disabled for this Host." diff --git a/Frameworks/TermindPresentation/Strings/Workspace.swift b/Frameworks/TermindPresentation/Strings/Workspace.swift index 7be22432..c96b86a6 100644 --- a/Frameworks/TermindPresentation/Strings/Workspace.swift +++ b/Frameworks/TermindPresentation/Strings/Workspace.swift @@ -306,8 +306,8 @@ extension AppStrings { "Open Telnet Connection?" } - public static var openDirectoryWithSSHHelp: LocalizedStringResource { - "Open current directory with SSH — right-click for more options" + public static func openDirectoryWithTransportHelp(_ name: String) -> LocalizedStringResource { + "Open current directory with \(name) — right-click for more options" } public static var previousWorkspace: LocalizedStringResource { diff --git a/TFI/Features/Workspaces/MobileWorkspace.swift b/TFI/Features/Workspaces/MobileWorkspace.swift index 7052e12c..dbbeaecf 100644 --- a/TFI/Features/Workspaces/MobileWorkspace.swift +++ b/TFI/Features/Workspaces/MobileWorkspace.swift @@ -241,10 +241,12 @@ final class MobileWorkspace: Identifiable { path: RemotePath.normalized(workingDirectory) ) case .sftp: - guard !sftpChannel.path.isEmpty else { return nil } + guard !sftpChannel.path.isEmpty, let host = connectionHost(descriptor.hostID), + host.sshEnabled, let transport = host.preferredTerminalProtocol + else { return nil } return MobileWorkspaceDirectoryLaunch( hostID: descriptor.hostID, - kind: .ssh, + kind: .terminal(transport), path: RemotePath.normalized(sftpChannel.path) ) case .telnet: diff --git a/TFI/Features/Workspaces/MobileWorkspaceView.swift b/TFI/Features/Workspaces/MobileWorkspaceView.swift index df8fa9e5..bcde5858 100644 --- a/TFI/Features/Workspaces/MobileWorkspaceView.swift +++ b/TFI/Features/Workspaces/MobileWorkspaceView.swift @@ -765,7 +765,12 @@ struct MobileWorkspaceView: View { if let launch = workspace.sameDirectoryLaunch { switch launch.kind { case .ssh, .mosh, .eternalTerminal: - Button(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: "terminal") { + Button( + AppStrings.Connections.openWithTransport( + (launch.kind.terminalProtocol ?? .ssh).displayName + ).swiftUIKey, + systemImage: launch.kind.symbol + ) { openVerifiedDirectory(launch) } case .telnet: diff --git a/TFITests/MobileAppTests.swift b/TFITests/MobileAppTests.swift index c4eed964..36a7f77a 100644 --- a/TFITests/MobileAppTests.swift +++ b/TFITests/MobileAppTests.swift @@ -345,13 +345,18 @@ struct MobileWorkspaceTests { @Suite @MainActor struct MobileWorkspaceConnectionRecoveryTests { - @Test func sameDirectorySFTPStartupUsesTheOverrideAndOffersSSHOnlyWhileConnected() async throws { + @Test(arguments: [HostTerminalProtocol.ssh, .mosh, .eternalTerminal]) + func sameDirectorySFTPUsesTheDefaultTransportOnlyWhileConnected(_ transport: HostTerminalProtocol) async throws { let connections = MobileSFTPConnectionBox(clients: [MobileTestSFTPClient()]) let app = try MobileAppModel( inMemory: true, workspaceConnectionServices: connections.services ) - let host = try configureConnectionFixture(in: app) + var host = try configureConnectionFixture(in: app) + host.terminalTransports.moshEnabled = true + host.terminalTransports.eternalTerminalEnabled = true + host.terminalTransports.defaultTransport = transport + app.library.upsert(host) let id = try #require( app.testWorkspaceSession.open(hostID: host.id, kind: .sftp, initialPath: "/srv/releases/")) let workspace = try #require(app.workspaces.first(where: { $0.id == id })) @@ -362,7 +367,8 @@ struct MobileWorkspaceConnectionRecoveryTests { #expect(workspace.sftpChannel.path == "/srv/releases") #expect( workspace.sameDirectoryLaunch - == MobileWorkspaceDirectoryLaunch(hostID: host.id, kind: .ssh, path: "/srv/releases") + == MobileWorkspaceDirectoryLaunch( + hostID: host.id, kind: .terminal(transport), path: "/srv/releases") ) #expect(app.testWorkspaceSession.renameWorkspace(id, to: " Release Files ")) #expect(workspace.descriptor.title == "Release Files") diff --git a/TFM/Features/SFTP/SFTPBrowserView.swift b/TFM/Features/SFTP/SFTPBrowserView.swift index 1de0b517..12467657 100644 --- a/TFM/Features/SFTP/SFTPBrowserView.swift +++ b/TFM/Features/SFTP/SFTPBrowserView.swift @@ -505,7 +505,8 @@ struct SFTPBrowserView: View { } private func actionsMenu(layout: Layout) -> some View { - Menu { + let transport = app.hostLibrary.host(sftpChannel.hostID)?.preferredTerminalProtocol ?? .ssh + return Menu { Button(AppStrings.SFTP.newFolder.swiftUIKey, systemImage: MenuSymbol.newFolder) { newFolderPresented = true } @@ -513,7 +514,10 @@ struct SFTPBrowserView: View { Button(AppStrings.Common.paste.swiftUIKey, systemImage: MenuSymbol.paste) { pasteRemoteItems() } .disabled(sftpChannel.status != .connected || !canPasteRemoteItems) Divider() - Menu(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: MenuSymbol.ssh) { + Menu( + AppStrings.Connections.openWithTransport(transport.displayName).swiftUIKey, + systemImage: transport.symbol + ) { Button(AppStrings.Workspace.inCurrentWorkspace.swiftUIKey, systemImage: MenuSymbol.currentWorkspace) { window.openCompanionSession(from: sftpChannel.id, placement: .currentWorkspace) } diff --git a/TFM/Features/StatusBar/StatusBarView.swift b/TFM/Features/StatusBar/StatusBarView.swift index 91e246cb..de42ec7d 100644 --- a/TFM/Features/StatusBar/StatusBarView.swift +++ b/TFM/Features/StatusBar/StatusBarView.swift @@ -301,20 +301,25 @@ struct StatusBarView: View { } private func sftpDirectoryButton(_ path: String, displayed: String) -> some View { - Button { + let transport = host?.preferredTerminalProtocol ?? .ssh + return Button { app.onboarding.recordUse(of: .statusBarPath) openInSSH(path, placement: .currentWorkspace) } label: { StatusBarPathLabel( - symbol: "apple.terminal", text: displayed, destinationSymbol: "arrow.up.right" + symbol: transport.symbol, text: displayed, destinationSymbol: "arrow.up.right" ) } .buttonStyle(.plain) - .help(Text(AppStrings.Workspace.openDirectoryWithSSHHelp)) - .accessibilityLabel(Text(AppStrings.Connections.openWithSsh)) + .disabled(host?.sshEnabled != true) + .help(Text(AppStrings.Workspace.openDirectoryWithTransportHelp(transport.displayName))) + .accessibilityLabel(Text(AppStrings.Connections.openWithTransport(transport.displayName))) .onDrag { TerminalPathDrag.provider(for: path) } .contextMenu { - Menu(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: MenuSymbol.ssh) { + Menu( + AppStrings.Connections.openWithTransport(transport.displayName).swiftUIKey, + systemImage: transport.symbol + ) { Button(AppStrings.Workspace.inCurrentWorkspace.swiftUIKey, systemImage: MenuSymbol.currentWorkspace) { app.onboarding.recordUse(of: .statusBarPath) openInSSH(path, placement: .currentWorkspace) @@ -355,8 +360,8 @@ struct StatusBarView: View { } private func openInSSH(_ path: String, placement: SessionOpenPlacement) { - guard let host else { return } - window.openSSHSession(host: host, initialPath: path, placement: placement) + guard let host, host.sshEnabled, let transport = host.preferredTerminalProtocol else { return } + window.openSSHSession(host: host, transport: transport, initialPath: path, placement: placement) } } diff --git a/TFM/Features/Workspaces/WindowModel.swift b/TFM/Features/Workspaces/WindowModel.swift index f1d9cb3a..d9100e35 100644 --- a/TFM/Features/Workspaces/WindowModel.swift +++ b/TFM/Features/Workspaces/WindowModel.swift @@ -860,7 +860,8 @@ final class WindowModel { } func canOpenCompanionSession(from sessionID: UUID) -> Bool { - companionSessionSource(sessionID) != nil + guard let source = companionSessionSource(sessionID) else { return false } + return source.pane.sshSession != nil || source.host.sshEnabled } /// Resolve the owning pane again at dispatch, not the pane that happened to @@ -892,7 +893,8 @@ final class WindowModel { if source.pane.sshSession != nil { return openSFTPChannel(host: source.host, initialPath: source.path, placement: placement) } - return openSSHSession(host: source.host, initialPath: source.path, placement: placement) + guard source.host.sshEnabled, let transport = source.host.preferredTerminalProtocol else { return nil } + return openSSHSession(host: source.host, transport: transport, initialPath: source.path, placement: placement) } private func companionSessionSource(_ sessionID: UUID) -> ( diff --git a/TFMTests/WindowStateTests.swift b/TFMTests/WindowStateTests.swift index 8757c607..15fede07 100644 --- a/TFMTests/WindowStateTests.swift +++ b/TFMTests/WindowStateTests.swift @@ -1454,6 +1454,39 @@ struct WindowStateTests { #expect(sftpWorkspace.focusedPane.sshSession?.hostID == host.id) } + @Test + func sftpCompanionUsesTheHostDefaultWithoutChangingExplicitSSH() throws { + for transport in [HostTerminalProtocol.ssh, .mosh, .eternalTerminal] { + var host = Host(name: "Shell", host: "shell.example") + host.terminalTransports.moshEnabled = true + host.terminalTransports.eternalTerminalEnabled = true + host.terminalTransports.defaultTransport = transport + let channel = SFTPChannelModel.demo(hostID: host.id, path: "/srv/app") + let workspace = Workspace(pane: Pane(.sftpChannel(channel))) + let window = WindowModel(workspaces: [workspace]) + let library = HostLibraryModel(hosts: [host]) + window.hostStore = library + + #expect(window.canOpenCompanionSession(from: channel.id)) + let companion = try #require( + window.openCompanionSession(from: channel.id, placement: .newWorkspace)) + #expect(companion.focusedPane.sshSession?.transport == transport) + #expect(window.openSSHSession(host: host).focusedPane.sshSession?.transport == .ssh) + + window.select(workspace.id) + let split = try #require( + window.openCompanionSession(from: channel.id, placement: .currentWorkspace)) + #expect(split === workspace) + #expect(split.focusedPane.sshSession?.transport == transport) + + host.sshEnabled = false + host.telnetEnabled = true + #expect(library.saveHost(host)) + #expect(!window.canOpenCompanionSession(from: channel.id)) + #expect(window.openCompanionSession(from: channel.id, placement: .newWorkspace) == nil) + } + } + @Test func sftpWithoutAnOverrideKeepsTheHostsConfiguredInitialPath() { var host = Host(name: "Files", host: "files.example")