From 00b95ca51fbff12e04cd0adf83dd9337c9c50c49 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Thu, 16 Jul 2026 17:21:35 +0800 Subject: [PATCH 1/3] gterm: support collapsing and expanding the on-screen keyboard Add a hide-keyboard button fixed at the trailing edge of the accessory bar (always reachable regardless of scroll position) and a keyboard toggle in the terminal status bar. Collapsing resigns first responder, dismissing both the software keyboard and the accessory bar; tapping the terminal or the status-bar button expands it again. Co-Authored-By: Claude Fable 5 --- Sources/Ghostty/AccessoryKeyboardView.swift | 30 +++++++++++++++++++++ Sources/Ghostty/TerminalSurfaceView.swift | 20 ++++++++++++-- Sources/UI/TerminalScreen.swift | 4 +++ 3 files changed, 52 insertions(+), 2 deletions(-) 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..64c310a 100644 --- a/Sources/UI/TerminalScreen.swift +++ b/Sources/UI/TerminalScreen.swift @@ -133,6 +133,10 @@ struct TerminalScreen: View { .font(.subheadline.weight(.medium)) .lineLimit(1) Spacer() + Button { terminalView?.toggleKeyboard() } label: { + Image(systemName: "keyboard").font(.body.weight(.semibold)) + } + .accessibilityLabel("Toggle keyboard") Button { showingAICommands = true } label: { Image(systemName: "sparkles").font(.body.weight(.semibold)) } From bd5c9218ecff4a637dd8f045d64a83b0e1e524fe Mon Sep 17 00:00:00 2001 From: Max Lv Date: Fri, 17 Jul 2026 07:59:53 +0800 Subject: [PATCH 2/3] gterm: dismiss the terminal when the remote closes the session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the shell exits cleanly (e.g. typing exit), the PTY channel goes inactive and SSHSession reports .closed — auto-dismiss the terminal screen instead of leaving a dead surface up. Failures still keep the screen visible so the error message stays readable. Co-Authored-By: Claude Fable 5 --- Sources/UI/TerminalScreen.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/UI/TerminalScreen.swift b/Sources/UI/TerminalScreen.swift index 64c310a..7e6ccc1 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: 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( From 3cc2a130e18bb6a7f289501c2519ce7321a0680e Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sun, 16 Aug 2026 06:07:22 +0800 Subject: [PATCH 3/3] gterm: retarget close-on-exit and keyboard toggle to ActiveSession Rebase onto main left the old TerminalScreen locals (`state`, `terminalView`) in place; the screen now reads them from the session-owned surface. --- Sources/UI/TerminalScreen.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/UI/TerminalScreen.swift b/Sources/UI/TerminalScreen.swift index 7e6ccc1..810efcb 100644 --- a/Sources/UI/TerminalScreen.swift +++ b/Sources/UI/TerminalScreen.swift @@ -74,7 +74,7 @@ struct TerminalScreen: View { } .background(Color.black.ignoresSafeArea()) .preferredColorScheme(.dark) - .onChange(of: state) { _, newState in + .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() } @@ -138,7 +138,7 @@ struct TerminalScreen: View { .font(.subheadline.weight(.medium)) .lineLimit(1) Spacer() - Button { terminalView?.toggleKeyboard() } label: { + Button { session.surface.toggleKeyboard() } label: { Image(systemName: "keyboard").font(.body.weight(.semibold)) } .accessibilityLabel("Toggle keyboard")