Skip to content

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

  1. PluginManager: The brain of the extension system. It manages the lifecycle (load, activate, deactivate) of all plugins and acts as the central registry.
  2. ServiceContainer: A dependency injection container that holds all system services (MusicService, BatteryService, etc.). Plugins request services from here.
  3. 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?).
  4. NotchContentRouter: The View layer component that maps the State Machine’s output to actual SwiftUI views.
  5. DisplayPrioritizer: Pure struct that determines which plugin wins the closed notch based on DisplayRequest priorities. Extracted from PluginManager (SRP).
  6. 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

  1. Critical (30): Urgent system warnings (e.g., β€œBattery Low”).
  2. High (20): Active user engagement (e.g., β€œMusic Playing”, β€œTimer Running”).
  3. Normal (10): Passive information (e.g., β€œWeather”).
  4. 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 VisualizerPlugin doesn’t need to know about MusicPlugin. It just listens for playbackChanged events on the bus.

⚑️ Concurrency Model

Structured concurrency (async/await) and Actors are strictly used to ensure thread safety.

ComponentIsolationReasoning
NotchPlugin@MainActorPlugins directly drive UI state, so they must stay on the main thread.
Services@MainActorMost system APIs (EventKit, etc.) are main-thread bound or updated via UI run loops.
WorkersTask / actorHeavy 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 around Defaults that namespaces keys.
    • Plugin ID: com.machnotch.weather
    • Key: showTemperature
    • Actual UserDefaults Key: plugin.com.machnotch.weather.showTemperature

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.

  1. System Event: A system event occurs (e.g., Song changed).
  2. Service Update: The MusicService updates its @Observable properties.
  3. Plugin Reaction: The MusicPlugin (observing the service) updates its own state.
  4. 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 functions

Weather 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.

IssuePrinciplePriorityNotes
ShelfItem referenced by PluginEventBusBounded ContextP2Event bus carries domain type from shelf context. Fix: type-erased event payload.
ShelfSelectionModel in ShelfServiceProtocolDDD LayersP2ViewModel type exposed through service protocol. Fix: expose selection as ID set.
NotchViewModel dependency in all plugin viewsDIPP3Plugin views use @Environment(NotchViewModel.self). Not fully self-contained.
NotchContentRouter.openContent() switches on NotchViews enumOCPP1Blocks Phase 9 dynamic plugin views.
Constants.swift imports SwiftUI for CGFloatDomain PurityP3Could 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)
}