Fetching latest headlines…
I Built a Windows-Style Alt+Tab Window Switcher for macOS in Pure Swift
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’March 22, 2026

I Built a Windows-Style Alt+Tab Window Switcher for macOS in Pure Swift

2 views0 likes0 comments
Originally published byDev.to

One of my biggest frustrations after spending years on Windows was the loss of a proper window switcher when I moved to macOS. Cmd-Tab switches between applications, not windows. If you have four Terminal windows open, you cannot directly jump to a specific one β€” you have to Cmd-Tab to Terminal, then use Cmd-` to cycle through its windows. It is a two-step dance every time.

So I built AltTab β€” a lightweight, zero-dependency macOS utility that brings back the Windows Alt-Tab experience using Option-Tab.

What It Does

Hold Option and tap Tab β€” a panel appears showing thumbnails of every open window across all your apps. Cycle through them with Tab / Shift-Tab or the arrow keys, then release Option to jump straight to the selected window. Click any thumbnail to switch instantly.

Shortcut Action
Option-Tab Open switcher / next window
Tab Cycle forward
Shift-Tab Cycle backward
← / β†’ Navigate left / right
Escape Cancel
Enter Confirm and switch
Click Select and switch immediately

Minimized windows are included and will automatically unminimize when selected. No Dock icon, no clutter β€” just a menu bar icon.

AltTab menu bar menu showing Launch at Login, About AltTab, and Quit AltTab options

Installation

Requirements: macOS 13 Ventura or later Β· Full Xcode installation

One-liner install

bash
git clone https://github.com/sergio-farfan/alttab-macos.git
cd alttab-macos
./build.sh install
open ~/Applications/AltTab.app

Then open System Settings β†’ Privacy & Security β†’ Accessibility and grant permission to AltTab. That is all β€” press Option-Tab and it just works.

Build options

bash
./build.sh build # Build Release binary only
./build.sh install # Build and install to ~/Applications
./build.sh install --system # Build and install to /Applications (requires sudo)
./build.sh run # Build and launch immediately
./build.sh uninstall # Remove the installed app

Permissions

  • Accessibility (required) β€” needed to intercept the Option-Tab hotkey and raise windows
  • Screen Recording (optional) β€” enables live window thumbnails; gracefully falls back to app icons if denied

How It Works Under the Hood

Building this taught me a lot about low-level macOS APIs. Here are the most interesting technical pieces.

1. Global Hotkey Detection via CGEvent Tap

The most fundamental challenge: how do you intercept Option-Tab system-wide, in every app, without stealing keyboard focus?

The answer is a CGEvent tap at the session level:

swift
let tap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(
(1 << CGEventType.keyDown.rawValue) |
(1 << CGEventType.flagsChanged.rawValue)
),
callback: eventTapCallback,
userInfo: Unmanaged.passRetained(self).toOpaque()
)

This sits in front of every application and intercepts keyboard events before they are delivered. A simple 3-state machine (idle β†’ active β†’ idle) tracks whether the switcher is open. When the event is one we want to handle (Tab, arrows, Escape), we swallow it by returning nil. Everything else passes through untouched.

One subtle detail: we never swallow flagsChanged events (modifier key state). If we did, the system would get confused about which modifiers are held down.

Re-enable polling: macOS can disable your event tap if it is too slow or if the user is typing quickly. A 2-second timer watches for this and re-enables the tap. If the switcher was open when the tap was disabled, we force-cancel it β€” this prevents the panel from sticking on screen.

2. Discovering Every Window (Including Minimized Ones)

This was surprisingly tricky. No single API gives you a complete list.

On-screen windows come from CGWindowList:

swift
let list = CGWindowListCopyWindowInfo(
[.optionOnScreenOnly, .excludeDesktopElements],
kCGNullWindowID
)

But minimized windows are not in this list. For those, you have to query each application via AXUIElement:

swift
let appElement = AXUIElementCreateApplication(pid)
var windowsRef: CFTypeRef?
AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsRef)
// Then check kAXMinimizedAttribute on each window

Bridging AXUIElement back to a CGWindowID requires a private SPI:

swift
@_silgen_name("_AXUIElementGetWindow")
func _AXUIElementGetWindow(_ element: AXUIElement, _ windowID: inout CGWindowID) -> AXError

Yes, it is undocumented. But it is the same approach used by Alfred, Raycast, and other window managers β€” well-established in practice. The combined result is a complete, deduplicated window list.

3. Most-Recently-Used Window Ordering

Windows are shown most-recently-used first. Tracking this correctly is harder than it sounds.

NSWorkspace.didActivateApplicationNotification tells you which app is in front β€” but not which window within that app. To track intra-app focus changes (like Cmd-` between Terminal windows), I install a per-app AXObserver:

`swift
AXObserverCreate(pid, axObserverCallback, &observer)
AXObserverAddNotification(
observer!, appElement,
kAXFocusedWindowChangedNotification as CFString,
Unmanaged.passRetained(self).toOpaque()
)
CFRunLoopAddSource(CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer!), .defaultMode)
`

Each callback fires when the focused window changes within an app and promotes that window to the front of the MRU list.

4. The Non-Activating Panel Trick

The switcher UI is an NSPanel with .nonactivatingPanel in its style mask. This is critical.

A regular NSWindow would steal keyboard focus when shown, making Option-Tab act on AltTab itself rather than the target window. With .nonactivatingPanel, the panel appears on screen but the previously active app retains focus β€” so when you release Option, the system activates the window you selected, not AltTab.

`swift
let panel = NSPanel(
contentRect: .zero,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
panel.level = .floating
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
`

5. Live Thumbnails with ScreenCaptureKit

On macOS 14+, I use SCScreenshotManager for high-quality async captures:

`swift
let image = try await SCScreenshotManager.captureImage(
contentFilter,
configuration: config
)
`

On macOS 13, it falls back to the synchronous CGWindowListCreateImage on a background queue. If Screen Recording permission is denied, it silently shows the app icon instead. No crash, no prompt β€” just graceful degradation.

Project Stats

  • ~2,000 lines of Swift across 9 source files
  • Zero external dependencies β€” pure Swift + AppKit
  • MIT licensed β€” fork it, modify it, ship it
  • macOS 13+ (Ventura, Sonoma, Sequoia)

Try It Out

The code is on GitHub: github.com/sergio-farfan/alttab-macos

If you have been frustrated by macOS window switching, give it a try. And if you are curious about the low-level macOS APIs β€” CGEvent taps, AXUIElement, ScreenCaptureKit β€” the codebase is small enough to read in an afternoon.

Feedback, issues, and PRs are welcome!

Comments (0)

Sign in to join the discussion

Be the first to comment!