Last Updated: January 2026 Version: v1.0.4 (January 21, 2026)
Aegis is a macOS menu bar application that integrates with Yabai window manager to provide:
- Visual workspace/space indicators in the menu bar
- Notch-area HUD for system status (volume, brightness, music, Bluetooth devices, Focus mode, notifications)
- System status display (battery, network, Focus mode, time)
- Window management via drag-drop and gestures
- Event-driven: EventRouter pub/sub decouples services from UI
- Split-state architecture: Per-space ViewModels minimize SwiftUI re-renders
- Lightweight animation: Timer-based interpolation for smooth progress bars
- Window lifecycle: Windows created once at startup, reused throughout session
- Configuration-driven: Centralized AegisConfig singleton for all customization
- CALayer rendering: GPU-accelerated AppKit views bypass SwiftUI overhead
Aegis/
├── App/ # Application entry point
├── Core/ # Configuration and services
│ ├── Config/ # Centralized settings
│ ├── Models/ # Core data models
│ └── Services/ # Business logic services
├── Components/ # UI components
│ ├── MenuBar/ # Workspace indicator UI
│ ├── Notch/ # Notch HUD system
│ ├── Systemstatus/ # System monitoring UI
│ └── SettingsPanel/ # Settings interface
├── Helpers/ # Shared utilities
└── AegisYabaiIntegration/ # Shell scripts
- SwiftUI app entry point
- Registers AppDelegate for lifecycle management
- Provides Settings window
-
Responsibilities:
- Initialize all services (Yabai, SystemInfo, Music)
- Create EventRouter and inject into services
- Setup MenuBar and Notch components
- Subscribe to events and route to UI
- Request automation permissions
-
Key Methods:
setupServices(): Initialize EventRouter, YabaiService, SystemInfoService, MusicServicesetupMenuBar(): Create MenuBarController and connect to servicessetupNotchHUD(): Create NotchHUDController, prepare windows, connect to MenuBarstartEventListening(): Subscribe to events (spaceChanged, windowsChanged, volumeChanged, etc.)
-
Purpose: Centralized configuration singleton
-
Scope: 100+ @Published properties for every UI/behavior setting
-
Categories:
- Menu bar layout (height, padding, spacing, corner radii)
- Space indicators (size, icon display, overflow behavior)
- Typography (font sizes for labels, values, headers)
- Animation settings (spring response, damping, durations)
- System status thresholds (battery levels, WiFi strength)
- Notch HUD (size, position, progress bar dimensions)
- Color schemes and opacity values
-
Persistence: UserDefaults for settings recovery across launches
-
Access Pattern: All views use
@ObservedObject private var config = AegisConfig.shared
-
Purpose: Pub/sub event bus decoupling services from UI
-
Events:
.spaceChanged: Workspace switched.windowsChanged: Window added/removed/moved.volumeChanged: System volume adjusted.brightnessChanged: Display brightness adjusted.musicPlaybackChanged: Music track changed.deviceConnected/.deviceDisconnected: Bluetooth device state.focusChanged: Focus mode enabled/disabled.notificationReceived: System notification intercepted
-
API:
func publish(_ event: EventType, data: [String: Any]) func subscribe(to: EventType, handler: @escaping ([String: Any]) -> Void)
-
Thread Safety: All handler calls dispatched to main thread
-
Purpose: Interface to Yabai window manager
-
Responsibilities:
- Query spaces and windows:
getSpaces(),getWindowsOnSpace() - Execute commands: Focus, move, create, destroy spaces/windows
- Monitor Yabai events: Listen to FIFO pipe for real-time updates
- Provide window icons: Lookup app bundle identifiers for icons
- Query spaces and windows:
-
Event Monitoring:
- Reads from Yabai's FIFO pipe (configured in yabairc)
- Parses JSON events (space_changed, window_created, etc.)
- Publishes via EventRouter
-
Command Execution: Uses
YabaiCommandActorfor async shell commands
-
Purpose: Monitor hardware state (volume, brightness, battery)
-
Components:
- Volume: CoreAudio API for system volume and mute state
- Brightness: Private DisplayServices API via Objective-C helper (
Brightness/) - Battery: IOKit for battery level, charging state, time remaining
-
Event Sources:
- Volume: Event-driven (CoreAudio property listener)
- Brightness: Event-driven (DisplayServices callback registration)
- Battery: Polled every 10s
-
Events Published:
.volumeChanged,.brightnessChanged
-
Purpose: Monitor Bluetooth device connections/disconnections
-
Capabilities:
- Detect device connect/disconnect events via IOBluetooth
- Identify device types (AirPods, AirPods Pro, AirPods Max, Beats, keyboards, mice, etc.)
- Fetch battery levels via system_profiler
- Debounce rapid connect/disconnect events
-
Implementation:
- Registers for IOBluetooth notifications
- Maps device names to types with appropriate SF Symbols
- Handles battery key variations (device_batteryLevel, device_batteryLevelLeft, etc.)
- Ignores spurious reconnect notifications within 2s of disconnect
-
Events Published:
.deviceConnected,.deviceDisconnected
-
Purpose: Monitor system-wide now-playing information from ALL media sources
-
Capabilities:
- Query current track (title, artist, album) from any app
- Fetch album artwork (base64-encoded from MediaRemote)
- Detect playback state (playing/paused)
- Control playback (play, pause, next, previous)
- Works with Music, Spotify, Safari, Chrome, Firefox, YouTube, video players, etc.
-
Implementation:
- Uses
mediaremote-adapter(Perl script + MediaRemote framework) - Event-driven via continuous JSON stream (not polling)
- 50ms debounce on stream to reduce CPU during rapid updates
- Album art cached per-track with LRU eviction (max 10 entries)
- Uses
-
Events Published:
.mediaPlaybackChanged
-
Purpose: Intercept macOS system notifications via Accessibility API
-
Capabilities:
- Detect notification banners from any app
- Extract notification content (app name, title, body)
- Dismiss native banners to replace with custom HUD
- Identify source app via bundle identifier lookup
-
Implementation:
- Uses
AXObserverto watchcom.apple.notificationcenteruiprocess - Listens for
kAXWindowCreatedNotificationevents (event-driven, zero idle CPU) - Traverses AX hierarchy to find
AXNotificationCenterBannerelements - Parses banner description attribute for app name, title, body
- Dismisses native banner via AX "Close" action
- Runs on background thread with CFRunLoop
- Uses
-
Dismissal Optimization:
- Dismisses banner BEFORE content extraction to minimize visible flash
- Some flash (~50-150ms) is unavoidable due to macOS architecture limitation
- AX events fire AFTER notification is already rendered
-
Bundle ID Resolution:
- Hardcoded lookup table for common apps (Messages, WhatsApp, Slack, etc.)
- Case-insensitive matching against running applications
- Partial match fallback for locale differences
-
Events Published:
.notificationReceived -
Permissions Required: Accessibility (already needed for Yabai)
-
Purpose: Custom Cmd+Tab window switcher with space-aware organization
-
Capabilities:
- Intercept Cmd+Tab via CGEvent tap
- Display windows organized by space in a centered overlay
- Multiple input methods: keyboard, mouse hover, two-finger scroll
- Type-to-filter search while switcher is active
- Focus any window via Yabai commands
-
Event Tap:
- Captures keyDown, keyUp, flagsChanged, leftMouseDown
- Intercepts Cmd+Tab to activate switcher
- Handles Escape to cancel, arrow keys to navigate
- Cmd+1-9 for direct window selection
-
Input Methods:
- Keyboard: Cmd+Tab/Cmd+Shift+Tab cycles, Cmd+1-9 direct select
- Mouse: Hover highlights, click confirms selection
- Scroll: Two-finger scroll cycles with configurable threshold/notched behavior
-
Components:
AppSwitcherWindowController: Manages overlay NSPanelAppSwitcherViewModel: Published state for SwiftUI viewsMouseTrackingNSView: Efficient mouse/scroll event handling
-
Models:
SpaceGroup: Windows grouped by space indexSwitcherWindow: Window info with icon, state (minimized/hidden)
Purpose: Display Yabai workspaces as interactive indicators in menu bar
MenuBarController (Facade)
↓
MenuBarCoordinator (Orchestrator)
↓ creates
MenuBarWindowController (Window management)
MenuBarViewModel (State)
MenuBarInteractionMonitor (Native menu bar tracking)
↓ renders
SpaceIndicatorView (UI)
-
Central orchestrator for menu bar component
-
Responsibilities:
- Create and manage window, view model, and interactions
- Route user actions to YabaiService
- Subscribe to EventRouter events (spaceChanged, windowsChanged)
- Update spaces and windows on events
- Handle native macOS menu bar conflicts (hide when system menu bar active)
-
Key Methods:
show(): Initialize and display menu barupdateSpaces(): Query Yabai and refresh space listupdateWindows(): Query Yabai for windows on each spacehandleSpaceClick(),handleWindowClick(), etc.
- Facade for backward compatibility
- Delegates all calls to
MenuBarCoordinator - Public interface used by
AppDelegate
-
State management for menu bar UI using split-state architecture
-
Components:
SpaceViewModelStore: Manages collection of per-space ViewModelsSharedMenuBarState: Cross-space coordination (drag, expansion, HUD)- Private caches for raw spaces, window icons, focused indices
-
Split-State Architecture:
- Each
SpaceViewModelis observed only by its ownSpaceIndicatorView - Changes to one space don't trigger re-renders of other spaces
- Reduces CPU usage by ~95% during focus changes
- Each
-
Fallback Polling: Updates every 60s as safety net (event-driven is primary)
-
Icon Management: Async loading of app icons with caching
- Window lifecycle management
- Creates borderless NSWindow at top of screen
- Handles window positioning and z-ordering
- Manages visibility and interaction
-
Main UI view for individual workspace display
-
Layout:
- Space number/label
- Grid of window icons (up to
maxDisplayedIcons) - Overflow menu ("..." button) for additional windows
- Stack badge indicators
-
Interactions:
- Click space: Focus that workspace
- Click window: Focus that window
- Right-click window: Expand to show title
- Drag window: Move to different space (via SpaceDropController)
- Swipe-to-destroy: Delete space (via SwipeableSpaceContainer)
-
Visual States:
- Active space: Highlighted background
- Focused window: Blue accent
- Hidden windows: Dimmed opacity
- Stack indicators: Badge count
- SpaceIndicatorViewContainer.swift: Isolates per-space re-renders via @ObservedObject
- AppKitActionButton.swift: CALayer-based buttons (context menu, app launcher)
- GPU-accelerated rendering bypasses SwiftUI overhead
- Scroll throttling at 20fps max to reduce CPU
- SpaceStyleView.swift: Visual styling helpers
- SpaceViewModel.swift: Per-space observable state (space data, window icons, focus)
- SpaceViewModelStore.swift: Manages SpaceViewModel lifecycle, routes updates
- SharedMenuBarState.swift: Shared state for drag/expansion/HUD coordination
- SpaceDropController.swift: Drag-drop delegate for moving windows
- SwipeableSpaceContainer.swift: Swipe gesture for space destruction
- MenuBarInteractionMonitor.swift: Track native macOS menu bar state
- SwipeDetectorView.swift: Scroll event handling with 50ms throttle
Purpose: Display system notifications (volume, brightness, music, Bluetooth devices, Focus mode, app notifications) at notch location
NotchHUDController (Window + visibility management)
↓ owns
OverlayHUDViewModel (Volume/brightness state)
↓ contains
ProgressBarAnimator (Frame-locked interpolation)
↓ renders
MinimalHUDWrapper (UI)
NotchHUDController
↓ owns
MusicHUDViewModel (Music state)
↓ renders
MusicHUDView (UI)
NotchHUDController
↓ owns
DeviceHUDViewModel (Bluetooth device state)
↓ renders
DeviceHUDView (UI)
NotchHUDController
↓ owns
FocusHUDViewModel (Focus mode state)
↓ renders
FocusHUDView (UI)
NotchHUDController
↓ owns
NotificationHUDViewModel (Notification state)
↓ renders
NotificationHUDView (UI)
-
Responsibilities:
- Create and manage overlay and music windows
- Handle visibility and auto-hide logic
- Route events from EventRouter to ViewModels
- Coordinate with MenuBar for visibility toggling
-
Window Management:
prepareWindows(): Called at app startup to create windows once- Windows created with
isReleasedWhenClosed = falsefor reuse - Ordered front but invisible initially (alpha = 0)
- Force layout pass to prevent first-show jank
-
Overlay HUD Methods:
showVolume(level:isMuted:): Update animator target, show if hiddenshowBrightness(level:): Update animator target, show if hiddenbumpHideDeadline(): Extend auto-hide timerhideOverlayHUD(): Animate out and hide window
-
Media HUD Methods:
showMedia(info:): Update media view with track infohideMediaHUD(): Hide media display
-
Auto-hide Logic:
- Timestamp-based deadline (not timer recreation)
- Single polling timer checks deadline every 0.1s
- Timer only created once when showing, invalidated when hiding
-
Persistent state for volume/brightness HUD
-
Properties:
@Published var isVisible: Bool: Drives slide-in animation@Published var level: Float: Current value (only updated on first show)@Published var isMuted: Bool: Mute state@Published var iconName: String: Icon to displaylet progressAnimator: ProgressBarAnimator: Frame-locked animator instance
-
Why Persistent: Survives view rebuilds, prevents animation restarts
-
Lightweight timer-based interpolation for smooth progress bar
-
Implementation:
- Uses
DispatchSourceTimeron main queue at 60fps (~16ms interval) - Timer starts on-demand when animation needed, stops when settled
- Exponential ease-out:
displayed += (target - displayed) * 0.35 - Auto-stops when within 0.5% of target to save energy
- Uses
-
API:
setTarget(_ value: Double): Update target (called from controller)start()/stop(): Called when HUD shows/hides@Published displayed: Double: Actual displayed value (observed by view)
-
Performance:
- ~1-2% CPU during animation (vs 10-15% with CVDisplayLink)
- Zero CPU when animation settled (timer stops)
- Snap-to-target on first show after hide (no stale state)
-
UI view for volume/brightness HUD
-
Layout:
- Left side: Icon (slides from under notch)
- Center: Notch spacer (transparent)
- Right side: Progress bar or numeric value (slides from under notch)
-
Animation:
- Slide-in from under notch (offset by
notchWidth/2) - Spring animation (0.25s response, 0.8 damping)
- Only animates when expanding, not collapsing
- Slide-in from under notch (offset by
-
HUDProgressBar:
- Observes
animator.displayeddirectly - Disables SwiftUI animation (
.animation(nil)) - Freezes layout during updates (
.transaction) - Width calculated as
barWidth * CGFloat(animator.displayed)
- Observes
- Persistent state for media HUD
- Properties:
@Published var isVisible: Bool: Drives animation@Published var info: MediaInfo: Current track info@Published var isOverlayActive: Bool: Whether overlay HUDs (volume/brightness/device/focus/notification) are showingoverlayCount: Int: Counter for active overlays (prevents race conditions with async hide timers)
- Overlay Counter Pattern:
overlayDidShow(): Increments counter when any overlay HUD showsoverlayDidHide(): Decrements counter when any overlay HUD hidesresetOverlayState(): Safety valve to reset counter if it gets stuck- Right panel hides when
isOverlayActive == trueto avoid overlap with overlays
-
UI view for media playback display (works with all media sources)
-
Layout:
- Left: Album art with scale/opacity animation
- Right: Either media visualizer OR track info (configurable via
mediaHUDRightPanelMode)
-
Right Panel Modes:
- Visualizer mode (default): 5 animated capsule bars with randomized heights
- Track Info mode: Song title and artist with marquee scrolling for long text
-
Visualizer:
- 5 capsule bars with randomized heights
- Updates every 0.2s when playing
- Resets to flat bars when paused
- Optional blur effect (shows wallpaper through bars)
-
Track Info Display (TrackInfoView):
- Shows song title and artist name
- Auto-expands width on track change to show full text
- Collapses after 3 seconds to standard width
- Marquee scrolling for text that overflows collapsed width:
- Title and artist scroll independently (each only if it overflows)
- When both overflow, they scroll in sync at same speed
- Energy-efficient 30fps timer-based animation (not 60fps SwiftUI animation)
- GPU-accelerated via
.drawingGroup()modifier
- Tap album art to toggle between visualizer and track info modes
-
MarqueeScrollController:
- State machine with phases: idle → initialDelay → scrolling → endPause → resetPause
- Uses
CACurrentMediaTime()for accurate timing - 30fps
Timerfor energy efficiency (half the energy of 60fps) - Configurable scroll speed (30 points/second), delays, and gap between text copies
-
UI view for Bluetooth device connection notifications
-
Layout:
- Left: Device type icon (AirPods, headphones, keyboard, etc.)
- Right: Device type name, connection status, battery ring indicator
-
Components:
DeviceHUDViewModel: Holds device info, connection state, visibilityBatteryRingView: Circular progress ring showing battery level (color-coded: green/orange/red)
-
Device Types Supported:
- AirPods, AirPods Pro, AirPods Max
- Beats headphones, generic headphones
- Speakers, keyboards, mice, trackpads
-
UI view for Focus mode change notifications
-
Layout:
- Left: Focus mode icon (SF Symbol from user's Focus configuration)
- Right: Focus name and On/Off status
-
Components:
FocusHUDViewModel: Holds focus status, visibility- Shows actual Focus mode name (e.g., "Study", "Work", "Do Not Disturb")
- Purple status color when enabled, gray when disabled
- When disabling, shows which mode was turned off (e.g., "Study / Off")
-
UI view for system notification display
-
Layout:
- Left: App icon (from bundle identifier or app name lookup)
- Right: Notification title and body text
-
Components:
NotificationHUDViewModel: Holds app name, title, body, app icon, visibility- Dynamic width calculation based on text content
openAppHandlercallback for Yabai integration
-
Interaction:
- Click anywhere on HUD focuses/opens the source application
- Uses Yabai
focusWindowByAppName()first (respects window layout) - Falls back to
NSWorkspace.launchApplication()if no window found - Auto-hide after configurable delay (default 8s)
- Shared panel shapes for all HUD views
- Shapes:
HUDLeftPanelShape: Curved outer edges, inner edge curves outward to connect with notchHUDRightPanelShape: Mirror of left panel shape
- Used by: MediaHUDView, MinimalHUDWrapper, DeviceHUDView, FocusHUDView, NotificationHUDView
- Calculates notch geometry from screen properties
- Uses
NSScreen.safeAreaInsets.topfor height - Uses
auxiliaryTopLeftAreaandauxiliaryTopRightAreafor width - Provides padding constants for content positioning
Purpose: Display system information in menu bar
SystemStatusMonitor (Aggregates all status)
↓
BatteryStatusMonitor (Battery specific)
NetworkStatus (Network model)
FocusStatusMonitor (Focus mode)
↓
SystemStatusView (Container)
↓
Individual icon views (Battery, Network, Focus, Clock, Date)
- Aggregates battery, network, and other system state
- Publishes updates for UI consumption
- Uses IOKit to query battery status
- Tracks level, charging state, time remaining
-
Purpose: Monitor macOS Focus mode status
-
Implementation:
- Watches
~/Library/DoNotDisturb/DB/directory for file changes - Parses
Assertions.jsonto detect active Focus mode - Reads
ModeConfigurations.jsonfor mode names and SF Symbols - Supports all built-in modes (Do Not Disturb, Work, Personal, Sleep, etc.)
- Supports custom Focus modes with their user-defined symbols
- Watches
-
Events Published:
.focusChanged -
State Tracking: Remembers last active Focus mode to show "Study / Off" when disabling
- SystemStatusView.swift: Container orchestrator
- BatteryStatusIconView.swift: Battery icon with level indicator
- NetworkStatusIconView.swift: WiFi/Ethernet status icon
- FocusStatusIconView.swift: Focus mode icon (animated slide in/out)
- ClockView.swift: Current time display
- DateView.swift: Current date display
Purpose: User preferences interface
- Manages settings window lifecycle
- Shows/hides panel on demand
-
Large monolithic view with all settings
-
Categories:
- Menu bar appearance
- Space indicators
- Notch HUD
- System status
- Battery/network thresholds
- Animation settings
-
Note: Could be split into focused sub-views for better organization
- Utility functions for settings UI
- Formatters, validators, converters
User presses volume key
↓
SystemInfoService detects change (CoreAudio listener)
↓
EventRouter.publish(.volumeChanged, level: 0.75)
↓
AppDelegate subscription fires
↓
NotchHUDController.showVolume(level: 0.75)
↓
overlayViewModel.progressAnimator.setTarget(0.75) [BYPASS ViewModel]
↓
DispatchSourceTimer ticks at 60fps (~16ms)
↓
ProgressBarAnimator.tick() interpolates: displayed += (0.75 - displayed) * 0.35
↓
@Published displayed updates
↓
MinimalHUDWrapper observes animator.displayed
↓
SwiftUI re-renders progress bar with new width
↓
Timer auto-stops when displayed is within 0.5% of target
Key Optimization: Controller directly updates animator target, bypassing ViewModel to avoid SwiftUI re-render storm during rapid input. Timer-based animation uses ~1-2% CPU vs 10-15% with CVDisplayLink.
User switches space (Yabai command)
↓
Yabai writes event to FIFO pipe
↓
YabaiService reads pipe, parses JSON
↓
EventRouter.publish(.spaceChanged)
↓
MenuBarCoordinator subscription fires
↓
MenuBarCoordinator.updateSpaces()
↓
YabaiService.getSpaces() queries Yabai
↓
MenuBarViewModel.spaces updated
↓
SpaceIndicatorView re-renders with new active state
User drags window icon to different space
↓
SpaceDropController.performDrop()
↓
MenuBarCoordinator.handleWindowDrop()
↓
YabaiService.moveWindowToSpace()
↓
YabaiCommandActor executes: yabai -m window 123 --space 2
↓
Yabai moves window, writes event to pipe
↓
YabaiService detects window_moved event
↓
EventRouter.publish(.windowsChanged)
↓
MenuBarCoordinator updates windows on both spaces
User connects AirPods
↓
IOBluetooth notification fires
↓
BluetoothDeviceService.deviceConnected()
↓
Identify device type from name (e.g., "AirPods Pro")
↓
Fetch battery level via system_profiler (async)
↓
EventRouter.publish(.deviceConnected, deviceInfo)
↓
AppDelegate subscription fires
↓
NotchHUDController.showDevice(info:isConnecting:)
↓
DeviceHUDView displays: icon + name + "Connected" + battery ring
↓
Auto-hide after 1.5s
User enables Focus mode in Control Center
↓
macOS writes to ~/Library/DoNotDisturb/DB/Assertions.json
↓
FocusStatusMonitor detects directory change (DispatchSource)
↓
Parse Assertions.json for active mode identifier
↓
Lookup mode name and symbol from ModeConfigurations.json
↓
EventRouter.publish(.focusChanged, status)
↓
AppDelegate subscription fires
↓
NotchHUDController.showFocus(status:)
↓
FocusHUDView displays: icon + "Study" + "On" (purple)
↓
Auto-hide after 1.5s
App sends notification (e.g., Messages receives SMS)
↓
macOS creates notification banner window
↓
AXObserver receives kAXWindowCreatedNotification (background thread)
↓
NotificationService.handleNotificationWindow()
↓
IMMEDIATELY dismiss native banner via AX "Close" action [OPTIMIZATION]
↓
Extract content from AXNotificationCenterBanner description
↓
Lookup bundle identifier (hardcoded table → running apps → partial match)
↓
EventRouter.publish(.notificationReceived, data)
↓
AppDelegate subscription fires (main thread)
↓
NotchHUDController.showNotification(...)
↓
NotificationHUDView displays: app icon + title + body
↓
Auto-hide after 8s (or tap to open source app)
Key Limitation: Native banner flashes briefly (~50-150ms) before dismissal. This is unavoidable - macOS renders the notification before firing the AX event. No public API exists to intercept notifications before display.
Problem: SwiftUI animations lag during rapid input (15+ events/sec) Solution: DispatchSourceTimer provides smooth interpolation with minimal CPU overhead
Key Techniques:
- DispatchSourceTimer on main queue at 60fps (~16ms interval)
- Timer starts on-demand when animation needed, auto-stops when settled
- Exponential ease-out:
displayed += (target - displayed) * 0.35 - Auto-stops when within 0.5% of target (zero CPU when idle)
- Bypass ViewModel updates during rapid input (only update animator target)
- Disable SwiftUI animation (
.animation(nil)) - ~1-2% CPU during animation vs 10-15% with CVDisplayLink
Problem: Creating windows during interaction causes stutters Solution: Prepare windows at app startup, reuse throughout session
Key Techniques:
isReleasedWhenClosed = falseto prevent deallocation- Order front but invisible (alpha = 0) for proper initialization
- Force layout pass with
layoutIfNeeded()before first show - Never destroy windows, just hide and reshow
Problem: Polling Yabai every frame is expensive Solution: Yabai FIFO pipe provides real-time events, poll only as fallback
Key Techniques:
- FIFO pipe monitoring in background thread
- EventRouter decouples services from UI (no direct dependencies)
- All handler calls dispatched to main thread for UI updates
- Fallback polling every 60s if pipe not available
Problem: View rebuilds restart animations Solution: Hoist ViewModels above view hierarchy, survive rebuilds
Key Techniques:
- ViewModels owned by controllers, not views
- Bindings passed down to views
ProgressBarAnimatorinstance persists in ViewModel- State survives SwiftUI view invalidation
Problem: Loading app icons blocks UI Solution: Async fetch with caching, show placeholder immediately
Key Techniques:
- Icons loaded in background queue
- Published updates trigger UI refresh when ready
- Icon cache prevents repeated disk access
- Placeholder shown while loading
- Height, padding, spacing, corner radii
- Background opacity and blur
- Divider spacing between spaces
- Circle size, icon size, overflow button size
- Max displayed icons before overflow
- Grid layout (rows, columns, spacing)
- Stack badge position and size
- Font sizes for space labels, window titles, system status
- Font weights (regular, semibold, bold)
- Spring response (0.3s typical)
- Spring damping (0.7-0.8 typical)
- Fade durations (0.2-0.5s)
- Slide offsets
- Battery low level (20%)
- Battery critical level (10%)
- WiFi strength thresholds (good/fair/poor)
- Width, height, corner radius
- Icon size, value font size
- Progress bar dimensions (width, height)
- Animation timings
- Auto-hide delay (1.5s)
- Background opacity (0.9 typical)
- Icon opacity states (active, inactive, dimmed)
- Accent colors (system accent or custom)
- Controllers manage window lifecycle and visibility
- Coordinators orchestrate component initialization and interactions
- ViewModels hold persistent state with @Published properties
- Views are stateless SwiftUI views observing ViewModels
- Services handle business logic and external integrations
- Models are simple data structures (structs with Codable/Identifiable)
Component/
├── Controllers/ # Window and lifecycle management
├── Coordinators/ # Orchestration and routing
├── ViewModels/ # Persistent state with @Published
├── Views/ # SwiftUI views (stateless)
├── Models/ # Data structures
├── Interaction/ # Gesture and input handlers
└── Helpers/ # Component-specific utilities
- Controllers:
*Controller.swift(lifecycle, window management) - Coordinators:
*Coordinator.swift(orchestration) - ViewModels:
*ViewModel.swift(state management) - Views:
*View.swift(UI rendering) - Services:
*Service.swift(business logic) - Models: Noun without suffix (e.g.,
Space.swift,MusicInfo.swift)
- Communication: Shell commands via
Process(async with YabaiCommandActor) - Event Monitoring: FIFO pipe at
/tmp/yabai_$USER.socket - Setup: User configures yabairc to write events to pipe
- Commands: Focus, move, create, destroy spaces/windows; query state
- Communication: MediaRemote framework via mediaremote-adapter
- Sources: Music.app, Spotify, Safari, Chrome, Firefox, YouTube, video players, etc.
- Queries: Current track (title, artist, album), playback state, bundle identifier
- Artwork: Fetched asynchronously, cached per-track (LRU, max 10 entries)
- Event-driven: Continuous JSON stream via mediaremote-adapter (50ms debounce)
- CoreAudio: Volume level and mute state (event-driven)
- IOKit: Battery status (polled every 10s)
- IOBluetooth: Device connection/disconnection notifications
- Private API: Brightness monitoring via Objective-C helper
- AppKit: Window management, screen geometry, workspace integration
- DispatchSource: File system monitoring for Focus mode changes
- Console logs with emoji prefixes (🚀 launch, 🔆 brightness, 🪟 window, etc.)
- Frame-time logging for performance analysis
- Yabai command output capture
- Animation lag: Check CVDisplayLink is running, verify no SwiftUI render storm
- HUD not showing: Check window preparation at startup, verify alpha value
- Spaces not updating: Check Yabai FIFO pipe, verify yabairc configuration
- Music not displaying: Check Music.app permissions, verify osascript access
- Use Instruments (Time Profiler) to identify bottlenecks
- Monitor main thread for blocking operations
- Check CVDisplayLink callback frequency
- Dependency Injection: Replace
AegisConfig.sharedwith injected dependencies - Protocol-Oriented Design: Define protocols for services, enable mocking for tests
- Split Large Files: Break SettingsPanelView into focused sub-views
- Music Visualizers: Implement audio spectrum analyzer (empty
/Visualisers/directory) - Persistent Layouts: Save window positions per space
- Keyboard Shortcuts: Global hotkeys for window management
- Icon Caching: Use NSCache for memory-managed icon storage
- Event Debouncing: Consolidate rapid events (e.g., multiple window moves)
- Lazy Loading: Defer icon loading for off-screen spaces
// ❌ BAD: Updates ViewModel on every event (triggers SwiftUI re-renders)
func showBrightness(level: Float) {
overlayViewModel.level = level // @Published property
overlayViewModel.progressAnimator.setTarget(Double(level))
}
// ✅ GOOD: Bypass ViewModel, update animator directly
func showBrightness(level: Float) {
overlayViewModel.progressAnimator.setTarget(Double(level))
if !overlayViewModel.isVisible {
overlayViewModel.level = level // Only update on first show
showOverlayHUD()
}
bumpHideDeadline()
}// DispatchSourceTimer for smooth progress bar animation
private func startTimer() {
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now(), repeating: .milliseconds(16)) // ~60fps
timer.setEventHandler { [weak self] in
self?.tick()
}
timer.resume()
self.timer = timer
}
private func tick() {
let delta = target - displayed
if abs(delta) < snapThreshold {
displayed = target
stopTimer() // Auto-stop when settled
return
}
// Exponential ease-out
displayed += delta * interpolationSpeed // 0.35
}func prepareWindows() {
// Create window ONCE at startup
overlayWindow = NSWindow(...)
overlayWindow.isReleasedWhenClosed = false
// Order front immediately but invisible
overlayWindow.orderFront(nil)
overlayWindow.alphaValue = 0
// Force initial layout pass
overlayWindow.layoutIfNeeded()
}| Component | Files | Total Lines (approx) |
|---|---|---|
| App | 2 | 200 |
| Core/Config | 1 | 1,045 |
| Core/Services | 6 | 1,600 |
| MenuBar | 13 | 2,500 |
| Notch | 12 | 1,200 |
| SystemPanel | 12 | 900 |
| SettingsPanel | 3 | 1,500 |
| Total | 49 | ~8,945 |
| File | Purpose | Lines |
|---|---|---|
AppDelegate.swift |
App initialization, service setup, event routing | 160 |
AegisConfig.swift |
Centralized configuration singleton | 1,045 |
EventRouter.swift |
Pub/sub event bus | 60 |
YabaiService.swift |
Yabai WM integration, window focus by app name | 530 |
BluetoothDeviceService.swift |
Bluetooth device monitoring | 400 |
NotificationService.swift |
System notification interception | 440 |
AppSwitcherService.swift |
Custom Cmd+Tab window switcher | 612 |
MenuBarCoordinator.swift |
Menu bar orchestration | 400+ |
SpaceIndicatorView.swift |
Workspace UI display | 776 |
NotchHUDController.swift |
Notch HUD window management, Yabai app focus | 350 |
NotchHUDViewModel.swift |
HUD state management, overlay counter | 150 |
ProgressBarAnimator.swift |
Lightweight timer interpolation | 142 |
HUDShapes.swift |
Shared panel shapes for all HUDs | 100 |
MinimalHUDWrapper.swift |
Volume/brightness UI | 255 |
MediaHUDView.swift |
Now Playing UI | 800+ |
DeviceHUDView.swift |
Bluetooth device connection UI | 165 |
FocusHUDView.swift |
Focus mode change UI | 142 |
NotificationHUDView.swift |
System notification UI | 142 |
FocusStatusMonitor.swift |
Focus mode detection | 200 |
End of Architecture Document