Project structure
The codebase is organized into four main layers:App layer
The app layer manages the application lifecycle and global state:- HopTabApp.swift - SwiftUI app entry point with
MenuBarExtraandSettingsscenes - AppDelegate.swift - Handles launch, permission checks, and hotkey initialization
- AppState.swift - Central
@ObservableObjectthat coordinates all services and publishes state changes to views
HopTab/App/HopTabApp.swift
AppState acts as the central coordinator, managing:
- Pinned apps store (
PinnedAppsStore) - Hotkey service (
HotkeyService) - Overlay window controllers
- Running apps list
- Shortcut configuration
- Profile switching logic
- Desktop assignment via Space tracking
Models layer
Models define the core data structures:- PinnedApp - Represents a pinned application with bundle ID, display name, and sort order
- Profile - Named collection of pinned apps (e.g., “Coding”, “Design”)
- PinnedAppsStore - Manages profiles, active profile, and persistence to
UserDefaults - ShortcutConfig - Configurable keyboard shortcut presets and custom shortcuts
- KeyCodeMapping - Maps key codes to human-readable names
HopTab/Models/PinnedApp.swift
Services layer
Services encapsulate system interactions:- HotkeyService - Global keyboard event monitoring via
CGEventtap - AppSwitcherService - App activation using
NSRunningApplicationandAXUIElement - SpaceService - Desktop (Space) tracking via private CGS APIs
- PermissionsService - Accessibility permission checks
AppState via callbacks.
Views layer
SwiftUI views for the UI:- MenuBarView - Menu bar dropdown with profile selector and settings access
- SettingsView - Multi-tab settings window (Pinned Apps, Profiles, Shortcut)
- OverlayPanel - Non-activating
NSPanelfor the app switcher overlay - OverlayView - SwiftUI view showing pinned apps with vibrancy blur
- ProfileOverlayView - Profile switcher overlay
- AppIconView - App icon rendering with running indicator
- ShortcutRecorderView - Custom shortcut key recorder
Core architectural decisions
SwiftUI + AppKit hybrid
HopTab uses SwiftUI for UI with strategic AppKit integration where needed:- SwiftUI for declarative UI (settings, menu bar, overlay content)
- AppKit for system-level functionality (event taps, window management, accessibility)
NSPanel wrapped in SwiftUI via NSHostingView for precise control over window level and behavior.
State management
AppState is the single source of truth, implemented as an @ObservableObject:
HopTab/App/AppState.swift
AppState through callbacks, not direct state mutation:
HopTab/App/AppState.swift
Persistence
All app data persists toUserDefaults:
- Pinned apps (per profile)
- Profiles and active profile
- Desktop-to-profile assignments
- Shortcut configuration
PinnedAppsStore handles serialization and deserialization of Codable models.
No sandboxing
This is a deliberate architectural choice. The alternative (NSEvent.addGlobalMonitorForEvents) cannot swallow events, which would cause the default Cmd+Tab behavior to trigger alongside HopTab.
Accessibility dependency
HopTab requires Accessibility permission for two reasons:- Event tap creation -
CGEvent.tapCreate(.cgSessionEventTap, ...)requires it - Window raising -
AXUIElementPerformAction(window, kAXRaiseAction)forces windows to the front
Data flow
- User presses shortcut →
HotkeyServicedetects via event tap - HotkeyService callback →
AppState.showSwitcher()called - AppState reads data → Gets pinned apps from
PinnedAppsStore - AppState shows overlay →
OverlayWindowController.show()displays UI - User cycles apps →
HotkeyServicedetects Tab presses →AppStateupdates selection - User releases modifier →
HotkeyServicecallback →AppState.dismissAndActivate() - AppState activates app →
AppSwitcherService.activate()brings app to front - Service uses AX API →
AXUIElementPerformActionraises windows
Threading model
All UI and state management runs on the main thread via@MainActor: