Skip to main content

Overview

Stash allows you to temporarily hide windows at the edges of your screen, keeping your workspace clean while maintaining quick access to important applications. Windows remain partially visible at the edge and automatically reveal when you hover near them or use a keybind. This feature is perfect for:
  • Monitoring apps (Activity Monitor, system stats)
  • Communication tools (Messages, Slack) that you check frequently
  • Reference materials you want nearby but not always visible
  • Music players or other utility apps

How Stashing Works

When you stash a window, Loop moves it to a screen edge with only a small portion remaining visible. The window automatically reveals when your mouse approaches the edge and hides again when you move away.

Basic Usage

1

Create a stash action

In Settings > Keybinds, create a new custom action with direction “Stash” and choose an edge (left, right, or bottom)
2

Assign a keybind

Record your keyboard shortcut (e.g., Trigger + S)
3

Stash a window

Focus the window you want to stash and press your keybind
4

Reveal on demand

Hover your mouse near the stashed edge to reveal the window, or press the same keybind again
Only one stashed window can be revealed at a time. Revealing a new window automatically hides the previously revealed one.

Stash Edges

Loop supports stashing windows at three screen edges:

Left Edge

Windows slide out from the left side of the screen

Right Edge

Windows slide out from the right side of the screen

Bottom Edge

Windows slide up from the bottom of the screen
Top edge stashing is not currently supported due to macOS menu bar conflicts.

Implementation Details

Stash functionality is managed by the StashManager singleton class:
Loop/Stashing/StashManager.swift
/// Manages the behavior of windows that can be temporarily hidden (stashed)
/// and revealed on screen edges.
final class StashManager {
    static let shared = StashManager()
    
    /// How many pixels of the window should be visible when stashed
    private var stashedWindowVisiblePadding: CGFloat {
        Defaults[.stashedWindowVisiblePadding]
    }
    
    /// Should the stashed windows be animated when revealed or hidden?
    private var animate: Bool {
        Defaults[.animateStashedWindows]
    }
}

Mouse-Based Revealing

StashManager listens for mouse movement events to reveal windows:
private func startListeningToRevealTriggers() {
    let monitor = PassiveEventMonitor(
        events: [
            .mouseMoved,        // Normal mouse movement
            .leftMouseDragged   // Dragging items to stashed windows
        ],
        callback: { [weak self] cgEvent in
            self?.handleMouseMoved(cgEvent: cgEvent)
        }
    )
    monitor.start()
}
The system uses debouncing to avoid excessive processing:
/// The time interval to debounce mouse moved events
private let mouseMovedDebounceInterval: TimeInterval = 0.05

/// The throttle interval for revealing/hiding windows
private let revealThrottleInterval: TimeInterval = 0.1

Stashed vs Revealed Frames

Each stashed window has two frame states:
func computeStashedFrame(peekSize: CGFloat) -> CGRect {
    // Window is mostly off-screen
    // Only `peekSize` pixels remain visible
    let frame = window.frame
    switch action.stashEdge {
    case .left:
        frame.origin.x = screen.frame.minX - frame.width + peekSize
    case .right:
        frame.origin.x = screen.frame.maxX - peekSize
    case .bottom:
        frame.origin.y = screen.frame.maxY - peekSize
    }
    return frame
}

Animation System

Reveal and hide transitions can be animated:
private func revealWindow(_ window: StashedWindowInfo) async {
    let frame = window.computeRevealedFrame()
    
    if animate {
        try? await window.window.setFrameAnimated(
            frame,
            bounds: .zero
        )
    } else {
        window.window.setFrame(frame)
    }
    
    store.markWindowAsRevealed(window.window.cgWindowID)
}

Configuration Options

stashedWindowVisiblePadding
number
default:"5"
How many pixels of the stashed window remain visible at the screen edge. Increase for easier mouse targeting.
animateStashedWindows
toggle
default:"true"
Enable smooth reveal/hide animations. Disable for instant transitions.
shiftFocusWhenStashed
toggle
default:"false"
Automatically focus revealed windows and shift focus away when hiding. When disabled, windows reveal without stealing focus.

Multi-Screen Support

StashManager intelligently handles multiple displays:
func getScreenForEdge(currentScreen: NSScreen, edge: StashEdge) -> NSScreen? {
    // Two screens are considered in the same "row" or "column" 
    // if they overlap by at least `threshold` points
    let threshold: CGFloat = 100
    
    return switch edge {
    case .left:
        currentScreen.leftmostScreenInSameRow(overlapThreshold: threshold)
    case .right:
        currentScreen.rightmostScreenInSameRow(overlapThreshold: threshold)
    case .bottom:
        currentScreen.bottommostScreenInSameColumn(overlapThreshold: threshold)
    }
}
This ensures:
  • Left stash uses the leftmost screen in the same row
  • Right stash uses the rightmost screen in the same row
  • Bottom stash uses the bottommost screen in the same column
Treat all screens as a unified virtual space when stashing. Loop automatically finds the appropriate screen edge.

