import Foundation import Cocoa import SwiftUI import GhosttyKit class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate { override var windowNibName: NSNib.Name? { "Terminal" } /// The app instance that this terminal view will represent. let ghostty: Ghostty.AppState /// The currently focused surface. var focusedSurface: Ghostty.SurfaceView? = nil /// Fullscreen state management. private let fullscreenHandler = FullScreenHandler() init(_ ghostty: Ghostty.AppState) { self.ghostty = ghostty super.init(window: nil) let center = NotificationCenter.default center.addObserver( self, selector: #selector(onToggleFullscreen), name: Ghostty.Notification.ghosttyToggleFullscreen, object: nil) center.addObserver( self, selector: #selector(onGotoTab), name: Ghostty.Notification.ghosttyGotoTab, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } //MARK: - NSWindowController override func windowWillLoad() { // We want every new terminal window to cascade so they don't directly overlap. shouldCascadeWindows = true } override func windowDidLoad() { guard let window = window else { return } // Terminals typically operate in sRGB color space and macOS defaults // to "native" which is typically P3. There is a lot more resources // covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376 window.colorSpace = NSColorSpace.sRGB // Center the window to start, we'll move the window frame automatically // when cascading. window.center() // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, delegate: self )) } // Shows the "+" button in the tab bar, responds to that click. override func newWindowForTab(_ sender: Any?) { // Trigger the ghostty core event logic for a new tab. guard let surface = self.focusedSurface?.surface else { return } ghostty.newTab(surface: surface) } //MARK: - NSWindowDelegate func windowWillClose(_ notification: Notification) { } //MARK: - TerminalViewDelegate func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { self.focusedSurface = to } func titleDidChange(to: String) { self.window?.title = to } func cellSizeDidChange(to: NSSize) { guard ghostty.windowStepResize else { return } self.window?.contentResizeIncrements = to } //MARK: - Notifications @objc private func onGotoTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard target == self.focusedSurface else { return } guard let window = self.window else { return } // Get the tab index from the notification guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return } guard let tabIndex = tabIndexAny as? Int32 else { return } guard let windowController = window.windowController else { return } guard let tabGroup = windowController.window?.tabGroup else { return } let tabbedWindows = tabGroup.windows // This will be the index we want to actual go to let finalIndex: Int // An index that is invalid is used to signal some special values. if (tabIndex <= 0) { guard let selectedWindow = tabGroup.selectedWindow else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) { finalIndex = selectedIndex - 1 } else if (tabIndex == GHOSTTY_TAB_NEXT.rawValue) { finalIndex = selectedIndex + 1 } else { return } } else { // Tabs are 0-indexed here, so we subtract one from the key the user hit. finalIndex = Int(tabIndex - 1) } guard finalIndex >= 0 && finalIndex < tabbedWindows.count else { return } let targetWindow = tabbedWindows[finalIndex] targetWindow.makeKeyAndOrderFront(nil) } @objc private func onToggleFullscreen(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard target == self.focusedSurface else { return } // We need a window to fullscreen guard let window = self.window else { return } // Check whether we use non-native fullscreen guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return } self.fullscreenHandler.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) // For some reason focus always gets lost when we toggle fullscreen, so we set it back. if let focusedSurface { Ghostty.moveFocus(to: focusedSurface) } } }