From b40245f01deb760233568912b3279359a34fec83 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 30 Oct 2023 14:36:20 -0700 Subject: [PATCH] macos: remove old primary window stuff --- macos/Ghostty.xcodeproj/project.pbxproj | 26 +- .../Features/Primary Window/PrimaryView.swift | 158 ------------- .../Primary Window/PrimaryWindow.swift | 65 ----- .../PrimaryWindowController.swift | 38 --- .../Primary Window/PrimaryWindowManager.swift | 223 ------------------ .../ErrorView.swift | 0 .../Terminal/TerminalController.swift | 12 +- macos/Sources/Ghostty/SurfaceView.swift | 7 +- 8 files changed, 14 insertions(+), 515 deletions(-) delete mode 100644 macos/Sources/Features/Primary Window/PrimaryView.swift delete mode 100644 macos/Sources/Features/Primary Window/PrimaryWindow.swift delete mode 100644 macos/Sources/Features/Primary Window/PrimaryWindowController.swift delete mode 100644 macos/Sources/Features/Primary Window/PrimaryWindowManager.swift rename macos/Sources/Features/{Primary Window => Terminal}/ErrorView.swift (100%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index f7a7cd625..012b1631d 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -8,12 +8,9 @@ /* Begin PBXBuildFile section */ 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; - 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; - 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */; }; A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; - A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */ = {isa = PBXBuildFile; fileRef = A545D1A12A5772CE006E0AE4 /* shell-integration */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; @@ -43,17 +40,13 @@ A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; - A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = ""; }; - 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowController.swift; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; - 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindow.swift; sourceTree = ""; }; A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = ""; }; A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowManager.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; @@ -86,7 +79,6 @@ A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -106,25 +98,12 @@ isa = PBXGroup; children = ( A56D58872ACDE6BE00508D2C /* Services */, - A53426372A7DC53A00EBB7A2 /* Primary Window */, A59630982AEE1C4400D64628 /* Terminal */, A534263E2A7DCC5800EBB7A2 /* Settings */, ); path = Features; sourceTree = ""; }; - A53426372A7DC53A00EBB7A2 /* Primary Window */ = { - isa = PBXGroup; - children = ( - A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */, - 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */, - 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */, - A5FECBD629D1FC3900022361 /* PrimaryView.swift */, - A535B9D9299C569B0017E2E4 /* ErrorView.swift */, - ); - path = "Primary Window"; - sourceTree = ""; - }; A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( @@ -192,6 +171,7 @@ A596309F2AEF6AEB00D64628 /* TerminalManager.swift */, A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, + A535B9D9299C569B0017E2E4 /* ErrorView.swift */, ); path = Terminal; sourceTree = ""; @@ -320,8 +300,6 @@ files = ( A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, - A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */, - 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, @@ -332,7 +310,6 @@ A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, - A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, @@ -342,7 +319,6 @@ A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, - 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift deleted file mode 100644 index f58a168f3..000000000 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ /dev/null @@ -1,158 +0,0 @@ -import SwiftUI -import GhosttyKit - -struct PrimaryView: View { - @ObservedObject var ghostty: Ghostty.AppState - - // We need access to our app delegate to know if we're quitting or not. - // Make sure to use `@ObservedObject` so we can keep track of `appDelegate.confirmQuit`. - @ObservedObject var appDelegate: AppDelegate - - // We need this to report back up the app controller which surface in this view is focused. - let focusedSurfaceWrapper: FocusedSurfaceWrapper - - // If this is set, this is the base configuration that we build our surface out of. - let baseConfig: Ghostty.SurfaceConfiguration? - - // We need access to our window to know if we're the key window and to - // modify window properties in response to events from the surface (e.g. - // updating the window title) - var window: NSWindow - - // This handles non-native fullscreen - @State private var fullScreen = FullScreenHandler() - - // This seems like a crutch after switching from SwiftUI to AppKit lifecycle. - @FocusState private var focused: Bool - - @FocusedValue(\.ghosttySurfaceView) private var focusedSurface - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle - @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 - } - } - - if let zoomedSplit = zoomedSplit { - if zoomedSplit { - title = "🔍 " + title - } - } - - return title - } - - var body: some View { - switch ghostty.readiness { - case .loading: - Text("Loading") - case .error: - ErrorView() - case .ready: - let center = NotificationCenter.default - let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab) - let toggleFullscreen = center.publisher(for: Ghostty.Notification.ghosttyToggleFullscreen) - - 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) { - DebugBuildWarningView() - } - - Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig) - .ghosttyApp(ghostty.app!) - .ghosttyConfig(ghostty.config!) - .onReceive(gotoTab) { onGotoTab(notification: $0) } - .onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) } - .focused($focused) - .onAppear { self.focused = true } - .onChange(of: focusedSurface) { newValue in - self.focusedSurfaceWrapper.surface = newValue?.surface - } - .onChange(of: title) { newValue in - // We need to handle this manually because we are using AppKit lifecycle - // so navigationTitle no longer works. - self.window.title = newValue - } - .onChange(of: cellSize) { newValue in - if !ghostty.windowStepResize { return } - guard let size = newValue else { return } - self.window.contentResizeIncrements = size - } - } - } - } - - static func closeWindow() { - guard let currentWindow = NSApp.keyWindow else { return } - currentWindow.close() - } - - private func onGotoTab(notification: SwiftUI.Notification) { - // Notification center indiscriminately sends to every subscriber (makes sense) - // but we only want to process this once. In order to process it once lets only - // handle it if we're the focused window. - guard self.window.isKeyWindow 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) - } - - private func onToggleFullscreen(notification: SwiftUI.Notification) { - // Just like in `onGotoTab`, we might receive this multiple times. But - // it's fine, because `toggleFullscreen` should only apply to the - // currently focused window. - guard self.window.isKeyWindow 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.fullScreen.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) - // After toggling fullscreen we need to focus the terminal again. - self.focused = true - - // For some reason focus always gets moved to the first split when - // toggling fullscreen, so we set it back to the correct one. - if let focusedSurface { - Ghostty.moveFocus(to: focusedSurface) - } - } -} diff --git a/macos/Sources/Features/Primary Window/PrimaryWindow.swift b/macos/Sources/Features/Primary Window/PrimaryWindow.swift deleted file mode 100644 index 7a8be5471..000000000 --- a/macos/Sources/Features/Primary Window/PrimaryWindow.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Cocoa -import SwiftUI -import GhosttyKit - -// FocusedSurfaceWrapper is here so that we can pass a reference down -// the view hierarchy and keep track of which surface is focused. -class FocusedSurfaceWrapper { - var surface: ghostty_surface_t? -} - -// PrimaryWindow is the primary window you'd associate with a terminal: the window -// that contains one or more terminals (splits, and such). -// -// We need to subclass NSWindow so that we can override some methods for features -// such as non-native fullscreen. -class PrimaryWindow: NSWindow { - var focusedSurfaceWrapper: FocusedSurfaceWrapper = FocusedSurfaceWrapper() - - override var canBecomeKey: Bool { - return true - } - - override var canBecomeMain: Bool { - return true - } - - static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: Ghostty.SurfaceConfiguration? = nil) -> PrimaryWindow { - let window = PrimaryWindow( - contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), - styleMask: getStyleMask(renderDecoration: ghostty.windowDecorations), - backing: .buffered, - defer: false) - window.center() - - // 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 - - window.contentView = NSHostingView(rootView: PrimaryView( - ghostty: ghostty, - appDelegate: appDelegate, - focusedSurfaceWrapper: window.focusedSurfaceWrapper, - baseConfig: baseConfig, - window: window - )) - - // We do want to cascade when new windows are created - window.windowController?.shouldCascadeWindows = true - - // A default title. This should be overwritten quickly by the Ghostty core. - window.title = "Ghostty 👻" - - return window - } - - static func getStyleMask(renderDecoration: Bool) -> NSWindow.StyleMask { - var mask: NSWindow.StyleMask = [.resizable, .closable, .miniaturizable] - if renderDecoration { - mask.insert(.titled) - } - - return mask - } -} diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowController.swift b/macos/Sources/Features/Primary Window/PrimaryWindowController.swift deleted file mode 100644 index 6add783e3..000000000 --- a/macos/Sources/Features/Primary Window/PrimaryWindowController.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Cocoa - -class PrimaryWindowController: NSWindowController, NSWindowDelegate { - // This is used to programmatically control tabs. - weak var windowManager: PrimaryWindowManager? - - // This should be set to true once a surface has been initialized once. - var didInitializeFromSurface: Bool = false - - // This is required for the "+" button to show up in the tab bar to add a - // new tab. - override func newWindowForTab(_ sender: Any?) { - guard let window = self.window as? PrimaryWindow else { preconditionFailure("Expected window to be loaded") } - guard let manager = self.windowManager else { return } - manager.triggerNewTab(for: window) - } - - deinit { - // I don't know if this is the right place, but because of WindowAccessor in our - // SwiftUI hierarchy, we have a reference cycle between view and window and windows - // are never freed. When the window is closed, the window controller is deinitialized, - // so we can use this opportunity detach the view from the window and break the cycle. - if let window = self.window { - window.contentView = nil - } - } - - func windowDidBecomeKey(_ notification: Notification) { - self.windowManager?.relabelTabs() - } - - func windowWillClose(_ notification: Notification) { - // Tabs must be relabeled when a window is closed because this event - // does not fire the "windowDidBecomeKey" event on the newly focused - // window - self.windowManager?.relabelTabs() - } -} diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift deleted file mode 100644 index eef81f449..000000000 --- a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift +++ /dev/null @@ -1,223 +0,0 @@ -import Cocoa -import Combine -import GhosttyKit -import SwiftUI - -// PrimaryWindowManager manages the windows and tabs in the primary window -// of the application. It keeps references to windows and cleans them up when -// they're cloned. -// -// If we ever have multiple tabbed window types we can make this generic but -// right now only our primary window is ever duplicated or tabbed so we're not -// doing that. -// -// It is based on the patterns presented in this blog post: -// https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/ -class PrimaryWindowManager { - struct ManagedWindow { - let windowController: NSWindowController - let window: NSWindow - let closePublisher: AnyCancellable - } - - // Keep track of the last point that our window was launched at so that new - // windows "cascade" over each other and don't just launch directly on top - // of each other. - static var lastCascadePoint = NSPoint(x: 0, y: 0) - - /// Returns the main window of the managed window stack. - /// Falls back the first element if no window is main. Note that this would - /// likely be an internal inconsistency we gracefully handle here. - var mainWindow: NSWindow? { - let mainManagedWindow = managedWindows - .first { $0.window.isMainWindow } - - return (mainManagedWindow ?? managedWindows.first) - .map { $0.window } - } - - private var ghostty: Ghostty.AppState - private var managedWindows: [ManagedWindow] = [] - - init(ghostty: Ghostty.AppState) { - self.ghostty = ghostty - - // Register self as observer for the NewTab/NewWindow notifications that - // are triggered via callback from Zig code. - let center = NotificationCenter.default; - center.addObserver( - self, - selector: #selector(onNewTab), - name: Ghostty.Notification.ghosttyNewTab, - object: nil) - center.addObserver( - self, - selector: #selector(onNewWindow), - name: Ghostty.Notification.ghosttyNewWindow, - object: nil) - } - - deinit { - // Clean up the observers. - let center = NotificationCenter.default; - center.removeObserver( - self, - name: Ghostty.Notification.ghosttyNewTab, - object: nil) - center.removeObserver( - self, - name: Ghostty.Notification.ghosttyNewWindow, - object: nil) - } - - /// Add the initial window for the application. This should only be called once from the AppDelegate. - func addInitialWindow() { - guard let controller = createWindowController() else { return } - controller.showWindow(self) - let result = addManagedWindow(windowController: controller) - if result == nil { - preconditionFailure("Failed to create initial window") - } - } - - func newWindow() { - if let window = mainWindow as? PrimaryWindow { - // If we already have a window, we go through Zig core code, which calls back into Swift. - self.triggerNewWindow(withParent: window) - } else { - self.addNewWindow() - } - } - - func triggerNewWindow(withParent window: PrimaryWindow) { - guard let surface = window.focusedSurfaceWrapper.surface else { return } - ghostty.newWindow(surface: surface) - } - - func addNewWindow(withBaseConfig config: Ghostty.SurfaceConfiguration? = nil) { - guard let controller = createWindowController(withBaseConfig: config) else { return } - - // For new windows, explicitly disallow tabbing with other windows. - // This overrides the value of userTabbingPreference. Rationale: - // Ghostty explicitly provides both "New Tab" and "New Window" - // functionality, so there's no reason to make "New Window" open in a - // tab. - controller.window?.tabbingMode = .disallowed; - - controller.showWindow(self) - guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } - newWindow.makeKeyAndOrderFront(nil) - } - - @objc private func onNewWindow(notification: SwiftUI.Notification) { - let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] - let config = configAny as? Ghostty.SurfaceConfiguration - - self.addNewWindow(withBaseConfig: config) - } - - // triggerNewTab tells the Zig core code to create a new tab, which then calls - // back into Swift code. - func triggerNewTab(for window: PrimaryWindow) { - guard let surface = window.focusedSurfaceWrapper.surface else { return } - ghostty.newTab(surface: surface) - } - - func newTab() { - if let window = mainWindow as? PrimaryWindow { - self.triggerNewTab(for: window) - } else { - self.addNewWindow() - } - } - - @objc private func onNewTab(notification: SwiftUI.Notification) { - guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard let window = surfaceView.window else { return } - - let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] - let config = configAny as? Ghostty.SurfaceConfiguration - - self.addNewTab(to: window, withBaseConfig: config) - } - - func addNewTab(to window: NSWindow, withBaseConfig config: Ghostty.SurfaceConfiguration? = nil) { - guard let controller = createWindowController(withBaseConfig: config, cascade: false) else { return } - guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } - window.addTabbedWindow(newWindow, ordered: .above) - newWindow.makeKeyAndOrderFront(nil) - } - - private func createWindowController(withBaseConfig config: Ghostty.SurfaceConfiguration? = nil, cascade: Bool = true) -> PrimaryWindowController? { - guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } - - let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, baseConfig: config) - if (cascade) { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) - } - - let controller = PrimaryWindowController(window: window) - controller.windowManager = self - return controller - } - - private func addManagedWindow(windowController: PrimaryWindowController) -> ManagedWindow? { - guard let window = windowController.window else { return nil } - - let pubClose = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: window) - .sink { notification in - guard let window = notification.object as? NSWindow else { return } - self.removeWindow(window: window) - } - - let managed = ManagedWindow(windowController: windowController, window: window, closePublisher: pubClose) - managedWindows.append(managed) - window.delegate = windowController - - return managed - } - - private func removeWindow(window: NSWindow) { - self.managedWindows.removeAll(where: { $0.window === window }) - - // If we remove a window, we reset the cascade point to the key window so that - // the next window cascade's from that one. - if let focusedWindow = NSApplication.shared.keyWindow { - // If we are NOT the focused window, then we are a tabbed window. If we - // are closing a tabbed window, we want to set the cascade point to be - // the next cascade point from this window. - if focusedWindow != window { - Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) - return - } - - // If we are the focused window, then we set the last cascade point to - // our own frame so that it shows up in the same spot. - let frame = focusedWindow.frame - Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY) - } - } - - /// Update the accessory view of each tab according to the keyboard - /// shortcut that activates it (if any). This is called when the key window - /// changes and when a window is closed. - func relabelTabs() { - guard let windows = self.mainWindow?.tabbedWindows else { return } - guard let cfg = ghostty.config else { return } - for (index, window) in windows.enumerated().prefix(9) { - let action = "goto_tab:\(index + 1)" - let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - guard let equiv = Ghostty.keyEquivalentLabel(key: trigger.key, mods: trigger.mods) else { - continue - } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.labelFont(ofSize: 0), - .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - let attributedString = NSAttributedString(string: " \(equiv) ", attributes: attributes) - let text = NSTextField(labelWithAttributedString: attributedString) - window.tab.accessoryView = text - } - } -} diff --git a/macos/Sources/Features/Primary Window/ErrorView.swift b/macos/Sources/Features/Terminal/ErrorView.swift similarity index 100% rename from macos/Sources/Features/Primary Window/ErrorView.swift rename to macos/Sources/Features/Terminal/ErrorView.swift diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index b462e97a5..00a57997e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -21,6 +21,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele /// Fullscreen state management. private let fullscreenHandler = FullScreenHandler() + /// The style mask to use for the new window + private var styleMask: NSWindow.StyleMask { + var mask: NSWindow.StyleMask = [.resizable, .closable, .miniaturizable] + if (ghostty.windowDecorations) { mask.insert(.titled) } + return mask + } + init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { self.ghostty = ghostty self.baseConfig = base @@ -59,11 +66,14 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele override func windowWillLoad() { // We want every new terminal window to cascade so they don't directly overlap. shouldCascadeWindows = true + + // TODO: The cascade is messed up with tabs. } override func windowDidLoad() { guard let window = window else { return } - + window.styleMask = self.styleMask + // 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 diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 5ad74a8b5..818520aa3 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -507,8 +507,8 @@ extension Ghostty { // If we have tabs, then do not change the window size guard let window = self.window else { return } guard let windowControllerRaw = window.windowController else { return } - guard let windowController = windowControllerRaw as? PrimaryWindowController else { return } - guard !windowController.didInitializeFromSurface else { return } + guard let windowController = windowControllerRaw as? TerminalController else { return } + guard case .noSplit = windowController.surfaceTree else { return } // Setup our frame. We need to first subtract the views frame so that we can // just get the chrome frame so that we only affect the surface view size. @@ -520,9 +520,6 @@ extension Ghostty { // We have no tabs and we are not a split, so set the initial size of the window. window.setFrame(frame, display: true) - - // Note that we did initialize - windowController.didInitializeFromSurface = true } override func becomeFirstResponder() -> Bool {