diff --git a/Rectangle/CooperativeCornerResize.swift b/Rectangle/CooperativeCornerResize.swift index 68604935c..92ced521e 100644 --- a/Rectangle/CooperativeCornerResize.swift +++ b/Rectangle/CooperativeCornerResize.swift @@ -314,6 +314,22 @@ struct CooperativeCornerResize { } } + /// Both edges along the axis moved: the window jumped to the other side (the `switchSides` + /// repeated-command mode) rather than resizing against a shared edge, so no neighbor gives way. + static func isSideSwitch(oldFocusedFrame: CGRect, + newFocusedFrame: CGRect, + axis: CornerCycleExpansionAxis, + tolerance: CGFloat) -> Bool { + switch axis { + case .horizontal: + return abs(oldFocusedFrame.minX - newFocusedFrame.minX) > tolerance + && abs(oldFocusedFrame.maxX - newFocusedFrame.maxX) > tolerance + case .vertical: + return abs(oldFocusedFrame.minY - newFocusedFrame.minY) > tolerance + && abs(oldFocusedFrame.maxY - newFocusedFrame.maxY) > tolerance + } + } + static func movedEdge(from oldFocusedFrame: CGRect, to newFocusedFrame: CGRect, axis: CornerCycleExpansionAxis, diff --git a/Rectangle/Localizable.xcstrings b/Rectangle/Localizable.xcstrings index 906ed3ac8..0f3905692 100644 --- a/Rectangle/Localizable.xcstrings +++ b/Rectangle/Localizable.xcstrings @@ -30265,6 +30265,16 @@ } } }, + "Switch to the opposite side on half actions" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "半屏操作切换到对侧" + } + } + } + }, "Third Fourth" : { "localizations" : { "ar" : { diff --git a/Rectangle/SubsequentExecutionMode.swift b/Rectangle/SubsequentExecutionMode.swift index c4eccdba5..219290dcc 100644 --- a/Rectangle/SubsequentExecutionMode.swift +++ b/Rectangle/SubsequentExecutionMode.swift @@ -9,6 +9,7 @@ enum SubsequentExecutionMode: Int { case acrossAndResize = 3 // across monitor for right/left, spectacle resize for all else case cycleMonitor = 4 case resizeAndCycleQuadrants = 5 + case switchSides = 6 // repeated half actions move to the opposite side var title: String { switch self { @@ -18,6 +19,7 @@ enum SubsequentExecutionMode: Int { case .acrossAndResize: return String(localized: "move to adjacent on left/right, or cycle size on side") case .cycleMonitor: return String(localized: "cycle through displays") case .resizeAndCycleQuadrants: return String(localized: "cycle positions on quadrants, cycle sizes on side") + case .switchSides: return String(localized: "Switch to the opposite side on half actions") } } @@ -28,7 +30,7 @@ enum SubsequentExecutionMode: Int { } } - static var ordered: [SubsequentExecutionMode] = [.none, .cycleMonitor, .resize, .acrossMonitor, .acrossAndResize, .resizeAndCycleQuadrants] + static var ordered: [SubsequentExecutionMode] = [.none, .cycleMonitor, .resize, .acrossMonitor, .acrossAndResize, .resizeAndCycleQuadrants, .switchSides] } class SubsequentExecutionDefault: Default { diff --git a/Rectangle/WindowCalculation/BottomHalfCalculation.swift b/Rectangle/WindowCalculation/BottomHalfCalculation.swift index 816acfa99..8a7b32b7f 100644 --- a/Rectangle/WindowCalculation/BottomHalfCalculation.swift +++ b/Rectangle/WindowCalculation/BottomHalfCalculation.swift @@ -10,6 +10,15 @@ class BottomHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalcul return tiledRect } + if Defaults.subsequentExecutionMode.value == .switchSides { + // Repeating Bottom Half while the window still sits on the bottom half moves it to the top + // half; see LeftRightHalfCalculation.calculateSwitchSides. + guard isRepeatedCommand(params) else { return calculateFirstRect(params) } + let opposite = RectCalculationParameters(window: params.window, visibleFrameOfScreen: params.visibleFrameOfScreen, + action: .topHalf, lastAction: params.lastAction) + return RectResult(WindowCalculationFactory.topHalfCalculation.calculateFirstRect(opposite).rect, resultingAction: .topHalf) + } + if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes { return calculateFirstRect(params) } diff --git a/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift b/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift index f53048ef1..679ea84b1 100644 --- a/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift +++ b/Rectangle/WindowCalculation/LeftRightHalfCalculation.swift @@ -26,7 +26,9 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal return calculateAcrossDisplays(params) case .resize, .resizeAndCycleQuadrants: return calculateResize(params) - case .none, .cycleMonitor: + case .switchSides where isRepeatedCommand(params): + return calculateSwitchSides(params) + case .none, .cycleMonitor, .switchSides: let screen = usableScreens.currentScreen let oneHalfRect = calculateFirstRect(params.asRectParams()) return WindowCalculationResult(rect: oneHalfRect.rect, screen: screen, resultingAction: params.action) @@ -56,6 +58,16 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal return WindowCalculationResult(rect: rectResult.rect, screen: screen, resultingAction: params.action) } + /// Repeating a half action while the window still sits on that half moves it to the opposite + /// half (Left Half → Right Half → Left Half). Reporting the opposite half as the resulting action + /// is what makes the next repeat flip back; a first execution, or a window that has moved since, + /// gets a plain half above. + func calculateSwitchSides(_ params: WindowCalculationParameters) -> WindowCalculationResult? { + let opposite: WindowAction = params.action == .rightHalf ? .leftHalf : .rightHalf + let oppositeRect = calculateFirstRect(params.asRectParams(differentAction: opposite)) + return WindowCalculationResult(rect: oppositeRect.rect, screen: params.usableScreens.currentScreen, resultingAction: opposite) + } + func calculateAcrossDisplays(_ params: WindowCalculationParameters) -> WindowCalculationResult? { let screen = params.usableScreens.currentScreen return params.action == .rightHalf diff --git a/Rectangle/WindowCalculation/TopHalfCalculation.swift b/Rectangle/WindowCalculation/TopHalfCalculation.swift index 99fbb973c..0516eaa8c 100644 --- a/Rectangle/WindowCalculation/TopHalfCalculation.swift +++ b/Rectangle/WindowCalculation/TopHalfCalculation.swift @@ -10,6 +10,15 @@ class TopHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculati return tiledRect } + if Defaults.subsequentExecutionMode.value == .switchSides { + // Repeating Top Half while the window still sits on the top half moves it to the bottom + // half; see LeftRightHalfCalculation.calculateSwitchSides. + guard isRepeatedCommand(params) else { return calculateFirstRect(params) } + let opposite = RectCalculationParameters(window: params.window, visibleFrameOfScreen: params.visibleFrameOfScreen, + action: .bottomHalf, lastAction: params.lastAction) + return RectResult(WindowCalculationFactory.bottomHalfCalculation.calculateFirstRect(opposite).rect, resultingAction: .bottomHalf) + } + if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes { return calculateFirstRect(params) } diff --git a/Rectangle/WindowCalculation/WindowCalculation.swift b/Rectangle/WindowCalculation/WindowCalculation.swift index 09af29f4f..328308d9a 100644 --- a/Rectangle/WindowCalculation/WindowCalculation.swift +++ b/Rectangle/WindowCalculation/WindowCalculation.swift @@ -37,11 +37,18 @@ class WindowCalculation: Calculation { } func isRepeatedCommand(_ params: WindowCalculationParameters) -> Bool { - if let lastAction = params.lastAction, lastAction.action == params.action { - let normalizedLastRect = lastAction.rect.screenFlipped - return normalizedLastRect == params.window.rect - } - return false + isRepeatedCommand(lastAction: params.lastAction, action: params.action, windowRect: params.window.rect) + } + + func isRepeatedCommand(_ params: RectCalculationParameters) -> Bool { + isRepeatedCommand(lastAction: params.lastAction, action: params.action, windowRect: params.window.rect) + } + + /// The same action was just executed on this window and the window has not moved since. + private func isRepeatedCommand(lastAction: RectangleAction?, action: WindowAction, windowRect: CGRect) -> Bool { + guard let lastAction, lastAction.action == action else { return false } + let normalizedLastRect = lastAction.rect.screenFlipped + return normalizedLastRect == windowRect } } diff --git a/Rectangle/WindowManager+CooperativeCornerResize.swift b/Rectangle/WindowManager+CooperativeCornerResize.swift index 66abc1df1..c3bd6976a 100644 --- a/Rectangle/WindowManager+CooperativeCornerResize.swift +++ b/Rectangle/WindowManager+CooperativeCornerResize.swift @@ -107,6 +107,16 @@ extension WindowManager { let tolerance = CooperativeCornerResize.detectionTolerance(screenFrame: screenFrame, configuredGap: gapSize) let captureTolerance = CooperativeCornerResize.captureTolerance(screenFrame: screenFrame, axis: cooperativeAxis) let isRepeatedCooperativeAction = action.isCompatibleRepeatedResizeAction(with: lastRectangleAction?.action) + if isRepeatedCooperativeAction, !action.isCooperativeCornerAction, + Defaults.subsequentExecutionMode.value == .switchSides, + CooperativeCornerResize.isSideSwitch(oldFocusedFrame: oldFocusedFrame, newFocusedFrame: newFocusedFrame, + axis: cooperativeAxis, tolerance: tolerance) { + // A repeated half action that switched sides is a move, not a resize of the shared edge: + // planned as one, the action's moved edge would be pushed across the screen, squeezing the + // neighbor and recording a wrong split ratio. Corners cycle quadrant positions through their + // own cooperative path and keep it. + return nil + } let actionDescription = isRepeatedCooperativeAction ? "repeated cooperative resize" : "initial corner/side cooperative placement" let screenFrameAX = screenFrame.screenFlipped let elementsById = AccessibilityElement.getAllWindowElements().reduce(into: [CGWindowID: AccessibilityElement]()) { elements, element in diff --git a/RectangleTests/RectangleTests.swift b/RectangleTests/RectangleTests.swift index 28684b88f..74facf7c2 100644 --- a/RectangleTests/RectangleTests.swift +++ b/RectangleTests/RectangleTests.swift @@ -9086,3 +9086,242 @@ final class FootprintGeometrySynchronizationTests: XCTestCase { } +final class SwitchSidesRepeatedCommandTests: XCTestCase { + private let windowId: CGWindowID = 4242 + private let screen = RepeatedMaximizeTestScreen(frame: CGRect(x: 0, y: 0, width: 1000, height: 600)) + private let floating = CGRect(x: 100, y: 100, width: 400, height: 300) + private var savedMode: SubsequentExecutionMode = .resize + private var savedPreserveOtherAxis = false + private var savedHorizontalSplitRatio: Float = 50 + private var savedVerticalSplitRatio: Float = 50 + private var savedGapSize: Float = 0 + + override func setUp() { + super.setUp() + savedMode = Defaults.subsequentExecutionMode.value + savedPreserveOtherAxis = Defaults.halvesPreserveOtherAxisSize.enabled + savedHorizontalSplitRatio = Defaults.horizontalSplitRatio.value + savedVerticalSplitRatio = Defaults.verticalSplitRatio.value + savedGapSize = Defaults.gapSize.value + Defaults.subsequentExecutionMode.value = .switchSides + Defaults.halvesPreserveOtherAxisSize.enabled = false + Defaults.horizontalSplitRatio.value = 50 + Defaults.verticalSplitRatio.value = 50 + Defaults.gapSize.value = 0 + ActiveSideSplitRatios.shared.resetAll() + } + + override func tearDown() { + Defaults.subsequentExecutionMode.value = savedMode + Defaults.halvesPreserveOtherAxisSize.enabled = savedPreserveOtherAxis + Defaults.horizontalSplitRatio.value = savedHorizontalSplitRatio + Defaults.verticalSplitRatio.value = savedVerticalSplitRatio + Defaults.gapSize.value = savedGapSize + ActiveSideSplitRatios.shared.resetAll() + super.tearDown() + } + + // MARK: - Left and right halves + + func testRepeatedLeftHalfSwitchesToTheRightHalfAndBack() throws { + let first = try XCTUnwrap(calculate(.leftHalf, window: floating, last: nil)) + XCTAssertEqual(first.resultingAction, .leftHalf) + assertRect(first.rect, plainHalf(.leftHalf)) + + let second = try XCTUnwrap(calculate(.leftHalf, window: first.rect, last: (first.resultingAction, first.rect))) + XCTAssertEqual(second.resultingAction, .rightHalf) + assertRect(second.rect, plainHalf(.rightHalf)) + + let third = try XCTUnwrap(calculate(.leftHalf, window: second.rect, last: (second.resultingAction, second.rect))) + XCTAssertEqual(third.resultingAction, .leftHalf) + assertRect(third.rect, plainHalf(.leftHalf)) + } + + func testRepeatedRightHalfSwitchesToTheLeftHalf() throws { + let first = try XCTUnwrap(calculate(.rightHalf, window: floating, last: nil)) + XCTAssertEqual(first.resultingAction, .rightHalf) + + let second = try XCTUnwrap(calculate(.rightHalf, window: first.rect, last: (first.resultingAction, first.rect))) + XCTAssertEqual(second.resultingAction, .leftHalf) + assertRect(second.rect, plainHalf(.leftHalf)) + } + + func testPressingTheOtherSideMovesThereWithoutSwitching() throws { + // Left Half, then Right Half: the window simply goes right; switching only happens on a repeat. + let left = try XCTUnwrap(calculate(.leftHalf, window: floating, last: nil)) + let right = try XCTUnwrap(calculate(.rightHalf, window: left.rect, last: (left.resultingAction, left.rect))) + XCTAssertEqual(right.resultingAction, .rightHalf) + assertRect(right.rect, plainHalf(.rightHalf)) + } + + func testAWindowThatMovedSinceTheLastActionGetsAPlainHalf() throws { + let first = try XCTUnwrap(calculate(.leftHalf, window: floating, last: nil)) + let moved = first.rect.offsetBy(dx: 0, dy: 10) + let result = try XCTUnwrap(calculate(.leftHalf, window: moved, last: (.leftHalf, first.rect))) + XCTAssertEqual(result.resultingAction, .leftHalf) + assertRect(result.rect, plainHalf(.leftHalf)) + } + + func testAnUnrelatedLastActionGetsAPlainHalf() throws { + let maximized = screen.frame + let result = try XCTUnwrap(calculate(.leftHalf, window: maximized, last: (.maximize, maximized))) + XCTAssertEqual(result.resultingAction, .leftHalf) + assertRect(result.rect, plainHalf(.leftHalf)) + } + + func testTheOppositeSideUsesTheComplementarySplitRatio() throws { + Defaults.horizontalSplitRatio.value = 60 + Defaults.verticalSplitRatio.value = 60 + ActiveSideSplitRatios.shared.resetAll() + let visible = screen.adjustedVisibleFrame() + + let left = try XCTUnwrap(calculate(.leftHalf, window: floating, last: nil)) + XCTAssertEqual(left.rect.width, floor(visible.width * 0.6), accuracy: 1) + let right = try XCTUnwrap(calculate(.leftHalf, window: left.rect, last: (left.resultingAction, left.rect))) + XCTAssertEqual(right.rect.width, floor(visible.width * 0.4), accuracy: 1) + XCTAssertEqual(right.rect.maxX, visible.maxX, accuracy: 0.5) + let back = try XCTUnwrap(calculate(.leftHalf, window: right.rect, last: (right.resultingAction, right.rect))) + XCTAssertEqual(back.rect.width, floor(visible.width * 0.6), accuracy: 1) + XCTAssertEqual(back.rect.minX, visible.minX, accuracy: 0.5) + + let top = try XCTUnwrap(calculate(.topHalf, window: floating, last: nil)) + XCTAssertEqual(top.rect.height, floor(visible.height * 0.6), accuracy: 1) + let bottom = try XCTUnwrap(calculate(.topHalf, window: top.rect, last: (top.resultingAction, top.rect))) + XCTAssertEqual(bottom.rect.height, floor(visible.height * 0.4), accuracy: 1) + XCTAssertEqual(bottom.rect.minY, visible.minY, accuracy: 0.5) + } + + // MARK: - Top and bottom halves + + func testRepeatedTopHalfSwitchesToTheBottomHalfAndBack() throws { + let first = try XCTUnwrap(calculate(.topHalf, window: floating, last: nil)) + XCTAssertEqual(first.resultingAction, .topHalf) + + let second = try XCTUnwrap(calculate(.topHalf, window: first.rect, last: (first.resultingAction, first.rect))) + XCTAssertEqual(second.resultingAction, .bottomHalf) + assertRect(second.rect, plainHalf(.bottomHalf)) + + let third = try XCTUnwrap(calculate(.topHalf, window: second.rect, last: (second.resultingAction, second.rect))) + XCTAssertEqual(third.resultingAction, .topHalf) + assertRect(third.rect, plainHalf(.topHalf)) + } + + func testRepeatedBottomHalfSwitchesToTheTopHalf() throws { + let first = try XCTUnwrap(calculate(.bottomHalf, window: floating, last: nil)) + let second = try XCTUnwrap(calculate(.bottomHalf, window: first.rect, last: (first.resultingAction, first.rect))) + XCTAssertEqual(second.resultingAction, .topHalf) + assertRect(second.rect, plainHalf(.topHalf)) + } + + // MARK: - Interplay with other options + + func testPlainHalvesStillSwitchWhenHalvesPreserveTheOtherAxis() throws { + Defaults.halvesPreserveOtherAxisSize.enabled = true + + let left = try XCTUnwrap(calculate(.leftHalf, window: floating, last: nil)) + let right = try XCTUnwrap(calculate(.leftHalf, window: left.rect, last: (left.resultingAction, left.rect))) + XCTAssertEqual(right.resultingAction, .rightHalf) + assertRect(right.rect, plainHalf(.rightHalf)) + + let top = try XCTUnwrap(calculate(.topHalf, window: floating, last: nil)) + let bottom = try XCTUnwrap(calculate(.topHalf, window: top.rect, last: (top.resultingAction, top.rect))) + XCTAssertEqual(bottom.resultingAction, .bottomHalf) + assertRect(bottom.rect, plainHalf(.bottomHalf)) + } + + func testAQuarterKeepsItsPlaceWhenHalvesPreserveTheOtherAxis() throws { + Defaults.halvesPreserveOtherAxisSize.enabled = true + let visible = screen.adjustedVisibleFrame() + let quarter = HalfSplitFrameCalculation.cornerRect(in: visible, horizontalSide: .leading, verticalSide: .leading, + horizontalFraction: 0.5, verticalFraction: 0.5) + + let result = try XCTUnwrap(calculate(.topHalf, window: quarter, last: (.topLeft, quarter))) + XCTAssertEqual(result.resultingAction, .topLeft) + assertRect(result.rect, quarter) + } + + func testASideSwitchIsNotACooperativeResize() { + let left = CGRect(x: 0, y: 0, width: 500, height: 600) + let right = CGRect(x: 500, y: 0, width: 500, height: 600) + let widerLeft = CGRect(x: 0, y: 0, width: 667, height: 600) + XCTAssertTrue(CooperativeCornerResize.isSideSwitch(oldFocusedFrame: left, newFocusedFrame: right, axis: .horizontal, tolerance: 4)) + XCTAssertFalse(CooperativeCornerResize.isSideSwitch(oldFocusedFrame: left, newFocusedFrame: widerLeft, axis: .horizontal, tolerance: 4)) + XCTAssertFalse(CooperativeCornerResize.isSideSwitch(oldFocusedFrame: left, newFocusedFrame: left, axis: .horizontal, tolerance: 4)) + + let top = CGRect(x: 0, y: 300, width: 1000, height: 300) + let bottom = CGRect(x: 0, y: 0, width: 1000, height: 300) + XCTAssertTrue(CooperativeCornerResize.isSideSwitch(oldFocusedFrame: top, newFocusedFrame: bottom, axis: .vertical, tolerance: 4)) + XCTAssertFalse(CooperativeCornerResize.isSideSwitch(oldFocusedFrame: top, newFocusedFrame: bottom, axis: .horizontal, tolerance: 4)) + } + + // MARK: - Scope of the mode + + func testOtherModesDoNotSwitchSides() throws { + for mode in SubsequentExecutionMode.ordered where mode != .switchSides { + Defaults.subsequentExecutionMode.value = mode + for action in [WindowAction.leftHalf, .rightHalf, .topHalf, .bottomHalf] { + let first = try XCTUnwrap(calculate(action, window: floating, last: nil)) + let second = try XCTUnwrap(calculate(action, window: first.rect, last: (first.resultingAction, first.rect))) + XCTAssertEqual(second.resultingAction, action, "\(mode) \(action)") + } + } + } + + func testThirdsKeepTogglingInTheSwitchSidesMode() { + let visibleFrame = CGRect(x: 0, y: 0, width: 900, height: 600) + let calculation = WindowCalculationFactory.calculationsByAction[.firstTwoThirds]! + func params(window: CGRect, last: RectangleAction?) -> RectCalculationParameters { + RectCalculationParameters(window: Window(id: windowId, rect: window), + visibleFrameOfScreen: visibleFrame, + action: .firstTwoThirds, + lastAction: last) + } + let first = calculation.calculateRect(params(window: visibleFrame, last: nil)) + XCTAssertEqual(first.rect.minX, 0) + XCTAssertEqual(first.rect.width, 600) + + let repeated = calculation.calculateRect(params(window: first.rect, + last: RectangleAction(action: .firstTwoThirds, subAction: first.subAction, rect: first.rect, count: 1))) + XCTAssertEqual(repeated.rect.minX, 300) + XCTAssertEqual(repeated.rect.width, 600) + XCTAssertNotEqual(repeated.subAction, first.subAction) + + let back = calculation.calculateRect(params(window: repeated.rect, + last: RectangleAction(action: .firstTwoThirds, subAction: repeated.subAction, rect: repeated.rect, count: 2))) + XCTAssertEqual(back.rect.minX, 0) + } + + func testTheModeIsOfferedAndNeitherResizesNorTraverses() { + XCTAssertTrue(SubsequentExecutionMode.ordered.contains(.switchSides)) + XCTAssertEqual(SubsequentExecutionMode(rawValue: 6), .switchSides) + XCTAssertFalse(SubsequentExecutionMode.switchSides.resizes) + XCTAssertFalse(Defaults.subsequentExecutionMode.resizes) + XCTAssertFalse(Defaults.subsequentExecutionMode.traversesDisplays) + XCTAssertFalse(Defaults.subsequentExecutionMode.cyclesQuadrantPositions) + } + + // MARK: - Helpers + + /// Runs the action's calculation the way WindowManager does: the window in screen coordinates, + /// and the recorded rect flipped as `recordAction` stores it, so a repeat is recognised when they match. + private func calculate(_ action: WindowAction, window: CGRect, last: (WindowAction, CGRect)?) -> WindowCalculationResult? { + let params = WindowCalculationParameters(window: Window(id: windowId, rect: window), + usableScreens: UsableScreens(currentScreen: screen, numScreens: 1), + action: action, + lastAction: last.map { RectangleAction(action: $0.0, subAction: nil, rect: $0.1.screenFlipped, count: 1) }, + ignoreTodo: false) + return WindowCalculationFactory.calculationsByAction[action]!.calculate(params) + } + + /// What a first execution of the action produces on this screen. + private func plainHalf(_ action: WindowAction) -> CGRect { + calculate(action, window: floating, last: nil)?.rect ?? .null + } + + private func assertRect(_ rect: CGRect, _ expected: CGRect, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(rect.minX, expected.minX, accuracy: 0.5, "minX", file: file, line: line) + XCTAssertEqual(rect.minY, expected.minY, accuracy: 0.5, "minY", file: file, line: line) + XCTAssertEqual(rect.width, expected.width, accuracy: 0.5, "width", file: file, line: line) + XCTAssertEqual(rect.height, expected.height, accuracy: 0.5, "height", file: file, line: line) + } +} diff --git a/TerminalCommands.md b/TerminalCommands.md index 2e972d5cf..95152288d 100644 --- a/TerminalCommands.md +++ b/TerminalCommands.md @@ -71,6 +71,8 @@ defaults write com.knollsoft.Rectangle subsequentExecutionMode -int 2 2: disabled 3: cycle displays for left/right actions, halves to thirds for the rest (old Rectangle behavior) 4: repeat same action on next display +5: cycle positions on quadrants, cycle sizes on side +6: switch to the opposite side on half actions ## Cycle thirds on repeated Center Half commands