mirror of
				https://github.com/ghostty-org/ghostty.git
				synced 2025-11-04 09:44:23 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			153 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			153 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
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<ViewModel: TerminalViewModel>: 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 {
 | 
						|
        var title = "👻"
 | 
						|
 | 
						|
        if let surfaceTitle = surfaceTitle {
 | 
						|
            if (surfaceTitle.count > 0) {
 | 
						|
                title = surfaceTitle
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return title
 | 
						|
    }
 | 
						|
 | 
						|
    // The pwd of the focused surface as a URL
 | 
						|
    private var pwdURL: URL? {
 | 
						|
        guard let 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
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |