mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: remove old primary window stuff
This commit is contained in:
@ -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 = "<group>"; };
|
||||
85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowController.swift; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindow.swift; sourceTree = "<group>"; };
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
||||
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowManager.swift; sourceTree = "<group>"; };
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; sourceTree = "<group>"; };
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||
@ -86,7 +79,6 @@
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -192,6 +171,7 @@
|
||||
A596309F2AEF6AEB00D64628 /* TerminalManager.swift */,
|
||||
A596309B2AEE1C9E00D64628 /* TerminalController.swift */,
|
||||
A596309D2AEE1D6C00D64628 /* TerminalView.swift */,
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||
);
|
||||
path = Terminal;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user