import SwiftUI import GhosttyKit /// This delegate is notified of actions and property changes regarding the terminal view. This /// delegate is optional and can be used by a TerminalView caller to react to changes such as /// titles being set, cell sizes being changed, etc. protocol TerminalViewDelegate: AnyObject { /// Called when the currently focused surface changed. This can be nil. func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) /// The title of the terminal should change. func titleDidChange(to: String) /// The URL of the pwd should change. func pwdDidChange(to: URL?) /// The cell size changed. func cellSizeDidChange(to: NSSize) /// The surface tree did change in some way, i.e. a split was added, removed, etc. This is /// not called initially. func surfaceTreeDidChange() /// This is called when a split is zoomed. func zoomStateDidChange(to: Bool) } /// 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 { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// and children. This should be @Published. var surfaceTree: Ghostty.SplitNode? { get set } } /// The main terminal view. This terminal view supports splits. struct TerminalView: View { @ObservedObject var ghostty: Ghostty.App // The required view model @ObservedObject var viewModel: ViewModel // An optional delegate to receive information about terminal changes. weak var delegate: (any TerminalViewDelegate)? = nil // This seems like a crutch after switching from SwiftUI to AppKit lifecycle. @FocusState private var focused: Bool // Various state values sent back up from the currently focused terminals. @FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfacePwd) private var surfacePwd @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize // The title for our window private var title: String { if let surfaceTitle, !surfaceTitle.isEmpty { return surfaceTitle } return "👻" } // The pwd of the focused surface as a URL private var pwdURL: URL? { guard let surfacePwd, surfacePwd != "" else { return nil } return URL(fileURLWithPath: surfacePwd) } var body: some View { switch ghostty.readiness { case .loading: Text("Loading") case .error: ErrorView() case .ready: VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { DebugBuildWarningView() } Ghostty.TerminalSplit(node: $viewModel.surfaceTree) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } .onChange(of: focusedSurface) { newValue in self.delegate?.focusedSurfaceDidChange(to: newValue) } .onChange(of: title) { newValue in self.delegate?.titleDidChange(to: newValue) } .onChange(of: pwdURL) { newValue in self.delegate?.pwdDidChange(to: newValue) } .onChange(of: cellSize) { newValue in guard let size = newValue else { return } self.delegate?.cellSizeDidChange(to: size) } .onChange(of: viewModel.surfaceTree?.hashValue) { _ in // This is funky, but its the best way I could think of to detect // ANY CHANGE within the deeply nested surface tree -- detecting a change // in the hash value. self.delegate?.surfaceTreeDidChange() } .onChange(of: zoomedSplit) { newValue in self.delegate?.zoomStateDidChange(to: newValue ?? false) } } // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) } } } struct DebugBuildWarningView: View { @State private var isPopover = false var body: some View { HStack { Spacer() Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.yellow) Text("You're running a debug build of Ghostty! Performance will be degraded.") .padding(.all, 8) .popover(isPresented: $isPopover, arrowEdge: .bottom) { Text(""" Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development. """) .padding(.all) } Spacer() } .background(Color(.windowBackgroundColor)) .frame(maxWidth: .infinity) .onTapGesture { isPopover = true } } }