diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 02c8258cb..0c68da534 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; }; A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; }; A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; + A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; @@ -168,6 +169,7 @@ A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; }; A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; }; A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; + A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; @@ -270,6 +272,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, @@ -623,6 +626,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */, A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f24261b9b..8507cf620 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -283,9 +283,12 @@ class TerminalController: BaseTerminalController { private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { guard let window else { return } - // If we don't have both an X and Y we center. + // If we don't have an X/Y then we try to use the previously saved window pos. guard let x, let y else { - window.center() + if (!LastWindowPosition.shared.restore(window)) { + window.center() + } + return } @@ -490,6 +493,20 @@ class TerminalController: BaseTerminalController { override func windowDidMove(_ notification: Notification) { super.windowDidMove(notification) self.fixTabBar() + + // Whenever we move save our last position 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 + // and I think its sensible. + if let window { + LastWindowPosition.shared.save(window) + } } // Called when the window will be encoded. We handle the data encoding here in the diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift new file mode 100644 index 000000000..a0dfa90dd --- /dev/null +++ b/macos/Sources/Helpers/LastWindowPosition.swift @@ -0,0 +1,34 @@ +import Cocoa + +/// Manages the persistence and restoration of window positions across app launches. +class LastWindowPosition { + static let shared = 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) + } + + func restore(_ window: NSWindow) -> Bool { + guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double], + points.count == 2 else { return false } + + let lastPosition = CGPoint(x: points[0], y: points[1]) + + guard let screen = window.screen ?? NSScreen.main else { return false } + let visibleFrame = screen.visibleFrame + + var newFrame = window.frame + newFrame.origin = lastPosition + 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)) + } + + window.setFrame(newFrame, display: true) + return true + } +}