ITADN

Architecture: HyperBar Standalone Process Extraction

#1999Closedrisenowrise 创建于 2026-03-10
bin
R
risenowrisecommented
## 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 条评论