diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 8ad57c4a5..94ced4890 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -388,6 +388,13 @@ + + + + + + + diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d7ddb793e..b06c19d34 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -27,6 +27,9 @@ class TerminalController: BaseTerminalController { /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] + /// This will be set to the initial frame of the window from the xib on load. + private var initialFrame: NSRect? = nil + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil @@ -308,6 +311,55 @@ class TerminalController: BaseTerminalController { y: frame.maxY - (CGFloat(y) + window.frame.height))) } + /// Returns the default size of the window. This is contextual based on the focused surface because + /// the focused surface may specify a different default size than others. + private var defaultSize: NSRect? { + guard let screen = window?.screen ?? NSScreen.main else { return nil } + + if derivedConfig.maximize { + return screen.visibleFrame + } else if let focusedSurface, + let initialSize = focusedSurface.initialSize { + // Get the current frame of the window + guard var frame = window?.frame else { return nil } + + // Calculate the chrome size (window size minus view size) + let chromeWidth = frame.size.width - focusedSurface.frame.size.width + let chromeHeight = frame.size.height - focusedSurface.frame.size.height + + // Calculate the new width and height, clamping to the screen's size + let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width) + let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height) + + // Update the frame size while keeping the window's position intact + frame.size.width = newWidth + frame.size.height = newHeight + + // Ensure the window doesn't go outside the screen boundaries + frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth)) + frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight)) + + return frame + } + + guard let initialFrame else { return nil } + guard var frame = window?.frame else { return nil } + + // Calculate the new width and height, clamping to the screen's size + let newWidth = min(initialFrame.size.width, screen.visibleFrame.width) + let newHeight = min(initialFrame.size.height, screen.visibleFrame.height) + + // Update the frame size while keeping the window's position intact + frame.size.width = newWidth + frame.size.height = newHeight + + // Ensure the window doesn't go outside the screen boundaries + frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth)) + frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight)) + + return frame + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -356,6 +408,9 @@ class TerminalController: BaseTerminalController { super.windowDidLoad() guard let window = window as? TerminalWindow else { return } + // Store our initial frame so we can know our default later. + initialFrame = window.frame + // I copy this because we may change the source in the future but also because // I regularly audit our codebase for "ghostty.config" access because generally // you shouldn't use it. Its safe in this case because for a new window we should @@ -372,36 +427,15 @@ class TerminalController: BaseTerminalController { // If window decorations are disabled, remove our title if (!config.windowDecorations) { window.styleMask.remove(.titled) } - // If we have only a single surface (no splits) and that surface requested - // an initial size then we set it here now. + // If we have only a single surface (no splits) and there is a default size then + // we should resize to that default size. if case let .leaf(leaf) = surfaceTree { - if config.maximize { - if let screen = window.screen ?? NSScreen.main { - window.setFrame(screen.visibleFrame, display: true) - } - } else if let initialSize = leaf.surface.initialSize, - let screen = window.screen ?? NSScreen.main { - // Get the current frame of the window - var frame = window.frame + // If this is our first surface then our focused surface will be nil + // so we force the focused surface to the leaf. + focusedSurface = leaf.surface - // Calculate the chrome size (window size minus view size) - let chromeWidth = frame.size.width - leaf.surface.frame.size.width - let chromeHeight = frame.size.height - leaf.surface.frame.size.height - - // Calculate the new width and height, clamping to the screen's size - let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width) - let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height) - - // Update the frame size while keeping the window's position intact - frame.size.width = newWidth - frame.size.height = newHeight - - // Ensure the window doesn't go outside the screen boundaries - frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth)) - frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight)) - - // Set the updated frame to the window - window.setFrame(frame, display: true) + if let defaultSize { + window.setFrame(defaultSize, display: true) } } @@ -578,6 +612,11 @@ class TerminalController: BaseTerminalController { window.close() } + @IBAction func returnToDefaultSize(_ sender: Any) { + guard let defaultSize else { return } + window?.setFrame(defaultSize, display: true) + } + @IBAction override func closeWindow(_ sender: Any?) { guard let window = window else { return } guard let tabGroup = window.tabGroup else { @@ -811,15 +850,55 @@ class TerminalController: BaseTerminalController { struct DerivedConfig { let backgroundColor: Color let macosTitlebarStyle: String + let maximize: Bool init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.macosTitlebarStyle = "system" + self.maximize = false } init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.macosTitlebarStyle = config.macosTitlebarStyle + self.maximize = config.maximize } } } + + +extension TerminalController: NSMenuItemValidation { + func validateMenuItem(_ item: NSMenuItem) -> Bool { + switch item.action { + case #selector(returnToDefaultSize): + guard let window else { return false } + + // Native fullscreen windows can't revert to default size. + if window.styleMask.contains(.fullScreen) { + return false + } + + // If we're fullscreen at all then we can't change size + if fullscreenStyle?.isFullscreen ?? false { + return false + } + + // If our window is already the default size or we don't have a + // default size, then disable. + guard let defaultSize, + window.frame.size != .init( + width: defaultSize.size.width, + height: defaultSize.size.height + ) + else { + return false + } + + return true + + default: + return true + } + } +} + diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index f30174b8e..10e5446cc 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -140,7 +140,7 @@ extension Ghostty { guard let ptr = v else { return "" } return String(cString: ptr) } - + var windowPositionX: Int16? { guard let config = self.config else { return nil } var v: Int16 = 0