diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 42479f0b3..bf73bc923 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -784,7 +784,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; @@ -953,7 +953,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty.debug; @@ -1006,7 +1006,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index 4af94491c..da1f7cf98 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -2,18 +2,18 @@ import SwiftUI @main struct Ghostty_iOSApp: App { - @StateObject private var ghostty_app = Ghostty.App() + @State private var ghostty_app = Ghostty.App() var body: some Scene { WindowGroup { iOS_GhosttyTerminal() - .environmentObject(ghostty_app) + .environment(ghostty_app) } } } struct iOS_GhosttyTerminal: View { - @EnvironmentObject private var ghostty_app: Ghostty.App + @Environment(Ghostty.App.self) private var ghostty_app: Ghostty.App var body: some View { ZStack { @@ -26,7 +26,7 @@ struct iOS_GhosttyTerminal: View { } struct iOS_GhosttyInitView: View { - @EnvironmentObject private var ghostty_app: Ghostty.App + @Environment(Ghostty.App.self) private var ghostty_app: Ghostty.App var body: some View { VStack { diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 8564bbb1e..e954bbf9e 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -4,8 +4,8 @@ import OSLog import Sparkle import GhosttyKit +@Observable class AppDelegate: NSObject, - ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, GhosttyAppDelegate @@ -99,7 +99,7 @@ class AppDelegate: NSObject, private var appearanceObserver: NSKeyValueObservation? = nil /// The custom app icon image that is currently in use. - @Published private(set) var appIcon: NSImage? = nil { + private(set) var appIcon: NSImage? = nil { didSet { NSApplication.shared.applicationIconImage = appIcon } diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index f999ce5ca..17d8e5468 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -12,7 +12,8 @@ import OSLog // it. You have to yield secure input on application deactivation (because // it'll affect other apps) and reacquire on reactivation, and every enable // needs to be balanced with a disable. -class SecureInput : ObservableObject { +@Observable +class SecureInput { static let shared = SecureInput() private static let logger = Logger( @@ -28,10 +29,11 @@ class SecureInput : ObservableObject { } // The scoped objects and whether they're currently in focus. + @ObservationIgnored private var scoped: [ObjectIdentifier: Bool] = [:] // This is set to true when we've successfully called EnableSecureInput. - @Published private(set) var enabled: Bool = false + private(set) var enabled: Bool = false // This is true if we want to enable secure input. We want to enable // secure input if its enabled globally or any of the scoped objects are diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsView.swift b/macos/Sources/Features/Settings/ConfigurationErrorsView.swift index 3ed84e3f0..dc8a2aa14 100644 --- a/macos/Sources/Features/Settings/ConfigurationErrorsView.swift +++ b/macos/Sources/Features/Settings/ConfigurationErrorsView.swift @@ -1,11 +1,11 @@ import SwiftUI -protocol ConfigurationErrorsViewModel: ObservableObject { +protocol ConfigurationErrorsViewModel: AnyObject, Observable { var errors: [String] { get set } } struct ConfigurationErrorsView: View { - @ObservedObject var model: ViewModel + var model: ViewModel var body: some View { VStack { diff --git a/macos/Sources/Features/Settings/SettingsView.swift b/macos/Sources/Features/Settings/SettingsView.swift index 82d24181a..a17e35458 100644 --- a/macos/Sources/Features/Settings/SettingsView.swift +++ b/macos/Sources/Features/Settings/SettingsView.swift @@ -2,7 +2,7 @@ import SwiftUI struct SettingsView: View { // We need access to our app delegate to know if we're quitting or not. - @EnvironmentObject private var appDelegate: AppDelegate + @Environment(AppDelegate.self) private var appDelegate: AppDelegate var body: some View { HStack { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 393c6ef4d..be1a6097b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -25,6 +25,7 @@ import GhosttyKit /// /// The primary idea of all the behaviors we don't implement here are that subclasses may not /// want these behaviors. +@Observable class BaseTerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, @@ -36,12 +37,13 @@ class BaseTerminalController: NSWindowController, let ghostty: Ghostty.App /// The currently focused surface. + @ObservationIgnored var focusedSurface: Ghostty.SurfaceView? = nil { didSet { syncFocusToSurfaceTree() } } /// The surface tree for this window. - @Published var surfaceTree: Ghostty.SplitNode? = nil { + var surfaceTree: Ghostty.SplitNode? = nil { didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 2da498e3a..0719d3e86 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -610,21 +610,22 @@ class TerminalController: BaseTerminalController { syncAppearance(focusedSurface.derivedConfig) // We also want to get notified of certain changes to update our appearance. - focusedSurface.$derivedConfig - .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } - .store(in: &surfaceAppearanceCancellables) - focusedSurface.$backgroundColor - .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } - .store(in: &surfaceAppearanceCancellables) + observeFocusedSurface(focusedSurface) } - - private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) { - guard let surface else { return } - DispatchQueue.main.async { [weak self, weak surface] in - guard let surface else { return } + + func observeFocusedSurface(_ focusedSurface: Ghostty.SurfaceView) { + /// Use Observable to observe properties that will invalidate the surface + /// appearance. + withObservationTracking { + _ = focusedSurface.derivedConfig + _ = focusedSurface.backgroundColor + } onChange: { [weak self] in guard let self else { return } - guard self.focusedSurface == surface else { return } - self.syncAppearance(surface.derivedConfig) + DispatchQueue.main.async { + guard self.focusedSurface == focusedSurface else { return } + self.syncAppearance(focusedSurface.derivedConfig) + self.observeFocusedSurface(focusedSurface) + } } } diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 15b504875..aa8ab7381 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -28,18 +28,18 @@ protocol TerminalViewDelegate: AnyObject { /// The view model is a required implementation for TerminalView callers. This contains /// the main state between the TerminalView caller and SwiftUI. This abstraction is what /// allows AppKit to own most of the data in SwiftUI. -protocol TerminalViewModel: ObservableObject { +protocol TerminalViewModel: AnyObject, Observable { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView - /// and children. This should be @Published. + /// and children. var surfaceTree: Ghostty.SplitNode? { get set } } /// The main terminal view. This terminal view supports splits. struct TerminalView: View { - @ObservedObject var ghostty: Ghostty.App + var ghostty: Ghostty.App // The required view model - @ObservedObject var viewModel: ViewModel + @Bindable var viewModel: ViewModel // An optional delegate to receive information about terminal changes. weak var delegate: (any TerminalViewDelegate)? = nil @@ -86,9 +86,9 @@ struct TerminalView: View { if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { DebugBuildWarningView() } - + Ghostty.TerminalSplit(node: $viewModel.surfaceTree) - .environmentObject(ghostty) + .environment(ghostty) .focused($focused) .onAppear { self.focused = true } .onChange(of: focusedSurface) { newValue in diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2d9822d6e..eebfaa850 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -13,7 +13,8 @@ protocol GhosttyAppDelegate: AnyObject { extension Ghostty { // IMPORTANT: THIS IS NOT DONE. // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS - class App: ObservableObject { + @Observable + class App { enum Readiness: String { case loading, error, ready } @@ -22,16 +23,16 @@ extension Ghostty { weak var delegate: GhosttyAppDelegate? /// The readiness value of the state. - @Published var readiness: Readiness = .loading + var readiness: Readiness = .loading /// The global app configuration. This defines the app level configuration plus any behavior /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some /// configuration (i.e. font size) from the previously focused window. This would override this. - @Published private(set) var config: Config + private(set) var config: Config /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { + var app: ghostty_app_t? = nil { didSet { guard let old = oldValue else { return } ghostty_app_free(old) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b6da07612..bd853f57e 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -3,7 +3,7 @@ import GhosttyKit extension Ghostty { /// Maps to a `ghostty_config_t` and the various operations on that. - class Config: ObservableObject { + class Config { // The underlying C pointer to the Ghostty config structure. This // should never be accessed directly. Any operations on this should // be called from the functions on this or another class. diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift index 899825d37..5c3e7a7de 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift @@ -195,9 +195,10 @@ extension Ghostty { } } - class Leaf: ObservableObject, Equatable, Hashable, Codable { + @Observable + class Leaf: Equatable, Hashable, Codable { let app: ghostty_app_t - @Published var surface: SurfaceView + var surface: SurfaceView weak var parent: SplitNode.Container? @@ -250,13 +251,14 @@ extension Ghostty { } } - class Container: ObservableObject, Equatable, Hashable, Codable { + @Observable + class Container: Equatable, Hashable, Codable { let app: ghostty_app_t let direction: SplitViewDirection - @Published var topLeft: SplitNode - @Published var bottomRight: SplitNode - @Published var split: CGFloat = 0.5 + var topLeft: SplitNode + var bottomRight: SplitNode + var split: CGFloat = 0.5 var resizeEvent: PassthroughSubject = .init() diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index cc3bef149..0207770ce 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -294,11 +294,11 @@ extension Ghostty { /// This represents a split view that is in the horizontal or vertical split state. private struct TerminalSplitContainer: View { - @EnvironmentObject var ghostty: Ghostty.App + @Environment(Ghostty.App.self) var ghostty: Ghostty.App let neighbors: SplitNode.Neighbors @Binding var node: SplitNode? - @StateObject var container: SplitNode.Container + @Bindable var container: SplitNode.Container var body: some View { SplitView( diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index b6147647e..32538ef62 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -6,10 +6,10 @@ import GhosttyKit extension Ghostty { /// InspectableSurface is a type of Surface view that allows an inspector to be attached. struct InspectableSurface: View { - @EnvironmentObject var ghostty: Ghostty.App + @Environment(Ghostty.App.self) var ghostty: Ghostty.App /// Same as SurfaceWrapper, see the doc comments there. - @ObservedObject var surfaceView: SurfaceView + var surfaceView: SurfaceView var isSplit: Bool = false // Maintain whether our view has focus or not diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 4abf87c7f..b1fd16bfd 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -5,7 +5,7 @@ import GhosttyKit extension Ghostty { /// Render a terminal for the active app in the environment. struct Terminal: View { - @EnvironmentObject private var ghostty: Ghostty.App + @Environment(Ghostty.App.self) private var ghostty: Ghostty.App @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? var body: some View { @@ -22,10 +22,10 @@ extension Ghostty { struct SurfaceForApp: View { let content: ((SurfaceView) -> Content) - @StateObject private var surfaceView: SurfaceView + @State private var surfaceView: SurfaceView init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) { - _surfaceView = StateObject(wrappedValue: SurfaceView(app)) + _surfaceView = State(wrappedValue: SurfaceView(app)) self.content = content } @@ -37,7 +37,7 @@ extension Ghostty { struct SurfaceWrapper: View { // The surface to create a view for. This must be created upstream. As long as this // remains the same, the surface that is being rendered remains the same. - @ObservedObject var surfaceView: SurfaceView + var surfaceView: SurfaceView // True if this surface is part of a split view. This is important to know so // we know whether to dim the surface out of focus. @@ -54,10 +54,15 @@ extension Ghostty { #if canImport(AppKit) // Observe SecureInput to detect when its enabled - @ObservedObject private var secureInput = SecureInput.shared + private var secureInput = SecureInput.shared #endif + + init(surfaceView: SurfaceView, isSplit: Bool = false) { + self.surfaceView = surfaceView + self.isSplit = isSplit + } - @EnvironmentObject private var ghostty: Ghostty.App + @Environment(Ghostty.App.self) private var ghostty: Ghostty.App var body: some View { let center = NotificationCenter.default diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 2cac4a0dd..84cb47b3a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -5,55 +5,56 @@ import GhosttyKit extension Ghostty { /// The NSView implementation for a terminal surface. - class SurfaceView: OSView, ObservableObject { + @Observable + class SurfaceView: OSView { /// Unique ID per surface let uuid: UUID // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go // to the app level and it is set from there. - @Published private(set) var title: String = "👻" + private(set) var title: String = "👻" // The current pwd of the surface as defined by the pty. This can be // changed with escape codes. - @Published var pwd: String? = nil + var pwd: String? = nil // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. // when the font size changes). This is used to allow windows to be // resized in discrete steps of a single cell. - @Published var cellSize: NSSize = .zero + var cellSize: NSSize = .zero // The health state of the surface. This currently only reflects the // renderer health. In the future we may want to make this an enum. - @Published var healthy: Bool = true + var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + var error: Error? = nil // The hovered URL string - @Published var hoverUrl: String? = nil + var hoverUrl: String? = nil // The currently active key sequence. The sequence is not active if this is empty. - @Published var keySequence: [Ghostty.KeyEquivalent] = [] + var keySequence: [Ghostty.KeyEquivalent] = [] // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + var focusInstant: ContinuousClock.Instant? = nil // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. - @Published var surfaceSize: ghostty_surface_size_s? = nil + var surfaceSize: ghostty_surface_size_s? = nil // Whether the pointer should be visible or not - @Published private(set) var pointerStyle: BackportPointerStyle = .default + private(set) var pointerStyle: BackportPointerStyle = .default /// The configuration derived from the Ghostty config so we don't need to rely on references. - @Published private(set) var derivedConfig: DerivedConfig + private(set) var derivedConfig: DerivedConfig /// The background color within the color palette of the surface. This is only set if it is /// dynamically updated. Otherwise, the background color is the default background color. - @Published private(set) var backgroundColor: Color? = nil + private(set) var backgroundColor: Color? = nil // An initial size to request for a window. This will only affect // then the view is moved to a new window. @@ -89,7 +90,7 @@ extension Ghostty { } // True if the inspector should be visible - @Published var inspectorVisible: Bool = false { + var inspectorVisible: Bool = false { didSet { if (oldValue && !inspectorVisible) { guard let surface = self.surface else { return } diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index 8ac08d0bd..12cb77330 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -3,37 +3,38 @@ import GhosttyKit extension Ghostty { /// The UIView implementation for a terminal surface. - class SurfaceView: UIView, ObservableObject { + @Observable + class SurfaceView: UIView { /// Unique ID per surface let uuid: UUID // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go // to the app level and it is set from there. - @Published var title: String = "👻" + var title: String = "👻" // The current pwd of the surface. - @Published var pwd: String? = nil + var pwd: String? = nil // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. // when the font size changes). This is used to allow windows to be // resized in discrete steps of a single cell. - @Published var cellSize: OSSize = .zero + var cellSize: OSSize = .zero // The health state of the surface. This currently only reflects the // renderer health. In the future we may want to make this an enum. - @Published var healthy: Bool = true + var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + var error: Error? = nil // The hovered URL - @Published var hoverUrl: String? = nil + var hoverUrl: String? = nil // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + var focusInstant: ContinuousClock.Instant? = nil // Returns sizing information for the surface. This is the raw C // structure because I'm lazy.