Architecture: HyperBar Standalone Process Extraction
bin
## Overview
HyperBar has been extracted from the HyperSpace monolith into a standalone helper process. Previously, the bar was hosted by Summon (as "SummonDash"), creating cross-process fragility where HyperSpace depended on a separate app for its primary UI surface. Now HyperSpace fully owns its bar.
The standalone process is bundled at `HyperSpace.app/Contents/Helpers/HyperBar.app`, managed by launchd for crash recovery, and communicates with the main process via DistributedNotificationCenter (state push) and CLI socket (action dispatch).
## Architecture
### SPM Module Graph
```
BarContracts (7 files) — pure types, zero dependencies
↓
BarSurface (5 files) — NSPanel + CALayer rendering
↓
BarRuntime (5 files) — actor state, widgets, snapshot persistence
↓
HyperBarApp (2 files) — @main executable, launchd-managed
```
All four modules are separate SPM targets. Core depends on BarContracts for shared types. The standalone HyperBarApp links all four plus SwiftLib.
### Module Contents
**BarContracts** — The shared boundary. No rendering, no state, no IO.
- `HyperBarState` — Codable workspace/window state (~2-4KB JSON)
- `BarIPC` — DNC notification names, userInfo keys, action identifiers
- `HyperBarFormatter` — Window count formatting (dot/number styles)
- `BarWidgetConfig`, `BarStyleConfig` — Config value types for parametrized rendering
- `CokeBadgeMode` — CPU badge state enum
**BarSurface** — Pure rendering. No IPC, no state management.
- `BarWindow` — Borderless NSPanel (non-activating, always-on-top, screen-aware positioning)
- `BarView` — NSView with CALayer rendering, hit detection, drag handling
- `BarItem` — Data model for rendered items (workspace badges, float badges, widgets)
- `BarStyles` — Style cache built from config (colors, fonts, alpha values)
- `BarAnimator` — Smooth show/hide/reveal transitions
**BarRuntime** — Widget lifecycle and state persistence.
- `BarStateActor` — Thread-safe actor wrapping `HyperBarState`, generation-based change detection
- `BarWidgetManager` — Clock, battery, Coke (CPU) widget lifecycle + callbacks
- `BarSnapshotStore` — Persists state to `~/.cache/screenshots/bar-state.json` (instant startup render)
- `BarConfigReader` — Reads `HyperBarStyle.yaml` for widget/style config
**HyperBarApp** — Minimal entry point.
- `HyperBarApp.swift` — `@main`, PID duplicate detection, `NSApplication.shared.run()`
- `HyperBarDelegate.swift` — `NSApplicationDelegate`, DNC observers, click→CLI dispatch
## IPC Design
### State Push: HyperSpace → HyperBar (DNC)
```
Core/HyperBar.pushState()
→ WindowRegistry.rebuild()
→ buildState() → HyperBarState
→ JSONEncoder.encode(state)
→ DispatchQueue.global(.utility).async {
DNC.post("vx.HyperSpace.barStateChanged",
userInfo: [stateKey: jsonString],
deliverImmediately: true)
}
```
Debounced at 50ms via `schedulePushState()` to coalesce rapid events. State is always pushed — both in-process render and DNC happen on every change.
Config reload posts `vx.HyperSpace.barConfigChanged` (no payload) so the standalone process re-reads `HyperBarStyle.yaml`.
### Action Dispatch: HyperBar → HyperSpace (CLI socket)
The standalone bar doesn't send DNC back. Instead it spawns a CLI subprocess:
```
HyperBarDelegate.sendBarAction("focus-workspace", args: ["dev"])
→ Process("/path/to/hyperspace", ["bar-action", "focus-workspace", "dev"])
```
CLI path resolution: `~/.local/bin/swift/hyperspace` first, falls back to `/Applications/vx/HyperSpace.app/Contents/MacOS/HyperSpaceCLI`.
This routes through the standard socket server. `BarActionCommand` in Core handles 6 actions:
| Action | Purpose |
|--------|---------|
| `focus-workspace` | Navigate to workspace |
| `float-toggle` | Toggle floating window visibility |
| `window-drop` | Move window to target workspace |
| `workspace-reorder` | Reorder workspace within pool |
| `context-close-ws` | Close all windows in workspace |
| `context-rename-ws` | Rename workspace |
### Why DNC + CLI (not bidirectional DNC)
- **State push (DNC):** Fire-and-forget, no response needed, ~2-4KB JSON is well within DNC limits. The main process doesn't need to know if the bar received it.
- **Action dispatch (CLI):** Needs the full command infrastructure (tree mutation, layout, focus). Routing through the socket server reuses all existing command validation, error handling, and state refresh. Adding DNC→command dispatch would duplicate this.
- **No auth needed:** DNC is localhost-only. CLI subprocess inherits user privileges. Both are local-only transports on a single-user desktop app.
## Process Lifecycle
### Bundle Layout
```
HyperSpace.app/Contents/
MacOS/HyperSpaceApp
Helpers/HyperBar.app/Contents/
MacOS/HyperBarApp
Info.plist (CFBundleIdentifier: vx.HyperBar, LSUIElement: true)
```
### launchd Management
`~/Library/LaunchAgents/vx.HyperBar.plist`:
- `RunAtLoad: true` — starts with user session
- `KeepAlive.SuccessfulExit: false` — restarts on crash (not on clean exit)
- `ProcessType: Interactive` — scheduling priority
- `LimitLoadToSessionType: Aqua` — only GUI sessions
### Build Integration
**`just deploy` hooks:**
1. `_bundle_extras` — copies built `HyperBarApp` binary into `Helpers/HyperBar.app`, generates minimal `Info.plist`
2. `_post_install` — bootouts existing agent, sed-replaces `__HYPERBAR_PATH__` in plist template, bootstraps new agent
### Startup Sequence
1. PID duplicate detection via `NSRunningApplication.runningApplications(withBundleIdentifier: "vx.HyperBar")`
2. Load snapshot from `bar-state.json` for instant render before first DNC push
3. Register DNC observers (`stateChanged`, `configChanged`)
4. Create `BarWindow` + `BarView` with snapshot state
5. Start `BarWidgetManager` (clock, battery, CPU)
6. Wait for live state pushes from main process
## Design Decisions
### Why standalone process?
1. **Crash isolation** — Bar survives HyperSpace crashes. launchd restarts the bar independently.
2. **Independent lifecycle** — Bar starts with user session, doesn't need HyperSpace running to show last-known state.
3. **Clean module boundary** — Forces all bar rendering to be parametrized (no global config reads). BarSurface takes `BarStyleConfig`; BarRuntime takes `BarWidgetConfig`.
4. **Future: independent updates** — Bar can be updated without restarting the WM.
### Why not a standalone toggle?
The original plan included `hyperbar.standalone: true/false` for fallback rendering. This was skipped because:
- The standalone bar doesn't render independently yet (`renderState` is a TODO stub)
- A toggle would disable the working in-process bar with nothing to replace it
- Both modes coexist naturally: in-process renders, standalone receives state for future use
### Why BarContracts as the shared boundary?
Core needs to serialize `HyperBarState` and post DNC notifications. The standalone app needs to deserialize the same types. `BarContracts` is the minimal shared surface — pure Codable types, string constants, and formatting helpers. No AppKit, no rendering, no state management.
### Why custom CmdArgs parser for bar-action?
Standard `cmdParser`/`parseSpecificCmdArgs` doesn't handle variable trailing args (action name + 0-N action-specific args). `parseBarActionCmdArgs` manually parses the raw args array, validates the action name against `BarActionKind`, and passes remaining args through.
## Current State
Both in-process and standalone bar coexist. Core always renders locally AND pushes state via DNC. The standalone process receives and persists state but doesn't yet render independently. Next step: implement `renderState()` in `HyperBarDelegate` to drive `BarView` from deserialized `HyperBarState`, then optionally disable in-process rendering.
## Related Files
- `Package.swift` — SPM targets for BarContracts, BarSurface, BarRuntime, HyperBarApp
- `Sources/BarContracts/` — 7 files (shared types)
- `Sources/BarSurface/` — 5 files (rendering)
- `Sources/BarRuntime/` — 5 files (state + widgets)
- `Sources/HyperBarApp/` — 2 files (executable)
- `Sources/Core/bar/HyperBar.swift` — state push integration
- `Sources/Core/command/impl/BarActionCommand.swift` — action dispatch
- `Sources/Shared/cmdArgs/impl/BarActionCmdArgs.swift` — CLI parser
- `App/vx.HyperBar.plist` — launchd template
- `justfile` — `_bundle_extras`, `_post_install` hooks
关闭于 2026-03-10 1 条评论