From 299a7ad2d4899d1c360fe423928e1aa26b122766 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:33:16 +0100 Subject: [PATCH] macOS: save window position by screen --- .../Sources/Helpers/LastWindowPosition.swift | 69 +++++++++++++++---- .../Helpers/LastWindowPositionTests.swift | 66 ++++++++++++++++++ 2 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 macos/Tests/Helpers/LastWindowPositionTests.swift diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift index c7989b6fa..cf06c893d 100644 --- a/macos/Sources/Helpers/LastWindowPosition.swift +++ b/macos/Sources/Helpers/LastWindowPosition.swift @@ -4,7 +4,13 @@ import Cocoa class LastWindowPosition { static let shared = LastWindowPosition() - private let positionKey = "NSWindowLastPosition" + fileprivate static let rectsKey = "NSWindowLastRectsByScreen" + + let defaults: UserDefaults + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + } @discardableResult func save(_ window: NSWindow?) -> Bool { @@ -13,9 +19,10 @@ class LastWindowPosition { // with the wrong one when window decorations change while creating, // e.g. adding a toolbar affects the window's frame. guard let window, window.isVisible else { return false } - let frame = window.frame - let rect = [frame.origin.x, frame.origin.y, frame.size.width, frame.size.height] - UserDefaults.ghostty.set(rect, forKey: positionKey) + guard let screenID = window.screen?.displayUUID?.uuidString else { + return false + } + savedWindowRectInfo[screenID] = window.frame return true } @@ -32,22 +39,22 @@ class LastWindowPosition { func restore(_ window: NSWindow, origin restoreOrigin: Bool = true, size restoreSize: Bool = true) -> Bool { guard restoreOrigin || restoreSize else { return false } - guard let values = UserDefaults.ghostty.array(forKey: positionKey) as? [Double], - values.count >= 2 else { return false } - - let lastPosition = CGPoint(x: values[0], y: values[1]) - - guard let screen = window.screen ?? NSScreen.main else { return false } + guard + let screen = window.screen ?? NSScreen.main, + let screenID = screen.displayUUID?.uuidString, + let lastFrame = savedWindowRectInfo[screenID] + else { + return false + } let visibleFrame = screen.visibleFrame - var newFrame = window.frame if restoreOrigin { - newFrame.origin = lastPosition + newFrame.origin = lastFrame.origin } - if restoreSize, values.count >= 4 { - newFrame.size.width = min(values[2], visibleFrame.width) - newFrame.size.height = min(values[3], visibleFrame.height) + if restoreSize { + newFrame.size.width = min(lastFrame.width, visibleFrame.width) + newFrame.size.height = min(lastFrame.height, visibleFrame.height) } // If the new frame is not constrained to the visible screen, @@ -65,3 +72,35 @@ class LastWindowPosition { return true } } + +extension LastWindowPosition { + var savedWindowRectInfo: [String: CGRect] { + get { + guard + let dict = defaults.dictionary(forKey: LastWindowPosition.rectsKey) as? [String: CFDictionary] + else { + // Restore previously saved rect on main screen + if + let rect = CGRect(valueArray: defaults.array(forKey: "NSWindowLastPosition")), + let screenID = NSScreen.main?.displayUUID?.uuidString { + return [screenID: rect] + } else { + return [:] + } + } + return dict.compactMapValues(CGRect.init(dictionaryRepresentation:)) + } + set { + defaults.set(newValue.mapValues(\.dictionaryRepresentation), forKey: LastWindowPosition.rectsKey) + } + } +} + +private extension CGRect { + init?(valueArray: [Any]?) { + guard let values = valueArray as? [Double], values.count >= 2 else { + return nil + } + self.init(x: values[0], y: values[1], width: values[safe: 2] ?? 0, height: values[safe: 3] ?? 0) + } +} diff --git a/macos/Tests/Helpers/LastWindowPositionTests.swift b/macos/Tests/Helpers/LastWindowPositionTests.swift new file mode 100644 index 000000000..5ef08c574 --- /dev/null +++ b/macos/Tests/Helpers/LastWindowPositionTests.swift @@ -0,0 +1,66 @@ +// +// LastWindowPositionTests.swift +// Ghostty +// +// Created by Lukas on 04.03.2026. +// + +import Testing +import AppKit +@testable import Ghostty + +@Suite(.serialized) +struct LastWindowPositionTests { + func usingTemporaryHelper(_ perform: (_ helper: LastWindowPosition) throws -> Void) rethrows { + let defaults = MockDefaults() + try perform(LastWindowPosition(defaults: defaults)) + defaults.reset() + } + + @Test func restorePoint() throws { + try usingTemporaryHelper { helper in + helper.defaults.set([20, 20], forKey: "NSWindowLastPosition") + let rect = try #require(helper.savedWindowRectInfo.values.first) + #expect(rect == CGRect(x: 20, y: 20, width: 0, height: 0)) + } + } + + @Test func restoreRect() throws { + try usingTemporaryHelper { helper in + helper.defaults.set([20, 20, 30, 30], forKey: "NSWindowLastPosition") + let rect = try #require(helper.savedWindowRectInfo.values.first) + #expect(rect == CGRect(x: 20, y: 20, width: 30, height: 30)) + } + } + + @Test func restoreScreenByRect() throws { + usingTemporaryHelper { helper in + helper.defaults.set([ + "main": CGRect(x: 20, y: 20, width: 30, height: 30).dictionaryRepresentation + ], forKey: "NSWindowLastRectsByScreen") + #expect(helper.savedWindowRectInfo == [ + "main": CGRect(x: 20, y: 20, width: 30, height: 30) + ]) + } + } +} + +class MockDefaults: UserDefaults { + private var values: [String: Any] = [:] + + func reset() { + values.removeAll() + } + + override func set(_ value: Any?, forKey defaultName: String) { + values[defaultName] = value + } + + override func dictionary(forKey defaultName: String) -> [String: Any]? { + values[defaultName] as? [String: Any] + } + + override func array(forKey defaultName: String) -> [Any]? { + values[defaultName] as? [Any] + } +}