From 7b17c588c6a6235fcdb860e0a3570d09d62fcfa2 Mon Sep 17 00:00:00 2001 From: 1pitaph Date: Fri, 22 May 2026 07:55:33 +0800 Subject: [PATCH] Fix macOS Swift warnings --- .../Command Palette/CommandPalette.swift | 8 ++--- .../TerminalCommandPalette.swift | 2 +- .../Features/Terminal/TerminalView.swift | 6 ++-- .../Sources/Features/Update/UpdatePill.swift | 2 +- macos/Sources/Ghostty/Ghostty.Inspector.swift | 2 +- macos/Sources/Ghostty/Ghostty.Surface.swift | 2 +- macos/Sources/Ghostty/GhosttyPackage.swift | 4 --- .../Ghostty/Surface View/InspectorView.swift | 2 +- .../Ghostty/Surface View/SurfaceView.swift | 4 +-- .../Surface View/SurfaceView_AppKit.swift | 34 +++++++++++-------- macos/Sources/Helpers/Backport.swift | 33 ++++++++++++++++++ .../KeyboardShortcut+Extension.swift | 2 ++ 12 files changed, 68 insertions(+), 33 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index de440fdc3..326bd5cbb 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -136,7 +136,7 @@ struct CommandPaletteView: View { break } } - .onChange(of: query) { newValue in + .backport.onChange(of: query) { newValue in // If the user types a query then we want to make sure the first // value is selected. If the user clears the query and we were selecting // the first, we unset any selection. @@ -181,7 +181,7 @@ struct CommandPaletteView: View { .shadow(radius: 32, x: 0, y: 12) .padding() .environment(\.colorScheme, scheme) - .onChange(of: isPresented) { newValue in + .backport.onChange(of: isPresented) { newValue in if !newValue { // This is optional, since most of the time // there will be a delay before the next use. @@ -261,7 +261,7 @@ private struct CommandPaletteQuery: View { .frame(height: 48) .textFieldStyle(.plain) .focused($isTextFieldFocused) - .onChange(of: isTextFieldFocused) { focused in + .backport.onChange(of: isTextFieldFocused) { focused in if !focused { onEvent?(.exit) } @@ -322,7 +322,7 @@ private struct CommandTable: View { .padding(10) } .frame(maxHeight: 200) - .onChange(of: selectedIndex) { _ in + .backport.onChange(of: selectedIndex) { guard let selectedIndex, selectedIndex < options.count else { return } proxy.scrollTo( diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 09e369d4a..6968098aa 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -41,7 +41,7 @@ struct TerminalCommandPaletteView: View { } } } - .onChange(of: isPresented) { newValue in + .backport.onChange(of: isPresented) { newValue in // When the command palette disappears we need to send focus back to the // surface view we were overlaid on top of. There's probably a better way // to handle the first responder state here but I don't know it. diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index b6e1c637c..6da29e198 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -86,7 +86,7 @@ struct TerminalView: View { .ghosttyLastFocusedSurface(lastFocusedSurface) .focused($focused) .onAppear { self.focused = true } - .onChange(of: focusedSurface) { newValue in + .backport.onChange(of: focusedSurface) { newValue in // We want to keep track of our last focused surface so even if // we lose focus we keep this set to the last non-nil value. if newValue != nil { @@ -94,10 +94,10 @@ struct TerminalView: View { self.delegate?.focusedSurfaceDidChange(to: newValue) } } - .onChange(of: pwdURL) { newValue in + .backport.onChange(of: pwdURL) { newValue in self.delegate?.pwdDidChange(to: newValue) } - .onChange(of: cellSize) { newValue in + .backport.onChange(of: cellSize) { newValue in guard let size = newValue else { return } self.delegate?.cellSizeDidChange(to: size) } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index b14cde1ac..8eaa0c19c 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -21,7 +21,7 @@ struct UpdatePill: View { UpdatePopoverView(model: model) } .transition(.opacity.combined(with: .scale(scale: 0.95))) - .onChange(of: model.state) { newState in + .backport.onChange(of: model.state) { newState in resetTask?.cancel() if case .notFound(let notFound) = newState { resetTask = Task { [weak model] in diff --git a/macos/Sources/Ghostty/Ghostty.Inspector.swift b/macos/Sources/Ghostty/Ghostty.Inspector.swift index 79567bc4a..e1a12859c 100644 --- a/macos/Sources/Ghostty/Ghostty.Inspector.swift +++ b/macos/Sources/Ghostty/Ghostty.Inspector.swift @@ -5,7 +5,7 @@ extension Ghostty { /// Represents the inspector for a surface within Ghostty. /// /// Wraps a `ghostty_inspector_t` - final class Inspector: Sendable { + final class Inspector: @unchecked Sendable { private let inspector: ghostty_inspector_t /// Read the underlying C value for this inspector. This is unsafe because the value will be diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 440b1b373..c409a8be1 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -9,7 +9,7 @@ extension Ghostty { /// all over. /// /// Wraps a `ghostty_surface_t` - final class Surface: Sendable { + final class Surface: @unchecked Sendable { private let surface: ghostty_surface_t /// Read the underlying C value for this surface. This is unsafe because the value will be diff --git a/macos/Sources/Ghostty/GhosttyPackage.swift b/macos/Sources/Ghostty/GhosttyPackage.swift index 03211862f..904fc9b68 100644 --- a/macos/Sources/Ghostty/GhosttyPackage.swift +++ b/macos/Sources/Ghostty/GhosttyPackage.swift @@ -7,10 +7,6 @@ import GhosttyKit /// A command is fully self-contained so it is Sendable. extension ghostty_command_s: @unchecked @retroactive Sendable {} -/// A surface is sendable because it is just a reference type. Using the surface in parameters -/// may be unsafe but the value itself is safe to send across threads. -extension ghostty_surface_t: @unchecked @retroactive Sendable {} - extension Ghostty { // The user notification category identifier static let userNotificationCategory = "com.mitchellh.ghostty.userNotification" diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index e7320c782..8c503c2fd 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -39,7 +39,7 @@ extension Ghostty { } } .onReceive(pubInspector) { onControlInspector($0) } - .onChange(of: surfaceView.inspectorVisible) { inspectorVisible in + .backport.onChange(of: surfaceView.inspectorVisible) { inspectorVisible in // When we show the inspector, we want to focus on the inspector. // When we hide the inspector, we want to move focus back to the surface. if inspectorVisible { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 4b90a3016..4d810beb7 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -937,7 +937,7 @@ extension Ghostty { .opacity(dotOpacity(for: index)) } } - .onChange(of: context.date.timeIntervalSinceReferenceDate) { newValue in + .backport.onChange(of: context.date.timeIntervalSinceReferenceDate) { newValue in animationPhase = newValue } } @@ -1011,7 +1011,7 @@ extension Ghostty { .allowsHitTesting(false) .opacity(highlighted ? 1.0 : 0.0) .animation(.easeOut(duration: 0.4), value: highlighted) - .onChange(of: highlighted) { newValue in + .backport.onChange(of: highlighted) { newValue in if newValue { withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true)) { borderPulse = true diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index b1920f170..df05e05c5 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1726,27 +1726,31 @@ extension Ghostty { trigger: nil ) - // Note the callback may be executed on a background thread as documented - // so we need @MainActor since we're reading/writing view state. - UNUserNotificationCenter.current().add(request) { @MainActor error in + // Note the callback may be executed on a background thread as documented, + // so view state is updated from an explicit main-actor task below. + UNUserNotificationCenter.current().add(request) { [weak self, uuid] error in if let error = error { AppDelegate.logger.error("Error scheduling user notification: \(error)") return } - // We need to keep track of this notification so we can remove it - // under certain circumstances - self.notificationIdentifiers.insert(uuid) + Task { @MainActor [weak self, uuid] in + guard let self else { return } - // If we're focused then we schedule to remove the notification - // after a few seconds. If we gain focus we automatically remove it - // in focusDidChange. - if self.focused { - Task { @MainActor [weak self] in - try await Task.sleep(for: .seconds(3)) - self?.notificationIdentifiers.remove(uuid) - UNUserNotificationCenter.current() - .removeDeliveredNotifications(withIdentifiers: [uuid]) + // We need to keep track of this notification so we can remove it + // under certain circumstances. + self.notificationIdentifiers.insert(uuid) + + // If we're focused then we schedule to remove the notification + // after a few seconds. If we gain focus we automatically remove it + // in focusDidChange. + if self.focused { + Task { @MainActor [weak self, uuid] in + try await Task.sleep(for: .seconds(3)) + self?.notificationIdentifiers.remove(uuid) + UNUserNotificationCenter.current() + .removeDeliveredNotifications(withIdentifiers: [uuid]) + } } } } diff --git a/macos/Sources/Helpers/Backport.swift b/macos/Sources/Helpers/Backport.swift index e2afb5b0c..3c778a4a8 100644 --- a/macos/Sources/Helpers/Backport.swift +++ b/macos/Sources/Helpers/Backport.swift @@ -25,6 +25,39 @@ enum BackportKeyPressResult { } extension Backport where Content: View { + /// Backported onChange that uses the modern two-parameter closure on + /// platforms that provide it while preserving macOS 13 support. + func onChange( + of value: Value, + _ action: @escaping (Value) -> Void + ) -> some View { + if #available(iOS 17, macOS 14, *) { + return content.onChange(of: value) { _, newValue in + action(newValue) + } + } else { + return content.onChange(of: value) { newValue in + action(newValue) + } + } + } + + /// Backported onChange variant for handlers that do not need the new value. + func onChange( + of value: Value, + _ action: @escaping () -> Void + ) -> some View { + if #available(iOS 17, macOS 14, *) { + return content.onChange(of: value) { _, _ in + action() + } + } else { + return content.onChange(of: value) { _ in + action() + } + } + } + func pointerVisibility(_ v: BackportVisibility) -> some View { #if canImport(AppKit) if #available(macOS 15, *) { diff --git a/macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift index 4c1b8dbcc..4b1f47da4 100644 --- a/macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift +++ b/macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift @@ -47,6 +47,8 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { } // This is available in macOS 14 so this only applies to early macOS versions. +@available(macOS, introduced: 10.15, obsoleted: 14.0) +@available(iOS, introduced: 13.0, obsoleted: 17.0) extension KeyEquivalent: @retroactive Equatable { public static func == (lhs: KeyEquivalent, rhs: KeyEquivalent) -> Bool { lhs.character == rhs.character