From 0287cf954711ccaa8e42e5406e444e00021660a4 Mon Sep 17 00:00:00 2001 From: Igor Spivak Date: Wed, 18 Feb 2026 20:58:36 -0800 Subject: [PATCH 1/4] feat: Configurable split ratio for half window actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Left/right and top/bottom half actions now support a custom split ratio instead of the fixed 50/50. Configure via Extra Settings (⋯) in the General preferences tab, or via terminal with horizontalSplitRatio and verticalSplitRatio defaults (1–99, default 50). The drag-to-snap footprint preview also reflects the configured ratio. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 84 +++++++++++++++++++ Rectangle/Defaults.swift | 4 + .../PrefsWindow/SettingsViewController.swift | 66 ++++++++++++++- .../BottomHalfCalculation.swift | 4 + .../LeftRightHalfCalculation.swift | 24 ++++-- .../TopHalfCalculation.swift | 4 + TerminalCommands.md | 15 ++++ 7 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b3c44761a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Rectangle is a macOS window management app written in Swift (based on Spectacle). It uses the macOS Accessibility API to move and resize windows via keyboard shortcuts and drag-to-snap. Requires macOS 10.15+. + +- Bundle ID: `com.knollsoft.Rectangle` +- Launcher bundle ID: `com.knollsoft.RectangleLauncher` + +## Build & Test + +```bash +# Build (unsigned, for local development) +xcodebuild -scheme Rectangle CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO + +# Run tests +xcodebuild test -scheme Rectangle + +# Build archive (as CI does) +xcodebuild archive -scheme Rectangle -archivePath Rectangle.xcarchive +``` + +The project uses Xcode with SPM dependencies: a forked MASShortcut (keyboard shortcut recording) and Sparkle (auto-update). + +## Architecture + +### Core Data Flow + +``` +User Action → ShortcutManager + → WindowManager.execute(ExecutionParameters) + → WindowCalculationFactory (selects calculation by WindowAction) + → WindowCalculation.calculate() → CGRect + → WindowMover chain → AccessibilityElement.setFrame() +``` + +### Key Files + +- **`WindowAction.swift`**: Enum of 85+ window actions. Each action maps to a calculation and has a URL name (e.g. `left-half`, `maximize`). +- **`WindowManager.swift`**: Orchestrates window operations. Calls the factory, invokes movers, records history. +- **`WindowCalculationFactory.swift`**: Static registry mapping each `WindowAction` to its `WindowCalculation` subclass. +- **`AccessibilityElement.swift`**: Wrapper around macOS Accessibility API for getting/setting window frames. +- **`Defaults.swift`**: Central registry for 80+ `UserDefaults` preferences (gaps, margins, behavior flags, etc.). +- **`AppDelegate.swift`**: App lifecycle, initialization, menu bar setup, URL scheme handling. +- **`ShortcutManager.swift`**: Registers/unregisters keyboard shortcuts via MASShortcut. +- **`ScreenDetection.swift`**: Determines which display a window is on. + +### Window Calculations (`WindowCalculation/`) + +78 files, each implementing one positioning strategy as a subclass of `WindowCalculation`. Examples: `LeftHalfCalculation`, `MaximizeCalculation`, `NextDisplayCalculation`. To add a new window action: +1. Add a case to `WindowAction.swift` (with display name, URL name, optional default shortcut) +2. Create a `WindowCalculation` subclass +3. Register it in `WindowCalculationFactory` + +### Window Movers (`WindowMover/`) + +Chain of responsibility—`StandardWindowMover` is tried first, then `BestEffortWindowMover` as fallback. `CenteringFixedSizedWindowMover` handles fixed-size windows. `QuantizedWindowMover` snaps to pixel boundaries. + +### Snap-to-Edge (`Snapping/`) + +`SnappingManager` monitors global mouse events to detect window dragging toward screen edges. `FootprintWindow` renders the preview overlay. `SnapAreaModel` defines the hot zones; compound snap areas in `CompoundSnapArea/` handle multi-step drag interactions (e.g., drag to bottom-center after bottom-third). + +### Preferences (`PrefsWindow/`) + +`SettingsViewController.swift` (~38 KB) is the main preferences UI. `Config.swift` handles JSON import/export of settings. Preferences are stored in `~/Library/Preferences/com.knollsoft.Rectangle.plist`. + +## URL Scheme + +`rectangle://execute-action?name=[action-name]` — triggers any window action by its URL name. Also supports `rectangle://execute-task?name=ignore-app[&app-bundle-id=...]`. + +## Hidden Preferences + +Many advanced settings are set via `defaults write com.knollsoft.Rectangle ...`. See `TerminalCommands.md` for the full list. These map to keys in `Defaults.swift`. + +## Notable Conventions + +- **No async/await**: The codebase predates Swift 5.5 patterns; synchronous Accessibility API calls throughout. +- **Singletons**: `AppDelegate.instance`, `RectangleStatusItem.instance`. +- **Failure signal**: `NSSound.beep()` is used when a window action cannot be applied. +- **Multi-display**: Actions like "Next Display" cycle through `NSScreen.screens`; screen orientation affects which third is "first." +- **Stage Manager**: `StageUtil.swift` handles detection and special-casing for macOS Stage Manager. +- **Desktop widgets**: Excluded from tile-all and cascade-all operations (see `WindowUtil.swift`). diff --git a/Rectangle/Defaults.swift b/Rectangle/Defaults.swift index 4ec4cce26..2c84bb07e 100644 --- a/Rectangle/Defaults.swift +++ b/Rectangle/Defaults.swift @@ -67,6 +67,8 @@ class Defaults { static let notifiedOfProblemApps = BoolDefault(key: "notifiedOfProblemApps") static let specifiedHeight = FloatDefault(key: "specifiedHeight", defaultValue: 1050) static let specifiedWidth = FloatDefault(key: "specifiedWidth", defaultValue: 1680) + static let horizontalSplitRatio = FloatDefault(key: "horizontalSplitRatio", defaultValue: 50) + static let verticalSplitRatio = FloatDefault(key: "verticalSplitRatio", defaultValue: 50) static let moveCursorAcrossDisplays = OptionalBoolDefault(key: "moveCursorAcrossDisplays") static let moveCursor = OptionalBoolDefault(key: "moveCursor") static let autoMaximize = OptionalBoolDefault(key: "autoMaximize") @@ -151,6 +153,8 @@ class Defaults { notifiedOfProblemApps, specifiedHeight, specifiedWidth, + horizontalSplitRatio, + verticalSplitRatio, moveCursorAcrossDisplays, moveCursor, autoMaximize, diff --git a/Rectangle/PrefsWindow/SettingsViewController.swift b/Rectangle/PrefsWindow/SettingsViewController.swift index ed280f7d7..c992230b3 100644 --- a/Rectangle/PrefsWindow/SettingsViewController.swift +++ b/Rectangle/PrefsWindow/SettingsViewController.swift @@ -316,6 +316,43 @@ class SettingsViewController: NSViewController { integerFormatter.minimum = 1 widthStepField.formatter = integerFormatter + let splitRatioHeaderLabel = NSTextField(labelWithString: NSLocalizedString("Half Split Ratios", tableName: "Main", value: "", comment: "")) + splitRatioHeaderLabel.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) + splitRatioHeaderLabel.translatesAutoresizingMaskIntoConstraints = false + + let hSplitLabel = NSTextField(labelWithString: NSLocalizedString("Horizontal (L/R, %)", tableName: "Main", value: "", comment: "")) + hSplitLabel.alignment = .right + hSplitLabel.translatesAutoresizingMaskIntoConstraints = false + + let vSplitLabel = NSTextField(labelWithString: NSLocalizedString("Vertical (T/B, %)", tableName: "Main", value: "", comment: "")) + vSplitLabel.alignment = .right + vSplitLabel.translatesAutoresizingMaskIntoConstraints = false + + let percentFormatter = NumberFormatter() + percentFormatter.allowsFloats = false + percentFormatter.minimum = 1 + percentFormatter.maximum = 99 + + let hSplitField = AutoSaveFloatField(frame: NSRect(x: 0, y: 0, width: 160, height: 19)) + hSplitField.stringValue = String(Int(Defaults.horizontalSplitRatio.value)) + hSplitField.delegate = self + hSplitField.defaults = Defaults.horizontalSplitRatio + hSplitField.fallbackValue = 50 + hSplitField.translatesAutoresizingMaskIntoConstraints = false + hSplitField.refusesFirstResponder = true + hSplitField.alignment = .right + hSplitField.formatter = percentFormatter + + let vSplitField = AutoSaveFloatField(frame: NSRect(x: 0, y: 0, width: 160, height: 19)) + vSplitField.stringValue = String(Int(Defaults.verticalSplitRatio.value)) + vSplitField.delegate = self + vSplitField.defaults = Defaults.verticalSplitRatio + vSplitField.fallbackValue = 50 + vSplitField.translatesAutoresizingMaskIntoConstraints = false + vSplitField.refusesFirstResponder = true + vSplitField.alignment = .right + vSplitField.formatter = percentFormatter + largerWidthShortcutView.setAssociatedUserDefaultsKey(WindowAction.largerWidth.name, withTransformerName: MASDictionaryTransformerName) smallerWidthShortcutView.setAssociatedUserDefaultsKey(WindowAction.smallerWidth.name, withTransformerName: MASDictionaryTransformerName) @@ -433,6 +470,20 @@ class SettingsViewController: NSViewController { widthStepRow.spacing = 18 widthStepRow.addArrangedSubview(widthStepLabel) widthStepRow.addArrangedSubview(widthStepField) + + let hSplitRow = NSStackView() + hSplitRow.orientation = .horizontal + hSplitRow.alignment = .centerY + hSplitRow.spacing = 18 + hSplitRow.addArrangedSubview(hSplitLabel) + hSplitRow.addArrangedSubview(hSplitField) + + let vSplitRow = NSStackView() + vSplitRow.orientation = .horizontal + vSplitRow.alignment = .centerY + vSplitRow.spacing = 18 + vSplitRow.addArrangedSubview(vSplitLabel) + vSplitRow.addArrangedSubview(vSplitField) let topVerticalThirdRow = NSStackView() topVerticalThirdRow.orientation = .horizontal @@ -479,9 +530,14 @@ class SettingsViewController: NSViewController { mainStackView.addArrangedSubview(bottomVerticalThirdRow) mainStackView.addArrangedSubview(topVerticalTwoThirdsRow) mainStackView.addArrangedSubview(bottomVerticalTwoThirdsRow) + mainStackView.addArrangedSubview(splitRatioHeaderLabel) + mainStackView.setCustomSpacing(10, after: splitRatioHeaderLabel) + mainStackView.addArrangedSubview(hSplitRow) + mainStackView.addArrangedSubview(vSplitRow) NSLayoutConstraint.activate([ headerLabel.widthAnchor.constraint(equalTo: mainStackView.widthAnchor), + splitRatioHeaderLabel.widthAnchor.constraint(equalTo: mainStackView.widthAnchor), largerWidthLabel.widthAnchor.constraint(equalTo: smallerWidthLabel.widthAnchor), smallerWidthLabel.widthAnchor.constraint(equalTo: widthStepLabel.widthAnchor), widthStepLabel.widthAnchor.constraint(equalTo: topVerticalThirdLabel.widthAnchor), @@ -489,6 +545,8 @@ class SettingsViewController: NSViewController { middleVerticalThirdLabel.widthAnchor.constraint(equalTo: bottomVerticalThirdLabel.widthAnchor), bottomVerticalThirdLabel.widthAnchor.constraint(equalTo: topVerticalTwoThirdsLabel.widthAnchor), topVerticalTwoThirdsLabel.widthAnchor.constraint(equalTo: bottomVerticalTwoThirdsLabel.widthAnchor), + bottomVerticalTwoThirdsLabel.widthAnchor.constraint(equalTo: hSplitLabel.widthAnchor), + hSplitLabel.widthAnchor.constraint(equalTo: vSplitLabel.widthAnchor), largerWidthLabelStack.widthAnchor.constraint(equalTo: smallerWidthLabelStack.widthAnchor), largerWidthShortcutView.widthAnchor.constraint(equalToConstant: 160), smallerWidthShortcutView.widthAnchor.constraint(equalToConstant: 160), @@ -498,6 +556,8 @@ class SettingsViewController: NSViewController { bottomVerticalThirdShortcutView.widthAnchor.constraint(equalToConstant: 160), topVerticalTwoThirdsShortcutView.widthAnchor.constraint(equalToConstant: 160), bottomVerticalTwoThirdsShortcutView.widthAnchor.constraint(equalToConstant: 160), + hSplitField.widthAnchor.constraint(equalToConstant: 160), + vSplitField.widthAnchor.constraint(equalToConstant: 160), widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor) ]) @@ -747,8 +807,9 @@ extension SettingsViewController: NSTextFieldDelegate { let defaults: FloatDefault = sender.defaults else { return } if sender.stringValue.isEmpty { - sender.stringValue = "30" - defaults.value = 30 + let fallback = sender.fallbackValue + sender.stringValue = "\(Int(fallback))" + defaults.value = fallback sender.defaultsSetAction?() } } @@ -757,4 +818,5 @@ extension SettingsViewController: NSTextFieldDelegate { class AutoSaveFloatField: NSTextField { var defaults: FloatDefault? var defaultsSetAction: (() -> Void)? + var fallbackValue: Float = 30 } diff --git a/Rectangle/WindowCalculation/BottomHalfCalculation.swift b/Rectangle/WindowCalculation/BottomHalfCalculation.swift index 4ac09d55b..2047e9ee6 100644 --- a/Rectangle/WindowCalculation/BottomHalfCalculation.swift +++ b/Rectangle/WindowCalculation/BottomHalfCalculation.swift @@ -19,6 +19,10 @@ class BottomHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalcul return calculateRepeatedRect(params) } + func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult { + return calculateFractionalRect(params, fraction: 1.0 - Defaults.verticalSplitRatio.value / 100.0) + } + func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult { let visibleFrameOfScreen = params.visibleFrameOfScreen diff --git a/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift b/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift index e130d898f..b75f3eff2 100644 --- a/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift +++ b/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift @@ -33,16 +33,22 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal } + func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult { + let ratio = Defaults.horizontalSplitRatio.value / 100.0 + let fraction = params.action == .rightHalf ? 1.0 - ratio : ratio + return calculateFractionalRect(params, fraction: fraction) + } + func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult { let visibleFrameOfScreen = params.visibleFrameOfScreen var rect = visibleFrameOfScreen - + rect.size.width = floor(visibleFrameOfScreen.width * CGFloat(fraction)) if params.action == .rightHalf { rect.origin.x = visibleFrameOfScreen.maxX - rect.width } - + return RectResult(rect) } @@ -96,15 +102,15 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal // Used to draw box for snapping override func calculateRect(_ params: RectCalculationParameters) -> RectResult { + let ratio = CGFloat(Defaults.horizontalSplitRatio.value / 100.0) + var rect = params.visibleFrameOfScreen + let leftWidth = floor(rect.width * ratio) if params.action == .leftHalf { - var oneHalfRect = params.visibleFrameOfScreen - oneHalfRect.size.width = floor(oneHalfRect.width / 2.0) - return RectResult(oneHalfRect) + rect.size.width = leftWidth } else { - var oneHalfRect = params.visibleFrameOfScreen - oneHalfRect.size.width = floor(oneHalfRect.width / 2.0) - oneHalfRect.origin.x += oneHalfRect.size.width - return RectResult(oneHalfRect) + rect.size.width = rect.width - leftWidth + rect.origin.x += leftWidth } + return RectResult(rect) } } diff --git a/Rectangle/WindowCalculation/TopHalfCalculation.swift b/Rectangle/WindowCalculation/TopHalfCalculation.swift index 9eff5915d..00c1bb0ba 100644 --- a/Rectangle/WindowCalculation/TopHalfCalculation.swift +++ b/Rectangle/WindowCalculation/TopHalfCalculation.swift @@ -19,6 +19,10 @@ class TopHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculati return calculateRepeatedRect(params) } + func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult { + return calculateFractionalRect(params, fraction: Defaults.verticalSplitRatio.value / 100.0) + } + func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult { let visibleFrameOfScreen = params.visibleFrameOfScreen diff --git a/TerminalCommands.md b/TerminalCommands.md index 4098eb80c..bfe76e116 100644 --- a/TerminalCommands.md +++ b/TerminalCommands.md @@ -35,6 +35,7 @@ The preferences window is purposefully slim, but there's a lot that can be modif - [Prevent a window that is quickly dragged above the menu bar from going into Mission Control](#prevent-a-window-that-is-quickly-dragged-above-the-menu-bar-from-going-into-mission-control) - [Change the behavior of double-click window title bar](#change-the-behavior-of-double-click-window-title-bar) - [Change the order of displays to order by x coordinate](#change-the-order-of-displays-to-order-by-x-coordinate-for-next-and-prev-displays-commands) +- [Configure half split ratios](#configure-half-split-ratios) ## Keyboard Shortcuts @@ -517,3 +518,17 @@ By default, display order is left-to-right, line-by-line. You can change this to ```bash defaults write com.knollsoft.Rectangle screensOrderedByX -int 1 ``` + +## Configure half split ratios + +By default, left/right/top/bottom half actions split the screen 50/50. You can configure a custom split ratio (1–99) via the Extra Settings popover in the General tab of the preferences window, or via terminal: + +```bash +# Set left half to 60% width (right half will be 40%) +defaults write com.knollsoft.Rectangle horizontalSplitRatio -float 60 + +# Set top half to 60% height (bottom half will be 40%) +defaults write com.knollsoft.Rectangle verticalSplitRatio -float 60 +``` + +The default value for both is 50 (50/50 split). The drag-to-snap footprint preview also reflects the configured ratio. From 59e6623431f0a3a5a4e628e7eb2b57ca89f42e7f Mon Sep 17 00:00:00 2001 From: Igor Spivak Date: Thu, 19 Feb 2026 08:24:04 -0800 Subject: [PATCH 2/4] fix: Align half split ratio UI with existing extra settings layout Center the section header and pin the input fields' trailing edges to match the shortcut views, consistent with how Width Step is laid out. Remove the half split ratio section from TerminalCommands.md since it is now configurable in the GUI. Co-Authored-By: Claude Sonnet 4.6 --- .../PrefsWindow/SettingsViewController.swift | 5 ++++- TerminalCommands.md | 17 +---------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/Rectangle/PrefsWindow/SettingsViewController.swift b/Rectangle/PrefsWindow/SettingsViewController.swift index c992230b3..b5426a2fe 100644 --- a/Rectangle/PrefsWindow/SettingsViewController.swift +++ b/Rectangle/PrefsWindow/SettingsViewController.swift @@ -318,6 +318,7 @@ class SettingsViewController: NSViewController { let splitRatioHeaderLabel = NSTextField(labelWithString: NSLocalizedString("Half Split Ratios", tableName: "Main", value: "", comment: "")) splitRatioHeaderLabel.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) + splitRatioHeaderLabel.alignment = .center splitRatioHeaderLabel.translatesAutoresizingMaskIntoConstraints = false let hSplitLabel = NSTextField(labelWithString: NSLocalizedString("Horizontal (L/R, %)", tableName: "Main", value: "", comment: "")) @@ -558,7 +559,9 @@ class SettingsViewController: NSViewController { bottomVerticalTwoThirdsShortcutView.widthAnchor.constraint(equalToConstant: 160), hSplitField.widthAnchor.constraint(equalToConstant: 160), vSplitField.widthAnchor.constraint(equalToConstant: 160), - widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor) + widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor), + hSplitField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor), + vSplitField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor) ]) let containerView = NSView() diff --git a/TerminalCommands.md b/TerminalCommands.md index bfe76e116..a10f6b80b 100644 --- a/TerminalCommands.md +++ b/TerminalCommands.md @@ -35,7 +35,6 @@ The preferences window is purposefully slim, but there's a lot that can be modif - [Prevent a window that is quickly dragged above the menu bar from going into Mission Control](#prevent-a-window-that-is-quickly-dragged-above-the-menu-bar-from-going-into-mission-control) - [Change the behavior of double-click window title bar](#change-the-behavior-of-double-click-window-title-bar) - [Change the order of displays to order by x coordinate](#change-the-order-of-displays-to-order-by-x-coordinate-for-next-and-prev-displays-commands) -- [Configure half split ratios](#configure-half-split-ratios) ## Keyboard Shortcuts @@ -517,18 +516,4 @@ By default, display order is left-to-right, line-by-line. You can change this to ```bash defaults write com.knollsoft.Rectangle screensOrderedByX -int 1 -``` - -## Configure half split ratios - -By default, left/right/top/bottom half actions split the screen 50/50. You can configure a custom split ratio (1–99) via the Extra Settings popover in the General tab of the preferences window, or via terminal: - -```bash -# Set left half to 60% width (right half will be 40%) -defaults write com.knollsoft.Rectangle horizontalSplitRatio -float 60 - -# Set top half to 60% height (bottom half will be 40%) -defaults write com.knollsoft.Rectangle verticalSplitRatio -float 60 -``` - -The default value for both is 50 (50/50 split). The drag-to-snap footprint preview also reflects the configured ratio. +``` \ No newline at end of file From 07c82f9174a4c9478d1edffd896b12ccfecb9cb1 Mon Sep 17 00:00:00 2001 From: Igor Spivak Date: Thu, 19 Feb 2026 08:24:51 -0800 Subject: [PATCH 3/4] Remove CLAUDE.md from git tracking --- CLAUDE.md | 84 ------------------------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index b3c44761a..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,84 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Rectangle is a macOS window management app written in Swift (based on Spectacle). It uses the macOS Accessibility API to move and resize windows via keyboard shortcuts and drag-to-snap. Requires macOS 10.15+. - -- Bundle ID: `com.knollsoft.Rectangle` -- Launcher bundle ID: `com.knollsoft.RectangleLauncher` - -## Build & Test - -```bash -# Build (unsigned, for local development) -xcodebuild -scheme Rectangle CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO - -# Run tests -xcodebuild test -scheme Rectangle - -# Build archive (as CI does) -xcodebuild archive -scheme Rectangle -archivePath Rectangle.xcarchive -``` - -The project uses Xcode with SPM dependencies: a forked MASShortcut (keyboard shortcut recording) and Sparkle (auto-update). - -## Architecture - -### Core Data Flow - -``` -User Action → ShortcutManager - → WindowManager.execute(ExecutionParameters) - → WindowCalculationFactory (selects calculation by WindowAction) - → WindowCalculation.calculate() → CGRect - → WindowMover chain → AccessibilityElement.setFrame() -``` - -### Key Files - -- **`WindowAction.swift`**: Enum of 85+ window actions. Each action maps to a calculation and has a URL name (e.g. `left-half`, `maximize`). -- **`WindowManager.swift`**: Orchestrates window operations. Calls the factory, invokes movers, records history. -- **`WindowCalculationFactory.swift`**: Static registry mapping each `WindowAction` to its `WindowCalculation` subclass. -- **`AccessibilityElement.swift`**: Wrapper around macOS Accessibility API for getting/setting window frames. -- **`Defaults.swift`**: Central registry for 80+ `UserDefaults` preferences (gaps, margins, behavior flags, etc.). -- **`AppDelegate.swift`**: App lifecycle, initialization, menu bar setup, URL scheme handling. -- **`ShortcutManager.swift`**: Registers/unregisters keyboard shortcuts via MASShortcut. -- **`ScreenDetection.swift`**: Determines which display a window is on. - -### Window Calculations (`WindowCalculation/`) - -78 files, each implementing one positioning strategy as a subclass of `WindowCalculation`. Examples: `LeftHalfCalculation`, `MaximizeCalculation`, `NextDisplayCalculation`. To add a new window action: -1. Add a case to `WindowAction.swift` (with display name, URL name, optional default shortcut) -2. Create a `WindowCalculation` subclass -3. Register it in `WindowCalculationFactory` - -### Window Movers (`WindowMover/`) - -Chain of responsibility—`StandardWindowMover` is tried first, then `BestEffortWindowMover` as fallback. `CenteringFixedSizedWindowMover` handles fixed-size windows. `QuantizedWindowMover` snaps to pixel boundaries. - -### Snap-to-Edge (`Snapping/`) - -`SnappingManager` monitors global mouse events to detect window dragging toward screen edges. `FootprintWindow` renders the preview overlay. `SnapAreaModel` defines the hot zones; compound snap areas in `CompoundSnapArea/` handle multi-step drag interactions (e.g., drag to bottom-center after bottom-third). - -### Preferences (`PrefsWindow/`) - -`SettingsViewController.swift` (~38 KB) is the main preferences UI. `Config.swift` handles JSON import/export of settings. Preferences are stored in `~/Library/Preferences/com.knollsoft.Rectangle.plist`. - -## URL Scheme - -`rectangle://execute-action?name=[action-name]` — triggers any window action by its URL name. Also supports `rectangle://execute-task?name=ignore-app[&app-bundle-id=...]`. - -## Hidden Preferences - -Many advanced settings are set via `defaults write com.knollsoft.Rectangle ...`. See `TerminalCommands.md` for the full list. These map to keys in `Defaults.swift`. - -## Notable Conventions - -- **No async/await**: The codebase predates Swift 5.5 patterns; synchronous Accessibility API calls throughout. -- **Singletons**: `AppDelegate.instance`, `RectangleStatusItem.instance`. -- **Failure signal**: `NSSound.beep()` is used when a window action cannot be applied. -- **Multi-display**: Actions like "Next Display" cycle through `NSScreen.screens`; screen orientation affects which third is "first." -- **Stage Manager**: `StageUtil.swift` handles detection and special-casing for macOS Stage Manager. -- **Desktop widgets**: Excluded from tile-all and cascade-all operations (see `WindowUtil.swift`). From 5f2ef2dcc5a8fdab42b3a05161f78b997fe65139 Mon Sep 17 00:00:00 2001 From: Ryan Hanson Date: Sat, 21 Feb 2026 21:37:22 -0800 Subject: [PATCH 4/4] Fix missing comma from merge conflict resolution --- Rectangle/PrefsWindow/SettingsViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rectangle/PrefsWindow/SettingsViewController.swift b/Rectangle/PrefsWindow/SettingsViewController.swift index cbf7666b9..fa36e8a27 100644 --- a/Rectangle/PrefsWindow/SettingsViewController.swift +++ b/Rectangle/PrefsWindow/SettingsViewController.swift @@ -776,7 +776,7 @@ class SettingsViewController: NSViewController { bottomCenterLeftEighthShortcutView.widthAnchor.constraint(equalToConstant: 160), bottomCenterRightEighthShortcutView.widthAnchor.constraint(equalToConstant: 160), bottomRightEighthShortcutView.widthAnchor.constraint(equalToConstant: 160), - widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor) + widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor), hSplitField.widthAnchor.constraint(equalToConstant: 160), vSplitField.widthAnchor.constraint(equalToConstant: 160), widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor),