From 49e4df78333ccdeb262e59d0f3c4de9d4b0bc7fd Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:19:21 +0200 Subject: [PATCH 1/2] macOS: rework for #12712 and #13645 --- .../Ghostty/Surface View/OSSurfaceView.swift | 51 +++++++------------ .../Ghostty/Surface View/SurfaceView.swift | 14 ++--- .../Surface View/SurfaceView_AppKit.swift | 1 + .../SurfaceView+SearchStateTests.swift | 31 ++++------- 4 files changed, 31 insertions(+), 66 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift index 2f559f2ae..5176a958b 100644 --- a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift @@ -117,18 +117,25 @@ extension Ghostty { extension Ghostty.OSSurfaceView { @MainActor class SearchState: ObservableObject { + + /// We should always change needle's text and its selection together + struct Needle: Equatable { + var text: String + var selection: Range? + + static let empty = Needle(text: "", selection: nil) + } + /// The pasteboard used to persist the search needle. /// /// The `.find` pasteboard lets us sync our needle across the system and other find bars. private let pasteboard: OSPasteboard - @Published private(set) var needle: String = "" + @Published var needle = Needle.empty + @Published var selected: UInt? @Published var total: UInt? - /// The range of the needle's text selection in the find bar. - @Published private(set) var needleSelection: Range? - init( from startSearch: Ghostty.Action.StartSearch, pasteboard: OSPasteboard = OSPasteboard.find @@ -144,45 +151,21 @@ extension Ghostty.OSSurfaceView { /// Replaces the search needle while keeping its selection valid. func setNeedle(_ needle: String, selectAll: Bool = false) { - if needle != self.needle { - // String.Index values are only valid for the string that created - // them, so publish a nil selection before changing the string. - needleSelection = nil - self.needle = needle - } - - if selectAll { - needleSelection = self.needle.startIndex..?) { - guard let selection else { - needleSelection = nil - return - } - - guard - let lowerBound = String.Index(selection.lowerBound, within: needle), - let upperBound = String.Index(selection.upperBound, within: needle) - else { - needleSelection = nil - return - } - - needleSelection = lowerBound.. AnyPublisher in if needle.isEmpty || needle.count >= 3 { diff --git a/macos/Tests/Ghostty/Surface View/SurfaceView+SearchStateTests.swift b/macos/Tests/Ghostty/Surface View/SurfaceView+SearchStateTests.swift index 95dab178a..609bc48f7 100644 --- a/macos/Tests/Ghostty/Surface View/SurfaceView+SearchStateTests.swift +++ b/macos/Tests/Ghostty/Surface View/SurfaceView+SearchStateTests.swift @@ -19,7 +19,7 @@ import Testing from: StartSearch(c: .init(needle: nil)), pasteboard: pasteboard ) - #expect(sut.needle == "pb") + #expect(sut.needle.text == "pb") } @Test func init_withEmptyNeedle_readsPasteboardNeedle() { @@ -28,7 +28,7 @@ import Testing from: StartSearch(c: .init(needle: needle)), pasteboard: pasteboard ) - #expect(sut.needle == "pb") + #expect(sut.needle.text == "pb") } } @@ -38,7 +38,7 @@ import Testing from: StartSearch(c: .init(needle: needle)), pasteboard: pasteboard ) - #expect(sut.needle == "start") + #expect(sut.needle.text == "start") } } @@ -67,25 +67,12 @@ import Testing from: StartSearch(c: .init(needle: nil)), pasteboard: pasteboard ) - sut.setNeedleSelection(sut.needle.startIndex.. Date: Mon, 10 Aug 2026 11:39:53 +0200 Subject: [PATCH 2/2] macOS: disable text selection on macOS 15 --- macos/Sources/Helpers/Backport.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Helpers/Backport.swift b/macos/Sources/Helpers/Backport.swift index 3dcbaa0c0..0b6c61ac1 100644 --- a/macos/Sources/Helpers/Backport.swift +++ b/macos/Sources/Helpers/Backport.swift @@ -132,8 +132,19 @@ enum BackportNSGlassStyle { #endif } -/// Backported `TextField` that supports text selection on macOS 15/iOS 18 and up. The `selection` -/// has no effect on versions below macOS 15/iOS 18. +/// Backported `TextField` that supports text selection on macOS 26/iOS 18 and up. The `selection` +/// has no effect on versions below macOS 26/iOS 18. +/// +/// Although the API is available from macOS 15, we force it to be 26. Because on macOS 15, +/// SwiftUI will crash when deleting texts, even for this simple example. +/// +/// struct ContentView: View { +/// @State private var text = "" +/// @State private var selection: TextSelection? +/// var body: some View { +/// TextField("Search", text: $text, selection: $selection) +/// } +/// } struct BackportSelectionTextField: View { private let titleKey: LocalizedStringKey @Binding private var text: String @@ -150,7 +161,7 @@ struct BackportSelectionTextField: View { } var body: some View { - if #available(iOS 18.0, macOS 15, *) { + if #available(iOS 18.0, macOS 26, *) { TextField( titleKey, text: _text,