From 0db32ab9a8ff72d0c1eaf9c9e0cc94c3b27b2dd1 Mon Sep 17 00:00:00 2001 From: A-AKB Date: Sat, 28 Feb 2026 01:34:18 +0100 Subject: [PATCH] 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. --- .../Terminal/TerminalController.swift | 36 +++++++++++++++++-- .../Window Styles/TerminalWindow.swift | 21 ++++++----- .../Sources/Helpers/LastWindowPosition.swift | 18 ++++++---- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c2c06e6e6..0534a066e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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 diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 62835e286..561d54885 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -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() { diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift index a0dfa90dd..5a9ce1d2c 100644 --- a/macos/Sources/Helpers/LastWindowPosition.swift +++ b/macos/Sources/Helpers/LastWindowPosition.swift @@ -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))