diff --git a/Sources/Ghostty/AccessoryKeyboardView.swift b/Sources/Ghostty/AccessoryKeyboardView.swift index 53fb7af..67a8e47 100644 --- a/Sources/Ghostty/AccessoryKeyboardView.swift +++ b/Sources/Ghostty/AccessoryKeyboardView.swift @@ -152,7 +152,37 @@ final class AccessoryKeyboardView: UIInputView { // MARK: Keys row + /// The scrollable key bar plus a fixed collapse button at the trailing + /// edge, so "hide keyboard" is always reachable regardless of scroll + /// position. private func buildKeysRow() -> UIView { + let row = UIStackView() + row.axis = .horizontal + row.spacing = 2 + row.alignment = .center + row.translatesAutoresizingMaskIntoConstraints = false + row.isLayoutMarginsRelativeArrangement = true + row.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 6) + row.addArrangedSubview(buildKeysScroll()) + row.addArrangedSubview(buildHideButton()) + return row + } + + private func buildHideButton() -> UIButton { + let button = UIButton(type: .system) + var config = UIButton.Configuration.plain() + config.image = UIImage(systemName: "keyboard.chevron.compact.down") + config.baseForegroundColor = .label + config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8) + button.configuration = config + button.accessibilityLabel = "Hide keyboard" + button.addAction(UIAction { [weak self] _ in + self?.target?.collapseKeyboard() + }, for: .touchUpInside) + return button + } + + private func buildKeysScroll() -> UIView { let scroll = UIScrollView() scroll.translatesAutoresizingMaskIntoConstraints = false scroll.showsHorizontalScrollIndicator = false diff --git a/Sources/Ghostty/TerminalSurfaceView.swift b/Sources/Ghostty/TerminalSurfaceView.swift index be15788..54719f2 100644 --- a/Sources/Ghostty/TerminalSurfaceView.swift +++ b/Sources/Ghostty/TerminalSurfaceView.swift @@ -203,8 +203,8 @@ final class TerminalSurfaceView: UIView { @objc private func keyboardWillHide() { keyboardVisible = false } /// Bring the software keyboard back if the user dismissed it (e.g. with the - /// iPadOS keyboard hide key). Tapping the terminal is the only affordance - /// for getting it back, so the single-tap gesture calls this. + /// iPadOS keyboard hide key, or the accessory bar's collapse button). The + /// single-tap gesture calls this so tapping the terminal expands it again. func showKeyboardIfNeeded() { if !isFirstResponder { _ = becomeFirstResponder() @@ -216,6 +216,22 @@ final class TerminalSurfaceView: UIView { } } + /// Collapse the on-screen keyboard (and the accessory bar with it), giving + /// the whole screen to the terminal. Tapping the terminal or the status-bar + /// keyboard button expands it again. + func collapseKeyboard() { + _ = resignFirstResponder() + } + + /// Toggle the on-screen keyboard: collapse when visible, expand otherwise. + func toggleKeyboard() { + if isFirstResponder && keyboardVisible { + collapseKeyboard() + } else { + showKeyboardIfNeeded() + } + } + // MARK: Font size (pinch-to-zoom) /// Perform a libghostty keybind action by name (e.g. "increase_font_size:1"). diff --git a/Sources/UI/TerminalScreen.swift b/Sources/UI/TerminalScreen.swift index 6486b37..810efcb 100644 --- a/Sources/UI/TerminalScreen.swift +++ b/Sources/UI/TerminalScreen.swift @@ -74,6 +74,11 @@ struct TerminalScreen: View { } .background(Color.black.ignoresSafeArea()) .preferredColorScheme(.dark) + .onChange(of: session.state) { _, newState in + // A clean remote close (e.g. `exit` in the shell) dismisses the + // terminal; failures keep the screen up so the error stays readable. + if newState == .closed { onClose() } + } .alert( session.hostKeyRequest?.prompt.kind == .changed ? "Host Key Changed" : "Unknown Host", isPresented: Binding( @@ -133,6 +138,10 @@ struct TerminalScreen: View { .font(.subheadline.weight(.medium)) .lineLimit(1) Spacer() + Button { session.surface.toggleKeyboard() } label: { + Image(systemName: "keyboard").font(.body.weight(.semibold)) + } + .accessibilityLabel("Toggle keyboard") Button { showingAICommands = true } label: { Image(systemName: "sparkles").font(.body.weight(.semibold)) }