scarab_plugin_api/
config.rs1use crate::{context::PluginConfigData, error::Result};
4use serde::{Deserialize, Serialize};
5use std::{
6 fs,
7 path::{Path, PathBuf},
8};
9
10#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct PluginConfig {
13 pub name: String,
15 pub path: PathBuf,
17 #[serde(default = "default_true")]
19 pub enabled: bool,
20 #[serde(default)]
22 pub config: PluginConfigData,
23}
24
25fn default_true() -> bool {
26 true
27}
28
29impl PluginConfig {
30 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 pub fn expanded_path(&self) -> PathBuf {
39 expand_path(&self.path)
40 }
41}
42
43#[derive(Debug, Deserialize, Serialize)]
45struct PluginsToml {
46 plugin: Vec<PluginConfig>,
47}
48
49pub struct PluginDiscovery {
51 search_paths: Vec<PathBuf>,
53}
54
55impl PluginDiscovery {
56 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 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 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 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 pub fn add_path(&mut self, path: impl Into<PathBuf>) {
92 self.search_paths.push(path.into());
93 }
94
95 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 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 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 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 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 if let Some(parent) = config_path.parent() {
157 fs::create_dir_all(parent)?;
158 }
159
160 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
242fn 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 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}