Snug is a menu bar app that tiles every window the moment it opens. Open a window and it drops into the next free tile; close one and the rest reflow with no holes. There's no config file, and it never asks for Screen Recording permission: it reads each window's position, size and owning app, and nothing else.
macOS has no window manager API, so everything goes through the Accessibility API (AXUIElement). Here's what that taught me.
1. Give every AX call one thread
Each AX call is a synchronous message to another app, so a busy or hung app can stall the caller. Snug runs one background thread that owns every AX call, every observer and the tiler's state. The main thread only draws UI.
A detail worth knowing: a thread's run loop exits immediately if it has no sources, so the AX thread adds a dummy port to keep it alive before any observer exists.
RunLoop.current.add(NSMachPort(), forMode: .default) // keeps the run loop alive with no sources
2. One observer per app, plus one per window
There's an AXObserver per running app, listening for window created, moved, resized, minimized and restored, focus changes, and the app hiding or showing. The catch: "destroyed" notifications are only delivered per window, so every managed window gets its own registration for kAXUIElementDestroyedNotification.
If an app isn't ready for observers yet (it's still launching), registration fails. Snug retries later instead of giving up on that app.
3. Batch events
Opening one window can fire a burst of notifications: created, moved, resized, focus changed. Reacting to each would mean laying out the screen four times. Snug batches window events for 60 ms and then runs one layout pass.
4. Don't react to your own moves
When Snug sets a window's frame, the app reports back that the window moved and resized, which would look exactly like the user dragging it. Without a filter, that's a feedback loop.
The fix is to remember what you wrote. A notification within 2 points of a frame Snug just wrote, and within 500 ms, is Snug's own:
/// A notification within 2 pt of a frame Snug wrote, within 500 ms, is Snug's own.
func isSelfCaused(_ id: CGWindowID, frame: CGRect) -> Bool {
guard let e = expected[id], Date().timeIntervalSince(e.at) < 0.5 else { return false }
return e.frames.contains { Self.close($0, frame) }
}
It also skips the write entirely when the window is already within 2 points of the target. Fewer writes, fewer echoes.
5. Apps say no
Ask a window for 300 points of width and some apps give you 500, because that's their minimum. You only find out by reading the frame back after the write. Snug records the smallest size each app has accepted, by bundle ID, in a small file in Application Support, and lays out with those minimums from then on. It learns each app's limits instead of guessing them.
6. The one private API
Accessibility elements don't expose the window's CGWindowID, which you need to match a window against the system window list (for example, to know which windows are on the current Space). Snug uses exactly one private function for it, declared in one place, with a public fallback that matches the window list by owner PID and frame:
@_silgen_name("_AXUIElementGetWindow")
func _AXUIElementGetWindow(_ element: AXUIElement, _ id: UnsafeMutablePointer<CGWindowID>) -> AXError
7. Windows can't fully leave the screen
Snug has chains: windows in one straight line that runs off the screen edge, which you slide through with ⌃⌥ and an arrow key. That needs windows positioned partly off screen, and macOS has opinions. What I measured on macOS 26:
- A window can leave the left or right edge with 1 point showing. Ask for more and it snaps back to 40 points.
- It can't go above the menu bar.
- At the bottom it keeps about 90 points on screen.
So a horizontal chain is one straight line running off both sides, and a vertical chain slides the windows it can't show out sideways. Where another display starts, the chain stops at the edge instead, or a window pushed there would land on the other screen.
8. Keep the layout math pure
All the layout maths (columns, rows, grid, thirds, the ultrawide "Center Stage" layout, chains, slot and stack state) lives in a Swift package with no AppKit or AX in it. That makes it testable without a single real window. Golden layout fixtures sit in one JSON file that both the Swift tests and the website's TypeScript tests read, so the live demo on the site follows the same layout rules as the app.
Results
On its performance page, a new window is placed in 78 ms (median), the app uses about 20 MB of memory and 0.0% CPU at idle, and the download is 2.5 MB.
Try it
Snug is $14.99 once for one Mac ($19.99 for two), macOS 15 or later. There's a working demo on the site: snug.macmuse.app.
If you're comparing options, here's an honest roundup of the best window managers for Mac, including the free ones and the ones that aren't mine.
Questions about the AX API welcome in the comments.
Top comments (0)