Skip to main content

Overview

Cycles are one of Loop’s most powerful features, enabling you to perform multiple window manipulations in quick succession by pressing the same key combination repeatedly or by left-clicking multiple times on the radial menu. Instead of memorizing different shortcuts for related actions (like thirds and halves on the same edge), you can create a cycle that transitions through them sequentially.

How Cycles Work

A cycle is a sequence of window actions that Loop remembers on a per-window basis. Each time you trigger the same cycle keybind, Loop advances to the next action in the sequence.

Example Workflow

Consider a cycle with these actions:
  1. Left Half
  2. Left Two Thirds
  3. Left Third
1

First press

Trigger + C: Window moves to left half
2

Second press

Trigger + C: Window resizes to left two thirds
3

Third press

Trigger + C: Window shrinks to left third
4

Fourth press

Trigger + C: Cycle restarts at left half
Loop remembers where each window is in its cycle progression. Switching between windows maintains their individual cycle positions.

Creating Cycles

Navigate to Settings > Keybinds to create custom cycles:
1

Add a new keybind

Click “Add” in the Keybinds section
2

Select 'Cycle' as the action

Choose “Cycle” from the action type dropdown
3

Name your cycle

Give it a descriptive name (e.g., “Left Edge Sizes” or “Center Variations”)
4

Add cycle actions

Click “Add” in the cycle configuration modal to add actions to the sequence
5

Assign a keybind

Record your keyboard shortcut

Configuration Options

Cycle Behavior Settings

Navigate to Settings > Keybinds > Cycles to configure global cycle behavior:
Always start cycles from first item
toggle
default:"false"
When enabled, every cycle activation starts from the first action instead of resuming from the last position.Default behavior: Loop remembers each window’s position in the cycle and resumes from there.
Cycle backward with Shift
toggle
default:"false"
Hold Shift while triggering a cycle to move backward through the sequence.
This option is only available if Shift is not part of your trigger key.

Implementation Details

Cycles are implemented as a special WindowAction type with an array of child actions:
Loop/Window Management/Window Action/WindowAction.swift
struct WindowAction {
    var direction: WindowDirection
    var cycle: [WindowAction]?  // Cycle-specific property
    
    /// Initializes a cycle without a name or keybind
    init(_ cycle: [WindowAction]) {
        self.id = UUID()
        self.direction = .cycle
        self.cycle = cycle
    }
    
    /// Initializes a named cycle with keybind
    init(_ name: String? = nil, cycle: [WindowAction], keybind: Set<CGKeyCode> = []) {
        self.id = UUID()
        self.direction = .cycle
        self.name = name
        self.cycle = cycle
        self.keybind = keybind
    }
}

Cycle Direction Check

Cycles are identified by their direction:
if direction == .cycle {
    result = if let name, !name.isEmpty {
        name
    } else {
        .init(localized: .init("Custom Cycle", defaultValue: "Custom Cycle"))
    }
}

Reverse Cycling Eligibility

Only certain cycles support backward traversal:
var eligibleForReverseCycle: Bool {
    direction == .cycle && !keybind.contains(.kVK_Shift)
}
Cycles that use Shift in their keybind cannot support reverse cycling (since Shift is the reverse modifier).

Cycle Configuration View

The cycle configuration interface is implemented in CycleActionConfigurationView:
Loop/Settings Window/Settings/Keybinds/Modal Views/CycleActionConfigurationView.swift
struct CycleActionConfigurationView: View {
    @Binding var windowAction: WindowAction
    @State private var selectedKeybinds = Set<WindowAction>()
    
    var body: some View {
        VStack(spacing: 12) {
            // Name field
            LuminareTextField("Cycle Keybind", text: Binding(
                get: { action.name ?? "" }, 
                set: { action.name = $0 }
            ))
            
            // Add/Remove buttons
            Button("Add") {
                if action.cycle == nil {
                    action.cycle = []
                }
                action.cycle?.insert(.init(.noAction), at: 0)
            }
            
            // List of cycle actions
            LuminareList(items: $action.cycle) { item in
                KeybindItemView(item, cycleIndex: ...)
            }
        }
    }
}
Cycle actions are displayed with their index number, making it easy to visualize the sequence.

Use Cases

Progressive Resizing

Create cycles that gradually resize windows: Right Edge Progression:
  1. Right Half → Right Two Thirds → Right Third
Maximize Variations:
  1. Maximize → Almost Maximize → Center

Multi-Screen Workflows

Combine screen switching with positioning: Cross-Screen Cycle:
  1. Right Half (current screen)
  2. Next Screen
  3. Left Half (new screen)
  4. Previous Screen

Vertical Stacking

Cycle through vertical arrangements: Vertical Layout Cycle:
  1. Top Half
  2. Top Two Thirds
  3. Maximize Height
  4. Center

Radial Menu Integration

Cycles work seamlessly with the radial menu:
  • Left-click: Step forward through cycle actions
  • Visual feedback: The radial menu shows the current cycle action
  • Angle display: Each action in the cycle displays at its appropriate angle
From the Radial Menu configuration documentation:
“Left-click to step through cycle actions.” - Settings > Theming > Radial Menu > Actions

Per-Window Memory

Loop tracks cycle state independently for each window:
// Conceptual implementation
struct WindowRecords {
    private static var cyclePositions: [CGWindowID: Int] = [:]
    
    static func getCyclePosition(for windowID: CGWindowID) -> Int {
        return cyclePositions[windowID] ?? 0
    }
    
    static func advanceCycle(for windowID: CGWindowID, in cycle: [WindowAction]) {
        let currentPosition = getCyclePosition(for: windowID)
        let nextPosition = (currentPosition + 1) % cycle.count
        cyclePositions[windowID] = nextPosition
    }
}
This allows you to:
  • Have different windows at different points in the same cycle
  • Switch between windows without losing cycle state
  • Use the same cycle keybind across all applications

Advanced Configuration

Restart Behavior

When “Always start cycles from first item” is enabled:
if Defaults[.cycleModeRestartEnabled] {
    // Always start from index 0
    currentCycleIndex = 0
} else {
    // Resume from last known position
    currentCycleIndex = WindowRecords.getCyclePosition(for: window.id)
}
This is useful for:
  • Enabled: Predictable, consistent behavior every time
  • Disabled (default): Contextual resizing that remembers your preferences

Backward Cycling

When “Cycle backward with Shift” is enabled:
if pressedKeys.contains(.kVK_Shift) && action.eligibleForReverseCycle {
    // Move backward through cycle
    currentIndex = (currentIndex - 1 + cycle.count) % cycle.count
} else {
    // Move forward through cycle  
    currentIndex = (currentIndex + 1) % cycle.count
}
1

Forward

Trigger + C: Move to next action
2

Backward

Trigger + Shift + C: Move to previous action

Best Practices

3-5 actions per cycle is optimal. Longer cycles become hard to remember and use.
Place your most-used action first in the cycle for quick access.
Name cycles clearly (“Left Sizes”, “Vertical Stack”) so you can identify them easily.
Try out your cycle configurations with real workflows before committing to them.
Combine cycles with the radial menu for maximum flexibility: use keybinds for your most frequent cycles, and the radial menu for occasional adjustments.

Build docs developers (and LLMs) love