macos: fix window size/position restoration on Cmd+W close

This fixes two overlapping issues regarding window positioning and Cmd+W window closures on macOS:

1. `window-position-x` and `window-position-y` coordinates were being ignored on initial launch because `TerminalWindow.setInitialWindowPosition` depended on the `TerminalController`, which isn't fully attached during `awakeFromNib`. This logic was moved so explicit coordinates are correctly enforced.
2. When closing a window via Cmd+W (leaving the app active), reopening the window would continuously cascade down and to the right rather than restoring to the previous position. It now checks if there are other windows open before cascading.
3. `LastWindowPosition` was updated to save both the frame origin and size (width/height), ensuring that restoring a closed window correctly mimics native AppKit State Restoration size behaviors while honoring explicit configurations.
pull/11070/head
A-AKB 2026-02-28 01:34:18 +01:00
parent 71cb9debb9
commit 0db32ab9a8
3 changed files with 58 additions and 17 deletions

View File

@ -253,7 +253,14 @@ 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
let shouldCascade = !hasFixedPos && TerminalController.all.count > 1
if shouldCascade {
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
} else if !hasFixedPos {
Self.lastCascadePoint = NSPoint(x: window.frame.minX, y: window.frame.maxY)
}
}
}
@ -323,7 +330,14 @@ 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
let shouldCascade = !hasFixedPos && TerminalController.all.count > 1
if shouldCascade {
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
} else if !hasFixedPos {
Self.lastCascadePoint = NSPoint(x: window.frame.minX, y: window.frame.maxY)
}
}
}
}
@ -429,7 +443,14 @@ 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
let shouldCascade = !hasFixedPos && TerminalController.all.count > 1
if shouldCascade {
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
} else if !hasFixedPos {
Self.lastCascadePoint = NSPoint(x: window.frame.minX, y: window.frame.maxY)
}
}
controller.showWindow(self)
@ -1165,6 +1186,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))