Overlap Handling

StashManager prevents overlapping stashed windows:
/// Two windows can be stacked along the same edge as long as there is
/// enough non-overlapping space to allow the user to easily position 
/// the cursor over either window.
private let minimumVisibleSizeToKeepWindowStacked: CGFloat = 100

private func unstashOverlappingWindows(_ windowToStash: StashedWindowInfo) {
    for (id, stashedWindow) in store.stashed {
        // If trying to stash in the same place as another window
        if stashedWindow.action.id == windowToStash.action.id, 
           stashedWindow.screen.isSameScreen(windowToStash.screen) {
            // Replace the old window
            unstash(stashedWindow, resetFrame: true, resetFrameAnimated: animate)
        }
    }
}
When you stash a window where another is already stashed, the original window is automatically unstashed and restored to its previous position.

Reveal Triggers

Windows can be revealed in multiple ways:

1. Mouse Proximity

private func isMouseOverStashed(window: StashedWindowInfo, location: CGPoint) -> Bool {
    let stashedFrame = window.computeStashedFrame(peekSize: stashedWindowVisiblePadding)
    return stashedFrame.contains(location)
}
Move your mouse over the visible portion of the stashed window.

2. Keybind Toggle

if store.isWindowRevealed(stashedWindow.window.cgWindowID) {
    await hideWindow(stashedWindow)
} else {
    await revealWindow(stashedWindow)
}
Press the same keybind that stashed the window to toggle reveal/hide.

3. Application Activation

private func processFrontmostAppChange(with notification: Notification) {
    // If a stashed window's application becomes active through
    // non-mouse means (Spotlight, Cmd+Tab, etc.)
    if appWindow.cgWindowID == window.window.cgWindowID {
        await revealWindow(window)
    }
}
Activate the stashed application via Cmd+Tab or Spotlight.

Auto-Hide Behavior

Revealed windows automatically hide when:
private func shouldHide(window: StashedWindowInfo, for location: CGPoint) -> Bool {
    // Add tolerance to avoid hiding during minor cursor movement
    let tolerance: CGFloat = 15
    let revealedFrame = window.computeRevealedFrame().insetBy(dx: -tolerance, dy: -tolerance)
    let stashedFrame = window.computeStashedFrame(peekSize: stashedWindowVisiblePadding)
    
    return !revealedFrame.contains(location) && !stashedFrame.contains(location)
}
  • Mouse moves away from the window (with tolerance)
  • Another window is focused (if shiftFocusWhenStashed is enabled)
  • You stash a different window

Persistence

Stashed window states persist across app restarts:
func start() {
    store.restore()  // Restore stashed windows from previous session
}

func onApplicationWillTerminate() {
    // Move all stashed windows back on-screen before closing
    restoreAllStashedWindows(animate: false)
}
When Loop quits, all stashed windows are automatically restored to prevent “lost” windows.

Unstashing Windows

To permanently unstash a window:
1

Create an unstash action

Add a new keybind with direction “Unstash”
2

Focus the stashed window

Reveal it using mouse hover or the stash keybind
3

Trigger unstash

Press your unstash keybind to restore the window to its original position
Alternatively, moving or resizing a stashed window with Loop automatically unstashes it.

Advanced Features

Focus Management

When shiftFocusWhenStashed is enabled:
private func unfocus(_ windowID: CGWindowID) {
    // Find the first visible, non-stashed window on the same screen
    let focusWindow = WindowUtility.windowList().first { window in
        return store.stashed[window.cgWindowID] == nil
            && window.cgWindowID != windowID
            && !window.isApplicationHidden
            && !window.isWindowHidden
            && !window.minimized
    }
    
    focusWindow?.focus()
}
Loop automatically shifts focus to another window when hiding a stashed window.

Z-Index Sorting

Multiple stashed windows are managed by z-order:
private func getZSortedStashedWindows() -> [StashedWindowInfo] {
    // Leverage the fact that WindowEngine returns windows sorted by z-index
    WindowUtility.windowList().compactMap { store.stashed[$0.cgWindowID] }
}
If stashed windows overlap, only the topmost window reveals when you hover over the shared area.

Drag and Drop Support

Stashed windows support drag and drop:
let monitor = PassiveEventMonitor(
    events: [
        .mouseMoved,
        .leftMouseDragged  // Dragging items to stashed windows
    ],
    callback: { ... }
)
Drag files or content to the stashed window edge to reveal and interact with it.

Use Cases

Stash Activity Monitor or system stats at the edge. Quickly glance when needed without cluttering your main workspace.
Keep Messages or Slack stashed for quick replies while maintaining focus on your primary work.
Stash your music player (Spotify, Apple Music) for easy access to playback controls.
Stash documentation, calculators, or notes that you reference occasionally but don’t need constantly visible.
Stash a terminal window for quick command execution without switching spaces or minimizing.
Combine stash with different edges for different types of applications: communication on the left, monitoring on the right, utilities at the bottom.

Build docs developers (and LLMs) love