diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c2c06e6e6..e744e7c38 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 = 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 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))