219 lines
8.0 KiB
Markdown
219 lines
8.0 KiB
Markdown
# 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<String, Box<dyn PanelProvider>>,
|
|
menu_items: Vec<Box<dyn MenuItem>>,
|
|
status_widgets: Vec<Box<dyn StatusWidget>>,
|
|
tools: HashMap<String, Box<dyn Tool>>,
|
|
}
|
|
```
|
|
|
|
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<Box<dyn Plugin>> {
|
|
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 |
|