mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 09:16:11 +03:00
"Return to Default Size" implementation (#5974)
## Added Support for "Return To Default Size" This update introduces support for the **"Return To Default Size"** feature. ### Fixes - Resolves [#1328](https://github.com/ghostty-org/ghostty/issues/1328) ### Screenshots | Description | Screenshot | |------------|------------| | **Ghostty** | <img width="1084" alt="Screenshot 2025-02-24 at 21 15 38" src="https://github.com/user-attachments/assets/4657ccdb-9c7a-4884-873c-bbe0f30f9400" /> | | **After changing window size** | <img width="1155" alt="Screenshot 2025-02-24 at 21 16 00" src="https://github.com/user-attachments/assets/9b3931f2-1c4b-4f86-8d56-8892bd5675cc" /> | | **Native Terminal App (for reference)** | <img width="630" alt="Screenshot 2025-02-24 at 21 16 20" src="https://github.com/user-attachments/assets/ae049931-b74d-4246-a9e7-d9be079b1a24" /> |
This commit is contained in:
@ -388,6 +388,13 @@
|
|||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="dgt-Tx-d4e"/>
|
<menuItem isSeparatorItem="YES" id="dgt-Tx-d4e"/>
|
||||||
|
<menuItem title="Return To Default Size" id="Gbx-Vi-OGC" userLabel="Return To Default Size">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="returnToDefaultSize:" target="-1" id="Bpt-GO-UU1"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="CpM-rI-Sc1"/>
|
||||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -27,6 +27,9 @@ class TerminalController: BaseTerminalController {
|
|||||||
/// The notification cancellable for focused surface property changes.
|
/// The notification cancellable for focused surface property changes.
|
||||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
/// 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,
|
init(_ ghostty: Ghostty.App,
|
||||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
@ -308,6 +311,55 @@ class TerminalController: BaseTerminalController {
|
|||||||
y: frame.maxY - (CGFloat(y) + window.frame.height)))
|
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
|
//MARK: - NSWindowController
|
||||||
|
|
||||||
override func windowWillLoad() {
|
override func windowWillLoad() {
|
||||||
@ -356,6 +408,9 @@ class TerminalController: BaseTerminalController {
|
|||||||
super.windowDidLoad()
|
super.windowDidLoad()
|
||||||
guard let window = window as? TerminalWindow else { return }
|
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 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
|
// 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
|
// 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 window decorations are disabled, remove our title
|
||||||
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
|
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
|
||||||
|
|
||||||
// If we have only a single surface (no splits) and that surface requested
|
// If we have only a single surface (no splits) and there is a default size then
|
||||||
// an initial size then we set it here now.
|
// we should resize to that default size.
|
||||||
if case let .leaf(leaf) = surfaceTree {
|
if case let .leaf(leaf) = surfaceTree {
|
||||||
if config.maximize {
|
// If this is our first surface then our focused surface will be nil
|
||||||
if let screen = window.screen ?? NSScreen.main {
|
// so we force the focused surface to the leaf.
|
||||||
window.setFrame(screen.visibleFrame, display: true)
|
focusedSurface = leaf.surface
|
||||||
}
|
|
||||||
} else if let initialSize = leaf.surface.initialSize,
|
|
||||||
let screen = window.screen ?? NSScreen.main {
|
|
||||||
// Get the current frame of the window
|
|
||||||
var frame = window.frame
|
|
||||||
|
|
||||||
// Calculate the chrome size (window size minus view size)
|
if let defaultSize {
|
||||||
let chromeWidth = frame.size.width - leaf.surface.frame.size.width
|
window.setFrame(defaultSize, display: true)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,6 +612,11 @@ class TerminalController: BaseTerminalController {
|
|||||||
window.close()
|
window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func returnToDefaultSize(_ sender: Any) {
|
||||||
|
guard let defaultSize else { return }
|
||||||
|
window?.setFrame(defaultSize, display: true)
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction override func closeWindow(_ sender: Any?) {
|
@IBAction override func closeWindow(_ sender: Any?) {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
guard let tabGroup = window.tabGroup else {
|
guard let tabGroup = window.tabGroup else {
|
||||||
@ -811,15 +850,55 @@ class TerminalController: BaseTerminalController {
|
|||||||
struct DerivedConfig {
|
struct DerivedConfig {
|
||||||
let backgroundColor: Color
|
let backgroundColor: Color
|
||||||
let macosTitlebarStyle: String
|
let macosTitlebarStyle: String
|
||||||
|
let maximize: Bool
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||||
self.macosTitlebarStyle = "system"
|
self.macosTitlebarStyle = "system"
|
||||||
|
self.maximize = false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ config: Ghostty.Config) {
|
init(_ config: Ghostty.Config) {
|
||||||
self.backgroundColor = config.backgroundColor
|
self.backgroundColor = config.backgroundColor
|
||||||
self.macosTitlebarStyle = config.macosTitlebarStyle
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ extension Ghostty {
|
|||||||
guard let ptr = v else { return "" }
|
guard let ptr = v else { return "" }
|
||||||
return String(cString: ptr)
|
return String(cString: ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
var windowPositionX: Int16? {
|
var windowPositionX: Int16? {
|
||||||
guard let config = self.config else { return nil }
|
guard let config = self.config else { return nil }
|
||||||
var v: Int16 = 0
|
var v: Int16 = 0
|
||||||
|
Reference in New Issue
Block a user