use ahash::AHashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
pub const PLUGIN_BUDGET_MS: u64 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PluginState {
#[default]
Registered,
Loading,
Loaded,
Failed,
Disabled,
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub name: String,
pub description: String,
pub version: String,
pub dependencies: Vec<String>,
pub lazy_loadable: bool,
}
impl PluginInfo {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: String::new(),
version: "1.0.0".to_string(),
dependencies: Vec::new(),
lazy_loadable: true,
}
}
#[must_use]
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = version.into();
self
}
#[must_use]
pub fn with_dependency(mut self, dep: impl Into<String>) -> Self {
self.dependencies.push(dep.into());
self
}
}
pub trait Plugin: Send + Sync {
fn info(&self) -> PluginInfo;
fn init(&mut self) -> Result<(), PluginError>;
fn shell_init(&self, shell: crate::ShellType) -> String;
fn aliases(&self) -> AHashMap<String, String> {
AHashMap::new()
}
fn env_vars(&self) -> AHashMap<String, String> {
AHashMap::new()
}
fn completions(&self) -> Vec<String> {
Vec::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum PluginError {
#[error("plugin not found: {0}")]
NotFound(String),
#[error("plugin load failed: {0}")]
LoadFailed(String),
#[error("plugin budget exceeded: {0}ms")]
BudgetExceeded(u64),
#[error("dependency not met: {0}")]
DependencyNotMet(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone)]
pub struct GitPlugin {
enabled: bool,
}
impl GitPlugin {
#[must_use]
pub const fn new() -> Self {
Self { enabled: false }
}
}
impl Default for GitPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for GitPlugin {
fn info(&self) -> PluginInfo {
PluginInfo::new("git")
.with_description("Git aliases and integration")
.with_version("1.0.0")
}
fn init(&mut self) -> Result<(), PluginError> {
self.enabled = true;
Ok(())
}
fn shell_init(&self, shell: crate::ShellType) -> String {
if !self.enabled {
return String::new();
}
match shell {
crate::ShellType::Zsh => {
r"
# pzsh git plugin
autoload -Uz vcs_info
precmd_functions+=( vcs_info )
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%b'
"
.to_string()
}
crate::ShellType::Bash => {
r"
# pzsh git plugin
__pzsh_git_branch() {
git branch 2>/dev/null | grep '^\*' | sed 's/^\* //'
}
"
.to_string()
}
}
}
fn aliases(&self) -> AHashMap<String, String> {
let mut aliases = AHashMap::new();
aliases.insert("g".to_string(), "git".to_string());
aliases.insert("ga".to_string(), "git add".to_string());
aliases.insert("gaa".to_string(), "git add --all".to_string());
aliases.insert("gb".to_string(), "git branch".to_string());
aliases.insert("gc".to_string(), "git commit".to_string());
aliases.insert("gcm".to_string(), "git commit -m".to_string());
aliases.insert("gco".to_string(), "git checkout".to_string());
aliases.insert("gd".to_string(), "git diff".to_string());
aliases.insert("gf".to_string(), "git fetch".to_string());
aliases.insert("gl".to_string(), "git pull".to_string());
aliases.insert("gp".to_string(), "git push".to_string());
aliases.insert("gs".to_string(), "git status".to_string());
aliases.insert("gst".to_string(), "git stash".to_string());
aliases.insert("glog".to_string(), "git log --oneline --graph".to_string());
aliases
}
}
#[derive(Debug, Clone)]
pub struct DockerPlugin {
enabled: bool,
}
impl DockerPlugin {
#[must_use]
pub const fn new() -> Self {
Self { enabled: false }
}
}
impl Default for DockerPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for DockerPlugin {
fn info(&self) -> PluginInfo {
PluginInfo::new("docker")
.with_description("Docker aliases and completions")
.with_version("1.0.0")
}
fn init(&mut self) -> Result<(), PluginError> {
self.enabled = true;
Ok(())
}
fn shell_init(&self, _shell: crate::ShellType) -> String {
String::new() }
fn aliases(&self) -> AHashMap<String, String> {
let mut aliases = AHashMap::new();
aliases.insert("d".to_string(), "docker".to_string());
aliases.insert("dc".to_string(), "docker compose".to_string());
aliases.insert("dcu".to_string(), "docker compose up".to_string());
aliases.insert("dcd".to_string(), "docker compose down".to_string());
aliases.insert("dps".to_string(), "docker ps".to_string());
aliases.insert("di".to_string(), "docker images".to_string());
aliases.insert("drm".to_string(), "docker rm".to_string());
aliases.insert("drmi".to_string(), "docker rmi".to_string());
aliases.insert("dex".to_string(), "docker exec -it".to_string());
aliases
}
}
pub struct PluginManager {
plugins: AHashMap<String, Box<dyn Plugin>>,
states: AHashMap<String, PluginState>,
load_order: Vec<String>,
plugin_dir: Option<PathBuf>,
}
impl std::fmt::Debug for PluginManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PluginManager")
.field("plugins", &self.plugins.keys().collect::<Vec<_>>())
.field("states", &self.states)
.field("load_order", &self.load_order)
.field("plugin_dir", &self.plugin_dir)
.finish()
}
}
impl PluginManager {
#[must_use]
pub fn new() -> Self {
let mut manager = Self {
plugins: AHashMap::new(),
states: AHashMap::new(),
load_order: Vec::new(),
plugin_dir: None,
};
manager.register(GitPlugin::new());
manager.register(DockerPlugin::new());
manager
}
pub fn set_plugin_dir(&mut self, dir: PathBuf) {
self.plugin_dir = Some(dir);
}
pub fn register(&mut self, plugin: impl Plugin + 'static) {
let info = plugin.info();
let name = info.name.clone();
self.plugins.insert(name.clone(), Box::new(plugin));
self.states.insert(name, PluginState::Registered);
}
pub fn load(&mut self, name: &str) -> Result<Duration, PluginError> {
let start = Instant::now();
if self.states.get(name) == Some(&PluginState::Loaded) {
return Ok(Duration::ZERO);
}
let plugin = self
.plugins
.get_mut(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
let deps = plugin.info().dependencies.clone();
for dep in &deps {
if !matches!(self.states.get(dep), Some(PluginState::Loaded)) {
return Err(PluginError::DependencyNotMet(dep.clone()));
}
}
self.states.insert(name.to_string(), PluginState::Loading);
plugin.init().inspect_err(|_| {
self.states.insert(name.to_string(), PluginState::Failed);
})?;
let elapsed = start.elapsed();
if elapsed > Duration::from_millis(PLUGIN_BUDGET_MS) {
self.states.insert(name.to_string(), PluginState::Failed);
return Err(PluginError::BudgetExceeded(elapsed.as_millis() as u64));
}
self.states.insert(name.to_string(), PluginState::Loaded);
self.load_order.push(name.to_string());
Ok(elapsed)
}
pub fn load_all(&mut self, names: &[String]) -> Vec<Result<Duration, PluginError>> {
names.iter().map(|name| self.load(name)).collect()
}
#[must_use]
pub fn all_aliases(&self) -> AHashMap<String, String> {
let mut aliases = AHashMap::new();
for name in &self.load_order {
if let Some(plugin) = self.plugins.get(name)
&& matches!(self.states.get(name), Some(PluginState::Loaded))
{
aliases.extend(plugin.aliases());
}
}
aliases
}
#[must_use]
pub fn all_env_vars(&self) -> AHashMap<String, String> {
let mut vars = AHashMap::new();
for name in &self.load_order {
if let Some(plugin) = self.plugins.get(name)
&& matches!(self.states.get(name), Some(PluginState::Loaded))
{
vars.extend(plugin.env_vars());
}
}
vars
}
#[must_use]
pub fn shell_init(&self, shell: crate::ShellType) -> String {
let mut init = String::new();
for name in &self.load_order {
if let Some(plugin) = self.plugins.get(name)
&& matches!(self.states.get(name), Some(PluginState::Loaded))
{
init.push_str(&plugin.shell_init(shell));
}
}
init
}
#[must_use]
pub fn state(&self, name: &str) -> Option<PluginState> {
self.states.get(name).copied()
}
#[must_use]
pub fn list(&self) -> Vec<(&str, PluginState)> {
self.plugins
.keys()
.map(|name| {
let state = self
.states
.get(name)
.copied()
.unwrap_or(PluginState::Registered);
(name.as_str(), state)
})
.collect()
}
#[must_use]
pub fn loaded_count(&self) -> usize {
self.states
.values()
.filter(|&&s| s == PluginState::Loaded)
.count()
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_plugin() {
let mut plugin = GitPlugin::new();
assert!(plugin.init().is_ok());
let info = plugin.info();
assert_eq!(info.name, "git");
let aliases = plugin.aliases();
assert!(aliases.contains_key("gs"));
assert_eq!(aliases.get("gs"), Some(&"git status".to_string()));
}
#[test]
fn test_docker_plugin() {
let mut plugin = DockerPlugin::new();
assert!(plugin.init().is_ok());
let info = plugin.info();
assert_eq!(info.name, "docker");
let aliases = plugin.aliases();
assert!(aliases.contains_key("dps"));
}
#[test]
fn test_plugin_manager() {
let manager = PluginManager::new();
let plugins = manager.list();
assert!(plugins.iter().any(|(name, _)| *name == "git"));
assert!(plugins.iter().any(|(name, _)| *name == "docker"));
}
#[test]
fn test_plugin_load() {
let mut manager = PluginManager::new();
let result = manager.load("git");
assert!(result.is_ok());
assert_eq!(manager.state("git"), Some(PluginState::Loaded));
}
#[test]
fn test_plugin_not_found() {
let mut manager = PluginManager::new();
let result = manager.load("nonexistent");
assert!(matches!(result, Err(PluginError::NotFound(_))));
}
#[test]
fn test_plugin_aliases_aggregation() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
manager.load("docker").unwrap();
let aliases = manager.all_aliases();
assert!(aliases.contains_key("gs")); assert!(aliases.contains_key("dps")); }
#[test]
fn test_plugin_info_builder() {
let info = PluginInfo::new("test")
.with_description("A test plugin")
.with_version("2.0.0")
.with_dependency("git");
assert_eq!(info.name, "test");
assert_eq!(info.description, "A test plugin");
assert_eq!(info.version, "2.0.0");
assert_eq!(info.dependencies, vec!["git"]);
}
#[test]
fn test_plugin_load_performance() {
let mut manager = PluginManager::new();
let start = Instant::now();
manager.load("git").unwrap();
manager.load("docker").unwrap();
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(10),
"Plugin loading too slow: {:?}",
elapsed
);
}
#[test]
fn test_shell_init_generation() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
let init = manager.shell_init(crate::ShellType::Zsh);
assert!(init.contains("vcs_info")); }
#[test]
fn test_loaded_count() {
let mut manager = PluginManager::new();
assert_eq!(manager.loaded_count(), 0);
manager.load("git").unwrap();
assert_eq!(manager.loaded_count(), 1);
manager.load("docker").unwrap();
assert_eq!(manager.loaded_count(), 2);
}
#[test]
fn test_git_plugin_new() {
let plugin = GitPlugin::new();
assert!(!plugin.enabled);
}
#[test]
fn test_git_plugin_default() {
let plugin = GitPlugin::default();
assert!(!plugin.enabled);
}
#[test]
fn test_git_plugin_info() {
let plugin = GitPlugin::new();
let info = plugin.info();
assert_eq!(info.name, "git");
assert!(info.lazy_loadable);
}
#[test]
fn test_git_plugin_init() {
let mut plugin = GitPlugin::new();
let result = plugin.init();
assert!(result.is_ok());
}
#[test]
fn test_git_plugin_shell_init_zsh() {
let mut plugin = GitPlugin::new();
plugin.init().unwrap(); let init = plugin.shell_init(crate::ShellType::Zsh);
assert!(init.contains("vcs_info"));
}
#[test]
fn test_git_plugin_shell_init_bash() {
let plugin = GitPlugin::new();
let init = plugin.shell_init(crate::ShellType::Bash);
assert!(init.contains("__git_ps1") || init.is_empty());
}
#[test]
fn test_git_plugin_aliases() {
let plugin = GitPlugin::new();
let aliases = plugin.aliases();
assert!(aliases.contains_key("g"));
assert!(aliases.contains_key("gs"));
assert!(aliases.contains_key("ga"));
assert!(aliases.contains_key("gc"));
assert!(aliases.contains_key("gp"));
}
#[test]
fn test_docker_plugin_new() {
let plugin = DockerPlugin::new();
assert!(!plugin.enabled);
}
#[test]
fn test_docker_plugin_default() {
let plugin = DockerPlugin::default();
assert!(!plugin.enabled);
}
#[test]
fn test_docker_plugin_info() {
let plugin = DockerPlugin::new();
let info = plugin.info();
assert_eq!(info.name, "docker");
assert!(info.lazy_loadable);
}
#[test]
fn test_docker_plugin_init() {
let mut plugin = DockerPlugin::new();
let result = plugin.init();
assert!(result.is_ok());
}
#[test]
fn test_docker_plugin_shell_init() {
let plugin = DockerPlugin::new();
let init = plugin.shell_init(crate::ShellType::Zsh);
assert!(init.is_empty() || !init.is_empty());
}
#[test]
fn test_docker_plugin_aliases() {
let plugin = DockerPlugin::new();
let aliases = plugin.aliases();
assert!(aliases.contains_key("d"));
assert!(aliases.contains_key("dps"));
assert!(aliases.contains_key("di"));
}
#[test]
fn test_plugin_state_debug() {
let states = [
PluginState::Registered,
PluginState::Loading,
PluginState::Loaded,
PluginState::Failed,
PluginState::Disabled,
];
for state in states {
let debug = format!("{:?}", state);
assert!(!debug.is_empty());
}
}
#[test]
fn test_plugin_state_equality() {
assert_eq!(PluginState::Loaded, PluginState::Loaded);
assert_ne!(PluginState::Loaded, PluginState::Failed);
}
#[test]
fn test_plugin_info_default() {
let info = PluginInfo::new("test");
assert_eq!(info.version, "1.0.0");
assert!(info.dependencies.is_empty());
assert!(info.lazy_loadable);
}
#[test]
fn test_plugin_error_display() {
let err1 = PluginError::NotFound("test".to_string());
assert!(err1.to_string().contains("not found"));
let err2 = PluginError::LoadFailed("failed".to_string());
assert!(err2.to_string().contains("load failed"));
let err3 = PluginError::BudgetExceeded(100);
assert!(err3.to_string().contains("budget"));
let err4 = PluginError::DependencyNotMet("dep".to_string());
assert!(err4.to_string().contains("dependency"));
}
#[test]
fn test_plugin_manager_debug() {
let manager = PluginManager::new();
let debug = format!("{:?}", manager);
assert!(debug.contains("PluginManager"));
}
#[test]
fn test_plugin_manager_state_unregistered() {
let manager = PluginManager::new();
assert!(manager.state("nonexistent").is_none());
}
#[test]
fn test_plugin_manager_reload() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
let result = manager.load("git");
assert!(result.is_ok());
}
#[test]
fn test_git_plugin_env_vars() {
let plugin = GitPlugin::new();
let env = plugin.env_vars();
assert!(env.is_empty());
}
#[test]
fn test_git_plugin_completions() {
let plugin = GitPlugin::new();
let completions = plugin.completions();
assert!(completions.is_empty());
}
#[test]
fn test_docker_plugin_env_vars() {
let plugin = DockerPlugin::new();
let env = plugin.env_vars();
assert!(env.is_empty());
}
#[test]
fn test_docker_plugin_completions() {
let plugin = DockerPlugin::new();
let completions = plugin.completions();
assert!(completions.is_empty());
}
#[test]
fn test_plugin_manager_load_all() {
let mut manager = PluginManager::new();
let names = vec!["git".to_string(), "docker".to_string()];
let results = manager.load_all(&names);
assert_eq!(results.len(), 2);
assert!(results[0].is_ok());
assert!(results[1].is_ok());
}
#[test]
fn test_plugin_manager_all_aliases() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
manager.load("docker").unwrap();
let aliases = manager.all_aliases();
assert!(aliases.contains_key("gs")); assert!(aliases.contains_key("d")); }
#[test]
fn test_plugin_manager_all_env_vars() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
let env = manager.all_env_vars();
assert!(env.is_empty());
}
#[test]
fn test_plugin_manager_shell_init() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
let init = manager.shell_init(crate::ShellType::Zsh);
assert!(init.contains("vcs_info"));
}
#[test]
fn test_plugin_manager_shell_init_bash() {
let mut manager = PluginManager::new();
manager.load("git").unwrap();
let init = manager.shell_init(crate::ShellType::Bash);
assert!(init.contains("__pzsh_git_branch"));
}
#[test]
fn test_plugin_manager_list() {
let manager = PluginManager::new();
let list = manager.list();
assert!(list.len() >= 2); assert!(list.iter().any(|(name, _)| *name == "git"));
assert!(list.iter().any(|(name, _)| *name == "docker"));
}
#[test]
fn test_plugin_manager_loaded_count() {
let mut manager = PluginManager::new();
assert_eq!(manager.loaded_count(), 0);
manager.load("git").unwrap();
assert_eq!(manager.loaded_count(), 1);
manager.load("docker").unwrap();
assert_eq!(manager.loaded_count(), 2);
}
#[test]
fn test_plugin_manager_set_plugin_dir() {
let mut manager = PluginManager::new();
manager.set_plugin_dir(PathBuf::from("/tmp/plugins"));
let debug = format!("{:?}", manager);
assert!(debug.contains("/tmp/plugins"));
}
#[test]
fn test_plugin_manager_load_not_found() {
let mut manager = PluginManager::new();
let result = manager.load("nonexistent");
assert!(matches!(result, Err(PluginError::NotFound(_))));
}
#[test]
fn test_plugin_info_with_dependency() {
let info = PluginInfo::new("test").with_dependency("other");
assert_eq!(info.dependencies.len(), 1);
assert_eq!(info.dependencies[0], "other");
}
#[test]
fn test_plugin_info_with_description() {
let info = PluginInfo::new("test").with_description("A test plugin");
assert_eq!(info.description, "A test plugin");
}
#[test]
fn test_plugin_info_with_version() {
let info = PluginInfo::new("test").with_version("2.0.0");
assert_eq!(info.version, "2.0.0");
}
#[test]
fn test_git_plugin_debug() {
let plugin = GitPlugin::new();
let debug = format!("{:?}", plugin);
assert!(debug.contains("GitPlugin"));
}
#[test]
fn test_docker_plugin_debug() {
let plugin = DockerPlugin::new();
let debug = format!("{:?}", plugin);
assert!(debug.contains("DockerPlugin"));
}
#[test]
fn test_git_plugin_clone() {
let plugin = GitPlugin::new();
let cloned = plugin.clone();
assert_eq!(cloned.enabled, plugin.enabled);
}
#[test]
fn test_docker_plugin_clone() {
let plugin = DockerPlugin::new();
let cloned = plugin.clone();
assert_eq!(cloned.enabled, plugin.enabled);
}
#[test]
fn test_plugin_state_default() {
let state = PluginState::default();
assert_eq!(state, PluginState::Registered);
}
#[test]
fn test_plugin_manager_empty_load_all() {
let mut manager = PluginManager::new();
let results = manager.load_all(&[]);
assert!(results.is_empty());
}
#[test]
fn test_plugin_manager_register_custom() {
#[derive(Clone, Debug)]
struct TestPlugin;
impl Plugin for TestPlugin {
fn info(&self) -> PluginInfo {
PluginInfo::new("test")
}
fn init(&mut self) -> Result<(), PluginError> {
Ok(())
}
fn shell_init(&self, _: crate::ShellType) -> String {
"# test".to_string()
}
fn aliases(&self) -> AHashMap<String, String> {
AHashMap::new()
}
}
let mut manager = PluginManager::new();
manager.register(TestPlugin);
assert!(manager.state("test").is_some());
}
#[test]
fn test_plugin_manager_state_transitions() {
let mut manager = PluginManager::new();
assert_eq!(manager.state("git"), Some(PluginState::Registered));
manager.load("git").unwrap();
assert_eq!(manager.state("git"), Some(PluginState::Loaded));
}
#[test]
fn test_git_plugin_all_aliases() {
let plugin = GitPlugin::new();
let aliases = plugin.aliases();
assert!(aliases.contains_key("g"));
assert!(aliases.contains_key("ga"));
assert!(aliases.contains_key("gaa"));
assert!(aliases.contains_key("gb"));
assert!(aliases.contains_key("gc"));
assert!(aliases.contains_key("gcm"));
assert!(aliases.contains_key("gco"));
assert!(aliases.contains_key("gd"));
assert!(aliases.contains_key("gf"));
assert!(aliases.contains_key("gl"));
assert!(aliases.contains_key("gp"));
assert!(aliases.contains_key("gs"));
assert!(aliases.contains_key("gst"));
}
#[test]
fn test_docker_plugin_all_aliases() {
let plugin = DockerPlugin::new();
let aliases = plugin.aliases();
assert!(aliases.contains_key("d"));
assert!(aliases.contains_key("dc"));
assert!(aliases.contains_key("dcu"));
assert!(aliases.contains_key("dcd"));
assert!(aliases.contains_key("dps"));
assert!(aliases.contains_key("di"));
assert!(aliases.contains_key("drm"));
assert!(aliases.contains_key("drmi"));
assert!(aliases.contains_key("dex"));
}
}