From 32920b6b2a5b92e20b70376f3c8e9f956648816f Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:46:00 +0100 Subject: [PATCH 1/2] macOS: handle surface focus more gracefully This will fix surface focus state is not consistent with first responder state when the search bar is open --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 ++--- macos/Sources/Helpers/Extensions/NSView+Extension.swift | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index d4b0ac080..5d9d5d527 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -301,9 +301,8 @@ class BaseTerminalController: NSWindowController, // Our focus state requires that this window is key and our currently // focused surface is the surface in this view. let focused: Bool = (window?.isKeyWindow ?? false) && - !commandPaletteIsShowing && - focusedSurface != nil && - surfaceView == focusedSurface! + surfaceView == focusedSurface && + surfaceView.isFirstResponder surfaceView.focusDidChange(focused) } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 2546caa38..6d055e5d4 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -14,6 +14,11 @@ extension NSView { return false } + + /// Returns true if this view is currently the first responder + var isFirstResponder: Bool { + window?.firstResponder === self + } } // MARK: Screenshot From 013579cfcf57aabf4fe599db2926efc3d1b8c01c Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:19:45 +0200 Subject: [PATCH 2/2] macOS: fix initial focus of command palette when building with Xcode 26.4 Tip works fine, but I've tried release and debug build with Xcode 26.4, it failed to focus as expected --- .../Command Palette/CommandPalette.swift | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 10c56f8dd..c4ba7106c 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -64,7 +64,6 @@ struct CommandPaletteView: View { @State private var query = "" @State private var selectedIndex: UInt? @State private var hoveredOptionID: UUID? - @FocusState private var isTextFieldFocused: Bool // The options that we should show, taking into account any filtering from // the query. Options with matching leadingColor are ranked higher. @@ -105,7 +104,7 @@ struct CommandPaletteView: View { } VStack(alignment: .leading, spacing: 0) { - CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in + CommandPaletteQuery(query: $query) { event in switch event { case .exit: isPresented = false @@ -178,27 +177,13 @@ struct CommandPaletteView: View { .padding() .environment(\.colorScheme, scheme) .onChange(of: isPresented) { newValue in - // Reset focus when quickly showing and hiding. - // macOS will destroy this view after a while, - // so task/onAppear will not be called again. - // If you toggle it rather quickly, we reset - // it here when dismissing. - isTextFieldFocused = newValue - if !isPresented { + if !newValue { // This is optional, since most of the time // there will be a delay before the next use. // To keep behavior the same as before, we reset it. query = "" } } - .task { - // Grab focus on the first appearance. - // This happens right after onAppear, - // so we don’t need to dispatch it again. - // Fixes: https://github.com/ghostty-org/ghostty/issues/8497 - // Also fixes initial focus while animating. - isTextFieldFocused = isPresented - } } /// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name. @@ -234,10 +219,9 @@ private struct CommandPaletteQuery: View { var onEvent: ((KeyboardEvent) -> Void)? @FocusState private var isTextFieldFocused: Bool - init(query: Binding, isTextFieldFocused: FocusState, onEvent: ((KeyboardEvent) -> Void)? = nil) { + init(query: Binding, onEvent: ((KeyboardEvent) -> Void)? = nil) { _query = query self.onEvent = onEvent - _isTextFieldFocused = isTextFieldFocused } enum KeyboardEvent { @@ -280,6 +264,17 @@ private struct CommandPaletteQuery: View { .onExitCommand { onEvent?(.exit) } .onMoveCommand { onEvent?(.move($0)) } .onSubmit { onEvent?(.submit) } + .onAppear { + // Grab focus on the first appearance. + // Debug and Release build using Xcode 26.4, + // has same issue again + // Fixes: https://github.com/ghostty-org/ghostty/issues/8497 + // SearchOverlay works magically as expected, I don't know + // why it's different here, but dispatching to next loop fixes it + DispatchQueue.main.async { + isTextFieldFocused = true + } + } } } }