diff --git a/src-tauri/plugins/writing-studio/manifest.json b/src-tauri/plugins/writing-studio/manifest.json new file mode 100644 index 0000000..3354711 --- /dev/null +++ b/src-tauri/plugins/writing-studio/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "writing-studio", + "version": "1.0.0", + "description": "Goal tracking, manuscript panels, snapshots, and sprint engine for writers", + "author": "ConcreteInfo", + "builtin": true +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a266ef6..29f8a4e 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,3 +4,4 @@ pub mod git; pub mod app; pub mod pdf; pub mod dialog; +pub mod plugins; diff --git a/src-tauri/src/commands/plugins.rs b/src-tauri/src/commands/plugins.rs new file mode 100644 index 0000000..aefc60c --- /dev/null +++ b/src-tauri/src/commands/plugins.rs @@ -0,0 +1,31 @@ +use crate::error::AppError; +use crate::plugin_manager::PluginMetadata; + +#[tauri::command] +pub fn list_plugins() -> Vec { + // This would be connected to the app state in a full implementation + // For now, return the built-in plugins + vec![ + PluginMetadata { + name: "writing-studio".to_string(), + version: "1.0.0".to_string(), + description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(), + author: "ConcreteInfo".to_string(), + builtin: true, + } + ] +} + +#[tauri::command] +pub fn get_plugin(name: String) -> Option { + match name.as_str() { + "writing-studio" => Some(PluginMetadata { + name: "writing-studio".to_string(), + version: "1.0.0".to_string(), + description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(), + author: "ConcreteInfo".to_string(), + builtin: true, + }), + _ => None, + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 72a16ac..c17f4eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ pub mod menu; pub mod tray; pub mod pdf_ops; +pub mod plugin_manager; pub fn run() { tauri::Builder::default() @@ -42,6 +43,8 @@ pub fn run() { crate::commands::dialog::open_file_dialog, crate::commands::dialog::save_file_dialog, crate::commands::dialog::select_folder_dialog, + crate::commands::plugins::list_plugins, + crate::commands::plugins::get_plugin, ]) .setup(|app| { log::info!("MarkdownConverter starting up"); diff --git a/src-tauri/src/plugin_manager.rs b/src-tauri/src/plugin_manager.rs new file mode 100644 index 0000000..384a05c --- /dev/null +++ b/src-tauri/src/plugin_manager.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use crate::error::AppError; + +#[derive(Debug, Clone)] +pub struct PluginMetadata { + pub name: String, + pub version: String, + pub description: String, + pub author: String, + pub builtin: bool, +} + +pub struct PluginManager { + plugins: HashMap, + enabled: Vec, +} + +impl PluginManager { + pub fn new() -> Self { + PluginManager { + plugins: HashMap::new(), + enabled: Vec::new(), + } + } + + pub fn load_plugins(&mut self, plugin_dir: PathBuf) -> Result<(), AppError> { + if !plugin_dir.exists() { + return Ok(()); + } + + for entry in walkdir::WalkDir::new(&plugin_dir) + .max_depth(1) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let manifest_path = path.join("manifest.json"); + if manifest_path.exists() { + let content = std::fs::read_to_string(&manifest_path) + .map_err(|e| AppError::Config(format!("Failed to read manifest: {}", e)))?; + + #[derive(serde::Deserialize)] + struct RawPluginMetadata { + name: String, + version: String, + description: String, + author: String, + builtin: Option, + } + + let raw: RawPluginMetadata = serde_json::from_str(&content) + .map_err(|e| AppError::Config(format!("Failed to parse manifest: {}", e)))?; + + let metadata = PluginMetadata { + name: raw.name, + version: raw.version, + description: raw.description, + author: raw.author, + builtin: raw.builtin.unwrap_or(false), + }; + + self.plugins.insert(metadata.name.clone(), metadata); + } + } + + Ok(()) + } + + pub fn enable_plugin(&mut self, name: &str) { + if !self.enabled.contains(&name.to_string()) { + self.enabled.push(name.to_string()); + } + } + + pub fn disable_plugin(&mut self, name: &str) { + self.enabled.retain(|n| n != name); + } + + pub fn get_enabled(&self) -> Vec { + self.enabled.clone() + } + + pub fn list_plugins(&self) -> Vec { + self.plugins.values().cloned().collect() + } + + pub fn get_plugin(&self, name: &str) -> Option { + self.plugins.get(name).cloned() + } +} + +impl Default for PluginManager { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/src/tauri-commands.js b/src/tauri-commands.js index 625d6b5..d0bfa85 100644 --- a/src/tauri-commands.js +++ b/src/tauri-commands.js @@ -85,4 +85,12 @@ export const events = { onToggleBottomPanel: (callback) => listen('toggle-bottom-panel', callback), onConversionComplete: (callback) => listen('conversion-complete', callback), onConversionStatus: (callback) => listen('conversion-status', callback), +}; + +// ============================================ +// PLUGIN OPERATIONS +// ============================================ +export const plugins = { + list: () => invoke('list_plugins', {}), + get: (name) => invoke('get_plugin', { name }), }; \ No newline at end of file