From f73c1a2c59e4194d6162b92d640ebd8c29f459c1 Mon Sep 17 00:00:00 2001 From: Mikhail Borisov Date: Mon, 24 Feb 2025 21:13:55 +0100 Subject: [PATCH 1/3] "Return to Default Size" implementation Added support for "Return To Default Size" --- macos/Sources/App/macOS/AppDelegate.swift | 34 +++++++++++++++++++++++ macos/Sources/App/macOS/MainMenu.xib | 7 +++++ 2 files changed, 41 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7caa04659..ad48e24de 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -753,6 +753,16 @@ class AppDelegate: NSObject, quickController.toggle() } + @IBAction func returnToDefaultSize(_ sender: Any) { + guard let window = NSApp.keyWindow else { return } + let currentOrigin = window.frame.origin + let newFrame = NSRect( + origin: currentOrigin, + size: WindowConfig.defaultSize + ) + window.setFrame(newFrame, display: true) + } + /// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application @IBAction func toggleVisibility(_ sender: Any) { // If we have focus, then we hide all windows. @@ -804,6 +814,10 @@ class AppDelegate: NSObject, } } + private enum WindowConfig { + static let defaultSize = CGSize(width: 800, height: 600) + } + private struct ToggleVisibilityState { let hiddenWindows: [Weak] let keyWindow: Weak? @@ -833,3 +847,23 @@ class AppDelegate: NSObject, } } } + +extension AppDelegate: NSMenuItemValidation { + func validateMenuItem(_ item: NSMenuItem) -> Bool { + switch item.action { + case #selector(returnToDefaultSize): + guard let focusedWindow = NSApp.keyWindow else { + return false + } + if focusedWindow.styleMask.contains(.fullScreen) { + return false + } + if focusedWindow.frame.size == WindowConfig.defaultSize { + return false + } + return true + default: + return true + } + } +} 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 @@ + + + + + + + From 8838ebf02a3e40b1fbf4d06db361516620ef5ec4 Mon Sep 17 00:00:00 2001 From: Mikhail Borisov Date: Tue, 25 Feb 2025 21:47:33 +0100 Subject: [PATCH 2/3] Refactor to use height/width from ghostty configuration --- macos/Sources/App/macOS/AppDelegate.swift | 34 -------------- .../Terminal/TerminalController.swift | 46 +++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Config.swift | 16 ++++++- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index ad48e24de..7caa04659 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -753,16 +753,6 @@ class AppDelegate: NSObject, quickController.toggle() } - @IBAction func returnToDefaultSize(_ sender: Any) { - guard let window = NSApp.keyWindow else { return } - let currentOrigin = window.frame.origin - let newFrame = NSRect( - origin: currentOrigin, - size: WindowConfig.defaultSize - ) - window.setFrame(newFrame, display: true) - } - /// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application @IBAction func toggleVisibility(_ sender: Any) { // If we have focus, then we hide all windows. @@ -814,10 +804,6 @@ class AppDelegate: NSObject, } } - private enum WindowConfig { - static let defaultSize = CGSize(width: 800, height: 600) - } - private struct ToggleVisibilityState { let hiddenWindows: [Weak] let keyWindow: Weak? @@ -847,23 +833,3 @@ class AppDelegate: NSObject, } } } - -extension AppDelegate: NSMenuItemValidation { - func validateMenuItem(_ item: NSMenuItem) -> Bool { - switch item.action { - case #selector(returnToDefaultSize): - guard let focusedWindow = NSApp.keyWindow else { - return false - } - if focusedWindow.styleMask.contains(.fullScreen) { - return false - } - if focusedWindow.frame.size == WindowConfig.defaultSize { - return false - } - return true - default: - return true - } - } -} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d7ddb793e..19fc0abfc 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -578,6 +578,23 @@ class TerminalController: BaseTerminalController { window.close() } + @IBAction func returnToDefaultSize(_ sender: Any) { + guard + let window = NSApp.keyWindow, + let initialWidth = ghostty.config.windowWidth, + let initialHeight = ghostty.config.windowHeight + else { return } + let currentOrigin = window.frame.origin + let newFrame = NSRect( + origin: currentOrigin, + size: .init( + width: CGFloat(initialWidth), + height: CGFloat(initialHeight) + ) + ) + window.setFrame(newFrame, display: true) + } + @IBAction override func closeWindow(_ sender: Any?) { guard let window = window else { return } guard let tabGroup = window.tabGroup else { @@ -823,3 +840,32 @@ class TerminalController: BaseTerminalController { } } } + + +extension TerminalController: NSMenuItemValidation { + func validateMenuItem(_ item: NSMenuItem) -> Bool { + switch item.action { + case #selector(returnToDefaultSize): + guard let focusedWindow = NSApp.keyWindow else { + return false + } + if focusedWindow.styleMask.contains(.fullScreen) { + return false + } + guard + let windowWidth = ghostty.config.windowWidth, + let windowHeight = ghostty.config.windowHeight, + focusedWindow.frame.size != .init( + width: CGFloat(windowWidth), + height: CGFloat(windowHeight) + ) + 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..70f21995f 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -140,7 +140,21 @@ extension Ghostty { guard let ptr = v else { return "" } return String(cString: ptr) } - + + var windowHeight: Int16? { + guard let config = self.config else { return nil } + var v: Int16 = 0 + let key = "window-height" + return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil + } + + var windowWidth: Int16? { + guard let config = self.config else { return nil } + var v: Int16 = 0 + let key = "window-width" + return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil + } + var windowPositionX: Int16? { guard let config = self.config else { return nil } var v: Int16 = 0 From afb154ee5d22b516609456763e3f68b144aedff0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 28 Feb 2025 14:51:41 -0800 Subject: [PATCH 3/3] macos: store default size as computed property --- .../Terminal/TerminalController.swift | 135 +++++++++++------- macos/Sources/Ghostty/Ghostty.Config.swift | 14 -- 2 files changed, 84 insertions(+), 65 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 19fc0abfc..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) } } @@ -579,20 +613,8 @@ class TerminalController: BaseTerminalController { } @IBAction func returnToDefaultSize(_ sender: Any) { - guard - let window = NSApp.keyWindow, - let initialWidth = ghostty.config.windowWidth, - let initialHeight = ghostty.config.windowHeight - else { return } - let currentOrigin = window.frame.origin - let newFrame = NSRect( - origin: currentOrigin, - size: .init( - width: CGFloat(initialWidth), - height: CGFloat(initialHeight) - ) - ) - window.setFrame(newFrame, display: true) + guard let defaultSize else { return } + window?.setFrame(defaultSize, display: true) } @IBAction override func closeWindow(_ sender: Any?) { @@ -828,15 +850,18 @@ 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 } } } @@ -846,23 +871,31 @@ extension TerminalController: NSMenuItemValidation { func validateMenuItem(_ item: NSMenuItem) -> Bool { switch item.action { case #selector(returnToDefaultSize): - guard let focusedWindow = NSApp.keyWindow else { + guard let window else { return false } + + // Native fullscreen windows can't revert to default size. + if window.styleMask.contains(.fullScreen) { return false } - if focusedWindow.styleMask.contains(.fullScreen) { + + // If we're fullscreen at all then we can't change size + if fullscreenStyle?.isFullscreen ?? false { return false } - guard - let windowWidth = ghostty.config.windowWidth, - let windowHeight = ghostty.config.windowHeight, - focusedWindow.frame.size != .init( - width: CGFloat(windowWidth), - height: CGFloat(windowHeight) - ) + + // 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 70f21995f..10e5446cc 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -141,20 +141,6 @@ extension Ghostty { return String(cString: ptr) } - var windowHeight: Int16? { - guard let config = self.config else { return nil } - var v: Int16 = 0 - let key = "window-height" - return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil - } - - var windowWidth: Int16? { - guard let config = self.config else { return nil } - var v: Int16 = 0 - let key = "window-width" - return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil - } - var windowPositionX: Int16? { guard let config = self.config else { return nil } var v: Int16 = 0