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:
Mitchell Hashimoto
2026-02-28 14:10:51 -08:00
committed by GitHub
3 changed files with 50 additions and 17 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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))