From 0db32ab9a8ff72d0c1eaf9c9e0cc94c3b27b2dd1 Mon Sep 17 00:00:00 2001 From: A-AKB Date: Sat, 28 Feb 2026 01:34:18 +0100 Subject: [PATCH 1/3] 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)) From 6e622f8c75d6a80b17e1ba5e40d3f9efca410855 Mon Sep 17 00:00:00 2001 From: A-AKB Date: Sat, 28 Feb 2026 01:43:00 +0100 Subject: [PATCH 2/3] fix(macos): extract window cascade logic into helper function --- .../Terminal/TerminalController.swift | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 0534a066e..ff4bba833 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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 = NSPoint(x: window.frame.minX, y: window.frame.maxY) + } + } + // The preferred parent terminal controller. static var preferredParent: TerminalController? { all.first { @@ -254,13 +264,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if let window = c.window { if !window.styleMask.contains(.fullScreen) { 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) - } + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } } @@ -331,13 +335,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr window.constrainToScreen() } else { 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) - } + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } } } @@ -444,13 +442,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if !window.styleMask.contains(.fullScreen) && window.tabGroup?.windows.count ?? 1 == 1 { 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) - } + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } controller.showWindow(self) From 0d5b9d554c14bc1b574267fa63dd95eee84edff3 Mon Sep 17 00:00:00 2001 From: Abdurrahman <96236378+abdurrahmanski@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:02:40 +0100 Subject: [PATCH 3/3] Update macos/Sources/Features/Terminal/TerminalController.swift apply reviewer suggestion for cascading Co-authored-by: Lukas <134181853+bo2themax@users.noreply.github.com> --- macos/Sources/Features/Terminal/TerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ff4bba833..e744e7c38 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -204,7 +204,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if all.count > 1 { lastCascadePoint = window.cascadeTopLeft(from: lastCascadePoint) } else { - lastCascadePoint = NSPoint(x: window.frame.minX, y: window.frame.maxY) + lastCascadePoint = window.cascadeTopLeft(from: NSPoint(x: window.frame.minX, y: window.frame.maxY)) } }