FFWF: Fast Fuzzy Window Finder for macOS
macOS has a built-in window switcher (Control+F4). It’s two steps: activate the list, then select a window. It has no memory of what you searched for last. Every time you switch, you start from scratch.
FFWF (Fast Fuzzy Window Finder) solves this. Menu bar app with global hotkey. Type to fuzzy filter windows by title. Windows stay visible in the list. Use arrow keys to select. Hit Enter. Done. Remembers your last search. Works with VoiceOver.
How It Works#
Press Option+Shift+Space (configurable). Popover appears showing all windows across all apps. Type any characters - “chme” matches “Chrome”. Space-separated terms for precision - “safari readme” matches “README.md - Safari”.
Windows stay in the filtered list. Use arrow keys to navigate. Enter switches to that window. Escape cancels.
The fuzzy matcher uses Sublime Text’s algorithm: consecutive characters, word boundaries, camelCase awareness, position scoring. Exact matches and prefix matches rank higher.
Fast by Design#
Pre-lowercased strings - Window titles lowercased once during enumeration, not on every keystroke.
Concurrent enumeration - Processes applications in parallel using DispatchQueue.concurrentPerform.
Early returns - Fuzzy matcher bails on non-matches immediately instead of scoring everything.
Icon caching - App icons fetched once per app, not per window.
Event-driven updates - NSWorkspace observers for app launch/quit. Background refresh every 0.5s only while popover is visible. Zero overhead when hidden.
Efficient diffing - SwiftUI List uses stable IDs. Minimal re-renders on filter changes.
Why FFWF is Ideal for VoiceOver Users#
macOS window management with VoiceOver is frustrating. Mission Control requires visual scanning. The built-in window switcher (Control+F4) provides a list but no search. You navigate linearly through every window until you find the one you want.
FFWF changes this completely. Type a few characters. VoiceOver announces the match count. Arrow down once or twice. You’re there.
Full VoiceOver integration:
- Window count announced when popover opens: “12 windows available”
- Selected window announced: “Safari - Documentation, window 3 of 12”
- Filter changes announced: “5 matches found”
- Search field always focused - type immediately
- Keyboard-only navigation - no mouse required
The memory feature helps VoiceOver users even more. Type “slack” once. Next time, “slack” is pre-filled. Arrow down to select if Slack is in the same position. Or clear and type something new.
Linear navigation through 20+ windows takes time. Fuzzy filtering reduces it to seconds. For VoiceOver users managing many windows, this is the difference between productive and frustrated.
Memory of Last Search#
Type “slack” to find Slack window. Switch to it. Press hotkey again later. Search field still has “slack” pre-filled. Arrow down to select if Slack is in the same position. Or clear and type something new.
This memory saves keystrokes when repeatedly switching to the same app. macOS built-in forgets immediately.
Keyboard-First Workflow#
No clicking. Press hotkey, type filter, arrow keys, Enter. Entire workflow from keyboard.
Option+Shift+Space → Opens popover
Type "term" → Filters windows
↑/↓ → Navigate selection
Enter → Switch to window
Escape → Cancel
For rapid switching, muscle memory develops. Same search, same position in list, same arrow key count.
Window Enumeration#
Uses macOS Accessibility API (AXUIElement) to enumerate windows per application:
// Simplified core
func enumerateWindows() -> [WindowInfo] {
let workspace = NSWorkspace.shared
let runningApps = workspace.runningApplications
return runningApps.flatMap { app -> [WindowInfo] in
let axApp = AXUIElementCreateApplication(app.processIdentifier)
let windows = getWindowList(axApp)
return windows.map { window in
WindowInfo(
title: getWindowTitle(window),
appName: app.localizedName ?? "",
processID: app.processIdentifier,
icon: app.icon
)
}
}
}
NSWorkspace observers detect app launches and quits, triggering window list refresh.
Search Algorithm#
Fuzzy matching with scoring:
func fuzzyMatch(_ query: String, _ target: String) -> Int? {
var score = 0
var targetIndex = target.startIndex
for char in query {
guard let foundIndex = target[targetIndex...].firstIndex(of: char) else {
return nil // Early return if char not found
}
// Score based on position and context
if foundIndex == targetIndex {
score += 10 // Consecutive match
}
if isWordBoundary(target, foundIndex) {
score += 5 // Word start
}
if isCamelCase(target, foundIndex) {
score += 3 // CamelCase boundary
}
score += 1 // Base match
targetIndex = target.index(after: foundIndex)
}
return score
}
Higher scores rank higher. Exact matches get bonus scoring. Prefix matches get bonus scoring.
Concurrent Processing#
For large window lists (50+ windows), parallel scoring:
func filterWindows(_ windows: [WindowInfo], query: String) -> [WindowInfo] {
let results = ThreadSafeArray<(WindowInfo, Int)>()
DispatchQueue.concurrentPerform(iterations: windows.count) { index in
let window = windows[index]
if let score = fuzzyMatch(query, window.title) {
results.append((window, score))
}
}
return results.sorted { $0.1 > $1.1 }.map { $0.0 }
}
Keeps UI responsive even with hundreds of windows open.
Settings and Persistence#
Hotkey stored in UserDefaults:
struct HotkeyPreference: Codable {
let keyCode: UInt16
let modifierFlags: NSEvent.ModifierFlags
}
// Save
UserDefaults.standard.set(encodedHotkey, forKey: "globalHotkey")
// Load on launch
if let savedHotkey = UserDefaults.standard.data(forKey: "globalHotkey") {
registerHotkey(savedHotkey)
}
Settings UI lets you click and press new key combination. Updates live without restart.
Comparison to macOS Built-in#
macOS Control+F4:
- Two-step process (show list, then select)
- No search/filter capability
- No memory between activations
- Ordered by app only
- Mouse or arrow keys for selection
FFWF:
- One-step process (popover with search)
- Instant fuzzy filtering
- Remembers last search
- Ranked by match quality
- Keyboard-only workflow
- Faster for specific window targeting
Building from Source#
# Clone
git clone https://github.com/Intelligrit/ffwf.git
cd ffwf
# Build and install
make install
# Or just build
make app
Makefile handles code signing and notarization steps.
Technical Stack#
- SwiftUI - Reactive UI framework
- Combine - Event handling and state management
- AppKit - Menu bar integration, global hotkeys
- Accessibility API - Window enumeration and control
- NSWorkspace - App lifecycle observers
Use Cases#
Developers: Switch between editor, terminal, browser, docs - all by typing partial window titles.
Designers: Jump between Figma, Sketch, preview windows, reference materials.
Writers: Move between research, drafts, email, references.
Anyone with 20+ windows: Find the right window in under a second instead of hunting through Mission Control.
Performance#
Window enumeration: ~20ms for 50 windows Fuzzy filtering: <1ms for 100 windows Icon caching: 0ms after first load Background refresh: 0.5s interval (only while popover visible) Memory footprint: <15MB
Available at github.com/Intelligrit/ffwf under MIT license.