Architecture Overview
Architecture Overview
Note: This document reflects the system architecture as of May 2026. For deep details on the plugin system, see the Plugin System Architecture guide. For full debt triage with P1/P2/P3 priorities, see
docs/prds/machNotch.md β Known Architecture Debt.
machNotch is a macOS application designed to transform the static camera notch into a dynamic, interactive utility hub. The architecture is built on a modular, plugin-first foundation, ensuring extensibility, testability, and separation of concerns.
π System Context (C4 Level 1)
At the highest level, machNotch sits between the user, the macOS system, and external services.
graph TD
User((User))
subgraph "macOS Environment"
SystemAudio[CoreAudio / Media APIs]
SystemEvents[EventKit / Notifications]
IOKit[IOKit / Battery]
BN[machNotch App]
end
User <-->|Clicks/Hovers| BN
BN <-->|Reads/Controls| SystemAudio
BN <-->|Reads| SystemEvents
BN <-->|Reads| IOKit
BN -->|Displays| Screen[Screen / Notch Area]
π§© Container Architecture (C4 Level 2)
The application is divided into three main layers: Core Infrastructure, Plugin Engine, and Feature Plugins.
graph TB
subgraph "Core Infrastructure"
AppDelegate[AppDelegate]
WindowCoord[WindowCoordinator]
StateMachine[NotchStateMachine]
ContentRouter[NotchContentRouter]
end
subgraph "Plugin Engine"
PM[PluginManager]
SC[ServiceContainer]
EventBus[PluginEventBus]
end
subgraph "Feature Plugins"
Music[MusicPlugin]
Battery[BatteryPlugin]
Shelf[ShelfPlugin]
Other[...Other Plugins]
end
%% Wiring
AppDelegate --> WindowCoord
AppDelegate --> PM
PM -->|Manages| Music
PM -->|Manages| Battery
PM -->|Manages| Shelf
PM --> SC
PM --> EventBus
Music -.->|Injects| SC
Battery -.->|Injects| SC
WindowCoord -->|Observes| StateMachine
StateMachine -->|Computes State| ContentRouter
ContentRouter -->|Renders| Music
Key Components
- PluginManager: The brain of the extension system. It manages the lifecycle (load, activate, deactivate) of all plugins and acts as the central registry.
- ServiceContainer: A dependency injection container that holds all system services (
MusicService,BatteryService, etc.). Plugins request services from here. - NotchStateMachine: A pure logic component that determines what should be shown on the screen based on various inputs (is music playing? is battery low? is user hovering?).
- NotchContentRouter: The View layer component that maps the State Machineβs output to actual SwiftUI views.
- DisplayPrioritizer: Pure struct that determines which plugin wins the closed notch based on
DisplayRequestpriorities. Extracted from PluginManager (SRP). - PluginID: Centralized enum of all plugin identifiers β eliminates stringly-typed references across 30+ call sites.
π§ State Determination & Routing
One of the most complex parts of the system is deciding what to show in the closed notch. The system uses a Priority-Based Arbitration mechanism.
The βDisplay Requestβ Flow
Plugins do not simply βdrawβ to the screen. They must request to be displayed.
sequenceDiagram
participant System as MusicService
participant Plugin as MusicPlugin
participant PM as PluginManager
participant SM as NotchStateMachine
participant Router as NotchContentRouter
participant UI as Screen
System->>Plugin: Playback Started (Event)
Plugin->>Plugin: Update Internal State
note over Plugin: displayRequest changes to:\n{ priority: .high, category: .music }
Plugin->>PM: (Implicitly observed via @Observable)
loop Every Render Cycle
SM->>PM: highestPriorityClosedNotchPlugin()
PM->>Plugin: Check displayRequest
PM-->>SM: Returns PluginID.music
SM->>SM: Compute State: .closed(content: .plugin(PluginID.music))
SM-->>Router: Update State
end
Router->>PM: closedNotchView(for: PluginID.music)
PM->>Plugin: closedNotchContent()
Plugin-->>UI: Renders MusicLiveActivity
Priority Levels
- Critical (30): Urgent system warnings (e.g., βBattery Lowβ).
- High (20): Active user engagement (e.g., βMusic Playingβ, βTimer Runningβ).
- Normal (10): Passive information (e.g., βWeatherβ).
- Background (0): Idle state.
π₯ Notch State Machine Logic
The NotchStateMachine is the single source of truth for the notchβs visual mode.
stateDiagram-v2
[*] --> Closed
state Closed {
[*] --> Idle
Idle --> ActivePlugin: Plugin Request (High/Crit)
ActivePlugin --> Idle: Request Ended
Idle --> SneakPeek: Short Hover
SneakPeek --> Idle: Hover Ended
Idle --> InlineHUD: System Event (Vol/Bright)
InlineHUD --> Idle: Timeout
}
state Open {
[*] --> Home
Home --> ExpandedPanel: Select Plugin
ExpandedPanel --> Home: Back
}
Closed --> Open: Click / Long Hover / Drag Down
Open --> Closed: Click Outside / Drag Up
π‘ Inter-Plugin Communication
Plugins are isolated by default but can communicate via the PluginEventBus. This decouples producers from consumers.
graph LR
Music[MusicPlugin] -->|Emits: .playbackChanged| Bus[PluginEventBus]
Shelf[ShelfPlugin] -->|Emits: .fileDropped| Bus
Bus -->|Notifies| Visualizer[VisualizerPlugin]
Bus -->|Notifies| Analytics[AnalyticsService]
Visualizer -.->|Reacts to| Music
- Example: The
VisualizerPlugindoesnβt need to know aboutMusicPlugin. It just listens forplaybackChangedevents on the bus.
β‘οΈ Concurrency Model
Structured concurrency (async/await) and Actors are strictly used to ensure thread safety.
| Component | Isolation | Reasoning |
|---|---|---|
| NotchPlugin | @MainActor | Plugins directly drive UI state, so they must stay on the main thread. |
| Services | @MainActor | Most system APIs (EventKit, etc.) are main-thread bound or updated via UI run loops. |
| Workers | Task / actor | Heavy lifting (image processing, network requests) is offloaded to background tasks. |
Key Rule: The main thread must never be blocked. If a plugin needs to fetch data (e.g., Weather), it must spawn a detached Task.
πΎ Persistence Strategy
Plugins are sandboxed. UserDefaults.standard is not accessed directly.
PluginSettings: A wrapper aroundDefaultsthat namespaces keys.- Plugin ID:
com.machnotch.weather - Key:
showTemperature - Actual UserDefaults Key:
plugin.com.machnotch.weather.showTemperature
- Plugin ID:
This prevents key collisions and facilitates resetting a specific plugin without wiping the entire app settings.
π Data Flow Patterns
Unidirectional Data Flow is strictly adhered to.
- System Event: A system event occurs (e.g., Song changed).
- Service Update: The
MusicServiceupdates its@Observableproperties. - Plugin Reaction: The
MusicPlugin(observing the service) updates its own state. - UI Render: SwiftUI detects the change in the Plugin and re-renders the View.
β Anti-Pattern (Avoid):
- Views observing singletons (
MusicManager.shared). - Plugins directly modifying Views.
β Correct Pattern:
- Views observe
Plugin. - Plugin observes
Service. - Service observes
System.
π Directory Structure
machNotch/βββ Core/ # Domain + Application Layerβ βββ NotchStateMachine.swift # Domain: pure state logic (no SwiftUI/AppKit)β βββ NotchPhase.swift # Domain: phase enumβ βββ SneakPeekTypes.swift # Domain: value typesβ βββ NotchSettingsSubProtocols # Domain: settings contractsβ βββ WindowCoordinator.swift # Application: window managementβ βββ NotchContentRouter.swift # Application: view routingβ βββ NotchHoverController.swift # Application: hover state machineβ βββ NotchSizeCalculator.swift # Application: sizing (ClosedNotchInput β CGSize)β βββ DefaultsNotchSettings.swift# Infrastructure: settings implementationβ βββ Constants.swift # Infrastructure: paths, notification namesβ βββ SettingsTypes.swift # Infrastructure: Defaults.Serializable enumsββββ ViewModel/ # NotchViewModel + Extensionsβ βββ NotchViewModel.swift # Per-screen orchestratorβ βββ +Camera, +Hover, +Observers, +OpenCloseββββ models/ # Pure Data Models Onlyβ βββ CalendarModel, EventModel, PlaybackState, WeatherData, etc.ββββ Plugins/β βββ Core/ # Plugin Frameworkβ β βββ NotchPlugin.swift # The Protocolβ β βββ PluginManager.swift # Registry + lifecycleβ β βββ PluginEventBus.swift # Inter-plugin communicationβ β βββ PluginID.swift # Centralized identifiersβ β βββ DisplayPrioritizer.swiftβ ββ βββ Services/ # ALL Infrastructure (61 files)β β βββ ServiceContainer.swift # DI Containerβ β βββ *Protocol.swift # Service contractsβ β βββ *Service.swift # Service implementationsβ β βββ *Manager.swift # System integrations (Volume, Bluetooth, etc.)β β βββ ...β ββ βββ BuiltIn/ # Feature Plugins (bounded contexts)β βββ MusicPlugin/ # Plugin + Views/β βββ ShelfPlugin/ # Plugin + Models/ + Services/ + ViewModels/ + Views/β βββ CalendarPlugin/ # Plugin + Views/β βββ WeatherPlugin/ # Plugin + Views/β βββ TeleprompterPlugin/ # Plugin + Views/ + state filesβ βββ ...ββββ components/ # Shared UI Only (not feature-specific)β βββ Notch/ # Notch chrome, shape, windowβ βββ Settings/ # Settings viewsβ βββ Onboarding/ # First-run flowβ βββ Effects/ # LiquidGlass, MetalBlurβ βββ Live activities/ # HUD views (cross-plugin)β βββ Tabs/ # Tab navigationββββ NotchViewCoordinator.swift # Shared cross-screen stateβββ AppObjectGraph.swift # DI rootβββ ContentView.swift # + Appearance, SubViewsβββ MediaControllers/ # NowPlaying, Spotify, AppleMusic, YouTube, Browserβββ sizing/matters.swift # Pure sizing utility functionsWeather source: OpenWeatherMap is the primary provider for the Weather plugin. WeatherKit is optional fallback-only behavior when available, and weather results are cached in memory for 30 minutes to avoid repeated API calls from hover/open interactions.
β οΈ Known Architecture Debt
Last reviewed: 2026-05-02. Full triage (P1/P2/P3 + blocks annotations) in docs/prds/machNotch.md β Known Architecture Debt.
| Issue | Principle | Priority | Notes |
|---|---|---|---|
ShelfItem referenced by PluginEventBus | Bounded Context | P2 | Event bus carries domain type from shelf context. Fix: type-erased event payload. |
ShelfSelectionModel in ShelfServiceProtocol | DDD Layers | P2 | ViewModel type exposed through service protocol. Fix: expose selection as ID set. |
NotchViewModel dependency in all plugin views | DIP | P3 | Plugin views use @Environment(NotchViewModel.self). Not fully self-contained. |
NotchContentRouter.openContent() switches on NotchViews enum | OCP | P1 | Blocks Phase 9 dynamic plugin views. |
Constants.swift imports SwiftUI for CGFloat | Domain Purity | P3 | Could extract spacing to avoid SwiftUI import in Core/ |
π§ͺ Testing Strategy
The architecture is designed for testability.
- Unit Tests: Plugins are tested in isolation by injecting Mock Services.
- Mocking: Every Service is defined by a protocol (e.g.,
MusicServiceProtocol), allowing the injection of fake implementations that return controlled data.
Example: Testing Music Display Logic
func testMusicPluginRequestsDisplay() async { // 1. Setup let mock = MockMusicService() let plugin = MusicPlugin() await plugin.activate(context: ...services: [music: mock]...)
// 2. Action mock.playbackState.isPlaying = true
// 3. Assertion XCTAssertEqual(plugin.displayRequest?.priority, .high)}