mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-13 19:15:48 +00:00
macOS: fix for Cmd+W window position/size restoration (#11070)
I'd like to contribute a fix for an issue I found regarding how macOS window restoration works when a window is closed via Cmd+W (leaving the app active). Currently, the position cascades down and to the right on every reopen, and size explicitly resets. Also, explicit `window-position-x/y` configs get ignored on first launch. I've diagnosed the issues: 1. In `TerminalWindow.swift`, `setInitialWindowPosition` relies on the `TerminalController` which isn't present during `awakeFromNib`. I moved the `screen.origin` calculation directly into the window class to ensure fixed coordinates are respected immediately. 2. In `TerminalController.swift`, I consolidated the window spawning cascade logic into a new `applyCascade(to:hasFixedPos:)` helper. It now only calls `cascadeTopLeft` if `TerminalController.all.count > 1` (meaning another window is active) and fixed coords aren't set. If it's the only window, it anchors exactly where `LastWindowPosition` placed it. 3. In `LastWindowPosition.swift`, I updated the `save` and `restore` logic to persist the full `window.frame` (origin + size) instead of just the origin. *Disclosure: I used Cursor (Tab) to assist in navigating the codebase and formatting the Swift code, but I fully understand the AppKit lifecycle changes and edge cases I'm proposing.* I have the commit locally formatted with `swiftlint` and ready to push!
This commit is contained in:
@@ -198,6 +198,16 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
// of each other.
|
||||
private static var lastCascadePoint = NSPoint(x: 0, y: 0)
|
||||
|
||||
private static func applyCascade(to window: NSWindow, hasFixedPos: Bool) {
|
||||
if hasFixedPos { return }
|
||||
|
||||
if all.count > 1 {
|
||||
lastCascadePoint = window.cascadeTopLeft(from: lastCascadePoint)
|
||||
} else {
|
||||
lastCascadePoint = window.cascadeTopLeft(from: NSPoint(x: window.frame.minX, y: window.frame.maxY))
|
||||
}
|
||||
}
|
||||
|
||||
// The preferred parent terminal controller.
|
||||
static var preferredParent: TerminalController? {
|
||||
all.first {
|
||||
@@ -253,7 +263,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
// Only cascade if we aren't fullscreen.
|
||||
if let window = c.window {
|
||||
if !window.styleMask.contains(.fullScreen) {
|
||||
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +334,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
window.setFrameTopLeftPoint(position)
|
||||
window.constrainToScreen()
|
||||
} else {
|
||||
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +441,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
// Only cascade if we aren't fullscreen and are alone in the tab group.
|
||||
if !window.styleMask.contains(.fullScreen) &&
|
||||
window.tabGroup?.windows.count ?? 1 == 1 {
|
||||
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
|
||||
let hasFixedPos = controller.derivedConfig.windowPositionX != nil && controller.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
|
||||
controller.showWindow(self)
|
||||
@@ -1165,6 +1178,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
}
|
||||
}
|
||||
|
||||
override func windowDidResize(_ notification: Notification) {
|
||||
super.windowDidResize(notification)
|
||||
|
||||
// Whenever we resize save our last position and size for the next start.
|
||||
if let window {
|
||||
LastWindowPosition.shared.save(window)
|
||||
}
|
||||
}
|
||||
|
||||
func windowDidBecomeMain(_ notification: Notification) {
|
||||
// Whenever we get focused, use that as our last window position for
|
||||
// restart. This differs from Terminal.app but matches iTerm2 behavior
|
||||
|
||||
@@ -538,7 +538,7 @@ class TerminalWindow: NSWindow {
|
||||
|
||||
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
|
||||
// If we don't have an X/Y then we try to use the previously saved window pos.
|
||||
guard x != nil, y != nil else {
|
||||
guard let x = x, let y = y else {
|
||||
if !LastWindowPosition.shared.restore(self) {
|
||||
center()
|
||||
}
|
||||
@@ -552,14 +552,19 @@ class TerminalWindow: NSWindow {
|
||||
return
|
||||
}
|
||||
|
||||
// We have an X/Y, use our controller function to set it up.
|
||||
guard let terminalController else {
|
||||
center()
|
||||
return
|
||||
}
|
||||
// Convert top-left coordinates to bottom-left origin using our utility extension
|
||||
let origin = screen.origin(
|
||||
fromTopLeftOffsetX: CGFloat(x),
|
||||
offsetY: CGFloat(y),
|
||||
windowSize: frame.size)
|
||||
|
||||
let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen)
|
||||
setFrameOrigin(frame.origin)
|
||||
// Clamp the origin to ensure the window stays fully visible on screen
|
||||
var safeOrigin = origin
|
||||
let vf = screen.visibleFrame
|
||||
safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width)
|
||||
safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height)
|
||||
|
||||
setFrameOrigin(safeOrigin)
|
||||
}
|
||||
|
||||
private func hideWindowButtons() {
|
||||
|
||||
@@ -7,22 +7,28 @@ class LastWindowPosition {
|
||||
private let positionKey = "NSWindowLastPosition"
|
||||
|
||||
func save(_ window: NSWindow) {
|
||||
let origin = window.frame.origin
|
||||
let point = [origin.x, origin.y]
|
||||
UserDefaults.standard.set(point, forKey: positionKey)
|
||||
let frame = window.frame
|
||||
let rect = [frame.origin.x, frame.origin.y, frame.size.width, frame.size.height]
|
||||
UserDefaults.standard.set(rect, forKey: positionKey)
|
||||
}
|
||||
|
||||
func restore(_ window: NSWindow) -> Bool {
|
||||
guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double],
|
||||
points.count == 2 else { return false }
|
||||
guard let values = UserDefaults.standard.array(forKey: positionKey) as? [Double],
|
||||
values.count >= 2 else { return false }
|
||||
|
||||
let lastPosition = CGPoint(x: points[0], y: points[1])
|
||||
let lastPosition = CGPoint(x: values[0], y: values[1])
|
||||
|
||||
guard let screen = window.screen ?? NSScreen.main else { return false }
|
||||
let visibleFrame = screen.visibleFrame
|
||||
|
||||
var newFrame = window.frame
|
||||
newFrame.origin = lastPosition
|
||||
|
||||
if values.count >= 4 {
|
||||
newFrame.size.width = min(values[2], visibleFrame.width)
|
||||
newFrame.size.height = min(values[3], visibleFrame.height)
|
||||
}
|
||||
|
||||
if !visibleFrame.contains(newFrame.origin) {
|
||||
newFrame.origin.x = max(visibleFrame.minX, min(visibleFrame.maxX - newFrame.width, newFrame.origin.x))
|
||||
newFrame.origin.y = max(visibleFrame.minY, min(visibleFrame.maxY - newFrame.height, newFrame.origin.y))
|
||||
|
||||
Reference in New Issue
Block a user