1pub mod daemon;
39
40use thiserror::Error;
41
42#[derive(Error, Debug)]
44pub enum PluginError {
45 #[error("Command failed: {0}")]
46 CommandFailed(String),
47
48 #[error("Invalid argument '{arg}': {message}")]
49 InvalidArg { arg: String, message: String },
50
51 #[error("Argument '{arg}' out of range: {value} (expected {min}-{max})")]
52 ArgOutOfRange {
53 arg: String,
54 value: i64,
55 min: i64,
56 max: i64,
57 },
58
59 #[error("IPC connection error: {message}")]
60 IpcConnection {
61 message: String,
62 #[source]
63 source: Option<Box<dyn std::error::Error + Send + Sync>>,
64 },
65
66 #[error("Daemon not running and could not be started")]
67 DaemonNotRunning {
68 #[source]
69 source: Option<Box<dyn std::error::Error + Send + Sync>>,
70 },
71
72 #[error("IO error: {0}")]
73 Io(#[from] std::io::Error),
74
75 #[error("JSON error: {0}")]
76 Json(#[from] serde_json::Error),
77
78 #[error("Plugin error: {0}")]
79 Other(String),
80}
81
82impl PluginError {
83 pub fn invalid_arg(arg: impl Into<String>, message: impl Into<String>) -> Self {
85 Self::InvalidArg {
86 arg: arg.into(),
87 message: message.into(),
88 }
89 }
90
91 pub fn arg_out_of_range(arg: impl Into<String>, value: i64, min: i64, max: i64) -> Self {
93 Self::ArgOutOfRange {
94 arg: arg.into(),
95 value,
96 min,
97 max,
98 }
99 }
100
101 pub fn ipc_connection(message: impl Into<String>) -> Self {
103 Self::IpcConnection {
104 message: message.into(),
105 source: None,
106 }
107 }
108
109 pub fn ipc_connection_with_source(
111 message: impl Into<String>,
112 source: impl std::error::Error + Send + Sync + 'static,
113 ) -> Self {
114 Self::IpcConnection {
115 message: message.into(),
116 source: Some(Box::new(source)),
117 }
118 }
119
120 pub fn daemon_not_running() -> Self {
122 Self::DaemonNotRunning { source: None }
123 }
124
125 pub fn daemon_not_running_with_source(
127 source: impl std::error::Error + Send + Sync + 'static,
128 ) -> Self {
129 Self::DaemonNotRunning {
130 source: Some(Box::new(source)),
131 }
132 }
133}
134
135pub type PluginResult<T> = Result<T, PluginError>;
137
138#[derive(Debug, Clone)]
140pub struct CommandDef {
141 pub name: String,
143 pub description: String,
145 pub usage: Option<String>,
147 pub args: Vec<ArgDef>,
149}
150
151impl CommandDef {
152 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
153 Self {
154 name: name.into(),
155 description: description.into(),
156 usage: None,
157 args: Vec::new(),
158 }
159 }
160
161 pub fn with_usage(mut self, usage: impl Into<String>) -> Self {
162 self.usage = Some(usage.into());
163 self
164 }
165
166 pub fn with_arg(mut self, arg: ArgDef) -> Self {
167 self.args.push(arg);
168 self
169 }
170}
171
172#[derive(Debug, Clone)]
174pub struct ArgDef {
175 pub name: String,
176 pub description: String,
177 pub required: bool,
178}
179
180impl ArgDef {
181 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
182 Self {
183 name: name.into(),
184 description: description.into(),
185 required: true,
186 }
187 }
188
189 pub fn optional(mut self) -> Self {
190 self.required = false;
191 self
192 }
193}
194
195#[derive(Debug)]
198pub struct PluginContext {
199 pub data_dir: std::path::PathBuf,
201 pub extra: std::collections::HashMap<String, String>,
203 pub config_json: Option<String>,
205}
206
207impl PluginContext {
208 pub fn new(data_dir: std::path::PathBuf) -> Self {
209 Self {
210 data_dir,
211 extra: std::collections::HashMap::new(),
212 config_json: None,
213 }
214 }
215
216 pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
217 self.extra.insert(key.into(), value.into());
218 self
219 }
220
221 pub fn with_config(mut self, config_json: impl Into<String>) -> Self {
222 self.config_json = Some(config_json.into());
223 self
224 }
225
226 pub fn get_config<T: serde::de::DeserializeOwned + Default>(&self) -> T {
229 self.config_json
230 .as_ref()
231 .and_then(|json| serde_json::from_str(json).ok())
232 .unwrap_or_default()
233 }
234
235 pub fn try_get_config<T: serde::de::DeserializeOwned>(&self) -> PluginResult<Option<T>> {
237 match &self.config_json {
238 Some(json) => Ok(Some(serde_json::from_str(json)?)),
239 None => Ok(None),
240 }
241 }
242}
243
244#[derive(Debug)]
246pub enum CommandResult {
247 Success(Option<String>),
249 Error(String),
251 Async(String),
253}
254
255pub trait Plugin: Send + Sync {
257 fn name(&self) -> &str;
259
260 fn version(&self) -> &str;
262
263 fn description(&self) -> &str;
265
266 fn commands(&self) -> Vec<CommandDef>;
268
269 fn execute(
271 &self,
272 command: &str,
273 args: &[String],
274 ctx: &mut PluginContext,
275 ) -> PluginResult<CommandResult>;
276
277 fn on_load(&self) -> PluginResult<()> {
279 Ok(())
280 }
281
282 fn on_unload(&self) -> PluginResult<()> {
284 Ok(())
285 }
286}
287
288#[async_trait::async_trait]
290pub trait AsyncPlugin: Plugin {
291 async fn execute_async(
293 &self,
294 command: &str,
295 args: &[String],
296 ctx: &mut PluginContext,
297 ) -> PluginResult<CommandResult>;
298}
299
300#[repr(C)]
302pub struct RawPlugin {
303 pub data: *mut (),
304 pub vtable: *const (),
305}
306
307unsafe impl Send for RawPlugin {}
309unsafe impl Sync for RawPlugin {}
310
311impl RawPlugin {
312 pub fn from_boxed(plugin: Box<dyn Plugin>) -> Self {
317 let raw: *mut dyn Plugin = Box::into_raw(plugin);
318 unsafe {
319 let parts: (*mut (), *const ()) = std::mem::transmute(raw);
320 Self {
321 data: parts.0,
322 vtable: parts.1,
323 }
324 }
325 }
326
327 pub unsafe fn into_boxed(self) -> Box<dyn Plugin> {
332 unsafe {
333 let raw: *mut dyn Plugin = std::mem::transmute((self.data, self.vtable));
334 Box::from_raw(raw)
335 }
336 }
337
338 pub fn is_null(&self) -> bool {
340 self.data.is_null()
341 }
342}
343
344pub type PluginCreateFn = unsafe extern "C" fn() -> RawPlugin;
346
347pub type PluginDestroyFn = unsafe extern "C" fn(RawPlugin);
349
350#[macro_export]
362macro_rules! export_plugin {
363 ($plugin_type:ty) => {
364 #[unsafe(no_mangle)]
365 pub extern "C" fn taiga_plugin_create() -> $crate::RawPlugin {
366 let plugin: Box<dyn $crate::Plugin> = Box::new(<$plugin_type>::new());
367 $crate::RawPlugin::from_boxed(plugin)
368 }
369
370 #[unsafe(no_mangle)]
371 pub extern "C" fn taiga_plugin_destroy(plugin: $crate::RawPlugin) {
372 unsafe {
373 let _ = plugin.into_boxed();
374 }
376 }
377 };
378}
379
380#[derive(Debug, Clone)]
382pub struct PluginInfo {
383 pub name: String,
384 pub version: String,
385 pub description: String,
386 pub commands: Vec<CommandDef>,
387}
388
389impl PluginInfo {
390 pub fn from_plugin(plugin: &dyn Plugin) -> Self {
391 Self {
392 name: plugin.name().to_string(),
393 version: plugin.version().to_string(),
394 description: plugin.description().to_string(),
395 commands: plugin.commands(),
396 }
397 }
398}