lattice_plugin_manager/lib.rs
1//! The buffer-backed `:plugins` manager view: `plugins-mode` and the
2//! `:plugins` ex-command (PL8.H.2).
3//!
4//! A pure PROVIDER crate (the `oil` / `lattice-dashboard` shape): it owns
5//! `plugins-mode` (major, read-only) and the `:plugins` ex-command, and reads the
6//! loader's `plugin_status()` (PL8.H.1) via the `PluginLoaderHandle` service at
7//! activation. Everything wires through the generic [`SubsystemBoot`] seam, so
8//! adding it to the editor is **one line** in the host's Phase-B install list and
9//! **zero** host internals — no `Editor::` method, no host `Action` variant (the
10//! mode-ownership acid test).
11//!
12//! Open flow, all generic primitives (no provider-specific host code):
13//! 1. `:plugins` (registered here) returns
14//! [`Effect::OpenSyntheticBuffer`](lattice_grammar::Effect::OpenSyntheticBuffer)
15//! `{ name: "*plugins*", mode_id: "plugins-mode" }`.
16//! 2. The host generically ensures that buffer under `plugins-mode` + activates
17//! it (`Editor::open_synthetic_buffer`).
18//! 3. `PluginManagerMode::on_activate` projects the status table into the
19//! buffer and subscribes to `PluginCrashed` for live health.
20//!
21//! Interactivity (reload / unload / describe chords) is PL8.H.3.
22
23use std::sync::Arc;
24
25use lattice_grammar::{Args, Effect, ExCommandSpec, LatencyClass, SurfaceForm};
26use lattice_mode::SubsystemBoot;
27
28mod actions;
29mod headerline;
30mod mode;
31mod render;
32
33pub use mode::PluginManagerMode;
34pub use render::{
35 PLUGINS_BUFFER_NAME, PLUGINS_MODE_ID, render_status, render_status_with_failures,
36};
37
38/// Install the plugin-manager view: register `plugins-mode` + the `:plugins`
39/// ex-command. Seated in the host's Phase-B install list (before the registry
40/// freeze), so it registers directly via `boot.modes_mut()` / `boot.commands_mut()`.
41///
42/// No dependency on the loader being installed first: the mode resolves
43/// `PluginLoaderHandle` at *activation* (by which point boot is complete), and
44/// the ex-command returns a static effect. A missing loader service at activation
45/// degrades to an empty buffer, never a panic.
46pub fn install(boot: &mut impl SubsystemBoot) {
47 boot.modes_mut()
48 .register(PluginManagerMode)
49 .expect("plugins-mode registers without conflict");
50
51 // PL8.H.3: the in-view `action:plugins-*` commands (dead-body) so the mode's
52 // keymap `cmd:` names resolve; the mode's `action_handlers` do the work.
53 actions::register_actions(boot.commands_mut());
54
55 boot.commands_mut().register_ex_command(
56 "plugins",
57 "Open the plugin manager (`:plugins`) — a buffer listing every loaded \
58 plugin with its health (quarantined after a crash), trust tier, and \
59 capabilities granted/denied. Read-only; reload / unload act on plugins \
60 via `:plugin-reload` / `:plugin-unload` (in-view chords land in a \
61 follow-up).",
62 plugins_spec(),
63 );
64}
65
66/// The `:plugins` ex-command: takes no argument, returns the generic
67/// open-synthetic-buffer effect. `Reflex` — it does no blocking work on the
68/// dispatch path (the host runs the open; the mode projects content off-thread).
69fn plugins_spec() -> ExCommandSpec {
70 ExCommandSpec {
71 latency_class: LatencyClass::Reflex,
72 accepts_bang: false,
73 accepts_range: false,
74 parse_args: Arc::new(|_line: &str, _bang: bool| Ok(Args::None)),
75 apply: Arc::new(|_ctx| {
76 Ok(Effect::OpenSyntheticBuffer {
77 name: PLUGINS_BUFFER_NAME.to_string(),
78 mode_id: PLUGINS_MODE_ID.to_string(),
79 content: None,
80 cursor: None,
81 activate_minor: None,
82 })
83 }),
84 args_schema: Vec::new(),
85 surface_form: SurfaceForm::Keyword,
86 }
87}