Files
Everjoy/doc/Architecture-Runtime-and-Plugin.md

8.0 KiB

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

/// 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

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:

// 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

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:

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

// 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