# Everjoy Architecture: Runtime Subsystem + Plugin Subsystem ## Design Principle **Separate concerns:** - **Runtime Subsystem** = the engine. Defines interfaces, manages lifecycle, routes events, schedules rendering. Everything (built-in + external) depends on it. - **Plugin Subsystem** = the adapter. Loads external code, translates C-ABI ↔ Rust trait. Only external modules go through it. ``` ┌─────────────────────────────────────────┐ │ Runtime Subsystem │ │ (supporter for everything) │ ├─────────────────────────────────────────┤ │ Module Registry │ │ - PanelProvider registry │ │ - MenuItem registry │ │ - StatusWidget registry │ │ - Tool registry │ ├─────────────────────────────────────────┤ │ Lifecycle Manager │ │ - init → render → event → hide → │ │ show → shutdown │ ├─────────────────────────────────────────┤ │ Event Router │ │ - Layer 1: System shortcuts │ │ - Layer 2: Focused PanelProvider │ │ - Layer 3: Global shortcuts / Menubar │ ├─────────────────────────────────────────┤ │ Render Scheduler │ │ - Calls Provider::render() each frame │ │ - Blits cell grid → Buffer for │ │ external providers │ └─────────────────────────────────────────┘ ↑ ↓ traits ┌─────────────────────────────────────────┐ │ Plugin Subsystem │ │ (loader + translator) │ ├─────────────────────────────────────────┤ │ Extension Loader │ │ - Scan plugin directory │ │ - Load .wasm / .so / static │ │ - Symbol resolution │ ├─────────────────────────────────────────┤ │ Translation Layer │ │ - C-ABI ↔ Rust trait │ │ - EjSurface memory management │ │ - Cell grid → Buffer blitting │ │ - Style resolution │ │ - Panic isolation │ └─────────────────────────────────────────┘ ``` --- ## Runtime Subsystem Defines the **trait interfaces** that all modules must satisfy. Built-in modules implement these directly. External modules satisfy them via the Plugin Subsystem. ### Core Traits ```rust /// Anything that can render into a panel pub trait PanelProvider: Send { fn init(&mut self, ctx: &mut dyn ProviderCtx); fn render(&mut self, area: Rect, buf: &mut Buffer); fn handle_event(&mut self, event: Event) -> EventResult; fn focus_changed(&mut self, focused: bool); fn hide(&mut self); fn show(&mut self); fn shutdown(&mut self); fn can_open(&self, context: &OpenContext) -> bool; fn keep_alive_on_hide(&self) -> bool { false } } /// Callable by Agents pub trait Tool: Send { fn name(&self) -> &str; fn description(&self) -> &str; fn schema(&self) -> ToolSchema; // JSON Schema for LLM function calling fn execute(&self, input: ToolInput) -> ToolOutput; } /// Menubar entry pub trait MenuItem: Send { fn id(&self) -> &str; fn label(&self) -> &str; fn parent(&self) -> Option<&str>; fn shortcut(&self) -> Option<&str>; fn invoke(&self); } /// Status bar widget pub trait StatusWidget: Send { fn id(&self) -> &str; fn position(&self) -> StatusPosition; fn render(&self, area: Rect, buf: &mut Buffer); } ``` ### Module Registry ```rust pub struct ModuleRegistry { panel_providers: HashMap>, menu_items: Vec>, status_widgets: Vec>, tools: HashMap>, } ``` Built-in modules register directly: ```rust // In everjoy/src/main.rs or a built-in module initializer runtime.register_panel_provider("editor", Box::new(EditorProvider::new())); runtime.register_panel_provider("terminal", Box::new(TerminalProvider::new())); runtime.register_panel_provider("tasktree", Box::new(TaskTreeProvider::new())); runtime.register_tool("read_file", Box::new(ReadFileTool)); runtime.register_menu_item(Box::new(QuitMenuItem)); ``` ### Lifecycle (Runtime-managed) ``` Load (built-in = static init, external = via Plugin Subsystem) ↓ bootstrap() — register all contributions ↓ Running — render / event / command loop ↓ Unload — shutdown() all providers, unregister all contributions ``` --- ## Plugin Subsystem Only responsible for **external modules**. Built-in modules never touch it. ### Extension Loader ```rust pub struct ExtensionLoader { plugin_dir: PathBuf, } impl ExtensionLoader { pub fn scan_and_load(&self, runtime: &mut Runtime) -> Result<()> { for entry in fs::read_dir(&self.plugin_dir)? { let path = entry?.path(); let plugin = self.load(&path)?; plugin.bootstrap(runtime.registrar())?; } Ok(()) } fn load(&self, path: &Path) -> Result> { match path.extension() { Some("wasm") => self.load_wasm(path), Some("so") | Some("dll") => self.load_dylib(path), _ => Err("Unknown plugin format"), } } } ``` ### Translation Layer For external plugins (especially WASM), the Plugin Subsystem provides a translation layer: ```rust /// Wraps a C-ABI / WASM plugin into Runtime-compatible traits pub struct TranslatedProvider { wasm_instance: wasmtime::Instance, // ... cached function handles } impl PanelProvider for TranslatedProvider { fn render(&mut self, area: Rect, buf: &mut Buffer) { // 1. Allocate EjSurface let mut surface = EjSurface::new(area.width, area.height); // 2. Call plugin's C render function self.call_render(&mut surface); // 3. Blit cell grid into ratatui Buffer self.blit(&surface, area, buf); } // ... other methods translated similarly } ``` ### Key Rule > **Built-in modules implement Runtime traits directly.** > **External modules are wrapped by Plugin Subsystem to satisfy Runtime traits.** ```rust // Built-in: direct trait implementation runtime.register_panel_provider("editor", Box::new(EditorProvider::new())); // External: loaded via Plugin Subsystem, wrapped to satisfy trait let plugin = plugin_subsystem.load("image-viewer.wasm")?; let provider = plugin.create_provider("image-viewer.main")?; runtime.register_panel_provider("image-viewer", Box::new(provider)); // ^^^^ provider here is a TranslatedProvider // that implements PanelProvider ``` --- ## Summary | Concern | Runtime Subsystem | Plugin Subsystem | |---------|-------------------|------------------| | **Defines** | Trait interfaces | Loading + translation mechanism | | **Used by** | Everyone (built-in + external) | Only external modules | | **Manages** | Registry, lifecycle, events, render | File I/O, WASM runtime, FFI | | **Depends on** | ratatui, crossterm, tokio | wasmtime / libloading, Runtime traits | | **Can be omitted?** | No — without it, no app | Yes — without it, only built-in modules work |