Skip to main content

scarab_plugin_api/
context.rs

1//! Plugin context providing access to terminal state
2
3use crate::{
4    error::Result,
5    types::{Cell, ModalItem, RemoteCommand},
6};
7use parking_lot::Mutex;
8use serde::Deserialize;
9use std::{collections::HashMap, sync::Arc};
10
11/// Shared state accessible to plugins
12///
13/// This wraps the protocol's SharedState with a simpler interface for plugins.
14/// The protocol's SharedState uses #[repr(C)] for IPC, while this one provides
15/// a high-level API for plugin development.
16#[derive(Debug)]
17pub struct PluginSharedState {
18    /// Terminal grid cells
19    pub cells: Vec<Cell>,
20    /// Grid width in columns
21    pub cols: u16,
22    /// Grid rows
23    pub rows: u16,
24    /// Current cursor position
25    pub cursor: (u16, u16),
26    /// Environment variables
27    pub env: HashMap<String, String>,
28    /// Custom plugin-specific data storage
29    pub data: HashMap<String, String>,
30    /// Aggregated list of commands from all plugins
31    pub commands: Vec<ModalItem>,
32}
33
34impl PluginSharedState {
35    /// Create new shared state
36    pub fn new(cols: u16, rows: u16) -> Self {
37        let size = (cols as usize) * (rows as usize);
38        Self {
39            cells: vec![Cell::default(); size],
40            cols,
41            rows,
42            cursor: (0, 0),
43            env: std::env::vars().collect(),
44            data: HashMap::new(),
45            commands: Vec::new(),
46        }
47    }
48
49    /// Get cell at position
50    pub fn get_cell(&self, x: u16, y: u16) -> Option<Cell> {
51        if x >= self.cols || y >= self.rows {
52            return None;
53        }
54        let idx = (y as usize) * (self.cols as usize) + (x as usize);
55        self.cells.get(idx).copied()
56    }
57
58    /// Set cell at position
59    pub fn set_cell(&mut self, x: u16, y: u16, cell: Cell) -> bool {
60        if x >= self.cols || y >= self.rows {
61            return false;
62        }
63        let idx = (y as usize) * (self.cols as usize) + (x as usize);
64        if let Some(c) = self.cells.get_mut(idx) {
65            *c = cell;
66            true
67        } else {
68            false
69        }
70    }
71
72    /// Get line of text
73    pub fn get_line(&self, y: u16) -> Option<String> {
74        if y >= self.rows {
75            return None;
76        }
77        let start = (y as usize) * (self.cols as usize);
78        let end = start + (self.cols as usize);
79        Some(
80            self.cells[start..end]
81                .iter()
82                .map(|c| c.c)
83                .collect::<String>()
84                .trim_end()
85                .to_string(),
86        )
87    }
88}
89
90/// Context provided to plugins for interacting with the terminal
91#[derive(Clone)]
92pub struct PluginContext {
93    /// Plugin-specific configuration
94    pub config: PluginConfigData,
95    /// Shared terminal state
96    pub state: Arc<Mutex<PluginSharedState>>,
97    /// Logger name for this plugin
98    pub logger_name: String,
99    /// Queue of commands to be sent to the client/daemon
100    pub commands: Arc<Mutex<Vec<RemoteCommand>>>,
101}
102
103impl PluginContext {
104    /// Create new plugin context
105    pub fn new(
106        config: PluginConfigData,
107        state: Arc<Mutex<PluginSharedState>>,
108        logger_name: impl Into<String>,
109    ) -> Self {
110        Self {
111            config,
112            state,
113            logger_name: logger_name.into(),
114            commands: Arc::new(Mutex::new(Vec::new())),
115        }
116    }
117
118    /// Queue a command to be sent to the client or daemon
119    pub fn queue_command(&self, cmd: RemoteCommand) {
120        self.commands.lock().push(cmd);
121    }
122
123    /// Get cell at position
124    pub fn get_cell(&self, x: u16, y: u16) -> Option<Cell> {
125        self.state.lock().get_cell(x, y)
126    }
127
128    /// Set cell at position
129    pub fn set_cell(&self, x: u16, y: u16, cell: Cell) -> bool {
130        self.state.lock().set_cell(x, y, cell)
131    }
132
133    /// Get line of text at row
134    pub fn get_line(&self, y: u16) -> Option<String> {
135        self.state.lock().get_line(y)
136    }
137
138    /// Get terminal size
139    pub fn get_size(&self) -> (u16, u16) {
140        let state = self.state.lock();
141        (state.cols, state.rows)
142    }
143
144    /// Get cursor position
145    pub fn get_cursor(&self) -> (u16, u16) {
146        self.state.lock().cursor
147    }
148
149    /// Get environment variable
150    pub fn get_env(&self, key: &str) -> Option<String> {
151        self.state.lock().env.get(key).cloned()
152    }
153
154    /// Store plugin-specific data
155    pub fn set_data(&self, key: impl Into<String>, value: impl Into<String>) {
156        self.state.lock().data.insert(key.into(), value.into());
157    }
158
159    /// Retrieve plugin-specific data
160    pub fn get_data(&self, key: &str) -> Option<String> {
161        self.state.lock().data.get(key).cloned()
162    }
163
164    /// Log a message with the integrated logging system
165    ///
166    /// Messages are sent to both the Rust logging infrastructure (using the `log` crate)
167    /// and queued as a remote command to be forwarded to connected clients for display.
168    pub fn log(&self, level: LogLevel, message: &str) {
169        // Use Rust's standard logging macros for local logging
170        match level {
171            LogLevel::Error => log::error!("[{}] {}", self.logger_name, message),
172            LogLevel::Warn => log::warn!("[{}] {}", self.logger_name, message),
173            LogLevel::Info => log::info!("[{}] {}", self.logger_name, message),
174            LogLevel::Debug => log::debug!("[{}] {}", self.logger_name, message),
175        }
176
177        // Queue a remote command to send the log to clients
178        self.queue_command(RemoteCommand::PluginLog {
179            plugin_name: self.logger_name.clone(),
180            level,
181            message: message.to_string(),
182        });
183    }
184
185    /// Send a notification to the user
186    ///
187    /// Notifications are displayed as UI overlays in the client with auto-dismiss after 5 seconds.
188    /// The notification level determines the visual styling (color, icon, etc.).
189    pub fn notify(&self, title: &str, body: &str, level: NotifyLevel) {
190        // Queue notification as a remote command
191        self.queue_command(RemoteCommand::PluginNotify {
192            title: title.to_string(),
193            body: body.to_string(),
194            level,
195        });
196    }
197
198    /// Convenience method to send an info notification
199    pub fn notify_info(&self, title: &str, body: &str) {
200        self.notify(title, body, NotifyLevel::Info);
201    }
202
203    /// Convenience method to send a success notification
204    pub fn notify_success(&self, title: &str, body: &str) {
205        self.notify(title, body, NotifyLevel::Success);
206    }
207
208    /// Convenience method to send a warning notification
209    pub fn notify_warning(&self, title: &str, body: &str) {
210        self.notify(title, body, NotifyLevel::Warning);
211    }
212
213    /// Convenience method to send an error notification
214    pub fn notify_error(&self, title: &str, body: &str) {
215        self.notify(title, body, NotifyLevel::Error);
216    }
217}
218
219/// Log levels for plugin logging
220#[derive(Debug, Copy, Clone, PartialEq, Eq)]
221pub enum LogLevel {
222    Error,
223    Warn,
224    Info,
225    Debug,
226}
227
228/// Notification severity levels
229#[derive(Debug, Copy, Clone, PartialEq, Eq)]
230pub enum NotifyLevel {
231    Error,
232    Warning,
233    Info,
234    Success,
235}
236
237/// Plugin-specific configuration data
238#[derive(Debug, Clone, Default, Deserialize, serde::Serialize)]
239pub struct PluginConfigData {
240    #[serde(flatten)]
241    pub data: HashMap<String, toml::Value>,
242}
243
244impl PluginConfigData {
245    /// Get configuration value
246    pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T> {
247        let value = self.data.get(key).ok_or_else(|| {
248            crate::error::PluginError::ConfigError(format!("Missing key: {}", key))
249        })?;
250        T::deserialize(value.clone())
251            .map_err(|e| crate::error::PluginError::ConfigError(e.to_string()))
252    }
253
254    /// Get optional configuration value
255    pub fn get_opt<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
256        self.data
257            .get(key)
258            .and_then(|v| T::deserialize(v.clone()).ok())
259    }
260}