From 8f3fdc11850addee518b4cde1b3ad299e62ca938 Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Fri, 7 Aug 2026 05:42:03 +0530 Subject: [PATCH 1/2] Test authoritative shortcut config import --- RectangleTests/RectangleTests.swift | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/RectangleTests/RectangleTests.swift b/RectangleTests/RectangleTests.swift index e555b3c4..c6bcf065 100644 --- a/RectangleTests/RectangleTests.swift +++ b/RectangleTests/RectangleTests.swift @@ -146,6 +146,107 @@ class DefaultsExportTests: XCTestCase { } } +class ConfigImportTests: XCTestCase { + + private static let shortcutKeys = WindowAction.active.map(\.name) + TodoManager.defaultsKeys + private var storedValues = [String: Any]() + private var absentKeys = Set() + + override func setUp() { + super.setUp() + for key in Self.shortcutKeys { + if let value = UserDefaults.standard.object(forKey: key) { + storedValues[key] = value + } else { + absentKeys.insert(key) + } + } + } + + override func tearDown() { + for key in Self.shortcutKeys { + if let value = storedValues[key] { + UserDefaults.standard.set(value, forKey: key) + } else if absentKeys.contains(key) { + UserDefaults.standard.removeObject(forKey: key) + } + } + storedValues.removeAll() + absentKeys.removeAll() + super.tearDown() + } + + func testImportClearsOmittedActiveShortcut() throws { + let action = WindowAction.almostMaximize + store(Shortcut(NSEvent.ModifierFlags.command.rawValue, 10), forKey: action.name) + + try loadConfig(shortcuts: [:]) + + XCTAssertNil(UserDefaults.standard.object(forKey: action.name)) + } + + func testImportClearsOmittedTodoShortcut() throws { + let defaultsKey = TodoManager.toggleDefaultsKey + store(Shortcut(NSEvent.ModifierFlags.command.rawValue, 11), forKey: defaultsKey) + + try loadConfig(shortcuts: [:]) + + XCTAssertNil(UserDefaults.standard.object(forKey: defaultsKey)) + } + + func testImportAppliesSuppliedActiveShortcut() throws { + let action = WindowAction.almostMaximize + let importedShortcut = Shortcut(NSEvent.ModifierFlags.command.rawValue, 12) + + try loadConfig(shortcuts: [action.name: importedShortcut]) + + let storedShortcut = try XCTUnwrap(ShortcutCycle.shortcut(for: action)) + XCTAssertEqual(storedShortcut.keyCode, importedShortcut.keyCode) + XCTAssertEqual(storedShortcut.modifierFlags.rawValue, importedShortcut.modifierFlags) + } + + func testImportUsesActiveShortcutAlias() throws { + let action = WindowAction.leftHalf + let alias = try XCTUnwrap(action.aliasName) + let importedShortcut = Shortcut(NSEvent.ModifierFlags.command.rawValue, 13) + + try loadConfig(shortcuts: [alias: importedShortcut]) + + let storedShortcut = try XCTUnwrap(ShortcutCycle.shortcut(for: action)) + XCTAssertEqual(storedShortcut.keyCode, importedShortcut.keyCode) + XCTAssertEqual(storedShortcut.modifierFlags.rawValue, importedShortcut.modifierFlags) + } + + func testImportClearsInvalidActiveShortcut() throws { + let action = WindowAction.almostMaximize + store(Shortcut(NSEvent.ModifierFlags.command.rawValue, 14), forKey: action.name) + + try loadConfig(shortcuts: [action.name: Shortcut(NSEvent.ModifierFlags.command.rawValue, -1)]) + + XCTAssertNil(UserDefaults.standard.object(forKey: action.name)) + } + + private func store(_ shortcut: Shortcut, forKey key: String) { + let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: MASDictionaryTransformerName))! + let value = transformer.reverseTransformedValue(shortcut.toMASSHortcut()) + UserDefaults.standard.set(value, forKey: key) + } + + private func loadConfig(shortcuts: [String: Shortcut]) throws { + let config = Config(bundleId: "com.knollsoft.Rectangle", + version: "ConfigImportTests", + shortcuts: shortcuts, + defaults: [:]) + let data = try JSONEncoder().encode(config) + let fileURL = FileManager.default.temporaryDirectory + .appendingPathComponent("ConfigImportTests-\(UUID().uuidString).json") + defer { try? FileManager.default.removeItem(at: fileURL) } + try data.write(to: fileURL, options: .atomic) + + Defaults.load(fileUrl: fileURL, notificationCenter: NotificationCenter()) + } +} + class StackBadgeGeometryTests: XCTestCase { private let laptopFrame = CGRect(x: 0, y: 0, width: 2336, height: 1510) From eb03a99ec3f846e81ca3c4616ac5a3850d9a4c29 Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Fri, 7 Aug 2026 05:42:52 +0530 Subject: [PATCH 2/2] Make shortcut config import authoritative --- Rectangle/PrefsWindow/Config.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Rectangle/PrefsWindow/Config.swift b/Rectangle/PrefsWindow/Config.swift index 8339063c..2bdbb758 100644 --- a/Rectangle/PrefsWindow/Config.swift +++ b/Rectangle/PrefsWindow/Config.swift @@ -53,7 +53,7 @@ extension Defaults { return try? decoder.decode(Config.self, from: jsonData) } - static func load(fileUrl: URL) { + static func load(fileUrl: URL, notificationCenter: NotificationCenter = .default) { guard let dictTransformer = ValueTransformer(forName: NSValueTransformerName(rawValue: MASDictionaryTransformerName)) else { return } // Size cap: legitimate configs are ~tens of KB; refuse anything that @@ -74,19 +74,25 @@ extension Defaults { for action in WindowAction.active { let importedShortcut = config.shortcuts[action.name] ?? action.aliasName.flatMap { config.shortcuts[$0] } - if let shortcut = importedShortcut?.toMASSHortcut() { + if let importedShortcut, importedShortcut.keyCode >= 0 { + let shortcut = importedShortcut.toMASSHortcut() let dictValue = dictTransformer.reverseTransformedValue(shortcut) UserDefaults.standard.setValue(dictValue, forKey: action.name) + } else { + UserDefaults.standard.removeObject(forKey: action.name) } } for defaultsKey in TodoManager.defaultsKeys { - if let shortcut = config.shortcuts[defaultsKey]?.toMASSHortcut() { + if let importedShortcut = config.shortcuts[defaultsKey], importedShortcut.keyCode >= 0 { + let shortcut = importedShortcut.toMASSHortcut() let dictValue = dictTransformer.reverseTransformedValue(shortcut) UserDefaults.standard.setValue(dictValue, forKey: defaultsKey) + } else { + UserDefaults.standard.removeObject(forKey: defaultsKey) } } - Notification.Name.configImported.post() + Notification.Name.configImported.post(center: notificationCenter) } static func loadFromSupportDir() {