Skip to main content

santui_core/
plugin.rs

1use crate::auth::{AuthHandle, User};
2use crate::db_access::DbAccess;
3use crate::theme::Theme;
4use crossterm::event::{KeyEvent, MouseEvent};
5use ratatui::layout::Rect;
6use ratatui::Frame;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10/// Factory that creates a `Box<dyn Plugin>` from an id, name, and binary path.
11/// The binary (`santui`) sets this to `IpcPluginHost::new_boxed`.
12pub type PluginFactory = Arc<dyn Fn(&str, &str, &Path) -> Box<dyn Plugin + Send> + Send + Sync>;
13
14/// A command that a plugin registers for the Ctrl+P palette.
15#[derive(Debug, Clone)]
16pub struct PluginCmdItem {
17    pub category: String,
18    pub label: String,
19}
20
21pub trait Plugin: Send {
22    fn id(&self) -> &str;
23    fn name(&self) -> &str;
24    fn init(&mut self, ctx: &mut PluginContext) -> Result<(), Box<dyn std::error::Error>>;
25    fn handle_key(&mut self, _key: KeyEvent) -> bool {
26        false
27    }
28    fn handle_mouse(&mut self, _event: &MouseEvent) -> bool {
29        false
30    }
31    fn render(&self, _f: &mut Frame, _area: Rect) {}
32    fn tick(&mut self) {}
33    fn on_focus(&mut self) {}
34    fn on_blur(&mut self) {}
35    fn on_theme_change(&mut self, _theme: &Theme) {}
36    fn on_user_update(&mut self, _user: Option<&User>) {}
37    fn status_hints(&self) -> Vec<(String, String)> {
38        vec![]
39    }
40    /// Palette commands that this plugin registers (category, label).
41    fn commands(&self) -> Vec<PluginCmdItem> {
42        vec![]
43    }
44    /// Called when a palette command from this plugin is selected.
45    /// `index` is the position in `commands()`.
46    fn handle_palette_command(&mut self, _index: usize) {}
47
48    /// Called when a plugin-to-plugin message arrives.
49    fn on_plugin_message(&mut self, _from: &str, _action: &str, _data: &str) {}
50
51    /// Called before the plugin is unloaded (hot-reload or app exit).
52    /// The plugin should flush state, close handles, and prepare to be dropped.
53    /// For IPC plugins this sends `Shutdown` (non-blocking) — the child process
54    /// is killed later in `Drop`/`kill()` when the channel senders are dropped.
55    fn shutdown(&mut self) {}
56
57    /// Return the filesystem path to this plugin's binary, if it runs as an
58    /// external process.  `None` for in-process plugins.  Used by the hot-reload
59    /// mechanism to detect when the binary has been updated on disk.
60    fn binary_path(&self) -> Option<&Path> {
61        None
62    }
63
64    /// Whether the plugin process is still running.
65    /// Returns `true` for in-process plugins.
66    /// IPC plugins override this to check the child process.
67    fn is_alive(&self) -> bool {
68        true
69    }
70
71    /// Whether the plugin has completed initialization and is ready to render.
72    /// In-process plugins are ready immediately. IPC plugins become ready once
73    /// the child process responds to the `Init` message.
74    fn is_ready(&self) -> bool {
75        true
76    }
77
78    /// Whether the plugin's last render included a dim overlay (search palette, dialog, etc.).
79    /// The host uses this to extend the dim to the status bar.
80    fn has_dim_overlay(&self) -> bool {
81        false
82    }
83
84    /// Whether the plugin supports running in the background when the user
85    /// presses Esc (e.g., audio players that should keep playing).
86    /// Default is `false`. Override to return `true` for background-capable plugins.
87    fn can_background(&self) -> bool {
88        false
89    }
90
91    /// Set runtime capabilities (e.g. `"background"`) declared in the plugin manifest.
92    /// Default is no-op; IPC plugins override this to store the value.
93    fn set_capabilities(&mut self, _caps: Vec<String>) {}
94
95    /// Whether this plugin should stay loaded when the user presses Esc.
96    /// Persistent plugins are blurred but not shut down, keeping their
97    /// palette entries and background processes alive.
98    /// Default is `false`. Override to return `true` for plugins that
99    /// must not be unloaded (e.g., the registry plugin).
100    fn persistent(&self) -> bool {
101        false
102    }
103
104    /// Mark this plugin as persistent.  Default is no-op; IPC plugins
105    /// override this to store the value.
106    fn set_persistent(&mut self, _persistent: bool) {}
107
108    /// Check if a pending Esc key response has been resolved.
109    /// Returns `Some(consumed)` if resolved, `None` if still pending
110    /// or no Esc was sent.
111    /// IPC plugins override this; in-process plugins don't need it.
112    fn take_pending_esc_result(&mut self) -> Option<bool> {
113        None
114    }
115
116    /// Process any pending requests from the plugin (e.g. DB read/write,
117    /// authentication). Called once per main loop iteration.
118    /// The default implementation is a no-op. IPC plugins override this
119    /// to forward `PluginRequest` variants to the host.
120    fn process_pending_requests(
121        &mut self,
122        _db: &mut dyn DbAccess,
123        _auth: Option<&Arc<dyn AuthHandle>>,
124    ) {
125    }
126
127    /// Drain any pending launch request from this plugin.
128    /// Returns `Some((id, name))` if a launch was requested, `None` otherwise.
129    fn drain_pending_launch(&mut self) -> Option<(String, String)> {
130        None
131    }
132}
133
134pub struct PluginContext {
135    pub theme: Theme,
136    pub auth: Option<Arc<dyn AuthHandle>>,
137    /// Santui data directory (platform-standard, e.g. `%APPDATA%/santui` on Windows). Plugins can use this
138    /// for persistent storage. The registry plugin uses it to find
139    /// installed plugins and `registry.toml`.
140    pub data_dir: PathBuf,
141    /// Shared runtime log buffer. When set, the plugin receives log entries
142    /// via the host on each tick. Used by the log-viewer plugin.
143    pub log_buffer: Option<std::sync::Arc<crate::logger::LoggerBuffer>>,
144}
145
146impl PluginContext {
147    pub fn new() -> Self {
148        PluginContext {
149            theme: Theme::default(),
150            auth: None,
151            data_dir: PathBuf::new(),
152            log_buffer: None,
153        }
154    }
155}
156
157impl Default for PluginContext {
158    fn default() -> Self {
159        Self::new()
160    }
161}