From fe9dc6e20fa43ebbab2dd09511a0ff5c9f2a1ea5 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sat, 12 Sep 2026 07:35:39 +0800 Subject: [PATCH] Add paste button to mobile terminal keyboard --- .../Terminal/MobileTerminalKeyboard.swift | 22 +++++++++++++++++-- .../Workspaces/MobileWorkspaceView.swift | 5 ++++- TFITests/MobileIconRenderingTests.swift | 10 +++++---- TFMTests/ArchitectureBoundaryTests.swift | 9 +++++++- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/TFI/Features/Terminal/MobileTerminalKeyboard.swift b/TFI/Features/Terminal/MobileTerminalKeyboard.swift index 8b976a9e..2e6bef8f 100644 --- a/TFI/Features/Terminal/MobileTerminalKeyboard.swift +++ b/TFI/Features/Terminal/MobileTerminalKeyboard.swift @@ -5,12 +5,17 @@ struct MobileTerminalKeyboard: View { static let barHeight: CGFloat = 44 @ObservedObject var state: TerminalViewState + let paste: () -> Void @State private var control: TerminalPublicStickyActivation = .inactive @State private var alt: TerminalPublicStickyActivation = .inactive @State private var shift = MobileTerminalShiftState() - init(state: TerminalViewState, initialShift: MobileTerminalShiftState = MobileTerminalShiftState()) { + init( + state: TerminalViewState, initialShift: MobileTerminalShiftState = MobileTerminalShiftState(), + paste: @escaping () -> Void + ) { self.state = state + self.paste = paste _shift = State(initialValue: initialShift) } @@ -37,7 +42,11 @@ struct MobileTerminalKeyboard: View { .frame(maxWidth: .infinity) Divider().padding(.vertical, 8) - keyboardToggle.padding(.horizontal, 8) + HStack(spacing: 0) { + pasteButton + keyboardToggle + } + .padding(.horizontal, 8) } .buttonStyle(.plain) .font(.system(.callout, design: .monospaced)) @@ -62,6 +71,15 @@ struct MobileTerminalKeyboard: View { .accessibilityAddTraits(shift.activation == .inactive ? [] : .isSelected) } + private var pasteButton: some View { + Button(action: paste) { + Label("Paste", systemImage: "doc.on.clipboard") + .labelStyle(.iconOnly) + .frame(width: 44, height: 44) + .contentShape(.rect) + } + } + private var keyboardToggle: some View { Button { if state.isFocused { diff --git a/TFI/Features/Workspaces/MobileWorkspaceView.swift b/TFI/Features/Workspaces/MobileWorkspaceView.swift index 8b7f979d..775ee82c 100644 --- a/TFI/Features/Workspaces/MobileWorkspaceView.swift +++ b/TFI/Features/Workspaces/MobileWorkspaceView.swift @@ -45,7 +45,10 @@ struct MobileWorkspaceView: View { workspace.terminal.applyPreferences(value) } .onTapGesture { workspace.terminal.requestFocus() } - MobileTerminalKeyboard(state: workspace.terminal) + MobileTerminalKeyboard(state: workspace.terminal) { + guard let text = UIPasteboard.general.string else { return } + workspace.insertTerminalText(text) + } }.opacity(showsJourney ? 0 : 1) .allowsHitTesting(!showsJourney).accessibilityHidden(showsJourney) if showsJourney { connectionJourney.background(.background) } diff --git a/TFITests/MobileIconRenderingTests.swift b/TFITests/MobileIconRenderingTests.swift index 1afd7782..572cc788 100644 --- a/TFITests/MobileIconRenderingTests.swift +++ b/TFITests/MobileIconRenderingTests.swift @@ -15,11 +15,12 @@ import struct TermindDomain.Host struct MobileIconRenderingTests { @Test func terminalKeyboardKeepsItsCompactHeight() { #expect(UIImage(systemName: "play.rectangle") != nil) + #expect(UIImage(systemName: "doc.on.clipboard") != nil) #expect(MobileTerminalKeyboard.barHeight == 44) for width in [CGFloat(320), 390, 430] { for dynamicTypeSize in [DynamicTypeSize.large, .accessibility3] { let controller = UIHostingController( - rootView: MobileTerminalKeyboard(state: TerminalViewState()) + rootView: MobileTerminalKeyboard(state: TerminalViewState(), paste: {}) .environment(\.dynamicTypeSize, dynamicTypeSize)) let size = controller.sizeThatFits(in: CGSize(width: width, height: 800)) #expect(abs(size.height - 44) < 0.5) @@ -126,7 +127,7 @@ struct MobileIconRenderingTests { try await capture("terminal-keyboard-\(suffix)", language: language, scheme: scheme) { VStack(spacing: 0) { Spacer() - MobileTerminalKeyboard(state: TerminalViewState()) + MobileTerminalKeyboard(state: TerminalViewState(), paste: {}) } .environment(\.locale, locale) } @@ -135,7 +136,8 @@ struct MobileIconRenderingTests { Spacer() MobileTerminalKeyboard( state: TerminalViewState(), - initialShift: MobileTerminalShiftState(activation: .locked)) + initialShift: MobileTerminalShiftState(activation: .locked), + paste: {}) } .environment(\.locale, locale) } @@ -170,7 +172,7 @@ struct MobileIconRenderingTests { try await capture("terminal-keyboard-large-type", language: language, scheme: .light) { VStack(spacing: 0) { Spacer() - MobileTerminalKeyboard(state: TerminalViewState()) + MobileTerminalKeyboard(state: TerminalViewState(), paste: {}) } .environment(\.locale, locale).environment(\.dynamicTypeSize, .accessibility3) } diff --git a/TFMTests/ArchitectureBoundaryTests.swift b/TFMTests/ArchitectureBoundaryTests.swift index 18f5fc10..0c719cbb 100644 --- a/TFMTests/ArchitectureBoundaryTests.swift +++ b/TFMTests/ArchitectureBoundaryTests.swift @@ -530,13 +530,20 @@ struct ArchitectureBoundaryTests { #expect(historyDivider.lowerBound < nextMatch.lowerBound) #expect(workspace.contains(".dropDestination(for: URL.self)")) #expect(workspace.contains(".toolbar(.hidden, for: .tabBar)")) + #expect(workspace.contains("guard let text = UIPasteboard.general.string else { return }")) + #expect(workspace.contains("workspace.insertTerminalText(text)")) #expect(root.contains("private func closeWorkspace(_ id: UUID)")) #expect(!workspace.contains("Circle().fill(workspace.connected")) let keyboard = try String( contentsOf: sourceRoot.appending(path: "../TFI/Features/Terminal/MobileTerminalKeyboard.swift"), encoding: .utf8) #expect(keyboard.contains("ScrollView(.horizontal")) - #expect(keyboard.contains("keyboardToggle.padding(.horizontal, 8)")) + let pasteButton = try #require(keyboard.range(of: "pasteButton")) + let keyboardToggle = try #require(keyboard.range(of: "keyboardToggle")) + #expect(pasteButton.lowerBound < keyboardToggle.lowerBound) + #expect(keyboard.contains("Button(action: paste)")) + #expect(keyboard.contains("Label(\"Paste\", systemImage: \"doc.on.clipboard\")")) + #expect(keyboard.contains(".padding(.horizontal, 8)")) #expect(keyboard.contains("static let barHeight: CGFloat = 44")) #expect(keyboard.contains(".buttonStyle(.plain)")) #expect(!keyboard.contains(".buttonStyle(.bordered)"))