Skip to main content

scarab_plugin_api/
config.rs

1//! Plugin configuration loading and discovery
2
3use crate::{context::PluginConfigData, error::Result};
4use serde::{Deserialize, Serialize};
5use std::{
6    fs,
7    path::{Path, PathBuf},
8};
9
10/// Plugin configuration from TOML file
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct PluginConfig {
13    /// Plugin name
14    pub name: String,
15    /// Path to plugin file (.fzb or .fsx)
16    pub path: PathBuf,
17    /// Whether plugin is enabled
18    #[serde(default = "default_true")]
19    pub enabled: bool,
20    /// Plugin-specific configuration
21    #[serde(default)]
22    pub config: PluginConfigData,
23}
24
25fn default_true() -> bool {
26    true
27}
28
29impl PluginConfig {
30    /// Load plugin configuration from TOML file
31    pub fn from_file(path: impl AsRef<Path>) -> Result<Vec<Self>> {
32        let content = fs::read_to_string(path)?;
33        let config: PluginsToml = toml::from_str(&content)?;
34        Ok(config.plugin)
35    }
36
37    /// Expand path with home directory
38    pub fn expanded_path(&self) -> PathBuf {
39        expand_path(&self.path)
40    }
41}
42
43/// Root TOML structure for plugins.toml
44#[derive(Debug, Deserialize, Serialize)]
45struct PluginsToml {
46    plugin: Vec<PluginConfig>,
47}
48
49/// Plugin discovery system
50pub struct PluginDiscovery {
51    /// Plugin directories to search
52    search_paths: Vec<PathBuf>,
53}
54
55impl PluginDiscovery {
56    /// Create new discovery with default paths
57    pub fn new() -> Self {
58        let mut search_paths = vec![
59            Self::default_plugin_dir(),
60            PathBuf::from("/usr/local/share/scarab/plugins"),
61            PathBuf::from("/usr/share/scarab/plugins"),
62        ];
63
64        // Add custom path from environment
65        if let Ok(custom_path) = std::env::var("SCARAB_PLUGIN_PATH") {
66            search_paths.insert(0, PathBuf::from(custom_path));
67        }
68
69        Self { search_paths }
70    }
71
72    /// Get default plugin directory (~/.config/scarab/plugins)
73    pub fn default_plugin_dir() -> PathBuf {
74        if let Some(home) = std::env::var_os("HOME") {
75            PathBuf::from(home).join(".config/scarab/plugins")
76        } else {
77            PathBuf::from(".config/scarab/plugins")
78        }
79    }
80
81    /// Get default config file path (~/.config/scarab/plugins.toml)
82    pub fn default_config_path() -> PathBuf {
83        if let Some(home) = std::env::var_os("HOME") {
84            PathBuf::from(home).join(".config/scarab/plugins.toml")
85        } else {
86            PathBuf::from(".config/scarab/plugins.toml")
87        }
88    }
89
90    /// Add search path
91    pub fn add_path(&mut self, path: impl Into<PathBuf>) {
92        self.search_paths.push(path.into());
93    }
94
95    /// Discover all plugin files in search paths
96    pub fn discover(&self) -> Vec<PathBuf> {
97        let mut plugins = Vec::new();
98
99        for dir in &self.search_paths {
100            if let Ok(entries) = fs::read_dir(dir) {
101                for entry in entries.flatten() {
102                    let path = entry.path();
103                    if Self::is_plugin_file(&path) {
104                        plugins.push(path);
105                    }
106                }
107            }
108        }
109
110        plugins
111    }
112
113    /// Check if file is a valid plugin file
114    fn is_plugin_file(path: &Path) -> bool {
115        if !path.is_file() {
116            return false;
117        }
118
119        matches!(
120            path.extension().and_then(|e| e.to_str()),
121            Some("fzb") | Some("fsx")
122        )
123    }
124
125    /// Load plugins from configuration file
126    pub fn load_config(&self, path: Option<&Path>) -> Result<Vec<PluginConfig>> {
127        let config_path = path
128            .map(PathBuf::from)
129            .unwrap_or_else(Self::default_config_path);
130
131        if !config_path.exists() {
132            return Ok(Vec::new());
133        }
134
135        PluginConfig::from_file(config_path)
136    }
137
138    /// Create default plugin directory if it doesn't exist
139    pub fn ensure_plugin_dir() -> Result<PathBuf> {
140        let dir = Self::default_plugin_dir();
141        if !dir.exists() {
142            fs::create_dir_all(&dir)?;
143        }
144        Ok(dir)
145    }
146
147    /// Create default config file with example
148    pub fn create_default_config() -> Result<PathBuf> {
149        let config_path = Self::default_config_path();
150
151        if config_path.exists() {
152            return Ok(config_path);
153        }
154
155        // Ensure parent directory exists
156        if let Some(parent) = config_path.parent() {
157            fs::create_dir_all(parent)?;
158        }
159
160        // Create example config with delightful examples
161        let example_config = r#"# 🎉 Scarab Plugin Configuration
162#
163# Welcome to plugin paradise! This is where you configure all your terminal
164# superpowers. Each plugin can transform your terminal experience in unique ways.
165#
166# 💡 Pro Tips:
167#   - Plugins are loaded in the order they appear here
168#   - Use `enabled = false` to temporarily disable a plugin
169#   - Check ~/.config/scarab/plugins/ for available plugins
170#   - Create your own plugins - it's easier than you think!
171#
172# 🚀 Get started by uncommenting one of the examples below!
173
174# Example 1: Error Notification Plugin
175# Gets your attention when something goes wrong
176#
177# [[plugin]]
178# name = "error-notifier"
179# path = "~/.config/scarab/plugins/error-notifier.fzb"
180# enabled = true
181#
182# [plugin.config]
183# keywords = ["ERROR", "FAIL", "PANIC", "FATAL"]
184# notification_style = "urgent"
185# play_sound = false
186
187# Example 2: Git Status Plugin
188# Shows git branch and status in your terminal
189#
190# [[plugin]]
191# name = "git-helper"
192# path = "~/.config/scarab/plugins/git-helper.fsx"
193# enabled = true
194#
195# [plugin.config]
196# show_branch = true
197# show_dirty = true
198# emoji_mode = true  # 🌿 for branches, ✨ for clean, 💥 for dirty
199
200# Example 3: Command History Plugin
201# Keeps track of your most-used commands
202#
203# [[plugin]]
204# name = "command-stats"
205# path = "~/.config/scarab/plugins/command-stats.fzb"
206# enabled = true
207#
208# [plugin.config]
209# track_frequency = true
210# suggest_aliases = true
211
212# Example 4: Custom Welcome Message
213# Greet yourself with style every time
214#
215# [[plugin]]
216# name = "welcome"
217# path = "~/.config/scarab/plugins/welcome.fsx"
218# enabled = true
219#
220# [plugin.config]
221# message = "Ready to do amazing things? Let's go! 🚀"
222# show_time = true
223# show_quote_of_the_day = true
224
225# ✨ Your plugins go here! ✨
226# Just uncomment the examples above or add your own.
227# Happy customizing!
228
229"#;
230
231        fs::write(&config_path, example_config)?;
232        Ok(config_path)
233    }
234}
235
236impl Default for PluginDiscovery {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242/// Expand ~ in path to home directory
243fn expand_path(path: &Path) -> PathBuf {
244    if let Some(s) = path.to_str() {
245        if let Some(stripped) = s.strip_prefix("~/") {
246            if let Some(home) = std::env::var_os("HOME") {
247                return PathBuf::from(home).join(stripped);
248            }
249        }
250    }
251    path.to_path_buf()
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_expand_path() {
260        let path = PathBuf::from("~/test/path");
261        let expanded = expand_path(&path);
262        assert!(!expanded.to_string_lossy().contains('~'));
263    }
264
265    #[test]
266    fn test_is_plugin_file() {
267        // is_plugin_file checks if path.is_file() first, so we need actual files
268        // For unit testing, we just test the extension logic
269        use std::path::Path;
270
271        let has_valid_ext = |path: &Path| -> bool {
272            matches!(
273                path.extension().and_then(|e| e.to_str()),
274                Some("fzb") | Some("fsx")
275            )
276        };
277
278        assert!(has_valid_ext(Path::new("test.fzb")));
279        assert!(has_valid_ext(Path::new("test.fsx")));
280        assert!(!has_valid_ext(Path::new("test.txt")));
281    }
282}